加固包:去重集清除条件、持仓行转多留痕、登记弱表态开关、时钟用例改相对时刻

评审第五节四处小加固,每处开关关掉或默认值即旧行为:
- 转空去重集只在 SAR 明确为多时才清除(原来「不是空就清」把缺读数也当翻多,
  会重复减仓)。抽出纯函数 _keep_tech_exit_done,第三十批加三例。
- 持仓行的择时票带上当日盘中转多留痕:decorate_positions 从买入信号同一来源
  取 flip_map,昨夜中性当日转多时择时票升看多。第二十八批加一例。
- 登记 PMS_CONSENSUS_WEAK_CONFIRM(默认开):合议方向看多但只有一方表态时
  交人,关掉则放行。consensus.decide 加 weak_confirm 参数。第二十八批加两例。
- 第二十四批时钟用例改相对当前时刻构造陈旧告警,任何时刻跑都过(原来写死
  今天09:35,开发机09:36前跑年龄算成负数)。

开发机全量单测846例 ALL SUITES PASS。未在155部署(收盘后随观察读数包一起)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-14 10:32:54 +08:00
parent f2d6b37fd2
commit e21b73b46d
8 changed files with 93 additions and 15 deletions

View File

@ -16,7 +16,7 @@ def _reason(fund, tech, timing, direction, strength):
return head + "" + direction + (("·" + strength) if strength else "") return head + "" + direction + (("·" + strength) if strength else "")
def _route(fund, tech, timing, direction, tech_phase, fund_required): def _route(fund, tech, timing, direction, tech_phase, fund_required, weak_confirm=True):
"""附录丙路由表, 从上到下第一个命中。返回 (路由, 原因)。路由取值: 放行/观察/交人/跳过。""" """附录丙路由表, 从上到下第一个命中。返回 (路由, 原因)。路由取值: 放行/观察/交人/跳过。"""
if fund_required and fund == _NA: if fund_required and fund == _NA:
return "跳过", "没有买方评析" return "跳过", "没有买方评析"
@ -33,16 +33,21 @@ def _route(fund, tech, timing, direction, tech_phase, fund_required):
if timing == _BEAR: # 异议在择时系统 if timing == _BEAR: # 异议在择时系统
return "交人", "方向看多但择时系统有异议" return "交人", "方向看多但择时系统有异议"
if len(voiced) <= 1: # 只有一方表态 if len(voiced) <= 1: # 只有一方表态
# 弱表态: weak_confirm 开着交人, 关掉放行 (2026-09-14 加固, 登记 PMS_CONSENSUS_WEAK_CONFIRM)
if weak_confirm:
return "交人", "方向看多但只有一方表态" return "交人", "方向看多但只有一方表态"
return "放行", "方向看多但只有一方表态(弱表态放行)"
return "放行", "方向看多" # 看多强, 或异议只在技术面 return "放行", "方向看多" # 看多强, 或异议只在技术面
if fund == _NA and tech == _NA and timing == _NA: if fund == _NA and tech == _NA and timing == _NA:
return "放行", "三方都无读数,机械方案" return "放行", "三方都无读数,机械方案"
return "观察", "方向中性" return "观察", "方向中性"
def decide(fund, tech, timing, *, tech_phase=None, fund_required=True) -> dict: def decide(fund, tech, timing, *, tech_phase=None, fund_required=True, weak_confirm=True) -> dict:
"""fund/tech/timing 是三个立场字符串 (看多/看空/中性/无读数)。tech_phase 是技术面相位 """fund/tech/timing 是三个立场字符串 (看多/看空/中性/无读数)。tech_phase 是技术面相位
(收口等待 ), 路由用fund_required: 无买方评析是否拦 (开关 PMS_FUND_REQUIRED)""" (收口等待 ), 路由用fund_required: 无买方评析是否拦 (开关 PMS_FUND_REQUIRED)
weak_confirm: 方向看多但只有一方表态时是否交人 (开关 PMS_CONSENSUS_WEAK_CONFIRM, 默认真;
关掉则放行)"""
votes = {"fund": fund, "tech": tech, "timing": timing} votes = {"fund": fund, "tech": tech, "timing": timing}
voiced = [v for v in (fund, tech, timing) if v in (_BULL, _BEAR)] voiced = [v for v in (fund, tech, timing) if v in (_BULL, _BEAR)]
bulls = sum(1 for v in voiced if v == _BULL) bulls = sum(1 for v in voiced if v == _BULL)
@ -66,7 +71,8 @@ def decide(fund, tech, timing, *, tech_phase=None, fund_required=True) -> dict:
else: else:
strength = "" strength = ""
route, route_reason = _route(fund, tech, timing, direction, tech_phase, fund_required) route, route_reason = _route(fund, tech, timing, direction, tech_phase, fund_required,
weak_confirm)
return {"direction": direction, "votes": votes, "strength": strength, return {"direction": direction, "votes": votes, "strength": strength,
"reason": _reason(fund, tech, timing, direction, strength), "reason": _reason(fund, tech, timing, direction, strength),
"route": route, "route_reason": route_reason} "route": route, "route_reason": route_reason}

View File

@ -27,6 +27,7 @@ def _params() -> dict:
"fund_stale_days": gi("PMS_FUND_STALE_DAYS", 120), "fund_stale_days": gi("PMS_FUND_STALE_DAYS", 120),
"fund_consensus_good_min": gi("PMS_FUND_CONSENSUS_GOOD_MIN", 8), "fund_consensus_good_min": gi("PMS_FUND_CONSENSUS_GOOD_MIN", 8),
"consensus_route": gb("PMS_CONSENSUS_ROUTE", True), "consensus_route": gb("PMS_CONSENSUS_ROUTE", True),
"weak_confirm": gb("PMS_CONSENSUS_WEAK_CONFIRM", True),
"timing_stale_tdays": gi("PMS_REF_STALE_TDAYS", 3), "timing_stale_tdays": gi("PMS_REF_STALE_TDAYS", 3),
} }
@ -72,7 +73,8 @@ def assemble(row, *, nightly=None, tech_states=None, flip_at=None, params=None)
# 合议参与分流的开关关掉时, 基本面无读数不由合议这层拦 (回到只按判决分流) # 合议参与分流的开关关掉时, 基本面无读数不由合议这层拦 (回到只按判决分流)
con = consensus.decide(fund["stance"], tech.get("stance") or "无读数", tm["stance"], con = consensus.decide(fund["stance"], tech.get("stance") or "无读数", tm["stance"],
tech_phase=tech.get("phase"), tech_phase=tech.get("phase"),
fund_required=(p["fund_required"] and p["consensus_route"])) fund_required=(p["fund_required"] and p["consensus_route"]),
weak_confirm=p.get("weak_confirm", True))
return {"fund": fund, "tech": tech, "timing": tm, "consensus": con} return {"fund": fund, "tech": tech, "timing": tm, "consensus": con}

View File

@ -112,6 +112,8 @@ RUNTIME_EXTRA = {
"PMS_FUND_STALE_DAYS": (120, int, "买方评析报告超过这么多天算无读数"), "PMS_FUND_STALE_DAYS": (120, int, "买方评析报告超过这么多天算无读数"),
"PMS_FUND_CONSENSUS_GOOD_MIN": (8, int, "十视角好达这么多个且估值贵 → 打共识已定价标记, 让首批推迟"), "PMS_FUND_CONSENSUS_GOOD_MIN": (8, int, "十视角好达这么多个且估值贵 → 打共识已定价标记, 让首批推迟"),
"PMS_CONSENSUS_ROUTE": (True, bool, "三源合议参与新建仓分流 (拍板: 上线即开; 关掉即回到只按选股判决分流)"), "PMS_CONSENSUS_ROUTE": (True, bool, "三源合议参与新建仓分流 (拍板: 上线即开; 关掉即回到只按选股判决分流)"),
# 2026-09-14 加固包 (方案第六节承诺、评审发现五): 方向看多但只有一方表态时是否交人。
"PMS_CONSENSUS_WEAK_CONFIRM": (True, bool, "合议方向看多但只有一方表态时交人确认 (默认开; 关掉则弱表态也放行)"),
# ── 2026-09-11 三源合议工作包二·接入 (台账 006/009): 持仓增持门与候选按质地排序。 # ── 2026-09-11 三源合议工作包二·接入 (台账 006/009): 持仓增持门与候选按质地排序。
"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, "候选先按基本面立场分桶 (质地看多的桶排前) 再按分数, 在主题限额与截断之前生效; 技术面不参与排序 (关掉即回到只按分数排)"),

View File

@ -135,7 +135,14 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
_lmap = _lss.state_map() _lmap = _lss.state_map()
_crs = {code: (st or {}).get("company_review") _crs = {code: (st or {}).get("company_review")
for code, st in (_lmap or {}).items()} for code, st in (_lmap or {}).items()}
consensus_service.decorate_positions(view["held"], company_reviews=_crs) # 持仓行的择时票也带上当日盘中转多留痕 (2026-09-14 加固, 评审发现四): 来源与候选
# 相同 —— 决策系统判过盘中转多的留痕 (_buy_signals_today)。没这留痕的行照旧只按
# 昨夜定性。持仓行昨夜中性而当日转多时, 择时票升为看多, 增持门的方向判断才跟得上。
_sig = _buy_signals_today()
_flip = {code: (v or {}).get("at") for code, v in (_sig or {}).items()
if (v or {}).get("at")}
consensus_service.decorate_positions(view["held"], company_reviews=_crs,
flip_map=_flip)
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 读, # 技术面转空自动离场 (2026-09-11 工作包三): 给持仓行挂技术面状态供 eval_tech_exit 读,
@ -147,8 +154,7 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
_tech_states = _ts.state_map() _tech_states = _ts.state_map()
_ts.attach(view["held"], _tech_states) _ts.attach(view["held"], _tech_states)
_done = _load_tech_exit_done() _done = _load_tech_exit_done()
params["tech_exit_done"] = {c for c in _done params["tech_exit_done"] = _keep_tech_exit_done(_done, _tech_states)
if (_tech_states.get(c) or {}).get("sar_side") == ""}
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
logger.warning("[技术面离场] 挂技术面状态失败, 本轮不评转空离场: %s", e) logger.warning("[技术面离场] 挂技术面状态失败, 本轮不评转空离场: %s", e)
params["tech_exit_done"] = set() params["tech_exit_done"] = set()
@ -242,6 +248,17 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
_TECH_EXIT_DONE_KEY = "PMS_TECH_EXIT_DONE" _TECH_EXIT_DONE_KEY = "PMS_TECH_EXIT_DONE"
def _keep_tech_exit_done(done, tech_states) -> set:
"""本次翻空去重集里, 本轮该保留 (不清除) 的代码集。
2026-09-14 加固 (评审发现三): 原来是SAR 方向是空才保留, 不是空映射里没有
这只票也算了进去 停牌或决策系统那晚漏算的持仓票缺读数, 会被当成翻多而清出去重集;
若同一轮另有别的票处理了转空离场, 清过的集合会被写回, 第二天读数回来翻空仍在两天内就
再减一次 (多减三分之一)改成**只在 SAR 方向明确为多时才清除**: 翻空与缺读数一律保留"""
return {c for c in (done or set())
if (tech_states.get(c) or {}).get("sar_side") != ""}
def _load_tech_exit_done() -> set: def _load_tech_exit_done() -> set:
"""本次翻空已处理过的代码集 (跨轮去重, 不动表结构; 与 tech_state_map 同一手法用运行参数承载)。 """本次翻空已处理过的代码集 (跨轮去重, 不动表结构; 与 tech_state_map 同一手法用运行参数承载)。
读不到或坏了返回空集清除 (SAR 翻多=新一轮) scan_and_route 扫描前按当轮技术面映射做""" 读不到或坏了返回空集清除 (SAR 翻多=新一轮) scan_and_route 扫描前按当轮技术面映射做"""

View File

@ -75,7 +75,7 @@
读数陈旧按无读数/参数登记与交易日差/upsert VALUES 与按 matched 分页两个真机哨兵 (29 ) 读数陈旧按无读数/参数登记与交易日差/upsert VALUES 与按 matched 分页两个真机哨兵 (29 )
test_batch28_units.py 三源合议·工作包二纯逻辑 (2026-09-11): 基本面质地档映射与共识已定价/择时定性 test_batch28_units.py 三源合议·工作包二纯逻辑 (2026-09-11): 基本面质地档映射与共识已定价/择时定性
映射与盘中转多升级/合议多数决与强弱/附录丙路由表逐条/装配服务凑齐四块与六键/ 映射与盘中转多升级/合议多数决与强弱/附录丙路由表逐条/装配服务凑齐四块与六键/
advise_v2 基本面×技术面仓位矩阵逐格 (37 ) advise_v2 基本面×技术面仓位矩阵逐格 + 加固包弱表态开关两例与持仓行转多留痕一例 (40 )
test_batch29_units.py 三源合议·工作包二接入下单链路 (2026-09-11): scan_open 合议分流(跳过/观察带 test_batch29_units.py 三源合议·工作包二接入下单链路 (2026-09-11): scan_open 合议分流(跳过/观察带
wait_tech/交人强制确认/放行/缺合议退旧路/开关关掉逐字回旧)/定档改用 advise_v2 wait_tech/交人强制确认/放行/缺合议退旧路/开关关掉逐字回旧)/定档改用 advise_v2
且六键进硬数字而不进送研判白名单/持仓增持门矩阵与 scan 端到端(减持不受影响 且六键进硬数字而不进送研判白名单/持仓增持门矩阵与 scan 端到端(减持不受影响
@ -85,7 +85,8 @@
夹紧/开关关掉逐字回旧)/卖出优先级目标价<研究走弱<技术面转空<保垫减仓/scan 端到端/ 夹紧/开关关掉逐字回旧)/卖出优先级目标价<研究走弱<技术面转空<保垫减仓/scan 端到端/
策略票只看目标价按函数判(技术面转空对策略票不评)/跟踪止盈盘中 SAR 止损线 策略票只看目标价按函数判(技术面转空对策略票不评)/跟踪止盈盘中 SAR 止损线
(09:45 后跌破缓冲全清当日一次没刷进不判开关关掉不触发)/SAR 线刷新腿只动 (09:45 后跌破缓冲全清当日一次没刷进不判开关关掉不触发)/SAR 线刷新腿只动
自动跟踪止盈且无读数撤旧线/弱基本面试探仓紧止盈自动挂载(入场标记识别回撤3%硬目标8%带SAR线不占名额冷却让路)/参数登记 (33 ) 自动跟踪止盈且无读数撤旧线/弱基本面试探仓紧止盈自动挂载(入场标记识别回撤3%硬目标8%带SAR线不占名额冷却让路)/参数登记
+ 加固包去重集清除条件三例 (缺读数/明确多/明确空) (36 )
test_batch31_units.py 观察读数包 (2026-09-14 台账 013): 动作引擎给合议跳过项打标签 test_batch31_units.py 观察读数包 (2026-09-14 台账 013): 动作引擎给合议跳过项打标签
(跳过分 fund/bear观察 wait_tech增持门 gate_block gate_kind, (跳过分 fund/bear观察 wait_tech增持门 gate_block gate_kind,
开关关掉不打标签)/record_round 归类落表 (检查点写路由与装配两类 开关关掉不打标签)/record_round 归类落表 (检查点写路由与装配两类
@ -98,10 +99,10 @@
test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) + test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) +
目标价到价必定入队 (档位 full 也不自动卖) + 目标价到价必定入队 (档位 full 也不自动卖) +
用户设的止损价与目标价单独成列显示 (70 ) 用户设的止损价与目标价单独成列显示 (70 )
840 846
(总数按实跑逐批相加校正过两次: 曾写 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; 工作包三离场纪律第三十批十八例到 805; part3 盘中 SAR 止损线补九例到 814; part4 弱基本面紧止盈补六例到 820; 减持与研究理由两键各一例, 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 654; 又加了页面文案守卫一例, 655; 09-07 审查修复加了跨轮减持等五例, 660; 第二件低把握驳回交人一例, 661; 第三件逻辑状态接入第二十二批十五例, 676; 第四件安全边际整句透传一例, 677; 参考目标价一例, 678; 催化事件与定价状态透传一例, 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例到 805; part3 盘中 SAR 止损线补九例到 814; part4 弱基本面紧止盈补六例到 820;
09-14 观察读数包第三十一批二十例, 现为 840) 09-14 观察读数包第三十一批二十例840; 加固包给第二十八批加三例第三十批加三例, 现为 846)
任一子集失败即整体失败 (退出码 1) 任一子集失败即整体失败 (退出码 1)
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了): 哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):

View File

@ -130,7 +130,11 @@ def test_parse_sources():
assert rows[1]["src"] == "decision" and rows[1]["time"] == "10:04" and rows[1]["confidence"] == 0.95, rows[1] assert rows[1]["src"] == "decision" and rows[1]["time"] == "10:04" and rows[1]["confidence"] == 0.95, rows[1]
assert rows[1]["cat_label"] == "建议买入" and rows[1]["price"] == 43.15 assert rows[1]["cat_label"] == "建议买入" and rows[1]["price"] == 43.15
old = _today_ms(9, 35) # 陈旧告警的时刻**相对当前时刻构造** (2026-09-14 加固): 取十分钟前, 不写死 09:35 ——
# 服务器收盘后跑测试碰不上, 但开发机在 09:35 之前跑, "今天 09:35" 落在未来、年龄算成负数,
# 陈旧判定与 age_min>=0 都会失败。十分钟前在任何时刻都已过 60 秒的陈旧线, 且年龄恒非负。
old = int((time.time() - 600) * 1000)
old_hhmm = time.strftime("%H:%M", time.localtime(old / 1000))
with _Patch() as p: with _Patch() as p:
p(us, "_alert_max_age_sec", lambda: 60) p(us, "_alert_max_age_sec", lambda: 60)
al = [("1-0", {"ts_code": "601126.SH", "source": "fund_flow_strength", "level": "HIGH", al = [("1-0", {"ts_code": "601126.SH", "source": "fund_flow_strength", "level": "HIGH",
@ -139,7 +143,7 @@ def test_parse_sources():
"trigger_time": int(time.time() * 1000), "metadata": '{"direction": "down"}'})] "trigger_time": int(time.time() * 1000), "metadata": '{"direction": "down"}'})]
arows, an, _ = us._bycode_alerts(_FakeRedis({"intraday_alerts:%s" % ymd: al}), ymd, "SH601126", 5000) arows, an, _ = us._bycode_alerts(_FakeRedis({"intraday_alerts:%s" % ymd: al}), ymd, "SH601126", 5000)
assert an == 2 and len(arows) == 2 assert an == 2 and len(arows) == 2
stale = [r for r in arows if r["time"] == "09:35"][0] stale = [r for r in arows if r["time"] == old_hhmm][0]
assert stale["stale"] is True and stale["cat"] and stale["age_min"] >= 0, stale assert stale["stale"] is True and stale["cat"] and stale["age_min"] >= 0, stale
unknown = [r for r in arows if not r["stale"]][0] unknown = [r for r in arows if not r["stale"]][0]
assert unknown["cat"] == "other" and unknown["direction"] == "看跌", unknown assert unknown["cat"] == "other" and unknown["direction"] == "看跌", unknown

View File

@ -283,6 +283,32 @@ def _():
assert _av2("看多", "看多", ls="逻辑存疑")["tier"] == "不建" assert _av2("看多", "看多", ls="逻辑存疑")["tier"] == "不建"
# ================================================================ F 加固包 (2026-09-14)
@case("C 路由·弱表态开着 (weak_confirm 真): 方向看多但只有一方表态 → 交人")
def _():
r = cons.decide("看多", "无读数", "无读数", weak_confirm=True)
assert r["direction"] == "看多" and r["route"] == "交人"
@case("C 路由·弱表态关掉 (weak_confirm 假): 方向看多但只有一方表态 → 放行")
def _():
r = cons.decide("看多", "无读数", "无读数", weak_confirm=False)
assert r["direction"] == "看多" and r["route"] == "放行"
@case("D 装配·持仓行昨夜中性当日转多 → 择时票升看多 (decorate_positions 带 flip_map)")
def _():
from app.services import consensus_service as csv
from app.core import tradedays as _td
rows = [{"ts_code": "600000.SH"}]
csv.decorate_positions(
rows, nightly={"600000.SH": {"verdict": "WATCH", "trade_date": _td.ymd()}},
tech_states={}, company_reviews={"600000.SH": _cr(overall="")},
flip_map={"600000.SH": "10:30"}, params=_ASM_P)
tmb = rows[0]["consensus_blocks"]["timing"]
assert tmb["stance"] == "看多" and tmb["intraday_flip_at"] == "10:30"
def main(): def main():
ok = 0 ok = 0
for name, fn in RESULTS: for name, fn in RESULTS:

View File

@ -411,6 +411,26 @@ def _():
assert abs(ps.RUNTIME_EXTRA["PMS_TECH_TIGHT_TRAIL_TARGET"][0] - 0.08) < 1e-9 assert abs(ps.RUNTIME_EXTRA["PMS_TECH_TIGHT_TRAIL_TARGET"][0] - 0.08) < 1e-9
# ================================================================ I 去重集清除条件 (2026-09-14 加固)
# 评审发现三: 原来「SAR 不是空就清」把缺读数也当翻多清掉, 会重复减仓。改成「明确为多才清」。
@case("I 去重集·映射里没这只票则保留 (缺读数不当翻多)")
def _():
from app.services import proposal_service as ps
assert ps._keep_tech_exit_done({"600000.SH"}, {}) == {"600000.SH"}
@case("I 去重集·SAR 明确为多则清除 (新一轮翻向)")
def _():
from app.services import proposal_service as ps
assert ps._keep_tech_exit_done({"600000.SH"}, {"600000.SH": {"sar_side": ""}}) == set()
@case("I 去重集·SAR 明确为空则保留 (同一翻空不重复处理)")
def _():
from app.services import proposal_service as ps
assert ps._keep_tech_exit_done({"600000.SH"}, {"600000.SH": {"sar_side": ""}}) == {"600000.SH"}
def main(): def main():
ok = 0 ok = 0
for name, fn in RESULTS: for name, fn in RESULTS: