From a1abe122989b82b8be3837cad6ab013d5fc88b66 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Fri, 11 Sep 2026 20:08:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(ops):=20=E6=A1=A5=E6=97=A5=E5=8E=86?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=A5=91=E7=BA=A6=E4=B8=89=E9=87=8D=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E2=80=94=E2=80=94server=E9=83=A8=E7=BD=B2=E4=BD=8D+sh?= =?UTF-8?q?im=E5=8F=8C=E5=BD=92=E4=B8=80=E5=8C=96+=E5=93=A8=E5=85=B5?= =?UTF-8?q?=E7=AC=AC=E4=BA=94=E6=8E=A2=E9=92=88=20[vps]=E2=80=94=E2=80=940?= =?UTF-8?q?9-11=E4=BA=8B=E6=95=85:mini=E6=A0=88=E6=AE=8B=E7=95=99=E4=BB=A4?= =?UTF-8?q?=E6=A1=A5native=E8=B7=AF=E5=BE=84=E8=BF=94YYYYMMDD=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2,=E6=B6=88=E8=B4=B9=E7=AB=AFts/1000=20TypeErr?= =?UTF-8?q?or=E8=87=B412=E5=BC=95=E6=93=8E=E8=B0=83=E5=BA=A6=E5=99=A8?= =?UTF-8?q?=E5=85=A8=E5=A4=A9=E5=81=9C=E6=91=86(=E5=9B=9B=E4=BB=B6?= =?UTF-8?q?=E5=A5=97=E9=97=B8=E9=97=A8=E5=85=A8=E7=BB=BF=E7=9A=84=E7=9B=B2?= =?UTF-8?q?=E5=8C=BA);=E4=BF=AE=3D=E2=91=A0market=5Fbigqmt.py=20get=5Ftrad?= =?UTF-8?q?ing=5Fdates=E5=87=BA=E5=8F=A3=E5=BD=92=E4=B8=80=E5=8C=96(?= =?UTF-8?q?=E5=8C=97=E4=BA=AC=E5=8D=88=E5=A4=9C=E6=AF=AB=E7=A7=92,?= =?UTF-8?q?=E5=8F=8C=E9=83=A8=E7=BD=B2=E4=BD=8D:=E6=BA=90=E7=A0=81?= =?UTF-8?q?=E5=89=AF=E6=9C=AC+=E5=9B=BD=E9=87=91python=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E4=BD=8D,.bak=E7=95=99=E6=A1=A3)=E2=91=A1xtquant=5Fbridge/xtda?= =?UTF-8?q?ta.py=E5=90=8C=E6=AC=BE(=E5=AE=A2=E6=88=B7=E7=AB=AFfresh?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E5=8F=8C=E4=BF=9D=E9=99=A9)=E2=91=A2?= =?UTF-8?q?=E5=93=A8=E5=85=B5=E7=AC=AC=E4=BA=94=E6=8E=A2=E9=92=88calendar?= =?UTF-8?q?=5Fprobe(fresh=E5=AD=90=E8=BF=9B=E7=A8=8B=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E6=97=A5=E5=8E=86=E5=85=83=E7=B4=A0=E6=95=B0=E5=AD=97=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B,=E5=AD=97=E7=AC=A6=E4=B8=B2=3DRED=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=A7=A6=E5=8F=91relogin,=E7=A9=BA/=E6=89=A7=E8=A1=8C=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=3DAMBER=E4=B8=8D=E8=AF=AF=E7=BA=A2);=E9=A1=BA?= =?UTF-8?q?=E6=89=8Brelogin=20stale=20WARN=E6=B8=85=E7=90=86(xt=5Feod?= =?UTF-8?q?=E5=B7=B2=E5=88=87=E6=A1=A5mini=E7=BC=BA=E5=B8=AD=3D=E6=AD=A3?= =?UTF-8?q?=E5=B8=B8);=E5=AE=9E=E6=B5=8B20:03=E6=A1=A5=E5=9B=9E20:04?= =?UTF-8?q?=E5=85=AD=E5=BC=95=E6=93=8E=E3=80=8C=E6=96=B0=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E6=97=A509-11=E3=80=8D=E8=90=BD=E5=9C=B0=E6=97=A5=E5=8E=86?= =?UTF-8?q?=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patch_market_bigqmt_20260911.py | 67 +++++++++++++++++++ .../patch_market_bigqmt_deploy_20260911.py | 67 +++++++++++++++++++ .../qmt_relogin/patch_shim_xtdata_20260911.py | 61 +++++++++++++++++ scripts/qmt_relogin/qmt_gate_common.py | 56 ++++++++++++++++ scripts/qmt_relogin/qmt_relogin.py | 13 ++-- scripts/qmt_relogin/qmt_sentinel.py | 12 +++- 6 files changed, 265 insertions(+), 11 deletions(-) create mode 100644 scripts/qmt_relogin/patch_market_bigqmt_20260911.py create mode 100644 scripts/qmt_relogin/patch_market_bigqmt_deploy_20260911.py create mode 100644 scripts/qmt_relogin/patch_shim_xtdata_20260911.py diff --git a/scripts/qmt_relogin/patch_market_bigqmt_20260911.py b/scripts/qmt_relogin/patch_market_bigqmt_20260911.py new file mode 100644 index 0000000..6f1664d --- /dev/null +++ b/scripts/qmt_relogin/patch_market_bigqmt_20260911.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +"""Contract fix 2026-09-11: server-side get_trading_dates output normalization. + +The native xtdata SDK path returns 'YYYYMMDD' strings whenever any local +quote source has been reachable; the ContextInfo path returns epoch-ms ints. +Consumers (bullet_trade miniqmt provider ~line 1824) do ts/1000, so both +shapes must leave the server as Beijing-midnight epoch-ms ints. +Idempotent: re-running on a patched file exits 0. + +Run ON THE VPS: C:\\Python310\\python.exe -X utf8 patch_market_bigqmt_20260911.py +""" +import io +import shutil +import sys + +P = r"C:\sanguo_bigqmt\bigqmt_signal_trader\adapters\market_bigqmt.py" +BAK = P + ".bak_20260911" + +OLD = ( + " return self._native_or_context(\n" + ' "get_trading_dates", _via_context, market, start_time, end_time, count\n' + " )" +) +NEW = ( + " return _norm_trading_dates(self._native_or_context(\n" + ' "get_trading_dates", _via_context, market, start_time, end_time, count))' +) + +HELPER = ''' + +def _norm_trading_dates(result): + """Contract fix 2026-09-11: native returns 'YYYYMMDD' strings whenever a + local quote source has been reachable; ContextInfo returns epoch-ms ints. + Normalize to Beijing-midnight epoch ms (official xtquant contract; + consumers do ts/1000 with local-timezone fromtimestamp, server runs + Asia/Shanghai so datetime().timestamp() is the exact inverse). + All-numeric lists pass through untouched; mixed/odd payloads keep + original elements that are not 8-digit strings.""" + if not isinstance(result, list) or not result: + return result + if all(isinstance(x, (int, float)) and not isinstance(x, bool) for x in result): + return result + out = [] + for x in result: + if isinstance(x, str) and len(x) == 8 and x.isdigit(): + import datetime as _dt + out.append(int(_dt.datetime(int(x[:4]), int(x[4:6]), int(x[6:8])).timestamp() * 1000)) + else: + out.append(x) + return out +''' + +src = io.open(P, encoding="utf-8").read() + +if "_norm_trading_dates(self._native_or_context" in src: + print("ALREADY_PATCHED") + sys.exit(0) + +if src.count(OLD) != 1: + print("ANCHOR_NOT_UNIQUE: %d" % src.count(OLD)) + sys.exit(1) + +shutil.copyfile(P, BAK) +src = src.replace(OLD, NEW) +src = src.rstrip("\n") + "\n" + HELPER +io.open(P, "w", encoding="utf-8", newline="\n").write(src) +print("PATCHED ok, backup=%s" % BAK) diff --git a/scripts/qmt_relogin/patch_market_bigqmt_deploy_20260911.py b/scripts/qmt_relogin/patch_market_bigqmt_deploy_20260911.py new file mode 100644 index 0000000..f9e3c1e --- /dev/null +++ b/scripts/qmt_relogin/patch_market_bigqmt_deploy_20260911.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +"""Contract fix 2026-09-11: server-side get_trading_dates output normalization. + +The native xtdata SDK path returns 'YYYYMMDD' strings whenever any local +quote source has been reachable; the ContextInfo path returns epoch-ms ints. +Consumers (bullet_trade miniqmt provider ~line 1824) do ts/1000, so both +shapes must leave the server as Beijing-midnight epoch-ms ints. +Idempotent: re-running on a patched file exits 0. + +Run ON THE VPS: C:\\Python310\\python.exe -X utf8 patch_market_bigqmt_20260911.py +""" +import io +import shutil +import sys + +P = r"C:\国金QMT交易端模拟\python\bigqmt_signal_trader\adapters\market_bigqmt.py" +BAK = P + ".bak_20260911" + +OLD = ( + " return self._native_or_context(\n" + ' "get_trading_dates", _via_context, market, start_time, end_time, count\n' + " )" +) +NEW = ( + " return _norm_trading_dates(self._native_or_context(\n" + ' "get_trading_dates", _via_context, market, start_time, end_time, count))' +) + +HELPER = ''' + +def _norm_trading_dates(result): + """Contract fix 2026-09-11: native returns 'YYYYMMDD' strings whenever a + local quote source has been reachable; ContextInfo returns epoch-ms ints. + Normalize to Beijing-midnight epoch ms (official xtquant contract; + consumers do ts/1000 with local-timezone fromtimestamp, server runs + Asia/Shanghai so datetime().timestamp() is the exact inverse). + All-numeric lists pass through untouched; mixed/odd payloads keep + original elements that are not 8-digit strings.""" + if not isinstance(result, list) or not result: + return result + if all(isinstance(x, (int, float)) and not isinstance(x, bool) for x in result): + return result + out = [] + for x in result: + if isinstance(x, str) and len(x) == 8 and x.isdigit(): + import datetime as _dt + out.append(int(_dt.datetime(int(x[:4]), int(x[4:6]), int(x[6:8])).timestamp() * 1000)) + else: + out.append(x) + return out +''' + +src = io.open(P, encoding="utf-8").read() + +if "_norm_trading_dates(self._native_or_context" in src: + print("ALREADY_PATCHED") + sys.exit(0) + +if src.count(OLD) != 1: + print("ANCHOR_NOT_UNIQUE: %d" % src.count(OLD)) + sys.exit(1) + +shutil.copyfile(P, BAK) +src = src.replace(OLD, NEW) +src = src.rstrip("\n") + "\n" + HELPER +io.open(P, "w", encoding="utf-8", newline="\n").write(src) +print("PATCHED ok, backup=%s" % BAK) diff --git a/scripts/qmt_relogin/patch_shim_xtdata_20260911.py b/scripts/qmt_relogin/patch_shim_xtdata_20260911.py new file mode 100644 index 0000000..da5fa3d --- /dev/null +++ b/scripts/qmt_relogin/patch_shim_xtdata_20260911.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"""B-pack client-side fix 2026-09-11: shim get_trading_dates output normalization. + +The server-side fix (market_bigqmt.py patch) normalizes the RPC answer, but +fresh client processes (xt_eod wrapper, future engines) load THIS shim first. +Defense in depth: normalize here too, so the contract holds even if the server +side regresses (e.g. server file restored from a backup). +Idempotent: re-running on a patched file exits 0. + +Run ON THE VPS: C:\\Python310\\python.exe -X utf8 patch_shim_xtdata_20260911.py +""" +import io +import shutil +import sys + +P = r"C:\sanguo_bigqmt\xtquant_bridge\xtquant\xtdata.py" +BAK = P + ".bak_20260911" + +OLD = '''def get_trading_dates(market, start_time="", end_time="", count=-1): + return _compat.xtdata.get_trading_dates(market, start_time, end_time, count)''' + +NEW = '''def _norm_calendar_ms(result): + """Contract fix 2026-09-11: 'YYYYMMDD' strings -> Beijing-midnight epoch ms. + + The bridge server's native xtdata path returns string dates whenever a + local quote source has been reachable (2026-09-11 incident: all 12 engine + schedulers starved on ts/1000 TypeErrors). Consumers divide by 1000 with + local-timezone fromtimestamp, and this host runs Asia/Shanghai, so + datetime().timestamp() is the exact inverse. All-numeric lists pass + through untouched.""" + if not isinstance(result, list) or not result: + return result + if all(isinstance(x, (int, float)) and not isinstance(x, bool) for x in result): + return result + out = [] + for x in result: + if isinstance(x, str) and len(x) == 8 and x.isdigit(): + import datetime as _dt + out.append(int(_dt.datetime(int(x[:4]), int(x[4:6]), int(x[6:8])).timestamp() * 1000)) + else: + out.append(x) + return out + + +def get_trading_dates(market, start_time="", end_time="", count=-1): + return _norm_calendar_ms(_compat.xtdata.get_trading_dates(market, start_time, end_time, count))''' + +src = io.open(P, encoding="utf-8").read() + +if "_norm_calendar_ms(_compat.xtdata.get_trading_dates" in src: + print("ALREADY_PATCHED") + sys.exit(0) + +if src.count(OLD) != 1: + print("ANCHOR_NOT_UNIQUE: %d" % src.count(OLD)) + sys.exit(1) + +shutil.copyfile(P, BAK) +src = src.replace(OLD, NEW) +io.open(P, "w", encoding="utf-8", newline="\n").write(src) +print("PATCHED ok, backup=%s" % BAK) diff --git a/scripts/qmt_relogin/qmt_gate_common.py b/scripts/qmt_relogin/qmt_gate_common.py index 36dbf6d..22effe6 100644 --- a/scripts/qmt_relogin/qmt_gate_common.py +++ b/scripts/qmt_relogin/qmt_gate_common.py @@ -83,6 +83,62 @@ def procs_alive(): return {name: ('"%s"' % name) in out for name in QMT_PROCS} +CALENDAR_PROBE_CODE = ( + "from xtquant import xtdata; " + "r = xtdata.get_trading_dates('SH', '20260101', '20261231'); " + "e = r[-1] if isinstance(r, list) and r else None; " + "print('CAL', type(e).__name__, repr(e))" +) + + +def calendar_probe(timeout=25): + """Fifth gate (added 2026-09-11): fresh child process fetches the trading + calendar through the bridge and checks the element type. + + On 2026-09-11 all 12 engine schedulers starved because the bridge returned + 'YYYYMMDD' STRINGS from its native xtdata path while every other probe + (process / ping / asset / probe order) stayed green. Consumers divide by + 1000, so a string element is a hard contract break. + + Returns (state, detail): state in ok / bad / empty / exec. + ok - non-empty numeric elements (epoch ms), contract healthy + bad - string elements -> RED (relogin restarts the patched server, + which normalizes the output again) + empty - probe shape got no data (not proof the bridge is bad) -> AMBER + exec - child process failed -> AMBER + """ + import os + env = dict(os.environ) + env["PYTHONPATH"] = r"C:\sanguo_bigqmt\xtquant_bridge" + env["BIGQMT_ACCOUNT_ID"] = ACCOUNT + with open(r"C:\redis\redis.conf") as f: + conf = f.read() + env["BIGQMT_REDIS_PASSWORD"] = re.search( + r"^requirepass (\S+)", conf, re.M).group(1) + try: + r = subprocess.run( + ["C:\\Python310\\python.exe", "-X", "utf8", "-c", + CALENDAR_PROBE_CODE], + capture_output=True, text=True, timeout=timeout, env=env) + except Exception as exc: + return "exec", "CAL_PROBE_EXEC %s %s" % (type(exc).__name__, + str(exc)[:100]) + cal = next((l for l in (r.stdout or "").splitlines() + if l.startswith("CAL ")), "") + if not cal: + return "exec", "CAL_PROBE_NO_OUT rc=%s %s" % ( + r.returncode, (r.stderr or "").strip()[:100]) + parts = cal.split(None, 2) + if len(parts) < 3: + return "exec", "CAL_PROBE_BAD '%s'" % cal[:80] + etype, eval_ = parts[1], parts[2][:40] + if etype == "NoneType": + return "empty", "calendar probe returned no data (%s)" % eval_ + if etype in ("int", "float"): + return "ok", "calendar elem=%s %s" % (etype, eval_) + return "bad", "CALENDAR_STR_CONTRACT elem=%s %s" % (etype, eval_) + + def relogin_running(): """True if the v2 relogin python is mid-flight (avoid double trigger).""" try: diff --git a/scripts/qmt_relogin/qmt_relogin.py b/scripts/qmt_relogin/qmt_relogin.py index c339aba..5658ed0 100644 --- a/scripts/qmt_relogin/qmt_relogin.py +++ b/scripts/qmt_relogin/qmt_relogin.py @@ -161,12 +161,11 @@ def bridge_alive(timeout_seconds=150, rc=None): def warn_data_leg(): - """xt_eod (21:00) runs on XtMiniQmt.exe; its absence is tonight's data - gap, not a trading failure -- warn loudly but stay green.""" - st = _procs_alive() - if not st.get("XtMiniQmt.exe"): - log("WARN: XtMiniQmt.exe absent -> tonight's xt_eod leg will fail; " - "family=%s" % st) + """2026-09-11: xt_eod switched to the big-QMT bridge (xtquant_bridge + shim), so XtMiniQmt.exe absence is NORMAL and no longer a data gap. + Kept as a no-op placeholder because the relogin flow used to log a + misleading warning here; see xt-eod-bridge-leg-switch-20260911.""" + return None def main(): @@ -209,7 +208,6 @@ def main(): if got: log("bridge up after exe-mode (remembered session) via %r ok=%s" % (got[0], got[1].get("ok"))) - warn_data_leg() log("=== RELOGIN_DONE ===") return 0 @@ -237,7 +235,6 @@ def main(): log("FATAL: bridge rpc did not answer after login") return 5 log("bridge rpc answering via %r: ok=%s" % (got[0], got[1].get("ok"))) - warn_data_leg() log("=== RELOGIN_DONE ===") return 0 diff --git a/scripts/qmt_relogin/qmt_sentinel.py b/scripts/qmt_relogin/qmt_sentinel.py index 644d42c..e04a1f4 100644 --- a/scripts/qmt_relogin/qmt_sentinel.py +++ b/scripts/qmt_relogin/qmt_sentinel.py @@ -14,9 +14,9 @@ Exit codes: 0 green/recovered | 1 red-remains | 2 amber (cannot judge). """ import json -from qmt_gate_common import (ACCOUNT, bridge_ping, gate_json, gate_log, - procs_alive, redis_client, relogin_running, - trigger_relogin, wait_bridge) +from qmt_gate_common import (ACCOUNT, bridge_ping, calendar_probe, gate_json, + gate_log, procs_alive, redis_client, + relogin_running, trigger_relogin, wait_bridge) RECOVER_TIMEOUT = 600 # 10 min budget for relogin round trip @@ -55,6 +55,12 @@ def main(): if not up: gate_log("SENTINEL", "RED", "bridge ping dead (%s)" % detail) return recover("bridge-dead") + cal_state, cal_detail = calendar_probe() + if cal_state == "bad": + gate_log("SENTINEL", "RED", cal_detail) + return recover("calendar-str-contract") + if cal_state in ("empty", "exec"): + gate_log("SENTINEL", "AMBER", "fifth-probe inconclusive: %s" % cal_detail) t = telemetry(rc) msg = "proc=1 bridge=up %s mini=%d miniquote=%d cash=%s mktval=%s q=%s" % ( detail, procs.get("XtMiniQmt.exe", 0), procs.get("miniquote.exe", 0),