From 029ea34aecd03baeae773842f275c819effba778 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Thu, 3 Sep 2026 13:51:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(live):=20=E5=BC=95=E6=93=8E=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E6=9C=9F=E8=A1=8C=E6=83=85=E8=85=BF=E8=87=AA=E6=A3=80?= =?UTF-8?q?quote=5Fpreflight=E2=80=94=E2=80=9409-03=E8=A1=8C=E6=83=85?= =?UTF-8?q?=E4=BA=8B=E6=95=85=E5=AE=9A=E4=BD=8D=E4=BB=B6:momentum=209:30?= =?UTF-8?q?=E5=85=AD=E7=AC=94=E5=8D=96=E5=8D=95fail-closed=E5=BF=BD?= =?UTF-8?q?=E7=95=A5+18/22=E6=8E=A2=E9=92=88=E9=9B=B6=E5=8D=95,=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E8=BF=9B=E7=A8=8B=E5=86=85=E5=8F=96=E4=BB=B7=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E4=BD=86=E7=8B=AC=E7=AB=8B=E6=8E=A2=E9=92=88.SH?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=85=A8=E9=80=9A(dev=E5=8F=8D=E8=AF=81,?= =?UTF-8?q?=E6=88=91=E5=85=88=E5=89=8D=E7=9A=84=E3=80=8C=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=AD=BB=E3=80=8D=E8=AF=AF=E5=88=A4=E6=94=B6=E5=9B=9E:get=5Ffu?= =?UTF-8?q?ll=5Ftick=E9=A1=BB.SH=E5=90=8E=E7=BC=80+subscribe=E9=A1=BBxtdat?= =?UTF-8?q?a.run(),=E4=B8=A4=E4=B8=AA=E5=9D=91=E5=85=A8=E8=B8=A9);?= =?UTF-8?q?=E9=9C=80=E8=BF=9B=E7=A8=8B=E5=86=85=E7=9B=B4=E6=8E=A5=E8=AF=81?= =?UTF-8?q?=E6=8D=AE=E5=8C=BA=E5=88=86=E3=80=8C=E5=BC=95=E6=93=8E=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E5=9D=8Fvs=E6=9C=8D=E5=8A=A1=E5=9D=8F=E3=80=8D:boot?= =?UTF-8?q?=E6=97=B6broker=E4=BA=A4=E6=98=93=E8=85=BF=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E5=90=8E=E4=B8=80=E7=AC=94get=5Ffull=5Ftick(510500.SH)?= =?UTF-8?q?=E7=95=99=E7=97=95OK/EMPTY/EXC,=E6=B0=B8=E4=B8=8D=E6=8A=9B?= =?UTF-8?q?=E4=B8=8D=E9=98=BB=E6=96=AD;=E4=BB=8A=E6=99=9A23:05=E8=BD=AE?= =?UTF-8?q?=E6=8D=A2=E5=90=8E=E6=96=B0=E8=BF=9B=E7=A8=8B=E5=8D=B3=E5=87=BA?= =?UTF-8?q?=E8=AF=81=E6=8D=AE,=E6=98=8E=E6=97=A99:30=E4=B8=89=E7=81=AF(mom?= =?UTF-8?q?entum=E9=87=8D=E8=AF=95/small=5Fcap=E8=B0=83=E4=BB=93/9:35?= =?UTF-8?q?=E6=8E=A2=E9=92=88)=E5=89=8D=E7=BD=AE=E8=AF=8A=E6=96=AD;3?= =?UTF-8?q?=E6=96=B0=E6=B5=8B+591=E7=BB=BF=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sanguo_portfolio/runner_live.py | 28 +++++++++++++++++++ .../test_runner_live_connect_retry.py | 28 +++++++++++++++++++ 2 files changed, 56 insertions(+) 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