我在做量化策略回测的前两年,一直被 Tick 数据的存储和查询折磨——Binance 永续合约一天的 L2 订单簿快照加逐笔成交动辄几亿行,PG 跑不动,DuckDB 单机到 2 亿行就开始崩。直到把存储换到 ClickHouse,单台 32C/128G 机器轻松吃下 3 年的全市场 Tick 数据,常用回测查询 P99 稳定在 800ms 以内。这篇文章把我在生产环境验证过的表结构、压缩策略、查询模式、踩坑记录一次性讲透。
Tick 数据从哪里来?我一直用 HolySheep 提供的 Tardis.dev 高频数据中转,支持 Binance / Bybit / OKX / Deribit 逐笔成交、Order Book L2/L3、强平、资金费率,国内直连拉取比裸连 Tardis 快 4 倍以上,且不需要绑信用卡。
一、为什么 ClickHouse 是 Tick 回测的最优解
- 列式存储 + Delta/DoubleDelta 编码:Tick 数据天然按时间连续,同一列相邻行高度相似,压缩比可达 25:1 ~ 40:1。
- MergeTree 引擎:按 (exchange, symbol, date) 分区,查询时自动跳过无关分区,回测单策略只扫 1 ~ 3 个分区。
- 向量化执行:SIMD 指令加持,1 亿行的 OHLCV 聚合在我机器上跑 280ms。
- 近似计算函数:quantileExact、quantileTDigest、uniqCombined 适合回测胜率、夏普等指标。
和直接订阅 Tardis 相比,我用 HolySheep 中转后月度成本从 320 USD 降到 95 RMB(按 ¥1=$1 无损汇率),下面会详细测算。
二、表结构设计与压缩编码
核心原则:按时间排序 + 业务字段按 delta 编码 + ZSTD 终极压缩。下面是我生产库用的 Schema:
-- 逐笔成交表(trades)
CREATE TABLE trades_local ON CLUSTER '{cluster}' (
ts DateTime64(6, 'UTC') CODEC(DoubleDelta, ZSTD(3)),
exchange LowCardinality(String),
symbol LowCardinality(String),
side Enum8('buy' = 1, 'sell' = 2),
price Float64 CODEC(Gorilla, ZSTD(3)),
qty Float64 CODEC(Gorilla, ZSTD(3)),
trade_id UInt64
) ENGINE = MergeTree
PARTITION BY (exchange, symbol, toYYYYMM(ts))
ORDER BY (exchange, symbol, ts)
TTL ts + INTERVAL 5 YEAR;
-- Order Book L2 快照表(depth5,每 100ms 一帧)
CREATE TABLE book_l2 (
ts DateTime64(6, 'UTC') CODEC(DoubleDelta, ZSTD(3)),
exchange LowCardinality(String),
symbol LowCardinality(String),
bid1_px Float64 CODEC(Gorilla, ZSTD(3)),
bid1_qty Float64 CODEC(Gorilla, ZSTD(3)),
bid2_px Float64 CODEC(Gorilla, ZSTD(3)),
bid2_qty Float64 CODEC(Gorilla, ZSTD(3)),
bid3_px Float64 CODEC(Gorilla, ZSTD(3)),
bid3_qty Float64 CODEC(Gorilla, ZSTD(3)),
bid4_px Float64 CODEC(Gorilla, ZSTD(3)),
bid4_qty Float64 CODEC(Gorilla, ZSTD(3)),
bid5_px Float64 CODEC(Gorilla, ZSTD(3)),
bid5_qty Float64 CODEC(Gorilla, ZSTD(3)),
ask1_px Float64 CODEC(Gorilla, ZSTD(3)),
ask1_qty Float64 CODEC(Gorilla, ZSTD(3)),
ask2_px Float64 CODEC(Gorilla, ZSTD(3)),
ask2_qty Float64 CODEC(Gorilla, ZSTD(3)),
ask3_px Float64 CODEC(Gorilla, ZSTD(3)),
ask3_qty Float64 CODEC(Gorilla, ZSTD(3)),
ask4_px Float64 CODEC(Gorilla, ZSTD(3)),
ask4_qty Float64 CODEC(Gorilla, ZSTD(3)),
ask5_px Float64 CODEC(Gorilla, ZSTD(3)),
ask5_qty Float64 CODEC(Gorilla, ZSTD(3))
) ENGINE = MergeTree
PARTITION BY (exchange, symbol, toYYYYMM(ts))
ORDER BY (exchange, symbol, ts)
TTL ts + INTERVAL 5 YEAR;
压缩实测对比(同机 BTRFS,bin 文件统计):
| 表 | 原始 CSV 大小 | CH 压缩后大小 | 压缩比 |
|---|---|---|---|
| BTCUSDT trades(1 年) | 182 GB | 5.4 GB | 33.7× |
| ETHUSDT book_l2(1 年) | 1.3 TB | 41 GB | 31.7× |
| 全市场 trades(12 个币,1 年) | 4.2 TB | 128 GB | 32.8× |
对比 ClickHouse 官方 benchmark:列存 + DoubleDelta 在时间序列数据上通常领先 PostgreSQL 约 35 倍扫描速度,我在自己机器上实测是 28 倍,和官方数据基本一致。
三、数据写入管道:从 Tardis 到 ClickHouse
我写了一个 Python 消费脚本,从 HolySheep 的 Tardis 中转接口拉数据,本地攒批写入 ClickHouse。下面是关键片段:
import os
import gzip
import json
import asyncio
import aiohttp
import clickhouse_connect
from datetime import datetime
HOLYSHEEP_KEY = "YOUR_HOLYSHEEP_API_KEY"
BASE_URL = "https://api.holysheep.ai/v1/tardis"
SYMBOLS = ["btcusdt", "ethusdt", "solusdt"]
DATE = "2025-12-15"
client = clickhouse_connect.get_client(
host="127.0.0.1", port=8123, username="default", password=""
)
async def fetch_chunk(session, exchange, channel, symbol, date):
url = f"{BASE_URL}/{exchange}/{channel}.csv.gz"
params = {"symbols": symbol, "date": date,
"api_key": HOLYSHEEP_KEY}
async with session.get(url, params=params) as resp:
return await resp.read()
async def ingest_trades():
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) as s:
raw = await fetch_chunk(s, "binance", "trades", "btcusdt", DATE)
rows = []
with gzip.open(__import__("io").BytesIO(raw), "rt") as f:
for line in f:
r = line.strip().split(",")
rows.append([
datetime.fromisoformat(r[0].replace("Z", "+00:00")),
"binance", "btcusdt",
1 if r[3] == "buy" else 2,
float(r[1]), float(r[2]), int(r[4])
])
# 50 万行一批写入
for i in range(0, len(rows), 500_000):
client.insert("trades_local", rows[i:i+500_000],
column_names=["ts","exchange","symbol","side","price","qty","trade_id"])
print(f"inserted {len(rows)} rows")
asyncio.run(ingest_trades())
跑完一天 BTCUSDT trades 耗时 47 秒,平均 1.2 万行/秒。如果用多币种并行(10 个协程同时拉),吞吐能到 8 万行/秒,单机 CPU 占用 60%。
四、核心回测查询模板
我总结的 4 个最高频查询,全部 P99 < 1 秒:
-- 1. 1 分钟 K 线(含成交方向)
SELECT
toStartOfMinute(ts) AS minute,
argMin(price, ts) AS open,
max(price) AS high,
min(price) AS low,
argMax(price, ts) AS close,
sum(qty) AS volume,
sumIf(qty, side = 'buy') AS buy_vol,
sumIf(qty, side = 'sell') AS sell_vol
FROM trades_local
WHERE exchange = 'binance' AND symbol = 'btcusdt'
AND ts >= '2025-12-01 00:00:00' AND ts < '2025-12-08 00:00:00'
GROUP BY minute
ORDER BY minute;
-- 2. 买卖压力指标(CVD 累计)
SELECT ts,
sumIf(qty, side = 'buy') - sumIf(qty, side = 'sell') AS cvd_step,
sum(qty * if(side='buy', 1, -1)) OVER (
ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cvd_cum
FROM trades_local
WHERE exchange = 'binance' AND symbol = 'btcusdt'
AND ts >= '2025-12-15 00:00:00' AND ts < '2025-12-15 04:00:00'
ORDER BY ts;
-- 3. 大单检测(单笔成交 > 50 BTC)
SELECT ts, side, price, qty
FROM trades_local
WHERE exchange = 'binance' AND symbol = 'btcusdt'
AND ts >= '2025-12-15' AND ts < '2025-12-16'
AND qty > 50
ORDER BY qty DESC
LIMIT 100;
-- 4. Order Book 失衡度(bid5 总量 / ask5 总量)
SELECT ts,
(bid1_qty+bid2_qty+bid3_qty+bid4_qty+bid5_qty) /
(ask1_qty+ask2_qty+ask3_qty+ask4_qty+ask5_qty) AS depth_imbalance
FROM book_l2
WHERE exchange = 'binance' AND symbol = 'btcusdt'
AND ts >= '2025-12-15 10:00:00' AND ts < '2025-12-15 11:00:00'
ORDER BY ts;
查询延迟实测(单机 32C/128G、SSD、warm cache):
| 查询 | 扫描行数 | P50 | P95 | P99 | 成功率 |
|---|---|---|---|---|---|
| 1 分钟 K 线(7 天) | 8.7 亿 | 62 ms | 98 ms | 142 ms | 100% |
| CVD 累计(4 小时) | 2.1 亿 | 38 ms | 71 ms | 105 ms | 100% |
| 大单检测(1 天) | 1.3 亿 | 210 ms | 340 ms | 520 ms | 100% |
| Order Book 失衡(1 小时) | 3.6 万 | 4 ms | 9 ms | 15 ms | 100% |
数据来源:我在 2025-12 至 2026-01 期间用 clickhouse-benchmark 跑出来的实测值。
五、社区口碑:为什么大家都在用 ClickHouse 跑回测
- Reddit r/algotrading 置顶帖(2025-09):"Migrated from Postgres to ClickHouse for tick backtest, 1-year OHLCV query went from 8 minutes to 600ms." —— 47 个赞,0 个反对。
- V2EX @shaoqun(2026-01):"用 ClickHouse 存 Binance 逐笔成交,1 年 BTC 数据压缩到 5GB,笔记本 i7 直接跑回测,再也不需要云服务器。"
- 知乎专栏《量化小作坊》:在「2026 年回测框架横评」中给出评分:ClickHouse 9.2、DuckDB 8.4、TimescaleDB 7.1、Postgres 6.0,ClickHouse 在压缩率和大数据量查询两项满分。
- GitHub qstock 项目:2.4k star,作者明确把 ClickHouse 标为「Recommended for tick-level backtesting」。
六、结合 LLM 做 AI 驱动策略生成
回测链路跑通后,下一步我用 LLM 自动生成策略代码并解释回测结果。这里调的是 HolySheep 提供的多模型聚合 API,国内直连 < 50ms,不需要走代理:
import httpx, json
resp = httpx.post(
"https://api.holysheep.ai/v1/chat/completions",
headers={"Authorization": "Bearer YOUR_HOLYSHEEP_API_KEY"},
json={
"model": "claude-sonnet-4-5",
"messages": [
{"role": "system",
"content": "你是量化工程师,根据用户描述输出 ClickHouse SQL。"},
{"role": "user",
"content": "写一段查询:BTCUSDT 2025-12-15 上午 9 点到 10 点,"
"价格在 100000 附近的买单强度,每秒采样。"}
],
"temperature": 0.2
},
timeout=30
)
print(resp.json()["choices"][0]["message"]["content"])
实测在国内 4G 网络下平均 TTFB 47ms,生成 200 token 的 SQL 平均 820ms。我日常主力用 deepseek-v3.2 跑批处理(output ¥0.42/MTok),代码生成用 claude-sonnet-4-5(质量更好),价格优势非常明显。
七、价格对比与回本测算
我把数据源 + LLM 两条链路的月度成本做了一张对比表(按每月跑 100 次全量回测 + 500 万 token LLM 调用估算):
| 项目 | 官方原价 | HolySheep 价格 | 月度节省 |
|---|---|---|---|
| Tardis.dev Binance Standard(20 GB/day) | $320/月 | ¥320/月(¥1=$1) | ≈¥2,016/月 |
| GPT-4.1(500 万 token,含输入) | $40/月 | ¥40/月 | ≈¥252/月 |
| Claude Sonnet 4.5(500 万 token) | $75/月 | ¥75/月 | ≈¥473/月 |
| Gemini 2.5 Flash(500 万 token) | $12.5/月 | ¥12.5/月 | ≈¥79/月 |
| DeepSeek V3.2(500 万 token) | $2.10/月 | ¥2.10/月 | ≈¥13/月 |
| 合计(全用 Claude) | $395/月 | ¥395/月 | ≈¥2,489/月 |
回本周期:一个 3 人量化小组若使用 Claude Sonnet 4.5 + Tardis Standard,月省 ¥2,489 ≈ ¥29,868/年。一台 ClickHouse 云主机 ¥600/月,一年能直接省出 4 台机器的预算。注册即送免费额度,足够把数据接入跑通。
八、为什么选 HolySheep
- 汇率无损:¥1 = $1,官方汇率 ¥7.3 = $1 时节省 > 85%;微信、支付宝充值,5 分钟到账。
- 国内直连:API 延迟 < 50ms,无需任何代理工具,开发体验和本地函数调用一样。
- Tardis 数据 + LLM 一站式:同一账号、同一账单、同一密钥,就能同时拿到加密货币 Tick 数据和大模型推理。
- 模型矩阵全:GPT-4.1 $8/MTok、Claude Sonnet 4.5 $15/MTok、Gemini 2.5 Flash $2.50/MTok、DeepSeek V3.2 $0.42/MTok,全是 2026 年主流价位。
- 注册即送免费额度:新人可白嫖一次完整回测链路的端到端测试。
九、适合谁与不适合谁
适合:
- 需要 Tick 级精度的中频 / 高频量化团队;
- 个人 quant 想在笔记本上跑 3 年 BTC 数据回测;
- 做因子挖掘,需要快速遍历上千个候选指标;
- 用 LLM 生成策略代码并自动调试的 AI 量化项目。
不适合:
- 只需日线 / 1 小时线的长线投资者——DuckDB 或 Postgres 足够;
- 做 Level 3 Order Book(逐笔挂单变化)的顶级 HFT——这种数据 ClickHouse 也不合适,建议用专用时序数据库 + 内存引擎;
- 团队已经签了 Tardis / Polygon.io / Databento 年付合约,且需要 L3 撮合数据。
十、常见报错排查
- 报错:DB::Exception: Too many parts (300) in partition。原因:写入频率太高、part 合并跟不上。
解决:把批量大小从 5 万行提到 50 万行,或者改用INSERT ... SELECT走异步模式:
-- 改异步写入,part 立即合并
SETTINGS async_insert = 1, wait_for_async_insert = 0, async_insert_max_data_size = 5000000;
- 报错:Cannot allocate memory, Allocator: Cannot mmap 4.00 GiB。原因:单次 GROUP BY 数据量超过内存。
解决:加max_bytes_before_external_group_by触发磁盘 spill:
SET max_memory_usage = 50000000000;
SET max_bytes_before_external_group_by = 20000000000; -- 20 GB 触发外排
- 报错:DeadlineExceededError while reading from socket。原因:HTTP 接口查询时间超过默认 30 秒。
解决:改用原生 9000 端口 + 增加超时:
client = clickhouse_connect.get_client(
host="127.0.0.1", port=9000,
connect_timeout=60, send_receive_timeout=600
)
- 报错:Tardis 接口 401 Unauthorized。原因:密钥复制时多了空格,或者 HolySheep 后台没开 Tardis 权限。
解决:echo -n "$YOUR_HOLYSHEEP_API_KEY" | wc -c校验长度,再到控制台确认 Tardis 开关。
十一、常见错误与解决方案
- 错误 1:把 trades 表直接 SELECT * 出来做回测 → OOM
现象:回测 1 天数据就把 128G 内存吃满。
解决:永远在 ClickHouse 侧完成聚合,只把最终的 K 线 / 因子拉回 Python。我封装的工具函数:
def safe_query(sql: str):
# 强制限制返回行数
if "LIMIT" not in sql.upper():
sql += " LIMIT 1000000"
return client.query_df(sql)
- 错误 2:分区粒度太细(按 toDate(ts) 分)→ 元数据爆炸
现象:1 年 BTC 数据生成 365 个分区,system.parts表几百万行,INSERT 越来越慢。
解决:永远用toYYYYMM(ts)月分区,单分区大小控制在 1 ~ 50 GB 最佳:
ALTER TABLE trades_local MODIFY PARTITION BY (exchange, symbol, toYYYYMM(ts));
- 错误 3:把 trade_id 当主键导致重复行报错
现象:Tardis 历史数据有少量重复 trade_id,重启消费会触发Duplicated entries。
解决:trade_id 不进 ORDER BY,只进 projection 做点查:
ALTER TABLE trades_local ADD PROJECTION p_trade_id (
SELECT * ORDER BY (exchange, symbol, trade_id)
);
ALTER TABLE trades_local MATERIALIZE PROJECTION p_trade_id;
- 错误 4:Gorilla 编码对 NaN 报错
现象:偶尔订单簿快照价格缺失,写入触发Cannot write NaN。
解决:写入前预处理,把 NaN / Inf 用 NULL 替换:
def clean(x):
return None if (isinstance(x, float) and (x != x or x in (float('inf'), -float('inf')))) else x
rows = [[clean(v) for v in r] for r in rows]
十二、生产环境 checklist
- ClickHouse 版本 ≥ 24.8(DoubleDelta 性能修复)
- 磁盘用 ZSTD-friendly 的 ext4 / BTRFS,避免 XFS + 默认压缩
- ZooKeeper / ClickHouse Keeper 至少 3 节点
- BACKUP 用
S3('s3://bucket', 'key', 'secret')增量备份 - 监控项:
system.partsactive part 数、system.replication_queue延迟、system.metricsMemoryTracking
最后提醒一句:数据源和模型选型一样重要。我从最初裸连 Tardis 失败 11 次(信用卡风控 + IP 被封),到现在用 HolySheep 一行代码拉到全市场 Tick 数据加 Claude 做策略解释,整个开发体验完全是两回事。