diff --git a/scripts/qmt_relogin/qmt_gate_common.py b/scripts/qmt_relogin/qmt_gate_common.py
new file mode 100644
index 0000000..36dbf6d
--- /dev/null
+++ b/scripts/qmt_relogin/qmt_gate_common.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+"""Shared helpers for the QMT gate scripts (qmt_sentinel.py / qmt_probe_0915.py).
+
+Design: scripts judge AVAILABILITY (process + bridge RPC + counter behaviour),
+agents/humans judge CORRECTNESS (cash baseline drift is recorded, never red).
+Audit trail: C:\\sanguo_bigqmt\\gate\\qmt_gate_YYYYMMDD.log (append) +
+qmt_gate_latest.json (overwrite status light).
+"""
+import datetime
+import json
+import re
+import subprocess
+
+ACCOUNT = "66639661"
+GATE_DIR = r"C:\sanguo_bigqmt\gate"
+RELOGIN_TASK = "sanguo-qmt-relogin"
+QMT_PROCS = ("XtItClient.exe", "XtMiniQmt.exe", "miniquote.exe")
+
+
+def now_str():
+ return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+
+def gate_log(kind, verdict, msg):
+ line = "%s [%s] %s %s" % (now_str(), kind, verdict, msg)
+ try:
+ import os
+ os.makedirs(GATE_DIR, exist_ok=True)
+ path = r"%s\qmt_gate_%s.log" % (
+ GATE_DIR, datetime.datetime.now().strftime("%Y%m%d"))
+ with open(path, "a", encoding="utf-8") as f:
+ f.write(line + "\n")
+ except Exception as exc:
+ line += " (log-write-failed %r)" % exc
+ print(line, flush=True)
+ return line
+
+
+def gate_json(kind, verdict, **detail):
+ import os
+ payload = {"ts": now_str(), "kind": kind, "verdict": verdict}
+ payload.update(detail)
+ try:
+ os.makedirs(GATE_DIR, exist_ok=True)
+ with open(r"%s\qmt_gate_latest.json" % GATE_DIR, "w",
+ encoding="utf-8") as f:
+ json.dump(payload, f, ensure_ascii=False, default=str, indent=1)
+ except Exception as exc:
+ print("GATE_JSON_WRITE_FAIL %r" % exc, flush=True)
+ return payload
+
+
+def redis_client():
+ import redis
+ with open(r"C:\redis\redis.conf") as f:
+ pw = re.search(r"^requirepass (\S+)", f.read(), re.M).group(1)
+ return redis.Redis(host="127.0.0.1", port=6379, db=5, password=pw,
+ socket_connect_timeout=5, decode_responses=True)
+
+
+def bridge_ping(rc, timeout=10):
+ """(up, detail) — up means the RPC server answered ping with ok+pong."""
+ from bigqmt_signal_trader.redis_rpc import call_redis_rpc
+ try:
+ r = call_redis_rpc(rc, ACCOUNT, "ping", {}, timeout_seconds=timeout)
+ except Exception as exc:
+ return False, "RPC_DOWN %s %s" % (type(exc).__name__, str(exc)[:120])
+ if not isinstance(r, dict):
+ return False, "RPC_BAD %r" % (r,)
+ if not r.get("ok"):
+ return False, "RPC_ERR %s" % str(r.get("error") or r)[:150]
+ d = r.get("data") or {}
+ if not d.get("pong"):
+ return False, "RPC_NO_PONG %s" % str(d)[:150]
+ return True, "server_time=%s rev=%s allow_order=%s" % (
+ d.get("server_time"), d.get("rpc_revision"), d.get("allow_order_methods"))
+
+
+def procs_alive():
+ """QMT family process presence via tasklist (no psutil dependency)."""
+ out = subprocess.run(["tasklist", "/FO", "CSV", "/NH"],
+ capture_output=True, text=True, timeout=30).stdout or ""
+ return {name: ('"%s"' % name) in out for name in QMT_PROCS}
+
+
+def relogin_running():
+ """True if the v2 relogin python is mid-flight (avoid double trigger)."""
+ try:
+ import psutil
+ except ImportError:
+ return False
+ for p in psutil.process_iter(["pid", "name", "cmdline"]):
+ try:
+ if "python" not in (p.info["name"] or "").lower():
+ continue
+ cmd = " ".join(p.info["cmdline"] or [])
+ except Exception:
+ continue
+ if "qmt_relogin.py" in cmd:
+ return True
+ return False
+
+
+def trigger_relogin():
+ r = subprocess.run(["schtasks", "/run", "/tn", RELOGIN_TASK],
+ capture_output=True, text=True, timeout=30)
+ ok = r.returncode == 0
+ return ok, ((r.stdout or "") + (r.stderr or "")).strip()[:120]
+
+
+def wait_bridge(rc, timeout_sec, kind):
+ """Poll bridge ping until it answers; every probe logged once at the end."""
+ import time
+ t0 = time.time()
+ last = ""
+ while time.time() - t0 < timeout_sec:
+ up, detail = bridge_ping(rc)
+ if up:
+ return True, int(time.time() - t0), detail
+ last = detail
+ time.sleep(5)
+ return False, int(time.time() - t0), last
diff --git a/scripts/qmt_relogin/qmt_probe0915_task.xml b/scripts/qmt_relogin/qmt_probe0915_task.xml
new file mode 100644
index 0000000..53e2c95
--- /dev/null
+++ b/scripts/qmt_relogin/qmt_probe0915_task.xml
@@ -0,0 +1,42 @@
+
+
+
+ Big QMT pre-open ACTIVE trade probe 09:15 Mon-Fri: 510050 x100 @0.01 never-fill order + instant cancel. Junked order = stale session -> triggers sanguo-qmt-relogin before the open. Audit trail C:\sanguo_bigqmt\gate\qmt_gate_*.log
+
+
+
+ 2026-09-10T09:15:00
+ true
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ false
+ false
+ PT12M
+ IgnoreNew
+ true
+ true
+
+
+
+ S-1-5-18
+ HighestAvailable
+
+
+
+
+ C:\Python310\python.exe
+ -X utf8 C:\sanguo_bigqmt\qmt_probe_0915.py
+
+
+
diff --git a/scripts/qmt_relogin/qmt_probe_0915.py b/scripts/qmt_relogin/qmt_probe_0915.py
new file mode 100644
index 0000000..36105bd
--- /dev/null
+++ b/scripts/qmt_relogin/qmt_probe_0915.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+"""sanguo-qmt-probe0915 — pre-open ACTIVE trade probe (the only gold standard
+for session-day freshness: passive queries cannot see a stale session).
+
+Order shape (proven 2026-09-09 09:40 on this exact account/counter):
+ 510050.SH BUY 100 @ 0.01 FIX_PRICE — a price that can NEVER fill, so the
+ probe is risk-free; what matters is how the counter treats it.
+ accepted (status 48..56) -> session is day-current -> GREEN -> cancel it
+ junked (status 57 / reject msg) -> stale session -> RED -> trigger
+ sanguo-qmt-relogin (~4min, back before the 09:30 open)
+
+Window is hard-coded 09:15:00-09:19:30: 09:20-09:25 is the no-cancel auction
+phase — an order placed there could not be pulled before the open.
+
+Exit codes: 0 green | 1 red | 2 amber/skip.
+"""
+import datetime
+import sys
+import time
+
+from qmt_gate_common import (ACCOUNT, bridge_ping, gate_json, gate_log,
+ procs_alive, redis_client, relogin_running,
+ trigger_relogin, wait_bridge)
+
+STOCK = "510050.SH"
+WINDOW_START = (9, 15, 0)
+WINDOW_END = (9, 19, 30)
+LIVE_STATUSES = {48, 49, 50, 55, 56} # unreported..reported, part/full fill
+CANCEL_FAMILY = {51, 52, 53, 54} # accepted then (part) cancelled
+JUNK_STATUS = 57 # ORDER_JUNK = counter rejection
+HOLIDAY_WORDS = ("非交易", "休市", "闭市", "非交易日", "节假日")
+RECOVER_TIMEOUT = 360 # 6 min: back before the 09:30 open
+
+
+def _in_window(now):
+ hms = (now.hour, now.minute, now.second)
+ if hms < WINDOW_START:
+ wait = (WINDOW_START[0] * 3600 + WINDOW_START[1] * 60 + WINDOW_START[2]
+ - (hms[0] * 3600 + hms[1] * 60 + hms[2]))
+ return "early", wait
+ if hms > WINDOW_END:
+ return "late", 0
+ return "in", 0
+
+
+def rpc(rc, method, params, timeout=30):
+ from bigqmt_signal_trader.redis_rpc import call_redis_rpc
+ return call_redis_rpc(rc, ACCOUNT, method, params, timeout_seconds=timeout)
+
+
+def find_order(rc, remark):
+ """The probe order of today by exact remark, from the counter's order list."""
+ for _ in range(5):
+ try:
+ r = rpc(rc, "query_stock_orders", {"account_id": ACCOUNT})
+ orders = r.get("data") if isinstance(r.get("data"), list) else []
+ for o in orders:
+ if str(o.get("remark") or o.get("user_order_id") or "") == remark:
+ return o
+ except Exception as exc:
+ gate_log("PROBE0915", "NOTE", "orders query err %r" % exc)
+ time.sleep(2)
+ return None
+
+
+def place(rc, remark):
+ res = rpc(rc, "order_stock", {
+ "account_id": ACCOUNT, "stock_code": STOCK, "order_type": 23,
+ "order_volume": 100, "price_type": 11, "price": 0.01,
+ "strategy_name": "probe", "order_remark": remark})
+ gate_log("PROBE0915", "NOTE",
+ "order placed remark=%s res=%s" % (remark, str(res)[:160]))
+
+
+def cancel(rc, order):
+ sysid = str(order.get("order_sys_id") or "")
+ if not sysid:
+ gate_log("PROBE0915", "WARN", "no order_sys_id to cancel: %s"
+ % str(order)[:160])
+ return False
+ for attempt in range(3):
+ try:
+ r = rpc(rc, "cancel_order_stock_sysid",
+ {"account_id": ACCOUNT, "market": "", "order_sysid": sysid})
+ ok = "ok" in r and r.get("ok") is not False
+ gate_log("PROBE0915", "NOTE", "cancel sysid=%s try%d -> %s"
+ % (sysid, attempt + 1, str(r)[:120]))
+ except Exception as exc:
+ gate_log("PROBE0915", "WARN", "cancel exc %r" % exc)
+ ok = False
+ time.sleep(2)
+ cur = find_order(rc, str(order.get("remark") or ""))
+ if cur and int(cur.get("status") or 0) in (CANCEL_FAMILY | LIVE_STATUSES):
+ st = int(cur.get("status") or 0)
+ if st in CANCEL_FAMILY:
+ return True
+ elif cur is None:
+ return True # gone from the list: fully pulled
+ gate_log("PROBE0915", "CRITICAL",
+ "PROBE_ORDER_LEFT_LIVE sysid=%s status=%s — cannot fill @0.01, "
+ "day-end settlement clears it; review manually"
+ % (sysid, (cur or {}).get("status")))
+ return False
+
+
+def main():
+ now = datetime.datetime.now()
+ remark = "probe:gate:%s" % now.strftime("%Y%m%d")
+ state, wait = _in_window(now)
+ if state == "late":
+ gate_log("PROBE0915", "SKIP", "past 09:19:30 window, no probe today")
+ gate_json("probe0915", "SKIP", reason="late")
+ return 2
+ if state == "early":
+ gate_log("PROBE0915", "NOTE", "early by %ds, waiting for window" % wait)
+ time.sleep(min(wait + 1, 300))
+ now = datetime.datetime.now()
+ if _in_window(now)[0] != "in":
+ gate_log("PROBE0915", "SKIP", "window lost while waiting")
+ gate_json("probe0915", "SKIP", reason="window-lost")
+ return 2
+
+ try:
+ rc = redis_client()
+ except Exception as exc:
+ gate_log("PROBE0915", "AMBER", "redis connect failed %r" % exc)
+ return 2
+ up, detail = bridge_ping(rc)
+ if not up:
+ gate_log("PROBE0915", "AMBER",
+ "bridge already down pre-order (%s); relogin is sentinel's "
+ "job, skipping probe" % detail)
+ return 2
+
+ existing = find_order(rc, remark)
+ if existing is None:
+ place(rc, remark)
+ # counter reporting can lag at the 09:15:00 boundary second
+ # (night test 09-09: after-hours submissions stayed invisible);
+ # poll patiently — the cancel window still has minutes of margin.
+ for _ in range(3):
+ time.sleep(3)
+ existing = find_order(rc, remark)
+ if existing is not None:
+ break
+ if existing is None:
+ gate_log("PROBE0915", "AMBER",
+ "order not visible after 5 queries; cannot judge, no trigger")
+ gate_json("probe0915", "AMBER", reason="order-not-visible")
+ return 2
+
+ status = int(existing.get("status") or 0)
+ msg = str(existing.get("status_msg") or "")[:100]
+ gate_log("PROBE0915", "NOTE",
+ "order status=%d msg=%r sysid=%s" % (
+ status, msg, existing.get("order_sys_id")))
+
+ if status == JUNK_STATUS or (msg and any(w in msg for w in ("废", "拒"))):
+ if any(w in msg for w in HOLIDAY_WORDS):
+ gate_log("PROBE0915", "AMBER",
+ "junked with holiday wording (%r) — not a staleness "
+ "signal, no trigger" % msg)
+ gate_json("probe0915", "AMBER", status=status, msg=msg)
+ return 2
+ gate_log("PROBE0915", "RED",
+ "counter junked the probe order -> session stale; "
+ "triggering relogin")
+ return recover(rc, "junked:%s" % msg)
+
+ # accepted by the counter in any form -> session is day-current
+ ok_cancel = True
+ if status in LIVE_STATUSES:
+ ok_cancel = cancel(rc, existing)
+ gate_log("PROBE0915", "GREEN",
+ "order accepted (status=%d) -> session day-current; cancelled=%s"
+ % (status, ok_cancel))
+ gate_json("probe0915", "GREEN", status=status, cancelled=ok_cancel,
+ sysid=str(existing.get("order_sys_id")))
+ return 0
+
+
+def recover(rc, reason):
+ if relogin_running():
+ gate_log("PROBE0915", "SKIP", "relogin already in flight (%s)" % reason)
+ else:
+ ok, out = trigger_relogin()
+ if not ok:
+ gate_log("PROBE0915", "AMBER", "schtasks /run failed: %s" % out)
+ return 2
+ gate_log("PROBE0915", "ACTION",
+ "relogin triggered (%s); polling bridge <=%ds" % (reason,
+ RECOVER_TIMEOUT))
+ ok, took, detail = wait_bridge(rc, RECOVER_TIMEOUT, "PROBE0915")
+ procs = procs_alive()
+ if ok:
+ gate_log("PROBE0915", "RECOVERED",
+ "bridge back in %ds before open (%s) procs=%s" % (
+ took, detail, procs))
+ gate_json("probe0915", "RED_RECOVERED", reason=reason, took_sec=took)
+ return 1 # the session WAS stale this morning; red stays on record
+ gate_log("PROBE0915", "RED",
+ "NOT recovered in %ds (%s) — manual attention before open" % (
+ took, detail))
+ gate_json("probe0915", "RED", reason=reason, took_sec=took, procs=procs)
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/qmt_relogin/qmt_sentinel.py b/scripts/qmt_relogin/qmt_sentinel.py
new file mode 100644
index 0000000..644d42c
--- /dev/null
+++ b/scripts/qmt_relogin/qmt_sentinel.py
@@ -0,0 +1,107 @@
+# -*- 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())
diff --git a/scripts/qmt_relogin/qmt_sentinel_task.xml b/scripts/qmt_relogin/qmt_sentinel_task.xml
new file mode 100644
index 0000000..1364d40
--- /dev/null
+++ b/scripts/qmt_relogin/qmt_sentinel_task.xml
@@ -0,0 +1,40 @@
+
+
+
+ Big QMT 30min availability sentinel: XtItClient process + bridge RPC ping; RED triggers sanguo-qmt-relogin then polls recovery. Audit trail C:\sanguo_bigqmt\gate\qmt_gate_*.log
+
+
+
+ 2026-09-10T00:10:00
+ true
+
+ 1
+
+
+ PT30M
+ P1D
+ false
+
+
+
+
+ false
+ false
+ PT12M
+ IgnoreNew
+ true
+ true
+
+
+
+ S-1-5-18
+ HighestAvailable
+
+
+
+
+ C:\Python310\python.exe
+ -X utf8 C:\sanguo_bigqmt\qmt_sentinel.py
+
+
+
diff --git a/scripts/qmt_relogin/ui_act.py b/scripts/qmt_relogin/ui_act.py
index 8ab86c5..8a6da67 100644
--- a/scripts/qmt_relogin/ui_act.py
+++ b/scripts/qmt_relogin/ui_act.py
@@ -60,7 +60,8 @@ for a in plan:
if op == "activate":
import ctypes
user32 = ctypes.windll.user32
- h = user32.FindWindowW(None, a["title"])
+ title = a.get("title")
+ h = user32.FindWindowW(None, title) if title else 0
if not h and a.get("contains"):
Ws = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
found = []
@@ -136,7 +137,13 @@ for a in plan:
time.sleep(0.4)
pyautogui.typewrite(pw, interval=0.06)
print("CREDTYPE typed len=%d" % len(pw))
- ctypes.windll.kernel32.LocalFree(ctypes.c_void_p(bout.pbData))
+ ctypes.windll.kernel32.LocalFree(bout.pbData)
+ if op == "text":
+ # type arbitrary (ASCII) text at x,y — used for captcha fields
+ pyautogui.click(a["x"], a["y"])
+ time.sleep(0.3)
+ pyautogui.typewrite(a["s"], interval=0.08)
+ print("TEXT typed len=%d" % len(a["s"]))
if op == "info":
im = pyautogui.screenshot()
print("SIZE_cursor=%s SIZE_bitmap=%s" % (pyautogui.size(), im.size))