2. 数据采集层设计:实时行情数据源接入与存储

数据采集层,说白了就是量化系统的「耳朵」和「眼睛」。

你想想看,没有数据,再牛的策略也是白搭。我做了这么多年量化架构,见过太多团队在策略上花了大把精力,结果数据层一塌糊涂——行情延迟高、数据对不齐、回测和实盘结果天差地别。嗯,这些坑我都踩过。

2.1 实时行情数据源接入

实时行情接入,核心就两个路子:交易所API和WebSocket。我个人习惯把前者叫「拉模式」,后者叫「推模式」。

2.1.1 交易所API(RESTful)

REST API适合做历史数据补录、账户查询这类低频操作。但你要是用它做实时行情,那就等着被市场甩下车吧。为什么?因为每次请求都要建立HTTP连接,延迟通常在几十到几百毫秒。高频交易里,这时间够价格跳好几个档位了。

注意: 别指望用REST API做Tick级行情。我在项目中遇到过有人这么干,结果交易所直接封了IP——因为请求太频繁,被当成DDoS攻击了。

2.1.2 WebSocket(推荐方案)

WebSocket才是实时行情的正确姿势。它建立一次连接,服务端主动推送数据给你。延迟能压到1-5毫秒,这才是做量化该有的样子。

这里贴一段我常用的WebSocket行情接入代码框架:

import websocket
import json

class MarketDataFeed:
    def __init__(self, url, symbols):
        self.url = url
        self.symbols = symbols
        self.ws = None
    
    def on_message(self, ws, message):
        # 解析Tick数据
        tick = json.loads(message)
        # 这里做数据清洗和对齐
        self.process_tick(tick)
    
    def on_error(self, ws, error):
        # 我曾经因为没处理断线重连,导致一整个交易时段数据全丢了
        print(f"WebSocket error: {error}")
        self.reconnect()
    
    def on_close(self, ws, close_status_code, close_msg):
        print("Connection closed, reconnecting...")
        self.reconnect()
    
    def on_open(self, ws):
        # 订阅行情
        subscribe_msg = {
            "type": "subscribe",
            "symbols": self.symbols
        }
        ws.send(json.dumps(subscribe_msg))
    
    def connect(self):
        self.ws = websocket.WebSocketApp(
            self.url,
            on_open=self.on_open,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close
        )
        self.ws.run_forever()
    
    def reconnect(self):
        # 指数退避重连策略
        import time
        for i in range(5):
            try:
                time.sleep(2 ** i)
                self.connect()
                break
            except:
                continue
经验之谈: WebSocket一定要做心跳检测。我见过有些交易所的连接,如果30秒没动静就自动断开。所以每15秒发个ping包,这是基本操作。

2.2 历史数据存储方案

数据存哪里?这问题我纠结了很久。InfluxDB和ClickHouse,我两个都用过,各有千秋。

2.2.1 InfluxDB:时序数据库的老牌选手

InfluxDB天生为时序数据而生。写入快、查询简单,尤其适合做实时监控和短周期分析。

特性 InfluxDB ClickHouse
写入速度 极快(百万点/秒) 快(数十万行/秒)
查询延迟 毫秒级(短时间范围) 毫秒级(大规模数据)
存储压缩比 3:1 ~ 5:1 5:1 ~ 10:1
SQL兼容性 类SQL(Flux查询) 标准SQL
适用场景 实时监控、短周期分析 大规模历史分析、复杂聚合

我个人习惯用InfluxDB做「热数据」存储——比如最近7天的Tick数据。查询快,写入也扛得住。

2.2.2 ClickHouse:分析型数据库的王者

ClickHouse是真正的「大数据」选手。如果你要存几年的Tick数据,InfluxDB会变得很慢,但ClickHouse依然游刃有余。

为什么?因为ClickHouse的列式存储和向量化执行引擎,对大规模聚合查询简直是降维打击。

-- ClickHouse建表示例:存储Tick级行情
CREATE TABLE tick_data (
    symbol      String,
    timestamp   DateTime64(3),
    price       Float64,
    volume      Float64,
    bid_price   Float64,
    ask_price   Float64,
    bid_volume  Float64,
    ask_volume  Float64
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (symbol, timestamp);

-- 查询某只股票某天的最高价
SELECT symbol, max(price) as high
FROM tick_data
WHERE symbol = 'BTCUSDT'
  AND timestamp >= '2024-01-01'
  AND timestamp < '2024-01-02'
GROUP BY symbol;
我的建议: 用分层存储策略。InfluxDB存最近7天的热数据,ClickHouse存全量历史数据。查询时先查InfluxDB,没有再去ClickHouse。这样既保证了实时查询速度,又节省了存储成本。

2.3 数据清洗与对齐

数据清洗,这活儿看着简单,做起来全是坑。我刚开始做量化时,以为拿到交易所的原始数据就能直接用。结果回测跑出来收益惊人,实盘一跑就亏——后来才发现是数据没对齐。

2.3.1 Tick级数据去重

为什么会有重复数据?两个原因:

  • 网络重传: WebSocket断线重连后,可能会收到重复的Tick
  • 多源接入: 同时接入多个交易所,同一时刻的行情可能重复

去重策略其实不复杂,核心就是「时间戳+价格+成交量」作为唯一键。

import pandas as pd

def deduplicate_ticks(ticks_df):
    """
    Tick级去重
    我曾经因为没做去重,导致回测里同一个Tick被算了两次成交量
    结果策略的滑点模型完全不准
    """
    # 按时间戳和价格去重
    deduped = ticks_df.drop_duplicates(
        subset=['timestamp', 'price', 'volume'],
        keep='first'
    )
    
    # 按时间戳排序
    deduped = deduped.sort_values('timestamp')
    
    # 检查时间戳是否严格递增
    if not deduped['timestamp'].is_monotonic_increasing:
        # 处理乱序数据
        deduped = deduped.sort_values('timestamp').reset_index(drop=True)
    
    return deduped

2.3.2 数据对齐:不同时间粒度的统一

不同交易所的Tick时间戳精度不一样。有的精确到毫秒,有的精确到微秒。如果你不做对齐,回测和实盘的数据口径就不一致。

我常用的做法是:统一将时间戳对齐到毫秒级,然后做「时间桶」聚合。

def align_ticks_to_millisecond(ticks_df):
    """
    将Tick数据对齐到毫秒级时间桶
    """
    # 截断到毫秒
    ticks_df['aligned_ts'] = ticks_df['timestamp'].dt.floor('ms')
    
    # 按时间桶聚合
    aligned = ticks_df.groupby(['symbol', 'aligned_ts']).agg({
        'price': 'last',      # 取最后一笔价格
        'volume': 'sum',      # 成交量累加
        'bid_price': 'last',
        'ask_price': 'last'
    }).reset_index()
    
    return aligned
避坑指南: 我曾经遇到过一个坑——某交易所的Tick时间戳用的是本地时间,不是UTC。结果数据入库后,跟其他交易所的数据对不上,差了8个小时。所以接入数据时,第一件事就是检查时间戳的时区。

2.4 数据采集层的整体架构

说了这么多,咱们用一张图把整个数据采集层的架构串起来:

数据采集层架构图 数据源层 交易所A (WebSocket) 交易所B (REST API) 交易所C (WebSocket) 其他数据源 数据接入层 WebSocket连接管理 心跳检测 & 断线重连 数据解析 & 格式统一 数据处理层 Tick级去重 时间戳对齐 & 时区校正 数据质量校验 存储层 InfluxDB (热数据 - 7天) ClickHouse (全量历史数据) 消息队列(Kafka)

这张图把整个数据采集层的流程串起来了。从数据源接入,到清洗对齐,再到分层存储,每一步都有讲究。

最后说一句: 数据采集层是量化系统的地基。地基不稳,上面盖的楼再漂亮也没用。我见过太多团队在策略上精益求精,结果数据层随便搞搞,最后回测和实盘对不上,白白浪费几个月时间。所以,把数据层做扎实了,后面的路才好走。

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