chore: init qmt_bridge repo (HTTP+WS bridge, MCP endpoint, docs, references)
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user