tradingSystem/scripts/test_wiring.py

908 lines
42 KiB
Python

# -*- coding: utf-8 -*-
"""
装配自检 (零外部依赖, 不连库)
==============================
运行: python scripts/test_wiring.py
用内存桩替换 repo/行情, 验证「服务层 → 核心逻辑 → 落表」整条链路的接线是否正确:
导入链、API 路由、调度表与守卫、单表访问守卫、交易日历、参数中心回退,
以及命令下达→方案落表→撤销、成交回放→批次入账→成本重算 两条主干流程。
"""
import os
import sys
import traceback
from datetime import date, datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
RESULTS = []
def case(name):
def deco(fn):
RESULTS.append((name, fn))
return fn
return deco
# ================================================================ 内存桩
class FakeRepo:
"""pms_repo 的内存替身 (只实现服务层用到的函数, 语义与 SQL 版一致)。"""
def __init__(self):
self.params, self.commands, self.plans = {}, {}, []
self.positions, self.lots, self.instructions = {}, [], {}
self.proposals, self.ledger, self.reports, self.industry = {}, [], {}, {}
self._lot_id = 0
# --- runtime param ---
def all_params(self):
return {k: {"param_value": v, "updated_by": "test", "updated_at": ""}
for k, v in self.params.items()}
def set_param(self, key, value, updated_by="user"):
self.params[key] = str(value)
return 1
def get_param(self, key):
return self.params.get(key)
# --- command ---
def next_command_seq(self, ymd):
return len(self.commands) + 1
def insert_command(self, **kw):
self.commands[kw["command_id"]] = {
"id": len(self.commands) + 1, "command_id": kw["command_id"],
"cmd_class": kw["cmd_class"], "cmd_type": kw["cmd_type"], "ts_code": kw["ts_code"],
"params": kw["params"], "status": kw["status"], "progress": kw.get("progress") or {},
"issued_by": kw.get("issued_by"), "issued_at": datetime.now(), "done_at": None,
"note": kw.get("note")}
return 1
def get_command(self, cid):
return self.commands.get(cid)
def list_commands(self, *, statuses=None, cmd_class=None, limit=200):
out = [c for c in self.commands.values()
if (not statuses or c["status"] in statuses)
and (not cmd_class or c["cmd_class"] == cmd_class)]
return sorted(out, key=lambda c: -c["id"])[:limit]
def update_command(self, cid, *, status=None, progress=None, done_at=None, note=None):
c = self.commands.get(cid)
if not c:
return 0
if status is not None:
c["status"] = status
if progress is not None:
c["progress"] = progress
if done_at is not None:
c["done_at"] = done_at
if note is not None:
c["note"] = note
return 1
def supersede_param_commands(self, cmd_type, ts_code=None, keep_command_id=None):
n = 0
for c in self.commands.values():
if (c["cmd_class"] == "param" and c["status"] == "EFFECTIVE"
and c["cmd_type"] == cmd_type and c["ts_code"] == ts_code
and c["command_id"] != keep_command_id):
c["status"] = "SUPERSEDED"
n += 1
return n
def latest_effective(self, cmd_type, ts_code=None):
hits = [c for c in self.commands.values()
if c["cmd_class"] == "param" and c["status"] == "EFFECTIVE"
and c["cmd_type"] == cmd_type and (not ts_code or c["ts_code"] == ts_code)]
return sorted(hits, key=lambda c: c["id"])[-1] if hits else None
def list_effective_stock_params(self, cmd_types=None):
return [c for c in self.commands.values()
if c["cmd_class"] == "param" and c["status"] == "EFFECTIVE" and c["ts_code"]
and (not cmd_types or c["cmd_type"] in cmd_types)]
# --- plan ---
def insert_plans(self, rows):
for r in rows:
d = dict(r)
d.setdefault("filled_qty", 0)
self.plans.append(d)
return len(rows)
def list_plans(self, *, command_id=None, statuses=None, ts_code=None, limit=500):
out = [p for p in self.plans
if (not command_id or p["command_id"] == command_id)
and (not statuses or p["status"] in statuses)
and (not ts_code or p["ts_code"] == ts_code)]
return sorted(out, key=lambda p: p.get("priority", 100))[:limit]
def update_plan(self, plan_id, *, status=None, filled_qty=None):
for p in self.plans:
if p["plan_id"] == plan_id:
if status:
p["status"] = status
if filled_qty is not None:
p["filled_qty"] = filled_qty
return 1
return 0
def cancel_plans_of_command(self, cid):
n = 0
for p in self.plans:
if p["command_id"] == cid and p["status"] in ("PENDING", "EXECUTING"):
p["status"] = "CANCELLED"
n += 1
return n
def set_plans_deadline(self, cid, dl):
for p in self.plans:
if p["command_id"] == cid:
p["deadline"] = dl
return 1
# --- position / lot ---
def list_positions(self, *, only_open=False):
out = list(self.positions.values())
return [p for p in out if int(p.get("total_qty") or 0) > 0] if only_open else out
def get_position(self, code):
return self.positions.get(code)
def ensure_position(self, code):
self.positions.setdefault(code, {"ts_code": code, "status": "PLANNED", "total_qty": 0,
"avail_qty": 0, "frozen_reason": "NONE"})
return 1
def update_position(self, code, **fields):
self.ensure_position(code)
self.positions[code].update(fields)
return 1
def bump_position_qty(self, code, *, total_delta=0, avail_delta=0):
self.ensure_position(code)
p = self.positions[code]
p["total_qty"] = max(0, int(p.get("total_qty") or 0) + total_delta)
p["avail_qty"] = max(0, int(p.get("avail_qty") or 0) + avail_delta)
return 1
def reset_avail_all(self):
for p in self.positions.values():
p["avail_qty"] = p.get("total_qty", 0)
p["t0_count_today"] = 0
return len(self.positions)
def list_lots(self, ts_code=None, *, status="OPEN", limit=1000):
out = [l for l in self.lots
if (not ts_code or l["ts_code"] == ts_code)
and (not status or l["status"] == status)]
return sorted(out, key=lambda l: (str(l["open_date"]), l["id"]))[:limit]
def insert_lot(self, *, ts_code, lot_type, qty, open_price, open_date, instruction_id=None,
note=None):
self._lot_id += 1
self.lots.append({"id": self._lot_id, "ts_code": ts_code, "lot_type": lot_type,
"qty": int(qty), "open_price": float(open_price),
"open_date": open_date, "closed_qty": 0, "close_avg_price": None,
"realized_pnl": 0.0, "status": "OPEN",
"instruction_id": instruction_id, "note": note})
return 1
def close_lot_qty(self, lot_id, *, qty, close_price, realized_pnl):
for l in self.lots:
if l["id"] == lot_id:
old_closed = int(l["closed_qty"])
l["close_avg_price"] = ((float(l["close_avg_price"] or 0) * old_closed
+ close_price * qty) / (old_closed + qty))
l["qty"] = max(0, int(l["qty"]) - qty)
l["closed_qty"] = old_closed + qty
l["realized_pnl"] += realized_pnl
l["status"] = "CLOSED" if l["qty"] <= 0 else "OPEN"
return 1
return 0
def update_lot(self, lot_id, **fields):
for l in self.lots:
if l["id"] == lot_id:
l.update(fields)
return 1
return 0
# --- instruction / proposal / ledger / report / industry ---
def insert_instruction(self, **kw):
kw.setdefault("exec_qty", 0)
kw["created_at"] = kw["updated_at"] = datetime.now()
self.instructions[kw["instruction_id"]] = kw
return 1
def list_instructions(self, *, statuses=None, side=None, ts_code=None, limit=300):
out = [i for i in self.instructions.values()
if (not statuses or i["status"] in statuses)
and (not side or i.get("side") == side)
and (not ts_code or i["ts_code"] == ts_code)]
return out[:limit]
def get_instruction(self, iid):
return self.instructions.get(iid)
def update_instruction(self, iid, **kw):
i = self.instructions.get(iid)
if not i:
return 0
for k, v in kw.items():
if v is not None:
i[k] = v
i["updated_at"] = datetime.now()
return 1
def add_instruction_exec(self, iid, qty):
i = self.instructions.get(iid)
if i:
i["exec_qty"] = int(i.get("exec_qty") or 0) + int(qty)
return 1
def insert_proposal(self, **kw):
kw["status"] = kw.get("status", "WAIT_USER")
kw["hard_numbers"] = kw.get("hard_numbers") or {}
self.proposals[kw["proposal_id"]] = kw
return 1
def list_proposals(self, *, statuses=("WAIT_USER",), limit=200):
return [p for p in self.proposals.values() if p["status"] in statuses][:limit]
def get_proposal(self, pid):
return self.proposals.get(pid)
def decide_proposal(self, pid, status):
p = self.proposals.get(pid)
if p and p["status"] == "WAIT_USER":
p["status"] = status
return 1
return 0
def expire_proposals(self, now=None):
return 0
def insert_ledger(self, **kw):
self.ledger.append(kw)
return 1
def list_ledger(self, *, ts_code=None, limit=200):
return self.ledger[-limit:]
def upsert_report(self, ymd, report):
self.reports[int(ymd)] = report
return 1
def get_report(self, ymd):
r = self.reports.get(int(ymd))
return {"ymd": ymd, "report": r} if r else None
def latest_report(self):
if not self.reports:
return None
y = max(self.reports)
return {"ymd": y, "report": self.reports[y]}
def get_industry(self, code):
return self.industry.get(code)
def list_industry(self, limit=5000):
return [{"ts_code": k, "industry": v} for k, v in self.industry.items()]
def upsert_industry(self, rows):
for r in rows:
self.industry[r["ts_code"]] = r["industry"]
return len(rows)
def install_fakes(prices=None, positions=None, params=None):
"""把内存桩装到各模块上, 返回 FakeRepo 实例。"""
from app.repo import downstream_repo, pms_repo
from app.services import industry, market, param_store, portfolio
fake = FakeRepo()
fake.params.update(params or {})
for p in (positions or []):
base = {"ts_code": p["ts_code"], "status": "HOLDING", "frozen_reason": "NONE",
"total_qty": 0, "avail_qty": 0, "base_qty": 0, "fill_qty": 0, "add_qty": 0,
"dca_qty": 0, "t0_qty": 0, "avg_cost": None, "cushion_peak": 0,
"realized_t_profit": 0, "t0_enabled": 0}
base.update(p)
fake.positions[p["ts_code"]] = base
for mod in (pms_repo,):
for name in dir(FakeRepo):
if name.startswith("_"):
continue
setattr(mod, name, getattr(fake, name))
market.get_price = lambda c: (prices or {}).get(c)
market.get_prices = lambda cs_: {c: (prices or {}).get(c) for c in (cs_ or [])}
market.get_refs = lambda c, **kw: {"support": None, "pressure": None, "stop": None,
"source": "none"}
market.get_ma5 = lambda c: (prices or {}).get(c)
market.day_snapshot = lambda c: ({} if not (prices or {}).get(c) else {
"price": prices[c], "vwap": prices[c], "open": prices[c], "high": prices[c] * 1.02,
"low": prices[c] * 0.98, "day_chg_from_open": 0.0, "bars": 60})
industry.get_many = lambda cs_: {c: None for c in (cs_ or [])}
industry.get = lambda c: None
industry.ready = lambda: False
industry.status = lambda: {"source": "", "ready": False, "count": 0, "hint": "test"}
# 下游只读表默认桩 (单测不触网; 个别用例内再按需覆盖)
downstream_repo.fetch_buy_plans = lambda **kw: []
downstream_repo.fetch_filled_orders = lambda **kw: []
downstream_repo.fetch_positions = lambda: {"rows": [], "columns": {"qty": None},
"raw_count": 0}
downstream_repo.fetch_refs = lambda c: None
param_store._cache.update({"at": 0.0, "data": {}, "error": None, "loaded": False})
portfolio.save_neg_streak({})
return fake
# ================================================================ 用例
@case("装配·全部模块可导入 (无循环依赖)")
def _():
import importlib
for m in ["app.core.sizer", "app.core.cushion", "app.core.command_spec", "app.core.planner",
"app.core.recon", "app.core.tradedays", "app.db.session", "app.repo.pms_repo",
"app.repo.downstream_repo", "app.services.param_store", "app.services.industry",
"app.services.market", "app.services.portfolio", "app.services.command_service",
"app.services.ledger_service", "app.web.main", "app.scheduler"]:
importlib.import_module(m)
@case("装配·API 路由齐全 (四块页面 + 运维)")
def _():
from app.web.main import app
paths = {r.path for r in app.routes}
need = ["/", "/health", "/api/overview", "/api/params", "/api/commands",
"/api/commands/catalog", "/api/commands/active", "/api/commands/{command_id}",
"/api/commands/{command_id}/cancel", "/api/plans", "/api/positions",
"/api/positions/{ts_code}/lots", "/api/instructions", "/api/ledger", "/api/report",
"/api/proposals", "/api/proposals/{proposal_id}/decide", "/api/ops/replay",
"/api/ops/reconcile", "/api/ops/daily-settle", "/api/ops/report",
"/api/ops/downstream-schema", "/api/industry"]
missing = [p for p in need if p not in paths]
assert not missing, missing
import os as _os
from app.web import main as web
assert _os.path.exists(_os.path.join(web.STATIC_DIR, "index.html")), "单页文件缺失"
@case("装配·调度表覆盖设计 §10 全部调度位")
def _():
from app import scheduler as sch
names = set(sch.celery_app.conf.beat_schedule)
assert names == {"premarket", "command_poll", "replay_fills", "intraday_exec",
"signal_digest", "t0_close", "daily_settle", "daily_report"}, names
tasks = set(sch.celery_app.tasks)
for t in ("pms.premarket", "pms.command_poll", "pms.replay_fills", "pms.daily_settle",
"pms.daily_report", "pms.t0_close", "pms.intraday_exec", "pms.signal_digest"):
assert t in tasks, t
assert sch.in_session(datetime(2026, 7, 27, 10, 0)) is True
assert sch.in_session(datetime(2026, 7, 27, 12, 0)) is False
assert sch.in_session(datetime(2026, 7, 27, 14, 59)) is True
@case("装配·调度守卫: 非交易日跳过 / 休假模式跳过 / 异常不外抛")
def _():
from app import scheduler as sch
from app.core import tradedays as tdm
orig = tdm.is_trade_day
try:
tdm.is_trade_day = lambda d=None: False
@sch.guard(trade_day=True)
def t1():
raise AssertionError("非交易日不应执行")
assert t1() == {"skipped": "not_trade_day"}
tdm.is_trade_day = lambda d=None: True
install_fakes(params={"PMS_GLOBAL_EXEC_HALT": "true"})
@sch.guard(trade_day=True)
def t2():
raise AssertionError("休假模式不应执行")
assert t2() == {"skipped": "exec_halt"}
install_fakes(params={})
@sch.guard(trade_day=True)
def t3():
raise ValueError("boom")
r = t3()
assert "error" in r and "boom" in r["error"], r
finally:
tdm.is_trade_day = orig
@case("装配·严格单表访问守卫 (JOIN 与多表一律拒绝)")
def _():
from app.db.session import MultiTableSQL, assert_single_table
assert_single_table("SELECT * FROM pms_position WHERE ts_code = :c")
assert_single_table("UPDATE pms_lot SET qty = 0 WHERE id = :id")
for bad in ["SELECT a.* FROM pms_position a JOIN pms_lot b ON a.ts_code = b.ts_code",
"SELECT * FROM pms_position, pms_lot",
"SELECT * FROM pms_plan WHERE ts_code IN (SELECT ts_code FROM pms_position)"]:
try:
assert_single_table(bad)
assert False, f"未拦截: {bad}"
except MultiTableSQL:
pass
@case("交易日历·执行窗口与剩余交易日 (跨周末)")
def _():
from app.core import tradedays as tdm
fri, mon = date(2026, 7, 24), date(2026, 7, 27) # 2026-07-24 周五, 07-27 周一
assert tdm.is_trade_day(fri) and tdm.is_trade_day(mon)
assert tdm.is_trade_day(date(2026, 7, 25)) is False # 周六
assert tdm.next_trade_day(fri, 1) == mon
assert tdm.window_deadline(mon, 1) == mon
assert tdm.window_deadline(mon, 3) == date(2026, 7, 29)
assert tdm.window_deadline(fri, 2) == mon # 周五起 2 个交易日 = 周一
assert tdm.trade_days_left(date(2026, 7, 29), mon) == 3
assert tdm.trade_days_left(date(2026, 7, 20), mon) == 0
assert tdm.ymd(mon) == 20260727
@case("参数中心·表值优先/文件回退/非法值拒绝")
def _():
from app.services import param_store
from config.settings import settings
fake = install_fakes(params={"PMS_TOTAL_SCALE": "3000000", "PMS_AUTONOMY": "full"})
assert param_store.get("PMS_TOTAL_SCALE") == 3_000_000.0
assert param_store.get("PMS_AUTONOMY") == "full"
assert param_store.get("PMS_STOCK_CAP") == settings.PMS_STOCK_CAP # 未改写 → 文件初值
assert param_store.get_bool("PMS_GLOBAL_BUY_HALT") is False # 运行态默认
assert param_store.set_param("PROXY_DB_URL", "x")["ok"] is False # 基础设施不可改
assert param_store.set_param("PMS_PORTFOLIO_CAP", 1.5)["ok"] is False # 越界
assert param_store.set_param("PMS_AUTONOMY", "auto")["ok"] is False # 枚举非法
assert param_store.set_param("PMS_PORTFOLIO_CAP", 0.5)["ok"] is True
assert fake.params["PMS_PORTFOLIO_CAP"] == "0.5"
assert param_store.get("PMS_PORTFOLIO_CAP") == 0.5
sp = param_store.sizing_params()
assert sp["scale"] == 3_000_000.0 and sp["portfolio_cap"] == 0.5
@case("命令服务·参数命令立即生效并覆盖旧记录")
def _():
from app.services import command_service as csvc, param_store
fake = install_fakes()
r1 = csvc.issue("SET_SCALE", {"scale": 2_000_000})
assert r1["ok"] and r1["status"] == "EFFECTIVE", r1
assert param_store.get("PMS_TOTAL_SCALE") == 2_000_000.0
r2 = csvc.issue("SET_SCALE", {"scale": 2_500_000})
assert r2["ok"] and param_store.get("PMS_TOTAL_SCALE") == 2_500_000.0
assert fake.commands[r1["command_id"]]["status"] == "SUPERSEDED"
assert fake.commands[r2["command_id"]]["status"] == "EFFECTIVE"
bad = csvc.issue("SET_SCALE", {"scale": -1})
assert not bad["ok"] and any("OUT_OF_RANGE" in e for e in bad["errors"]), bad
@case("命令服务·个股参数命令投影到账本 (冻结/做T/止损价)")
def _():
from app.services import command_service as csvc
fake = install_fakes(positions=[{"ts_code": "600000.SH", "total_qty": 1000,
"avg_cost": 10.0}])
assert csvc.issue("FREEZE_STOCK", {"ts_code": "600000.SH"})["ok"]
assert fake.positions["600000.SH"]["frozen_reason"] == "COMMAND_HALT"
assert csvc.issue("T0_ENABLE", {"ts_code": "600000.SH", "t_ratio": "30%"})["ok"]
assert fake.positions["600000.SH"]["t0_enabled"] == 1
assert abs(float(fake.positions["600000.SH"]["t0_ratio"]) - 0.30) < 1e-9
assert csvc.issue("SET_STOP_PRICE", {"ts_code": "600000.SH", "price": 9.2})["ok"]
assert fake.positions["600000.SH"]["stop_ref"] == 9.2
assert fake.positions["600000.SH"]["ref_source"] == "user"
assert csvc.issue("UNFREEZE_STOCK", {"ts_code": "600000.SH"})["ok"]
assert fake.positions["600000.SH"]["frozen_reason"] == "NONE"
sp = csvc.effective_stock_params()["600000.SH"]
assert sp["frozen"] is False and sp["t0"] is True and sp["stop_price"] == 9.2, sp
# 做T比例超硬上限 1/3 被拒
assert not csvc.issue("T0_ENABLE", {"ts_code": "600000.SH", "t_ratio": "40%"})["ok"]
@case("命令服务·降仓命令端到端 (方案落表 + 进度 + 撤销)")
def _():
from app.services import command_service as csvc
fake = install_fakes(
prices={"600000.SH": 10.0, "000001.SZ": 8.0},
params={"PMS_TOTAL_SCALE": "2000000", "PMS_WEAK_NEG_DAYS": "5"},
positions=[{"ts_code": "600000.SH", "total_qty": 14000, "base_qty": 7000,
"fill_qty": 3500, "add_qty": 3500, "avg_cost": 8.93},
{"ts_code": "000001.SZ", "total_qty": 10000, "base_qty": 10000,
"avg_cost": 8.8}])
from app.services import portfolio
portfolio.save_neg_streak({"000001.SZ": 6})
r = csvc.issue("REDUCE_EXPOSURE", {"pct": "5%", "window_tdays": 3})
assert r["ok"] and r["status"] == "EXECUTING", r
cid = r["command_id"]
plans = fake.list_plans(command_id=cid)
assert plans, "方案未落表"
assert {p["action"] for p in plans} <= {"EXIT", "TRIM", "HALT"}, plans
exit_p = [p for p in plans if p["action"] == "EXIT"]
assert exit_p and exit_p[0]["ts_code"] == "000001.SZ", plans # 弱票优先清
assert all(p["deadline"] for p in plans)
prog = fake.commands[cid]["progress"]
assert prog["target_amount"] == 100_000.0 and prog["plan_count"] == len(plans), prog
# 进度结算: 方案未成交 → 保持 EXECUTING; 全部成交 → DONE
r2 = csvc.refresh_progress(cid)
assert r2["commands"][0]["status"] in ("EXECUTING", "PARTIAL"), r2
for p in plans:
fake.update_plan(p["plan_id"], status="DONE", filled_qty=p["qty"])
csvc.refresh_progress(cid)
assert fake.commands[cid]["status"] == "DONE", fake.commands[cid]
# 撤销: 已完成命令不可撤
assert csvc.cancel(cid)["ok"] is False
r3 = csvc.issue("REDUCE_EXPOSURE", {"pct": "3%"}, force_conflict=True)
assert csvc.cancel(r3["command_id"])["ok"] is True
assert fake.commands[r3["command_id"]]["status"] == "CANCELLED"
assert all(p["status"] == "CANCELLED"
for p in fake.list_plans(command_id=r3["command_id"]))
@case("命令服务·冲突拦截与强制下达")
def _():
from app.services import command_service as csvc
install_fakes(prices={"600000.SH": 10.0}, params={"PMS_TOTAL_SCALE": "2000000"},
positions=[{"ts_code": "600000.SH", "total_qty": 14000, "base_qty": 7000,
"avg_cost": 9.0}])
a = csvc.issue("REDUCE_EXPOSURE", {"pct": "5%"})
assert a["ok"], a
b = csvc.issue("INCREASE_EXPOSURE", {"pct": "5%"})
assert not b["ok"] and b["conflicts"], b
assert any("方向相反" in c["reason"] for c in b["conflicts"])
c = csvc.issue("INCREASE_EXPOSURE", {"pct": "5%"}, force_conflict=True)
assert c["ok"] and c["conflicts"], c # 强制下达但冲突仍留痕
@case("命令服务·全局暂停买入撤在途买入指令并置开关")
def _():
from app.services import command_service as csvc, param_store
fake = install_fakes(prices={"600000.SH": 10.0})
fake.insert_instruction(instruction_id="INS_1", origin_type="plan", origin_id="P1",
ts_code="600000.SH", action="OPEN", side="buy", qty=1000,
limit_price=10.0, status="DISPATCHED")
fake.insert_instruction(instruction_id="INS_2", origin_type="plan", origin_id="P2",
ts_code="600000.SH", action="EXIT", side="sell", qty=500,
limit_price=10.0, status="DISPATCHED")
r = csvc.issue("HALT_BUY", {})
assert r["ok"] and r["status"] == "DONE", r
assert fake.instructions["INS_1"]["status"] == "CANCELLED"
assert fake.instructions["INS_2"]["status"] == "DISPATCHED" # 卖出不受影响
assert param_store.get_bool("PMS_GLOBAL_BUY_HALT") is True
assert csvc.issue("RESUME_BUY", {})["ok"]
assert param_store.get_bool("PMS_GLOBAL_BUY_HALT") is False
@case("账本服务·成交回放入账 → 批次/持仓/成本重算")
def _():
from app.services import ledger_service as ls
from app.repo import downstream_repo
fake = install_fakes(prices={"600000.SH": 11.0})
fake.insert_instruction(instruction_id="INS_A", origin_type="plan", origin_id="P1",
ts_code="600000.SH", action="OPEN", side="buy", qty=6000,
limit_price=10.0, status="DISPATCHED")
# 建单时点必须显式钉死: 用 datetime.now() 会让本用例的通过与否取决于跑测试的钟点
# (时间守卫要求 下发 ≤ 成交), 17:12 跑就会误判成外部成交 —— 实机上已踩到。
fake.instructions["INS_A"]["created_at"] = "2026-07-27 09:30:00"
# updated_at 故意置在成交之后: 认领必须看建单时点, 不能看 updated_at
# (否则部分成交回写一次 updated_at, 后续成交就会被自己挡住)
fake.instructions["INS_A"]["updated_at"] = "2026-07-27 23:59:59"
fills = [{"order_id": 101, "ts_code": "600000.SH", "side": "buy", "qty": 6000,
"price": 10.0, "done_time": "2026-07-27 09:40:00"}]
orig = downstream_repo.fetch_filled_orders
try:
downstream_repo.fetch_filled_orders = lambda **kw: fills
r = ls.replay_fills()
assert r["ok"] and r["actions"] == 1, r
assert fake.positions["600000.SH"]["total_qty"] == 6000
assert fake.positions["600000.SH"]["avail_qty"] == 0 # T+1: 当日买入不可卖
assert abs(float(fake.positions["600000.SH"]["avg_cost"]) - 10.0) < 1e-6
assert fake.positions["600000.SH"]["base_qty"] == 6000
assert abs(float(fake.positions["600000.SH"]["cushion_pct"]) - 0.10) < 1e-4
assert fake.instructions["INS_A"]["status"] == "CONFIRMED"
assert fake.params["PMS_REPLAY_CURSOR"] == "101"
# 幂等: 游标已推进, 同一批不再重复入账
downstream_repo.fetch_filled_orders = lambda **kw: []
r2 = ls.replay_fills()
assert r2["fills"] == 0 and fake.positions["600000.SH"]["total_qty"] == 6000
finally:
downstream_repo.fetch_filled_orders = orig
@case("账本服务·卖出回放按核销次序 + 摊薄成本下降")
def _():
from app.services import ledger_service as ls
from app.repo import downstream_repo
fake = install_fakes(prices={"600000.SH": 12.0})
fake.insert_lot(ts_code="600000.SH", lot_type="BASE", qty=6000, open_price=10.0,
open_date="2026-07-01")
fake.insert_lot(ts_code="600000.SH", lot_type="ADD", qty=3000, open_price=11.0,
open_date="2026-07-20")
fake.update_position("600000.SH", total_qty=9000, avail_qty=9000)
fills = [{"order_id": 201, "ts_code": "600000.SH", "side": "sell", "qty": 3000,
"price": 12.0, "done_time": "2026-07-27 10:00:00"}]
orig = downstream_repo.fetch_filled_orders
try:
downstream_repo.fetch_filled_orders = lambda **kw: fills
r = ls.replay_fills()
assert r["ok"], r
add_lot = [l for l in fake.lots if l["lot_type"] == "ADD"][0]
assert add_lot["qty"] == 0 and add_lot["status"] == "CLOSED" # ADD 先核销
base_lot = [l for l in fake.lots if l["lot_type"] == "BASE"][0]
assert base_lot["qty"] == 6000 # 底仓保留
p = fake.positions["600000.SH"]
assert p["total_qty"] == 6000 and p["avail_qty"] == 6000
# 摊薄成本 = (60000+33000-36000)/6000 = 9.5
assert abs(float(p["avg_cost"]) - 9.5) < 1e-6, p["avg_cost"]
finally:
downstream_repo.fetch_filled_orders = orig
@case("账本服务·对账以下游为准 + 连续不一致升级")
def _():
from app.services import ledger_service as ls
from app.repo import downstream_repo
fake = install_fakes(prices={"600000.SH": 10.0})
fake.insert_lot(ts_code="600000.SH", lot_type="BASE", qty=1000, open_price=10.0,
open_date="2026-07-01")
fake.update_position("600000.SH", total_qty=1000, avail_qty=1000)
orig = downstream_repo.fetch_positions
try:
downstream_repo.fetch_positions = lambda: {
"rows": [{"ts_code": "600000.SH", "qty": 1500, "avail_qty": 1500}],
"columns": {"code": "stock_code", "qty": "current_qty"}, "raw_count": 1}
r = ls.reconcile()
assert r["ok"] and len(r["diffs"]) == 1 and r["diffs"][0]["delta"] == 500, r
assert fake.positions["600000.SH"]["total_qty"] == 1500 # 以下游为准
assert any(l["lot_type"] == "RECON" for l in fake.lots) # 修正留痕
assert any(x.get("action") == "RECON" for x in fake.ledger)
assert r["severity"] == "WARN"
for _i in range(2): # 连续第 3 日 → ERROR
downstream_repo.fetch_positions = lambda: {
"rows": [{"ts_code": "600000.SH", "qty": 1500 + 100 * (_i + 1)}],
"columns": {"qty": "current_qty"}, "raw_count": 1}
r = ls.reconcile()
assert r["severity"] == "ERROR", r
finally:
downstream_repo.fetch_positions = orig
@case("账本服务·日报生成 (关注区 + 次日除权检测快照)")
def _():
from app.services import ledger_service as ls
fake = install_fakes(prices={"600000.SH": 10.0}, params={"PMS_TOTAL_SCALE": "2000000"},
positions=[{"ts_code": "600000.SH", "total_qty": 6000,
"avg_cost": 9.0}])
rep = ls.build_daily_report()
assert rep["totals"]["portfolio_mv"] == 60_000.0
assert rep["snapshot"]["600000.SH"] == {"qty": 6000, "price": 10.0}
assert any(a["type"] == "行业约束停用" for a in rep["attention"]), rep["attention"]
assert fake.reports[rep["ymd"]]["ymd"] == rep["ymd"]
@case("账本服务·除权检测走通 (10送10 → 批次按比例调整)")
def _():
from app.services import ledger_service as ls
fake = install_fakes(prices={"600000.SH": 5.0}, params={"PMS_TOTAL_SCALE": "2000000"},
positions=[{"ts_code": "600000.SH", "total_qty": 2000,
"avg_cost": 10.0}])
fake.insert_lot(ts_code="600000.SH", lot_type="BASE", qty=2000, open_price=10.0,
open_date="2026-07-01")
fake.upsert_report(20260726, {"snapshot": {"600000.SH": {"qty": 1000, "price": 10.0}}})
r = ls.detect_and_apply_ex_right()
assert r["ex_rights"] and abs(r["ex_rights"][0]["ratio"] - 2.0) < 1e-6, r
lot = fake.lots[0]
assert lot["qty"] == 4000 and abs(lot["open_price"] - 5.0) < 1e-6, lot
@case("盘前准备·T+1 可卖重置")
def _():
from app.services import ledger_service as ls
fake = install_fakes(prices={"600000.SH": 10.0},
positions=[{"ts_code": "600000.SH", "total_qty": 6000, "avail_qty": 0,
"avg_cost": 9.0}])
r = ls.premarket()
assert r["avail_reset"] >= 1, r
assert fake.positions["600000.SH"]["avail_qty"] == 6000
@case("执行器·方案转指令 (GATED 批不转, HALT 直接完结)")
def _():
from app.services import command_service as csvc, executor
fake = install_fakes(prices={"600000.SH": 10.0}, params={"PMS_TOTAL_SCALE": "2000000"},
positions=[{"ts_code": "600000.SH", "total_qty": 14000,
"base_qty": 7000, "fill_qty": 3500, "add_qty": 3500,
"avail_qty": 14000, "avg_cost": 9.0}])
r = csvc.issue("REDUCE_EXPOSURE", {"pct": "3%"})
assert r["ok"], r
# 掺一条 GATED 方案 (建仓补足批) 与一条 HALT 方案
fake.insert_plans([
{"plan_id": "P_G", "command_id": r["command_id"], "ts_code": "600000.SH",
"action": "FILL", "qty": 3000, "amount": 30000, "priority": 20,
"deadline": "2026-07-30", "status": "GATED", "reason": "回踩补足批"},
{"plan_id": "P_H", "command_id": r["command_id"], "ts_code": "600000.SH",
"action": "HALT", "qty": 0, "amount": 0, "priority": 10,
"deadline": "2026-07-30", "status": "PENDING", "reason": "撤在途买入"}])
m = executor.materialize_plans()
assert m["created"], m
codes = {fake.instructions[i]["ts_code"] for i in m["created"]}
assert codes == {"600000.SH"}, codes
assert all(fake.instructions[i]["status"] == "PROPOSED" for i in m["created"])
gated = [p for p in fake.plans if p["plan_id"] == "P_G"][0]
assert gated["status"] == "GATED", gated # 未解锁的批次不转指令
halt = [p for p in fake.plans if p["plan_id"] == "P_H"][0]
assert halt["status"] == "DONE", halt
assert any("HALT" in s["why"] for s in m["skipped"]), m["skipped"]
# 幂等: 再跑一次不重复建指令
n1 = len(fake.instructions)
executor.materialize_plans()
assert len(fake.instructions) == n1
@case("执行器·出手一跳 (影子模式): 择时→规则闸→下发→子单→评审留痕")
def _():
from datetime import datetime as _dt
from app.services import executor
fake = install_fakes(prices={"600000.SH": 10.0}, params={"PMS_TOTAL_SCALE": "2000000"},
positions=[{"ts_code": "600000.SH", "total_qty": 6000,
"avail_qty": 6000, "avg_cost": 9.0}])
fake.insert_instruction(instruction_id="INS_S", origin_type="plan", origin_id="P1",
ts_code="600000.SH", action="TRIM", side="sell", qty=3000,
window_tdays=3, status="PROPOSED",
progress={"deadline": "2026-07-29", "is_command": True,
"children": []})
# 周一 10:05, 距截止 3 个交易日 → 当日配额 1000
r = executor.run_tick(now=_dt(2026, 7, 27, 10, 5))
assert r["ok"] and len(r["fired"]) == 1, r
fired = r["fired"][0]
assert fired["qty"] == 1000 and fired["mode"] == "shadow", fired
ins = fake.instructions["INS_S"]
assert ins["status"] == "DISPATCHED"
assert len(ins["progress"]["children"]) == 1
assert ins["progress"]["children"][0]["qty"] == 1000
assert ins["progress"]["dispatched_at"]
assert any(x["verdict"] == "PASS" and x["arbiter"] == "rule" for x in fake.ledger)
# 同一天再跳一次: 配额已出完, 不重复下发
r2 = executor.run_tick(now=_dt(2026, 7, 27, 10, 6))
assert not r2["fired"] and r2["waited"], r2
assert len(fake.instructions["INS_S"]["progress"]["children"]) == 1
@case("执行器·规则闸拦截时不下发且落拒绝留痕")
def _():
from datetime import datetime as _dt
from app.services import executor
fake = install_fakes(prices={"600000.SH": 10.0}, params={"PMS_TOTAL_SCALE": "2000000"},
positions=[{"ts_code": "600000.SH", "total_qty": 6000,
"avail_qty": 6000, "avg_cost": 9.0}])
fake.insert_instruction(instruction_id="INS_B", origin_type="plan", origin_id="P2",
ts_code="600000.SH", action="OPEN", side="buy", qty=1000,
window_tdays=1, status="PROPOSED",
progress={"deadline": "2026-07-27", "is_command": True,
"children": []})
from app.services import param_store
param_store.set_param("PMS_GLOBAL_BUY_HALT", True, "test")
r = executor.run_tick(now=_dt(2026, 7, 27, 10, 5))
assert not r["fired"] and r["rejected"], r
assert any("BUY_HALT" in f for f in r["rejected"][0]["failed"]), r["rejected"]
assert fake.instructions["INS_B"]["status"] == "PROPOSED" # 未下发
assert any(x["verdict"] == "REJECT" for x in fake.ledger)
@case("执行器·试算模式只算不发不落库")
def _():
from datetime import datetime as _dt
from app.services import executor
fake = install_fakes(prices={"600000.SH": 10.0}, params={"PMS_TOTAL_SCALE": "2000000"},
positions=[{"ts_code": "600000.SH", "total_qty": 6000,
"avail_qty": 6000, "avg_cost": 9.0}])
fake.insert_instruction(instruction_id="INS_D", origin_type="plan", origin_id="P3",
ts_code="600000.SH", action="EXIT", side="sell", qty=2000,
window_tdays=1, status="PROPOSED",
progress={"deadline": "2026-07-27", "children": []})
r = executor.run_tick(now=_dt(2026, 7, 27, 10, 5), dry_run=True)
assert r["fired"] and r["fired"][0]["dry_run"] is True, r
assert r["fired"][0]["qty"] == 2000 # 末日全出
assert fake.instructions["INS_D"]["status"] == "PROPOSED"
assert not fake.instructions["INS_D"]["progress"]["children"]
assert not fake.ledger
@case("执行器·窗口收口: 成交回写方案 + 足额置确认 + 命令类部分完成")
def _():
from datetime import datetime as _dt
from app.services import executor
fake = install_fakes(prices={"600000.SH": 10.0}, params={"PMS_TOTAL_SCALE": "2000000"},
positions=[{"ts_code": "600000.SH", "total_qty": 6000,
"avail_qty": 6000, "avg_cost": 9.0}])
fake.insert_plans([{"plan_id": "P_F", "command_id": "CMD_X", "ts_code": "600000.SH",
"action": "TRIM", "qty": 2000, "amount": 20000, "priority": 30,
"deadline": "2026-07-27", "status": "EXECUTING", "reason": "收利润"}])
fake.insert_instruction(instruction_id="INS_F", origin_type="plan", origin_id="P_F",
ts_code="600000.SH", action="TRIM", side="sell", qty=2000,
status="DISPATCHED", exec_qty=2000,
progress={"deadline": "2026-07-27", "is_command": True,
"children": []})
fake.insert_instruction(instruction_id="INS_P", origin_type="plan", origin_id="P_F",
ts_code="600000.SH", action="TRIM", side="sell", qty=2000,
status="DISPATCHED", exec_qty=500,
progress={"deadline": "2026-07-20", "is_command": True,
"children": []})
fake.insert_instruction(instruction_id="INS_A2", origin_type="proposal", origin_id="PR1",
ts_code="600000.SH", action="ADD", side="buy", qty=1000,
status="DISPATCHED", exec_qty=0,
progress={"deadline": "2026-07-20", "is_command": False,
"children": []})
r = executor.sweep_windows(now=_dt(2026, 7, 27, 15, 10))
assert fake.instructions["INS_F"]["status"] == "CONFIRMED", fake.instructions["INS_F"]
assert [p for p in fake.plans if p["plan_id"] == "P_F"][0]["filled_qty"] == 500
assert any(x["instruction_id"] == "INS_P" for x in r["partial"]), r
assert fake.instructions["INS_P"]["progress"]["window_verdict"]["verdict"] == "PARTIAL"
assert "INS_A2" in r["expired"], r # 自主类窗口耗尽即作废
assert fake.instructions["INS_A2"]["status"] == "EXPIRED"
@case("下发通道·三模式描述与影子回执; 撤销走本地置状态")
def _():
from app.services import dispatcher, executor, param_store
fake = install_fakes()
assert dispatcher.mode() == "shadow"
d = dispatcher.dispatch(instruction_id="INS_1", ts_code="600000.SH", side="sell",
qty=1000, limit_price=9.98)
assert d["ok"] and d["ref"] == "manual:INS_1" and "人工" in d["note"], d
assert param_store.set_param("PMS_DISPATCH_MODE", "bad_mode")["ok"] is False
assert param_store.set_param("PMS_DISPATCH_MODE", "plan_x")["ok"] is True
d2 = dispatcher.dispatch(instruction_id="INS_2", ts_code="600000.SH", side="sell",
qty=1000, limit_price=9.98)
assert d2["ok"] and d2["mode"] == "shadow" and "无卖出通道" in d2["note"], d2
param_store.set_param("PMS_DISPATCH_MODE", "shadow")
fake.insert_instruction(instruction_id="INS_C", origin_type="plan", origin_id="P1",
ts_code="600000.SH", action="TRIM", side="sell", qty=1000,
status="DISPATCHED", progress={"children": []})
r = executor.cancel_instruction("INS_C")
assert r["ok"] and fake.instructions["INS_C"]["status"] == "CANCELLED", r
assert executor.cancel_instruction("INS_C")["ok"] is False
@case("装配·执行相关路由与调度接线到位")
def _():
from app.web.main import app
from app import scheduler as sch
paths = {r.path for r in app.routes}
for p in ("/api/ops/materialize", "/api/ops/exec-tick", "/api/ops/sweep-windows",
"/api/instructions/{instruction_id}/cancel", "/api/dispatch-mode"):
assert p in paths, p
import inspect
src = inspect.getsource(sch.intraday_exec)
assert "executor" in src and "run_tick" in src, "调度器未接执行器"
# ---------------------------------------------------------------- runner
def main():
passed, failed = 0, 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()