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 整体流程:一张图说清楚
我把整个数据解析与标准化的流程,画了张图。你一看就明白:
嗯,流程其实不复杂。但每个环节都有坑。我建议你从最简单的交易所开始做,比如先接一个WebSocket JSON的币安,把整个链路跑通。然后再去啃那些二进制协议。
📌 本章核心要点:
- 协议解析:拆包、校验、字节序转换,一步不能少
- 统一模型:固定长度、够用就行、时间统一
- 字段映射:用配置驱动,别硬编码
- 做完映射,一定要做合理性校验
数据解析这块,说白了就是“翻译”。把各家交易所的方言,翻译成咱们系统能懂的普通话。翻译得准不准,直接决定了后面策略能不能赚钱。
公众号:蓝海数据掘金营,微信deep3321