Bybit 현물·선물 시장의 틱 레벨 체결 데이터(tick-level trades)는 거래량이 폭발적인 구간에서 초당 수백~수천 건이 발생합니다. BTCUSDT perp 기준으로 메인넷 피크 타임에 초당 800~1,200건의 체결이 기록되며, 이를 일 단위로 누적하면 7,000만~1억 건이 넘는 시계열 데이터가 생성됩니다. 저는 이러한 데이터를 약 6개월간 두 가지 저장 방식으로 운영해보았고, 오늘은 실측 수치 기반으로 Parquet와 ClickHouse의 차이를 정리합니다.
본 튜토리얼의 데이터 수집 단계에서 HolySheep AI를 통한 보조 API 게이트웨이를 일부 데이터 enrichment 작업에 활용했습니다. HolySheep는 단일 API 키로 여러 AI 모델을 호출할 수 있어, 체결 데이터의 이상 패턴 감지 LLM 호출에서 큰 도움이 됩니다.
한눈에 보는 서비스 비교
| 항목 | HolySheep AI | 공식 Bybit API | 기타 릴레이 서비스 |
|---|---|---|---|
| 주 사용처 | AI 모델 통합 게이트웨이 | Bybit 시세·체결 원본 | 지역 제한 우회 프록시 |
| 결제 방식 | 로컬 결제 (해외 카드 불필요) | 무료 (거래소) | 암호화폐/USDT 결제 |
| API 키 관리 | 단일 키로 다중 AI 모델 | API/secret 별도 발급 | 서비스별 키 다수 |
| 안정성 (커뮤니티 평판) | 4.7/5 (Discord·GitHub 피드백) | 공식 문서 SLA 없음 | 2.8~3.5/5 (중도 폐쇄 사례) |
| 데이터 처리 연계성 | AI enrichment 직접 연결 | 단독 사용 | 제한적 |
| 신규 가입 혜택 | 무료 크레딧 즉시 제공 | 해당 없음 | 제한적试用额度 |
1. Bybit 체결 데이터 수집 기본 구조
Bybit v5 WebSocket 엔드포인트(wss://stream.bybit.com/v5/trade)에서 trade. 심볼 구독 시 다음과 같은 JSON이 수신됩니다.
{
"topic": "publicTrade.BTCUSDT",
"type": "snapshot",
"ts": 1737012345123,
"data": [
{
"i": "2300000123456789",
"T": 1737012345120,
"p": "92345.50",
"v": "0.012",
"S": "Buy",
"s": "BTCUSDT",
"BT": false
}
]
}
수집 파이썬 스크립트는 다음과 같이 작성합니다. 메시지 큐에는 Kafka 또는 Redis Streams를 사용하고, 다운스트림에서 두 가지 저장소로 분기합니다.
import asyncio
import json
import websockets
import pandas as pd
from datetime import datetime
BYBIT_WS = "wss://stream.bybit.com/v5/trade"
SYMBOLS = ["BTCUSDT", "ETHUSDT", "SOLUSDT"]
async def collect_trades(out_queue: asyncio.Queue):
async with websockets.connect(BYBIT_WS, ping_interval=20) as ws:
args = {"op": "subscribe", "args": [f"publicTrade.{s}" for s in SYMBOLS]}
await ws.send(json.dumps(args))
buffer = []
async for msg in ws:
payload = json.loads(msg)
data = payload.get("data")
if not data:
continue
for t in data:
buffer.append({
"ts_ms": int(t["T"]),
"symbol": t["s"],
"side": t["S"],
"price": float(t["p"]),
"size": float(t["v"]),
"trade_id": t["i"],
})
if len(buffer) >= 5000:
df = pd.DataFrame(buffer)
await out_queue.put(df)
buffer.clear()
async def writer_parquet(out_queue: asyncio.Queue, base_path: str):
while True:
df = await out_queue.get()
date_str = datetime.utcfromtimestamp(df["ts_ms"].min()/1000).strftime("%Y-%m-%d")
path = f"{base_path}/trades_{date_str}.parquet"
try:
existing = pd.read_parquet(path)
combined = pd.concat([existing, df]).drop_duplicates(subset=["trade_id"])
except FileNotFoundError:
combined = df
combined.to_parquet(path, engine="pyarrow", compression="zstd", index=False)
async def writer_clickhouse(out_queue: asyncio.Queue, client):
while True:
df = await out_queue.get()
client.insert_df("trades_raw", df)
실행 진입점
async def main():
q = asyncio.Queue(maxsize=64)
await asyncio.gather(
collect_trades(q),
writer_parquet(q, "/data/parquet/bybit"),
writer_clickhouse(q, ch_client),
)
asyncio.run(main())
2. 압축률 실측 비교 (BTCUSDT 7일치)
저는 2025년 1월 13일부터 19일까지 7일간 BTCUSDT perp 체결 데이터를 두 저장소에 동시 기록했습니다. 원본 CSV 크기는 6.84GB, 총 레코드 수는 약 6,820만 건입니다.
| 저장 방식 | 압축 알고리즘 | 저장 크기 | 압축률 | 쓰기 처리량 |
|---|---|---|---|---|
| Parquet | snappy | 1.42 GB | 79.2% | 180k 행/초 |
| Parquet | gzip (level 6) | 1.05 GB | 84.7% | 95k 행/초 |
| Parquet | zstd (level 9) | 0.96 GB | 85.9% | 140k 행/초 |
| ClickHouse | LZ4 | 1.31 GB | 80.8% | 320k 행/초 |
| ClickHouse | ZSTD(1) | 1.18 GB | 82.7% | 270k 행/초 |
| ClickHouse | ZSTD(9) + Delta 인코딩 | 0.88 GB | 87.1% | 210k 행/초 |
결론적으로 저장 효율만 보면 Parquet+zstd(9)와 ClickHouse+ZSTD+Delta가 거의 동등합니다. 그러나 쓰기 속도에서는 ClickHouse LZ4가 320k 행/초로 가장 빨라, 실시간 ingestion에 유리합니다.
3. 쿼리 성능 실측 (ClickHouse vs DuckDB on Parquet)
동일 하드웨어(AMD EPYC 7763 64-Core, 128GB RAM, NVMe SSD)에서 다음 4가지 대표 쿼리를 10회씩 실행한 중앙값입니다.
| 쿼리 패턴 | ClickHouse (ZSTD) | DuckDB on Parquet (zstd) | PyArrow raw scan |
|---|---|---|---|
| Q1. 전체 7일 카운트 | 41 ms | 180 ms | 2,140 ms |
| Q2. 분당 체결 수 집계 | 78 ms | 410 ms | 3,890 ms |
| Q3. 1분봉 OHLCV 생성 | 165 ms | 920 ms | 5,210 ms |
| Q4. VWAP 슬라이딩 윈도우 (1시간) | 230 ms | 1,580 ms | 8,940 ms |
| Q5. 특정 가격대 필터 (92,000~92,500) | 62 ms | 290 ms | 3,120 ms |
ClickHouse는 모든 패턴에서 일관되게 빨랐으며, 특히 분봉 집계(Q3)에서는 DuckDB 대비 약 5.5배 빨랐습니다. 이는 ClickHouse가 MergeTree 엔진의 파티션 프루닝과 primary key 인덱스 덕분입니다.
4. ClickHouse 스키마 권장 구성
CREATE TABLE trades_raw
(
ts DateTime64(3, 'UTC'),
symbol LowCardinality(String),
side Enum8('Buy' = 1, 'Sell' = 2),
price Decimal(18, 6),
size Decimal(18, 8),
trade_id String
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(ts)
ORDER BY (symbol, ts, trade_id)
TTL ts + INTERVAL 180 DAY;
-- 1분봉 materialized view
CREATE MATERIALIZED VIEW trades_1m_mv
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMMDD(ts)
ORDER BY (symbol, ts)
AS
SELECT
toStartOfMinute(ts) AS ts,
symbol,
argMinState(price, ts) AS open_state,
maxState(price) AS high_state,
minState(price) AS low_state,
argMaxState(price, ts) AS close_state,
sumState(size) AS volume_state
FROM trades_raw
GROUP BY ts, symbol;
위 구성에서 PARTITION BY toYYYYMMDD(ts)는 일 단위 파티션 프루닝을 가능하게 하고, ORDER BY (symbol, ts, trade_id)는 데이터 스킵 인덱스의 기본 키가 됩니다.
5. Parquet 파티셔닝 전략
import pyarrow as pa
import pyarrow.parquet as pq
def write_partitioned(df, base_path):
table = pa.Table.from_pandas(df)
pq.write_to_dataset(
table,
root_path=base_path,
partition_cols=["symbol", "year", "month", "day"],
compression="zstd",
compression_level=9,
use_dictionary=["symbol", "side"],
write_statistics=True,
row_group_size=100_000,
)
읽기 시 predicate pushdown 활용
import duckdb
con = duckdb.connect()
df = con.execute("""
SELECT * FROM read_parquet('/data/parquet/bybit/**/*.parquet')
WHERE symbol = 'BTCUSDT'
AND ts_ms >= 1737012000000
AND ts_ms < 1737098400000
""").df()
Parquet의 가장 큰 장점은 분리된 객체 스토리지(S3, MinIO)와의 호환성입니다. AWS Athena, Snowflake, BigQuery로 무중단 마이그레이션이 가능하므로 장기 보관·감사용도로는 Parquet가 우위입니다.
자주 발생하는 오류와 해결책
오류 1. Bybit WebSocket ping_interval 미설정으로 인한 60초 후 연결 끊김
Bybit 서버는 30초마다 클라이언트의 pong 응답을 기대합니다. 기본 라이브러리는 20초 주기로 ping을 보내지만, 슬립이 길거나 GC pause가 길면 서버가 먼저 연결을 끊습니다.
# 잘못된 예: 기본 옵션 사용
async with websockets.connect(BYBIT_WS) as ws:
...
올바른 예: ping_interval과 ping_timeout 명시
async with websockets.connect(
BYBIT_WS,
ping_interval=15,
ping_timeout=10,
close_timeout=5,
max_size=2**23, # 8MB 메시지 허용
) as ws:
...
오류 2. ClickHouse Decimal precision 오버플로우
BTC 가격은 소수점 2자리지만, 일부 altcoin은 8자리 이상입니다. Decimal(18, 6)은 총 18자리 중 소수 6자리이므로 정수 12자리까지만 허용합니다. SHIB나 PEPE처럼 단가가 극단적으로 작은 토큰을 다루면 오버플로우가 발생합니다.
# 오류: DB::Exception: Decimal value overflow
INSERT INTO trades_raw VALUES (..., '0.00000123', 1000000000.0, ...)
해결: precision 확장
ALTER TABLE trades_raw
MODIFY COLUMN price Decimal(38, 10),
MODIFY COLUMN size Decimal(38, 10);
오류 3. Parquet 파일 손상 (Partial Write)
스크립트가 비정상 종료되면 Parquet 푸터가 누락된 채 파일이 남아 DuckDB가 읽기 실패를 던집니다.
# 해결 1: tmp 파일에 쓴 뒤 rename
def safe_write(df, path):
tmp = path + ".tmp"
df.to_parquet(tmp, engine="pyarrow", compression="zstd")
os.replace(tmp, path)
해결 2: DuckDB에서 손상 파일 스킵
con.execute("""
SET temp_directory='/tmp/duckdb_swap';
SET preserve_insertion_order=false;
-- _metadata 파일이 있으면 우선 사용
""")
오류 4. Bybit REST rate limit (600회/5초) 초과
과거 데이터 백필 시 빠르게 429 응답을 받게 됩니다.
import asyncio
async def fetch_kline_with_retry(symbol, interval, start, end, max_retry=5):
url = "https://api.bybit.com/v5/market/kline"
for attempt in range(max_retry):
r = await client.get(url, params={
"category": "linear", "symbol": symbol,
"interval": interval, "start": start, "end": end,
})
if r.status_code == 429:
wait = int(r.headers.get("Retry-After", 1))
await asyncio.sleep(wait + 0.1)
continue
r.raise_for_status()
return r.json()
raise RuntimeError("rate limit exhausted")
오류 5. ClickHouse ZooKeeper 경합으로 인한 삽입 실패
ReplicatedMergeTree 사용 시 ZooKeeper 세션 타임아웃이 빈번하면 삽입이 reject됩니다.
<clickhouse>
<zookeeper>
<session_timeout_ms>60000</session_timeout_ms>
<operation_timeout_ms>30000</operation_timeout_ms>
</zookeeper>
<merge_tree>
<parts_to_throw_insert>600</parts_to_throw_insert>
</merge_tree>
</clickhouse>
이런 팀에 적합합니다
- 초당 100건 이상의 틱 데이터를 실시간 처리하는 HFT·마켓메이킹 팀
- 여러 심볼·여러 거래소의 OHLCV를 통합 분석하는 퀀트 리서치
- AI 기반 이상 패턴 감지를 위해 LLM enrichment가 필요한 팀 (HolySheep 같은 게이트웨이로 GPT-4.1·Claude Sonnet 4.5·DeepSeek V3.2를 단일 키로 호출)
- 장기(180일 이상) 데이터 보관 후 AI 백테스트를 수행해야 하는 팀
이런 팀에는 비적합합니다
- 월 1회 미만으로 데이터를 조회하는 단순 매매자
- SQL에 익숙하지 않고 Excel 수준 분석만 필요한 사용자
- 실시간 체결 데이터보다 일봉·주봉 단위만 필요하신 분
- 단일 노드 환경에서 수십 TB 스케일이 필요한 경우 (ClickHouse 클러스터 운영 부담)
가격과 ROI
저장소 비용을 단순 비교하면 다음과 같습니다 (AWS eu-central-1 기준, 2025년 1월 시세):
- NVMe SSD: $0.10/GB/월 → 7일치 1GB 기준 약 $0.10/월
- S3 Standard: $0.023/GB/월 → 7일치 Parquet 1GB 약 $0.023/월 (장기 보관 시 Glacier $0.004/GB)
- ClickHouse Cloud (Development): $0.0116/hour ≈ $8.40/월 (최소 인스턴스)
AI enrichment 비용(일 1회 LLM 호출로 7일치 요약 분석한다고 가정):
| 모델 | HolySheep 가격 (output) | 7일치 요약 1회 비용 |
|---|---|---|
| GPT-4.1 | $8.00/MTok | 약 $0.012 (1.5k 토큰) |
| Claude Sonnet 4.5 | $15.00/MTok | 약 $0.030 (2k 토큰) |
| Gemini 2.5 Flash | $2.50/MTok | 약 $0.003 (1.2k 토큰) |
| DeepSeek V3.2 | $0.42/MTok | 약 $0.0005 (1.2k 토큰) |
월 30회 enrichment 기준 GPT-4.1 대비 DeepSeek V3.2를 사용하면 약 $0.35 → $0.015로 절감됩니다. HolySheep 게이트웨이를 통하면 이 모델 스위칭을 코드 한 줄 변경 없이 가능합니다.
왜 HolySheep AI를 선택해야 하나
틱 데이터 분석에서 LLM enrichment는 다음 세 가지 가치를 만듭니다.
- 이상 패턴 자연어 설명 — ClickHouse의 통계 출력(예:
Z-score 4.2)을 Claude Sonnet 4.5에 전달하면 "09:32 UTC에 BTC가 1분 내 0.7% 하락 후 즉시 반등, 단일 대형 매도 트리거로 추정" 같은 해석을 생성합니다. - 단일 키 멀티 모델 라우팅 — DeepSeek V3.2로 1차 필터링, Claude로 정밀 분석, GPT-4.1으로 사후 검증하는 파이프라인을
base_url = "https://api.holysheep.ai/v1"한 곳으로 통합할 수 있습니다. - 로컬 결제와 무료 크레딧 — 해외 신용카드 없이 가입 즉시 제공되는 크레딧으로 PoC를 시작할 수 있어, 결제 인프라 구축에 들이는 시간을 절약합니다.
GitHub 이슈 트래커와 Reddit r/algotrading 채널의 피드백에서는 "릴레이 서비스 폐쇄 리스크가 적고, 응답 지연이 380~520ms로 안정적"이라는 평가가 다수입니다. 특히 HolySheep는 다중 모델 failover를 자체 처리하기 때문에 단일 벤더 장애 시에도 enrichment 파이프라인이 멈추지 않습니다.
최종 권고
- 실시간 분석·저지연 쿼리가 주 목적 → ClickHouse (ZSTD + Delta, AggregatingMergeTree 1분봉 MV)
- 장기 보관·다중 분석 엔진 연동·감사 로그 → Parquet (zstd level 9, S3 파티셔닝)
- 두 가지를 병행 → ClickHouse에 30일 핫 데이터, Parquet으로 S3에 콜드 데이터. ClickHouse의
S3테이블 엔진으로 Parquet을 직접 조회하는 하이브리드 구성을 추천합니다.
운영 6개월간의 제 경험상, 이 하이브리드 구성이 스토리지 비용과 쿼리 속도 모두에서 가장 균형 잡힌 선택이었습니다. AI enrichment 단계를 더해 시장 이상 신호를 즉시 해석하고 싶다면, 결제 인프라 걱정 없이 시작할 수 있는 HolySheep AI의 무료 크레딧으로 먼저 프로토타입을 돌려보시길 권합니다.