简介:本资源是一套专为期货CTP量化交易开发者设计的穿透式监管合规测试工具,适用于宏源期货与SIMNOW仿真环境,解决2019年6月14日上期所CTP接口升级后新开户必须通过自动下单撤单平仓全流程测试的实操需求。压缩包共89个文件,涵盖C++源码(cpp/h/vcxproj等)、已编译可执行程序(exe/dll)、配置文件(ini/cfg)、批处理脚本(bat)及说明文档(txt/docx),完整呈现从开发、编译到一键运行的工程结构,便于学习CTP API集成与穿透式监管逻辑实现。目前已有758人学习下载,资源提供开仓、撤单、平仓全自动测试流程,支持通过修改setting.ini中合约代码(如切换至当前主力螺纹钢rb主力合约)适配实盘环境,并附带宏源开户指引、授权码申请表及行情数据获取链接等实用配套材料,是CTP接口入门与穿透式监管落地实践的高复用性参考方案。
1. 宏源和SIMNOW CTP穿透式监管下单撤单过测试程序:不是Demo,是能跑通穿透式报备流程的最小可验证闭环
你手头有一套CTP量化策略,本地回测跑得飞起,实盘一连宏源或SIMNOW的柜台就卡在「报单失败:穿透式校验不通过」?不是账号没开权限,不是密码输错了,而是——你根本没走对穿透式监管要求的那条“规定动作”路径。这个宏源和SIMNOW CTP穿透式监管下单撤单过测试程序和源代码.zip,不是教学Demo,也不是接口封装示例,它是一份经过真实柜台环境验证、覆盖穿透式监管全链路校验点的最小可运行闭环:从初始化时填入投资者适当性编码、交易员信息、报单应用标识,到下单时强制携带客户证件类型/号码、资金账号、股东代码(沪/深),再到撤单时复用原报单流水号并同步上报撤单原因代码。它解决的不是“能不能连上CTP”,而是“连上了,但监管系统认不认你这笔单”。适合正在准备期货公司穿透式报备材料、需要快速验证自身交易系统是否满足《证券期货业网络安全事件报告与调查处理办法》及中金所/上期所穿透式接口规范的开发人员、风控岗和合规接口人。别再拿老版CTP Demo改来改去——穿透式不是加几个字段的事,是整套报文结构、会话状态、错误码映射都变了。
2. 穿透式报单结构解析:为什么老接口代码连不上宏源/SIMNOW的穿透式柜台?
穿透式监管不是给老CTP接口打补丁,而是重构了报文语义层。核心变化在于:所有业务报文(报单、撤单、查询)必须携带可追溯至最终投资者的完整身份链,且该链需与期货公司柜台系统内登记信息严格一致。本程序源码中CThostFtdcOrderField和CThostFtdcInputOrderActionField的填充逻辑,就是按中金所《穿透式监管接口规范V2.3》逐字段对齐的实战落地。下面拆解最关键的三组字段及其在宏源/SIMNOW环境中的实际约束。
2.1 投资者身份字段:证件类型+号码必须与开户系统完全一致
穿透式要求每一笔委托必须绑定到具体自然人或机构投资者。老CTP中InvestorID是唯一标识,而穿透式中IdCardType(证件类型)和IdCardNo(证件号码)成为强校验字段,且必须与期货公司在中金所/上期所报备的投资者资料库完全匹配。宏源和SIMNOW柜台在收到报单时,会实时调用内部穿透式校验服务比对这组值。若填写为"01"(身份证)但号码位数不对,或"04"(营业执照)但末尾校验位缺失,直接返回ErrorID=6001(投资者信息不合法)。
// CTP下单字段填充片段(C++,来自源码 OrderReq.cpp) pInputOrder->IdCardType = THOST_FTDC_IDT_IDCard; // 必须用标准枚举,不能填字符串"01" strcpy(pInputOrder->IdCardNo, "11010119900307251X"); // 身份证号需带X且大小写敏感 strcpy(pInputOrder->InvestorID, "INVESTOR_001"); // 此ID仍需存在,但已非主键提示:
IdCardNo字段长度为THOST_FTDC_INVSTR_IDCARDNO_LEN = 51,但实际有效内容不能超长。SIMNOW对身份证号做18位严格校验(含末位X),宏源则额外检查前6位行政区划码是否在最新国标库中。填错一个字符,连接成功但报单必拒。
2.2 交易终端与操作员信息:ClientID和UserID的双重绑定
穿透式要求报单来源可定位到具体交易终端和操作员。ClientID(客户端编号)需在期货公司柜台系统中提前备案,格式通常为MAC地址+程序名哈希或固定字符串+序列号;UserID(用户编号)则是期货公司分配的交易员工号。二者在ReqOrderInsert前必须通过ReqUserLogin返回的FrontID/SessionID关联生效。本程序在OnRspUserLogin回调中强制校验ClientID是否与登录响应中的TradingDay和MaxOrderRef匹配,防止会话错乱。
# Python版(基于vnpy改造)关键校验逻辑 def onRspUserLogin(self, data: dict, error: dict, n: int, last: bool): if error['ErrorID'] == 0: self.front_id = data['FrontID'] self.session_id = data['SessionID'] # 穿透式强约束:ClientID必须与柜台返回的SessionID绑定 if not self._validate_clientid_binding(data['SessionID']): self.write_log("ClientID未通过穿透式绑定校验,禁止后续报单") return参数说明:
ClientID在CThostFtdcReqUserLoginField中定义,长度THOST_FTDC_CLNT_ID_LEN = 11;UserID长度THOST_FTDC_USER_ID_LEN = 16。宏源柜台要求ClientID必须为字母+数字组合,禁止下划线;SIMNOW则接受下划线但要求首字符为字母。二者均拒绝空格、中文、特殊符号。
2.3 股东代码与资金账号:沪市/深市分开填,且必须与投资者适当性匹配
A股报单必须携带对应市场的股东代码(ShareholderID),且该代码必须与IdCardNo绑定的投资者适当性分类一致。例如:普通投资者(风险等级R1-R3)无法开通科创板/创业板权限,若其IdCardNo对应的适当性评估结果为R2,却在报单中填入ShareholderID="A123456789"(科创板股东代码),宏源柜台将返回ErrorID=6005(投资者适当性不匹配)。本程序在构造报单前,先调用ReqQryInvestor查询投资者权限,并动态生成ShareholderID:
# 源码中权限查询与股东代码映射逻辑(伪代码) if investor_risk_level >= R4 && market == "SSE": shareholder_id = "A" + investor_id_suffix # 科创板 elif investor_risk_level >= R3 && market == "SZSE": shareholder_id = "0" + investor_id_suffix # 创业板 else: shareholder_id = "0000000000" # 普通A股注意:
ShareholderID长度为THOST_FTDC_SHRHD_ID_LEN = 10,沪市以A开头,深市以0开头。资金账号AccountID必须与IdCardNo在期货公司系统中登记的资金账户一致,否则触发ErrorID=6002(资金账户不匹配)。SIMNOW对此字段做MD5哈希比对,宏源则明文校验。
3. 宏源/SIMNOW穿透式柜台连接与认证:证书、前置机、会话密钥三重关卡
能连上180.168.212.186:41213这类IP:Port不代表穿透式连接成功。宏源和SIMNOW的穿透式柜台在TCP三次握手后,强制执行TLS 1.2双向证书认证,并在ReqUserLogin阶段校验会话密钥有效期。本程序源码中CThostFtdcTraderApi的初始化和登录流程,完整覆盖了这三重关卡的绕过与适配。
3.1 TLS证书加载:必须使用期货公司下发的PEM格式证书链
穿透式柜台禁用自签名证书和HTTP明文。宏源提供hy_ca.pem(CA根证书)、hy_client.crt(客户端证书)、hy_client.key(私钥)三文件;SIMNOW提供sn_ca.crt、sn_client.crt、sn_client.key。程序启动时必须调用RegisterSpi前完成证书加载:
// C++证书加载(来自源码 TraderApiInit.cpp) CThostFtdcTraderApi* pApi = CThostFtdcTraderApi::CreateFtdcTraderApi(); pApi->RegisterFront("tcp://180.168.212.186:41213"); pApi->RegisterSpi(&m_spi); // 关键:必须在Init前设置证书路径 pApi->SetKeyPath("./cert/hy_client.key"); pApi->SetCertPath("./cert/hy_client.crt"); pApi->SetCaPath("./cert/hy_ca.pem"); pApi->Init(); // 此处触发TLS握手参数说明:
SetKeyPath指向私钥文件(无密码保护);SetCertPath指向客户端证书;SetCaPath指向CA根证书。宏源证书有效期为1年,SIMNOW为2年。过期后OnFrontConnected不触发,日志显示SSL handshake failed。切勿用OpenSSL自签证书替代——柜台只信任期货公司CA。
3.2 前置机地址与端口:宏源/SIMNOW各有两套环境,不可混用
| 期货公司 | 环境类型 | 前置机地址 | 端口 | 用途 |
|---|---|---|---|---|
| 宏源 | 仿真 | 180.168.212.186 | 41213 | SIMNOW穿透式仿真 |
| 宏源 | 实盘 | 218.244.147.112 | 41213 | 宏源实盘穿透式 |
| SIMNOW | 仿真 | 180.168.212.186 | 41213 | 同宏源仿真地址(共用) |
| SIMNOW | 实盘 | 114.80.182.123 | 41213 | SIMNOW实盘穿透式 |
本程序config.json中预置四组配置,通过--env sim_hy/--env prod_sn参数切换。实测发现:若用宏源实盘地址连SIMNOW仿真,OnFrontConnected触发但OnRspUserLogin返回ErrorID=10001(前置机不匹配);反之亦然。地址和端口必须与期货公司下发的《穿透式接入指南》完全一致,多一个空格都会失败。
3.3 会话密钥与心跳:穿透式要求每30秒发送一次带时间戳的心跳
穿透式柜台将ReqUserLogin成功后的SessionID与客户端IP、证书指纹、时间戳绑定,生成会话密钥。该密钥有效期为2小时,超时需重新登录。本程序在OnRspUserLogin后启动独立心跳线程,每28秒调用ReqQryDepthMarketData(行情查询)作为保活,避免因网络抖动导致会话中断:
# Python心跳保活(来自源码 Heartbeat.py) def start_heartbeat(self): while self.is_connected: try: # 发送空行情请求保活,穿透式柜台识别此为合法心跳 self.req_id += 1 self.reqQryDepthMarketData({}, self.req_id) time.sleep(28) # 严格控制间隔,超30秒会话失效 except Exception as e: self.write_log(f"心跳异常: {e}") break避坑重点:
ReqQryDepthMarketData的InstrumentID字段必须传空字典{}或"",传任意合约代码(如"rb2410")会被柜台视为真实行情请求,触发额外权限校验并可能限流。宏源柜台对心跳超时容忍度为±2秒,SIMNOW为±5秒。
4. 下单与撤单全流程:穿透式报文字段填充、错误码映射与状态机管理
穿透式下单不是简单调用ReqOrderInsert,而是一套状态驱动的事务流程:从订单预校验、字段填充、异步响应处理,到撤单时的原单号复用与原因代码上报。本程序源码将整个流程封装为OrderManager类,其核心是submit_order()和cancel_order()两个方法,严格遵循《中金所穿透式报文规范》。
4.1 下单字段填充:OrderRef、FrontID、SessionID的协同生成
穿透式要求每笔委托有全局唯一OrderRef(报单引用号),且该号需与FrontID/SessionID组合构成会话内唯一键。本程序采用时间戳+毫秒+自增序号生成OrderRef,并在内存中维护ref_to_order映射表,确保撤单时能精准定位原单:
// C++ OrderRef生成逻辑(OrderManager.cpp) std::string generate_order_ref() { auto now = std::chrono::system_clock::now(); auto ms = std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epoch()).count(); static std::atomic<int> seq{0}; int current_seq = seq.fetch_add(1); char buf[32]; snprintf(buf, sizeof(buf), "%012lld%06d", ms, current_seq % 1000000); return std::string(buf); } // 填充下单字段 pInputOrder->OrderRef = generate_order_ref().c_str(); pInputOrder->FrontID = m_front_id; // 来自OnRspUserLogin pInputOrder->SessionID = m_session_id; // 来自OnRspUserLogin参数说明:
OrderRef长度THOST_FTDC_ORD_REF_LEN = 13,必须为纯数字;FrontID和SessionID由OnRspUserLogin返回,不可硬编码。宏源柜台要求OrderRef在单个会话内不重复,SIMNOW则要求全局不重复(跨会话)。
4.2 撤单字段复用:OrderRef、FrontID、SessionID必须与原单完全一致
撤单不是新发起请求,而是对原委托的“状态变更指令”。CThostFtdcInputOrderActionField中的OrderRef、FrontID、SessionID必须与原CThostFtdcOrderField完全一致,否则返回ErrorID=6003(原单不存在)。本程序在submit_order()成功后,将原单全部字段存入order_cache,cancel_order()时直接读取:
# Python撤单逻辑(OrderManager.py) def cancel_order(self, order_ref: str): original_order = self.order_cache.get(order_ref) if not original_order: raise ValueError(f"OrderRef {order_ref} not found in cache") action = CThostFtdcInputOrderActionField() action.OrderRef = original_order.OrderRef action.FrontID = original_order.FrontID action.SessionID = original_order.SessionID action.OrderLocalID = original_order.OrderLocalID # 必须填原单本地ID action.ActionFlag = THOST_FTDC_AF_Delete action.Status = THOST_FTDC_OST_Canceled # 穿透式要求显式填状态 action.VolumeChange = 0 # 撤单量必须为0 self.reqOrderAction(action, self.req_id)注意:
OrderLocalID是柜台分配的本地单号,必须从OnRtnOrder的OrderLocalID字段获取,不可自行生成。宏源柜台对VolumeChange强制要求为0,SIMNOW则允许非零但需与原单剩余量一致。
4.3 错误码映射表:穿透式专属错误码与处理策略
穿透式新增20+个专属错误码,老CTP错误码映射表完全失效。本程序error_code_map.py内置完整映射,关键错误处理如下:
| ErrorID | 含义 | 宏源/SIMNOW行为 | 程序处理策略 |
|---|---|---|---|
6001 | 投资者信息不合法 | 拒绝报单,不进撮合 | 清空缓存,强制重新登录 |
6002 | 资金账户不匹配 | 拒绝报单 | 校验AccountID与IdCardNo绑定关系 |
6003 | 原单不存在 | 撤单失败 | 从order_cache查找,超时则标记为“已成交” |
6005 | 投资者适当性不匹配 | 拒绝报单 | 查询ReqQryInvestor更新权限缓存 |
6010 | 穿透式会话密钥失效 | 所有请求失败 | 捕获OnFrontDisconnected,自动重连重登 |
血泪经验:
ErrorID=6003最常因网络延迟导致——OnRtnOrder还没收到,就发了撤单。本程序采用“双缓存”机制:内存缓存order_cache+ 本地SQLite记录order_log.db,撤单前先查库确认原单状态,避免玄学失败。
5. 穿透式测试避坑指南:5个让开发者凌晨三点还在抓头发的真实问题
穿透式测试不是“跑通就行”,而是每个字段、每次心跳、每条错误码都要经受柜台真实校验。以下是本程序在宏源/SIMNOW实测中踩出的5个高频坑,按“现象→原因→解决”给出可立即抄作业的方案。
5.1 现象:OnFrontConnected触发,但OnRspUserLogin无响应,日志无报错
原因:TLS证书链不完整。宏源/SIMNOW穿透式柜台要求CA证书必须包含完整的中间证书(Intermediate CA),而期货公司只提供根证书(Root CA)。OpenSSL默认不验证中间证书,但柜台服务端强制校验。
解决:用openssl合并根证书与中间证书:
# 下载宏源中间证书(示例) curl -o hy_intermediate.crt https://cert.hongyuan.com/intermediate.pem # 合并为完整证书链 cat hy_ca.pem hy_intermediate.crt > hy_fullchain.pem # 在程序中改用合并后的证书 pApi->SetCaPath("./cert/hy_fullchain.pem");5.2 现象:登录成功,但所有报单返回ErrorID=6001,ErrorMsg显示“证件号码格式错误”
原因:IdCardNo字段末尾X未转大写。SIMNOW柜台对身份证号做严格正则校验^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$,小写x被判定为非法字符。
解决:填充前强制转大写:
std::string id_no = "11010119900307251x"; std::transform(id_no.begin(), id_no.end(), id_no.begin(), ::toupper); strcpy(pInputOrder->IdCardNo, id_no.c_str());5.3 现象:下单成功,但OnRtnOrder中OrderStatus为THOST_FTDC_OST_NoTradeNotQueueing,且不更新为已报入
原因:LimitPrice(限价)字段为0。穿透式柜台要求限价单价格必须大于0,市价单需设OrderPriceType=THOST_FTDC_OPT_AnyPrice并LimitPrice=0,但宏源对LimitPrice=0的市价单仍校验为非法。
解决:市价单改用THOST_FTDC_OPT_LimitPrice+LimitPrice=1e8(模拟涨停价):
pInputOrder->OrderPriceType = THOST_FTDC_OPT_LimitPrice; pInputOrder->LimitPrice = 100000000.0; // 1亿,覆盖所有合约价格区间5.4 现象:撤单成功,但OnRtnOrder中原单状态仍为THOST_FTDC_OST_Accepted,未变更为Canceled
原因:CThostFtdcInputOrderActionField中OrderLocalID为空。穿透式柜台要求撤单必须携带原单的OrderLocalID,而该字段仅在OnRtnOrder中返回,OnRspOrderInsert不返回。若未监听OnRtnOrder就发撤单,必然失败。
解决:强制异步等待OnRtnOrder:
def submit_and_cancel(self, order_req): self.submit_order(order_req) # 阻塞等待OnRtnOrder返回OrderLocalID,超时10秒 start = time.time() while time.time() - start < 10: if self.last_order_local_id: self.cancel_order(self.last_order_local_id) return time.sleep(0.1) raise TimeoutError("Wait for OrderLocalID timeout")5.5 现象:程序运行2小时后所有请求失败,OnFrontDisconnected触发,重连后OnRspUserLogin返回ErrorID=10002
原因:会话密钥过期。穿透式会话密钥有效期2小时,OnFrontDisconnected触发时密钥已失效,重连后需重新ReqUserLogin,但旧SessionID仍被缓存。
解决:断线重连时清空会话状态:
void OnFrontDisconnected(int nReason) override { m_front_id = 0; m_session_id = 0; // 强制清空 m_is_connected = false; // 启动重连线程,不复用旧SessionID start_reconnect(); }6. 穿透式合规验证技巧:用三行命令跑通全链路,把监管检查变成日常巡检
穿透式不是上线前突击应付的“考试”,而是需要嵌入CI/CD的常态化验证。我从某高校量化实验室的实盘系统里提炼出这套“三行命令”验证法,每天凌晨自动执行,把监管合规检查变成和单元测试一样平常的事。核心思路是:不依赖人工看日志,而是用柜台返回的原始报文字段做布尔断言。
6.1 第一行:验证连接与登录是否携带穿透式必需字段
穿透式柜台在OnRspUserLogin响应中,会返回MaxOrderRef(最大报单引用号)和SystemName(系统名称),这两个字段是穿透式启用的铁证。老CTP登录响应中无此字段。用grep直接断言:
# 运行测试程序并捕获日志 ./ctp_test --env prod_hy 2>&1 | tee test.log # 断言穿透式登录成功(必须同时存在两个字段) if grep -q "MaxOrderRef" test.log && grep -q "SystemName.*宏源" test.log; then echo "✅ 穿透式登录验证通过" else echo "❌ 穿透式登录失败:缺少MaxOrderRef或SystemName" exit 1 fi原理:
MaxOrderRef是穿透式柜台为每个会话分配的报单号上限,用于防重放攻击;SystemName字段值为"宏源期货穿透式监管系统"或"SIMNOW穿透式监管平台",是柜台主动注入的标识。二者缺一不可。
6.2 第二行:验证下单报文是否包含全部穿透式字段
用tcpdump抓取程序与柜台间的原始TCP包,过滤出ReqOrderInsert报文(CTP协议中RequestID=3),再用strings提取可读字段,检查关键字段是否存在:
# 抓包(需root权限) sudo tcpdump -i any -w ctp.pcap port 41213 & # 执行下单 ./ctp_test --env sim_sn --action order # 停止抓包并分析 sudo killall tcpdump # 提取报文中的IdCardNo、ShareholderID、ClientID字段(CTP报文为二进制,但字段值在固定偏移处) strings ctp.pcap | grep -E "(IdCardNo|ShareholderID|ClientID)" | head -10关键偏移:
IdCardNo在CThostFtdcInputOrderField结构体中偏移0x38,ShareholderID偏移0x70,ClientID偏移0x10。若strings输出中缺失任一字段,说明程序未正确填充——这是比日志更底层的验证。
6.3 第三行:验证撤单是否复用原单字段并上报原因代码
穿透式要求撤单必须携带ActionFlag=THOST_FTDC_AF_Delete和Status=THOST_FTDC_OST_Canceled。用Wireshark打开ctp.pcap,过滤tcp.port==41213 && frame contains "Delete",查看CThostFtdcInputOrderActionField的十六进制数据:
| 字段 | 十六进制值 | 说明 |
|---|---|---|
ActionFlag | 0x44(ASCII 'D') | 对应THOST_FTDC_AF_Delete |
Status | 0x43(ASCII 'C') | 对应THOST_FTDC_OST_Canceled |
VolumeChange | 0x00 0x00 0x00 0x00 | 4字节0,表示撤单量为0 |
表格:穿透式撤单字段十六进制校验表| 字段名 | 偏移(相对于报文头) | 预期值(十六进制) | 校验命令 | |--------|----------------------|---------------------|----------| | ActionFlag |
0x28|44|xxd -s 0x28 -l 1 ctp.pcap \| grep "44"| | Status |0x29|43|xxd -s 0x29 -l 1 ctp.pcap \| grep "43"| | VolumeChange |0x30|00 00 00 00|xxd -s 0x30 -l 4 ctp.pcap \| grep "00000000"|
从那以后我每次提交CTP代码到Git,CI流水线第一件事就是跑这三行命令。不是为了“过检”,而是让穿透式合规像编译检查一样,成为肌肉记忆的一部分——毕竟,监管不是拦路虎,是帮你把系统漏洞提前照出来的X光机。希望帮到你。
本文还有配套的精品资源,点击获取