跳过原因分得开: skip 改成带原因的字典 (在途提议/在途指令/规则闸/研判闸四种处境), 兼容集合口径
This commit is contained in:
parent
a14db0c833
commit
d8829daa6b
48
DEVLOG.md
48
DEVLOG.md
|
|
@ -172,6 +172,54 @@ PMS_UNAVAILABLE 三个值,而 `pms_advisor._today_exit_verdict` 第 102 行明
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-08-06 · 新建仓盘中真跑了 40 分钟,第一份实测;跳过原因改成分得开
|
||||||
|
|
||||||
|
**这一条是实测记录,不是设计。** 新建仓在今天下午约 14:07 起真跑了四十分钟
|
||||||
|
(部署时间比原计划提前,是在盘中做的)。
|
||||||
|
|
||||||
|
**研判判据生效,方向对。** 评审账本里 11 条 `arbiter=judge、action=OPEN、verdict=REJECT`,
|
||||||
|
理由**每一条都在谈「开新仓」**——「建仓缺乏资金支持」「开新仓风险过高」「新建仓证据不足」,
|
||||||
|
没有一条把它当成加仓在谈;而且确实用上了决策系统自动补的实时资金分布(主力净流占比、
|
||||||
|
主散背离)去对照「把这只票挑出来的驱动到今天还成不成立」。判收第一步通过。
|
||||||
|
|
||||||
|
**当日去重立竿见影。** 驳回集中在 14:07 到 14:38 这 31 分钟,扫完一遍候选池之后就静默了。
|
||||||
|
没有这道去重,这 22 只会每分钟重送一次研判,一个下午上千次大模型调用与上千行账本。
|
||||||
|
研判速度也对得上预期:31 分钟十几只,正是 150 秒预算下一跳一到两条的节奏。
|
||||||
|
|
||||||
|
**今天一股没建,驳回率 100%。** 多条理由提到「宏观空头环境」「市场空头相位」「熊市共振」
|
||||||
|
「下降通道」,今天本来就是空头,全驳回大概率是合理判断,不是判据写窄。
|
||||||
|
**但埋了一处要盯**:OPEN 判据末尾那句「这是开新仓不是加仓,没有既有仓位做缓冲,建错了
|
||||||
|
代价全额承担,标准应当比加仓类更严,不确定就 REJECT」,加上边界声明里的「宁可错杀不可
|
||||||
|
错放」,**是双重保守**。若接下来两三天在正常或偏多的市场里仍接近全驳回,就是这句话把模型
|
||||||
|
推过头了,要调轻。一天的数据不足以判断。
|
||||||
|
|
||||||
|
**这份实测暴露的可读性问题,已改**:22 条跳过全写着同一句「已有在途提议或指令, 或今日已被
|
||||||
|
闸门拒过」,把四种完全不同的处境揉成一句——有在途提议、有在途指令、今日被规则闸拒过、
|
||||||
|
今日被研判闸驳回过。前两种明天照样被挡,后两种日切就重新评估,而它们长得一模一样,
|
||||||
|
看的人判断不了这只票明天还会不会再被评估。这违反了 `scan` 入口自己立的那条规矩:
|
||||||
|
「什么都没发生」和「明着跳过了」必须是两回事——现在虽然明着跳过了,却说不清为什么。
|
||||||
|
|
||||||
|
改法:`skip` 从集合改成 `{(代码, 动作): 原因}` 字典。`in` 的语义不变,
|
||||||
|
新增 `action_engine.skip_why()` 取原因,**传集合进来仍然工作**(退回一句兜底话,
|
||||||
|
既有单测与临时调用不受影响)。三个来源各写各的原因,在途的排在最后覆盖被拒的——
|
||||||
|
一只票既有在途指令又今天被拒过时,「有在途」更贴近它此刻的状态。
|
||||||
|
在途那两条还带上提议号、指令号与状态,让人从这一行就能接着往下查。
|
||||||
|
|
||||||
|
**动了哪些文件**:`app/core/action_engine.py`(`skip_why` 与两个扫描入口)、
|
||||||
|
`app/services/proposal_service.py`(三个来源改成带原因的字典);
|
||||||
|
测试 `scripts/test_batch12_units.py` 加 3 例,`scripts/test_batch4_units.py` 与
|
||||||
|
`scripts/test_wiring.py` 各改一处断言(原本钉着那句笼统话的字面量)。
|
||||||
|
全量单测 **ALL SUITES PASS,467 例**。
|
||||||
|
|
||||||
|
**一条要更正的承诺**:先前说过「既有 `scan()` 一个字不动」,这次动了它——为的是让跳过留痕
|
||||||
|
说得清。`in` 的语义与向后兼容都保住了,但这个承诺本身要如实更正,不该含糊过去。
|
||||||
|
|
||||||
|
**我在这一轮犯的一个误判**:账本里 14:38 有两条对没持仓票的 `PMS_REJECT`,我据此推断
|
||||||
|
「新建仓还没上线,所以不可能,应该是探活脚本」——实际是新建仓已经部署并在跑。
|
||||||
|
**推理没落地就当结论**,与 07 月那次 `audit_date` 的教训同型。该先查账本再下判断。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-08-06 · 收盘后四条只读诊断,外加一个由它们暴露出来的窗口末日追高
|
## 2026-08-06 · 收盘后四条只读诊断,外加一个由它们暴露出来的窗口末日追高
|
||||||
|
|
||||||
**四条诊断的结论**
|
**四条诊断的结论**
|
||||||
|
|
|
||||||
|
|
@ -200,16 +200,39 @@ def eval_trim(p: dict, params: dict, mkt: dict = None):
|
||||||
EVALUATORS = ((A_TRIM, eval_trim), (A_ADD, eval_add), (A_FILL, eval_fill), (A_DCA, eval_dca))
|
EVALUATORS = ((A_TRIM, eval_trim), (A_ADD, eval_add), (A_FILL, eval_fill), (A_DCA, eval_dca))
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ 跳过原因
|
||||||
|
SKIP_WHY_FALLBACK = "已有在途提议或指令, 或今日已被闸门拒过"
|
||||||
|
|
||||||
|
|
||||||
|
def skip_why(skip, key):
|
||||||
|
"""这个 (代码, 动作) 为什么被跳过。不在跳过集合里返回 None。
|
||||||
|
|
||||||
|
skip 可以是 `{(代码, 动作): 原因}` 字典, 也可以是只有键的集合 (旧口径)。
|
||||||
|
集合形式退回一句笼统的兜底话 —— 这是 2026-08-06 之前的样子, 留着是为了兼容
|
||||||
|
既有单测与临时调用, 生产路径由 proposal_service 传字典进来。
|
||||||
|
|
||||||
|
为什么值得专门改这一处: 那句兜底话把四种处境揉成了一句 —— 有在途提议、有在途指令、
|
||||||
|
今天被规则闸拒过、今天被研判闸驳回过。前两种明天照样会被挡, 后两种日切就重新评估,
|
||||||
|
而它们在页面上长得一模一样, 看的人判断不了这只票明天还会不会再被评估。
|
||||||
|
「明着跳过了」不该等于「说不清为什么跳过」。
|
||||||
|
"""
|
||||||
|
if not skip or key not in skip:
|
||||||
|
return None
|
||||||
|
if isinstance(skip, dict):
|
||||||
|
return skip.get(key) or SKIP_WHY_FALLBACK
|
||||||
|
return SKIP_WHY_FALLBACK
|
||||||
|
|
||||||
|
|
||||||
# ================================================================ 扫描入口
|
# ================================================================ 扫描入口
|
||||||
def scan(*, positions: list, params: dict, market: dict, skip: set = None) -> dict:
|
def scan(*, positions: list, params: dict, market: dict, skip=None) -> dict:
|
||||||
"""扫描全部持仓, 产出候选动作。
|
"""扫描全部持仓, 产出候选动作。
|
||||||
|
|
||||||
positions: portfolio.positions_view()["held"] 的口径
|
positions: portfolio.positions_view()["held"] 的口径
|
||||||
market: {ts_code: {ma5, high5, tdays_since_open, tdays_since_last_add}}
|
market: {ts_code: {ma5, high5, tdays_since_open, tdays_since_last_add}}
|
||||||
skip: 已有在途提议/指令的 (ts_code, action) 集合 —— 不重复提
|
skip: 不再评估的 (代码, 动作) —— 字典时值是跳过原因, 集合时退回兜底话
|
||||||
返回 {"candidates": [...], "skipped": [...]}
|
返回 {"candidates": [...], "skipped": [...]}
|
||||||
"""
|
"""
|
||||||
skip = skip or set()
|
skip = skip or {}
|
||||||
out, skipped = [], []
|
out, skipped = [], []
|
||||||
for p in positions or []:
|
for p in positions or []:
|
||||||
code = p.get("ts_code")
|
code = p.get("ts_code")
|
||||||
|
|
@ -228,8 +251,9 @@ def scan(*, positions: list, params: dict, market: dict, skip: set = None) -> di
|
||||||
mkt = (market or {}).get(code) or {}
|
mkt = (market or {}).get(code) or {}
|
||||||
frozen = (p.get("frozen_reason") or "NONE") != "NONE"
|
frozen = (p.get("frozen_reason") or "NONE") != "NONE"
|
||||||
for action, fn in EVALUATORS:
|
for action, fn in EVALUATORS:
|
||||||
if (code, action) in skip:
|
why = skip_why(skip, (code, action))
|
||||||
skipped.append({"ts_code": code, "action": action, "why": "已有在途提议/指令"})
|
if why:
|
||||||
|
skipped.append({"ts_code": code, "action": action, "why": why})
|
||||||
continue
|
continue
|
||||||
# 冻结只禁增持, 减仓照评 (与规则闸同一口径, 这里先剪枝少算一遍)
|
# 冻结只禁增持, 减仓照评 (与规则闸同一口径, 这里先剪枝少算一遍)
|
||||||
if frozen and action != A_TRIM:
|
if frozen and action != A_TRIM:
|
||||||
|
|
@ -334,7 +358,7 @@ def eval_open(c: dict, params: dict, caps: dict, room_amt: float):
|
||||||
|
|
||||||
|
|
||||||
def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
|
def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
|
||||||
slots: int, skip: set = None) -> dict:
|
slots: int, skip=None) -> dict:
|
||||||
"""从上游候选池挑新票建仓。名额与金额**边走边扣**, 所以产出的这一组候选彼此不冲突。
|
"""从上游候选池挑新票建仓。名额与金额**边走边扣**, 所以产出的这一组候选彼此不冲突。
|
||||||
|
|
||||||
为什么滚动必须在这里做, 而不是交给规则闸: `_route_one` 给每条候选调 `caps_ctx` 时,
|
为什么滚动必须在这里做, 而不是交给规则闸: `_route_one` 给每条候选调 `caps_ctx` 时,
|
||||||
|
|
@ -348,9 +372,10 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
|
||||||
|
|
||||||
slots: 还能开几只新仓 = 最大持仓数 − 当前持仓数 (由调用方算好)
|
slots: 还能开几只新仓 = 最大持仓数 − 当前持仓数 (由调用方算好)
|
||||||
room_amt: 还有多少钱可投, 已取「总仓上限×总规模 − 组合市值」与「真实可用资金」的小者
|
room_amt: 还有多少钱可投, 已取「总仓上限×总规模 − 组合市值」与「真实可用资金」的小者
|
||||||
skip: 不再评估的 (代码, 动作) 集合 —— 在途提议/指令、当日已被规则闸或研判闸拒过的
|
skip: 不再评估的 (代码, 动作) —— 字典时值是跳过原因 (在途提议 / 在途指令 /
|
||||||
|
今日被规则闸拒过 / 今日被研判闸驳回过, 这四种处境完全不同, 必须分得开)
|
||||||
"""
|
"""
|
||||||
skip = skip or set()
|
skip = skip or {}
|
||||||
out, skipped = [], []
|
out, skipped = [], []
|
||||||
slots = int(slots or 0)
|
slots = int(slots or 0)
|
||||||
left = _f(room_amt)
|
left = _f(room_amt)
|
||||||
|
|
@ -390,9 +415,9 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
|
||||||
skipped.append({"ts_code": code, "action": A_OPEN,
|
skipped.append({"ts_code": code, "action": A_OPEN,
|
||||||
"why": "本轮可投金额已用完 (下一跳按最新组合市值重算)"})
|
"why": "本轮可投金额已用完 (下一跳按最新组合市值重算)"})
|
||||||
continue
|
continue
|
||||||
if (code, A_OPEN) in skip:
|
why_skip = skip_why(skip, (code, A_OPEN))
|
||||||
skipped.append({"ts_code": code, "action": A_OPEN,
|
if why_skip:
|
||||||
"why": "已有在途提议或指令, 或今日已被闸门拒过"})
|
skipped.append({"ts_code": code, "action": A_OPEN, "why": why_skip})
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
cand, why = eval_open(c, params, ctx, left)
|
cand, why = eval_open(c, params, ctx, left)
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,12 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
|
||||||
# 一次, 一天往评审账本灌几千行一模一样的记录。闸门结论当天基本不会变, 记一次就够。
|
# 一次, 一天往评审账本灌几千行一模一样的记录。闸门结论当天基本不会变, 记一次就够。
|
||||||
# ③ 只挡新建仓: 加仓类对研判驳回**有意**不去重 (契约里那句「研判结论会变」), 那条
|
# ③ 只挡新建仓: 加仓类对研判驳回**有意**不去重 (契约里那句「研判结论会变」), 那条
|
||||||
# 纪律不动; 而候选池是几十只的量级, 不挡的话 ② 那个洞会原样从研判闸重来一遍。
|
# 纪律不动; 而候选池是几十只的量级, 不挡的话 ② 那个洞会原样从研判闸重来一遍。
|
||||||
skip = _inflight_keys() | _rejected_today_keys() | _judge_rejected_open_keys()
|
#
|
||||||
|
# 三份合并成 `{(代码, 动作): 原因}`, **在途的排在最后, 覆盖被拒的** (2026-08-06):
|
||||||
|
# 这四种处境完全不同 —— 在途的明天照样被挡, 被拒的日切就重新评估。从前它们在
|
||||||
|
# skipped 里长得一模一样, 看的人判断不了这只票明天还会不会再被评估。
|
||||||
|
# 一只票既有在途指令又今天被拒过时, 显示"有在途"更贴近它此刻的实际状态。
|
||||||
|
skip = {**_judge_rejected_open_keys(), **_rejected_today_keys(), **_inflight_keys()}
|
||||||
scanned = ae.scan(positions=view["held"], params=params, market=mkt, skip=skip)
|
scanned = ae.scan(positions=view["held"], params=params, market=mkt, skip=skip)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("提议扫描失败")
|
logger.exception("提议扫描失败")
|
||||||
|
|
@ -453,15 +458,16 @@ def _tdays_between(start, today):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _rejected_today_keys() -> set:
|
def _rejected_today_keys() -> dict:
|
||||||
"""今天已被规则闸拒过的 (代码, 动作)。读不到就返回空集 —— 去重是降噪, 不是纪律,
|
"""今天已被规则闸拒过的 {(代码, 动作): 原因}。读不到就返回空 —— 去重是降噪, 不是纪律,
|
||||||
读失败时宁可多记几行日志, 也不能因此漏扫一个本该评估的动作。"""
|
读失败时宁可多记几行日志, 也不能因此漏扫一个本该评估的动作。"""
|
||||||
try:
|
try:
|
||||||
return pms_repo.rule_rejected_today(datetime.now().replace(
|
keys = pms_repo.rule_rejected_today(datetime.now().replace(
|
||||||
hour=0, minute=0, second=0, microsecond=0))
|
hour=0, minute=0, second=0, microsecond=0))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("读当日规则闸拒绝记录失败 (按未拒过继续扫描): %s", e)
|
logger.warning("读当日规则闸拒绝记录失败 (按未拒过继续扫描): %s", e)
|
||||||
return set()
|
return {}
|
||||||
|
return {k: "今日已被规则闸拒过 (日切后重新评估; 拒因看 make t-gate)" for k in keys}
|
||||||
|
|
||||||
|
|
||||||
def _buy_signals_today() -> dict:
|
def _buy_signals_today() -> dict:
|
||||||
|
|
@ -475,8 +481,8 @@ def _buy_signals_today() -> dict:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _judge_rejected_open_keys() -> set:
|
def _judge_rejected_open_keys() -> dict:
|
||||||
"""今天已被研判闸驳回的**新建仓** (代码, 动作)。读不到就返回空集 (同上: 去重是降噪)。
|
"""今天已被研判闸驳回的**新建仓** {(代码, 动作): 原因}。读不到就返回空 (同上: 去重是降噪)。
|
||||||
|
|
||||||
只取 OPEN 那些键。加仓类对研判驳回**有意**不做当日去重 —— 契约里那句「研判结论会变」,
|
只取 OPEN 那些键。加仓类对研判驳回**有意**不做当日去重 —— 契约里那句「研判结论会变」,
|
||||||
节流责任放在决策系统那侧的半小时缓存上, 这条纪律一个字不动。
|
节流责任放在决策系统那侧的半小时缓存上, 这条纪律一个字不动。
|
||||||
|
|
@ -486,21 +492,29 @@ def _judge_rejected_open_keys() -> set:
|
||||||
hour=0, minute=0, second=0, microsecond=0))
|
hour=0, minute=0, second=0, microsecond=0))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("读当日研判驳回记录失败 (按未驳回过继续扫描): %s", e)
|
logger.warning("读当日研判驳回记录失败 (按未驳回过继续扫描): %s", e)
|
||||||
return set()
|
return {}
|
||||||
return {(code, act) for code, act in keys if act == ae.A_OPEN}
|
return {(code, act): "今日已被研判闸驳回 (日切后重新评估; 驳回理由看 make t-gate)"
|
||||||
|
for code, act in keys if act == ae.A_OPEN}
|
||||||
|
|
||||||
|
|
||||||
def _inflight_keys() -> set:
|
def _inflight_keys() -> dict:
|
||||||
"""已有在途提议或在途指令的 (代码, 动作) —— 同一件事不重复提。"""
|
"""已有在途提议或在途指令的 {(代码, 动作): 原因} —— 同一件事不重复提。
|
||||||
keys = set()
|
|
||||||
|
提议与指令分开写原因, 并且**指令写在后面覆盖提议**: 一条提议被采纳之后会变成指令,
|
||||||
|
这时候「有在途指令」比「有提议在等确认」更贴近它此刻的状态。原因里带上单号与状态,
|
||||||
|
是为了让人从 skipped 那一行就能接着往下查, 不必再去翻两张表。
|
||||||
|
"""
|
||||||
|
keys = {}
|
||||||
try:
|
try:
|
||||||
for p in pms_repo.list_proposals(statuses=("WAIT_USER",), limit=200):
|
for p in pms_repo.list_proposals(statuses=("WAIT_USER",), limit=200):
|
||||||
keys.add((p["ts_code"], p["action"]))
|
keys[(p["ts_code"], p["action"])] = \
|
||||||
|
f"已有在途提议 {p.get('proposal_id')} 在等人确认"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("读提议队列失败: %s", e)
|
logger.warning("读提议队列失败: %s", e)
|
||||||
try:
|
try:
|
||||||
for i in pms_repo.list_instructions(statuses=list(executor.LIVE), limit=300):
|
for i in pms_repo.list_instructions(statuses=list(executor.LIVE), limit=300):
|
||||||
keys.add((i["ts_code"], i.get("action")))
|
keys[(i["ts_code"], i.get("action"))] = \
|
||||||
|
f"已有在途指令 {i.get('instruction_id')} ({i.get('status')})"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("读在途指令失败: %s", e)
|
logger.warning("读在途指令失败: %s", e)
|
||||||
return keys
|
return keys
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 (48 例)
|
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 (48 例)
|
||||||
test_batch11_units.py 择时实现A: 本地检查等价/应答折算/缓存冷却/退B (24 例)
|
test_batch11_units.py 择时实现A: 本地检查等价/应答折算/缓存冷却/退B (24 例)
|
||||||
test_batch12_units.py 自主新建仓: 选票与滚动扣减/硬数字裁剪/漂移/预算/
|
test_batch12_units.py 自主新建仓: 选票与滚动扣减/硬数字裁剪/漂移/预算/
|
||||||
转多信号插队与留痕/窗口末日只对命令强制完成 (39 例)
|
转多信号插队与留痕/窗口末日只对命令/跳过原因分得开 (42 例)
|
||||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
||||||
共 464 例
|
共 467 例
|
||||||
任一子集失败即整体失败 (退出码 1)。
|
任一子集失败即整体失败 (退出码 1)。
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,49 @@ def _():
|
||||||
assert "在途" in r["skipped"][0]["why"], r["skipped"]
|
assert "在途" in r["skipped"][0]["why"], r["skipped"]
|
||||||
|
|
||||||
|
|
||||||
|
@case("跳过原因·字典形式把四种处境分得开 (在途 / 规则闸 / 研判闸)")
|
||||||
|
def _():
|
||||||
|
skip = {
|
||||||
|
("600000.SH", "OPEN"): "已有在途指令 INS_20260806_600000SH_OPEN_001 (DISPATCHED)",
|
||||||
|
("600001.SH", "OPEN"): "今日已被规则闸拒过 (日切后重新评估; 拒因看 make t-gate)",
|
||||||
|
("600002.SH", "OPEN"): "今日已被研判闸驳回 (日切后重新评估; 驳回理由看 make t-gate)",
|
||||||
|
}
|
||||||
|
cs = [cand("600000.SH", score=300, rank=1), cand("600001.SH", score=200, rank=2),
|
||||||
|
cand("600002.SH", score=100, rank=3)]
|
||||||
|
r = ae.scan_open(candidates=cs, params=params(), caps=caps(),
|
||||||
|
room_amt=1_400_000, slots=20, skip=skip)
|
||||||
|
assert r["candidates"] == [], r
|
||||||
|
got = {s["ts_code"]: s["why"] for s in r["skipped"]}
|
||||||
|
assert "在途指令" in got["600000.SH"] and "DISPATCHED" in got["600000.SH"], got
|
||||||
|
assert "规则闸" in got["600001.SH"] and "日切后重新评估" in got["600001.SH"], got
|
||||||
|
assert "研判闸" in got["600002.SH"] and "日切后重新评估" in got["600002.SH"], got
|
||||||
|
# 「明天还会不会再评估」这个问题, 光看这一行就该答得出来
|
||||||
|
assert "日切后重新评估" not in got["600000.SH"], "在途的明天照样被挡, 别写成会重来"
|
||||||
|
|
||||||
|
|
||||||
|
@case("跳过原因·传集合进来仍然工作 (旧口径的向后兼容)")
|
||||||
|
def _():
|
||||||
|
r = ae.scan_open(candidates=[cand("600000.SH")], params=params(), caps=caps(),
|
||||||
|
room_amt=1_400_000, slots=20, skip={("600000.SH", "OPEN")})
|
||||||
|
assert r["skipped"][0]["why"] == ae.SKIP_WHY_FALLBACK, r["skipped"]
|
||||||
|
assert ae.skip_why({("A", "OPEN")}, ("A", "OPEN")) == ae.SKIP_WHY_FALLBACK
|
||||||
|
assert ae.skip_why({("A", "OPEN")}, ("B", "OPEN")) is None
|
||||||
|
assert ae.skip_why(None, ("A", "OPEN")) is None
|
||||||
|
# 字典里值是空的也不能当成"没被跳过"
|
||||||
|
assert ae.skip_why({("A", "OPEN"): ""}, ("A", "OPEN")) == ae.SKIP_WHY_FALLBACK
|
||||||
|
|
||||||
|
|
||||||
|
@case("跳过原因·已有四类动作也跟着分得开 (scan 与 scan_open 同一套)")
|
||||||
|
def _():
|
||||||
|
pos = [{"ts_code": "600000.SH", "total_qty": 1000, "avail_qty": 1000,
|
||||||
|
"price": 10.0, "price_ok": True, "cushion_pct": 0.05, "cushion_peak": 0.10,
|
||||||
|
"frozen_reason": "NONE", "base_qty": 1000}]
|
||||||
|
skip = {("600000.SH", "TRIM"): "已有在途指令 INS_x (DISPATCHED)"}
|
||||||
|
r = ae.scan(positions=pos, params=params(), market={}, skip=skip)
|
||||||
|
trim = [s for s in r["skipped"] if s["action"] == "TRIM"]
|
||||||
|
assert trim and "在途指令" in trim[0]["why"], r["skipped"]
|
||||||
|
|
||||||
|
|
||||||
@case("新建仓·单票评估异常不拖垮整轮, 后面的票照常评")
|
@case("新建仓·单票评估异常不拖垮整轮, 后面的票照常评")
|
||||||
def _():
|
def _():
|
||||||
orig = ae.eval_open
|
orig = ae.eval_open
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,10 @@ def _():
|
||||||
"600519.SH": mkt()},
|
"600519.SH": mkt()},
|
||||||
skip={("000001.SZ", "ADD")})
|
skip={("000001.SZ", "ADD")})
|
||||||
assert ("000001.SZ", "ADD") not in {(c["ts_code"], c["action"]) for c in r2["candidates"]}
|
assert ("000001.SZ", "ADD") not in {(c["ts_code"], c["action"]) for c in r2["candidates"]}
|
||||||
assert any(s.get("why") == "已有在途提议/指令" for s in r2["skipped"]), r2["skipped"]
|
# 2026-08-06: skip 改成 {(代码, 动作): 原因} 的字典, 好让 skipped 说得出是哪一种跳过
|
||||||
|
# (在途提议 / 在途指令 / 今日被规则闸拒过 / 今日被研判闸驳回过 —— 前两种明天照样被挡,
|
||||||
|
# 后两种日切就重新评估)。这里传的仍是**集合**, 走的是向后兼容那条路, 所以拿到兜底话。
|
||||||
|
assert any(s.get("why") == ae.SKIP_WHY_FALLBACK for s in r2["skipped"]), r2["skipped"]
|
||||||
|
|
||||||
# 空仓票直接跳过, 不进候选
|
# 空仓票直接跳过, 不进候选
|
||||||
r3 = ae.scan(positions=[pos(ts_code="300750.SZ", total_qty=0)], params=PARAMS, market={})
|
r3 = ae.scan(positions=[pos(ts_code="300750.SZ", total_qty=0)], params=PARAMS, market={})
|
||||||
|
|
|
||||||
|
|
@ -1507,10 +1507,15 @@ def _():
|
||||||
add_prop = [p for p in fake.proposals.values() if p["action"] == "ADD"]
|
add_prop = [p for p in fake.proposals.values() if p["action"] == "ADD"]
|
||||||
assert add_prop and add_prop[0]["qty"] == 2700, add_prop
|
assert add_prop and add_prop[0]["qty"] == 2700, add_prop
|
||||||
assert add_prop[0]["hard_numbers"]["price"] == 11.0
|
assert add_prop[0]["hard_numbers"]["price"] == 11.0
|
||||||
# 再扫一轮不重复提 (在途去重)
|
# 再扫一轮不重复提 (在途去重)。
|
||||||
|
# 2026-08-06: 跳过原因从一句笼统的「已有在途提议/指令」改成分得开的四种 ——
|
||||||
|
# 提议还是指令、单号是哪个、是当日被闸拒过 (日切重来) 还是在途 (明天照样挡)。
|
||||||
|
# 这里同时钉住两条: 提议那条要报出提议号, 指令那条要报出指令号与状态。
|
||||||
r2 = ps.scan_and_route()
|
r2 = ps.scan_and_route()
|
||||||
assert not r2["executed"] and not r2["queued"], r2
|
assert not r2["executed"] and not r2["queued"], r2
|
||||||
assert any(s.get("why") == "已有在途提议/指令" for s in r2["skipped"]), r2["skipped"]
|
whys = [s.get("why") or "" for s in r2["skipped"]]
|
||||||
|
assert any("在途提议" in w and "PRP_" in w for w in whys), r2["skipped"]
|
||||||
|
assert any("在途指令" in w and "INS_" in w for w in whys), r2["skipped"]
|
||||||
|
|
||||||
|
|
||||||
@case("自主提议·full + 研判通过: 增持直接落指令; 深档补仓仍强制确认")
|
@case("自主提议·full + 研判通过: 增持直接落指令; 深档补仓仍强制确认")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue