tradingSystem/scripts/test_batch14_units.py

632 lines
28 KiB
Python
Raw Normal View History

2026-08-19 10:04:20 +08:00
# -*- 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()