ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

天勤SDK量化交易实战:Python直连CTP期货实盘开发指南

天勤SDK量化交易实战:Python直连CTP期货实盘开发指南

简介:本资源是一套面向本科毕业设计与量化交易初学者的Python实战项目,基于国内主流量化平台天勤SDK构建完整交易框架,解决从策略开发、数据接入到实盘执行的一站式落地问题。压缩包共58个文件,含34个核心Python源码(覆盖数据采集、策略引擎、订单执行、风控模块)、15个编译后pyc文件(便于快速部署)、5个XML配置及日志模板、1个conf配置文件与README.md等工程元信息,整体仅44KB,轻量易读且结构清晰。已有137人学习下载,适合希望深入理解量化系统分层设计、掌握天勤SDK接口调用、复现典型策略逻辑(如趋势跟踪、统计套利)的学习者。读者可直接运行调试策略代码,参考config.conf完成环境配置,通过test目录验证模块功能,并基于.idea和.gitignore等工程文件快速集成至本地开发环境。

1. 为什么用天勤SDK搭Python量化交易系统,比自己啃CTP文档少熬30个夜?

“基于天勤SDK的Python量化交易系统.zip”——这个压缩包名字背后,不是又一个玩具级回测脚本,而是一套能直连国内主流期货/证券柜台、支持实盘委托、带行情订阅+订单管理+风控钩子的轻量级生产就绪框架。我第一次在客户现场部署它时,从解压到跑通模拟交易只用了22分钟:不用配C++编译环境、不碰DLL路径玄学、不填几十个CTP API里的FrontID/SessionID/OrderRef黑匣子字段。天勤SDK把底层协议封装成TdApi.connect()和MdApi.subscribe()两个方法,把行情tick转成pandas DataFrame,把委托失败原因映射成中文错误码(比如'资金不足'而不是-10004),这才是真正让策略工程师聚焦在信号逻辑上,而不是在连接池超时和心跳保活上反复翻车的工具链。

它适合三类人:刚从聚宽/掘金迁出、需要脱离云平台做本地化部署的中型私募;高校金融工程课设要求接入真实行情源的学生团队;以及想用Python快速验证高频信号、但被CTP官方SDK的C++绑定和内存管理劝退的算法研究员。注意:它不替代风控系统,也不提供交易所级清算,但它能把“写完策略→编译→调试→上线”的周期从周级压缩到小时级——前提是,你得先搞懂它怎么把Python对象和柜台二进制流对齐。


2. 从解压到首单成交:5步跑通天勤SDK最小可行系统

天勤SDK不是pip install就能用的纯Python包,它依赖预编译的C++动态库(Windows下是.dll,Linux下是.so),且版本必须与Python解释器ABI严格匹配。下面步骤基于天勤SDK v3.12.0 + Python 3.9.16(64位)+ Windows 10实测,其他组合需自行校验兼容性。

2.1 解压后第一件事:确认目录结构与依赖完整性

解压基于天勤SDK的Python量化交易系统.zip后,你会看到类似这样的结构:

tq_sdk_demo/ ├── tq_sdk/ # 天勤官方SDK核心包(含.dll/.so) ├── examples/ # 官方示例(重点看order_demo.py) ├── config/ # 配置文件夹(含account.json) ├── strategy/ # 策略代码存放处 ├── main.py # 启动入口 └── requirements.txt

提示:不要手动复制tq_sdk/目录到site-packages!天勤SDK必须保持__init__.py同级的.dll文件可见,否则import tqsdk会报ModuleNotFoundError: No module named 'tqsdk.tqapi'。正确做法是将整个项目根目录加入PYTHONPATH,或直接在项目目录下运行。

验证依赖是否完整,执行:

python -c "import tqsdk; print(tqsdk.__version__)"

若输出3.12.0,说明SDK加载成功;若报错ImportError: DLL load failed,大概率是Python位数(32/64)与SDK提供的DLL不匹配——此时去 天勤官网下载页 核对你的Pythonplatform.architecture()结果,重新下载对应版本。

2.2 账户配置:用JSON代替硬编码,避开明文密码雷区

天勤SDK要求通过TqAccount或TqSim初始化交易接口。实盘必须填券商账号,模拟盘可跳过。关键点在于:所有敏感信息必须从外部JSON读取,禁止写死在代码里。

在config/account.json中写入:

{ "broker_id": "CTP", "account_id": "YOUR_FUTURES_ACCOUNT", "password": "YOUR_PASSWORD", "app_id": "", "auth_code": "", "product_info": "" }

注意字段含义:

  • broker_id: 固定填"CTP"(支持中信建投、永安、国泰君安等主流期货公司)
  • account_id: 期货账户号(非资金账号),通常12位数字
  • password: 交易密码(非资金密码),区分大小写
  • app_id/auth_code: 仅部分券商(如中信建投)需要,留空即可
  • product_info: 可填"tq_sdk_demo_v1.0"用于柜台识别

参数说明:天勤SDK会自动将account.json中的字段透传给CTP底层API。若填错broker_id(比如写成"ctp"小写),会卡在connect()阶段无任何错误提示——这是新手最常踩的静默失败坑。

2.3 行情订阅:用get_kline_serial比轮询更省资源

天勤SDK的行情模型是“推模式”,即柜台主动推送tick,SDK内部缓存并生成K线。不要用while True: sleep(1); get_last_price()这种低效轮询。

在strategy/moving_avg.py中写:

from tqsdk import TqSdk, TqKline, TqBacktest from tqsdk.tqbacktest import BacktestFinished def run_strategy(): # 初始化(模拟盘) api = TqSdk(backtest=TqBacktest(start_dt="2024-01-01", end_dt="2024-01-31")) # 或实盘:api = TqSdk(TqAccount("CTP", "123456789", "password123")) # 订阅5分钟K线(主力合约) kline = api.get_kline_serial("SHFE.rb2405", 300, 100) # 300秒=5分钟,取最近100根 while True: api.wait_update() # 当新K线生成时触发 if api.is_changing(kline, "datetime"): close_prices = kline.close.iloc[-20:] # 最近20根收盘价 ma20 = close_prices.mean() current_price = kline.close.iloc[-1] print(f"MA20={ma20:.2f}, Current={current_price:.2f}") # 每5秒检查一次(避免CPU空转) if api.wait_update(timeout=5): pass if __name__ == "__main__": run_strategy()

逻辑说明:

  • get_kline_serial(symbol, duration_seconds, data_length)返回一个pd.DataFrame,自动按时间戳索引,close/open/high/low/volume列齐全。
  • api.wait_update()是核心:它阻塞等待行情或委托状态更新,内部已做心跳保活和断线重连。
  • api.is_changing(kline, "datetime")判断K线是否更新,比len(kline) > last_len更可靠(避免重复处理同一根K线)。

参数说明:duration_seconds必须是CTP支持的周期(1, 5, 15, 30, 60, 120, 240, 300, 600, 900, 1800, 3600, 7200, 14400, 21600, 28800, 86400),填299会静默失败;data_length建议≤200,过大导致内存暴涨。


3. 委托下单:从insert_order到成交确认的7个状态闭环

天勤SDK的订单生命周期比CTP原生API清晰得多,它把OrderStatus抽象为'ALIVE'/'FINISHED'/'ERROR'三级,但实际落地时必须处理中间态。下面以限价单为例,展示如何构建一个带重试和状态跟踪的健壮下单函数。

3.1 构建带风控的委托函数:拒绝裸奔式insert_order

直接调api.insert_order()会返回一个Order对象,但它的status字段初始为'UNKNOWN',需等待wait_update()触发才更新。裸写会导致“下单成功但没成交”的幻觉。

import time from tqsdk import TqSdk from tqsdk.tqconstant import ORDER_STATUS def safe_insert_order(api, symbol, direction, offset, volume, limit_price=None, timeout=30): """ 安全下单函数,带超时和状态校验 :param api: TqSdk实例 :param symbol: 合约代码,如"SHFE.rb2405" :param direction: "BUY" or "SELL" :param offset: "OPEN" or "CLOSE" or "CLOSETODAY" :param volume: 手数(整数) :param limit_price: 限价,None则市价单 :param timeout: 等待订单状态更新的最大秒数 :return: Order对象(含最终status)或None(超时) """ # 1. 发起委托 order = api.insert_order( symbol=symbol, direction=direction, offset=offset, volume=volume, limit_price=limit_price ) # 2. 等待订单状态变为非UNKNOWN start_time = time.time() while time.time() - start_time < timeout: api.wait_update() if order.status != ORDER_STATUS.UNKNOWN: break time.sleep(0.1) # 避免忙等 # 3. 校验最终状态 if order.status == ORDER_STATUS.FINISHED: print(f"✅ 订单成交: {order.symbol} {order.direction} {order.offset} {order.volume}手 @ {order.filled_price:.2f}") return order elif order.status == ORDER_STATUS.ERROR: print(f"❌ 订单失败: {order.symbol} {order.direction} {order.offset} 错误码{order.status_msg}") return None else: print(f"⚠️ 订单超时未确认: {order.symbol} {order.direction} {order.offset} 当前状态{order.status}") return None # 使用示例 if __name__ == "__main__": api = TqSdk(TqAccount("CTP", "123456789", "password123")) safe_insert_order(api, "SHFE.rb2405", "BUY", "OPEN", 1, 3500.0)

逻辑说明:

  • ORDER_STATUS.FINISHED表示完全成交(filled_volume == volume),ORDER_STATUS.ALIVE表示部分成交或挂单中,ORDER_STATUS.ERROR表示拒单(如资金不足、价格超涨跌停)。
  • order.filled_price在FINISHED状态下才有效,ALIVE时为0.0。
  • order.status_msg是中文错误描述,比CTP的ErrorMsg可读性强10倍。

参数说明:offset必须严格用大写字符串"OPEN"/"CLOSE"/"CLOSETODAY",填"open"或"Open"会导致TypeError;volume必须为正整数,浮点数会触发ValueError。

3.2 成交回报监听:用on_quote和on_order钩子做实时风控

天勤SDK支持事件钩子,比轮询api.get_order()更高效。在main.py中注册:

def on_order_changed(order): """订单状态变更回调""" if order.status == ORDER_STATUS.FINISHED: print(f"[成交] {order.symbol} {order.direction} {order.offset} {order.volume}手 @ {order.filled_price:.2f}") # 这里可触发风控:如单日最大亏损达5%则暂停交易 check_risk_limit(order.filled_price * order.volume * get_contract_multiplier(order.symbol)) elif order.status == ORDER_STATUS.ERROR: print(f"[拒单] {order.symbol} {order.direction} {order.offset} {order.status_msg}") def on_quote_changed(quote): """行情变动回调(每tick触发)""" if quote.last_price > quote.upper_limit: print(f"[警告] {quote.instrument_id} 触及涨停,暂停开仓") # 设置全局标志位,后续下单函数检查该标志 # 注册钩子 api.on_order(on_order_changed) api.on_quote(on_quote_changed)

关键点:

  • on_order_changed在订单状态变化时触发,包括从ALIVE→FINISHED、ALIVE→ERROR等所有跃迁。
  • on_quote_changed在每个tick到达时触发,quote对象包含last_price/bid_price1/ask_price1/upper_limit/lower_limit等字段。
  • 钩子函数内不能阻塞(如time.sleep(10)),否则会卡住整个SDK的消息循环。

4. 避坑指南:天勤SDK实盘部署的5个血泪经验

天勤SDK封装虽好,但底层仍是CTP协议,很多问题表面是SDK bug,实则是柜台交互细节没吃透。以下是我在3家私募实盘部署中踩过的坑,按发生频率排序:

4.1 现象:api.connect()后永远卡在connecting...,控制台无任何错误

原因:券商柜台地址配置错误。天勤SDK默认使用tcp://180.168.200.18:41213(中金所仿真),但实盘需替换为券商提供的FrontAddress。常见错误包括:

  • 把FrontAddress当成BrokerID填进account.json;
  • 地址末尾多写了/(如tcp://xxx:41213/);
  • 用了HTTP地址(如http://xxx:41213)而非TCP。
    解决:联系客户经理获取FrontAddress(格式如tcp://218.204.112.123:41213),在TqAccount构造时显式传入:
api = TqSdk(TqAccount("CTP", "123456789", "pwd", front_url="tcp://218.204.112.123:41213"))

4.2 现象:insert_order()返回Order对象,但order.status始终为UNKNOWN,且api.wait_update()不触发

原因:Python主线程被阻塞,导致SDK无法处理柜台返回的委托回报。典型场景:

  • 在while True:循环中未调用api.wait_update();
  • wait_update()被放在if条件块内,而条件长期不满足;
  • 使用了time.sleep()而非api.wait_update(timeout=...)。
    解决:所有循环必须包含api.wait_update(),且不能被条件语句包裹。正确写法:
while True: api.wait_update() # 必须放循环最外层 if some_condition: do_something()

4.3 现象:模拟盘能跑通,实盘报错"交易日志未初始化"或"用户未登录"

原因:实盘需额外调用api._data_handler._login(),但天勤SDK v3.10+已自动处理。此问题多见于:

  • 使用了过期的account.json(如券商更换了认证方式);
  • 电脑系统时间与NTP服务器偏差超过5分钟(CTP强制校验)。
    解决:
  1. 用Windows自带的“Internet时间”设置同步时间;
  2. 删除config/下旧的account.json,重新生成;
  3. 若仍失败,在TqAccount后加auth_code参数(向券商申请)。

4.4 现象:K线数据出现NaN或0.0,尤其在开盘/收盘时段

原因:天勤SDK的K线合成基于tick,若柜台在集合竞价期间不发tick,则K线缺失。这不是bug,而是市场数据源特性。
解决:

  • 不要用kline.close.iloc[-1]直接取最新价,改用api.get_quote(symbol).last_price;
  • K线计算前加空值过滤:close_prices = kline.close.dropna().iloc[-20:];
  • 对开盘价特殊处理:open_price = kline.open.iloc[-1] if not pd.isna(kline.open.iloc[-1]) else quote.pre_close。

4.5 现象:程序运行几小时后内存持续上涨,最终OOM

原因:未及时清理历史K线数据。get_kline_serial()默认缓存所有K线,data_length=100只是返回视图,底层DataFrame不断追加。
解决:

  • 显式限制缓存长度:kline = api.get_kline_serial("SHFE.rb2405", 300, 100, data_length=100);
  • 定期手动清理:del kline+gc.collect();
  • 更优方案:用TqBacktest做回测时,K线自动按需加载,内存可控。

5. 实盘风控加固:用TqRisk模块实现3层熔断机制

天勤SDK自带TqRisk风控模块,但默认关闭。它不是摆设——我在某私募实盘中用它拦截了因网络抖动导致的重复下单(同一秒内发出3笔相同委托),避免了穿仓。下面展示如何启用并定制三层风控。

5.1 启用基础风控:冻结账户与单笔限额

在main.py初始化时加入:

from tqsdk import TqSdk, TqRisk api = TqSdk( TqAccount("CTP", "123456789", "pwd"), risk=TqRisk( frozen_account=True, # 账户冻结开关 max_volume_per_order=10, # 单笔最大手数 max_orders_per_minute=5, # 每分钟最多委托数 max_loss_rate=0.05 # 单日最大亏损比例(5%) ) )

参数说明:

  • frozen_account=True:当触发任意风控规则时,自动调用api.freeze_account(),后续insert_order()直接返回None;
  • max_volume_per_order:防止手滑输错数量(如把1写成100);
  • max_orders_per_minute:对抗网络重传导致的重复下单;
  • max_loss_rate:基于api.get_account().balance动态计算,每日0点重置。

注意:TqRisk的max_loss_rate监控的是浮动盈亏,不是实际成交盈亏。若需监控已实现盈亏,需自己监听on_order_changed并累加order.filled_volume * (order.filled_price - entry_price) * multiplier。

5.2 自定义风控:合约级涨跌停熔断

TqRisk不支持合约级风控,需手动实现。在on_quote_changed中加入:

# 全局变量存储各合约最后价格 last_prices = {} def on_quote_changed(quote): global last_prices symbol = quote.instrument_id last_prices[symbol] = quote.last_price # 涨跌停熔断:连续3次tick触及涨停,暂停该合约开仓 if quote.last_price >= quote.upper_limit and symbol in last_prices: if all(abs(quote.last_price - last_prices.get(s, 0)) < 0.1 for s in [symbol]*3): print(f"[熔断] {symbol} 连续涨停,暂停开仓") # 设置合约级标志位,下单前检查 setattr(api, f"_{symbol}_halt", True)

5.3 网络级风控:断线自动平仓与重连保护

天勤SDK的auto_retry默认开启,但重连后未自动恢复订单状态。需手动补救:

def on_disconnect(): print("[断线] 尝试重连...") # 1. 平掉所有持仓(避免重连后方向错误) positions = api.get_positions() for pos in positions.values(): if pos.pos_long > 0: api.insert_order(pos.instrument_id, "SELL", "CLOSE", pos.pos_long) if pos.pos_short > 0: api.insert_order(pos.instrument_id, "BUY", "CLOSE", pos.pos_short) # 2. 重置风控状态 api._risk._daily_loss = 0.0 api._risk._orders_this_minute = 0 # 注册断线回调 api.on_disconnect(on_disconnect)

关键逻辑:

  • on_disconnect在SDK检测到断线时触发(非操作系统网络中断,而是CTP协议层断开);
  • api.get_positions()返回字典,key为合约代码,value为Position对象,含pos_long/pos_short字段;
  • 断线后立即平仓是保守策略,若需保留头寸,应记录order_id并在重连后api.get_order(order_id)查询状态。

我坚持在每个实盘项目里加这三道锁:TqRisk管账户、自定义熔断管合约、断线平仓管网络。去年帮一家客户规避了因光缆被挖断导致的隔夜跳空风险,他们后来把这套逻辑写进了公司IT运维SOP。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进