行情数据模拟: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级) 行情回放与加速 二进制结构体解析 大端序字节转换 字符串价格处理 随机游走价格模型 买卖盘口生成 极端场景模拟(跳空/爆发) 时间戳映射加速 忙等待微调时序 批量发送与并发测试 核心目标:生成真实、可控、可复现的行情数据 用于验证系统在高负载、极端行情下的稳定性 三个模块相互独立,但组合使用效果最佳 建议:先用CTP解析器读取实盘数据 → 再用模拟器补充极端场景 → 最后用回放加速器压测

避坑指南:我踩过的三个大坑

  1. 时间戳精度问题:CTP的Tick时间戳精确到毫秒,但有些交易所只提供秒级精度。回放时如果直接用秒级时间戳,加速后所有Tick会挤在一起。我的解决办法是:在秒级时间戳内随机分配毫秒偏移。
  2. 内存泄漏:行情回放时,如果每秒生成上千个Tick,内存分配和释放的频率极高。我建议用对象池来复用Tick结构体,别频繁new/delete。
  3. 日志爆炸:100倍加速回放时,日志量也会放大100倍。我曾经把磁盘写满了都不知道。现在我会在加速模式下关闭详细日志,只保留关键指标。

好了,行情数据模拟这块就聊到这儿。记住一句话:测试数据的质量,决定了压力测试的价值。花时间把模拟器做好,比花时间调优一个不存在的性能瓶颈,要划算得多。


公众号:蓝海资料掘金营,微信deep3321