3. 数据解析与标准化:不同交易所协议解析、统一数据模型设计、字段映射

好,咱们进入第三个实战环节。

行情数据从网络流里捞出来,是原始报文。不同交易所,协议五花八门。你得把它们“翻译”成系统能懂的语言。这一步做不好,后面全是垃圾数据。

我个人习惯,把这块拆成三个子任务:协议解析统一模型字段映射。咱们一个一个说。

3.1 协议解析:跟交易所“对话”

不同交易所,报文格式天差地别。我遇到过最头疼的,是某家海外交易所用自定义二进制协议,连文档都是错的。

常见的协议类型,我整理了一下:

交易所类型 协议格式 特点
国内A股(深交所/上交所) STEP(基于FIX) 标签-值对,可读性还行
国内期货(中金所/大商所) 自定义二进制 紧凑,解析麻烦
海外主流(CME/LSE) FIX/FAST/SBE FAST压缩率高,SBE效率高
数字货币(Binance/Coinbase) WebSocket JSON 简单,但量大

解析的核心,其实就是拆包。拿一个典型的二进制协议举例:

// 伪代码:解析一个交易所的行情快照
struct MarketDataSnapshot {
    uint32_t msg_type;      // 消息类型
    uint64_t timestamp;     // 时间戳
    char symbol[8];         // 合约代码
    double last_price;      // 最新价
    uint64_t volume;        // 成交量
    // ... 更多字段
};

// 解析函数
void parse_snapshot(const char* raw_data, size_t len) {
    // 1. 检查长度
    if (len < sizeof(MarketDataSnapshot)) {
        // 报文不完整,丢弃
        return;
    }
    // 2. 直接内存映射(注意字节序!)
    MarketDataSnapshot* snap = (MarketDataSnapshot*)raw_data;
    // 3. 转换字节序(网络序转主机序)
    snap->timestamp = ntoh64(snap->timestamp);
    snap->last_price = ntoh64(snap->last_price);
    // 4. 校验
    if (!validate_checksum(raw_data, len)) {
        // 校验失败,丢弃
        return;
    }
    // 5. 投递给下游
    dispatch_to_pipeline(snap);
}
⚠️ 注意字节序! 很多交易所用网络字节序(Big-Endian),而x86是Little-Endian。不转换,价格直接翻倍。我曾经因为这个bug,在测试环境亏了虚拟币。

3.2 统一数据模型:给数据“定标准”

解析完各家报文,你会发现:同样一个“最新价”,有的叫last_price,有的叫trade_price,还有的叫px

所以,必须设计一个统一数据模型。说白了,就是定义一套内部通用的数据结构。所有交易所的数据,都转成这个格式。

我设计的模型,一般长这样:

// 统一行情数据模型(C++结构体)
struct UnifiedMarketData {
    // 元信息
    uint8_t  exchange_id;       // 交易所编号
    uint8_t  instrument_type;   // 品种类型:股票/期货/期权/币
    uint32_t instrument_id;     // 内部合约ID
    
    // 时间
    int64_t  exchange_time;     // 交易所时间(纳秒)
    int64_t  local_time;        // 本地接收时间(纳秒)
    
    // 行情核心
    double   last_price;        // 最新价
    double   bid_price[5];      // 买一到买五
    double   ask_price[5];      // 卖一到卖五
    uint64_t bid_volume[5];     // 买量
    uint64_t ask_volume[5];     // 卖量
    uint64_t total_volume;      // 总成交量
    double   open_interest;     // 持仓量(期货)
    
    // 状态
    uint8_t  trading_status;    // 交易状态:连续/集合/休市
};
💡 设计原则:
  • 够用就行:别把所有字段都塞进去。只放策略真正需要的。
  • 固定长度:避免动态内存分配。用数组代替vector。
  • 时间统一:全部转成纳秒级int64。别用字符串时间戳。

3.3 字段映射:把“方言”翻译成“普通话”

模型定好了,接下来就是映射。每个交易所的字段,要一一对应到统一模型里。

我习惯用配置驱动的方式。写一个映射表,而不是硬编码。

// 字段映射配置(YAML格式)
mapping:
  binance:
    last_price: "c"          # Binance的"c"字段对应最新价
    bid_price: "b"           # "b"是买单数组
    ask_price: "a"           # "a"是卖单数组
    volume: "v"              # 成交量
  
  cme:
    last_price: "LastPx"     # CME的FIX标签
    bid_price: "BidPx"       # 买价
    ask_price: "AskPx"       # 卖价
    volume: "LastQty"        # 成交量
  
  shfe:
    last_price: "LastPrice"  # 上期所STEP协议
    bid_price: "BidPrice1"   # 买一价
    ask_price: "AskPrice1"   # 卖一价
    volume: "Volume"         # 成交量

代码里,我写一个通用的映射引擎:

// 字段映射引擎(简化版)
class FieldMapper {
public:
    void load_config(const std::string& config_file);
    
    void map_to_unified(const RawData& raw, UnifiedMarketData& unified) {
        // 根据交易所ID,找到对应的映射规则
        auto& rules = mapping_rules_[raw.exchange_id];
        
        // 遍历统一模型的每个字段
        unified.last_price = get_field(raw, rules["last_price"]);
        unified.bid_price[0] = get_field(raw, rules["bid_price"]);
        // ... 其他字段
    }
    
private:
    std::map<uint8_t, std::map<std::string, std::string>> mapping_rules_;
};
💡 避坑指南: 我曾经遇到一个坑:某交易所的“最新价”在盘前是0,但策略没判断,直接用了。结果开仓价变成0,差点爆仓。所以,映射完一定要做合理性校验:价格>0,时间戳不能是未来,买卖价差不能为负。

3.4 整体流程:一张图说清楚

我把整个数据解析与标准化的流程,画了张图。你一看就明白:

数据解析与标准化流程 交易所A原始报文 交易所B原始报文 交易所C原始报文 协议解析器A 协议解析器B 协议解析器C 字段映射器A 字段映射器B 字段映射器C 统一数据模型 UnifiedMarketData STEP / FIX / JSON 拆包 / 校验 / 字节序 配置驱动 / 字段转换 标准化输出

嗯,流程其实不复杂。但每个环节都有坑。我建议你从最简单的交易所开始做,比如先接一个WebSocket JSON的币安,把整个链路跑通。然后再去啃那些二进制协议。

📌 本章核心要点:
  • 协议解析:拆包、校验、字节序转换,一步不能少
  • 统一模型:固定长度、够用就行、时间统一
  • 字段映射:用配置驱动,别硬编码
  • 做完映射,一定要做合理性校验

数据解析这块,说白了就是“翻译”。把各家交易所的方言,翻译成咱们系统能懂的普通话。翻译得准不准,直接决定了后面策略能不能赚钱。


公众号:蓝海数据掘金营,微信deep3321