资讯详情

资讯详情

CTP量化交易Demo:对接SimNow的行情交易K线策略完整实现

简介面向量化交易初学者的CTP官方接口入门Demo演示如何用C搭建行情订阅、交易报单、K线生成与简单策略触发的最小骨架。工程连接simnow模拟环境可避开实盘资金风险完整体验从tick行情接收、K线维护、条件判断到委托回报的闭环流程并理解行情接口与交易接口的分工配合适合作为程序化交易学习的第一份对照代码。源码按职责拆分行情回调处理实时行情推送交易回调处理委托回报与状态更新K线转换辅助类完成Tick到分钟K线的聚合策略模块则在K线之上编写条件判断与下单逻辑。配合内置的多份csv历史行情数据可直接运行观察K线生成结果与策略信号触发过程。压缩包共33个文件整体约1.94MB以h/cpp源码、sln/vcxproj工程配置、dll/lib接口库及csv行情数据为主要组成各类型分工明确结构清晰、便于检索。已有5759人学习使用VS2013及以上环境即可编译运行。无论是想弄懂CTP接口的调用顺序与回调机制还是希望在现成框架上继续扩展自己的策略思路这份Demo都提供了清晰可循的起点。1. 先说结论一个能连 SimNow 跑通的 CTP 量化 Demo很多做量化的同学一开始都被 CTP 官方接口的复杂度劝退——回调函数十几个、字段结构体上百个、还得自己处理行情和交易两套 API。这个项目是一份难得的简化范本它把行情、交易、K 线聚合、策略信号全部打通并且直接对接 SimNow 模拟柜台。拿到手做完配置就能下单不用先啃完一沓接口文档再动手。适合刚接触 CTP 的开发者快速建立「下单、回报、持仓、撤单」的完整链路认知也适合想找一个最小可运行骨架来做二次开发的团队。它不是一个生产级系统但恰好是那个「能跑起来、能看明白、能改」的起点。2. 工程骨架目录结构与三个核心模块的分工2.1 目录结构先看这一层整个项目是 C 写的控制台程序配合 VS 工程组织一眼扫过去目录分层很清晰ctp_demo/ ├── api/ # CTP 官方头文件与动态库 │ ├── ThostFtdcMdApi.h │ ├── ThostFtdcTraderApi.h │ ├── ThostFtdcUserApiStruct.h │ └── lib/ # Windows 下的 thostmduserapi_se.lib 等 ├── config/ # 配置文件柜台地址、账号、合约列表 ├── include/ # 项目自定义头文件 ├── src/ │ ├── md/ # 行情模块 │ ├── trader/ # 交易模块 │ ├── kline/ # K 线聚合模块 │ ├── strategy/ # 策略模块 │ └── main.cpp # 入口 └── output/ # 运行日志与成交记录落盘依赖关系是单向的main 负责初始化三个模块并串起来md 只负责收行情trader 只负责下单撤单查询kline 监听 md 回调进行 K 线聚合strategy 消费 K 线算信号再调 trader 下单。这种分层的直接收获是你不用为了加一个策略去动行情和交易代码只需要往 strategy 目录里加文件。后面讲二次开发的时候这点会非常有用。2.2 配置文件不把地址写死在代码里config 目录下有一个配置文件把 SimNow 的行情地址、交易地址、BrokerID、用户密码全部外置了。这是我最喜欢这个 Demo 的一点CTP 登录一旦写死地址换环境就得重新编译而这里改配置就能切换 SimNow 和实盘柜台的地址。[simnow] md_address180.168.146.187:10130 td_address180.168.146.187:10130 broker_id9999 user_id000001 password888888 appidsimnow_client_test auth_code0000000000000000 [market] instrumentrb2010 sub_interval500 [strategy] kline_interval5 fast_period5 slow_period20这里需要注意appid和auth_code是 SimNow 最新要求的两项认证字段老教程里通常只填用户名密码就能登录现在不填会直接报错。行情和交易共用同一端口是 SimNow 的典型配置实盘柜台这两者通常是分开的。2.3 日志与成交落盘最小可用的调试手段项目在 output 目录下输出两类文件一类是运行日志包含登录状态、订阅是否成功、每次下单的回报另一类是成交记录以 CSV 格式保存每笔成交的时间、合约、手数、价格、开平标志。这种落盘方式虽然简单但对于确认「策略到底有没有真的把单子下出去」很关键——量化里最怕的就是界面看起来没动实际单子已经抛出去了。3. 核心代码拆解从 Tick 到下单的完整链路3.1 初始化 CTP 接口两套 Api一个回调入口CTP 的核心是两套 API行情接口负责接收 Tick 数据交易接口负责下单、撤单、查询持仓和账户。这个 Demo 在 main.cpp 里把两个接口的初始化顺序写得比较清晰。// CThostFtdcMdApi 初始化 CThostFtdcMdApi* pMdApi CThostFtdcMdApi::CreateFtdcMdApi(./output/md/); pMdApi-RegisterSpi(new CMdHandler()); pMdApi-RegisterFront((char*)180.168.146.187:10130); pMdApi-Init(); // CThostFtdcTraderApi 初始化 CThostFtdcTraderApi* pTraderApi CThostFtdcTraderApi::CreateFtdcTraderApi(./output/td/); pTraderApi-RegisterSpi(new CTraderHandler()); pTraderApi-SubscribePrivateTopic(THOST_TERT_RESUME); pTraderApi-SubscribePublicTopic(THOST_TERT_RESUME); pTraderApi-RegisterFront((char*)180.168.146.187:10130); pTraderApi-Init();CreateFtdcMdApi和CreateFtdcTraderApi传入的参数是行情和交易各自的流文件目录CTP 会在这个目录下生成持久化文件。SubscribePrivateTopic和SubscribePublicTopic用的是THOST_TERT_RESUME表示断线重连后自动补全错过的回报数据。如果改成THOST_TERT_QUICK则只取当前状态实盘回调用这个容易漏单这个参数建议不要动。初始化之后框架里专门做了一个同步等待登录回调里会记录登录成功的标志主线程用while (!bLogin)空转等待确保行情和交易都登录成功后才开始订阅行情。这是 CTP 编程里最常见的坑——接口Init是异步的不等待直接订阅会失败。3.2 行情回调与 K 线聚合CTP 行情接口的核心回调是OnRtnDepthMarketData每个 Tick 都会触发一次。这个 Demo 在收到 Tick 后立即更新分钟 K 线代码里用了定时器判断而不是简单计数因为这个接口的 Tick 频率是不均匀的行情停顿的时候也会继续推。void CMdHandler::OnRtnDepthMarketData(CThostFtdcDepthMarketDataField* pData) { if (!pData) return; // 用本地时间判断是否新开一根K线 time_t now time(NULL); if (now - kline_start_time_ kline_interval_ * 60) { // 把上一根K线推到策略队列 if (kline_buf_.count 0) strategy_engine_-OnNewKline(kline_buf_); // 新开一根K线 kline_buf_.open pData-LastPrice; kline_buf_.high pData-LastPrice; kline_buf_.low pData-LastPrice; kline_buf_.close pData-LastPrice; kline_buf_.count 1; kline_start_time_ now; } else { // 更新当前K线的高低点和收盘价 kline_buf_.high max(kline_buf_.high, pData-LastPrice); kline_buf_.low min(kline_buf_.low, pData-LastPrice); kline_buf_.close pData-LastPrice; kline_buf_.count; kline_buf_.volume pData-Volume; } }K 线聚合的粒度在配置里由kline_interval控制Demo 默认是 5 分钟。聚合逻辑里open取第一笔 Tick 价、close取最后一笔这是实盘里和行情软件最常见的差异点——某些软件会把最后一笔的买一价当收盘价导致策略回测和实盘信号对不上。如果你把这个模块拿去改造最值得扩展的是加入 Volume 的累计逻辑和成交额字段目前只存了持仓量变化量做资金流向分析时不够用。3.3 交易模块下单、撤单、查询持仓交易模块的核心在CTraderHandler下单入口是OnRspOrderInsert回报入口是OnRtnOrder和OnRtnTrade。这个 Demo 的代码把三者之间最容易混淆的字段点讲清楚了。void CTraderHandler::OnRspOrderInsert( CThostFtdcInputOrderField* pInputOrder, CThostFtdcRspInfoField* pRspInfo) { if (pRspInfo pRspInfo-ErrorID ! 0) { // 下单被柜台拒绝输出错误码和错误信息 LOG(下单拒绝: %s, pRspInfo-ErrorMsg); return; } // pInputOrder 里能看到我们原始的报单参数 LOG(报单成功: OrderRef%d Price%.2f Vol%d, pInputOrder-OrderRef, pInputOrder-LimitPrice, pInputOrder-VolumeTotalOriginal); }OnRspOrderInsert只代表交易所接受了这笔报单请求不代表已经成交。真正成交要看OnRtnTrade这个回调里会携带TradeID成交编号和Price两个关键字段。很多入门开发者把OnRspOrderInsert当成成交信号结果策略在开盘瞬间重复开仓就是因为没搞懂这个区别。下单参数里OrderRef是本地编号CTP 的OrderSysID才是交易所编号查询回报时要用后者。Demo 里维护了一个从 OrderRef 到 OrderSysID 的映射撤单时通过映射查找正确的交易所报单引用。3.4 策略引擎把信号计算和下单动作拆开策略模块用了最简单的双均线模型核心逻辑就几十行没有引入任何第三方依赖。但代码组织上有意做了分层让我觉得这个 Demo 的架构可以复用。void CStrategyEngine::OnNewKline(const KlineData kline) { // 均线计算维护一个固定长度的价格窗口 price_window_.push_back(kline.close); if (price_window_.size() fast_period_) price_window_.erase(price_window_.begin()); if (price_window_.size() slow_period_) return; // 数据不够不产生信号 double fast_ma CalcMA(fast_period_); double slow_ma CalcMA(slow_period_); if (prev_fast_ma_ prev_slow_ma_ fast_ma slow_ma) { // 金叉开多仓 trader_-BuyOpen(kline.close, 1); } else if (prev_fast_ma_ prev_slow_ma_ fast_ma slow_ma) { // 死叉平多仓 trader_-SellClose(kline.close, 1); } prev_fast_ma_ fast_ma; prev_slow_ma_ slow_ma; }策略引擎通过接口依赖注入的方式拿到交易模块的指针没有直接访问 CTP 的类型这意味着换策略时完全不用动交易模块。金叉死叉的判断用prev_fast_ma_和prev_slow_ma_做差值比较这是为了避免收盘价在小数点后两位的微小波动造成频繁翻转。如果数据量只有一根 K 线就产生金叉信号可能是因为price_window_.size() slow_period_的判断放在均线计算之前就返回了但 Demo 里这个判断放在了push_back之后逻辑上没有问题唯独循环开始时没有清空历史窗口这一点要注意——重新订阅合约时需要显式重置策略状态。3.5 参数表速查参数含义建议值md_address行情服务器地址SimNow: 180.168.146.187:10130td_address交易服务器地址SimNow: 180.168.146.187:10130broker_id经纪商代码SimNow 统一用 9999user_id / password登录账号密码SimNow 官网注册后分配appid / auth_code认证信息SimNow 官网申请后分配kline_intervalK 线周期分钟5fast_period / slow_period双均线周期5 / 20这些参数在实盘环境里全部要换尤其注意 appid 和 auth_code实盘柜台每个用户的认证信息是唯一的互相不能借用。4. 运行与避坑SimNow 登录、模拟交易常见问题排查4.1 跑通这个 Demo 的前置条件环境方面需要 Windows 系统加 VS2017 及以上版本项目里内置了 CTP 的 Windows 动态库不需要额外安装接口文件。SimNow 账号需要提前在官网注册注册后会提供一套用户名密码和十位的 auth_code注意 auth_code 有有效期过期后需要重新申请。编译顺序建议先编译整个解决方案然后打开 config 配置文件把账号密码替换成自己的确认配置文件最后的[strategy]段kline_interval5是 5 分钟而fast_period5和slow_period20是均线周期初次实验不建议动这个参数先用默认值跑通链路再调参。启动后观察日志先看到「行情登录成功」和「交易登录成功」然后会输出「订阅合约 rb2010 成功」大约 10 秒内如果看到「计算 MA5xxxx MA20xxxx」就说明 K 线聚合和策略引擎已经正常工作了。4.2 常见问题一登录一直挂在「正在连接行情前置」现象程序启动后卡在RegisterFront之后的Init调用上没有任何登录回报日志停留在「正在连接行情前置」。原因SimNow 的行情和交易前置地址都写在同一个配置文件里如果只改了交易地址没改行情地址或者把行情地址写成了实盘的 IP柜台端口无法连通时 CTP 接口会一直尝试重连不会主动报错。解决确认配置文件的md_address与td_address用的是 SimNow 官网最新公布的地址不要用网络上的旧文章里的 IP。注意 SimNow 有多个交易组每个组的行情和交易地址是不同的。4.3 常见问题二登录直接报错 3CTP: 不合法的登录现象日志很快输出「Login failed: 3」并没有继续往下走。原因错误码 3 表示用户登录信息不合法最常见的是 BrokerID 写错、账号密码错、auth_code 过期。我在调试时最容易翻车的是把 SimNow 中「投资者账号」当成「用户代码」这两者在 SimNow 控制台是两套不同的字段报名默认的 user_id 字段填的是投资者账号。解决回到 SimNow 官网查到正确的 BrokerID 和投资者账号确认 auth_code 没有过期。如果还是报错 3注释掉 appid 和 auth_code 两个字段试一下老版认证模式有些柜台还在用老认证。4.4 常见问题三能登录但订阅行情没反应现象交易登录成功行情也提示登录成功但是日志里迟迟没有“订阅成功”的输出。原因SubscribeMarketData必须在行情接口登录成功的回调之后调用Demo 里把订阅放在了登录回调之后这个顺序是对的。但如果你改过代码把订阅放到了Init()之后立刻执行就会出现这个问题。还有一个隐蔽原因是合约代码写错了比如螺纹钢主力在 SimNow 里可能是 rb2601配置里写死 rb2010 已经退市。解决把SubscribeMarketData调用放回OnRspUserLogin回调内部同时确认合约代码是当前月份的有效主力合约。可以在 SimNow 的行情查询页面先手动订阅一遍验证合约代码是否存在。4.5 常见问题四下单返回错误码 26CTP: 当前没有可卖持仓现象策略在死叉信号时执行SellClose但日志报错 26单子没下出去。原因SimNow 对平仓的方向字段要求很严格CombOffsetFlag要写成THOST_FTDC_OF_CloseToday或者THOST_FTDC_OF_Close。Demo 里的默认写法是Close但如果前一天有持仓SimNow 会要求区分平今和平昨两个标志不能混用。这个报错在实盘里更常见因为实盘柜台风控更严格。解决在执行平仓前先查一遍持仓明细根据持仓的PositionDate字段判断是今仓还是昨仓再写入对应的偏移标志。Demo 里没有做这个判断导致收盘前平仓的指令有一定概率被柜台拒单这也是为什么实盘前必须先跑一遍 SimNow 验证方向逻辑。5. 二次开发把 Demo 改成你自己的策略框架先聊一个我拿到手就改进的点把策略信号和下单动作中间加了一个「人工确认」开关。默认情况下策略算出金叉直接买入这在模拟盘无所谓但你在实盘前用它做联测时建议在OnNewKline里输出信号后不直接调trader_-BuyOpen而是把信号写入一个队列由交易员人工确认后再下单。最简单的实现是加一个全局 bool 变量bManualConfirm在策略集成交叉信号时只LOG(金叉准备买入等待确认)然后通过控制台输入字符触发下单。这能帮你熟悉整个策略链路又避免实盘开盘瞬间策略自动下单带来风险。第二个值得改的地方是策略参数。现在config.ini里已经外置了fast_period和slow_period但 K 线周期kline_interval改动后策略引擎里的时间窗口没有同步适应比如把 K 线改成 15 分钟均线的 5 和 20 在语义上就从「5 个 5 分钟」变成了「5 个 15 分钟」信号频率会完全变样。我一般会在ReadConfig之后做一次参数校验把每次启动的配置快照写入日志避免跑了一整天发现参数没生效。第三个改造方向是加一个简单的回测后处理脚本。Demo 只输出 CSV 成交记录你可以在策略运行结束后用 Python 读这个 CSV按买卖配对计算收益率曲线。我习惯把每次模拟交易的OrderRef作为主键把OnRtnTrade里的成交价、手数、开平标志解析成一个结构化 DataFrame这样不用写复杂的回测引擎也能看到策略的真实胜率。还有一个小技巧把OnRtnOrder里的Status字段打全。CTP 的报单状态有已报、已撤、部成、全成、拒单等十余种Demo 里只打了IsOk遇到部分成交的极端行情时你会误以为单子全成交了。把OrderStatus映射成可读字符串写在日志里排查问题效率会高很多。从那以后我每次写 CTP 接入代码都会强制自己先跑一遍 SimNow 的完整流程——从行情订阅、下单、撤单到平仓全走一遍确认每一条回报链路都清晰了再上实盘这个习惯帮我避掉了不少盘中才发现的黑匣子问题。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →