四、行情订阅实战:合约代码规则、订阅深度行情(MarketData)、处理行情快照与增量数据

行情订阅,是量化交易系统的心脏起搏器。没有实时、准确的行情数据,你的策略再牛也白搭。今天我们就来聊聊,怎么把交易所的行情数据稳稳地接进来。

4.1 合约代码规则:别让命名坑了你

每家交易所的合约代码规则都不一样。我刚开始做期货量化时,就吃过这个亏。以为所有交易所都是「品种+月份」的格式,结果对接某商品交易所时,发现它的主力连续合约代码里藏着字母后缀,直接导致订阅失败。

咱们先看看主流的规则:

交易所 合约代码示例 规则说明
中金所 IF2403 品种代码(IF)+ 年份后两位(24)+ 月份(03)
上期所 rb2405 小写品种代码(rb)+ 年份后两位 + 月份
大商所 m2405 小写品种代码 + 年份后两位 + 月份
郑商所 SR405 大写品种代码(SR)+ 年份后两位 + 月份
上期能源 sc2406 小写品种代码 + 年份后两位 + 月份

嗯,这里要注意一点:主力合约和连续合约的代码规则是独立的。比如中金所的IF主力连续合约代码是IF00,而上期所的主力连续可能是rb888。我建议你在代码里维护一个合约代码映射表,别硬编码。

避坑指南:我曾经在对接CTP时,发现某些合约在非交易时段订阅会返回错误。后来才知道,交易所对非上市合约的订阅请求会直接拒绝。所以订阅前,一定要先检查合约状态。

4.2 订阅深度行情:不只是买卖一档

深度行情(MarketData),说白了就是买卖盘口的快照。但很多新手只订阅买卖一档,这其实不够用。做高频策略或者套利策略时,你需要的是五档甚至十档的深度数据。

订阅深度行情的流程,我一般这么设计:

# 伪代码示例:订阅深度行情
class MarketDataSubscriber:
    def __init__(self, api_connection):
        self.api = api_connection
        self.subscribed_contracts = set()
    
    def subscribe_depth(self, contract_code, depth_level=5):
        """
        订阅指定合约的深度行情
        :param contract_code: 合约代码,如 'IF2403'
        :param depth_level: 深度档位,默认5档
        """
        # 检查合约是否已订阅
        if contract_code in self.subscribed_contracts:
            print(f"合约 {contract_code} 已订阅,跳过")
            return
        
        # 构建订阅请求
        req = {
            'type': 'SUBSCRIBE_MARKET_DATA',
            'contract': contract_code,
            'depth': depth_level,
            'is_snapshot': True  # 首次订阅需要快照
        }
        
        # 发送订阅请求
        response = self.api.send_request(req)
        if response['status'] == 'success':
            self.subscribed_contracts.add(contract_code)
            print(f"成功订阅 {contract_code},深度 {depth_level} 档")
        else:
            print(f"订阅失败: {response['error_msg']}")
    
    def unsubscribe_depth(self, contract_code):
        """取消订阅"""
        if contract_code in self.subscribed_contracts:
            req = {
                'type': 'UNSUBSCRIBE_MARKET_DATA',
                'contract': contract_code
            }
            self.api.send_request(req)
            self.subscribed_contracts.remove(contract_code)

你想想看,为什么订阅时要指定深度档位?因为不同策略对数据精度的要求不一样。做市商策略可能需要十档数据,而趋势跟踪策略可能只看买卖一档就够了。订阅太多不必要的数据,只会增加网络带宽和内存压力。

4.3 处理行情快照与增量数据:别漏掉任何一笔

行情数据分为两种:快照(Snapshot)增量(Incremental)。快照是某一时刻的完整盘口状态,增量是后续的变动数据。

我个人习惯的做法是:以快照为基准,用增量来更新。具体来说:

  1. 首次订阅时,获取一个完整的快照
  2. 后续收到的增量数据,在快照基础上进行加减操作
  3. 定期(比如每10秒)重新请求一次快照,防止累积误差

为什么会这样?因为增量数据在传输过程中可能丢包,或者顺序错乱。如果不定期校准,你的本地盘口数据就会和交易所的真实数据越差越远。

小技巧:增量数据通常包含一个序列号(Sequence Number)。你可以用这个序列号来检测是否有数据丢失。如果发现序列号不连续,立即请求一次快照进行校准。

下面是一个处理增量数据的核心逻辑:

class OrderBook:
    def __init__(self, contract_code):
        self.contract = contract_code
        self.bids = {}  # 买盘,价格 -> 数量
        self.asks = {}  # 卖盘,价格 -> 数量
        self.last_snapshot_time = 0
    
    def apply_snapshot(self, snapshot):
        """应用快照数据"""
        self.bids.clear()
        self.asks.clear()
        
        for bid in snapshot['bids']:
            self.bids[bid['price']] = bid['volume']
        for ask in snapshot['asks']:
            self.asks[ask['price']] = ask['volume']
        
        self.last_snapshot_time = time.time()
    
    def apply_incremental(self, incremental):
        """应用增量数据"""
        # 处理买盘变动
        for bid_change in incremental['bid_changes']:
            price = bid_change['price']
            volume = bid_change['volume']
            
            if volume == 0:
                # 数量为0表示删除该价位
                self.bids.pop(price, None)
            else:
                # 更新或新增价位
                self.bids[price] = volume
        
        # 处理卖盘变动
        for ask_change in incremental['ask_changes']:
            price = ask_change['price']
            volume = ask_change['volume']
            
            if volume == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = volume
        
        # 检查是否需要重新校准
        if time.time() - self.last_snapshot_time > 10:
            self.request_snapshot()  # 请求新的快照
    
    def get_best_bid_ask(self):
        """获取最优买卖价"""
        if not self.bids or not self.asks:
            return None
        
        best_bid = max(self.bids.keys())
        best_ask = min(self.asks.keys())
        
        return {
            'bid_price': best_bid,
            'bid_volume': self.bids[best_bid],
            'ask_price': best_ask,
            'ask_volume': self.asks[best_ask]
        }

警告:增量数据中的价格和数量,一定要用整数或定点数处理,别用浮点数。浮点数的精度问题在频繁计算时会累积成灾难。我在一个项目中就因为这个原因,导致套利策略的价差计算偏差了0.5个tick,亏了不少钱。

4.4 整体架构:一张图看懂

说了这么多,咱们用一张图来总结行情订阅的整体流程:

行情订阅整体架构 交易所行情网关 行情订阅管理器 数据分发器 快照处理器 增量处理器 本地 OrderBook 交易所 订阅管理 数据分发 快照处理 增量处理 本地OrderBook

从这张图可以看出,行情订阅的核心就是三个环节:订阅管理数据分发本地维护。订阅管理器负责和交易所网关通信,数据分发器把原始数据拆成快照和增量两条路径,最后在本地OrderBook中合并成完整的盘口数据。

嗯,这里要特别强调一下:数据分发器一定要做去重和排序。因为网络延迟可能导致增量数据乱序到达,如果不处理,你的OrderBook就会乱套。我一般会在分发器里维护一个环形缓冲区,按序列号排序后再分发。

核心要点:行情订阅不是简单的「订阅-接收」两步走。它需要你处理好合约代码映射、深度档位选择、快照与增量的合并、数据校准等多个环节。任何一个环节出问题,你的策略都可能跑偏。

最后说一句,行情数据的性能优化,其实就两个方向:减少不必要的数据传输加快本地处理速度。前者靠合理的订阅策略,后者靠高效的数据结构和算法。这两点做好了,你的行情系统就能稳稳地跑上几年。


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