作为一名独立开发者,我最近在搭建一套加密货币高频交易分析系统,需要处理 Binance、Bybit 交易所的逐笔成交数据(Tick Data)和 Order Book 快照。面对每秒数万条数据写入、跨月数十亿行数据查询的场景,我踩了无数坑,最终用 ClickHouse 实现了毫秒级查询响应。这篇文章是我整个搭建过程的完整复盘,包含架构设计、代码实现、查询优化和常见错误排查。
为什么选择 ClickHouse 存储 Tardis 历史数据
在我开始这个项目时,数据来源是 HolySheep AI 提供的 Tardis.dev 加密货币历史数据中转服务,支持 Binance/Bybit/OKX/Deribit 等主流合约交易所的逐笔成交、Order Book、强平、资金费率等数据。原始数据量大到什么程度?光是 Binance USDT-M 合约的逐笔成交,单日就能产生超过 5000 万条记录。
我对比过几种存储方案:
- PostgreSQL:单机写入性能瓶颈明显,10万/秒写入就开始出现延迟
- TimescaleDB:虽然支持时序数据,但压缩率不如 ClickHouse
- ClickHouse:列式存储、向量化执行,实测单表 10 亿行数据聚合查询在 3 秒内完成
最终我选择 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 数据服务,数据成本结构如下:
| 数据订阅方案 | 月费 | 包含内容 | 适用规模 |
|---|---|---|---|
| 个人/开发版 | ¥199 | 3个交易对,90天历史 | 学习、个人项目 |
| 专业版 | ¥799 | 全交易对,2年历史 | 独立开发者/小团队 |
| 企业版 | ¥2999 | 无限交易对,完整历史 | 量化基金/交易机构 |
回本测算:假设你做的是加密货币套利策略,历史数据分析能帮你在回测中多发现 1 个有效策略,按策略月收益 ¥5000 计算,¥799 的月费投入产出比达 1:6。实际采访中,一位使用 HolySheep 数据的独立开发者表示,数据成本占他策略收益的 8% 以内,完全可接受。
为什么选 HolySheep
对比直接对接原生 Tardis.dev API,我选择 HolySheep 的核心原因有三个:
- 国内直连延迟低:Tardis.dev 服务器在海外,延迟 200-300ms;HolySheep 国内节点实测延迟 <50ms,数据消费速度稳定
- 汇率优势:HolySheep 汇率 ¥1=$1,而我查到的 Tardis.dev 官方汇率是 $1≈¥7.3,同样订阅 HolySheep 节省超过 85%
- 充值方便:支持微信/支付宝,无需信用卡,对国内开发者极度友好
此外,HolySheep 同时提供大模型 API 中转服务,如果你像我一样同时需要 LLM API(比如做 RAG 问答、策略报告生成),可以在同一平台管理,统一充值,体验更顺畅。
总结与下一步
本文完整介绍了从零搭建 Tardis 历史数据本地存储系统的完整方案,包括:
- ClickHouse 表结构设计(MergeTree 引擎、分区策略)
- Python 数据消费者实现(批量写入、断线重连)
- 查询优化技巧(分区裁剪、物化视图、采样)
- 4 个常见错误的完整解决方案
下一步你可以尝试:
- 部署 ClickHouse 集群版本,支持更高并发写入
- 接入 Grafana 做实时数据可视化
- 结合 HolySheep 的 LLM API,用历史数据做 RAG 增强的策略分析
有任何问题欢迎在评论区交流,我会尽量回复。
👉 免费注册 HolySheep AI,获取首月赠额度