Files
qmt_bridge/reference/xtquant_big_convert/qmt-trader/scripts/qmt.py
T

1009 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
qmt.py - QMT 交易/行情统一 CLI 入口
设计目标:让大模型通过确定性命令调用 QMT 全部能力(行情、持仓、委托、下单、撤单),
避免每次现场写 Python。所有子命令默认输出 JSON,加 --table 可切换人类可读表格。
配置来源(按优先级):
1. 环境变量 BIGQMT_ACCOUNT_ID / BIGQMT_REDIS_HOST / ...
2. bigqmt_signal_trader_client_config 模块(在 PYTHONPATH 中)
3. bigqmt_signal_trader_local_config 模块
4. 本脚本所在仓库的 src/ 自动加入 sys.path(开发模式)
用法示例:
python qmt.py ping
python qmt.py account
python qmt.py positions
python qmt.py orders --cancelable
python qmt.py trades
python qmt.py tick 600000.SH 000001.SZ
python qmt.py kline 600000.SH --period 1d --count 60
python qmt.py instrument 600000.SH
python qmt.py sector "沪深A股"
python qmt.py trading-dates --count 10
python qmt.py north
python qmt.py longhubang 600000.SH --count 5
python qmt.py buy 600000.SH 100 --price 7.50 --strategy my_strat
python qmt.py sell 600000.SH 100 --price 7.50
python qmt.py cancel 12345 --market SH
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import traceback
from datetime import datetime
from pathlib import Path
# ---------------------------------------------------------------------------
# 路径自动发现:把仓库 src/ 加到 sys.path,确保开发模式也能 import
# ---------------------------------------------------------------------------
def _ensure_src_on_path() -> None:
here = Path(__file__).resolve().parent
# skill/scripts/qmt.py -> 上溯到仓库根
for ancestor in [here, *here.parents]:
candidate = ancestor / "src"
if (candidate / "bigqmt_signal_trader" / "__init__.py").exists():
src_str = str(candidate)
if src_str not in sys.path:
sys.path.insert(0, src_str)
return
# 没找到仓库 src,假设用户已 pip install
return
def _ensure_qmt_python_on_path() -> None:
"""把 QMT 的 python 目录加到 sys.path,让 local_config.py 能被发现。
这样客户端能读到 QMT 端的 transport=zmq 配置(服务端用 zmq 时客户端也得用)。
通过环境变量 BIGQMT_QMT_PYTHON_DIR 指定,或自动从常见路径/当前目录探测。
"""
candidates = []
env_dir = os.environ.get("BIGQMT_QMT_PYTHON_DIR")
if env_dir:
candidates.append(env_dir)
# 常见 QMT 安装路径(国金证券、华泰等)
for root in ("D:\\", "C:\\", "E:\\"):
if not os.path.isdir(root):
continue
try:
for entry in os.listdir(root):
if "QMT" in entry.upper():
p = os.path.join(root, entry, "python")
if os.path.isdir(p):
candidates.append(p)
except Exception:
continue
# 当前工作目录(如果就在 QMT python 目录里)
cwd = os.getcwd()
if cwd.endswith(r"\python") or cwd.endswith("/python"):
candidates.append(cwd)
for c in candidates:
local_cfg = os.path.join(c, "bigqmt_signal_trader_local_config.py")
if os.path.isfile(local_cfg):
if c not in sys.path:
sys.path.insert(0, c)
return
_ensure_src_on_path()
_ensure_qmt_python_on_path()
# ---------------------------------------------------------------------------
# 工具函数
# ---------------------------------------------------------------------------
def _json_default(o):
"""JSON 序列化兜底:处理 pandas / numpy / CompatObject / Decimal 等。"""
# pandas DataFrame / Series
if hasattr(o, "to_dict"):
try:
if hasattr(o, "index") and hasattr(o, "columns"):
# DataFrame -> list of dict records
return o.reset_index().to_dict(orient="records")
return o.to_dict()
except Exception:
pass
# numpy
if hasattr(o, "tolist"):
return o.tolist()
if hasattr(o, "isoformat"):
return o.isoformat()
# CompatObjectxtquant_compat 的属性包对象)
if hasattr(o, "__dict__") and not isinstance(o, type):
return {k: v for k, v in o.__dict__.items() if not k.startswith("_")}
if isinstance(o, (set, tuple)):
return list(o)
if isinstance(o, bytes):
return o.decode("utf-8", errors="replace")
return str(o)
def _print_json(data, indent=2):
print(json.dumps(data, ensure_ascii=False, indent=indent, default=_json_default))
def _print_table(rows, headers=None):
"""简易表格输出。"""
if not rows:
print("(empty)")
return
if isinstance(rows, list) and rows and isinstance(rows[0], dict):
headers = headers or list(rows[0].keys())
lines = []
widths = [max(len(str(h)), *(len(str(r.get(h, ""))) for r in rows)) for h in headers]
lines.append(" ".join(str(h).ljust(w) for h, w in zip(headers, widths)))
lines.append(" ".join("-" * w for w in widths))
for r in rows:
lines.append(" ".join(str(r.get(h, "")).ljust(w) for h, w in zip(headers, widths)))
print("\n".join(lines))
else:
for r in rows:
print(r)
def _ok(data, table=False, headers=None):
out = {"ok": True, "data": data, "ts": datetime.now().isoformat(timespec="seconds")}
if table:
_print_table(data if isinstance(data, list) else [data], headers)
else:
_print_json(out)
def _err(msg, detail=None, code=None):
out = {"ok": False, "error": msg, "ts": datetime.now().isoformat(timespec="seconds")}
if detail:
out["detail"] = detail
if code:
out["code"] = code
_print_json(out)
sys.exit(1)
def _position_to_dict(p):
"""持仓对象转 dict,补充计算字段。"""
d = {}
for attr in [
"account_id", "stock_code", "stock_name", "volume", "can_use_volume",
"available_amount", "enable_amount", "avg_price", "price", "open_price",
"cost_price", "market_value", "frozen_volume", "yesterday_volume",
"direction",
]:
d[attr] = getattr(p, attr, None)
vol = d.get("volume") or 0
price = d.get("price") or 0
d["market_value"] = d.get("market_value") or round(vol * price, 2)
d["profit"] = None
if d.get("avg_price") and vol:
d["profit"] = round((price - d["avg_price"]) * vol, 2)
d["profit_pct"] = round((price - d["avg_price"]) / d["avg_price"] * 100, 2) if d["avg_price"] else 0
return d
def _order_to_dict(o):
d = {}
for attr in [
"account_id", "stock_code", "order_type", "order_status",
"order_volume", "traded_volume", "price", "order_sysid", "order_id",
"strategy_name", "order_remark", "order_time",
]:
d[attr] = getattr(o, attr, None)
# 语义化
d["order_type_name"] = {23: "BUY", 24: "SELL"}.get(d.get("order_type"), str(d.get("order_type", "")))
status_map = {
48: "UNREPORTED", 49: "WAIT_REPORTING", 50: "REPORTED",
51: "REPORTED_CANCEL", 52: "PARTSUCC_CANCEL", 53: "PART_CANCEL",
54: "CANCELED", 55: "PART_SUCC", 56: "SUCCEEDED", 57: "JUNK", 255: "UNKNOWN",
}
d["order_status_name"] = status_map.get(d.get("order_status"), str(d.get("order_status", "")))
d["cancelable"] = d.get("order_status") in (49, 50, 55)
return d
def _trade_to_dict(t):
d = {}
for attr in [
"account_id", "stock_code", "order_type", "order_sysid", "order_id",
"trade_id", "traded_volume", "traded_price", "traded_at", "order_remark",
]:
d[attr] = getattr(t, attr, None)
d["order_type_name"] = {23: "BUY", 24: "SELL"}.get(d.get("order_type"), str(d.get("order_type", "")))
return d
# ---------------------------------------------------------------------------
# 延迟初始化的兼容层对象
# ---------------------------------------------------------------------------
_xt_trader = None
_xtdata = None
_acc = None
def _init():
"""延迟初始化 xt_trader / xtdata / acc,避免 import 失败时整个 CLI 崩溃。"""
global _xt_trader, _xtdata, _acc
if _xt_trader is not None:
return _xt_trader, _xtdata, _acc
try:
from bigqmt_signal_trader.xtquant_compat import (
StockAccount, configure, xt_trader, xtdata,
)
except ImportError as e:
_err(
"无法导入 bigqmt_signal_trader。请确保:\n"
" 1) 已 pip install xtquant-big-convert,或\n"
" 2) 仓库 src/ 在 PYTHONPATH 中,或\n"
" 3) 在仓库目录下运行",
detail=str(e),
code="IMPORT_FAIL",
)
try:
configure()
except Exception as e:
_err(
"configure() 失败。请检查配置:\n"
" - 环境变量 BIGQMT_ACCOUNT_ID / BIGQMT_REDIS_HOST 等\n"
" - 或 bigqmt_signal_trader_client_config.py 配置文件",
detail=str(e),
code="CONFIG_FAIL",
)
_xt_trader = xt_trader
_xtdata = xtdata
try:
_acc = StockAccount(xt_trader.client.account_id, "STOCK")
except Exception:
_acc = None
return _xt_trader, _xtdata, _acc
def _acc_or(acc_arg):
"""用传入的 account_id 或全局 _acc。"""
from bigqmt_signal_trader.xtquant_compat import StockAccount
if acc_arg:
return StockAccount(acc_arg, "STOCK")
_, _, acc = _init()
if acc is None:
_err("无法确定 account_id,请用 --account 显式指定")
return acc
# ===========================================================================
# 子命令实现
# ===========================================================================
def cmd_ping(args):
tr, _, _ = _init()
t0 = time.time()
try:
result = tr.client.call("ping", {})
except Exception as e:
_err("ping 失败", detail=str(e), code="PING_FAIL")
elapsed = round((time.time() - t0) * 1000, 1)
_ok({"result": result, "latency_ms": elapsed})
def cmd_account(args):
tr, _, _ = _init()
acc = _acc_or(args.account)
try:
asset = tr.query_stock_asset(acc)
except Exception as e:
_err("查询资产失败", detail=str(e), code="QUERY_FAIL")
if asset is None:
_err("查询资产返回空")
d = {
"account_id": getattr(asset, "account_id", None),
"cash": getattr(asset, "cash", None),
"available_cash": getattr(asset, "available_cash", None),
"frozen_cash": getattr(asset, "frozen_cash", 0),
"total_asset": getattr(asset, "total_asset", None),
"market_value": getattr(asset, "market_value", None),
}
_ok(d, table=args.table, headers=["account_id", "cash", "frozen_cash", "market_value", "total_asset"])
def cmd_positions(args):
tr, _, _ = _init()
acc = _acc_or(args.account)
try:
positions = tr.query_stock_positions(acc)
except Exception as e:
_err("查询持仓失败", detail=str(e), code="QUERY_FAIL")
if args.code:
positions = [p for p in (positions or []) if getattr(p, "stock_code", "") == args.code]
rows = [_position_to_dict(p) for p in (positions or [])]
# 汇总
summary = {
"count": len(rows),
"total_market_value": round(sum(r.get("market_value") or 0 for r in rows), 2),
"total_profit": round(sum(r.get("profit") or 0 for r in rows), 2),
}
_ok({"positions": rows, "summary": summary}, table=args.table,
headers=["stock_code", "stock_name", "volume", "can_use_volume", "avg_price", "price", "market_value", "profit", "profit_pct"])
def cmd_orders(args):
tr, _, _ = _init()
acc = _acc_or(args.account)
try:
orders = tr.query_stock_orders(
acc,
cancelable_only=args.cancelable,
strategy_name=args.strategy or "",
)
except Exception as e:
_err("查询委托失败", detail=str(e), code="QUERY_FAIL")
rows = [_order_to_dict(o) for o in (orders or [])]
_ok({"orders": rows, "count": len(rows)}, table=args.table,
headers=["stock_code", "order_type_name", "order_status_name", "order_volume", "traded_volume", "price", "order_sysid", "cancelable"])
def cmd_trades(args):
tr, _, _ = _init()
acc = _acc_or(args.account)
try:
trades = tr.query_stock_trades(acc, strategy_name=args.strategy or "")
except Exception as e:
_err("查询成交失败", detail=str(e), code="QUERY_FAIL")
rows = [_trade_to_dict(t) for t in (trades or [])]
_ok({"trades": rows, "count": len(rows)}, table=args.table,
headers=["stock_code", "order_type_name", "traded_volume", "traded_price", "traded_at", "order_sysid"])
def cmd_tick(args):
_, xtdata, _ = _init()
codes = args.codes
if not codes:
_err("请指定股票代码,如: tick 600000.SH 000001.SZ")
try:
ticks = xtdata.get_full_tick(codes)
except Exception as e:
_err("查询行情失败", detail=str(e), code="QUERY_FAIL")
# 精简输出
result = {}
for code, tick in (ticks or {}).items():
t = dict(tick) if hasattr(tick, "items") else {}
# 只保留关键字段
compact = {
"code": code,
"lastPrice": t.get("lastPrice"),
"open": t.get("open"),
"high": t.get("high"),
"low": t.get("low"),
"lastClose": t.get("lastClose"),
"volume": t.get("volume"),
"amount": t.get("amount"),
"bidPrice": (t.get("bidPrice") or [])[:5],
"bidVol": (t.get("bidVol") or [])[:5],
"askPrice": (t.get("askPrice") or [])[:5],
"askVol": (t.get("askVol") or [])[:5],
"time": t.get("time"),
"stime": t.get("stime"),
}
# 涨跌幅
if t.get("lastClose") and t.get("lastPrice"):
compact["change_pct"] = round(
(t["lastPrice"] - t["lastClose"]) / t["lastClose"] * 100, 2
)
result[code] = compact
_ok(result, table=args.table,
headers=["code", "lastPrice", "change_pct", "bidPrice", "askPrice", "volume"])
def cmd_kline(args):
_, xtdata, _ = _init()
fields = args.fields.split(",") if args.fields else None
try:
result = xtdata.get_market_data_ex(
field_list=fields,
stock_list=[args.code],
period=args.period,
start_time=args.start or "",
end_time=args.end or "",
count=args.count,
dividend_type=args.dividend,
fill_data=not args.no_fill,
)
except Exception as e:
_err("查询 K 线失败", detail=str(e), code="QUERY_FAIL")
if not result or args.code not in result:
_err("未获取到 K 线数据", code="NO_DATA")
df = result[args.code]
if df is None or len(df) == 0:
_err("K 线数据为空")
# 转为 list of dict
records = df.reset_index().to_dict(orient="records")
# 精简大数字字段
for r in records:
for k, v in list(r.items()):
if hasattr(v, "item"):
r[k] = v.item()
# 统计
closes = [r.get("close") for r in records if r.get("close") is not None]
stats = {}
if closes:
stats["count"] = len(closes)
stats["first_close"] = closes[0]
stats["last_close"] = closes[-1]
stats["high"] = max(closes)
stats["low"] = min(closes)
stats["change_pct"] = round((closes[-1] - closes[0]) / closes[0] * 100, 2) if closes[0] else None
# 简单均线
if len(closes) >= 5:
stats["ma5"] = round(sum(closes[-5:]) / 5, 3)
if len(closes) >= 20:
stats["ma20"] = round(sum(closes[-20:]) / 20, 3)
if len(closes) >= 60:
stats["ma60"] = round(sum(closes[-60:]) / 60, 3)
_ok({"code": args.code, "period": args.period, "bars": records, "stats": stats},
table=args.table, headers=["time", "open", "high", "low", "close", "volume"])
def cmd_instrument(args):
_, xtdata, _ = _init()
try:
detail = xtdata.get_instrument_detail(args.code)
except Exception as e:
_err("查询合约详情失败", detail=str(e), code="QUERY_FAIL")
if detail is None:
_err("未找到合约: %s" % args.code)
_ok(detail, table=args.table)
def cmd_sector(args):
_, xtdata, _ = _init()
if args.name:
try:
stocks = xtdata.get_stock_list_in_sector(args.name)
except Exception as e:
_err("查询板块成分股失败", detail=str(e), code="QUERY_FAIL")
_ok({"sector": args.name, "count": len(stocks or []), "stocks": stocks or []})
else:
try:
sectors = xtdata.get_sector_list()
except Exception as e:
_err("查询板块列表失败", detail=str(e), code="QUERY_FAIL")
_ok({"sectors": sectors or []})
def cmd_trading_dates(args):
_, xtdata, _ = _init()
try:
dates = xtdata.get_trading_dates(
market=args.market,
start_time=args.start or "",
end_time=args.end or "",
count=args.count if args.count > 0 else -1,
)
except Exception as e:
_err("查询交易日历失败", detail=str(e), code="QUERY_FAIL")
_ok({"dates": dates or []})
def cmd_north(args):
_, xtdata, _ = _init()
try:
data = xtdata.get_north_finance_change(period=args.period or "1d")
except Exception as e:
_err("查询北向资金失败", detail=str(e), code="QUERY_FAIL")
# data 可能是 dict[code -> DataFrame]
if isinstance(data, dict):
result = {}
for k, v in data.items():
if hasattr(v, "to_dict"):
result[k] = v.reset_index().to_dict(orient="records")
else:
result[k] = v
elif hasattr(data, "to_dict"):
result = data.reset_index().to_dict(orient="records")
else:
result = data
_ok({"north_finance": result})
def cmd_longhubang(args):
_, xtdata, _ = _init()
try:
df = xtdata.get_longhubang(
stock_list=[args.code],
start_time=args.start or "",
end_time=args.end or "",
count=args.count if args.count > 0 else 5,
)
except Exception as e:
_err("查询龙虎榜失败", detail=str(e), code="QUERY_FAIL")
if df is None:
_err("龙虎榜数据为空")
if hasattr(df, "to_dict"):
records = df.reset_index().to_dict(orient="records")
else:
records = df
_ok({"longhubang": records})
def cmd_financial(args):
_, xtdata, _ = _init()
tables = args.tables.split(",") if args.tables else ["Capital.CAPITAL"]
try:
data = xtdata.get_financial_data(
stock_list=args.codes,
table_list=tables,
start_time=args.start or "",
end_time=args.end or "",
)
except Exception as e:
_err("查询财务数据失败", detail=str(e), code="QUERY_FAIL")
result = {}
for code, df in (data or {}).items():
if hasattr(df, "to_dict"):
result[code] = df.reset_index().to_dict(orient="records")
else:
result[code] = df
_ok({"financial": result})
def cmd_download(args):
_, xtdata, _ = _init()
try:
result = xtdata.download_history_data2(
stock_list=args.codes,
period=args.period,
start_time=args.start or "",
end_time=args.end or "",
dividend_type=args.dividend,
)
except Exception as e:
_err("下载数据失败", detail=str(e), code="DOWNLOAD_FAIL")
_ok({"download_result": result})
def _place_order(args, action):
"""下单通用逻辑。action = 'BUY''SELL'"""
tr, _, _ = _init()
acc = _acc_or(args.account)
from bigqmt_signal_trader.xtquant_compat import (
STOCK_BUY, STOCK_SELL, FIX_PRICE, LATEST_PRICE,
)
order_type = STOCK_BUY if action == "BUY" else STOCK_SELL
if args.latest:
price_type = LATEST_PRICE
price = 0.0
else:
if args.price is None:
_err("限价单必须指定 --price,或用 --latest 使用最新价")
price_type = FIX_PRICE
price = args.price
strategy = args.strategy or "llm_agent"
remark = args.remark or "llm_%s_%d" % (action.lower(), int(time.time()))
# 干跑模式
if args.dry_run:
_ok({
"dry_run": True,
"action": action,
"stock_code": args.code,
"volume": args.volume,
"price": price,
"price_type": "LATEST" if args.latest else "LIMIT",
"strategy_name": strategy,
"order_remark": remark,
})
# 真实下单
try:
order_id = tr.order_stock(
acc, args.code, order_type, args.volume,
price_type, price, strategy, remark,
)
except PermissionError as e:
_err("下单被拒绝:服务端未开启下单权限(rpc_allow_order_methods=False", detail=str(e), code="ORDER_DISABLED")
except TimeoutError as e:
_err(
"下单超时——委托可能已提交但未收到响应。请先用 query_orders 确认,避免重复下单",
detail=str(e), code="ORDER_TIMEOUT",
)
except Exception as e:
_err("下单失败", detail=str(e), code="ORDER_FAIL")
if order_id == -1:
_err("下单返回 -1(失败),请检查:1) 账户权限 2) 价格范围 3) QMT 风控", code="ORDER_REJECTED")
# 等 0.5 秒后查委托确认
time.sleep(0.5)
try:
orders = tr.query_stock_orders(acc, strategy_name=strategy)
except Exception:
orders = None
placed_order = None
if orders:
for o in orders:
if getattr(o, "order_remark", "") == remark or getattr(o, "order_sysid", "") == str(order_id):
placed_order = _order_to_dict(o)
break
_ok({
"order_sys_id": str(order_id),
"action": action,
"stock_code": args.code,
"volume": args.volume,
"price": price,
"strategy_name": strategy,
"order_remark": remark,
"confirmed_order": placed_order,
})
def cmd_buy(args):
_place_order(args, "BUY")
def cmd_sell(args):
_place_order(args, "SELL")
def cmd_cancel(args):
tr, _, _ = _init()
acc = _acc_or(args.account)
if args.dry_run:
_ok({"dry_run": True, "order_sysid": args.order_id, "market": args.market or ""})
try:
success = tr.cancel_order_stock_sysid(acc, args.market or "", args.order_id)
except Exception as e:
_err("撤单失败", detail=str(e), code="CANCEL_FAIL")
_ok({"order_sysid": args.order_id, "market": args.market or "", "success": bool(success)})
def cmd_snapshot(args):
"""一键快照:资产+持仓+今日委托+今日成交,一次返回。"""
tr, _, _ = _init()
acc = _acc_or(args.account)
result = {}
# 资产
try:
asset = tr.query_stock_asset(acc)
if asset:
result["asset"] = {
"account_id": getattr(asset, "account_id", None),
"cash": getattr(asset, "cash", None),
"frozen_cash": getattr(asset, "frozen_cash", 0),
"total_asset": getattr(asset, "total_asset", None),
"market_value": getattr(asset, "market_value", None),
}
except Exception as e:
result["asset_error"] = str(e)
# 持仓
try:
positions = tr.query_stock_positions(acc)
result["positions"] = [_position_to_dict(p) for p in (positions or [])]
result["position_count"] = len(result["positions"])
except Exception as e:
result["position_error"] = str(e)
# 委托
try:
orders = tr.query_stock_orders(acc, strategy_name="")
result["orders"] = [_order_to_dict(o) for o in (orders or [])]
result["order_count"] = len(result["orders"])
except Exception as e:
result["order_error"] = str(e)
# 成交
try:
trades = tr.query_stock_trades(acc, strategy_name="")
result["trades"] = [_trade_to_dict(t) for t in (trades or [])]
result["trade_count"] = len(result["trades"])
except Exception as e:
result["trade_error"] = str(e)
_ok(result, table=args.table)
def cmd_rpc(args):
"""通用 RPC 调用入口:任意白名单方法 + JSON 参数。
用于调用没有专用子命令的 API(如 get_holidays、get_sector_info、
get_hkt_statistics、bsm_price 等)。
用法:
python qmt.py rpc get_holidays
python qmt.py rpc get_stock_name '{"stock":"600000.SH"}'
python qmt.py rpc bsm_price '{"opt_type":"C","target_price":3.0,"strike_price":2.8,"risk_free":0.03,"sigma":0.3,"days":30}'
"""
tr, _, _ = _init()
params = {}
if args.params:
try:
params = json.loads(args.params)
if not isinstance(params, dict):
_err("params 必须是 JSON 对象(如 '{\"key\":\"value\"}'", code="PARAM_ERROR")
except json.JSONDecodeError as e:
_err("params JSON 解析失败", detail=str(e), code="PARAM_ERROR")
try:
result = tr.client.call(args.method, params)
except Exception as e:
_err("RPC 调用失败: %s" % args.method, detail=str(e), code="RPC_FAIL")
_ok({"method": args.method, "result": result}, table=args.table)
def cmd_quote_subscribe(args):
"""订阅全推行情(打印前 N 条后退出)。"""
_, xtdata, _ = _init()
received = []
def on_quote(data):
for code, tick in (data or {}).items():
entry = {
"code": code,
"lastPrice": tick.get("lastPrice"),
"volume": tick.get("volume"),
"time": tick.get("time"),
}
received.append(entry)
print(json.dumps(entry, ensure_ascii=False))
if len(received) >= args.max:
xtdata.unsubscribe_quote(sub_id)
# 给一点时间让退订生效
time.sleep(0.5)
os._exit(0)
try:
sub_id = xtdata.subscribe_whole_quote(args.codes, callback=on_quote)
except Exception as e:
_err("订阅行情失败", detail=str(e), code="SUBSCRIBE_FAIL")
print("# subscribed id=%s, waiting for quotes (max %d)..." % (sub_id, args.max), file=sys.stderr)
# 等待
timeout = args.timeout
t0 = time.time()
try:
while time.time() - t0 < timeout:
time.sleep(0.5)
except KeyboardInterrupt:
pass
try:
xtdata.unsubscribe_quote(sub_id)
except Exception:
pass
_ok({"sub_id": sub_id, "received": received, "count": len(received)})
# ===========================================================================
# argparse 路由
# ===========================================================================
def build_parser():
p = argparse.ArgumentParser(
prog="qmt.py",
description="QMT 交易/行情统一 CLI(给大模型用)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
p.add_argument("--account", default=None, help="指定账号 ID(覆盖配置)")
p.add_argument("--table", action="store_true", help="输出表格而非 JSON")
sub = p.add_subparsers(dest="command", required=True)
# ping
sub.add_parser("ping", help="连通性检测").set_defaults(func=cmd_ping)
# account
sub.add_parser("account", help="查询账户资产").set_defaults(func=cmd_account)
# positions
sp = sub.add_parser("positions", help="查询持仓")
sp.add_argument("code", nargs="?", default=None, help="可选:只查指定股票")
sp.set_defaults(func=cmd_positions)
# orders
sp = sub.add_parser("orders", help="查询今日委托")
sp.add_argument("--cancelable", action="store_true", help="只查可撤委托")
sp.add_argument("--strategy", default=None, help="按策略名过滤(空=全部)")
sp.set_defaults(func=cmd_orders)
# trades
sp = sub.add_parser("trades", help="查询今日成交")
sp.add_argument("--strategy", default=None, help="按策略名过滤")
sp.set_defaults(func=cmd_trades)
# tick
sp = sub.add_parser("tick", help="实时五档盘口")
sp.add_argument("codes", nargs="+", help="股票代码,如 600000.SH 000001.SZ")
sp.set_defaults(func=cmd_tick)
# kline
sp = sub.add_parser("kline", help="K线/历史行情")
sp.add_argument("code", help="股票代码")
sp.add_argument("--period", default="1d", help="周期: 1d/1m/5m/15m/30m/60m/tick")
sp.add_argument("--count", type=int, default=-1, help="获取根数(-1=全部)")
sp.add_argument("--start", default=None, help="开始日期 YYYYMMDD")
sp.add_argument("--end", default=None, help="结束日期 YYYYMMDD")
sp.add_argument("--fields", default=None, help="字段逗号分隔,如 close,open,volume")
sp.add_argument("--dividend", default="none", help="复权: none/front/back")
sp.add_argument("--no-fill", action="store_true", help="不填充缺失数据")
sp.set_defaults(func=cmd_kline)
# instrument
sp = sub.add_parser("instrument", help="合约详情")
sp.add_argument("code", help="股票代码")
sp.set_defaults(func=cmd_instrument)
# sector
sp = sub.add_parser("sector", help="板块查询")
sp.add_argument("name", nargs="?", default=None, help="板块名(不填则列板块)")
sp.set_defaults(func=cmd_sector)
# trading-dates
sp = sub.add_parser("trading-dates", help="交易日历")
sp.add_argument("--market", default="SH", help="市场 SH/SZ")
sp.add_argument("--count", type=int, default=10, help="获取天数")
sp.add_argument("--start", default=None)
sp.add_argument("--end", default=None)
sp.set_defaults(func=cmd_trading_dates)
# north
sp = sub.add_parser("north", help="北向资金")
sp.add_argument("--period", default="1d")
sp.set_defaults(func=cmd_north)
# longhubang
sp = sub.add_parser("longhubang", help="龙虎榜")
sp.add_argument("code", help="股票代码")
sp.add_argument("--count", type=int, default=5)
sp.add_argument("--start", default=None)
sp.add_argument("--end", default=None)
sp.set_defaults(func=cmd_longhubang)
# financial
sp = sub.add_parser("financial", help="财务数据")
sp.add_argument("codes", nargs="+", help="股票代码")
sp.add_argument("--tables", default=None, help="表名逗号分隔,如 Capital.CAPITAL,Performance.EXPRESS")
sp.add_argument("--start", default=None)
sp.add_argument("--end", default=None)
sp.set_defaults(func=cmd_financial)
# download
sp = sub.add_parser("download", help="下载历史数据到服务端")
sp.add_argument("codes", nargs="+", help="股票代码")
sp.add_argument("--period", default="1d")
sp.add_argument("--start", default=None)
sp.add_argument("--end", default=None)
sp.add_argument("--dividend", default="none")
sp.set_defaults(func=cmd_download)
# buy
sp = sub.add_parser("buy", help="买入下单")
sp.add_argument("code", help="股票代码")
sp.add_argument("volume", type=int, help="委托数量(股)")
sp.add_argument("--price", type=float, default=None, help="限价单价格")
sp.add_argument("--latest", action="store_true", help="用最新价下单")
sp.add_argument("--strategy", default=None, help="策略名")
sp.add_argument("--remark", default=None, help="委托备注/user_order_id")
sp.add_argument("--dry-run", action="store_true", help="只打印不下单")
sp.set_defaults(func=cmd_buy)
# sell
sp = sub.add_parser("sell", help="卖出下单")
sp.add_argument("code", help="股票代码")
sp.add_argument("volume", type=int, help="委托数量(股)")
sp.add_argument("--price", type=float, default=None, help="限价单价格")
sp.add_argument("--latest", action="store_true", help="用最新价下单")
sp.add_argument("--strategy", default=None, help="策略名")
sp.add_argument("--remark", default=None, help="委托备注/user_order_id")
sp.add_argument("--dry-run", action="store_true", help="只打印不下单")
sp.set_defaults(func=cmd_sell)
# cancel
sp = sub.add_parser("cancel", help="撤单")
sp.add_argument("order_id", help="委托号 order_sysid")
sp.add_argument("--market", default=None, help="市场 SH/SZ")
sp.add_argument("--dry-run", action="store_true", help="只打印不撤单")
sp.set_defaults(func=cmd_cancel)
# snapshot
sub.add_parser("snapshot", help="一键快照:资产+持仓+委托+成交").set_defaults(func=cmd_snapshot)
# rpc — 通用 RPC 调用(兜底所有白名单方法)
sp = sub.add_parser("rpc", help="通用 RPC 调用(任意白名单方法 + JSON 参数)")
sp.add_argument("method", help="方法名,如 get_holidays / get_stock_name / bsm_price")
sp.add_argument("params", nargs="?", default=None, help='JSON 参数,如 \'{"stock":"600000.SH"}\'')
sp.set_defaults(func=cmd_rpc)
# ---- 高频快捷命令(转发到 xtdata 对应方法) ----
def _quick(name, help_text, method, params_builder):
def make(args):
tr, xtdata, _ = _init()
params = params_builder(args)
try:
fn = getattr(xtdata, method)
result = fn(**params) if isinstance(params, dict) else fn(*params)
except AttributeError:
# 方法不存在时回退到 RPC 调用
result = tr.client.call(method, params if isinstance(params, dict) else {})
except Exception as e:
_err("%s 失败" % name, detail=str(e), code="QUERY_FAIL")
_ok({"result": result}, table=getattr(args, "table", False))
sp2 = sub.add_parser(name, help=help_text)
sp2.add_argument("args", nargs="*", help="位置参数(按方法签名顺序)")
sp2.set_defaults(func=make)
return sp2
# 节假日
_quick("holiday", "节假日列表", "get_holidays", lambda a: {})
# 股票名称
_quick("stock-name", "股票名称", "get_stock_name", lambda a: {"stock": a.args[0] if a.args else _err("需传股票代码")})
# 品种类型
_quick("instrument-type", "品种类型(stock/fund/etf/bond/index)", "get_instrument_type", lambda a: {"stock_code": a.args[0] if a.args else _err("需传代码")})
# 除权除息因子
_quick("divid-factors", "除权除息因子", "get_divid_factors", lambda a: {"stock_code": a.args[0] if a.args else _err("需传代码"), "start_time": a.args[1] if len(a.args) > 1 else "", "end_time": a.args[2] if len(a.args) > 2 else ""})
# 交易时段
_quick("market-times", "日内交易时段", "get_trade_times", lambda a: {"stockcode": a.args[0] if a.args else "SH"})
# 交易日历(含时段)
_quick("trading-calendar", "交易日历(含时段)", "get_trading_calendar", lambda a: {"market": a.args[0] if a.args else "SH", "start_time": a.args[1] if len(a.args) > 1 else "", "end_time": a.args[2] if len(a.args) > 2 else ""})
# 期权列表
_quick("option-list", "期权列表", "get_option_list", lambda a: {"undl_code": a.args[0] if a.args else _err("需传标的代码"), "dedate": a.args[1] if len(a.args) > 1 else ""})
# BSM 期权定价
_quick("bsm-price", "BSM 期权定价", "bsm_price", lambda a: {"opt_type": a.args[0] if a.args else "C", "target_price": float(a.args[1]) if len(a.args) > 1 else 3.0, "strike_price": float(a.args[2]) if len(a.args) > 2 else 2.8, "risk_free": float(a.args[3]) if len(a.args) > 3 else 0.03, "sigma": float(a.args[4]) if len(a.args) > 4 else 0.3, "days": int(a.args[5]) if len(a.args) > 5 else 30})
# BSM 隐含波动率
_quick("bsm-iv", "BSM 隐含波动率", "bsm_iv", lambda a: {"opt_type": a.args[0] if a.args else "C", "target_price": float(a.args[1]) if len(a.args) > 1 else 3.0, "strike_price": float(a.args[2]) if len(a.args) > 2 else 2.8, "option_price": float(a.args[3]) if len(a.args) > 3 else 0.25, "risk_free": float(a.args[4]) if len(a.args) > 4 else 0.03, "days": int(a.args[5]) if len(a.args) > 5 else 30})
# 港股通统计
_quick("hkt-stats", "港股通统计", "get_hkt_statistics", lambda a: {"stock_code": a.args[0] if a.args else _err("需传代码")})
# 港股通明细
_quick("hkt-details", "港股通明细", "get_hkt_details", lambda a: {"stock_code": a.args[0] if a.args else _err("需传代码")})
# 港股通汇率
_quick("hkt-rate", "港股通汇率", "get_hkt_exchange_rate", lambda a: {})
# 十大股东
_quick("top10-holder", "十大股东", "get_top10_share_holder", lambda a: {"stock_list": [a.args[0] if a.args else _err("需传代码")], "data_name": "holder", "start_time": a.args[1] if len(a.args) > 1 else "", "end_time": a.args[2] if len(a.args) > 2 else ""})
# 股东户数
_quick("holder-num", "股东户数", "get_holder_num", lambda a: {"stock_list": [a.args[0] if a.args else _err("需传代码")]})
# 新股数据
_quick("ipo", "新股数据", "get_ipo_data", lambda a: {})
# 新股申购额度
_quick("ipo-limit", "新股申购额度", "get_new_purchase_limit", lambda a: {})
# 融资融券担保品
_quick("credit-assure", "融资融券担保品合约", "get_assure_contract", lambda a: {})
# 融资融券融券标的
_quick("credit-short", "融券标的合约", "get_enable_short_contract", lambda a: {})
# 负债合约
_quick("credit-debt", "负债合约", "get_debt_contract", lambda a: {})
# 历史 ST
_quick("his-st", "历史 ST 数据", "get_his_st_data", lambda a: {"stock_code": a.args[0] if a.args else _err("需传代码")})
# 指数权重
_quick("index-weight", "指数权重", "get_index_weight", lambda a: {"index_code": a.args[0] if a.args else _err("需传指数代码")})
# 行业
_quick("industry", "行业成分", "get_industry", lambda a: {"industry_name": a.args[0] if a.args else _err("需传行业名")})
# 板块信息
_quick("sector-info", "板块详情", "get_sector_info", lambda a: {"sector_name": a.args[0] if a.args else ""})
# 时间转换
_quick("timetag2dt", "毫秒时间戳转日期", "timetag_to_datetime", lambda a: {"timetag": int(a.args[0]) if a.args else _err("需传毫秒时间戳"), "format": a.args[1] if len(a.args) > 1 else "%Y%m%d %H:%M:%S"})
_quick("dt2timetag", "日期转毫秒时间戳", "datetime_to_timetag", lambda a: {"datetime_str": a.args[0] if a.args else _err("需传日期字符串"), "format": a.args[1] if len(a.args) > 1 else "%Y%m%d%H%M%S"})
# 本地数据(缓存)
_quick("local-data", "本地缓存数据", "get_local_data", lambda a: {"field_list": ["close"], "stock_list": [a.args[0] if a.args else _err("需传代码")], "period": a.args[1] if len(a.args) > 1 else "1d", "count": -1})
# quote-subscribe
sp = sub.add_parser("quote-subscribe", help="订阅全推行情(实时推送)")
sp.add_argument("codes", nargs="+", help="代码或市场,如 SH SZ 600000.SH")
sp.add_argument("--max", type=int, default=10, help="收到 N 条后退出")
sp.add_argument("--timeout", type=int, default=30, help="超时秒数")
sp.set_defaults(func=cmd_quote_subscribe)
return p
def main():
parser = build_parser()
args = parser.parse_args()
if not hasattr(args, "func"):
parser.print_help()
sys.exit(1)
try:
args.func(args)
except SystemExit:
raise
except KeyboardInterrupt:
print("\n(interrupted)", file=sys.stderr)
sys.exit(130)
except Exception as e:
_err("未预期错误", detail=traceback.format_exc(), code="UNEXPECTED")
if __name__ == "__main__":
main()