chore: init qmt_bridge repo (HTTP+WS bridge, MCP endpoint, docs, references)

This commit is contained in:
Docker
2026-08-26 16:53:15 +08:00
commit 22a5b8ca04
210 changed files with 68176 additions and 0 deletions
@@ -0,0 +1,300 @@
# coding: utf-8
"""Systematically test all RPC APIs and MiniQMT alias mapping (end-to-end validation).
For each method: call it with sensible params, report ok/error, and VALIDATE the
result is actually correct (not just "call succeeded"). Catches silent failures
like:
- get_positions returns {} when the account HAS positions
- submit_order returns SUBMITTED but the order never entered the system
- query_orders returns [] because strategy_name didn't match
- client transport (redis) doesn't match server (zmq) → timeout
Config is read from bigqmt_signal_trader_local_config (gitignored) or env vars;
no credentials are hard-coded here. Run from a dir where that config module
resolves, e.g.:
PYTHONPATH="src;D:\\国金证券QMT交易端\\python" python test_all_apis.py
or set BIGQMT_ACCOUNT_ID / BIGQMT_REDIS_HOST / BIGQMT_REDIS_PORT /
BIGQMT_REDIS_DB / BIGQMT_REDIS_PASSWORD env vars.
"""
import os
import sys
import time
# Add src to path so bigqmt_signal_trader resolves when run from repo root.
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
import redis
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
def _load_account_and_redis():
cfg = {}
try:
import bigqmt_signal_trader_local_config as _c # noqa
cfg = getattr(_c, "BIGQMT_REDIS_CONFIG", {}) or {}
account = getattr(_c, "BIGQMT_ACCOUNT_ID", None) or cfg.get("account_id")
except Exception:
account = None
account = account or os.environ.get("BIGQMT_ACCOUNT_ID", "")
redis_cfg = dict(
host=cfg.get("host") or os.environ.get("BIGQMT_REDIS_HOST", "127.0.0.1"),
port=int(cfg.get("port") or os.environ.get("BIGQMT_REDIS_PORT", 6379)),
db=int(cfg.get("db") or os.environ.get("BIGQMT_REDIS_DB", 5)),
password=cfg.get("password", os.environ.get("BIGQMT_REDIS_PASSWORD", "")),
socket_timeout=15,
)
if not redis_cfg["password"]:
redis_cfg.pop("password")
return str(account), redis_cfg
ACCOUNT, REDIS = _load_account_and_redis()
# account_id placeholder filled in main() once ACCOUNT is confirmed.
_ACCT_PARAM = {"account_id": None}
# (method, params, label) — params chosen to be valid during/after market hours
TESTS = [
# --- 行情快照 ---
("get_full_tick", {"codes": ["000001.SZ"]}, "tick"),
("get_ticks", {"codes": ["000001.SZ"]}, "ticks-alias"),
# --- 合约/品种 ---
("get_instrument", {"code": "000001.SZ"}, "instrument"),
("get_instrument_detail", {"code": "000001.SZ"}, "instrument-alias"),
("get_instrumentdetail", {"code": "000001.SZ"}, "instrument-alias2"),
("get_instrument_type", {"code": "000001.SZ", "variety_list": ["stock", "fund"]}, "inst-type"),
# --- K线/历史 ---
("get_market_data_ex", {"field_list": ["close"], "stock_list": ["000001.SZ"], "period": "1d", "count": 3}, "md-ex"),
("get_market_data", {"field_list": ["close"], "stock_list": ["000001.SZ"], "period": "1d", "count": 3}, "md"),
("get_local_data", {"field_list": ["close"], "stock_list": ["000001.SZ"], "period": "1d", "count": 3}, "local-data"),
# --- 板块 ---
("get_sector_list", {}, "sector-list"),
("get_stock_list_in_sector", {"sector_name": "沪深A股"}, "sector-stocks"),
# --- 交易日历 ---
("get_trading_dates", {"market": "SH", "count": 3}, "trade-dates"),
("get_holidays", {}, "holidays"),
("get_markets", {}, "markets"),
("get_market_last_trade_date", {"market": "SH"}, "last-trade-date"),
# --- 账户 ---
("get_asset", {}, "asset"),
("get_positions", {}, "positions"),
("query_stock_asset", dict(_ACCT_PARAM), "asset-alias"),
("query_stock_positions", dict(_ACCT_PARAM), "positions-alias"),
("query_stock_position", dict(stock_code="000001.SZ", **_ACCT_PARAM), "position-single"),
]
def data_summary(data):
"""One-line summary of returned data for readability."""
if data is None:
return "None"
if isinstance(data, dict):
if not data:
return "{}"
if "__bigqmt_type__" in data:
return "[%s cols=%s records=%d]" % (
data.get("__bigqmt_type__"),
data.get("columns"),
len(data.get("records") or []),
)
keys = list(data.keys())[:3]
return "{%s%s: ...}(%d keys)" % (keys, "" if len(keys) < 3 else ", ...", len(data))
if isinstance(data, list):
return "[list len=%d]" % len(data)
return repr(data)[:60]
def _is_empty(data):
return data is None or data == {} or data == [] or data == ""
def _call(r, method, params, timeout=12):
"""Call and return (response, latency_ms, error_str)."""
t0 = time.time()
try:
resp = call_redis_rpc(r, ACCOUNT, method, params, timeout_seconds=timeout)
return resp, (time.time() - t0) * 1000, None
except Exception as e:
return None, (time.time() - t0) * 1000, str(e)
def main():
if not ACCOUNT:
raise SystemExit("ACCOUNT is empty: set BIGQMT_ACCOUNT_ID or configure bigqmt_signal_trader_local_config")
# Fill the account_id into the account-query test params now that we know it.
for i, (method, params, label) in enumerate(TESTS):
if "account_id" in params and params["account_id"] is None:
params["account_id"] = ACCOUNT
r = redis.Redis(**REDIS)
print("=" * 90)
print("全量 API 测试 (account=%s) — 端到端验证" % ACCOUNT)
print("=" * 90)
# === 端到端验证 0: 客户端/服务端 transport 一致性 ===
print("\n--- 端到端验证: 客户端/服务端一致性 ---")
# 检测客户端配置里的 transport
client_transport = "redis" # 默认
try:
import bigqmt_signal_trader_local_config as _c
client_transport = str(getattr(_c, "BIGQMT_REDIS_CONFIG", {}).get("transport", "redis")).lower()
except Exception:
pass
print("客户端配置 transport: %s" % client_transport)
# 如果客户端是 zmq 但服务端不是, ping 会超时
ping_resp, ping_ms, ping_err = _call(r, "ping", {}, timeout=8)
if ping_err:
print("❌ ping 失败: %s" % ping_err)
if "timeout" in ping_err.lower():
print(" 可能原因: 客户端 transport 和服务端不匹配")
print(" - 客户端配置 transport=%s" % client_transport)
print(" - 如果服务端是 zmq, 客户端也要设 transport=zmq")
print(" - 如果服务端是 redis, 客户端保持 redis 即可")
return
print("✅ ping OK (%.0fms) — 客户端/服务端连通" % ping_ms)
# === 端到端验证 2: 账户有持仓时 get_positions 必须返回非空 ===
print("\n--- 端到端验证: 持仓查询 ---")
pos_resp, pos_ms, pos_err = _call(r, "get_positions", {}, timeout=12)
if pos_err:
print("❌ get_positions 失败: %s" % pos_err)
elif not pos_resp.get("ok"):
print("❌ get_positions 返回错误: %s" % pos_resp.get("error"))
else:
positions = pos_resp.get("data") or {}
if len(positions) > 0:
print("✅ get_positions OK (%.0fms) — 返回 %d 只持仓" % (pos_ms, len(positions)))
else:
print("⚠️ get_positions 返回空 — 账户可能真的没持仓, 或查询失败 (检查 QMT 上下文)")
# === 端到端验证 3: query_orders 验证 (strategy_name 陷阱) ===
print("\n--- 端到端验证: 委托查询 ---")
ord_resp, ord_ms, ord_err = _call(r, "query_orders", {}, timeout=12)
if ord_err:
print("❌ query_orders 失败: %s" % ord_err)
elif not ord_resp.get("ok"):
print("❌ query_orders 返回错误: %s" % ord_resp.get("error"))
else:
orders = ord_resp.get("data") or []
if len(orders) > 0:
print("✅ query_orders OK (%.0fms) — 返回 %d 条委托" % (ord_ms, len(orders)))
else:
print("⚠️ query_orders 返回空 — 可能 strategy_name 不匹配 (默认应为 '' 返回全部)")
# === 端到端验证 4: 买入/卖出后委托必须进系统 ===
print("\n--- 端到端验证: 买入/卖出 (仅交易时段) ---")
# 用极低价格买入 (确保不成交), 然后查委托确认进了系统
# 先拿一只股票的现价
tick_resp, _, tick_err = _call(r, "get_full_tick", {"codes": ["600654.SH"]}, timeout=12)
if tick_err or not tick_resp.get("ok"):
print("⚠️ 跳过买入测试 (get_full_tick 失败: %s)" % (tick_err or tick_resp.get("error")))
else:
d = (tick_resp.get("data") or {}).get("600654.SH", {})
last_close = float(d.get("lastClose") or d.get("lastPrice") or 3.0)
buy_price = round(last_close * 0.8, 2) # 跌停价, 确保不成交
print(" 用 600654.SH @%.2f 买入 100 股 (跌停价, 不成交)" % buy_price)
# 下单前委托数
ord_before, _, _ = _call(r, "query_orders", {}, timeout=12)
before_count = len((ord_before or {}).get("data") or []) if ord_before else 0
# 下单
sub_resp, sub_ms, sub_err = _call(r, "submit_order", {
"stock_code": "600654.SH", "action": "BUY", "volume": 100,
"price": buy_price, "price_type": "LIMIT", "strategy_name": "rpc_test",
"signal_id": "e2e-test-%d" % int(time.time()),
}, timeout=15)
if sub_err:
print("❌ submit_order 失败: %s" % sub_err)
elif not sub_resp.get("ok"):
print("❌ submit_order 返回错误: %s" % sub_resp.get("error"))
else:
server_err = sub_resp.get("server_error") or ""
print("✅ submit_order OK (%.0fms)" % sub_ms)
if server_err:
print(" ⚠️ server_error: %s" % server_err)
# 等 1s 让 QMT 处理, 然后查委托确认进了系统
time.sleep(1)
ord_after, _, _ = _call(r, "query_orders", {}, timeout=12)
after_orders = (ord_after or {}).get("data") or [] if ord_after else []
found = any(
str(o.get("stock_code") or "").upper() == "600654.SH"
and str(o.get("action") or "").upper() == "BUY"
and abs(float(o.get("price") or 0) - buy_price) < 0.01
for o in after_orders
)
if found:
print("✅ 委托已进系统 (query_orders 确认)")
# 尝试撤单
oid = None
for o in after_orders:
if (str(o.get("stock_code") or "").upper() == "600654.SH"
and str(o.get("action") or "").upper() == "BUY"
and abs(float(o.get("price") or 0) - buy_price) < 0.01):
oid = str(o.get("order_sys_id") or "")
break
if oid:
cancel_resp, cancel_ms, cancel_err = _call(r, "cancel_order", {
"order_sys_id": oid, "market": "SH"
}, timeout=15)
if cancel_err:
print("⚠️ cancel_order 失败: %s" % cancel_err)
elif cancel_resp and cancel_resp.get("ok"):
print("✅ cancel_order OK (%.0fms) — 已撤单" % cancel_ms)
else:
print("⚠️ cancel_order 返回: %s" % (cancel_resp or {}))
else:
print("❌ 委托没进系统 — submit_order 成功但 query_orders 找不到")
print(" 这是静默失败 (passorder 被 QMT 拒绝但没报错)")
print(" 检查: 1) 价格是否超出范围 2) 账户权限 3) QMT 风控")
# === 全量 API 测试 ===
print("\n" + "=" * 90)
print("全量 API 测试")
print("=" * 90)
print("%-22s %-8s %-8s %s" % ("method", "ok", "ms", "data summary"))
print("-" * 90)
results = {"ok": [], "ok_empty": [], "fail": [], "timeout": []}
for method, params, label in TESTS:
resp, dt, err = _call(r, method, params, timeout=12)
if err:
is_timeout = "timeout" in err.lower()
bucket = "timeout" if is_timeout else "fail"
results[bucket].append((method, err[:60]))
print("%-22s %-8s %6.0f %s" % (method, "TIMEOUT" if is_timeout else "ERROR", dt, err[:50]))
continue
ok = resp.get("ok")
data = resp.get("data")
error = resp.get("error", "")
server_err = resp.get("server_error", "")
empty = _is_empty(data)
if ok and not empty:
results["ok"].append(method)
status = "OK"
elif ok and empty:
results["ok_empty"].append(method)
status = "EMPTY"
else:
results["fail"].append((method, error))
status = "FAIL"
summary = data_summary(data) if ok else error[:50]
if server_err:
summary += " [server_error: %s]" % server_err[:40]
print("%-22s %-8s %6.0f %s" % (method, status, dt, summary))
print("-" * 90)
print("\n=== 汇总 ===")
print("有数据 (OK): %d" % len(results["ok"]))
print("成功但空 (EMPTY): %d%s" % (len(results["ok_empty"]), results["ok_empty"]))
print("失败 (FAIL): %d%s" % (len(results["fail"]), [m for m, _ in results["fail"]]))
print("超时 (TIMEOUT): %d%s" % (len(results["timeout"]), [m for m, _ in results["timeout"]]))
if __name__ == "__main__":
main()