3、数据层设计(上):行情数据源接入(REST/WebSocket)、数据清洗与标准化、数据存储选型(InfluxDB/ClickHouse/PostgreSQL)、数据分片策略

行情数据,是量化系统的血液。没有它,你的策略再牛也是空中楼阁。这一章,我们聊聊数据层最核心的几个问题:数据从哪来?怎么洗干净?存哪里?怎么存才能扛住千万级写入?

我个人习惯把数据层拆成三个环节:接入层 → 清洗层 → 存储层。每个环节都有坑,咱们一个一个说。

3.1 行情数据源接入:REST vs WebSocket

行情源接入,说白了就是跟交易所或数据商打交道。目前主流就两种方式:REST 和 WebSocket。

3.1.1 REST 接口:适合历史数据与低频快照

REST 是请求-响应模式。你发一个 HTTP 请求,对方返回数据。简单、可靠,但延迟高。

适用场景:

  • 拉取历史 K 线数据
  • 获取交易品种列表
  • 低频的快照数据(比如每分钟一次)

代码示例:

import requests
import time

def fetch_kline(symbol, interval, limit=100):
    url = f"https://api.exchange.com/v1/klines"
    params = {
        "symbol": symbol,
        "interval": interval,
        "limit": limit
    }
    resp = requests.get(url, params=params, timeout=5)
    if resp.status_code == 200:
        return resp.json()
    else:
        # 我曾经遇到过交易所返回 429(限流),所以这里一定要加重试
        time.sleep(1)
        return fetch_kline(symbol, interval, limit)
注意:REST 接口有频率限制。我曾经在项目中没注意限流,结果 IP 被封了半小时。建议用令牌桶算法做本地限流。

3.1.2 WebSocket:实时行情的首选

WebSocket 是长连接,服务端主动推送数据。延迟低,适合 tick 级行情。

核心要点:

  • 连接建立后,需要定时发送心跳包(ping/pong)
  • 断线重连机制必须实现
  • 数据解压:很多交易所会压缩数据(如 gzip)

代码示例:

import websocket
import json

def on_message(ws, message):
    data = json.loads(message)
    # 这里直接推送到消息队列,不要做复杂处理
    print(f"收到行情: {data['symbol']} {data['price']}")

def on_error(ws, error):
    print(f"连接异常: {error}")
    # 自动重连逻辑

def on_close(ws, close_status_code, close_msg):
    print("连接关闭,准备重连...")
    # 重连

ws = websocket.WebSocketApp(
    "wss://stream.exchange.com/ws",
    on_message=on_message,
    on_error=on_error,
    on_close=on_close
)
ws.run_forever()
我的经验:WebSocket 连接数不要开太多。一个进程维护 10-20 个连接就够了,多了反而容易触发交易所的限流。用连接池管理。

3.2 数据清洗与标准化

行情数据到了本地,第一件事不是存,而是洗。你想想看,不同交易所的数据格式千奇百怪,字段名都不一样。不标准化,后面策略根本没法用。

3.2.1 清洗规则

  • 去重:同一时间戳的重复数据只保留一条
  • 去噪:价格异常值(比如突然跳变 100%)要标记或剔除
  • 补全:缺失的时间戳用前值填充或插值
  • 对齐:不同交易所的时间戳要统一到同一时区(建议 UTC)

3.2.2 标准化字段

我建议所有行情数据统一成以下结构:

字段 类型 说明
symbol string 交易对,如 BTCUSDT
exchange string 交易所名称
timestamp int64 Unix 毫秒时间戳
open float64 开盘价
high float64 最高价
low float64 最低价
close float64 收盘价
volume float64 成交量
核心原则:清洗层只做一件事——把脏数据变成干净数据。不要在这里做任何计算或聚合。保持幂等性。

3.3 数据存储选型

存行情数据,选型很关键。我见过不少团队用 MySQL 存 tick 数据,结果一个月后查询就慢成狗。选对存储,事半功倍。

3.3.1 InfluxDB:时序数据库的首选

InfluxDB 是专门为时序数据设计的。写入快,查询快,压缩率高。

优点:

  • 写入吞吐极高,单机可支撑百万点/秒
  • 自带降采样和保留策略
  • 查询语法简单,适合做聚合

缺点:

  • 集群版收费,社区版单机有瓶颈
  • 不支持复杂 JOIN

3.3.2 ClickHouse:适合大规模分析

ClickHouse 是列式存储,适合做海量数据的聚合查询。如果你需要做回测或统计,它很合适。

优点:

  • 压缩比极高,存储成本低
  • 查询速度极快,特别是聚合查询
  • 支持 SQL,学习成本低

缺点:

  • 不适合高频写入(每秒超过 10 万行会有压力)
  • 单行更新/删除性能差

3.3.3 PostgreSQL:适合元数据与配置

PostgreSQL 不是用来存行情数据的,但适合存交易对信息、策略配置、用户数据等。

适用场景:

  • 交易品种列表
  • 策略参数配置
  • 交易日志
我的建议:不要只用一种数据库。我习惯用 InfluxDB 存实时行情,ClickHouse 存历史数据做分析,PostgreSQL 存元数据。各司其职。

3.4 数据分片策略

数据量大了,单机扛不住。分片是必须的。分片策略直接影响查询性能和扩展性。

3.4.1 按时间分片

最常用的方式。比如按天、按小时分片。

  • 优点:查询时间范围明确时效率高,过期数据容易清理
  • 缺点:热点问题——最近的数据写入压力集中

3.4.2 按交易对分片

每个交易对独立一个分片。

  • 优点:查询单个交易对时性能好,扩展容易
  • 缺点:跨交易对的查询需要合并结果

3.4.3 混合分片

我个人最推荐的方式。先按交易对分片,再按时间分片。

示例:

# 分片键设计
shard_key = f"{symbol}_{date}"

# 例如
BTCUSDT_20240101
ETHUSDT_20240101
BTCUSDT_20240102
分片原则:
  • 数据均匀分布,避免热点
  • 查询尽量落在单个或少部分分片上
  • 分片粒度不要太小(比如按小时分片会导致分片过多)

3.5 整体架构图

下面这张图展示了行情数据从接入到存储的完整流程:

行情数据接入与存储架构 交易所 REST API 交易所 WebSocket 第三方数据商 接入层 连接管理 / 心跳 / 重连 / 限流 清洗与标准化层 去重 / 去噪 / 补全 / 字段标准化 InfluxDB 实时行情 ClickHouse 历史分析 PostgreSQL 元数据 分片策略 按交易对分片 按时间分片 混合分片 分片键设计 数据路由 负载均衡
避坑指南:我曾经在分片时犯过一个错误——按分钟分片。结果一天产生了 1440 个分片,管理成本极高,查询反而变慢了。分片粒度建议按天或按小时,别太细。

嗯,数据层设计的上半部分就到这里。核心就是三件事:接入要稳、清洗要净、存储要准。下一章我们继续聊数据层的下半部分——缓存、消息队列和数据治理。


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