tradingSystem/scripts/test_batch12_units.py

551 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
第十二批模块单测 (实机运行, 零外部依赖, 不连库)
==================================================
运行: 在 tradingSystem 仓库根目录执行 python scripts/test_batch12_units.py
覆盖: 自主新建仓 (动作引擎 OPEN) 的选票、数量、名额与金额的滚动扣减、行业集中度滚动;
研判请求的硬数字裁剪 (仓位数字不送决策系统);
参考位漂移的当日首答锁定与偏离即停;
研判的单轮时间预算。
约定同前: 全过输出 "ALL PASS (n cases)" 退出码 0。
"""
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 exec_advisor as ea # noqa: E402
from app.services import judge as jd # 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 params(**kw):
"""action_engine 那侧要的参数快照 (口径同 param_store.sizing_params)。"""
p = {"scale": 2_000_000.0, "stock_target_default": 0.06,
"batch_split": (0.5, 0.25, 0.25), "min_lot_merge": True}
p.update(kw)
return p
def caps(**kw):
"""portfolio.caps_ctx 的产出形状 (组合快照)。"""
c = {"scale": 2_000_000.0, "portfolio_cap": 0.70, "stock_cap": 0.08,
"max_names": 20, "portfolio_mv": 0.0, "names_count": 0,
"stock_mv": 0.0, "is_new_name": False, "sector": None,
"sector_names": 0, "sector_mv": 0.0,
"sector_names_map": {}, "sector_mv_map": {},
"sector_max_names": 4, "sector_max_ratio": 0.40,
"cash_reserve": 0.0, "sector_source_ready": True,
"cash_avail": None, "cash_source": "estimate"}
c.update(kw)
return c
def cand(code, price=10.0, score=100.0, sector=None, rank=1, **kw):
d = {"ts_code": code, "price": price, "score": score, "sector": sector,
"theme": kw.pop("theme", None), "tier": kw.pop("tier", "强传导"),
"upside": kw.pop("upside", 0.3), "heat": kw.pop("heat", 0.5),
"rank": rank, "bucket": "main", "src": "plan_api"}
d.update(kw)
return d
class _PatchParams:
"""把 param_store 的取值函数临时换成一份字典 —— 单测不连库。"""
def __init__(self, values):
self.values = values
self.saved = {}
def __enter__(self):
for name in ("get", "get_int", "get_float", "get_bool"):
self.saved[name] = getattr(ps, name)
v = self.values
ps.get = lambda k, d=None: v.get(k, d)
ps.get_int = lambda k, d=0: int(v.get(k, d))
ps.get_float = lambda k, d=0.0: float(v.get(k, d))
ps.get_bool = lambda k, d=False: bool(v.get(k, d))
return self
def __exit__(self, *a):
for name, fn in self.saved.items():
setattr(ps, name, fn)
return False
# ================================================================ 动作引擎: 新建仓
@case("新建仓·正常产出: 只提底仓批, 数量按 50% 取整到一手")
def _():
# 目标 6% × 200 万 = 12 万; 底仓 50% = 6 万; 现价 10 元 → 6000 股
r = ae.scan_open(candidates=[cand("600000.SH")], params=params(), caps=caps(),
room_amt=1_400_000, slots=20)
assert len(r["candidates"]) == 1, r
c = r["candidates"][0]
assert c["action"] == "OPEN" and c["side"] == "buy", c
assert c["qty"] == 6000, c
assert c["judge_required"] is True, "OPEN 必须有资格送研判闸"
assert c["needs_user_confirm"] is False, "新建仓不强制人工确认 (档位说了算)"
# 现价与行业必须挂在候选上 —— 新票在账本里没有行, 服务层取不到这两样
assert c["price"] == 10.0 and "sector" in c, c
hn = c["hard_numbers"]
assert hn["score"] == 100.0 and hn["price"] == 10.0, hn
assert hn["target_amount"] == 120000.0, hn # 三批取整后正好 12 万
assert hn["base_amount"] == 60000.0, hn
@case("新建仓·没有名额时一条不产, 且说得出为什么")
def _():
r = ae.scan_open(candidates=[cand("600000.SH")], params=params(),
caps=caps(names_count=20), room_amt=1_400_000, slots=0)
assert r["candidates"] == [], r
assert "名额" in r["skipped"][0]["why"], r["skipped"]
@case("新建仓·没有可投金额时一条不产, 且说得出为什么")
def _():
r = ae.scan_open(candidates=[cand("600000.SH")], params=params(), caps=caps(),
room_amt=0, slots=20)
assert r["candidates"] == [], r
assert "可投金额" in r["skipped"][0]["why"], r["skipped"]
@case("新建仓·取不到实时价整只跳过并留痕 (昨收不能拿来下单)")
def _():
r = ae.scan_open(candidates=[cand("600000.SH", price=None)], params=params(),
caps=caps(), room_amt=1_400_000, slots=20)
assert r["candidates"] == [], r
assert "实时价" in r["skipped"][0]["why"], r["skipped"]
@case("新建仓·底仓批买不足一手 → 不提, 原因是一手不是别的")
def _():
# 目标 6% × 10 万 = 6000 元; 现价 500 元一手就要 5 万 → 拆不出
r = ae.scan_open(candidates=[cand("600000.SH", price=500.0)],
params=params(scale=100_000.0), caps=caps(scale=100_000.0),
room_amt=70_000, slots=20)
assert r["candidates"] == [], r
assert "一手" in r["skipped"][0]["why"], r["skipped"]
@case("新建仓·名额边走边扣: 只剩一个名额就只产一条, 其余留到下一跳")
def _():
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(names_count=19),
room_amt=1_400_000, slots=1)
assert [c["ts_code"] for c in r["candidates"]] == ["600000.SH"], r["candidates"]
whys = [s["why"] for s in r["skipped"]]
assert any("名额已用完" in w for w in whys), whys
@case("新建仓·金额边走边扣: 三条候选只够两条, 第三条被金额挡下")
def _():
# 每条整只目标 12 万; 可投 25 万 → 两条 (24 万) 之后只剩 1 万, 不够一整只
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=250_000, slots=20)
assert [c["ts_code"] for c in r["candidates"]] == ["600000.SH", "600001.SH"], r["candidates"]
whys = " ".join(s["why"] for s in r["skipped"])
assert "不开半截新仓" in whys, whys
@case("新建仓·钱不够一整只就不开 (不拿名额去换零头仓位)")
def _():
# 目标 12 万, 只剩 5 万 —— 拆得出一手批次, 但那是个补不到目标的零头仓位
r = ae.scan_open(candidates=[cand("600000.SH")], params=params(), caps=caps(),
room_amt=50_000, slots=20)
assert r["candidates"] == [], r
assert "不开半截新仓" in r["skipped"][0]["why"], r["skipped"]
@case("新建仓·滚动扣减真的生效: 组合上限只容得下两条时不会三条都放出来")
def _():
# 总仓上限 70% × 200 万 = 140 万, 已有 116 万 → 只剩 24 万 = 两只
# 如果不滚动 (每条都拿同一份旧快照算), 三条会各自都通过 —— 这一例就是为它写的
cs = [cand(f"60000{i}.SH", score=300 - i, rank=i + 1) for i in range(3)]
r = ae.scan_open(candidates=cs, params=params(),
caps=caps(portfolio_mv=1_160_000, names_count=5),
room_amt=240_000, slots=20)
assert len(r["candidates"]) == 2, [c["ts_code"] for c in r["candidates"]]
@case("新建仓·行业集中度也滚动: 同行业名额只剩一个时第二只被拦")
def _():
cs = [cand("600000.SH", score=300, rank=1, sector="储能"),
cand("600001.SH", score=200, rank=2, sector="储能")]
r = ae.scan_open(candidates=cs, params=params(),
caps=caps(sector_max_names=1), room_amt=1_400_000, slots=20)
assert len(r["candidates"]) == 1, [c["ts_code"] for c in r["candidates"]]
whys = " ".join(s["why"] for s in r["skipped"])
assert "SECTOR_NAMES" in whys, whys
@case("新建仓·行业数据源没配时不误拦 (sector 一律按空处理)")
def _():
cs = [cand("600000.SH", score=300, rank=1, sector="储能"),
cand("600001.SH", score=200, rank=2, sector="储能")]
r = ae.scan_open(candidates=cs, params=params(),
caps=caps(sector_max_names=1, sector_source_ready=False),
room_amt=1_400_000, slots=20)
assert len(r["candidates"]) == 2, [c["ts_code"] for c in r["candidates"]]
@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["candidates"] == [], r
assert "在途" in r["skipped"][0]["why"], r["skipped"]
@case("新建仓·单票评估异常不拖垮整轮, 后面的票照常评")
def _():
orig = ae.eval_open
def boom(c, *a, **kw):
if c["ts_code"] == "600000.SH":
raise RuntimeError("求值炸了")
return orig(c, *a, **kw)
ae.eval_open = boom
try:
cs = [cand("600000.SH", score=300, rank=1), cand("600001.SH", score=200, rank=2)]
r = ae.scan_open(candidates=cs, params=params(), caps=caps(),
room_amt=1_400_000, slots=20)
finally:
ae.eval_open = orig
assert [c["ts_code"] for c in r["candidates"]] == ["600001.SH"], r["candidates"]
whys = " ".join(s["why"] for s in r["skipped"])
assert "评估异常" in whys and "RuntimeError" in whys, whys
@case("转多信号·插队排最前: 分数低但有信号的票抢在前面")
def _():
cs = [cand("600000.SH", score=300, rank=1),
cand("600001.SH", score=200, rank=2),
dict(cand("600002.SH", score=100, rank=3), sig_buy={"reason": "盘中判转多"})]
r = ae.scan_open(candidates=cs, params=params(), caps=caps(names_count=19),
room_amt=1_400_000, slots=1)
assert [c["ts_code"] for c in r["candidates"]] == ["600002.SH"], r["candidates"]
c = r["candidates"][0]
assert c["hard_numbers"]["intraday_buy_signal"] is True, c["hard_numbers"]
assert c["hard_numbers"]["intraday_buy_reason"] == "盘中判转多"
assert "转多" in c["reason"], c["reason"]
@case("转多信号·开关关掉就退回纯分数排序")
def _():
cs = [cand("600000.SH", score=300, rank=1),
dict(cand("600002.SH", score=100, rank=3), sig_buy={"reason": "盘中判转多"})]
r = ae.scan_open(candidates=cs, params=params(open_signal_priority=False),
caps=caps(names_count=19), room_amt=1_400_000, slots=1)
assert [c["ts_code"] for c in r["candidates"]] == ["600000.SH"], r["candidates"]
@case("转多信号·只改先后不改资格: 没信号的票照样能建, 有信号也过同样的闸")
def _():
# 有信号但组合没名额 —— 一样建不了, 信号不是免死金牌
r = ae.scan_open(candidates=[dict(cand("600002.SH"), sig_buy={"reason": "转多"})],
params=params(), caps=caps(names_count=20), room_amt=1_400_000, slots=0)
assert r["candidates"] == [], r
# 没信号的票照常建
r2 = ae.scan_open(candidates=[cand("600000.SH")], params=params(), caps=caps(),
room_amt=1_400_000, slots=20)
assert len(r2["candidates"]) == 1
assert r2["candidates"][0]["hard_numbers"]["intraday_buy_signal"] is False
@case("信号消化·未持仓票的买入信号也要留痕 (从前它连账本都没有)")
def _():
from app.core import signal_rules as sr
sig = {"source": "intraday", "ts_code": "600002.SH", "action": "BUY",
"confidence": 0.95, "suggested_price": 12.3, "reason": "放量突破"}
d = sr.digest(sig, None, {"sell_conf_min": 0.75, "auto_exit_conf": 0.85})
assert d["action"] == sr.ACT_NOTE_BUY, d
assert d["qty"] == 0, "留痕不产生任何数量"
assert "无持仓" in d["reason"] and "只留痕" in d["reason"], d["reason"]
assert d["hard_numbers"]["suggested_price"] == 12.3
@case("信号消化·持仓票的买入信号同样走留痕, 且标明有持仓")
def _():
from app.core import signal_rules as sr
sig = {"source": "intraday", "ts_code": "600000.SH", "action": "BUY", "confidence": 0.95}
d = sr.digest(sig, {"total_qty": 1000}, {})
assert d["action"] == sr.ACT_NOTE_BUY and d["hard_numbers"]["held"] == 1000, d
assert "有持仓" in d["reason"], d["reason"]
@case("信号消化·卖出与 HOLD 的口径一个字没变")
def _():
from app.core import signal_rules as sr
prm = {"sell_conf_min": 0.75, "auto_exit_conf": 0.85, "trim_ratio": 1 / 3}
# HOLD 仍走老的 RECORD
d = sr.digest({"source": "intraday", "ts_code": "600000.SH", "action": "HOLD"}, None, prm)
assert d["action"] == sr.ACT_RECORD, d
# 高置信卖出仍是清仓
d = sr.digest({"source": "risk_sell", "ts_code": "600000.SH", "action": "SELL",
"confidence": 0.9}, {"total_qty": 1000, "avail_qty": 1000}, prm)
assert d["action"] == sr.ACT_EXIT and d["qty"] == 1000, d
# 未持有的卖出信号仍然忽略
d = sr.digest({"source": "risk_sell", "ts_code": "600000.SH", "action": "SELL",
"confidence": 0.9}, None, prm)
assert d["action"] == sr.ACT_IGNORE, d
@case("研判请求·转多信号是定性材料, 要送过去; 仓位数字照旧不送")
def _():
hard = {"price": 10.0, "score": 300.0, "intraday_buy_signal": True,
"intraday_buy_reason": "放量突破", "target_amount": 120000.0, "names_before": 3}
got = jd._judge_hard_numbers("OPEN", hard)
assert got["intraday_buy_signal"] is True and got["intraday_buy_reason"] == "放量突破"
assert "target_amount" not in got and "names_before" not in got, got
@case("回归·既有四类动作的判据与研判范围没被动过")
def _():
assert ae.JUDGE_ACTIONS == {"FILL", "ADD", "DCA", "OPEN"}, ae.JUDGE_ACTIONS
assert "TRIM" not in ae.JUDGE_ACTIONS, "减持永远不送研判"
assert [a for a, _ in ae.EVALUATORS] == ["TRIM", "ADD", "FILL", "DCA"]
# 持仓那条扫描的输入与产出一个字没改: 空持仓进去, 空候选出来, 不会去碰候选池
r = ae.scan(positions=[], params=params(), market={})
assert r == {"candidates": [], "skipped": []}, r
# ================================================================ 研判请求的硬数字裁剪
@case("研判请求·新建仓只送定性材料, 仓位数字一个不送")
def _():
hard = {"price": 10.0, "score": 300.0, "theme": "储能", "tier": "强传导",
"upside": 0.3, "heat": 0.5, "plan_rank": 1, "plan_bucket": "main",
"plan_src": "plan_api", "sector": "电力设备",
# 下面这些是仓位口径, 决策系统本来就不管仓位
"target_pct": 0.06, "target_amount": 120000.0, "base_amount": 60000.0,
"batch_scheme": "0.5,0.25,0.25", "names_before": 3, "max_names": 20,
"room_amt_before": 1_400_000.0}
got = jd._judge_hard_numbers("OPEN", hard)
assert set(got) <= set(jd.OPEN_JUDGE_KEYS), got
assert set(got) == {k for k in hard if k in jd.OPEN_JUDGE_KEYS}, got
for k in ("target_amount", "names_before", "max_names", "room_amt_before",
"batch_scheme", "base_amount", "target_pct"):
assert k not in got, f"{k} 不该送到决策系统"
assert got["score"] == 300.0 and got["price"] == 10.0, got
@case("研判请求·加仓类原样送 (它们的硬数字本身就是判据)")
def _():
hard = {"cushion_pct": -0.085, "stage": 1, "base_qty": 1000}
for act in ("FILL", "ADD", "DCA", "SWITCH"):
assert jd._judge_hard_numbers(act, hard) == hard, act
assert jd._judge_hard_numbers("DCA", None) == {}
# ================================================================ 参考位漂移
@case("参考位漂移·只管新建仓, 别的动作一律不比对")
def _():
with _PatchParams({"PMS_OPEN_REF_DRIFT_MAX": 0.03}):
prog = {"ref_lock": {"ymd": 20260806, "support": 31.27, "pressure": 40.0}}
adv = {"support": 29.00, "pressure": 40.0}
assert ea._check_ref_drift(action="FILL", prog=prog, adv=adv, today=20260806) == ""
assert ea._check_ref_drift(action="ADD", prog=prog, adv=adv, today=20260806) == ""
assert ea._check_ref_drift(action=None, prog=prog, adv=adv, today=20260806) == ""
@case("参考位漂移·当日首答只锁定不比对")
def _():
with _PatchParams({"PMS_OPEN_REF_DRIFT_MAX": 0.03}):
prog = {}
adv = {"support": 31.27, "pressure": 40.0, "buy_band": [30.96, 33.89],
"consulted_at": "09:35"}
assert ea._check_ref_drift(action="OPEN", prog=prog, adv=adv, today=20260806) == ""
assert prog["ref_lock"]["ymd"] == 20260806
assert prog["ref_lock"]["support"] == 31.27
assert prog["ref_lock"]["at"] == "09:35"
@case("参考位漂移·没漂过阈值就放行")
def _():
with _PatchParams({"PMS_OPEN_REF_DRIFT_MAX": 0.03}):
prog = {"ref_lock": {"ymd": 20260806, "support": 31.27, "pressure": 40.0}}
adv = {"support": 31.50, "pressure": 40.0} # 差 0.7%
assert ea._check_ref_drift(action="OPEN", prog=prog, adv=adv, today=20260806) == ""
@case("参考位漂移·漂过阈值就报出来 (002335 实证: 支撑 31.27→29.00)")
def _():
with _PatchParams({"PMS_OPEN_REF_DRIFT_MAX": 0.03}):
prog = {"ref_lock": {"ymd": 20260806, "support": 31.27, "pressure": 40.0,
"at": "09:35"}}
adv = {"support": 29.00, "pressure": 40.0}
why = ea._check_ref_drift(action="OPEN", prog=prog, adv=adv, today=20260806)
assert why, "漂了 7.3% 必须报出来"
assert "31.27" in why and "29.0" in why, why
assert "当日暂停" in why, why
@case("参考位漂移·压力位单独漂也算 (000035 实证: 压力 5.2→5.15 未过阈值)")
def _():
with _PatchParams({"PMS_OPEN_REF_DRIFT_MAX": 0.03}):
prog = {"ref_lock": {"ymd": 20260806, "support": 5.0, "pressure": 5.2}}
assert ea._check_ref_drift(action="OPEN", prog=prog,
adv={"support": 5.0, "pressure": 5.15},
today=20260806) == "" # 差 0.96%, 不到阈值
why = ea._check_ref_drift(action="OPEN", prog=prog,
adv={"support": 5.0, "pressure": 4.5},
today=20260806) # 差 13.5%
assert "压力" in why, why
@case("参考位漂移·锁按日重置, 次日的新结论不算漂移")
def _():
with _PatchParams({"PMS_OPEN_REF_DRIFT_MAX": 0.03}):
prog = {"ref_lock": {"ymd": 20260806, "support": 31.27, "pressure": 40.0}}
adv = {"support": 29.00, "pressure": 40.0, "consulted_at": "09:31"}
assert ea._check_ref_drift(action="OPEN", prog=prog, adv=adv, today=20260807) == ""
assert prog["ref_lock"]["ymd"] == 20260807, "次日首答应该重新锁定"
assert prog["ref_lock"]["support"] == 29.00
@case("参考位漂移·阈值设成 0 等于关掉这道防护")
def _():
with _PatchParams({"PMS_OPEN_REF_DRIFT_MAX": 0.0}):
prog = {"ref_lock": {"ymd": 20260806, "support": 31.27, "pressure": 40.0}}
adv = {"support": 1.0, "pressure": 40.0}
assert ea._check_ref_drift(action="OPEN", prog=prog, adv=adv, today=20260806) == ""
@case("参考位漂移·拿不到支撑压力时不锁也不误报")
def _():
with _PatchParams({"PMS_OPEN_REF_DRIFT_MAX": 0.03}):
prog = {}
assert ea._check_ref_drift(action="OPEN", prog=prog,
adv={"support": None, "pressure": None},
today=20260806) == ""
assert "ref_lock" not in prog, "没有位就不该锁一份空的进去"
# ================================================================ 窗口末日的强制完成
@case("窗口末日·命令驱动照旧强制完成 (用户下过命令, 到期必须完成)")
def _():
from app.core import exec_timing as et
prm = {"buy_halt_dayup": 0.05, "eod_force_time": "14:45", "eod_force_discount": 0.998}
d = {"price": 10.0, "day_chg_from_open": 0.01, "bars": 60}
r = et.hard_gate(side="buy", now="14:46", day=d, params=prm,
is_last_day=True, is_command=True, quota=1000)
assert r["action"] == et.ACT_FIRE and r["forced"] is True, r
assert r["limit_price"] == 10.02, r # 10 × 1.002
@case("窗口末日·自主买入不强制追买, 到期作废 (与 window_verdict 的口径对齐)")
def _():
from app.core import exec_timing as et
prm = {"buy_halt_dayup": 0.05, "eod_force_time": "14:45", "eod_force_discount": 0.998}
d = {"price": 10.0, "day_chg_from_open": 0.01, "bars": 60}
r = et.hard_gate(side="buy", now="14:46", day=d, params=prm,
is_last_day=True, is_command=False, quota=1000)
assert r["action"] == et.ACT_WAIT and not r["forced"], r
assert "不追买" in r["reason"] and "到期作废" in r["reason"], r["reason"]
# 与窗口收口那侧说的是同一件事: 自主类到期作废
assert et.window_verdict(remaining_qty=500, tdays_left=0,
is_command=False)["verdict"] == "EXPIRED"
@case("窗口末日·非末日两侧行为一致 (都是顺延次日)")
def _():
from app.core import exec_timing as et
prm = {"buy_halt_dayup": 0.05, "eod_force_time": "14:45", "eod_force_discount": 0.998}
d = {"price": 10.0, "day_chg_from_open": 0.01, "bars": 60}
for is_cmd in (True, False):
r = et.hard_gate(side="buy", now="14:46", day=d, params=prm,
is_last_day=False, is_command=is_cmd, quota=1000)
assert r["action"] == et.ACT_WAIT and "顺延次日" in r["reason"], (is_cmd, r)
@case("窗口末日·卖出侧一个字没改: 两种来源都照常兜底 (宁可买不上, 不能卖不掉)")
def _():
from app.core import exec_timing as et
prm = {"sell_avoid_open_min": 30, "eod_force_time": "14:45", "eod_force_discount": 0.998}
d = {"price": 10.0, "day_chg_from_open": -0.01, "bars": 60}
for is_cmd in (True, False):
r = et.hard_gate(side="sell", now="14:46", day=d, params=prm,
is_last_day=True, is_command=is_cmd, quota=1000)
assert r["action"] == et.ACT_FIRE and r["forced"] is True, (is_cmd, r)
assert r["limit_price"] == 9.98, r # 10 × 0.998
@case("窗口末日·hard_gate 的 is_command 是必填的 (漏传立刻炸, 不许静默按某一侧走)")
def _():
from app.core import exec_timing as et
try:
et.hard_gate(side="buy", now="14:46", day={"price": 10.0}, params={},
is_last_day=True, quota=1000)
except TypeError:
pass
else:
raise AssertionError("漏传 is_command 必须抛 TypeError")
@case("窗口末日·executor 真的把 is_command 传下去了 (签名对得上)")
def _():
import inspect
from app.services import exec_advisor as ea
from app.core import exec_timing as et
assert "is_command" in inspect.signature(ea.decide).parameters
assert "is_command" in inspect.signature(et.hard_gate).parameters
assert "is_command" in inspect.signature(et.decide).parameters
src = inspect.getsource(__import__("app.services.executor", fromlist=["x"]).run_tick)
assert "is_command=bool(prog.get(\"is_command\"))" in src, \
"executor.run_tick 必须显式把指令的 is_command 传给择时, 否则这道分岔形同虚设"
# ================================================================ 研判的单轮时间预算
@case("研判预算·没设预算就一律放行")
def _():
from app.services import proposal_service as prs
assert prs._judge_budget_left(None) is True
@case("研判预算·够一次就放行, 不够就留到下一跳")
def _():
import time
from app.services import proposal_service as prs
with _PatchParams({"PMS_JUDGE_TIMEOUT": 90}):
assert prs._judge_budget_left(time.monotonic() + 150) is True # 150 > 90
assert prs._judge_budget_left(time.monotonic() + 30) is False # 30 < 90
assert prs._judge_budget_left(time.monotonic() - 1) is False # 已经超了
# ================================================================ 跑
def main():
passed, failed = 0, []
for name, fn in RESULTS:
try:
fn()
passed += 1
print(f"{name}")
except Exception as e:
failed.append((name, e))
print(f"{name}: {type(e).__name__}: {e}")
traceback.print_exc()
print()
if failed:
print(f"FAILED {len(failed)}/{len(RESULTS)}")
sys.exit(1)
print(f"ALL PASS ({passed} cases)")
if __name__ == "__main__":
main()