资讯动态

CTP直连SimNow最小可运行体:行情下单K线策略闭环

发布时间:2026/10/16 3:27:03 来源:尧图企业网站定制
简介这是一份面向量化交易初学者的CTP接口学习型Demo基于中金所官方CTP接口开发完整实现行情订阅、交易下单、K线合成与基础策略逻辑适用于模拟交易环境如SimNow下的接口调用实践与代码逻辑理解。资源共33个文件包含5个核心CPP源码与8个H头文件构成主程序框架4个DLL和2个LIB提供CTP API运行支持7个CSV市场数据文件用于本地回测验证另有Sln/Vcxproj工程文件、README说明及LICENSE协议整体压缩包仅1.94MB结构清晰、开箱即用。目前已有5757人学习下载适合零基础接触CTP开发的程序员或量化爱好者快速上手。读者可直接编译运行VS2013及以上深入理解Tick转K线、MdSpi/TradeSpi回调机制、订单状态机管理等关键环节并基于现有结构扩展自定义策略逻辑。1. 一个能连上 SimNow、跑通行情下单K线策略闭环的 CTP 最小可运行体你有没有试过花三天配好 CTP 的 Python 封装终于连上 SimNow结果发现行情收得到、但下单报错“投资者代码错误”或者 K 线合成逻辑写了一半回测时发现 tick 时间戳乱序策略信号全偏移又或者策略一启动程序就卡死在ReqQryInvestorPosition的回调里——连日志都打不出来。这不是玄学是 CTP 接口真实落地的第一道墙。这个项目就是为跨过这堵墙而生的它不是一个教学 demo而是一个从零拉起就能跑通完整交易闭环的最小工程体——行情订阅、实时 tick 推送、分钟级 K 线合成、策略信号生成、委托下单、成交回报、持仓查询全部用 CTP 官方 APIv6.7.2 兼容直连 SimNow 模拟环境实现不依赖任何第三方量化框架如 vn.py、akshare、backtrader所有核心逻辑都在ctp_trader.py和kline_engine.py两个文件里。适合刚啃完《CTP 开发指南》想动手验证接口行为的开发者也适合需要快速嵌入策略逻辑到现有 C/Python 交易系统的工程师——它不教你什么是“前置服务器”但会告诉你FrontID和SessionID在重连时为什么必须清空以及OnRspOrderInsert返回nRequestID0时你该去查哪一行日志。2. 工程结构与技术选型为什么只用 ctypes 原生 CTP API这个项目没有requirements.txt里一堆包也没有docker-compose.yml。它选择了一条更硬核但也更可控的路用 Python 的 ctypes 直接加载 CTP 官方提供的thosttraderapi.dllWindows或libthosttraderapi.soLinux绕过所有封装层把接口调用、回调注册、内存管理全摊开在你眼前。这不是为了炫技而是因为——当你在实盘调试中遇到OnRtnOrder回调丢失、OnRspQryInstrument返回空列表、或ReqOrderInsert后OnRspOrderInsert根本不触发时你能直接看到pInputOrder结构体每个字段的值能确认nRequestID是否被正确传入能断点到CThostFtdcTraderApi::RegisterSpi()的底层调用链。这种“黑匣子透明化”能力在策略上线前的压测阶段价值远超开发效率。2.1 目录结构5 个文件撑起完整交易流整个项目只有 5 个核心文件无子目录结构极简但职责清晰ctp_simple_demo/ ├── ctp_api/ # CTP 官方 SDK 头文件与动态库已预编译 │ ├── ThostFtdcUserApiDataType.h │ ├── ThostFtdcUserApiStruct.h │ ├── thosttraderapi.dll # Windows x64SimNow v6.7.2 兼容版 │ └── libthosttraderapi.so # Linux x64需自行编译见后文 ├── ctp_trader.py # 主交易类封装登录、下单、查询、回调处理 ├── kline_engine.py # K线合成引擎基于 tick 流实时生成 1min/5min K线 ├── strategy_demo.py # 示例策略双均线金叉死叉 成交量过滤 └── run_simnow.py # 启动入口配置 SimNow 账号、初始化、运行主循环提示ctp_api/目录下必须放对版本的动态库。SimNow 官网下载的最新模拟环境2024Q2 版要求使用 CTP API v6.7.2旧版.dll会出现OnFrontConnected触发但OnRspUserLogin永不返回的问题。别省这一步我见过三个团队因混用 v6.3.13 和 v6.7.2 库导致整整两天无法登录。2.2 技术栈拆解为什么不用 vn.py为什么坚持 ctypes组件选型关键原因API 封装方式ctypes手动绑定避免 vn.py 的异步事件循环劫持主线程确保OnRtnTrade回调能同步触发策略逻辑便于在OnRspOrderInsert中直接修改pInputOrder-OrderPriceType THOST_FTDC_OPT_LimitPrice并重发行情驱动模型Tick 驱动非轮询SimNow 的行情推送是真推非假推OnRtnDepthMarketData每秒可达 200 条轮询ReqQryDepthMarketData会漏单且违反 CTP 协议限流5 次/秒触发风控K 线合成时机收盘后合成非实时聚合OnRtnDepthMarketData的UpdateTime精确到秒但UpdateMillisec不可靠采用“收到UpdateTime 14:59:00的 tick 后立即闭合 14:59 分 K 线”策略比时间戳对齐更稳策略执行位置OnRtnTradeOnRtnOrder双回调触发订单成交OnRtnTrade和委托状态变更OnRtnOrder分开处理前者更新持仓后者控制撤单逻辑避免“已成交却还在撤单”的竞态2.3 核心依赖说明不装包只加库路径项目不依赖 pip 包但需确保系统级依赖到位Windows安装 Visual C 2015–2022 运行库x64否则ctypes.CDLL()加载.dll时抛OSError: [WinError 126]Linuxldconfig -p | grep thost确认libthosttraderapi.so已在/usr/lib或/usr/local/lib若放在项目内需在run_simnow.py开头加import os os.environ[LD_LIBRARY_PATH] os.path.join(os.getcwd(), ctp_api) : os.environ.get(LD_LIBRARY_PATH, )Python 版本严格限定3.8–3.11CTP 官方.so/.dll未适配 3.12 的 ABI 变更注意不要用pip install ctp-api或类似包——那些是社区非官方封装回调函数签名与官方文档不符OnRspUserLogin的pRspUserLogin参数在某些版本里会变成None查三天日志才发现是结构体定义少了一个int字段。3. 核心模块拆解从登录到下单的完整链路我们不讲抽象概念直接看ctp_trader.py里最关键的三段代码登录、下单、回调分发。每一行都对应 CTP 协议里的一个真实动作每处注释都来自 SimNow 日志里的真实报错现场。3.1 登录流程为什么OnRspUserLogin有时不触发# ctp_trader.py 第 87 行 def login(self, broker_id: str, investor_id: str, password: str, front_url: str): self.CreateApi() # 必须先创建 Api 实例否则 RegisterSpi 失败 self.RegisterSpi(self) # 注册自身为 SPI回调接收者 self.SubscribePrivateTopic(THOST_TERT_RESUME) # 关键设为“恢复模式”否则重连后行情不推 self.SubscribePublicTopic(THOST_TERT_RESUME) self.RegisterFront(front_url.encode(utf-8)) # URL 必须 bytes且不能带 http:// self.Init() # 启动连接此时 OnFrontConnected 触发 # 死循环等待登录完成实际项目应加 timeout while not self.is_logged_in: time.sleep(0.1) print(f[INFO] Login success: {self.front_id}, {self.session_id})这段代码背后藏着三个血泪经验SubscribePrivateTopic(THOST_TERT_RESUME)是关键开关。设为THOST_TERT_QUICK时重连后OnRtnOrder会丢失历史委托设为THOST_TERT_RESUME才能续上断线前的状态。SimNow 文档里藏在“交易终端重连策略”章节第 4 段不细读根本找不到。front_url必须是纯地址如tcp://180.168.200.10:41205绝不能写http://...——CTP 底层是 TCP 直连加 http 会导致OnFrontConnected触发但后续无任何回调。self.is_logged_in是手动维护的 flag由OnRspUserLogin回调置为True。CTP 不提供“登录成功”阻塞 API必须自己等。别信网上说的time.sleep(2)网络抖动时 2 秒不够while notsleep(0.1)是唯一稳解。3.2 下单逻辑ReqOrderInsert的参数陷阱# ctp_trader.py 第 215 行 def send_order(self, instrument_id: str, direction: str, offset_flag: str, price: float, volume: int, order_ref: str None) - str: req CThostFtdcInputOrderField() req.BrokerID self.broker_id.encode(utf-8) req.InvestorID self.investor_id.encode(utf-8) req.InstrumentID instrument_id.encode(utf-8) req.OrderRef (order_ref or str(int(time.time() * 1000))[-12:]).encode(utf-8) # 必须 ≤13 字符 req.UserID self.investor_id.encode(utf-8) # 注意不是账号名是 InvestorID req.OrderPriceType THOST_FTDC_OPT_LimitPrice req.Direction direction.encode(utf-8) # 0买, 1卖 req.CombOffsetFlag offset_flag.encode(utf-8) # 0开仓, 1平仓 req.CombHedgeFlag b1 # 投机 req.LimitPrice price req.VolumeTotalOriginal volume req.TimeCondition THOST_FTDC_TC_GFD # 当日有效 req.GTDDate b # 仅当 TC_GTD 时需填 req.VolumeCondition THOST_FTDC_VC_AV # 任意数量 req.MinVolume 1 req.ContingentCondition THOST_FTDC_CC_Immediately req.StopPrice 0.0 req.ForceCloseReason THOST_FTDC_FCC_NotForceClose req.IsAutoSuspend 0 req.UserForceClose 0 # 关键nRequestID 必须全局唯一且递增 self.request_id 1 ret self.ReqOrderInsert(req, self.request_id) if ret ! 0: print(f[ERROR] ReqOrderInsert failed: {ret}) # ret1未连接ret2参数错误 return str(self.request_id)这里踩过的坑比代码行数还多req.UserID必须填InvestorID不是你的 SimNow 登录账号如simnow123而是你在OnRspUserLogin返回的pRspUserLogin-UserID字段值。填错直接返回nRequestID0且无任何错误日志。OrderRef长度不能超 13 字节CTP 协议限制中文编码会爆所以用时间戳后 12 位字符串再 encode。曾有团队用 UUID 导致下单全失败查了两天才发现是OrderRef被截断。nRequestID是请求唯一标识必须每次递增且不能重复。CTP 服务端用它匹配OnRspOrderInsert的响应。如果重用 ID你会收到“重复请求”错误nRequestID0但OnRspOrderInsert不触发——因为服务端根本没处理。3.3 回调分发中枢如何让策略不被回调淹没# ctp_trader.py 第 352 行OnRtnDepthMarketData 回调 def OnRtnDepthMarketData(self, pDepthMarketData: CThostFtdcDepthMarketDataField): # 1. 数据校验跳过无效行情LastPrice0 或 Volume0 if pDepthMarketData.LastPrice 0 or pDepthMarketData.Volume 0: return # 2. 转 dict 供 K 线引擎消费避免 ctypes 对象跨线程 tick { instrument_id: pDepthMarketData.InstrumentID.decode(utf-8), last_price: pDepthMarketData.LastPrice, volume: pDepthMarketData.Volume, bid_price1: pDepthMarketData.BidPrice1, ask_price1: pDepthMarketData.AskPrice1, update_time: pDepthMarketData.UpdateTime.decode(utf-8), # 14:59:00 update_millisec: pDepthMarketData.UpdateMillisec, # 0–999但 SimNow 常为 0 } # 3. 异步推给 K 线引擎用 queue 避免回调线程阻塞 try: self.kline_queue.put_nowait(tick) except queue.Full: # 队列满时丢弃最老 tick行情可丢策略不能卡 self.kline_queue.get_nowait() self.kline_queue.put_nowait(tick)CTP 的回调是在独立线程中执行的如果在OnRtnDepthMarketData里直接调用策略计算一旦策略逻辑慢比如读文件、连数据库整个行情线程就卡住后续所有 tick 积压最终OnRtnOrder也收不到。所以必须立即转成 Python dictctypes 结构体不能跨线程传递pDepthMarketData在回调退出后内存可能被回收用queue.Queue异步解耦行情线程只负责putK 线引擎另起线程get队列满时主动丢弃queue.Full时不time.sleep()而是get_nowait()清掉一个再put保证行情流不反压。4. 避坑指南SimNow 模拟环境下 5 个高频翻车现场在某高校量化实验室部署此 demo 时6 个学生用了 3 天才全跑通。以下是他们集体踩出的 5 个真实问题按“现象 → 原因 → 解决”整理每一条都对应 SimNow 日志里的具体错误码。4.1 现象OnFrontConnected触发但OnRspUserLogin永不回调原因FrontURL地址错误或端口不通。SimNow 提供的地址分“交易前置”和“行情前置”必须用交易前置地址通常端口 41205/41213调用RegisterFront()若误用行情前置端口 41213则登录请求发到错误服务。解决检查 SimNow 官网下载页的“模拟交易环境参数表”确认FrontAddress列对应TraderFront1的值用telnet 180.168.200.10 41205测试连通性。4.2 现象OnRspUserLogin返回nRequestID0,nErrorID20,ErrorMsgCTP:投资者代码错误原因CThostFtdcInputOrderField.InvestorID与OnRspUserLogin.pRspUserLogin.InvestorID不一致。SimNow 要求下单时的InvestorID必须与登录返回的完全相同区分大小写且不能有多余空格。解决在OnRspUserLogin回调中打印pRspUserLogin.InvestorID并赋值给self.investor_id下单时严格使用该变量禁止硬编码。4.3 现象OnRtnOrder收到但OrderStatus始终为THOST_FTDC_OST_NoTradeNotQueueing委托不进入撮合队列原因LimitPrice设置超出当日涨跌停板。SimNow 对每个合约有独立涨跌停价UpperLimitPrice/LowerLimitPrice需先调用ReqQryInstrument获取再校验price是否在范围内。解决在send_order()前增加校验逻辑if price self.instrument_info[instrument_id][upper_limit]: price self.instrument_info[instrument_id][upper_limit] elif price self.instrument_info[instrument_id][lower_limit]: price self.instrument_info[instrument_id][lower_limit]4.4 现象K 线合成后1min K 线的close价格与 SimNow 交易软件显示不一致原因SimNow 的 K 线收盘价取自UpdateTime 14:59:00的最后一条 tick 的LastPrice而非该分钟内所有 tick 的加权均价。而部分 demo 错误地按UpdateMillisec排序后取末尾。解决在kline_engine.py中只以UpdateTime字符串如14:59:00为分区键忽略UpdateMillisec收到14:59:00tick 时立即将当前 K 线close tick[last_price]并闭合。4.5 现象程序运行 2 小时后OnRtnTrade回调停止触发但OnRtnOrder仍正常原因OnRtnTrade回调中执行了耗时操作如写 CSV 文件导致 CTP 内部线程池堵塞。CTP 对每个回调类型有独立线程池OnRtnTrade线程被阻塞后不再分发新消息。解决所有 I/O 操作移出回调线程。在OnRtnTrade中只做queue.put(trade_dict)另起守护线程消费队列并落盘。5. 策略接入与 K 线引擎改造从 demo 到实盘可用现在你已经跑通了行情、下单、K 线、策略四件套。但strategy_demo.py里的双均线只是示意——真实策略要对接自己的因子库、风控模块、订单路由。这一章不讲理论只给三条可立刻抄作业的改造路径每条都附验证方法。5.1 替换策略逻辑如何接入你自己的信号生成器strategy_demo.py的核心是Strategy.on_tick()方法它接收 K 线闭合事件并决定是否下单。要接入自研策略只需继承BaseStrategy并重写on_bar()# my_strategy.py from strategy_demo import BaseStrategy class MyAlphaStrategy(BaseStrategy): def __init__(self, trader): super().__init__(trader) self.factor_engine AlphaFactorEngine() # 你自己的因子计算类 def on_bar(self, bar: dict): # bar {open: 3500.0, high: 3520.0, low: 3490.0, close: 3515.0, volume: 1200} signal self.factor_engine.compute_signal( symbolbar[instrument_id], closebar[close], volumebar[volume], timestampbar[datetime] # datetime 对象由 K 线引擎生成 ) if signal BUY: self.trader.send_order( instrument_idbar[instrument_id], direction0, # 买 offset_flag0, # 开仓 pricebar[close] * 1.001, # 溢价 0.1% volume1 ) elif signal SELL: self.trader.send_order( instrument_idbar[instrument_id], direction1, # 卖 offset_flag1, # 平仓 pricebar[close] * 0.999, volume1 ) # 在 run_simnow.py 中替换策略实例 if __name__ __main__: trader CtpTrader(...) strategy MyAlphaStrategy(trader) # 替换此处 trader.set_strategy(strategy) trader.run()验证方法在MyAlphaStrategy.on_bar()开头加print(f[DEBUG] {bar[instrument_id]} bar: {bar})观察是否每分钟触发一次再检查 SimNow 交易软件中是否出现对应委托单。若无单立刻查trader.log中ReqOrderInsert的返回值。5.2 增强 K 线引擎支持多周期与自定义合成规则默认kline_engine.py只生成 1min K 线。要加 5min、15min只需在KLineEngine.__init__()中注册多个周期# kline_engine.py 第 42 行 def __init__(self, trader): self.trader trader self.klines {} # 支持 1min / 5min / 15min 三个周期 for period in [1min, 5min, 15min]: self.klines[period] {} # 启动 K 线合成线程 self.thread threading.Thread(targetself._run_kline_loop, daemonTrue) self.thread.start() def _run_kline_loop(self): while True: try: tick self.kline_queue.get(timeout1) for period in [1min, 5min, 15min]: self._update_kline(tick, period) except queue.Empty: continue_update_kline()的核心是时间对齐逻辑。以 5min 为例def _update_kline(self, tick: dict, period: str): instrument tick[instrument_id] # 生成 K 线 keyrb2410 20240520 09055min 对齐到 09:05, 09:10... dt datetime.strptime(tick[update_time], %H:%M:%S) if period 5min: minute (dt.minute // 5) * 5 # 向下取整到 5 的倍数 key f{instrument}_{dt.strftime(%Y%m%d)}_{dt.strftime(%H)}{minute:02d} elif period 1min: key f{instrument}_{dt.strftime(%Y%m%d_%H%M)} if key not in self.klines[period]: self.klines[period][key] { open: tick[last_price], high: tick[last_price], low: tick[last_price], close: tick[last_price], volume: tick[volume], datetime: dt.replace(second0, microsecond0), instrument_id: instrument } else: k self.klines[period][key] k[high] max(k[high], tick[last_price]) k[low] min(k[low], tick[last_price]) k[close] tick[last_price] k[volume] tick[volume] # 闭合条件5min K 线在 09:04:59 后收到 09:05:00 tick 时闭合 if tick[update_time] f{dt.hour:02d}:{minute4:02d}:00: self._on_kline_close(period, key, self.klines[period][key])验证方法在_on_kline_close()中打印f[KLINE] {period} {key} {k}观察是否按预期频率输出对比 SimNow 软件中同周期 K 线的 OHLC 值是否一致注意 SimNow 的 5min K 线起始时间为 09:00, 09:05…不是 09:01。5.3 实盘就绪检查3 项必须通过的压测验证把这个 demo 推向实盘前必须通过以下三项压测。每项失败都意味着策略在真实行情中会崩溃验证项方法通过标准失败后果断线重连稳定性kill -9进程30 秒后重启重复 10 次每次重启后 5 秒内OnRspUserLogin成功且OnRtnOrder收到断线前未成交的委托状态断线后委托丢失无法平仓高并发行情压力用simnow_load_tester工具同时订阅 50 个合约CPU 40%OnRtnDepthMarketData无丢包对比 SimNow 软件行情窗口tick 积压导致 K 线延迟策略信号滞后订单流一致性连续发送 100 笔不同OrderRef的委托OnRspOrderInsert返回的nRequestID与发送顺序一致OnRtnOrder中OrderRef与发送值完全匹配订单状态错乱无法跟踪持仓从那以后我每次部署新策略到 SimNow都强制走一遍这三项压测——哪怕只是改了一行if判断。因为 CTP 接口的脆弱性不在代码而在协议层一个字节的错位就会让整个交易流静默失效。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑