4. 重复数据问题本质

在Tick数据处理中,重复数据不是偶发异常,而是分布式行情系统与生俱来的常态。理解重复数据的本质,需要从“它从哪里来”和“它会造成什么后果”两个维度同时切入。很多开发者习惯把去重当作一个简单的drop_duplicates()操作,但在真实行情链路中,重复的产生机制决定了去重策略必须与业务语义深度绑定,否则轻则漏单,重则导致成交量虚增、订单簿错乱、信号误触发。

一、重复产生的三大根源

重复数据的来源可以归纳为三类,它们往往同时存在、相互叠加,使得问题更加隐蔽。

  • 网络重传:行情网关与客户端之间通常基于TCP或可靠UDP传输。当ACK丢失或超时,发送端会重传同一批Tick。接收端若未做序列号校验,就会把同一笔成交记录两次写入缓冲区。这类重复的特点是内容完全一致、时间戳相同,但到达时间可能相差几十毫秒到数秒。
  • 多源采集:为了容灾和低延迟,机构常同时接入主备行情源、交易所直连与第三方转发。同一笔成交可能从两条链路各到达一次。这类重复的内容高度相似但字段可能微调,例如交易所时间戳精度不同、买卖方向字段编码不同,甚至成交量单位存在手与股的差异。
  • 消息队列至少一次语义:Kafka、RocketMQ等消息中间件在生产者重试、消费者rebalance、offset提交失败等场景下,会保证“至少一次”投递。这意味着同一条消息可能被消费多次。这类重复的消息体完全一致,但消费位点不同,且可能跨越较长时间窗口。

关键认知:重复不是“错误数据”,而是“同一事实的多次表达”。去重的本质是识别事实的唯一性,而不是简单地删除相同行。如果两行内容完全相同但代表两笔真实成交(例如同一毫秒内两笔同价同量成交),盲目去重反而会丢失真实交易。

二、重复对核心计算的影响机制

重复数据一旦进入计算链路,会从三个层面造成破坏,且影响会逐层放大。

影响对象 直接后果 典型表现 严重程度
成交量/成交额 累加值虚增 某分钟成交量比交易所公布值高30% 高
订单簿 买卖档位数量错乱 同一价位挂单量翻倍或出现负值 极高
信号计算 指标误触发 均线突变、大单信号重复报警 中高
回测绩效 收益曲线失真 策略在回测中表现优异,实盘亏损 高

以订单簿为例,假设某价位原始挂单量为100手。若一笔50手的撤单消息被重复处理两次,订单簿会先减50再减50,最终变成0,而实际应为50。更危险的是,如果重复的是新增挂单,订单簿会虚增,导致后续基于盘口深度的策略误判支撑位或压力位。成交量方面,重复累加会直接扭曲VWAP、成交量加权均价等指标,使算法交易的价格基准偏移。

三、去重的核心方法与流程

有效的去重不是单点操作,而是一条从接入到计算的分层防线。推荐流程如下:

  1. 接入层序列号校验:每条Tick携带交易所或网关生成的唯一序列号(如seq_no)。维护滑动窗口,丢弃序列号小于等于已处理最大值的消息。这是最快、最廉价的第一道防线。
  2. 业务键去重:当序列号不可用时,使用业务唯一键组合,例如交易所代码 + 合约代码 + 成交时间 + 成交编号。注意时间戳精度必须与数据源一致,否则会误判。
  3. 幂等写入:在存储层使用唯一索引或Redis SETNX,确保同一业务键只落库一次。这一步解决跨批次、跨进程的重复。
  4. 状态机校验:对订单簿等有状态计算,维护订单ID到状态的映射。若收到已存在订单的新增消息,直接丢弃并记录告警。
# 基于业务键的滑动窗口去重示例(伪代码)
seen = OrderedDict()
WINDOW = 100000  # 保留最近10万条业务键

def dedup(tick):
    key = f"{tick.exchange}:{tick.symbol}:{tick.trade_time}:{tick.trade_id}"
    if key in seen:
        metrics.inc("duplicate_dropped")
        return None
    seen[key] = True
    if len(seen) > WINDOW:
        seen.popitem(last=False)
    return tick

去重窗口大小需要根据数据源的重传延迟分布来设定。如果重传可能延迟数分钟,窗口过小会导致重复漏网。建议先用离线数据统计重复消息的时间间隔分布,再确定窗口参数。

四、常见误区与复盘要点

在实际项目中,去重逻辑的缺陷往往在压力测试或极端行情下才暴露。以下是高频误区:

  • 误区一:用全字段哈希去重。多源采集时,同一笔成交的字段可能存在微小差异(如时间戳精度、方向编码),全字段哈希无法识别为重复。应使用业务键而非全字段。
  • 误区二:去重后不记录被丢弃的数据。被丢弃的重复消息是排查数据源质量的重要线索。应保留计数和样本,便于监控重传率和链路健康度。
  • 误区三:在计算之后去重。如果先累加成交量再去重,虚增已经发生。去重必须发生在状态更新之前。
  • 误区四:忽略“合法重复”。同一毫秒内两笔同价同量成交是真实存在的。仅凭价格和数量去重会误杀。必须依赖成交编号或序列号。

复盘时重点检查:去重键是否覆盖了所有数据源?窗口是否足够大?被丢弃的消息是否可追溯?在极端行情(如开盘集合竞价、涨跌停板打开)下,重复率通常会显著上升,需要单独压测验证。

五、可视化:重复数据从产生到影响的链路

重复数据产生与影响链路 网络重传 ACK丢失 / 超时重发 多源采集 主备链路 / 直连+转发 消息队列至少一次 重试 / rebalance 重复Tick进入计算链路 同一事实的多次表达 成交量虚增 VWAP偏移 / 分钟量失真 回测收益曲线失真 订单簿错乱 档位数量翻倍 / 负值 盘口深度误判 信号误触发 均线突变 / 大单重复报警 策略频繁交易 防线:序列号校验 → 业务键去重 → 幂等写入 → 状态机校验