3、数据流处理:实盘Tick/Bar数据的实时接入、清洗、特征工程与缓存策略

实盘交易和回测最大的区别在哪?

我个人觉得,就是数据流。回测时数据是死的,你随便折腾。实盘呢?数据是活的,每秒都在喷涌而来。你处理慢了,信号就滞后了;你处理错了,仓位就歪了。今天咱们就聊聊,怎么把这股「活水」接好、洗干净、喂给策略。

3.1 实时数据接入:别让数据等你

实盘数据接入,说白了就是「抢时间」。我见过不少团队,回测跑得飞起,一上实盘就崩,原因往往是数据管道堵了。

Tick 数据接入,我习惯用 WebSocket。为什么?因为它是全双工的,服务器主动推,延迟低。你想想看,如果每笔成交都用 HTTP 轮询,那延迟得有多感人。

核心原则:接入层要做到「零拷贝、无锁化」。数据从网卡到应用层,路径越短越好。

这里给个简单的 WebSocket 接入框架示意:

import asyncio
import websockets
import json

class TickStreamer:
    def __init__(self, url, symbols):
        self.url = url
        self.symbols = symbols
        self.buffer = asyncio.Queue(maxsize=10000)

    async def connect(self):
        async with websockets.connect(self.url) as ws:
            # 订阅合约
            sub_msg = {"op": "subscribe", "args": self.symbols}
            await ws.send(json.dumps(sub_msg))
            
            while True:
                raw = await ws.recv()
                # 直接丢进队列,不阻塞
                await self.buffer.put(raw)

    async def get_tick(self):
        return await self.buffer.get()

嗯,这里要注意:队列一定要设置最大长度。我曾经遇到过数据生产速度远大于消费速度,结果内存爆了,整个进程被 OOM Killer 干掉。那叫一个惨。

3.2 数据清洗:脏数据是策略的毒药

交易所的数据就一定干净吗?不一定。我遇到过的情况包括:

  • 重复推送:同一笔成交推了两次
  • 乱序到达:后发生的成交先到了
  • 异常值:价格突然跳到一个离谱的数字
  • 缺失数据:某几秒的 Tick 完全没收到

清洗策略其实不复杂,但必须做。我个人习惯用「三关过滤法」:

  1. 第一关:去重。用 (交易所时间戳 + 成交ID) 做唯一键,重复的直接扔掉。
  2. 第二关:排序。维护一个滑动窗口,窗口内的数据按时间戳重排。窗口大小我一般设 100ms。
  3. 第三关:合理性校验。价格不能超过上一笔的 ±10%,成交量不能为负,等等。

避坑指南:我曾经在排序环节吃过亏。窗口设得太小(比如 10ms),结果网络抖动时数据还是乱的。后来改成 100ms,问题就解决了。但窗口太大又会增加延迟,这个平衡点需要根据你的网络环境来调。

3.3 特征工程:从原始数据到策略信号

原始 Tick 数据,说白了就是一堆数字。你得把它变成策略能理解的特征。

Bar 的构建是第一步。我常用的几种方式:

Bar 类型 构建方式 适用场景
时间 Bar 固定时间窗口(如 1分钟) 趋势跟踪、均值回归
成交量 Bar 固定成交量(如 1000手) 高波动品种、避免无成交时段
Tick Bar 固定笔数(如 100笔) 微观结构分析、高频策略

我个人偏好成交量 Bar。为什么?因为时间 Bar 在夜盘或者流动性差的时候,可能一根 Bar 里只有几笔成交,那算出来的指标根本没法用。成交量 Bar 能保证每根 Bar 都有足够的样本量。

特征计算这块,我建议用增量更新。别每次来一个新 Tick 就把所有数据重算一遍,那太慢了。

class IncrementalEMA:
    def __init__(self, alpha):
        self.alpha = alpha
        self.value = None

    def update(self, price):
        if self.value is None:
            self.value = price
        else:
            self.value = self.alpha * price + (1 - self.alpha) * self.value
        return self.value

你看,EMA 的增量更新就一行公式。像布林带、RSI 这些指标,也都有对应的增量算法。你想想看,如果每次都要遍历过去 1000 根 Bar,那延迟得多高。

3.4 缓存策略:内存里的「高速公路」

数据清洗完了,特征算好了,接下来要存起来。但注意,实盘场景下,你不能每次都去查数据库。磁盘 IO 太慢了。

我一般用 三层缓存架构

  • L1 缓存:进程内内存。存最近 1 分钟的 Tick 数据,用环形缓冲区实现。读写都是 O(1)。
  • L2 缓存:共享内存。存最近 1 小时的 Bar 数据,多个策略进程可以共享。我用的是 Redis,但注意要开 AOF 持久化。
  • L3 缓存:本地文件。存当天的完整数据,用于盘后复盘。格式我推荐用 Parquet,压缩率高,读取快。

注意:L1 缓存一定要用预分配内存。我见过有人用 Python 的 list 动态增长,结果在行情剧烈波动时,频繁的内存分配导致 GC 暂停,策略直接错过了信号。用 array 或者 numpy 的固定大小数组,提前把内存申请好。

这里画个图,帮你理解整个数据流的走向:

交易所 WebSocket 数据接入 数据清洗 特征工程 三层缓存 L1: 进程内存 (环形缓冲区) L2: 共享内存 (Redis) L3: 本地文件 (Parquet) 策略引擎 实盘数据流处理架构

你看这个流程,数据从交易所出来,经过接入、清洗、特征工程,最后落到缓存里,策略引擎直接从缓存取数据。每一步都是流水线作业,互不阻塞。

3.5 实战中的几个坑

最后分享几个我踩过的坑,希望能帮你省点时间:

  • 时间同步:交易所的时间戳和你的服务器时间可能不一致。我建议用 NTP 同步,并且所有数据都统一用交易所时间戳,别用本地时间。
  • 断线重连:WebSocket 一定会断。重连时要注意数据补全,别出现缺口。我一般会在重连后拉取最近 10 秒的补数数据。
  • 内存泄漏:Python 的循环引用会导致内存泄漏。我习惯用 weakref 来管理缓存中的对象引用。
  • 日志记录:每条数据的处理耗时都要记下来。这样出了问题,你能快速定位是哪个环节慢了。

一个小技巧:在开发阶段,我会把实盘数据录下来,存成文件。然后在回测环境里「回放」这些数据,用来测试数据管道的吞吐量和稳定性。这比直接上实盘安全多了。

数据流处理这块,说白了就是「快」和「稳」的平衡。太快了容易出错,太稳了又跟不上行情。我个人觉得,先保证数据不丢、不乱,再慢慢优化速度。毕竟,一个慢但正确的信号,比一个快但错误的信号,要值钱得多。


专注资料整理