三源合议工作包三(一):技术面转空自动离场(part 1+2)
动作引擎加转空离场评估器 eval_tech_exit:相位转空且翻空≤2 交易日才评, 确认转空清仓全部可卖量、未确认减三分之一;默认自动执行(与保垫减仓同档), 档位 PMS_TECH_EXIT_AUTONOMY 三档 off/propose_only/full;同一翻空只处理一次 (PMS_TECH_EXIT_DONE 去重,SAR 翻多即清,不动表结构)。 - EVALUATORS 加一条改七条(TRIM、TRIM、EXIT、EXIT、ADD、FILL、DCA) - 卖出优先级改来源分档:目标价<研究走弱<技术面转空<保垫减仓(既有相对次序不变) - 策略票判据改按函数判:策略票只评到价清仓、不评转空离场(SAR 归策略层) 台账 010;第三十批 18 例;开发机全量 ALL SUITES PASS。 策略层两部分(盘中 SAR 止损线、弱基本面试探仓紧止盈)留下一增量。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cb6b308620
commit
8fdcc2ac16
30
DEVLOG.md
30
DEVLOG.md
|
|
@ -1403,6 +1403,36 @@ app/core/action_engine.py(合议分流五处:开关、跳过观察分流、a
|
||||||
|
|
||||||
一,工作包三离场纪律未做:eval_tech_exit 转空离场、盘中 SAR 止损线、弱基本面试探仓的紧止盈自动挂载。二,工作包四选股打分在另一仓库,单独审批后再动。三,工作包五其余文档:README 调度总表补到十三位、BIONIC_PMS_INTERFACE 技术面只读接口一节。四,页面管理视图持仓总览「证据列印两遍」的显示错误与研究面列,本轮未碰。五,提议卡合议行与增持门实际拦截,等有新提议、有持仓触发增持时在 155 补看。
|
一,工作包三离场纪律未做:eval_tech_exit 转空离场、盘中 SAR 止损线、弱基本面试探仓的紧止盈自动挂载。二,工作包四选股打分在另一仓库,单独审批后再动。三,工作包五其余文档:README 调度总表补到十三位、BIONIC_PMS_INTERFACE 技术面只读接口一节。四,页面管理视图持仓总览「证据列印两遍」的显示错误与研究面列,本轮未碰。五,提议卡合议行与增持门实际拦截,等有新提议、有持仓触发增持时在 155 补看。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-09-11 · 工作包三离场纪律(part 1+2):技术面转空自动离场
|
||||||
|
|
||||||
|
**做了什么**
|
||||||
|
|
||||||
|
把技术面从「只管入场投票」推进到「也管离场纪律」。这次做工作包三的前两部分,即动作引擎里的转空离场评估器,策略层的两部分(盘中 SAR 止损线、弱基本面试探仓紧止盈自动挂载)留到下一增量。
|
||||||
|
|
||||||
|
一,新评估器 eval_tech_exit。持仓票的技术面相位是「转空」且 SAR 翻空不超过两个交易日时评。确认转空清仓全部可卖量,未确认减三分之一。入场靠投票、离场靠纪律:转空像保垫减仓一样按规则自动执行,不进合议投票。档位 PMS_TECH_EXIT_AUTONOMY 三档,off 不评、propose_only 交人、full 自动执行,默认 full。同一次翻空只处理一次,用运行参数 PMS_TECH_EXIT_DONE 承载去重,SAR 翻多即清除,不动表结构。
|
||||||
|
|
||||||
|
二,卖出优先级改成来源分档。同一轮多条减持留一条,次序是目标价到价、研究证据走弱、技术面转空、保垫减仓。既有三类的相对先后一字不动,转空插在研究走弱与保垫减仓之间。
|
||||||
|
|
||||||
|
三,策略票判据改按函数判。到价清仓与转空离场都是 EXIT 动作,但策略票的 SAR 该由策略层的盘中止损线管,所以策略票只评到价清仓、不评转空离场。原先按动作名判会把转空离场对策略票误放行。
|
||||||
|
|
||||||
|
**动了哪些文件**
|
||||||
|
|
||||||
|
app/core/action_engine.py(eval_tech_exit、SRC_TECH_EXIT、EVALUATORS 加一条改成七条、_sell_priority 来源分档、策略票判据按函数判);app/services/proposal_service.py(扫描参数加离场档位、持仓挂技术面状态、去重集加载与写回两个辅助函数);app/services/param_store.py(登记 PMS_TECH_EXIT_AUTONOMY、PMS_TECH_EXIT_TRIM_RATIO、PMS_TECH_EXIT_DONE,减持比例加校验范围);scripts/test_batch30_units.py(新增十八例);scripts/test_batch12_units.py(EVALUATORS 次序哨兵改成七条);scripts/run_tests.py(登记第三十批、总数 805);docs/复盘决定台账.md(台账 010)。
|
||||||
|
|
||||||
|
**部署方式**
|
||||||
|
|
||||||
|
改了 Python,make deploy 重建镜像加 force-recreate。收盘后 155,本次与工作包二接入同批已授权直接部署。
|
||||||
|
|
||||||
|
**真机判收**
|
||||||
|
|
||||||
|
待部署后补。开发机全量 ALL SUITES PASS(第三十批十八例,含开关关掉逐字回旧)。这一增量对策略票不回退:接入前策略票也没有 SAR 离场,本次只是给常规持仓加了转空自动离场,策略票的 SAR 线是下一部分。
|
||||||
|
|
||||||
|
**还欠着什么**
|
||||||
|
|
||||||
|
工作包三剩两部分:跟踪止盈策略加盘中 SAR 止损线(09:45 后现价低于昨日 SAR 千分之三即卖、当日一次,每天 09:40 把映射里的 SAR 刷进自动挂载的跟踪止盈);弱基本面加技术面看多的试探仓次日 09:40 自动挂回撤 3%、硬目标 8%、带 SAR 线的紧止盈(台账 011)。工作包四选股打分另一仓库单独审批。工作包五文档。
|
||||||
|
|
||||||
---
|
---
|
||||||
<!--
|
<!--
|
||||||
下一条节点从这里往下写,格式照抄上面:
|
下一条节点从这里往下写,格式照抄上面:
|
||||||
|
|
|
||||||
|
|
@ -59,13 +59,20 @@ _SELL_PRIORITY = {A_EXIT: 0, A_TRIM: 1}
|
||||||
|
|
||||||
|
|
||||||
def _sell_priority(c: dict) -> int:
|
def _sell_priority(c: dict) -> int:
|
||||||
"""同一轮多条减持留哪一条 (数字小的优先; 2026-09-07 第三件起按动作加来源):
|
"""同一轮多条减持留哪一条 (数字小的优先; 2026-09-11 起按来源分档):
|
||||||
到价清仓 (人的意思) → 研究证据走弱的减持 (必交人) → 保垫减仓 (规则自动)。"""
|
到价清仓 (人的意思) → 研究证据走弱 (必交人) → 技术面转空离场 → 保垫减仓 (规则自动)。
|
||||||
if c.get("action") == A_EXIT:
|
只影响相对次序: 既有三类的先后与从前一字不差, 技术面转空插在研究走弱与保垫减仓之间 ——
|
||||||
|
转空是决策系统的趋势止损信号, 比常规的保垫锁盈更该先执行, 但仍让位于人的目标价与研究走弱。"""
|
||||||
|
src = c.get("source")
|
||||||
|
if src == SRC_TARGET_PRICE:
|
||||||
return 0
|
return 0
|
||||||
if c.get("source") == SRC_RESEARCH_WEAK:
|
if src == SRC_RESEARCH_WEAK:
|
||||||
return 1
|
return 1
|
||||||
return 2
|
if src == SRC_TECH_EXIT:
|
||||||
|
return 2
|
||||||
|
if c.get("action") == A_EXIT: # 兜底: 其它 EXIT 当人的意思 (历史行为)
|
||||||
|
return 0
|
||||||
|
return 3
|
||||||
|
|
||||||
# 候选来源 (2026-09-03)。每条候选都带一个来源, 提议分流那一步拿它决定「这条减持能不能不等人」。
|
# 候选来源 (2026-09-03)。每条候选都带一个来源, 提议分流那一步拿它决定「这条减持能不能不等人」。
|
||||||
#
|
#
|
||||||
|
|
@ -80,6 +87,10 @@ def _sell_priority(c: dict) -> int:
|
||||||
SRC_ENGINE = "engine" # 动作引擎按规则算出来的 (FILL / ADD / DCA / TRIM / OPEN)
|
SRC_ENGINE = "engine" # 动作引擎按规则算出来的 (FILL / ADD / DCA / TRIM / OPEN)
|
||||||
SRC_RESEARCH_WEAK = "research_weak" # 研究证据走弱触发的减持 —— 必定交人裁决, 绝不自动卖出
|
SRC_RESEARCH_WEAK = "research_weak" # 研究证据走弱触发的减持 —— 必定交人裁决, 绝不自动卖出
|
||||||
SRC_TARGET_PRICE = "target_price" # 用户设的目标价到价 —— 必定交人裁决, 绝不自动卖出
|
SRC_TARGET_PRICE = "target_price" # 用户设的目标价到价 —— 必定交人裁决, 绝不自动卖出
|
||||||
|
# 技术面转空自动离场 (2026-09-11 工作包三)。**不进 FORCE_QUEUE_SOURCES** —— 按用户拍板默认
|
||||||
|
# 自动执行, 与保垫减仓同档; 档位由 PMS_TECH_EXIT_AUTONOMY 控制 (propose_only 时评估器自己打
|
||||||
|
# needs_user_confirm 交人, off 时根本不评估)。
|
||||||
|
SRC_TECH_EXIT = "tech_exit"
|
||||||
|
|
||||||
WHY_RESEARCH_WEAK_CONFIRM = "研究证据走弱触发的减持,必须交人裁决,不自动卖出"
|
WHY_RESEARCH_WEAK_CONFIRM = "研究证据走弱触发的减持,必须交人裁决,不自动卖出"
|
||||||
WHY_TARGET_PRICE_CONFIRM = "你设的目标价到价,卖不卖由你决定,系统不自动卖出"
|
WHY_TARGET_PRICE_CONFIRM = "你设的目标价到价,卖不卖由你决定,系统不自动卖出"
|
||||||
|
|
@ -485,9 +496,66 @@ def clamp_sell_qty(qty: int, pos: dict) -> int:
|
||||||
return max(0, min(q, int(avail or 0)))
|
return max(0, min(q, int(avail or 0)))
|
||||||
|
|
||||||
|
|
||||||
|
def eval_tech_exit(p: dict, params: dict, mkt: dict = None):
|
||||||
|
"""技术面转空自动离场 (2026-09-11 工作包三, 台账 010)。入场靠投票, 离场靠纪律 ——
|
||||||
|
SAR 翻空像保垫减仓一样按规则执行, 不进入场那套合议投票。
|
||||||
|
|
||||||
|
只在技术面相位是「转空」且翻向不超过 flip_fresh_days 天时评 (相位本身已含这个窗口,
|
||||||
|
这里再显式判一次)。确认转空 (多空布林线空头区或价格贴下轨) 清仓全部可卖量; 未确认减三分之一。
|
||||||
|
默认自动执行 (来源 SRC_TECH_EXIT 不在强制入队名单; 与保垫减仓同档); 档位 propose_only 时
|
||||||
|
评估器自己打交人标记, off 时调用方根本不评估 (tech_exit_on 为假)。
|
||||||
|
|
||||||
|
同一次翻向只处理一次: params["tech_exit_done"] 是「本次翻空已处理过」的代码集, 服务层
|
||||||
|
在真执行后写入、SAR 转多后清除 (不动表结构, 与 tech_state_map 同一手法承载跨轮状态)。
|
||||||
|
技术面状态挂在 p["tech"] 上 (tech_service.attach 早上写)。没有读数不评 —— 无读数弃权。
|
||||||
|
"""
|
||||||
|
if not params.get("tech_exit_on"):
|
||||||
|
return None
|
||||||
|
t = p.get("tech")
|
||||||
|
if not isinstance(t, dict) or t.get("phase") != "转空":
|
||||||
|
return None
|
||||||
|
fd = t.get("sar_flip_days")
|
||||||
|
if fd is None or int(fd) > int(params.get("tech_exit_fresh_days", 2)):
|
||||||
|
return None
|
||||||
|
code = p.get("ts_code")
|
||||||
|
if code in (params.get("tech_exit_done") or {}):
|
||||||
|
return None # 同一次翻空已处理过
|
||||||
|
total = int(p.get("total_qty") or 0)
|
||||||
|
if total <= 0:
|
||||||
|
return None
|
||||||
|
confirm = bool(t.get("confirm"))
|
||||||
|
if confirm:
|
||||||
|
qty = clamp_sell_qty(total, p) # 确认转空: 清仓全部可卖量
|
||||||
|
action, ratio_txt = A_EXIT, "全部可卖量"
|
||||||
|
else:
|
||||||
|
ratio = _f(params.get("tech_exit_trim_ratio"), 1.0 / 3)
|
||||||
|
if ratio <= 0 or ratio > 1:
|
||||||
|
ratio = 1.0 / 3
|
||||||
|
qty = clamp_sell_qty(int(total * ratio // LOT) * LOT, p)
|
||||||
|
action, ratio_txt = A_TRIM, f"{ratio:.0%}"
|
||||||
|
if qty < lot_of(code):
|
||||||
|
return None
|
||||||
|
why = (f"技术面转空自动离场: SAR 翻空 {fd} 天"
|
||||||
|
f"{'(确认)' if confirm else '(未确认)'}, "
|
||||||
|
f"{'清仓' if confirm else '减'} {qty} 股({ratio_txt}); {t.get('reason') or ''}")
|
||||||
|
c = _cand(p, action, SELL, qty, why,
|
||||||
|
{"tech": t, "sar_value": t.get("sar_value"), "sar_flip_days": fd,
|
||||||
|
"confirm": confirm, "total_qty": total, "avail_qty": p.get("avail_qty"),
|
||||||
|
"price": p.get("price"), "cushion_pct": p.get("cushion_pct")},
|
||||||
|
source=SRC_TECH_EXIT)
|
||||||
|
# 档位 propose_only: 不自动执行, 打交人标记落进待确认队列 (与深档补仓走同一个字段)。
|
||||||
|
if params.get("tech_exit_propose_only"):
|
||||||
|
c["needs_user_confirm"] = True
|
||||||
|
c["confirm_why"] = "技术面转空离场(档位 propose_only,交你确认后再卖)"
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
# 研究走弱的减持排在保垫减仓前面 (2026-09-07 第三件): 两者动作名都是 TRIM, 同轮同现时按
|
# 研究走弱的减持排在保垫减仓前面 (2026-09-07 第三件): 两者动作名都是 TRIM, 同轮同现时按
|
||||||
# _sell_priority 留研究走弱那条 (它必交人), 保垫减仓让路 —— 证据在走弱的票, 不该由规则自动先卖一刀。
|
# _sell_priority 留研究走弱那条 (它必交人), 保垫减仓让路 —— 证据在走弱的票, 不该由规则自动先卖一刀。
|
||||||
|
# 2026-09-11 加技术面转空离场 eval_tech_exit: 次序改为 TRIM、TRIM、EXIT、EXIT、ADD、FILL、DCA
|
||||||
|
# (转空离场排在到价清仓之后、增持之前; 同轮多条减持按 _sell_priority 收敛成一条)。
|
||||||
EVALUATORS = ((A_TRIM, eval_weak_research), (A_TRIM, eval_trim), (A_EXIT, eval_target),
|
EVALUATORS = ((A_TRIM, eval_weak_research), (A_TRIM, eval_trim), (A_EXIT, eval_target),
|
||||||
|
(A_EXIT, eval_tech_exit),
|
||||||
(A_ADD, eval_add), (A_FILL, eval_fill), (A_DCA, eval_dca))
|
(A_ADD, eval_add), (A_FILL, eval_fill), (A_DCA, eval_dca))
|
||||||
|
|
||||||
# 减持方向的动作 —— 冻结只禁增持, 这几类照评 (与规则闸 _check_sell 同一口径)。
|
# 减持方向的动作 —— 冻结只禁增持, 这几类照评 (与规则闸 _check_sell 同一口径)。
|
||||||
|
|
@ -586,8 +654,11 @@ def scan(*, positions: list, params: dict, market: dict, skip=None,
|
||||||
if why:
|
if why:
|
||||||
skipped.append({"ts_code": code, "action": action, "why": why})
|
skipped.append({"ts_code": code, "action": action, "why": why})
|
||||||
continue
|
continue
|
||||||
if on_strategy and action != A_EXIT:
|
if on_strategy and fn is not eval_target:
|
||||||
continue # 策略票只看目标价, 原因已在上面记过
|
continue # 策略票只看目标价 (用户命令), 其余都由策略层接管
|
||||||
|
# 注: 判「只看目标价」按函数判而不是按动作名 (2026-09-11 工作包三): 到价清仓与技术面
|
||||||
|
# 转空离场都是 EXIT, 但策略票的 SAR 由策略层的盘中 SAR 止损线管 (part 3), 动作引擎这条
|
||||||
|
# 转空离场不该对策略票再评一遍 —— 按动作名判会把它误放行。
|
||||||
if inflight_sell and action in SELL_SIDE_ACTIONS:
|
if inflight_sell and action in SELL_SIDE_ACTIONS:
|
||||||
skipped.append({"ts_code": code, "action": action,
|
skipped.append({"ts_code": code, "action": action,
|
||||||
"why": f"这只票已有一条减持 ({inflight_sell}) 在处理中, "
|
"why": f"这只票已有一条减持 ({inflight_sell}) 在处理中, "
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,10 @@ RUNTIME_EXTRA = {
|
||||||
"PMS_TECH_GATE_INCREASE": (True, bool, "持仓增持门: 合议看空停回踩补足/盈利加仓/补仓, 技术面看空停回踩补足与盈利加仓, 基本面看空停补仓 (减持不受影响; 关掉即回到不设门)"),
|
"PMS_TECH_GATE_INCREASE": (True, bool, "持仓增持门: 合议看空停回踩补足/盈利加仓/补仓, 技术面看空停回踩补足与盈利加仓, 基本面看空停补仓 (减持不受影响; 关掉即回到不设门)"),
|
||||||
"PMS_PLAN_RANK_BY_QUALITY": (True, bool, "候选先按基本面立场分桶 (质地看多的桶排前) 再按分数, 在主题限额与截断之前生效; 技术面不参与排序 (关掉即回到只按分数排)"),
|
"PMS_PLAN_RANK_BY_QUALITY": (True, bool, "候选先按基本面立场分桶 (质地看多的桶排前) 再按分数, 在主题限额与截断之前生效; 技术面不参与排序 (关掉即回到只按分数排)"),
|
||||||
"PMS_PLAN_OBSERVE_IF_FUND_BULL": (False, bool, "把观察档里质地看多的票也纳入候选池 (默认关, 守宁缺毋滥; 开了它们与主榜一同参与分桶排序)"),
|
"PMS_PLAN_OBSERVE_IF_FUND_BULL": (False, bool, "把观察档里质地看多的票也纳入候选池 (默认关, 守宁缺毋滥; 开了它们与主榜一同参与分桶排序)"),
|
||||||
|
# ── 2026-09-11 三源合议工作包三·离场纪律 (台账 010): SAR 转空自动离场。翻向新鲜天数复用 PMS_TECH_FLIP_FRESH_DAYS。
|
||||||
|
"PMS_TECH_EXIT_AUTONOMY": ("full", str, "技术面转空离场档位: off 不评 / propose_only 交人确认 / full 自动执行 (与保垫减仓同档, 按拍板默认)"),
|
||||||
|
"PMS_TECH_EXIT_TRIM_RATIO": (0.3333, float, "转空未确认时的减持比例 (占总持仓; 确认转空则清仓全部可卖量; 默认三分之一)"),
|
||||||
|
"PMS_TECH_EXIT_DONE": ("", str, "本次翻空已处理过的代码集 (scan_and_route 写入的 JSON: codes/at; 跨轮去重, 页面只读, 勿手改)"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# **读不到时必须按"已暂停"处理的键 (fail-closed)。**
|
# **读不到时必须按"已暂停"处理的键 (fail-closed)。**
|
||||||
|
|
@ -494,6 +498,7 @@ _RANGES = {
|
||||||
"PMS_TECH_FLIP_FRESH_DAYS": (1, 10),
|
"PMS_TECH_FLIP_FRESH_DAYS": (1, 10),
|
||||||
# 三源合议 (2026-09-11)
|
# 三源合议 (2026-09-11)
|
||||||
"PMS_FUND_STALE_DAYS": (0, 3650), "PMS_FUND_CONSENSUS_GOOD_MIN": (0, 10),
|
"PMS_FUND_STALE_DAYS": (0, 3650), "PMS_FUND_CONSENSUS_GOOD_MIN": (0, 10),
|
||||||
|
"PMS_TECH_EXIT_TRIM_RATIO": (0, 1),
|
||||||
# 宏观择时层
|
# 宏观择时层
|
||||||
"PMS_MACRO_HOT_TH": (0, 100), "PMS_MACRO_COLD_TH": (-100, 0),
|
"PMS_MACRO_HOT_TH": (0, 100), "PMS_MACRO_COLD_TH": (-100, 0),
|
||||||
"PMS_MACRO_EXIT_BAND": (0, 100), "PMS_MACRO_CONFIRM_DAYS": (1, 10),
|
"PMS_MACRO_EXIT_BAND": (0, 100), "PMS_MACRO_CONFIRM_DAYS": (1, 10),
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,20 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
|
||||||
consensus_service.decorate_positions(view["held"], company_reviews=_crs)
|
consensus_service.decorate_positions(view["held"], company_reviews=_crs)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.warning("[合议] 持仓增持门装配失败, 本轮按无合议 (不设门): %s", e)
|
logger.warning("[合议] 持仓增持门装配失败, 本轮按无合议 (不设门): %s", e)
|
||||||
|
# 技术面转空自动离场 (2026-09-11 工作包三): 给持仓行挂技术面状态供 eval_tech_exit 读,
|
||||||
|
# 并备好「本次翻空已处理」去重集 (清掉 SAR 已不在空头的, 那是新一轮翻向)。挂不上只记日志、
|
||||||
|
# 本轮不评转空离场 (无读数弃权)。离场档位 off 时 tech_exit_on 已为假, 评估器自然不产出。
|
||||||
|
if params.get("tech_exit_on"):
|
||||||
|
try:
|
||||||
|
from app.services import tech_service as _ts
|
||||||
|
_tech_states = _ts.state_map()
|
||||||
|
_ts.attach(view["held"], _tech_states)
|
||||||
|
_done = _load_tech_exit_done()
|
||||||
|
params["tech_exit_done"] = {c for c in _done
|
||||||
|
if (_tech_states.get(c) or {}).get("sar_side") == "空"}
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("[技术面离场] 挂技术面状态失败, 本轮不评转空离场: %s", e)
|
||||||
|
params["tech_exit_done"] = set()
|
||||||
scanned = ae.scan(positions=view["held"], params=params, market=mkt, skip=skip,
|
scanned = ae.scan(positions=view["held"], params=params, market=mkt, skip=skip,
|
||||||
strategy_codes=strategy_codes, stock_params=stock_params)
|
strategy_codes=strategy_codes, stock_params=stock_params)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -198,10 +212,48 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
|
||||||
logger.exception("提议分流失败 %s", c.get("ts_code"))
|
logger.exception("提议分流失败 %s", c.get("ts_code"))
|
||||||
out["errors"].append(f"{c.get('ts_code')} {c.get('action')}: "
|
out["errors"].append(f"{c.get('ts_code')} {c.get('action')}: "
|
||||||
f"{type(e).__name__}: {e}")
|
f"{type(e).__name__}: {e}")
|
||||||
|
# 技术面转空离场去重集写回 (2026-09-11 工作包三): 本轮真处理过 (执行或入队, 未被闸拒) 的
|
||||||
|
# 转空离场记进去重集, 下次同一翻空不再重复处理。试算不写。清掉 SAR 已翻多的那步在扫描前做过了。
|
||||||
|
if not dry_run:
|
||||||
|
try:
|
||||||
|
_rej = {(r.get("ts_code"), r.get("action")) for r in out["rejected"]}
|
||||||
|
_acted = {c["ts_code"] for c in scanned["candidates"]
|
||||||
|
if c.get("source") == ae.SRC_TECH_EXIT
|
||||||
|
and (c["ts_code"], c["action"]) not in _rej}
|
||||||
|
if _acted:
|
||||||
|
_save_tech_exit_done(set(params.get("tech_exit_done") or set()) | _acted)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("[技术面离场] 去重集写回失败 (下轮可能重评一次): %s", e)
|
||||||
out["ok"] = not out["errors"]
|
out["ok"] = not out["errors"]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_TECH_EXIT_DONE_KEY = "PMS_TECH_EXIT_DONE"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_tech_exit_done() -> set:
|
||||||
|
"""本次翻空已处理过的代码集 (跨轮去重, 不动表结构; 与 tech_state_map 同一手法用运行参数承载)。
|
||||||
|
读不到或坏了返回空集。清除 (SAR 翻多=新一轮) 在 scan_and_route 扫描前按当轮技术面映射做。"""
|
||||||
|
import json
|
||||||
|
raw = param_store.get(_TECH_EXIT_DONE_KEY, "") or ""
|
||||||
|
if not raw:
|
||||||
|
return set()
|
||||||
|
try:
|
||||||
|
m = json.loads(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return set()
|
||||||
|
return set(m.get("codes") or []) if isinstance(m, dict) else set()
|
||||||
|
|
||||||
|
|
||||||
|
def _save_tech_exit_done(codes) -> None:
|
||||||
|
import json
|
||||||
|
r = param_store.set_param(_TECH_EXIT_DONE_KEY, json.dumps(
|
||||||
|
{"codes": sorted(codes), "at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
|
||||||
|
ensure_ascii=False), "system")
|
||||||
|
if not r.get("ok"):
|
||||||
|
logger.error("[技术面离场] 去重集写入失败: %s —— 同一翻空下轮可能再处理一次", r.get("error"))
|
||||||
|
|
||||||
|
|
||||||
# ================================================================ 新建仓的取数与筛选
|
# ================================================================ 新建仓的取数与筛选
|
||||||
def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
|
def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
|
||||||
"""候选池 → 新建仓候选。产出的候选数天生不超过剩余名额 (名额与金额在纯逻辑里边走边扣)。
|
"""候选池 → 新建仓候选。产出的候选数天生不超过剩余名额 (名额与金额在纯逻辑里边走边扣)。
|
||||||
|
|
@ -968,6 +1020,16 @@ def _scan_params(view: dict) -> dict:
|
||||||
# 两个都关掉时, 合议既不装配也不分流、增持门不设 —— 行为与接入前逐字相同。
|
# 两个都关掉时, 合议既不装配也不分流、增持门不设 —— 行为与接入前逐字相同。
|
||||||
"consensus_route": param_store.get_bool("PMS_CONSENSUS_ROUTE", True),
|
"consensus_route": param_store.get_bool("PMS_CONSENSUS_ROUTE", True),
|
||||||
"tech_gate_increase": param_store.get_bool("PMS_TECH_GATE_INCREASE", True),
|
"tech_gate_increase": param_store.get_bool("PMS_TECH_GATE_INCREASE", True),
|
||||||
|
# 技术面转空自动离场 (2026-09-11 工作包三, 台账 010)。档位 off/propose_only/full:
|
||||||
|
# off 不评 (tech_exit_on 为假)、propose_only 交人、full 自动执行 (与保垫减仓同档)。
|
||||||
|
# tech_exit_done 去重集在 scan_and_route 里按当轮技术面映射填 (这里给空占位)。
|
||||||
|
"tech_exit_on": (str(param_store.get("PMS_TECH_EXIT_AUTONOMY", "full") or "full").strip()
|
||||||
|
in ("full", "propose_only")) and param_store.get_bool("PMS_TECH_ENABLED", True),
|
||||||
|
"tech_exit_propose_only": str(param_store.get("PMS_TECH_EXIT_AUTONOMY", "full")
|
||||||
|
or "full").strip() == "propose_only",
|
||||||
|
"tech_exit_trim_ratio": param_store.get_float("PMS_TECH_EXIT_TRIM_RATIO", 1.0 / 3),
|
||||||
|
"tech_exit_fresh_days": param_store.get_int("PMS_TECH_FLIP_FRESH_DAYS", 2),
|
||||||
|
"tech_exit_done": set(),
|
||||||
})
|
})
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,3 +157,21 @@
|
||||||
**预期。** 质地好的候选排到前面,更容易在名额里占到位置。合议或技术面看空的持仓不再自动补仓加仓,但保垫减仓照常。
|
**预期。** 质地好的候选排到前面,更容易在名额里占到位置。合议或技术面看空的持仓不再自动补仓加仓,但保垫减仓照常。
|
||||||
|
|
||||||
**复核日期。** 上线后满三个交易日,看质地好的票进候选的比例有没有升,看增持门有没有误伤。
|
**复核日期。** 上线后满三个交易日,看质地好的票进候选的比例有没有升,看增持门有没有误伤。
|
||||||
|
|
||||||
|
## 010 · 2026-09-11 · 技术面转空自动离场
|
||||||
|
|
||||||
|
**改动。** 持仓票的技术面相位转为「转空」且 SAR 翻空不超过两个交易日时,系统自动提议离场。确认转空清仓全部可卖量,未确认减三分之一。这是纪律不是投票:SAR 翻空像保垫减仓一样按规则执行,不进入场那套三源合议。
|
||||||
|
|
||||||
|
自动执行,与保垫减仓同档。同一次翻空只处理一次。同一轮里若与别的减持同时出现,优先级是目标价到价最高,其次研究证据走弱,再次技术面转空,保垫减仓最低。
|
||||||
|
|
||||||
|
挂了交易方案的策略票不走这条离场,它的 SAR 由策略层的盘中 SAR 止损线管,那是工作包三的另一部分。这条判断按评估器函数判而不是按动作名判,否则策略票会被误评。
|
||||||
|
|
||||||
|
**依据。** SAR 是跟踪止损指标,翻空即趋势止损位被击穿。确认转空的两种确认是多空布林线在空头区、或价格贴下轨。震荡市里 SAR 翻向不当信号,所以「转空」相位本身已排除震荡市。
|
||||||
|
|
||||||
|
**参数。** 离场档位 PMS_TECH_EXIT_AUTONOMY 有三档:off 不评、propose_only 交人确认、full 自动执行,默认 full。未确认减持比例 PMS_TECH_EXIT_TRIM_RATIO 默认三分之一。翻向新鲜天数复用 PMS_TECH_FLIP_FRESH_DAYS。「已处理过的翻空」用运行参数 PMS_TECH_EXIT_DONE 承载,不动表结构。
|
||||||
|
|
||||||
|
**预期。** 技术面退出每周零到三条,误伤逐只记这条台账。off 或改 propose_only 即可收紧或转人工。
|
||||||
|
|
||||||
|
**复核日期。** 上线后每周复核一次退出条数与误伤。
|
||||||
|
|
||||||
|
**尚未完成的两部分(属工作包三,随后补台账)。** 一是跟踪止盈策略加盘中 SAR 止损线,二是弱基本面加技术面看多的试探仓次日自动挂紧止盈(记台账 011)。
|
||||||
|
|
|
||||||
|
|
@ -80,15 +80,19 @@
|
||||||
wait_tech/交人强制确认/放行/缺合议退旧路/开关关掉逐字回旧)/定档改用 advise_v2
|
wait_tech/交人强制确认/放行/缺合议退旧路/开关关掉逐字回旧)/定档改用 advise_v2
|
||||||
且六键进硬数字而不进送研判白名单/持仓增持门矩阵与 scan 端到端(减持不受影响、
|
且六键进硬数字而不进送研判白名单/持仓增持门矩阵与 scan 端到端(减持不受影响、
|
||||||
开关关掉回旧)/装配服务持仓挂合议/候选按质地分桶排序与观察档进池开关 (24 例)
|
开关关掉回旧)/装配服务持仓挂合议/候选按质地分桶排序与观察档进池开关 (24 例)
|
||||||
|
test_batch30_units.py 三源合议·工作包三离场纪律 (2026-09-11): eval_tech_exit 转空自动离场(确认清仓/
|
||||||
|
未确认减三分之一/翻向超期不评/同一翻空一次/无读数弃权/propose_only 交人/可卖量
|
||||||
|
夹紧/开关关掉逐字回旧)/卖出优先级目标价<研究走弱<技术面转空<保垫减仓/scan 端到端/
|
||||||
|
策略票只看目标价按函数判(技术面转空对策略票不评)/参数登记 (18 例)
|
||||||
test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
|
test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
|
||||||
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
|
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
|
||||||
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 例)
|
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 例)
|
||||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
||||||
目标价到价必定入队 (档位 full 也不自动卖) +
|
目标价到价必定入队 (档位 full 也不自动卖) +
|
||||||
用户设的止损价与目标价单独成列显示 (70 例)
|
用户设的止损价与目标价单独成列显示 (70 例)
|
||||||
共 787 例
|
共 805 例
|
||||||
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
|
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
|
||||||
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 到 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例, 现为 787)
|
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 到 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例, 现为 805)
|
||||||
任一子集失败即整体失败 (退出码 1)。
|
任一子集失败即整体失败 (退出码 1)。
|
||||||
|
|
||||||
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
||||||
|
|
@ -140,6 +144,7 @@ SUITES = ["test_core_units.py", "test_batch2_units.py", "test_batch3_units.py",
|
||||||
"test_batch27_units.py",
|
"test_batch27_units.py",
|
||||||
"test_batch28_units.py",
|
"test_batch28_units.py",
|
||||||
"test_batch29_units.py",
|
"test_batch29_units.py",
|
||||||
|
"test_batch30_units.py",
|
||||||
"test_page_enum_guard.py",
|
"test_page_enum_guard.py",
|
||||||
"test_page_wiring_guard.py",
|
"test_page_wiring_guard.py",
|
||||||
"test_wiring.py"]
|
"test_wiring.py"]
|
||||||
|
|
|
||||||
|
|
@ -369,10 +369,13 @@ def _():
|
||||||
# 次序与判据一个字没动, 减持照旧不送研判。
|
# 次序与判据一个字没动, 减持照旧不送研判。
|
||||||
# 2026-09-07 第三件: 研究走弱的减持 (eval_weak_research, 动作名同为 TRIM) 排在保垫减仓前面,
|
# 2026-09-07 第三件: 研究走弱的减持 (eval_weak_research, 动作名同为 TRIM) 排在保垫减仓前面,
|
||||||
# 其余五个的名单与次序一个字不动。
|
# 其余五个的名单与次序一个字不动。
|
||||||
assert [a for a, _ in ae.EVALUATORS] == ["TRIM", "TRIM", "EXIT", "ADD", "FILL", "DCA"]
|
# 2026-09-11 工作包三: 加技术面转空离场 (eval_tech_exit, 动作名 EXIT) 排在到价清仓之后、
|
||||||
|
# 三类买入之前 —— 次序变为 TRIM、TRIM、EXIT、EXIT、ADD、FILL、DCA。老六个的相对次序一个字不动。
|
||||||
|
assert [a for a, _ in ae.EVALUATORS] == ["TRIM", "TRIM", "EXIT", "EXIT", "ADD", "FILL", "DCA"]
|
||||||
assert [f.__name__ for _, f in ae.EVALUATORS] == [
|
assert [f.__name__ for _, f in ae.EVALUATORS] == [
|
||||||
"eval_weak_research", "eval_trim", "eval_target", "eval_add", "eval_fill", "eval_dca"]
|
"eval_weak_research", "eval_trim", "eval_target", "eval_tech_exit",
|
||||||
assert "EXIT" not in ae.JUDGE_ACTIONS, "减持永远不送研判 (目标价到价也是减持)"
|
"eval_add", "eval_fill", "eval_dca"]
|
||||||
|
assert "EXIT" not in ae.JUDGE_ACTIONS, "减持永远不送研判 (目标价到价、技术面转空离场也是减持)"
|
||||||
# 持仓那条扫描的输入与产出一个字没改: 空持仓进去, 空候选出来, 不会去碰候选池
|
# 持仓那条扫描的输入与产出一个字没改: 空持仓进去, 空候选出来, 不会去碰候选池
|
||||||
r = ae.scan(positions=[], params=params(), market={})
|
r = ae.scan(positions=[], params=params(), market={})
|
||||||
assert r == {"candidates": [], "skipped": []}, r
|
assert r == {"candidates": [], "skipped": []}, r
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""三源合议 · 工作包三离场纪律 (2026-09-11 方案第五节, 台账 010)。全部离线, 不连库。
|
||||||
|
|
||||||
|
入场靠投票, 离场靠纪律: SAR 转空像保垫减仓一样按规则执行, 不进入场那套合议投票。
|
||||||
|
A eval_tech_exit: 相位转空才评/确认清仓·未确认减三分之一/翻向超期不评/同一翻空一次/
|
||||||
|
无读数弃权/propose_only 交人/可卖量夹紧/不足一手不评/开关关掉逐字回旧。
|
||||||
|
B 卖出优先级: 目标价 < 研究走弱 < 技术面转空 < 保垫减仓。
|
||||||
|
C scan 端到端: 产出来源 tech_exit / 同轮与保垫减仓并现留转空 / 目标价优先于转空。
|
||||||
|
D 策略票只看目标价 (按函数判, 不按动作名) —— 技术面转空对策略票不评。
|
||||||
|
E 参数登记。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from app.core import action_engine as ae # noqa: E402
|
||||||
|
from app.services import param_store as ps # noqa: E402
|
||||||
|
|
||||||
|
RESULTS = []
|
||||||
|
|
||||||
|
|
||||||
|
def case(name):
|
||||||
|
def deco(fn):
|
||||||
|
RESULTS.append((name, fn))
|
||||||
|
return fn
|
||||||
|
return deco
|
||||||
|
|
||||||
|
|
||||||
|
def pos(**kw):
|
||||||
|
p = {"ts_code": "600000.SH", "price": 10.0, "avg_cost": 10.0, "total_qty": 6000,
|
||||||
|
"base_qty": 6000, "add_qty": 0, "dca_qty": 0, "market_value": 60_000,
|
||||||
|
"cushion_pct": 0.0, "cushion_peak": 0.0, "target_pct": 0.06,
|
||||||
|
"support_ref": None, "pressure_ref": None, "stop_ref": None,
|
||||||
|
"fill_count": 0, "dca_count": 0, "frozen_reason": "NONE", "avail_qty": 6000}
|
||||||
|
p.update(kw)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def tech(phase="转空", flip=1, confirm=False, sar_value=9.5, reason="SAR 翻空"):
|
||||||
|
return {"phase": phase, "sar_flip_days": flip, "confirm": confirm,
|
||||||
|
"sar_value": sar_value, "reason": reason, "stance": "看空"}
|
||||||
|
|
||||||
|
|
||||||
|
def tpos(t, **kw):
|
||||||
|
p = pos(**kw)
|
||||||
|
p["tech"] = t
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def tparams(**kw):
|
||||||
|
p = {"scale": 2_000_000, "stock_target_default": 0.06, "batch_split": (0.5, 0.25, 0.25),
|
||||||
|
"cushion_solid": 0.03, "trim_peak": 0.06, "trim_giveback": 0.5,
|
||||||
|
"dca_triggers": (-0.08, -0.15), "dca_deep_confirm": -0.15, "dca_max_ratio": 0.5,
|
||||||
|
"no_chase_ma5": 0.06, "build_window_tdays": 10, "fill_max_loss": -0.03,
|
||||||
|
"tech_exit_on": True, "tech_exit_propose_only": False,
|
||||||
|
"tech_exit_trim_ratio": 1.0 / 3, "tech_exit_fresh_days": 2, "tech_exit_done": set()}
|
||||||
|
p.update(kw)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ A eval_tech_exit
|
||||||
|
@case("A 转空离场·开关关掉逐字回旧 (tech_exit_on 假 → 不评)")
|
||||||
|
def _():
|
||||||
|
assert ae.eval_tech_exit(tpos(tech(confirm=True)), tparams(tech_exit_on=False)) is None
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 转空离场·相位非转空不评")
|
||||||
|
def _():
|
||||||
|
assert ae.eval_tech_exit(tpos(tech(phase="趋势空", confirm=True)), tparams()) is None
|
||||||
|
assert ae.eval_tech_exit(tpos(tech(phase="趋势多", confirm=False)), tparams()) is None
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 转空离场·确认转空清仓全部可卖量 (EXIT, 来源 tech_exit)")
|
||||||
|
def _():
|
||||||
|
c = ae.eval_tech_exit(tpos(tech(confirm=True), total_qty=6000, avail_qty=6000), tparams())
|
||||||
|
assert c and c["action"] == "EXIT" and c["side"] == "sell"
|
||||||
|
assert c["qty"] == 6000 and c["source"] == ae.SRC_TECH_EXIT
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 转空离场·未确认减三分之一 (TRIM)")
|
||||||
|
def _():
|
||||||
|
c = ae.eval_tech_exit(tpos(tech(confirm=False), total_qty=6000, avail_qty=6000), tparams())
|
||||||
|
assert c and c["action"] == "TRIM" and c["qty"] == 2000
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 转空离场·翻向超期不评 (超过 fresh_days)")
|
||||||
|
def _():
|
||||||
|
assert ae.eval_tech_exit(tpos(tech(flip=3, confirm=True)), tparams()) is None
|
||||||
|
assert ae.eval_tech_exit(tpos(tech(flip=2, confirm=True)), tparams()) is not None
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 转空离场·同一翻空已处理过不再评")
|
||||||
|
def _():
|
||||||
|
p = tpos(tech(confirm=True), ts_code="600000.SH")
|
||||||
|
assert ae.eval_tech_exit(p, tparams(tech_exit_done={"600000.SH"})) is None
|
||||||
|
assert ae.eval_tech_exit(p, tparams(tech_exit_done=set())) is not None
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 转空离场·无技术面读数不评 (无读数弃权)")
|
||||||
|
def _():
|
||||||
|
p = pos()
|
||||||
|
p.pop("tech", None)
|
||||||
|
assert ae.eval_tech_exit(p, tparams()) is None
|
||||||
|
p2 = pos()
|
||||||
|
p2["tech"] = None
|
||||||
|
assert ae.eval_tech_exit(p2, tparams()) is None
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 转空离场·propose_only 打交人标记")
|
||||||
|
def _():
|
||||||
|
c = ae.eval_tech_exit(tpos(tech(confirm=True), total_qty=6000, avail_qty=6000),
|
||||||
|
tparams(tech_exit_propose_only=True))
|
||||||
|
assert c and c.get("needs_user_confirm") is True and "propose_only" in (c.get("confirm_why") or "")
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 转空离场·按 T+1 可卖量夹紧 (确认时清可卖量而非总持仓)")
|
||||||
|
def _():
|
||||||
|
c = ae.eval_tech_exit(tpos(tech(confirm=True), total_qty=6000, avail_qty=3000), tparams())
|
||||||
|
assert c and c["qty"] == 3000
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 转空离场·不足一手不评")
|
||||||
|
def _():
|
||||||
|
assert ae.eval_tech_exit(tpos(tech(confirm=True), total_qty=50, avail_qty=50), tparams()) is None
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ B 卖出优先级
|
||||||
|
@case("B 卖出优先级·目标价 < 研究走弱 < 技术面转空 < 保垫减仓")
|
||||||
|
def _():
|
||||||
|
pr = lambda a, s: ae._sell_priority({"action": a, "source": s})
|
||||||
|
assert (pr("EXIT", ae.SRC_TARGET_PRICE) < pr("TRIM", ae.SRC_RESEARCH_WEAK)
|
||||||
|
< pr("EXIT", ae.SRC_TECH_EXIT) < pr("TRIM", ae.SRC_ENGINE))
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ C scan 端到端
|
||||||
|
@case("C scan·确认转空产出 EXIT 候选")
|
||||||
|
def _():
|
||||||
|
p = tpos(tech(confirm=True), total_qty=6000, avail_qty=6000)
|
||||||
|
r = ae.scan(positions=[p], params=tparams(), market={p["ts_code"]: {}})
|
||||||
|
exits = [c for c in r["candidates"] if c.get("source") == ae.SRC_TECH_EXIT]
|
||||||
|
assert len(exits) == 1 and exits[0]["action"] == "EXIT"
|
||||||
|
|
||||||
|
|
||||||
|
@case("C scan·同轮与保垫减仓并现留技术面转空 (优先级更高)")
|
||||||
|
def _():
|
||||||
|
p = tpos(tech(confirm=True), total_qty=6000, avail_qty=6000,
|
||||||
|
cushion_peak=0.08, cushion_pct=0.04)
|
||||||
|
r = ae.scan(positions=[p], params=tparams(), market={p["ts_code"]: {}})
|
||||||
|
sells = [c for c in r["candidates"] if c["side"] == "sell"]
|
||||||
|
assert len(sells) == 1 and sells[0]["source"] == ae.SRC_TECH_EXIT
|
||||||
|
|
||||||
|
|
||||||
|
@case("C scan·目标价到价优先于技术面转空")
|
||||||
|
def _():
|
||||||
|
p = tpos(tech(confirm=True), total_qty=6000, avail_qty=6000, price=13.0)
|
||||||
|
r = ae.scan(positions=[p], params=tparams(), market={p["ts_code"]: {}},
|
||||||
|
stock_params={"600000.SH": {"target_price": 12.5}})
|
||||||
|
sells = [c for c in r["candidates"] if c["side"] == "sell"]
|
||||||
|
assert len(sells) == 1 and sells[0]["source"] == ae.SRC_TARGET_PRICE
|
||||||
|
|
||||||
|
|
||||||
|
@case("C scan·开关关掉逐字回旧: 挂了转空读数也不产出转空离场")
|
||||||
|
def _():
|
||||||
|
p = tpos(tech(confirm=True), total_qty=6000, avail_qty=6000)
|
||||||
|
r = ae.scan(positions=[p], params=tparams(tech_exit_on=False), market={p["ts_code"]: {}})
|
||||||
|
assert not any(c.get("source") == ae.SRC_TECH_EXIT for c in r["candidates"])
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ D 策略票只看目标价 (Part 2)
|
||||||
|
@case("D 策略票·目标价照评但技术面转空不评 (按函数判)")
|
||||||
|
def _():
|
||||||
|
p = tpos(tech(confirm=True), total_qty=6000, avail_qty=6000, price=13.0)
|
||||||
|
r = ae.scan(positions=[p], params=tparams(), market={p["ts_code"]: {}},
|
||||||
|
strategy_codes={"600000.SH"}, stock_params={"600000.SH": {"target_price": 12.5}})
|
||||||
|
assert any(c.get("source") == ae.SRC_TARGET_PRICE for c in r["candidates"]), "策略票目标价照评"
|
||||||
|
assert not any(c.get("source") == ae.SRC_TECH_EXIT for c in r["candidates"]), "策略票技术面转空不评"
|
||||||
|
|
||||||
|
|
||||||
|
@case("D 策略票·无目标价时技术面转空也不产出 (策略层接管)")
|
||||||
|
def _():
|
||||||
|
p = tpos(tech(confirm=True), total_qty=6000, avail_qty=6000)
|
||||||
|
r = ae.scan(positions=[p], params=tparams(), market={p["ts_code"]: {}},
|
||||||
|
strategy_codes={"600000.SH"})
|
||||||
|
assert not any(c.get("source") == ae.SRC_TECH_EXIT for c in r["candidates"])
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ E 参数登记
|
||||||
|
@case("E 参数·转空离场三键登记, 档位默认 full")
|
||||||
|
def _():
|
||||||
|
for k in ("PMS_TECH_EXIT_AUTONOMY", "PMS_TECH_EXIT_TRIM_RATIO", "PMS_TECH_EXIT_DONE"):
|
||||||
|
assert k in ps.RUNTIME_EXTRA, k
|
||||||
|
assert ps.RUNTIME_EXTRA["PMS_TECH_EXIT_AUTONOMY"][0] == "full"
|
||||||
|
assert ps.RUNTIME_EXTRA["PMS_TECH_EXIT_AUTONOMY"][1] is str
|
||||||
|
assert abs(ps.RUNTIME_EXTRA["PMS_TECH_EXIT_TRIM_RATIO"][0] - 1.0 / 3) < 1e-3
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ok = 0
|
||||||
|
for name, fn in RESULTS:
|
||||||
|
try:
|
||||||
|
fn()
|
||||||
|
ok += 1
|
||||||
|
print(" ok " + name)
|
||||||
|
except Exception:
|
||||||
|
print(" FAIL " + name)
|
||||||
|
traceback.print_exc()
|
||||||
|
print("-" * 60)
|
||||||
|
if ok == len(RESULTS):
|
||||||
|
print("ALL PASS (%d cases)" % ok)
|
||||||
|
return 0
|
||||||
|
print("FAILED %d/%d" % (len(RESULTS) - ok, len(RESULTS)))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Loading…
Reference in New Issue