# -*- coding: utf-8 -*- """ 第十四批模块单测 (宏观择时层, 零外部依赖) ========================================== 运行: 在 tradingSystem 仓库根目录执行 python scripts/test_batch14_units.py 覆盖 (MACRO_TIMING_PLAN.md V3; 参数依据 MACRO_CALIB_2026-08-18.md): * 指数计算: 四步公式对照手算值 / 汇率 +1 自然日 as-of 对齐 / 样本不足与停更守卫; * 区域判定: 进出迟滞 / 带内横跳不切区 / 不可用与恢复; * 周期与对数映射: streak 与峰值推进 / 同深度不重复加码 / 首步保底 / 封顶 / 周期清零; * 分方向触发: 偏冷进区即动 / 偏热回落再动按峰值深度 / 直接翻区不动作; * 让路: 在途 / 冷却 / 暂停买入 / 刹车 / 地板天花板; * 服务层 (打桩): 全自动下命令与冲突让路 / 建议与采纳幂等 / 闸状态读写与陈旧失效 / proposal_service 的买入侧过滤留 TRIM。 """ import json import math import os import sys import traceback sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.core import macro_rules as mr # noqa: E402 RESULTS = [] def case(name): def deco(fn): RESULTS.append((name, fn)) return fn return deco P = {"hot_th": 25.0, "cold_th": -20.0, "exit_band": 15.0, "confirm_days": 1, "trigger_hot": "zone_exit", "trigger_cold": "zone_enter", "log_s0": 0.049, "log_k": 1.6, "shift_max": 0.20, "step_min": 0.02, "min_pct": 0.20, "max_pct": 0.0, "respect_brake": True, "ret_win": 20, "z_win": 40, "beta": 0.02, "stale_tdays": 3} F0 = {"portfolio_pct": 0.50, "portfolio_cap": 0.60, "buy_halt": False, "brake_active": False, "inflight": False, "cooldown_left_reduce": 0, "cooldown_left_increase": 0} # ================================================================ 计算 def _mkdays(n, start=20240102): """生成 n 个工作日 ymd (跳过周末, 够单测用)。""" from datetime import date, timedelta d = date(2024, 1, 2) out = [] while len(out) < n: if d.weekday() < 5: out.append(int(d.strftime("%Y%m%d"))) d += timedelta(days=1) return out @case("计算·四步公式对照手算值 (常数汇率利率, 股指恒定日涨)") def _(): days = _mkdays(70) zs = [(d, 3000.0 * math.exp(0.001 * i)) for i, d in enumerate(days)] fx = [(d, 7.2) for d in days] sh = [(d, 1.9) for d in days] r = mr.compute_hedge_index({"zs": zs, "fx": fx, "shibor": sh}, P, days[-1]) # 股 20 日收益恒 0.02, 汇 0, 利率变化 0 → spread_adj 恒 0.02 → 标准差为零 → 不可用 assert not r["ok"] and "标准差" in r["reason"], r @case("计算·有波动时值与手算 z 一致") def _(): days = _mkdays(75) zs, px = [], 3000.0 for i, d in enumerate(days): px *= math.exp(0.002 if i % 2 == 0 else -0.001) zs.append((d, px)) fx = [(d, 7.2 + (0.01 if i % 3 == 0 else 0.0)) for i, d in enumerate(days)] sh = [(d, 1.9) for d in days] r = mr.compute_hedge_index({"zs": zs, "fx": fx, "shibor": sh}, P, days[-1]) assert r["ok"], r # 手算: 复算 spread_adj 序列与末点 z×10 closes = [v for _, v in zs] fx_al, _f = mr.asof_align(days, fx, shift_days=1) sh_al, _s = mr.asof_align(days, sh, shift_days=0) sr_ = mr.log_rets(closes, 20) fr_ = mr.log_rets(fx_al, 20) sd_ = mr.diffs(sh_al, 20) adj = [None if (sr_[i] is None or fr_[i] is None or sd_[i] is None) else sr_[i] + fr_[i] - 0.02 * sd_[i] for i in range(len(days))] w = [v for v in adj[-40:] if v is not None] m = sum(w) / len(w) sd = math.sqrt(sum((x - m) ** 2 for x in w) / (len(w) - 1)) want = (adj[-1] - m) / sd * 10 assert abs(r["value"] - round(want, 4)) < 1e-6, (r["value"], want) @case("计算·汇率 +1 自然日 as-of: 周一用上周五值") def _(): tdays = [20260803, 20260804, 20260805] # 周一到周三 fx = [(20260731, 7.10), (20260803, 7.20)] # 周五 / 周一 vals, filled = mr.asof_align(tdays, fx, shift_days=1) assert vals == [7.10, 7.20, 7.20] and filled >= 2, (vals, filled) @case("计算·样本不足与数据停更都判不可用") def _(): days = _mkdays(30) zs = [(d, 3000.0 + i) for i, d in enumerate(days)] r = mr.compute_hedge_index({"zs": zs, "fx": zs, "shibor": zs}, P, days[-1]) assert not r["ok"] and "样本不足" in r["reason"], r days = _mkdays(70) zs = [(d, 3000.0 * math.exp((0.002 if i % 2 else -0.001))) for i, d in enumerate(days)] # 末日落后今天 30 个自然日 → 停更 r = mr.compute_hedge_index({"zs": zs, "fx": zs, "shibor": zs}, P, mr.ymd_plus_days(days[-1], 30)) assert not r["ok"] and "停更" in r["reason"], r # ================================================================ 区域迟滞 @case("区域·进出迟滞: 26 进热, 20/16 仍在热, 14 才离区") def _(): assert mr.zone_next(26, "NEUTRAL", P) == mr.Z_HOT assert mr.zone_next(20, mr.Z_HOT, P) == mr.Z_HOT # 带内延续 assert mr.zone_next(16, mr.Z_HOT, P) == mr.Z_HOT assert mr.zone_next(14, mr.Z_HOT, P) == mr.Z_NEUTRAL # 穿出退出带 assert mr.zone_next(24, mr.Z_NEUTRAL, P) == mr.Z_NEUTRAL # 未过进入阈值不再进 @case("区域·偏冷对称 + 直接翻区 + 不可用") def _(): assert mr.zone_next(-21, "NEUTRAL", P) == mr.Z_COLD assert mr.zone_next(-16, mr.Z_COLD, P) == mr.Z_COLD assert mr.zone_next(-14, mr.Z_COLD, P) == mr.Z_NEUTRAL assert mr.zone_next(-21, mr.Z_HOT, P) == mr.Z_COLD # 单日巨变直接翻 assert mr.zone_next(None, mr.Z_HOT, P) == mr.Z_UNAVAILABLE assert mr.zone_next(26, mr.Z_UNAVAILABLE, P) == mr.Z_HOT # 恢复后按进入规则重判 # ================================================================ 周期与映射 @case("周期·streak 与峰值推进, 退区带出峰值, 换区清零") def _(): c1, ev = mr.carry_cycle("NEUTRAL", None, mr.Z_HOT, 27.0, P, 20260801) assert c1["streak"] == 1 and abs(c1["e_peak"] - 2.0) < 1e-9 and ev is None c2, ev = mr.carry_cycle(mr.Z_HOT, c1, mr.Z_HOT, 31.0, P, 20260802) assert c2["streak"] == 2 and abs(c2["e_peak"] - 6.0) < 1e-9 and ev is None c3, ev = mr.carry_cycle(mr.Z_HOT, c2, mr.Z_NEUTRAL, 12.0, P, 20260803) assert c3 is None and ev and abs(ev["e_peak"] - 6.0) < 1e-9 c4, ev = mr.carry_cycle(mr.Z_HOT, c2, mr.Z_COLD, -22.0, P, 20260803) assert c4["zone"] == mr.Z_COLD and c4["streak"] == 1 and ev["zone"] == mr.Z_HOT @case("映射·对数目标: 标定锚点与封顶") def _(): # S0=0.049, k=1.6: e=2.9 → ≈5.1%; e=17.1 → ≈12% assert abs(mr.log_target(2.9, P) - 0.049 * math.log(1 + 2.9 / 1.6)) < 1e-9 assert 0.049 < mr.log_target(2.9, P) < 0.053 assert 0.115 < mr.log_target(17.1, P) < 0.125 assert mr.log_target(1e9, P) == 0.20 # 封顶 assert mr.log_target(0, P) == 0.0 and mr.log_target(-3, P) == 0.0 @case("决策·偏冷进区即动: 浅进区首步保底 2%") def _(): cyc = {"zone": mr.Z_COLD, "start": 1, "streak": 1, "e_peak": 0.5, "done_shift": 0.0} d = mr.decide(zone=mr.Z_COLD, cycle=cyc, exit_event=None, exit_acted=False, e_now=0.5, flags=dict(F0), params=P) assert d["action"] == mr.ACT_INCREASE and abs(d["pct"] - 0.02) < 1e-9, d @case("决策·偏冷加深才有新步, 同深度不重复加码") def _(): # 已执行 5%, 深度对应目标 ≈5% → 无新步 e = 2.9 done = mr.log_target(e, P) cyc = {"zone": mr.Z_COLD, "streak": 3, "e_peak": e, "done_shift": round(done, 4)} d = mr.decide(zone=mr.Z_COLD, cycle=cyc, exit_event=None, exit_acted=False, e_now=e, flags=dict(F0), params=P) assert d["action"] is None and not d["blocked"], d # 深度翻倍 → 目标增量超过 step_min → 出新步 d2 = mr.decide(zone=mr.Z_COLD, cycle=cyc, exit_event=None, exit_acted=False, e_now=10.0, flags=dict(F0), params=P) assert d2["action"] == mr.ACT_INCREASE \ and abs(d2["pct"] - (mr.log_target(10.0, P) - done)) < 1e-4, d2 @case("决策·确认天数: streak 不足不动") def _(): p2 = {**P, "confirm_days": 2} cyc = {"zone": mr.Z_COLD, "streak": 1, "e_peak": 3.0, "done_shift": 0.0} d = mr.decide(zone=mr.Z_COLD, cycle=cyc, exit_event=None, exit_acted=False, e_now=3.0, flags=dict(F0), params=p2) assert d["action"] is None and not d["blocked"], d @case("决策·偏热回落再动: 退区日按峰值深度一次动作, 当日已动过不重复") def _(): ev = {"zone": mr.Z_HOT, "e_peak": 6.0, "done_shift": 0.0} d = mr.decide(zone=mr.Z_NEUTRAL, cycle=None, exit_event=ev, exit_acted=False, e_now=0.0, flags=dict(F0), params=P) assert d["action"] == mr.ACT_REDUCE \ and abs(d["pct"] - mr.log_target(6.0, P)) < 1e-4, d d2 = mr.decide(zone=mr.Z_NEUTRAL, cycle=None, exit_event=ev, exit_acted=True, e_now=0.0, flags=dict(F0), params=P) assert d2["action"] is None, d2 @case("决策·偏热在区内 (zone_exit 口径) 不动手; 直接翻冷区旧周期退出不降仓") def _(): cyc = {"zone": mr.Z_HOT, "streak": 3, "e_peak": 8.0, "done_shift": 0.0} d = mr.decide(zone=mr.Z_HOT, cycle=cyc, exit_event=None, exit_acted=False, e_now=8.0, flags=dict(F0), params=P) assert d["action"] is None and not d["blocked"], d # HOT 直接翻 COLD: 退出事件不触发降仓 (新区自己的逻辑接管) ev = {"zone": mr.Z_HOT, "e_peak": 8.0} ccold = {"zone": mr.Z_COLD, "streak": 1, "e_peak": 2.0, "done_shift": 0.0} d2 = mr.decide(zone=mr.Z_COLD, cycle=ccold, exit_event=ev, exit_acted=False, e_now=2.0, flags=dict(F0), params=P) assert d2["action"] == mr.ACT_INCREASE, d2 @case("决策·偏热 zone_enter 口径 (可切): 进区步进") def _(): p2 = {**P, "trigger_hot": "zone_enter"} cyc = {"zone": mr.Z_HOT, "streak": 1, "e_peak": 3.0, "done_shift": 0.0} d = mr.decide(zone=mr.Z_HOT, cycle=cyc, exit_event=None, exit_acted=False, e_now=3.0, flags=dict(F0), params=p2) assert d["action"] == mr.ACT_REDUCE and d["pct"] > 0, d @case("决策·地板与天花板裁剪") def _(): ev = {"zone": mr.Z_HOT, "e_peak": 20.0} d = mr.decide(zone=mr.Z_NEUTRAL, cycle=None, exit_event=ev, exit_acted=False, e_now=0.0, flags={**F0, "portfolio_pct": 0.21}, params=P) assert d["action"] is None and any("地板" in b for b in d["blocked"]), d cyc = {"zone": mr.Z_COLD, "streak": 1, "e_peak": 9.0, "done_shift": 0.0} d2 = mr.decide(zone=mr.Z_COLD, cycle=cyc, exit_event=None, exit_acted=False, e_now=9.0, flags={**F0, "portfolio_pct": 0.595}, params=P) assert d2["action"] is None and any("天花板" in b for b in d2["blocked"]), d2 # 天花板裁剪但仍够一步 d3 = mr.decide(zone=mr.Z_COLD, cycle=cyc, exit_event=None, exit_acted=False, e_now=9.0, flags={**F0, "portfolio_pct": 0.55}, params=P) assert d3["action"] == mr.ACT_INCREASE and abs(d3["pct"] - 0.05) < 1e-9, d3 @case("决策·让路: 暂停买入/刹车只拦升仓, 在途与冷却双向拦") def _(): cyc = {"zone": mr.Z_COLD, "streak": 1, "e_peak": 5.0, "done_shift": 0.0} d = mr.decide(zone=mr.Z_COLD, cycle=cyc, exit_event=None, exit_acted=False, e_now=5.0, flags={**F0, "buy_halt": True}, params=P) assert d["action"] is None and any("暂停买入" in b for b in d["blocked"]), d d = mr.decide(zone=mr.Z_COLD, cycle=cyc, exit_event=None, exit_acted=False, e_now=5.0, flags={**F0, "brake_active": True}, params=P) assert d["action"] is None and any("刹车" in b for b in d["blocked"]), d d = mr.decide(zone=mr.Z_COLD, cycle=cyc, exit_event=None, exit_acted=False, e_now=5.0, flags={**F0, "brake_active": True}, params={**P, "respect_brake": False}) assert d["action"] == mr.ACT_INCREASE, d # 参数放开则不让 ev = {"zone": mr.Z_HOT, "e_peak": 6.0} d = mr.decide(zone=mr.Z_NEUTRAL, cycle=None, exit_event=ev, exit_acted=False, e_now=0.0, flags={**F0, "buy_halt": True, "brake_active": True}, params=P) assert d["action"] == mr.ACT_REDUCE, d # 降仓不受这两条限制 d = mr.decide(zone=mr.Z_NEUTRAL, cycle=None, exit_event=ev, exit_acted=False, e_now=0.0, flags={**F0, "inflight": True}, params=P) assert d["action"] is None and any("在途" in b for b in d["blocked"]), d d = mr.decide(zone=mr.Z_NEUTRAL, cycle=None, exit_event=ev, exit_acted=False, e_now=0.0, flags={**F0, "cooldown_left_reduce": 2}, params=P) assert d["action"] is None and any("冷却" in b for b in d["blocked"]), d # ================================================================ 服务层 (打桩) from app.services import macro_service as ms # noqa: E402 from app.services import param_store, proposal_service # noqa: E402 from app.repo import macro_repo, pms_repo # noqa: E402 class _Env: """打桩环境: 参数 / 信号表 / 命令服务 / 组合快照 全部进内存。""" def __init__(self, **params): self.params = {"PMS_MACRO_ENABLED": True, "PMS_MACRO_AUTONOMY": "full", "PMS_MACRO_STOCK_GATE": True, "PMS_GLOBAL_EXEC_HALT": False, "PMS_GLOBAL_BUY_HALT": False, "PMS_BRAKE_UNTIL": 0, "PMS_MACRO_COLD_TH": -20.0, **params} self.rows = {} # (key, ymd) -> row self.issued = [] self.ledger = [] self.commands = {} # command_id -> {status,...} self.issue_result = {"ok": True, "command_id": "CMD_T_0001"} self.saved = {} def install(self): env = self self._orig = (param_store.get, param_store.get_bool, param_store.get_float, param_store.get_int, param_store.get_list, param_store.set_param, macro_repo.fetch_hedge_inputs, macro_repo.upsert_signal, macro_repo.get_signal, macro_repo.recent_signals, pms_repo.insert_ledger, pms_repo.get_command) def _get(key, default=None): return env.params.get(key, ms._params.__defaults__ or default) \ if key in env.params else default param_store.get = lambda k, d=None: env.params.get(k, d) param_store.get_bool = lambda k, d=False: bool(env.params.get(k, d)) param_store.get_float = lambda k, d=0.0: float(env.params.get(k, d)) param_store.get_int = lambda k, d=0: int(env.params.get(k, d)) param_store.get_list = lambda k, d=None, sep=",": \ [x.strip() for x in str(env.params.get(k, "") or "").split(sep) if x.strip()] \ or list(d or []) def _setp(k, v, by="user"): env.saved[k] = v env.params[k] = v return {"ok": True, "key": k, "value": v} param_store.set_param = _setp macro_repo.fetch_hedge_inputs = lambda limit_days=140: env.inputs def _ups(**kw): env.rows[(kw["signal_key"], int(kw["trade_date"]))] = { "signal_key": kw["signal_key"], "trade_date": int(kw["trade_date"]), "value": kw.get("value"), "zone": kw.get("zone"), "detail": kw.get("detail") or {}, "action": kw.get("action") or "NONE", "ref_id": kw.get("ref_id") or "", "note": kw.get("note") or ""} return 1 macro_repo.upsert_signal = _ups macro_repo.get_signal = lambda k, d: env.rows.get((k, int(d))) macro_repo.recent_signals = lambda k, limit=30: sorted( [r for (kk, _), r in env.rows.items() if kk == k], key=lambda r: -r["trade_date"])[:limit] pms_repo.insert_ledger = lambda **kw: env.ledger.append(kw) or 1 pms_repo.get_command = lambda cid: env.commands.get(cid) import app.services.command_service as csvc self._orig_issue = csvc.issue def _issue(cmd_type, params, **kw): env.issued.append({"cmd_type": cmd_type, "params": params, **kw}) r = dict(env.issue_result) if r.get("ok"): env.commands[r["command_id"]] = {"command_id": r["command_id"], "cmd_type": cmd_type, "status": "EXECUTING", "progress": {}} return r csvc.issue = _issue import app.services.portfolio as pf self._orig_view = pf.positions_view pf.positions_view = lambda **kw: {"totals": {"portfolio_pct": 0.50}, "params": {"portfolio_cap": 0.60}} ms._gate_cache["at"] = 0.0 ms._gate_cache["data"] = None def uninstall(self): (param_store.get, param_store.get_bool, param_store.get_float, param_store.get_int, param_store.get_list, param_store.set_param, macro_repo.fetch_hedge_inputs, macro_repo.upsert_signal, macro_repo.get_signal, macro_repo.recent_signals, pms_repo.insert_ledger, pms_repo.get_command) = self._orig import app.services.command_service as csvc csvc.issue = self._orig_issue import app.services.portfolio as pf pf.positions_view = self._orig_view ms._gate_cache["at"] = 0.0 ms._gate_cache["data"] = None def _cold_inputs(): """构造一组会算出偏冷极值的序列: 末 20 日股指大跌 + 人民币升值。""" days = _mkdays(80) zs, px = [], 3300.0 fxs, fxv = [], 7.30 for i, d in enumerate(days): drift = 0.001 if i % 2 == 0 else -0.0005 if i >= len(days) - 15: drift = -0.012 # 尾段连续大跌 px *= math.exp(drift) zs.append((d, px)) fdrift = 0.0002 if i % 3 == 0 else 0.0 if i >= len(days) - 15: fdrift = -0.004 # 人民币升值 (USDCNH 下行) fxv *= math.exp(fdrift) fxs.append((d, fxv)) sh = [(d, 1.9 + (0.01 if i % 5 == 0 else 0.0)) for i, d in enumerate(days)] return {"zs": zs, "fx": fxs, "shibor": sh}, days[-1] @case("服务·全自动: 偏冷极值 → 下升仓命令 (署名 macro), 信号行与闸状态落表") def _(): env = _Env(PMS_MACRO_SIGNALS="hedge_fx") env.inputs, today = _cold_inputs() env.install() try: r = ms.scan(now=__import__("datetime").datetime.strptime(str(today), "%Y%m%d")) assert r["ok"], r assert env.issued and env.issued[0]["cmd_type"] == "INCREASE_EXPOSURE", env.issued assert env.issued[0]["issued_by"] == "macro" assert 0.02 <= env.issued[0]["params"]["pct"] <= 0.20 row = env.rows[("hedge_fx", today)] assert row["action"] == "CMD_ISSUED" and row["ref_id"] == "CMD_T_0001", row assert row["zone"] == mr.Z_COLD and float(row["value"]) < -20 # 周期已执行量被推进 (进区口径) assert row["detail"]["cycle"]["done_shift"] > 0, row["detail"] # 偏冷不落闸 st = json.loads(env.saved["PMS_MACRO_GATE_STATE"]) assert st["active"] is False # 评审账本有 PASS 留痕 assert any(x.get("verdict") == "PASS" for x in env.ledger) finally: env.uninstall() @case("服务·同日重扫不重复下命令; 冲突时让路留痕") def _(): env = _Env() env.inputs, today = _cold_inputs() env.install() try: now = __import__("datetime").datetime.strptime(str(today), "%Y%m%d") ms.scan(now=now) n1 = len(env.issued) ms.scan(now=now) # 重扫: 当日已下过 → 不再下 assert len(env.issued) == n1 == 1, env.issued # 冲突场景: 清空当日行, 命令服务回 CONFLICT env.rows.clear() env.issued.clear() env.ledger.clear() env.issue_result = {"ok": False, "errors": ["CONFLICT: 与在途命令冲突"], "conflicts": [{"with_cmd_type": "REDUCE_EXPOSURE"}]} r = ms.scan(now=now) row = env.rows[("hedge_fx", today)] assert row["action"] == "BLOCKED" and "CONFLICT" in row["note"], row assert any(x.get("verdict") == "REJECT" for x in env.ledger) assert r["skipped"], r finally: env.uninstall() @case("服务·只出建议档: 建议落行, 采纳幂等, 二次采纳被拒") def _(): env = _Env(PMS_MACRO_AUTONOMY="propose_only") env.inputs, today = _cold_inputs() env.install() try: now = __import__("datetime").datetime.strptime(str(today), "%Y%m%d") ms.scan(now=now) assert not env.issued row = env.rows[("hedge_fx", today)] assert row["action"] == "ADVICE_INCREASE" and row["detail"]["advice"]["pct"] > 0 # 采纳 (td.ymd() 是真实今天, 与合成日期不同 → 先把行挪到真实今天再采纳) from app.core import tradedays as td real_today = td.ymd() env.rows[("hedge_fx", real_today)] = {**row, "trade_date": real_today} r = ms.adopt("hedge_fx") assert r.get("ok") and env.issued and env.issued[0]["issued_by"] == "user", r r2 = ms.adopt("hedge_fx") assert not r2.get("ok") and "不重复" in r2.get("error", ""), r2 finally: env.uninstall() @case("服务·偏热日落闸: 闸状态写入, gate_state 读回生效; 陈旧与关档失效") def _(): env = _Env() days = _mkdays(80) # 构造偏热: 尾段股指大涨 + 人民币贬值 zs, px = [], 3000.0 fxs, fxv = [], 6.90 for i, d in enumerate(days): drift = 0.0008 if i % 2 == 0 else -0.0006 if i >= len(days) - 12: drift = 0.012 px *= math.exp(drift) zs.append((d, px)) fdrift = -0.0002 if i % 3 == 0 else 0.0 if i >= len(days) - 12: fdrift = 0.004 fxv *= math.exp(fdrift) fxs.append((d, fxv)) env.inputs = {"zs": zs, "fx": fxs, "shibor": [(d, 1.9 + (0.01 if i % 5 == 0 else 0.0)) for i, d in enumerate(days)]} env.install() try: now = __import__("datetime").datetime.strptime(str(days[-1]), "%Y%m%d") r = ms.scan(now=now) row = env.rows[("hedge_fx", days[-1])] assert row["zone"] == mr.Z_HOT, row # zone_exit 口径: 在区内不下命令 assert not env.issued st = json.loads(env.saved["PMS_MACRO_GATE_STATE"]) assert st["active"] is True and r["gate"]["active"] is True # gate_state 陈旧失效: 把 ymd 改到 30 天前 env.params["PMS_MACRO_GATE_STATE"] = json.dumps( {"active": True, "ymd": mr.ymd_plus_days(days[-1], -30)}) ms._gate_cache["at"] = 0.0 assert ms.gate_state()["active"] is False # 关总开关立即失效 env.params["PMS_MACRO_GATE_STATE"] = json.dumps( {"active": True, "ymd": __import__("app.core.tradedays", fromlist=["ymd"]).ymd()}) env.params["PMS_MACRO_ENABLED"] = False ms._gate_cache["at"] = 0.0 assert ms.gate_state()["active"] is False finally: env.uninstall() @case("服务·数据不可用: 不动作不落闸, 行记 UNAVAILABLE") def _(): env = _Env() days = _mkdays(30) env.inputs = {"zs": [(d, 3000.0 + i) for i, d in enumerate(days)], "fx": [], "shibor": []} env.install() try: now = __import__("datetime").datetime.strptime(str(days[-1]), "%Y%m%d") r = ms.scan(now=now) row = env.rows[("hedge_fx", days[-1])] assert row["zone"] == mr.Z_UNAVAILABLE and not env.issued, row st = json.loads(env.saved["PMS_MACRO_GATE_STATE"]) assert st["active"] is False assert r["ok"], r finally: env.uninstall() @case("闸·proposal_service 过滤: 拦买入侧留 TRIM; 读闸炸了按不拦") def _(): # 直接验证 scan_and_route 里那段过滤逻辑等价的行为: 用 gate_state 打桩 orig = ms.gate_state ms.gate_state = lambda: {"active": True, "why": "单测"} try: g = proposal_service._macro_gate() assert g.get("active") is True cands = [{"ts_code": "A", "action": "ADD", "side": "buy"}, {"ts_code": "B", "action": "TRIM", "side": "sell"}, {"ts_code": "C", "action": "DCA", "side": "buy"}] kept, skipped = [], [] for c in cands: # 与 scan_and_route 中的过滤同构 (skipped if c.get("side") == "buy" else kept).append(c) assert [c["ts_code"] for c in kept] == ["B"] assert len(skipped) == 2 finally: ms.gate_state = orig # 读闸抛异常 → _macro_gate 返回空 dict (不拦) ms.gate_state = lambda: (_ for _ in ()).throw(RuntimeError("boom")) try: assert proposal_service._macro_gate() == {} finally: ms.gate_state = orig @case("闸·strategy_runner 判定: 闸生效等价 buy_paused, 读不到按 False") def _(): from app.services import strategy_runner as srun orig = ms.gate_state ms.gate_state = lambda: {"active": True} try: assert srun._macro_gate_active() is True finally: ms.gate_state = orig ms.gate_state = lambda: (_ for _ in ()).throw(RuntimeError("boom")) try: assert srun._macro_gate_active() is False finally: ms.gate_state = orig @case("冷却·按信号历史行数计交易日距离, 零方案命令不占冷却") def _(): hist = [{"trade_date": 20260805, "action": "NONE", "ref_id": "", "detail": {}}, {"trade_date": 20260804, "action": "NONE", "ref_id": "", "detail": {}}, {"trade_date": 20260803, "action": "CMD_ISSUED", "ref_id": "C1", "detail": {"advice": {"direction": "REDUCE"}}}] # C1 在途 EXECUTING orig = pms_repo.get_command pms_repo.get_command = lambda cid: {"command_id": cid, "status": "EXECUTING", "cmd_type": "REDUCE_EXPOSURE"} try: inflight, last_red, last_inc = ms._action_history(hist) assert inflight and last_red == 20260803 and last_inc is None # 今天 0805 (行已在): 距 0803 过了 2 个交易日 → 冷却 3 剩 1 assert ms._cooldown_left(hist, last_red, 20260805, 3) == 1 # 今天 0806 首扫 (行未写): 距离 +1 → 剩 0 assert ms._cooldown_left(hist, last_red, 20260806, 3) == 0 # 零方案: 命令被立刻取消 → 不算生效动作 pms_repo.get_command = lambda cid: {"command_id": cid, "status": "CANCELLED", "cmd_type": "REDUCE_EXPOSURE"} inflight, last_red, _li = ms._action_history(hist) assert not inflight and last_red is None finally: pms_repo.get_command = orig @case("服务·休假模式与总开关: 扫描整体跳过") def _(): env = _Env(PMS_GLOBAL_EXEC_HALT=True) env.inputs, today = _cold_inputs() env.install() try: r = ms.scan() assert not env.issued and any("休假" in s for s in r["skipped"]), r env.params["PMS_GLOBAL_EXEC_HALT"] = False env.params["PMS_MACRO_ENABLED"] = False r2 = ms.scan() assert any("总开关" in s for s in r2["skipped"]), r2 finally: env.uninstall() # ================================================================ runner def main(): passed = failed = 0 for name, fn in RESULTS: try: fn() print(f" PASS {name}") passed += 1 except Exception: print(f" FAIL {name}") traceback.print_exc() failed += 1 print("-" * 60) if failed: print(f"FAILED: {failed} / {passed + failed}") sys.exit(1) print(f"ALL PASS ({passed} cases)") if __name__ == "__main__": main()