2. 行情数据校验:交易所行情协议解析、数据完整性校验、异常Tick过滤、时钟同步机制

做高频交易,最怕什么?

不是策略亏钱,是数据错了。

你想想看,一个错误的Tick进来,策略瞬间给你开一百手单子,等发现行情不对,已经来不及了。我见过太多团队,策略模型跑得飞起,结果栽在行情数据上。所以今天咱们聊聊行情数据校验,这块是风控系统的第一道防线,也是我最重视的环节之一。

2.1 交易所行情协议解析

国内期货交易所,主流就那几家:中金所、上期所、大商所、郑商所。每家都有自己的协议格式。我最早做这块时,以为各家协议大同小异,结果被坑得不轻。

举个例子,中金所用的是FIX协议变种,上期所早期用Step协议,后来也往FIX靠。但细节上差异很大:

  • 消息头长度:有的固定8字节,有的带变长字段
  • 时间戳精度:有的精确到毫秒,有的到微秒
  • 字段顺序:同一个字段在不同交易所的偏移量不同

我个人习惯,在解析层做一层抽象。不管底层协议怎么变,上层看到的都是统一的数据结构。这样换交易所或者协议升级时,改动最小。

核心原则:解析层只做格式转换,不做业务判断。校验逻辑放在下一层。

这里给一段伪代码,展示我常用的解析框架:

// 统一行情数据结构
struct MarketDataTick {
    uint64_t timestamp;    // 纳秒级时间戳
    uint32_t instrumentID; // 合约代码
    double   lastPrice;    // 最新价
    uint64_t volume;       // 成交量
    double   bidPrice;     // 买一价
    double   askPrice;     // 卖一价
    uint32_t bidVolume;    // 买一量
    uint32_t askVolume;    // 卖一量
};

// 协议解析器接口
class ProtocolParser {
public:
    virtual bool parse(const char* rawData, size_t len, MarketDataTick& tick) = 0;
    virtual bool validateHeader(const char* rawData) = 0;
};

2.2 数据完整性校验

数据完整性,说白了就是检查收到的行情包有没有丢、有没有错、有没有乱序。

我在项目中遇到过最典型的问题:网络抖动导致行情包乱序。你收到一个Tick,时间戳是10:00:01.000,下一个Tick是10:00:00.999。时间倒流了?不是,是包乱序了。

所以完整性校验,我一般做三层检查:

  1. 序列号连续性检查:每个行情包都有递增的序列号,如果发现跳号,说明有丢包
  2. 时间戳单调性检查:同一合约的Tick时间戳必须严格递增
  3. CRC校验:每个包末尾的校验码,确保传输过程中没被篡改

注意:序列号检查不能只看绝对值。交易所重启后序列号会重置,需要做特殊处理。我曾经因为这个bug,在实盘时误报了上百次丢包告警。

这里是我常用的校验逻辑:

bool checkDataIntegrity(const MarketDataTick& tick, uint64_t lastSeqNo, uint64_t lastTimestamp) {
    // 1. 序列号检查
    if (tick.seqNo <= lastSeqNo) {
        logWarning("序列号异常: 当前=%llu, 上次=%llu", tick.seqNo, lastSeqNo);
        return false;
    }
    
    // 2. 时间戳检查
    if (tick.timestamp < lastTimestamp) {
        logWarning("时间戳回退: 当前=%llu, 上次=%llu", tick.timestamp, lastTimestamp);
        return false;
    }
    
    // 3. CRC校验(假设CRC字段在tick中)
    if (!verifyCRC(tick)) {
        logError("CRC校验失败");
        return false;
    }
    
    return true;
}

2.3 异常Tick过滤

行情数据里,偶尔会出现一些「脏数据」。比如价格突然跳到一个离谱的值,或者买卖价差大得离谱。这些异常Tick如果不过滤掉,策略会做出错误决策。

我总结了几种常见的异常Tick类型:

异常类型 判断依据 处理方式
价格突变 价格变化超过N个标准差 丢弃,记录告警
买卖价差异常 ask - bid > 阈值 丢弃,等待下一个Tick
成交量异常 单笔成交量超过日均量10倍 标记可疑,人工复核
重复Tick 价格、成交量、时间戳完全相同 直接丢弃

嗯,这里要注意一点:阈值设置不能太死。比如价格突变,在开盘集合竞价时,价格波动本来就大。我建议用动态阈值,根据近期波动率自适应调整。

小技巧:异常Tick过滤最好分两级。第一级用简单规则(比如价格范围检查),计算快;第二级用统计模型(比如Z-score),精度高。两级串联,既保证速度又保证准确。

2.4 时钟同步机制

时钟同步,是高频交易里最容易被忽视的问题。

你想想看,你的交易服务器和交易所的时钟差了1毫秒,那你收到的行情时间戳就是错的。策略基于这个时间戳做决策,等于在错误的时间轴上跑。

我常用的时钟同步方案:

  • PTP(精确时间协议):硬件级同步,精度可达亚微秒级。但需要交换机支持
  • NTP(网络时间协议):软件级同步,精度一般在1-10毫秒。够用但不够快
  • GPS时钟:直接接收卫星信号,精度最高。但需要天线和专用设备

我个人建议,做高频交易至少用PTP。如果条件不允许,NTP加本地时钟漂移补偿也能凑合。但千万别只用系统时间,那误差能到秒级。

这里是我做时钟漂移补偿的代码片段:

class ClockSync {
private:
    int64_t localOffset_;  // 本地时钟与交易所时钟的偏差
    double  driftRate_;    // 时钟漂移率(纳秒/秒)
    
public:
    // 每次收到行情Tick时,更新偏差和漂移率
    void update(int64_t exchangeTimestamp, int64_t localTimestamp) {
        int64_t newOffset = exchangeTimestamp - localTimestamp;
        
        // 用滑动平均平滑偏差
        localOffset_ = localOffset_ * 0.9 + newOffset * 0.1;
        
        // 计算漂移率(简化版)
        driftRate_ = (newOffset - localOffset_) / 1.0e9;  // 假设1秒间隔
    }
    
    // 获取校正后的本地时间
    int64_t getCorrectedTime() {
        int64_t now = getLocalNanoseconds();
        return now + localOffset_ + driftRate_ * (now - lastSyncTime_);
    }
};

警告:时钟同步不是一劳永逸的。交换机重启、网络波动、温度变化都会影响同步精度。建议每秒钟做一次校准,并监控偏差值,超过阈值就告警。

2.5 整体校验流程

说了这么多,咱们把整个流程串起来。下面这张图是我设计的行情数据校验架构:

行情数据校验流程 网络数据接收 协议解析 数据完整性校验 异常处理/告警 异常Tick过滤 时钟同步校正 输出有效Tick 说明: ● 左侧分支:数据异常时触发告警 ● 右侧分支:正常数据继续处理

整个流程走下来,你会发现:数据校验不是单一环节,而是一整套链路。从网络接收开始,到最终输出有效Tick,每一步都在做减法——把脏数据、错数据、乱数据剔除掉。

最后说一句:行情数据校验做得好不好,直接决定了你的策略能不能稳定跑。别在这块省钱省力,否则后面填坑的成本更高。

总结:协议解析要抽象、完整性校验要分层、异常过滤要动态、时钟同步要硬件。这四块做好了,行情数据这块就稳了。

专注资料整理