第三章:行情数据接入层
行情数据接入层,说白了就是系统的「眼睛」和「耳朵」。
我做了这么多年债券做市系统,最深的体会就是:行情接不好,后面全是白搭。你策略再牛,模型再准,拿到的数据是错的或者慢了,那交易结果就是灾难。
3.1 交易所与银行间市场:两个世界的协议
国内债券市场,主要分两大块:交易所市场和银行间市场。这两个地方的行情协议,完全是两套逻辑。
交易所市场(上交所、深交所)走的是标准的证券行情协议。我记得早期用的是Step协议,后来逐步升级到STEP、MBO(Market By Order)这类。说白了,就是逐笔成交和快照数据。
银行间市场(CFETS)就复杂多了。它走的是外汇交易中心的专有协议,比如FX系列、CSTP。我当年第一次对接银行间行情时,差点被它的报文结构搞崩溃——字段嵌套深,而且有些字段是可选的,解析起来特别麻烦。
核心差异对比:
| 维度 | 交易所 | 银行间 |
|---|---|---|
| 协议类型 | Step/STEP/MBO | FX/CSTP |
| 数据频率 | 毫秒级快照+逐笔 | 秒级报价+成交 |
| 字段复杂度 | 中等 | 高(嵌套多) |
| 接入方式 | TCP直连/组播 | 专线+API |
3.2 多源数据融合:别让数据打架
你想想看,一个债券可能在交易所、银行间、甚至场外都有报价。这些报价时间不同、价格不同、量也不同。怎么融合?
我个人习惯的做法是:时间戳对齐 + 优先级策略。
举个例子,某只国债在交易所最新成交价是99.50,银行间报价是99.52。这时候不能简单取平均,因为两个市场的流动性不同。我一般会设定一个「可信度权重」——银行间的大额报价权重高,交易所的逐笔成交权重高。
避坑指南:我曾经遇到过一个问题:两个源的数据时间戳差了1秒,结果融合出来的价格剧烈抖动。后来我加了一个「时间窗口对齐」的逻辑——只融合时间差在100毫秒以内的数据。嗯,这个细节很重要。
3.3 行情数据缓存与分发:别让下游饿死
行情数据来了,不能直接丢给策略层。为什么?因为策略层处理不过来。一个活跃的债券,每秒可能有几百笔行情变化。策略层如果每笔都处理,CPU直接拉满。
所以,我们需要一个缓存层。我常用的方案是:
- 本地内存缓存:用ConcurrentHashMap或者Caffeine,存最近N笔行情
- 环形缓冲区:适合高频场景,避免GC压力
- Redis发布订阅:适合多进程/多机器分发
分发的时候,要注意「推拉结合」。推模式(Push)适合实时性要求高的场景,比如做市策略;拉模式(Pull)适合分析型应用,比如风控系统。
注意:缓存不能无限大。我见过有人把一天的所有行情都缓存下来,结果内存爆了。建议设置过期时间,比如只保留最近5分钟的逐笔数据。
3.4 行情质量监控:数据坏了要能发现
行情数据出问题,是迟早的事。网络抖动、源端故障、协议解析异常……我经历过太多次了。
监控什么?我总结了几点:
- 数据完整性:每个时间窗口内,行情条数是否在合理范围?突然少了,可能是丢包了。
- 数据时效性:最新行情的时间戳和当前系统时间差了多少?超过500毫秒就要报警。
- 数据合理性:价格突变超过5%?量突然放大100倍?这些很可能是异常数据。
- 源端健康度:每个数据源的连接状态、延迟、重连次数。
我曾经遇到过一个经典问题:银行间行情源突然停了10秒,但系统没报警,因为缓存里还有旧数据。结果策略用了10秒前的价格去报价,亏了不少。从那以后,我加了一个「缓存新鲜度」的监控——如果缓存数据超过1秒没更新,直接标记为不可用。
3.5 整体架构图
下面这张图,是我个人比较推荐的一种行情接入层架构。它把上面说的几个模块串起来了。
3.6 代码示例:一个简单的行情解析器
下面是一个简化版的Step协议解析器片段。实际生产环境要复杂得多,但核心逻辑就是这样。
public class StepQuoteParser {
// 解析Step协议的快照数据
public Quote parseSnapshot(byte[] rawData) {
Quote quote = new Quote();
// 跳过协议头
int offset = 8;
// 解析证券代码
byte[] secCodeBytes = new byte[8];
System.arraycopy(rawData, offset, secCodeBytes, 0, 8);
quote.setSecurityCode(new String(secCodeBytes).trim());
offset += 8;
// 解析最新价(定点数,4字节)
int priceInt = ByteBuffer.wrap(rawData, offset, 4).getInt();
quote.setLastPrice(priceInt / 10000.0); // 假设精度是万分位
offset += 4;
// 解析成交量(4字节)
int volume = ByteBuffer.wrap(rawData, offset, 4).getInt();
quote.setVolume(volume);
// 设置时间戳
quote.setTimestamp(System.currentTimeMillis());
return quote;
}
}
个人经验:解析行情时,一定要注意字节序(Big-Endian vs Little-Endian)。交易所和银行间用的可能不一样。我当年就因为这个bug,排查了整整一个下午。
3.7 总结
行情数据接入层,是整个做市系统的地基。地基不稳,楼盖得再高也没用。
核心就四点:协议解析要准、多源融合要稳、缓存分发要快、质量监控要全。把这四点做好了,后面的策略层才能安心干活。