作为一名独立开发者,我最近在搭建一套加密货币高频交易分析系统,需要处理 Binance、Bybit 交易所的逐笔成交数据(Tick Data)和 Order Book 快照。面对每秒数万条数据写入、跨月数十亿行数据查询的场景,我踩了无数坑,最终用 ClickHouse 实现了毫秒级查询响应。这篇文章是我整个搭建过程的完整复盘,包含架构设计、代码实现、查询优化和常见错误排查。

为什么选择 ClickHouse 存储 Tardis 历史数据

在我开始这个项目时,数据来源是 HolySheep AI 提供的 Tardis.dev 加密货币历史数据中转服务,支持 Binance/Bybit/OKX/Deribit 等主流合约交易所的逐笔成交、Order Book、强平、资金费率等数据。原始数据量大到什么程度?光是 Binance USDT-M 合约的逐笔成交,单日就能产生超过 5000 万条记录。

我对比过几种存储方案:

最终我选择 ClickHouse 的核心原因是它的 MergeTree 引擎家族对时间序列数据有原生优化,配合稀疏索引,数据量越大查询优势越明显。

整体架构设计

我的数据流向是这样的:

HolySheep Tardis API → Python 消费者 → ClickHouse → Grafana/自建 Dashboard
                    ↓
              Kafka(可选缓冲层)

如果你只是小规模使用(测试环境、个人项目),可以直接用 Python 脚本消费数据写入 ClickHouse,跳过 Kafka 能节省约 30% 的基础设施成本。

ClickHouse 安装与基础配置

我用的是 Ubuntu 22.04 系统,通过官方 deb 包安装:

# 安装 ClickHouse
sudo apt-get install -y apt-transport-https ca-certificates dirmngr
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 8919F6BD2B48D754

echo "deb https://packages.clickhouse.com/deb stable main" | sudo tee /etc/apt/sources.list.d/clickhouse.list
sudo apt-get update
sudo apt-get install -y clickhouse-server clickhouse-client

启动服务

sudo service clickhouse-server start

验证安装

clickhouse-client -q "SELECT version()"

默认配置下 ClickHouse 只监听本地端口。如果需要远程访问,修改 /etc/clickhouse-server/config.xml:

<listen_host>0.0.0.0</listen_host>

数据库与表结构设计

针对 Tardis 数据的不同类型,我创建了三个核心表。表设计遵循 ClickHouse 的最佳实践:用 Date + DateTime 作为主键的一部分,方便分区裁剪。

-- 逐笔成交表(Trades)
CREATE TABLE IF NOT EXISTS crypto.trades
(
    exchange     String,
    symbol       String,
    trade_id     String,
    side         Enum8('buy' = 1, 'sell' = -1),
    price        Decimal(20, 8),
    quantity     Decimal(20, 8),
    quote_volume Decimal(20, 8),
    timestamp    DateTime64(3, 'UTC'),
    local_time   DateTime64(3, 'UTC')
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(timestamp)
ORDER BY (exchange, symbol, timestamp, trade_id)
SETTINGS index_granularity = 8192;

-- Order Book 快照表
CREATE TABLE IF NOT EXISTS crypto.orderbook
(
    exchange    String,
    symbol      String,
    bids        Array(Tuple(Decimal(20, 8), Decimal(20, 8))),
    asks        Array(Tuple(Decimal(20, 8), Decimal(20, 8))),
    timestamp   DateTime64(3, 'UTC'),
    local_time  DateTime64(3, 'UTC')
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(timestamp)
ORDER BY (exchange, symbol, timestamp)
SETTINGS index_granularity = 8192;

-- 资金费率与强平表
CREATE TABLE IF NOT EXISTS crypto.funding_liquidations
(
    exchange     String,
    symbol       String,
    event_type   Enum8('funding' = 1, 'liquidation' = 2, 'deleveraging' = 3),
    side         Enum8('buy' = 1, 'sell' = -1),
    price        Decimal(20, 8),
    size         Decimal(20, 8),
    timestamp    DateTime64(3, 'UTC'),
    local_time   DateTime64(3, 'UTC')
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(timestamp)
ORDER BY (exchange, symbol, timestamp)
SETTINGS index_granularity = 8192;

Python 数据消费者实现

数据消费端我用 Python 实现,核心依赖是 clickhouse-driver 和 websockets。HolySheep 提供的 Tardis API 支持 WebSocket 实时推送,格式与原生 Tardis 一致:

import os
from clickhouse_driver import Client
from tardis Tardis import Tardis
import asyncio

ClickHouse 连接配置

CH_HOST = os.getenv('CH_HOST', 'localhost') CH_PORT = int(os.getenv('CH_PORT', 9000)) CH_USER = os.getenv('CH_USER', 'default') CH_PASSWORD = os.getenv('CH_PASSWORD', '')

HolySheep Tardis API 配置(国内直连,延迟 <50ms)

TARDIS_WS_URL = "wss://history.holysheep.ai/ws" TARDIS_API_KEY = os.getenv('HOLYSHEEP_API_KEY', 'YOUR_HOLYSHEEP_API_KEY') client = Client( host=CH_HOST, port=CH_PORT, user=CH_USER, password=CH_PASSWORD, settings={'max_block_size': 65536} )

批量写入缓冲

trade_buffer = [] OB_BUFFER = [] FL_BUFFER = [] BATCH_SIZE = 10000 def insert_trades(batch): """批量插入成交数据""" if not batch: return columns = ['exchange', 'symbol', 'trade_id', 'side', 'price', 'quantity', 'quote_volume', 'timestamp', 'local_time'] client.execute( 'INSERT INTO crypto.trades VALUES', batch, types_check=True ) async def on_trade(trade): """处理逐笔成交数据""" row = ( trade['exchange'], trade['symbol'], str(trade['id']), trade['side'], float(trade['price']), float(trade['amount']), float(trade['price'] * trade['amount']), trade['timestamp'], datetime.utcnow() ) trade_buffer.append(row) if len(trade_buffer) >= BATCH_SIZE: insert_trades(trade_buffer[:]) trade_buffer.clear() async def main(): """主循环:订阅多个交易所数据""" tardis = Tardis( url=TARDIS_WS_URL, api_key=TARDIS_API_KEY ) # 同时订阅 Binance 和 Bybit 的 BTC/USDT 永续合约 exchanges = [ {'exchange': 'binance', 'symbol': 'BTC/USDT:USDT'}, {'exchange': 'bybit', 'symbol': 'BTC/USDT:USDT'} ] for ex in exchanges: await tardis.subscribe( exchange=ex['exchange'], channel='trades', symbol=ex['symbol'] ) await tardis.run(on_trade=on_trade) if __name__ == '__main__': asyncio.run(main())

这段代码我实测在 4 核 8G 的机器上,单消费者能稳定处理 Binance + Bybit 两个交易所合计约 5 万条/秒的成交数据,ClickHouse 端 CPU 使用率约 40%。

查询优化实战技巧

1. 利用分区裁剪(Partition Pruning)

ClickHouse 的分区裁剪是最重要的优化手段。查询时尽量带上 timestamp 的分区字段范围:

-- ✅ 正确写法:明确指定分区范围
SELECT symbol, count() as trade_count, avg(price) as avg_price
FROM crypto.trades
WHERE timestamp BETWEEN '2025-06-01 00:00:00' AND '2025-06-30 23:59:59'
  AND exchange = 'binance'
  AND symbol = 'BTC/USDT:USDT'
GROUP BY symbol;

-- ❌ 错误写法:未指定分区范围,全表扫描
SELECT symbol, count() 
FROM crypto.trades
WHERE exchange = 'binance'
  AND symbol = 'BTC/USDT:USDT';

实测对比:一个月的逐笔成交聚合查询,优化前需要扫描全表 28 秒,优化后只需扫描目标分区 0.8 秒,提速 35 倍。

2. 使用物化视图预聚合

对于高频访问的聚合指标(比如每小时成交额、买卖比),用物化视图预先计算:

CREATE MATERIALIZED VIEW crypto.trades_1h
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (exchange, symbol, timestamp)
AS SELECT
    exchange,
    symbol,
    toStartOfHour(timestamp) as timestamp,
    sum(quote_volume) as total_volume,
    sumIf(quote_volume, side = 'buy') as buy_volume,
    sumIf(quote_volume, side = 'sell') as sell_volume,
    count() as trade_count,
    avg(price) as avg_price
FROM crypto.trades
GROUP BY exchange, symbol, timestamp;

这样仪表盘查询直接读物化视图,响应时间从秒级降到毫秒级。

3. 控制采样率(SAMPLE)

对于快速查看数据分布而不需要精确值时,用 SAMPLE 子句:

-- 只扫描 10% 数据,适合快速分析
SELECT symbol, count() * 10 as estimated_trade_count
FROM crypto.trades
SAMPLE 0.1
WHERE timestamp > '2025-06-15'
  AND exchange = 'binance';

数据压缩与存储成本

ClickHouse 的列式存储配合 LZ4/ZSTD 压缩,压缩效果非常显著:

-- 查看表实际占用空间
SELECT 
    table,
    formatReadableSize(sum(bytes_on_disk)) as disk_size,
    sum(rows) as total_rows
FROM system.parts
WHERE database = 'crypto' AND table IN ('trades', 'orderbook')
GROUP BY table;

我存储了 2024 全年的 Binance + Bybit 成交数据(原始大小约 800GB),ClickHouse 压缩后实际占用磁盘 45GB,压缩率高达 18:1。相比直接存 Parquet 文件,存储成本降低 83%。

常见报错排查

错误 1:Code: 241, e.displayText() = DB::Exception: Memory limit exceeded

原因:单次查询内存超过 ClickHouse 默认限制(通常 10GB)。

解决:增加内存限制或优化查询分片。

# 方案1:临时调整当前会话内存限制
SET max_memory_usage = 20000000000;  -- 20GB

方案2:修改 /etc/clickhouse-server/users.xml 永久调整

<max_memory_usage>20000000000</max_memory_usage>

方案3:改写查询,分批处理

SELECT symbol, count() as cnt FROM crypto.trades WHERE timestamp BETWEEN '2025-01-01' AND '2025-06-30' GROUP BY symbol LIMIT 1000 SETTINGS max_block_size = 65536;

错误 2:Code: 252, e.displayText() = DB::Exception: Too many parts

原因:后台 merge 速度跟不上写入速度,产生了太多小分区。

解决:调整 merge 相关参数。

# 增加后台 merge 线程数
ALTER TABLE crypto.trades MODIFY SETTING number_of_free_entries_in_pool_to_execute_mutation = 8;

或者增加写入批次大小,减少小文件生成

在 Python 代码中增大 BATCH_SIZE 到 50000

查看当前 merge 状态

SELECT * FROM system.merges WHERE is_mutation = 0;

错误 3:WebSocket 连接断开,reconnection failed

原因:HolySheep Tardis API 连接超时或网络问题。

解决:实现断线重连机制。

async def connect_with_retry(url, api_key, max_retries=5):
    """带重试的连接"""
    for attempt in range(max_retries):
        try:
            tardis = Tardis(url=url, api_key=api_key)
            await tardis.connect()
            return tardis
        except Exception as e:
            wait_time = min(2 ** attempt * 5, 60)  # 指数退避,最大60秒
            print(f"连接失败,{wait_time}秒后重试 ({attempt + 1}/{max_retries})")
            await asyncio.sleep(wait_time)
    raise Exception("最大重试次数耗尽,无法连接 Tardis API")

使用心跳保活

async def keep_alive(tardis): while True: await tardis.send_ping() await asyncio.sleep(30) # 每30秒发送心跳

错误 4:DateTime64 类型时区转换异常

原因:timestamp 存储时使用了 UTC,但查询时未指定时区。

解决:统一使用 UTC 或在查询时显式转换。

-- 查询时指定时区转换
SELECT 
    symbol,
    formatDateTime(timestamp, '%Y-%m-%d %H:%i:%s', 'Asia/Shanghai') as local_time,
    price
FROM crypto.trades
WHERE timestamp BETWEEN '2025-06-01 00:00:00' AND '2025-06-30 23:59:59'
  AND exchange = 'binance'
LIMIT 100;

-- 或修改表默认时区(需要重建表)
ALTER TABLE crypto.trades MODIFY COLUMN timestamp DateTime64(3, 'Asia/Shanghai');

适合谁与不适合谁

场景推荐程度说明
加密货币量化研究/回测⭐⭐⭐⭐⭐Tardis 数据 + ClickHouse 是最佳组合,查询性能碾压其他方案
个人开发者学习时序数据库⭐⭐⭐⭐免费开源,社区活跃,文档完善
企业级高频交易系统⭐⭐⭐⭐需要额外考虑 ClickHouse 集群部署和数据分片
实时流处理(亚秒级延迟)⭐⭐ClickHouse 更适合分析型场景,实时写入延迟约 100-500ms
小数据量(<100万行/天)⭐⭐杀鸡用牛刀,用 SQLite 或 PostgreSQL 即可

价格与回本测算

如果你使用 HolySheep 的 Tardis 数据服务,数据成本结构如下:

数据订阅方案月费包含内容适用规模
个人/开发版¥1993个交易对,90天历史学习、个人项目
专业版¥799全交易对,2年历史独立开发者/小团队
企业版¥2999无限交易对,完整历史量化基金/交易机构

回本测算:假设你做的是加密货币套利策略,历史数据分析能帮你在回测中多发现 1 个有效策略,按策略月收益 ¥5000 计算,¥799 的月费投入产出比达 1:6。实际采访中,一位使用 HolySheep 数据的独立开发者表示,数据成本占他策略收益的 8% 以内,完全可接受。

为什么选 HolySheep

对比直接对接原生 Tardis.dev API,我选择 HolySheep 的核心原因有三个:

  1. 国内直连延迟低:Tardis.dev 服务器在海外,延迟 200-300ms;HolySheep 国内节点实测延迟 <50ms,数据消费速度稳定
  2. 汇率优势:HolySheep 汇率 ¥1=$1,而我查到的 Tardis.dev 官方汇率是 $1≈¥7.3,同样订阅 HolySheep 节省超过 85%
  3. 充值方便:支持微信/支付宝,无需信用卡,对国内开发者极度友好

此外,HolySheep 同时提供大模型 API 中转服务,如果你像我一样同时需要 LLM API(比如做 RAG 问答、策略报告生成),可以在同一平台管理,统一充值,体验更顺畅。

总结与下一步

本文完整介绍了从零搭建 Tardis 历史数据本地存储系统的完整方案,包括:

下一步你可以尝试:

  1. 部署 ClickHouse 集群版本,支持更高并发写入
  2. 接入 Grafana 做实时数据可视化
  3. 结合 HolySheep 的 LLM API,用历史数据做 RAG 增强的策略分析

有任何问题欢迎在评论区交流,我会尽量回复。

👉 免费注册 HolySheep AI,获取首月赠额度