Files
sanguo_vnpy_v2/scripts/qmt_relogin/qmt_sentinel.py
T
claude_dev 2b2ae3c4d8
CI/CD / test (push) Successful in 28s
CI/CD / nas-deploy (push) Successful in 3s
CI/CD / nas-verify (push) Successful in 11s
feat(ops): 大QMT哨兵+09:15盘前探针三层防御上线——30min巡检+必不成交单即撤+gate留痕 [vps]
- qmt_sentinel.py: sanguo-qmt-sentinel 每30min(进程+桥RPC ping判红,账户类仅记录);
  红->schtasks /run sanguo-qmt-relogin->轮询桥恢复<=10min->复检留痕;无退避(拍板#4)
- qmt_probe_0915.py: sanguo-qmt-probe0915 周一至五09:15; 510050.SH买100@0.01
  FIX_PRICE必不成交单(09-09 09:40实证参数形状)+即撤; status57废单=会话陈旧
  ->触发relogin赶开盘; 窗口硬卡09:15:00-09:19:30(09:20-09:25不可撤)
- qmt_gate_common.py: redis/ping/tasklist/触发/轮询/gate留痕共享库
- 留痕: C:\sanguo_bigqmt\gate\qmt_gate_YYYYMMDD.log + qmt_gate_latest.json
- ui_act.py 修复: activate仅contains时KeyError; credtype LocalFree类型错; 新增text op
  (typewrite连打丢数字/shift字符,验证码须key/hotkey逐字符打)
- 实测: 首夜14跳全绿cash零漂移; 下单RPC管线ok=True; SKIP/退出码/桥死分支全验
- schtask XML x2(SYSTEM+HighestAvailable,照sanguo-xt-daily模式)
2026-09-10 07:24:45 +08:00

108 lines
4.2 KiB
Python

# -*- coding: utf-8 -*-
"""sanguo-qmt-sentinel — every-30min availability patrol for the Big QMT stack.
Verdict rules (2026-09-09 design, user-approved):
judge RED only on: XtItClient.exe gone OR bridge RPC ping dead.
account-class probes (asset/positions) and the data leg (XtMiniQmt/miniquote)
are recorded as telemetry only — post-close false-reds by design.
RED -> schtasks /run sanguo-qmt-relogin (reuse v2, never copy login code)
-> poll bridge <=10min -> re-check -> full audit trail in gate log.
No backoff on consecutive failures (user decision #4): every tick independent,
failures just stack in the log; the next tick retries naturally.
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)
RECOVER_TIMEOUT = 600 # 10 min budget for relogin round trip
def telemetry(rc):
"""Recorded, never judged: allow_order bit, asset cash, queue length."""
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
t = {}
try:
r = call_redis_rpc(rc, ACCOUNT, "query_stock_asset",
{"account_id": ACCOUNT}, timeout_seconds=15)
d = r.get("data") or {}
t["cash"] = d.get("cash")
t["mktval"] = d.get("market_value")
except Exception as exc:
t["asset_err"] = "%s %s" % (type(exc).__name__, str(exc)[:80])
try:
t["queue_len"] = rc.llen("bigqmt:rpc:queue:%s" % ACCOUNT)
except Exception as exc:
t["queue_err"] = str(exc)[:60]
return t
def main():
procs = procs_alive()
if not procs.get("XtItClient.exe"):
msg = "XtItClient.exe DEAD family=%s" % procs
gate_log("SENTINEL", "RED", msg)
return recover("process-dead")
try:
rc = redis_client()
except Exception as exc:
gate_log("SENTINEL", "AMBER", "redis connect failed %r" % exc)
return 2
up, detail = bridge_ping(rc)
if not up:
gate_log("SENTINEL", "RED", "bridge ping dead (%s)" % detail)
return recover("bridge-dead")
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),
t.get("cash"), t.get("mktval"), t.get("queue_len"))
gate_log("SENTINEL", "GREEN", msg)
gate_json("sentinel", "GREEN", bridge=detail, procs=procs, **t)
return 0
def recover(reason):
"""Trigger v2 relogin, wait for the bridge, re-check, log the outcome."""
try:
rc = redis_client()
except Exception as exc:
gate_log("SENTINEL", "AMBER", "redis failed pre-recovery %r" % exc)
return 2
if relogin_running():
gate_log("SENTINEL", "SKIP",
"relogin already in flight (%s); let it finish" % reason)
ok, took, detail = wait_bridge(rc, RECOVER_TIMEOUT, "SENTINEL")
return 0 if ok else 1
ok, out = trigger_relogin()
if not ok:
gate_log("SENTINEL", "AMBER",
"schtasks /run %s failed: %s" % (reason, out))
return 2
gate_log("SENTINEL", "ACTION",
"relogin triggered (%s); polling bridge <=%ds" % (reason,
RECOVER_TIMEOUT))
ok, took, detail = wait_bridge(rc, RECOVER_TIMEOUT, "SENTINEL")
procs = procs_alive()
if ok and procs.get("XtItClient.exe"):
t = telemetry(rc)
gate_log("SENTINEL", "RECOVERED",
"bridge back in %ds (%s) procs=%s cash=%s" % (
took, detail, procs, t.get("cash")))
gate_json("sentinel", "RECOVERED", reason=reason, took_sec=took,
bridge=detail, procs=procs, **t)
return 0
gate_log("SENTINEL", "RED",
"NOT recovered in %ds (bridge=%s procs=%s) — no backoff by "
"design, next tick retries" % (took, detail, procs))
gate_json("sentinel", "RED", reason=reason, took_sec=took, bridge=detail,
procs=procs)
return 1
if __name__ == "__main__":
import sys
sys.exit(main())