2b2ae3c4d8
- 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模式)
210 lines
8.3 KiB
Python
210 lines
8.3 KiB
Python
# -*- 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())
|