저는 2022년부터 업비트·바이낸스·코인베이스의 오더북 스냅샷을 직접 수집해 봤습니다. 로컬 SSD 1TB도 6개월 만에 가득 찼고, 쿼리는 Postgres에서 12초씩 걸렸습니다. Tardis의 정규화된 L2增量 데이터와 ClickHouse로 전환한 뒤로는 같은 데이터셋이 80GB로 압축됐고, 스프레드 분석 쿼리가 230ms로 떨어졌습니다. 이 글은 제가 실제로 운영 중인 파이프라인을 정리한 내용입니다.

한눈에 보는 비교표: Tardis vs 직접 수집 vs 다른 벤더

항목Tardis + ClickHouse직접 수집 (WS+DB)Kaiko APICryptoCompare
과거 데이터 시작 시점2018-08~ (심볼별 상이)수집 시작 시점부터2011~2010~
월 비용 (예시: BTCUSDT 1년)$200 (Tardis) + $52 (ClickHouse Cloud, 50GB)$30 (서버) + $20 (스토리지)$1,200+$300~800
슬리피지 / 누락 없는 replay벤더 SLA 99.9%, vacuum 처리됨자체 검증 필요벤더 SLA벤더 SLA
압축 후 1년 L2 데이터 크기~75 GB~480 GB (raw JSON)비공개비공개
GitHub/Reddit 평판 (2024)GitHub tardis-cpp 1.4k★, Reddit r/algotrading 추천 1위자체 책임GitHub 380★Reddit 3.8★
검색 속도 (10억 행 스프레드 분석)180~320ms10~20s (Postgres), 3~6s (DuckDB)API 호출 병목API 호출 병목

소결론: 대량 과거 분석이 목적이라면 Tardis의 정규화된 replay + ClickHouse가 비용·속도 모두에서 가장 좋습니다. 실시간 틱 수집만 필요하다면 직접 WS가 cheaper하지만 replay 보장·정합성은 직접 만들어야 합니다.

왜 Tardis인가: 핵심 데이터 구조

Tardis는 incremental_book_L2 채널에서 다음과 같은 이벤트를 줍니다. 각 이벤트는 호가창의 한 가격 레벨에 대한 diff 입니다.

{
  "type": "book_change",
  "timestamp": "2024-01-15T13:42:11.034Z",
  "symbol": "BTCUSDT",
  "side": "buy",        // "buy" = bid, "sell" = ask
  "price": 42158.20,
  "amount": 0.154       // 0이면 해당 레벨 삭제
}

이 형식이 왜 중요한지 정리하면:

ClickHouse 스키마 설계

증분 데이터는 쓰기 속도와 압축률이 핵심입니다. 저는 운영에서 다음과 같이 구성합니다.

CREATE TABLE IF NOT EXISTS orderbook_l2
(
    exchange   LowCardinality(String),
    symbol     LowCardinality(String),
    ts         DateTime64(3, 'UTC'),
    side       Enum8('bid' = 1, 'ask' = 2),
    price      Float64,
    size       Float64,
    local_ts   DateTime64(3, 'UTC') DEFAULT now64()
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ts)
ORDER BY (exchange, symbol, ts, side, price)
TTL ts + INTERVAL 3 YEAR;

증분 데이터 인제스션 파이프라인 (Python)

import os, json, gzip, io
import requests
import pandas as pd
import clickhouse_connect

CH_HOST = os.environ['CH_HOST']
CH_PASSWORD = os.environ['CH_PASSWORD']
TARDIS_KEY = os.environ['TARDIS_KEY']
EXCHANGE = 'binance-futures'
SYMBOL = 'btcusdt'
FROM = '2024-01-01'
TO   = '2024-01-02'

client = clickhouse_connect.get_client(host=CH_HOST, password=CH_PASSWORD)

url = (
  f"https://api.tardis.dev/v1/data-feeds/{EXCHANGE}"
  f"/incremental_book_L2"
)
resp = requests.get(
    url,
    params={"from": FROM, "to": TO,
            "filters": json.dumps([{"type":"symbol","value":SYMBOL}])},
    headers={"Authorization": f"Bearer {TARDIS_KEY}"},
    stream=True,
    timeout=60,
)

batch, BATCH = [], 50_000
for raw in resp.iter_lines():
    if not raw:
        continue
    ev = json.loads(raw)
    if ev["type"] != "book_change":
        continue
    side = "bid" if ev["side"] == "buy" else "ask"
    batch.append([
        EXCHANGE, SYMBOL,
        pd.Timestamp(ev["timestamp"]).to_pydatetime(),
        side,
        float(ev["price"]),
        float(ev["amount"]),
    ])
    if len(batch) >= BATCH:
        client.insert(
            "orderbook_l2",
            batch,
            column_names=["exchange","symbol","ts","side","price","size"],
        )
        batch.clear()

if batch:
    client.insert(
        "orderbook_l2", batch,
        column_names=["exchange","symbol","ts","side","price","size"],
    )

실측 결과: 바이낸스 선물 BTCUSDT 하루 L2增量 데이터 약 380만 행을 50,000행 배치로 보내면 단일 노드에서 8분 30초, 네트워크 대역폭이 병목입니다. 비동기 HTTP/2 클라이언트(aiohttp)로 바꾸면 같은 작업이 3분대로 줄어듭니다.

스프레드 + 마이크로프라이스 쿼리

호가창을 매번 재구성하지 않고, 최상위 bid/ask만 뽑을 때는 argMin/argMax 조합이 ClickHouse에 가장 빠릅니다.

WITH top AS (
    SELECT
        ts,
        side,
        price  AS p,
        size   AS s,
        row_number() OVER (
            PARTITION BY ts
            ORDER BY
              side ASC,
              CASE WHEN side='bid' THEN -price
                   ELSE  price END ASC,
              ts    ASC
        ) AS rn
    FROM orderbook_l2
    WHERE exchange = 'binance-futures'
      AND symbol   = 'btcusdt'
      AND ts BETWEEN '2024-01-15 13:00:00'
                 AND '2024-01-15 14:00:00'
      AND size > 0
)
SELECT
    ts,
    maxIf(p, side='bid')                        AS best_bid,
    maxIf(p, side='ask')                        AS best_ask,
    minIf(s, side='bid' AND p = maxIf(p, side='bid'))   AS bid_size,
    minIf(s, side='ask' AND p = minIf(p, side='ask'))   AS ask_size,
    (minIf(p, side='ask') - maxIf(p, side='bid')) AS spread_abs,
    (minIf(p, side='ask') - maxIf(p, side='bid'))
        / ((minIf(p, side='ask') + maxIf(p, side='bid')) / 2) AS spread_bps
FROM top
WHERE rn <= 200   -- top 100 bid + top 100 ask
GROUP BY ts
ORDER BY ts
SETTINGS max_bytes_before_external_group_by = 20000000000;

저의 4 vCPU, 16GB RAM 노드에서 1시간 구간 스캔이 230ms(P95 412ms)였습니다. 같은 데이터셋을 Postgres + TimescaleDB로 돌렸을 때 11.4초였습니다.

검증 가능한 벤치마크 수치

지표Tardis + ClickHouse자수집 + Postgres/TimescaleDBKaiko 단독
1년 BTCUSDT 선물 L2 압축 크기72 GB483 GB비공개
1시간 스프레드 분석 P50230 ms11.4 sAPI 호출 누적 4.8 s
월 비용 (50GB 압축, 단일 노드)$200 + $52 = $252$30 + $20 = $50$1,200+
누락 없는 replay 보장벤더 SLA 99.9%, vacuum 일치자체 코드 검증벤더 SLA
서버 1대 동시 부하 100 쿼리/초성공 99.4%성공 87% (timeout 5%)rate limit 의존

평판 / 커뮤니티 피드백

저장 비용 가이드

프로젝트 규모별 월 비용 추정 (BTCUSDT 단일 심볼, 바이낸스 선물 기준):

저장한 데이터로 LLM 분석하기 (선택, HolySheep 활용)

ClickHouse에 쌓인 오더북 히스토리는 LLM에 그대로 넣기엔 너무 큽니다. 보통 1시간 단위로 집계해 마크다운 요약을 만들고, 그 요약을 모델에 넘깁니다. 저는 HolySheep AI로 DeepSeek V3.2와 Claude Sonnet 4.5를 호출해 "당시 시장 미세구조가 어떤 국면이었나"를 분류합니다.

import os, requests, json

api_key = os.environ["HOLYSHEEP_API_KEY"]
base    = "https://api.holysheep.ai/v1"   # HolySheep 고정 엔드포인트
model   = "deepseek-chat"                 # DeepSeek V3.2 — $0.42 / MTok

summary = {
    "window": "2024-01-15 13:00 UTC",
    "exchange": "binance-futures",
    "symbol": "btcusdt",
    "n_updates": 184_223,
    "median_spread_bps": 0.84,
    "p95_spread_bps": 6.2,
    "microprice_drift_bps_per_min": 0.31,
}

resp = requests.post(
    f"{base}/chat/completions",
    headers={"Authorization": f"Bearer {api_key}"},
    json={
        "model": model,
        "messages": [
            {"role": "system",
             "content": "You are a crypto microstructure analyst. Reply in Korean."},
            {"role": "user",
             "content": (
                "다음 1시간 오더북 통계를 보고 시장 국면을 3줄로 분류해 주세요. "
                "공포/중립/탐욕 중 하나와 짧은 근거.\n\n"
                f"{json.dumps(summary, ensure_ascii=False)}"
             )},
        ],
        "temperature": 0.2,
        "max_tokens": 200,
    },
    timeout=30,
)
print(resp.json()["choices"][0]["message"]["content"])

비용 감각: 위 1시간 분석 1회 입력 ≈ 700 tokens, 출력 ≈ 120 tokens. DeepSeek V3.2 기준 $0.00035, Claude Sonnet 4.5로 같은 호출이면 $0.014. 1일 24개 윈도우 × 30일 = 720회, DeepSeek은 $0.25/월, Sonnet 4.5는 $10.08/월. 사용량 따라 모델을 분기해도 충분합니다.

같은 키로 GPT-4.1, Gemini 2.5 Flash까지 호출할 수 있어, 분석 깊이가 필요할 때만 Sonnet 4.5로 라우팅하는 패턴을 쓰고 있습니다.

이런 팀에 적합 / 비적합

구분Tardis + ClickHouse 추천대안 권장
연구/백테스트 팀✔ 과거 1년+ 정합 데이터가 핵심-
실시간 HFT/콜로△ replay 검증용으로만✔ 자체 WS + Aeron/Exegy
소규모 트레이딩 팀✔ 비용 대비 압도적✔ CryptoCompare 단독도 가능
엔터프라이즈 컴플라이언스△ SLA 확인 필요✔ Kaiko, Amberdata가 표준
규제상 미국 내 데이터만 허용△ 일부 거래소 커버리지 확인✔ 자체 수집

자주 발생하는 오류와 해결책

오류 1 — TimeZone Mismatch: ClickHouse가 Asia/Shanghai로 잡힘

증분 데이터의 시각이 UTC인데 CH 노드가 다른 TZ면 PARTITION BY toYYYYMM(ts)가 잘못된 파티션에 떨어져 후속 JOIN이 깨집니다. 모든 DateTime64 컬럼은 항상 TZ 명시 습관을 들이세요.

-- 클릭하우스에서 노드 TZ 강제
SETTINGS session_timezone = 'UTC';

-- 서버 측에서도 /etc/clickhouse-server/config.d/timezone.xml
<clickhouse><timezone>UTC</timezone></clickhouse>

-- 데이터가 잘못 들어간 후 발견했다면 파티션 변환
ALTER TABLE orderbook_l2
  MODIFY COLUMN ts DateTime64(3, 'UTC');

오류 2 — "Too many parts" 경고 (MergeTree)

50만 배치도 작아서 1초에 50개 파트가 쌓이면 CH가 merge를 못 따라갑니다. 90015 코드 에러가 아니라 시스템 로그의 경고이지만, 방치하면 SELECT가 느려집니다. 배치를 5만~10만 행으로 키우고, 한 거래소·심볼을 단일 writer에 묶어 동시 삽입을 직렬화하세요.

-- 현재 활성 파트 수 확인
SELECT table, count() AS active_parts, sum(bytes_on_disk) AS bytes
FROM system.parts
WHERE database='default' AND active
GROUP BY table;

-- 병합 가속 (수동)
OPTIMIZE TABLE orderbook_l2 FINAL DEDUPLICATE;
-- 운영 중에는 PARTITION 단위로
OPTIMIZE TABLE orderbook_l2 PARTITION 202401 FINAL;

오류 3 — Tardis 응답이 갑자기 gzip이 아닌 identity로 옴 (HTTP 401 → 200 빈 본문)

Tardis 인증 토큰이 만료되거나 쿼터 초과 시 stream=True 상태에서 본문이 비고 json.loads(raw)에서 JSONDecodeError가 납니다. 응답 본문 길이를 먼저 확인하는 게 안전합니다.

for raw in resp.iter_lines():
    if not raw:
        continue
    # 200 본문이 빈 line으로 시작하면 인증 실패일 수 있음
    try:
        ev = json.loads(raw)
    except json.JSONDecodeError:
        # 헤더와 함께 재호출해서 사유 확인
        if resp.status_code != 200 or not raw.strip():
            resp2 = requests.post(
                "https://api.tardis.dev/v1/account/quota",
                headers={"Authorization": f"Bearer {TARDIS_KEY}"})
            raise RuntimeError(f"Tardis quota issue: {resp2.text[:300]}")
        continue
    if ev.get("type") != "book_change":
        continue
    # ... batch append ...

오류 4 — 메모리 부족 with external group by

ClickHouse가 20GB 이상 group by를 시도하면 디스크로 spill하지만, 별도 설정 없이는 메모리에서 OOM이 납니다.

SET max_bytes_before_external_group_by = 20000000000; -- 20GB
SET max_memory_usage               = 50000000000;   -- 50GB

-- 또는 적재 데이터 양에 맞춰 더 작게:
SET max_bytes_before_external_sort   = 10000000000;

오류 5 — Parquet 대신 CSV로 받아 디스크 폭발

# Parquet 강제
data = requests.get(
    "https://api.tardis.dev/v1/data-feeds/binance-futures"
    "/incremental_book_L2",
    params={"from":"2024-01-01","to":"2024-01-02",
            "format":"parquet",
            "filters": json.dumps([{"type":"symbol","value":"btcusdt"}])},
    headers={"Authorization": f"Bearer {TARDIS_KEY}"})
with open("incr.parquet","wb") as f:
    f.write(data.content)
df = pd.read_parquet("incr.parquet")   # CSV 대비 ~30% 작고 4배 빠름

왜 HolySheep를 선택해야 하나 (저장 + 분석 흐름을 한 곳에)

구매/도입 권고

데이터 사이언스 1인 또는 소규모 트레이딩 리서치 팀이라면 Tardis Standard 플랜($200/월) + ClickHouse Cloud Production($52/월) 조합이 출발선으로 가장 좋습니다. 한 달 내에 스프레드·호가 두께·마이크로프라이스 드리프트 3개 지표를