#!/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() # CompatObject(xtquant_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()