行情数据模拟:CTP行情协议解析、模拟行情生成器(Tick级)、行情回放与加速
做高频交易,最怕什么?
怕实盘跑得好好的,一上压力测试就崩。更怕的是——你根本不知道行情数据是怎么来的,模拟出来的数据跟实盘完全是两码事。今天我就把这块硬骨头啃下来,咱们聊聊CTP行情协议怎么解析、Tick级模拟行情怎么生成、以及行情回放加速的那些坑。
CTP行情协议解析:别被二进制吓到
CTP的行情协议,说白了就是一套二进制编码。我第一次接触时也头大,满屏的十六进制看得眼晕。但后来发现,只要抓住几个关键点,解析起来其实挺顺的。
核心要点:CTP行情数据以DepthMarketData结构体为核心,包含合约代码、最新价、成交量、买卖盘口等字段。每个字段都有固定偏移和长度。
举个例子,一个典型的Tick数据包长这样:
struct CThostFtdcDepthMarketDataField {
char TradingDay[9]; // 交易日
char InstrumentID[31]; // 合约代码
char ExchangeID[9]; // 交易所
char LastPrice[9]; // 最新价(字符串,需转换)
int Volume; // 成交量
double Turnover; // 成交额
int OpenInterest; // 持仓量
double UpperLimitPrice; // 涨停价
double LowerLimitPrice; // 跌停价
// ... 还有几十个字段
};
嗯,这里要注意:CTP的行情数据是大端序的。我当年在项目中就吃过这个亏,解析出来的价格全是反的,排查了半天才发现是字节序没转。
我的习惯:写一个通用的CTP行情解析器,把字节流直接映射到结构体。用memcpy逐字段拷贝,比手动解析快得多。但记得做字节序转换——特别是价格字段,CTP用的是字符串表示,省去了浮点精度问题。
模拟行情生成器(Tick级):从零造数据
为什么要自己造Tick数据?
因为实盘数据太少了。你想测试系统在极端行情下的表现,比如一秒内价格波动5%,实盘数据里根本找不到这种场景。所以,我们需要一个能按需生成Tick级行情的模拟器。
我个人习惯用随机游走模型来生成价格序列。核心逻辑很简单:
// 伪代码:Tick级价格生成
double currentPrice = 5000.0; // 初始价格
double volatility = 0.0002; // 波动率(万分之二)
double tickSize = 0.2; // 最小变动价位
while (true) {
// 生成随机步长,符合正态分布
double step = normalRandom() * volatility * currentPrice;
// 对齐到最小变动价位
step = round(step / tickSize) * tickSize;
currentPrice += step;
// 生成买卖盘口
double bidPrice = currentPrice - tickSize * random(1, 5);
double askPrice = currentPrice + tickSize * random(1, 5);
int bidVolume = random(10, 500);
int askVolume = random(10, 500);
// 输出一个Tick
outputTick(currentPrice, bidPrice, askPrice, bidVolume, askVolume);
// 模拟500ms间隔
sleep(500);
}
我曾经踩过的坑:生成的Tick数据太“完美”了。实盘中的Tick有跳空、有重复、有延迟,而我的模拟器生成的数据像教科书一样平滑。结果压力测试全通过了,一上实盘就崩。后来我加入了随机跳空和延迟抖动,才真正模拟出真实场景。
你想想看,如果模拟器生成的每个Tick都间隔500ms,价格波动也均匀,那你的系统根本测不出问题。真正的考验是:
- 连续多个Tick在10ms内到达(行情爆发)
- 突然出现一个巨大的跳空(比如从5000跳到5050)
- 某个合约的行情突然中断几秒钟
这些场景,都得靠模拟器来构造。
行情回放与加速:让历史数据“活”起来
模拟数据再好,也不如实盘数据有说服力。所以行情回放是压力测试的标配功能。但回放有个问题——太慢了。实盘一天4小时的行情,你总不能真等4小时吧?
所以我们需要加速回放。核心思路就一个:压缩时间轴。
| 加速倍数 | 实际耗时(4小时行情) | 适用场景 |
|---|---|---|
| 1x | 4小时 | 验证策略逻辑 |
| 10x | 24分钟 | 日常压力测试 |
| 100x | 2.4分钟 | 快速回归测试 |
| 1000x | 14.4秒 | 极限压力测试 |
实现加速回放,我建议用时间戳映射的方式。别傻傻地sleep等待,而是直接按比例压缩时间间隔:
// 加速回放核心逻辑
double speedFactor = 100.0; // 100倍加速
long lastTimestamp = 0;
while (有下一个Tick) {
Tick tick = readNextTick();
if (lastTimestamp == 0) {
// 第一个Tick,立即发送
sendTick(tick);
lastTimestamp = tick.timestamp;
} else {
// 计算原始时间间隔
long interval = tick.timestamp - lastTimestamp;
// 压缩间隔
long compressedInterval = interval / speedFactor;
// 等待压缩后的时间
sleep(compressedInterval);
sendTick(tick);
lastTimestamp = tick.timestamp;
}
}
我的经验:加速到100倍以上时,sleep的精度就不够了。这时候我会用busy-waiting(忙等待)来微调时间,或者干脆把Tick按时间戳排序后批量发送。但要注意,批量发送会改变行情到达的时序,可能影响测试结果。
为什么会这样?因为实盘中Tick是逐个到达的,而批量发送相当于把所有Tick同时塞给系统。如果你的系统对并发处理有缺陷,这种测试反而能暴露问题——嗯,这其实是个好事。
知识体系总览
下面这张图,是我对本章内容的一个总结。你可以把它当作一个思维导图来看:
避坑指南:我踩过的三个大坑
- 时间戳精度问题:CTP的Tick时间戳精确到毫秒,但有些交易所只提供秒级精度。回放时如果直接用秒级时间戳,加速后所有Tick会挤在一起。我的解决办法是:在秒级时间戳内随机分配毫秒偏移。
- 内存泄漏:行情回放时,如果每秒生成上千个Tick,内存分配和释放的频率极高。我建议用对象池来复用Tick结构体,别频繁new/delete。
- 日志爆炸:100倍加速回放时,日志量也会放大100倍。我曾经把磁盘写满了都不知道。现在我会在加速模式下关闭详细日志,只保留关键指标。
好了,行情数据模拟这块就聊到这儿。记住一句话:测试数据的质量,决定了压力测试的价值。花时间把模拟器做好,比花时间调优一个不存在的性能瓶颈,要划算得多。
公众号:蓝海资料掘金营,微信deep3321