diff --git a/sanguo_portfolio/runner_live.py b/sanguo_portfolio/runner_live.py index 1d227e9..7981861 100644 --- a/sanguo_portfolio/runner_live.py +++ b/sanguo_portfolio/runner_live.py @@ -42,6 +42,32 @@ CONNECT_RETRY_ATTEMPTS = 5 CONNECT_RETRY_WAIT_SEC = 10.0 +PREFLIGHT_QUOTE_CODE = "510500.SH" # QMT 后缀格式;聚宽 .XSHG 传入返回空(09-03 踩坑) + + +def quote_preflight(_fetch: Any = None) -> str: + """启动期行情腿自检:进程内 get_full_tick 一笔,返回可读结论字符串。 + + 2026-09-03 行情腿事故定位用:引擎进程内取价失败(momentum 6 笔卖单 + fail-closed 忽略)而独立探针 .SH 格式全通——需要**进程内**直接证据区分 + 「引擎会话坏」vs「数据服务坏」。boot 时留痕一笔,轮换后即知新进程 + 行情腿通不通;只记日志不阻断启动(自检失败不挡交易,留证据即可)。 + """ + try: + if _fetch is None: + from xtquant import xtdata + ticks = xtdata.get_full_tick([PREFLIGHT_QUOTE_CODE]) + else: + ticks = _fetch() + d = ticks.get(PREFLIGHT_QUOTE_CODE) if isinstance(ticks, dict) else None + if d: + price = d.get("lastPrice", d.get("last_price")) + return "OK price=%s" % price + return "EMPTY ticks=%s" % (str(ticks)[:60] if ticks else "{}") + except Exception as e: # noqa: BLE001 - 自检不阻断引擎启动 + return "EXC: %s" % e + + def with_connect_retry(broker: Any, attempts: int = CONNECT_RETRY_ATTEMPTS, wait_sec: float = CONNECT_RETRY_WAIT_SEC, @@ -274,6 +300,8 @@ def run_live(provider_config: Dict[str, Any] | None = None) -> None: logger.info("QmtBroker 装配 account=%s data_path=%s(connect 失败退避重试 %d×%.0fs)", cfg["account"], cfg["mini_path"], CONNECT_RETRY_ATTEMPTS, CONNECT_RETRY_WAIT_SEC) + logger.info("[quote-preflight] 行情腿自检 %s: %s", + PREFLIGHT_QUOTE_CODE, quote_preflight()) # 实例虚拟账本(共享 QMT 账户的切片视图,2026-08-19 互卖/对账/收益率三问题同根): # 先建+恢复再起 engine——适配层 _setup 经 get_active() 注入 facade 通道 diff --git a/tests/portfolio/test_runner_live_connect_retry.py b/tests/portfolio/test_runner_live_connect_retry.py index 986042d..e81c94b 100644 --- a/tests/portfolio/test_runner_live_connect_retry.py +++ b/tests/portfolio/test_runner_live_connect_retry.py @@ -66,3 +66,31 @@ def test_live_config_carries_instance_strategy_name(): from bullet_trade.core.live_engine import LiveConfig cfg = LiveConfig.load({"strategy_name": "live_17"}) assert cfg.strategy_name == "live_17" + + +# ---------------- quote_preflight:启动期行情腿自检(2026-09-03 事故定位) ---------------- + +def test_quote_preflight_ok_with_price(): + """行情腿通:返回 OK+价格(进程内直接证据,boot 时一笔留痕)。""" + from sanguo_portfolio.runner_live import quote_preflight + out = quote_preflight(_fetch=lambda: {"510500.SH": {"lastPrice": 7.88}}) + assert out.startswith("OK") + assert "7.88" in out + + +def test_quote_preflight_empty_is_reported_not_raised(): + """取到空(格式错/会话坏/盘后无数据):返回 EMPTY 摘要,绝不抛。""" + from sanguo_portfolio.runner_live import quote_preflight + assert quote_preflight(_fetch=lambda: {}).startswith("EMPTY") + assert quote_preflight(_fetch=lambda: None).startswith("EMPTY") + + +def test_quote_preflight_exception_is_swallowed(): + """fetch 抛异常:返回 EXC 摘要——自检不阻断引擎启动。""" + from sanguo_portfolio.runner_live import quote_preflight + + def _boom(): + raise RuntimeError("xtdata down") + out = quote_preflight(_fetch=_boom) + assert out.startswith("EXC") + assert "xtdata down" in out