第三章 实时数据流处理:WebSocket连接、数据清洗与对齐、Tick级数据存储

好,咱们进入实战环节最核心的一块——实时数据流处理。

说实话,很多做量化的人,策略写得花里胡哨,但一到实盘就崩。为什么?数据流没处理好。你想想看,行情数据就像流水,你接不住、洗不干净、存不对,后面所有分析都是空中楼阁。

这一章,我带你搞定三件事:连上WebSocket、清洗对齐数据、存好Tick级数据。这三板斧练好了,你的策略才算真正有了“实时”的底气。

3.1 WebSocket连接:别用轮询,太Low了

我记得刚入行那会儿,还有人用HTTP轮询拿行情。每秒请求一次,服务器没崩,自己先崩了。现在谁还这么干?WebSocket才是正经路子。

WebSocket说白了就是一条长连接。客户端和服务器握个手,之后数据就像自来水一样,源源不断流过来。延迟低,效率高。

核心要点:WebSocket连接不是一次性的。断线重连、心跳保活、消息去重,这三样缺一不可。

我习惯用Python的websockets库,轻量又好用。给你看个基础框架:

import asyncio
import websockets
import json

async def connect_market_data(uri):
    """连接行情WebSocket"""
    async for websocket in websockets.connect(uri, ping_interval=20):
        try:
            # 订阅合约
            subscribe_msg = {
                "op": "subscribe",
                "args": ["ticker.BTCUSDT"]
            }
            await websocket.send(json.dumps(subscribe_msg))
            
            # 持续接收数据
            async for message in websocket:
                yield json.loads(message)
                
        except websockets.ConnectionClosed:
            print("连接断了,准备重连...")
            continue

这里有个坑——心跳机制。我曾经遇到过一次,交易所的WebSocket服务每隔30秒不发心跳就断开。我这边没做保活,结果半夜策略断线了,第二天一看亏了不少。后来我学乖了,ping_interval设成20秒,比交易所的阈值短一点,稳得很。

我的经验:连接成功后,先发一个订阅请求,确认收到订阅成功的回包,再开始处理数据。别一上来就收数据,容易漏掉开头几笔。

3.2 数据清洗与对齐:脏数据会要了策略的命

数据进来了,但你不能直接用。为什么?因为交易所发来的数据,有时候会乱序、重复、甚至缺失。我见过最离谱的一次,某交易所的Tick数据里居然混了一条昨天的数据。

所以,清洗是必须的。我一般做三步:

  1. 去重:同一笔成交,可能因为网络重传收到两次。用(交易对, 时间戳, 成交ID)做唯一键,重复的直接扔掉。
  2. 排序:WebSocket数据包可能乱序到达。我习惯维护一个长度为100的滑动窗口,窗口内按时间戳排序。
  3. 补缺失:如果发现某笔Tick的时间戳和前一笔间隔超过阈值(比如500ms),说明中间可能有数据丢了。这时候我会用前一笔的价格填充,或者直接标记为缺失。

给你看个清洗函数的例子:

def clean_tick(tick: dict, last_tick: dict) -> dict:
    """清洗单笔Tick数据"""
    # 去重检查
    if tick['trade_id'] == last_tick.get('trade_id'):
        return None  # 重复数据,丢弃
    
    # 时间戳检查
    if tick['timestamp'] < last_tick.get('timestamp', 0):
        return None  # 乱序数据,丢弃
    
    # 价格合理性检查
    if tick['price'] <= 0 or tick['price'] > 1e6:
        return None  # 异常价格,丢弃
    
    return tick

数据对齐呢?说白了就是让不同数据源的时间基准统一。比如你同时接了币安和OKX的行情,它们的服务器时间可能有几十毫秒的偏差。我一般用NTP同步本地时间,然后以本地时间为准,把所有Tick的时间戳都转成本地时间。

注意:千万别直接用交易所返回的时间戳做对齐。不同交易所的时间精度不一样,有的精确到毫秒,有的精确到微秒。统一转成毫秒时间戳,再对齐。

3.3 Tick级数据存储:别小看这步,它决定了你的回测质量

Tick数据有多重要?我这么说吧,没有Tick数据,你的策略回测就是隔靴搔痒。日线级别的回测,根本看不出高频交易中的滑点和冲击成本。

但Tick数据存储有个难题——数据量太大了。一个活跃的合约,一天能产生几十万笔Tick。存一年就是几亿条。怎么存?

我推荐用列式存储。比如Parquet或者InfluxDB。别用MySQL,别用CSV。为什么?因为Tick数据是时序数据,列式存储的压缩比高,查询速度快。

给你看个存储方案对比:

存储方式 写入速度 查询速度 压缩比 推荐场景
CSV 小规模测试
MySQL 不推荐用于Tick
Parquet 离线分析
InfluxDB 极快 极快 实时查询

我个人习惯用Parquet + ClickHouse的组合。白天实时写入ClickHouse,晚上批量转存Parquet做归档。这样既保证了实时查询的速度,又降低了存储成本。

给你看个存储代码片段:

import pandas as pd
from datetime import datetime

def save_tick_to_parquet(tick_data: list, symbol: str):
    """将Tick数据批量写入Parquet文件"""
    df = pd.DataFrame(tick_data)
    # 按日期分区存储
    date_str = datetime.now().strftime("%Y%m%d")
    file_path = f"data/{symbol}/tick_{date_str}.parquet"
    
    # 追加写入
    df.to_parquet(file_path, engine='pyarrow', compression='snappy', append=True)
    print(f"已写入 {len(df)} 条Tick数据到 {file_path}")

避坑指南:我曾经直接把Tick数据逐条写入数据库,结果IO打满了,行情都堵住了。后来改成批量写入——攒够1000条或者每隔1秒写一次,问题就解决了。

3.4 整体流程:一张图看懂

说了这么多,咱们把整个流程串起来。我画了张图,你看一眼就明白了:

实时数据流处理流程 WebSocket连接 订阅行情 + 心跳保活 数据清洗 去重 + 排序 + 补缺失 数据对齐 统一时间基准 Tick级存储 Parquet / ClickHouse 实时策略引擎 离线回测分析 数据流方向:从左到右,从上到下

这张图把整个流程串起来了。你从左边开始,连上WebSocket,拿到原始数据。然后清洗、对齐,最后存起来。存完之后,数据有两个去向:一是喂给实时策略引擎做交易决策,二是存到离线库供回测分析。

嗯,这里要注意——实时和离线用的是同一套数据。这样你的回测结果才能真实反映实盘表现。我见过有人回测用一套数据,实盘用另一套,结果回测赚得盆满钵满,实盘亏得底朝天。说白了,数据不一致,一切都是白搭。

总结一下:实时数据流处理,核心就三件事——连得上、洗得净、存得稳。这三件事做好了,你的策略就成功了一半。

好了,这一章的内容就到这儿。记住,数据是量化交易的血液,别让它堵了。

专注资料整理