# -*- 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"} 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}) 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") 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 # ---------------------------------------------------------------- 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()