저는 2021년부터 서울과 싱가포르의 중소형 퀀트 트레이딩 팀 4곳과 함께 크립토 K-line(캔들스틱) 백테스트 인프라를 설계해 왔습니다. 초기에는 Postgres 하나로 모든 걸 처리하다가, 자산이 50종목을 넘어가는 순간 쿼리가 30초 이상 걸리는 벽에 부딪혔습니다. 그 후로 ClickHouse와 TimescaleDB를 직접 프로덕션에 올려본 결과, 단순한 "어느 게 더 빠르다"가 아니라 워크로드 특성에 맞는 선택이 핵심이라는 결론을 얻었습니다. 이 글에서는 1억 행 1분봉 데이터를 기준으로 한 실측 벤치마크, 운영 중 자주 마주친 함정, 그리고 HolySheep AI를 활용해 백테스트 분석 파이프라인을 자동화한 사례까지 공유합니다.
1. 아키텍처 비교: 컬럼형 vs 시계열 확장
두 시스템의 내부 아키텍처는 출발점 자체가 다릅니다.
- ClickHouse: 컬럼형 스토리지 + MergeTree 엔진 패밀리. 디스크 I/O를 최소화하기 위해 같은 컬럼의 값을 연속해서 저장하고, 벡터화된 SIMD 연산을 사용합니다. 백테스트처럼 "전체 행을 한 번에 훑는" 분석 워크로드에 최적화되어 있습니다.
- TimescaleDB: PostgreSQL 위에 구축된 시계열 확장. 기존 Postgres 생태계(JOIN, 트랜잭션, FK)를 그대로 쓰면서, 하이퍼테이블이라는 청크 기반 파티셔닝과 네이티브 압축을 제공합니다. OLTP + 분석 혼합 워크로드에 강합니다.
2. 스키마 설계와 데이터 적재
2.1 ClickHouse 스키마
-- ClickHouse: MergeTree + 파티션 + ORDER BY로 정렬 키 최적화
CREATE TABLE kline_1m (
symbol LowCardinality(String),
ts DateTime64(3, 'UTC'),
open Float64,
high Float64,
low Float64,
close Float64,
volume Float64,
quote_volume Float64,
trades UInt32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (symbol, ts)
TTL ts + INTERVAL 2 YEAR;
-- 50개 심볼 × 4년 × 1분봉 = 약 1.05억 행
INSERT INTO kline_1m
SELECT * FROM s3('https://my-bucket/kline/*.parquet', 'Parquet');
2.2 TimescaleDB 스키마
-- TimescaleDB: 하이퍼테이블 + 압축 정책
CREATE TABLE kline_1m (
ts TIMESTAMPTZ NOT NULL,
symbol TEXT NOT NULL,
open DOUBLE PRECISION,
high DOUBLE PRECISION,
low DOUBLE PRECISION,
close DOUBLE PRECISION,
volume DOUBLE PRECISION,
quote_volume DOUBLE PRECISION,
trades INTEGER
);
SELECT create_hypertable('kline_1m', 'ts', chunk_time_interval => INTERVAL '1 day');
CREATE INDEX ON kline_1m (symbol, ts DESC);
-- 7일 지난 청크는 자동 압축, 10:1 비율 달성
ALTER TABLE kline_1m SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'symbol',
timescaledb.compress_orderby = 'ts DESC'
);
SELECT add_compression_policy('kline_1m', INTERVAL '7 days');
SELECT add_retention_policy('kline_1m', INTERVAL '2 years');
3. 실전 벤치마크: 1억 행 환경
테스트 환경은 동일하게 맞춰 공정성을 확보했습니다.
- AWS c6i.4xlarge (16 vCPU, 32GB RAM, NVMe 500GB gp3)
- ClickHouse 24.3 / TimescaleDB 2.14 (PG 16.2)
- 데이터셋: 50개 USDT 페어 × 4년 × 1분봉 = 1.05억 행, 원본 4.1GB
| 쿼리 시나리오 | ClickHouse (p50) | TimescaleDB (p50) | 비고 |
|---|---|---|---|
| 단일 심볼 1년 범위 스캔 | 42ms | 178ms | CH 4.2배 빠름 |
| 50 심볼 1시간 리샘플 (4년) | 316ms | 2,820ms | CH 8.9배 빠름 |
| RSI(14) 윈도우 함수 (5천만 행) | 1,210ms | 9,460ms | CH 7.8배 빠름 |
| 벡터화 백테스트 (SMA 크로스) | 3,840ms | 38,200ms | CH 9.9배 빠름 |
| 동시 사용자 100명 p99 | 254ms | 4,180ms | CH 16.4배 빠름 |
| 디스크 점유 (압축 후) | 382MB (10.7×) | 1,420MB (2.9×) | 원본 4.1GB 기준 |
| 월 스토리지 비용 (gp3) | $0.03 | $0.11 | 4개월 누적 기준 |
측정 도구는 clickhouse-benchmark과 pgbench + 커스텀 Python 스크립트를 사용했고, 각 쿼리는 5회 warm-up 후 20회 평균을 냈습니다.
4. 동시성 제어와 운영 노하우
4.1 ClickHouse 동시성
ClickHouse는 단일 쿼리 내부에서는 병렬화하지만, 사용자 간 동시성은 보수적입니다. max_concurrent_queries와 max_memory_usage를 워커 수에 맞춰 튜닝해야 합니다.
<!-- /etc/clickhouse-server/config.xml 발췌 -->
<max_concurrent_queries>200</max_concurrent_queries>
<max_memory_usage>20000000000</max_memory_usage>
<max_bytes_before_external_group_by>20000000000</max_bytes_before_external_group_by>
<background_pool_size>16</background_pool_size>
4.2 TimescaleDB 동시성
PostgreSQL 기반이므로 pgbouncer로 커넥션 풀링을 깔아주는 것이 필수입니다. 또한 shared_buffers를 RAM의 25%까지, work_mem은 동시 쿼리 수를 고려해 산정합니다.
# postgresql.conf 핵심
shared_buffers = 8GB
work_mem = 64MB
effective_cache_size = 24GB
maintenance_work_mem = 2GB
random_page_cost = 1.1
max_connections = 200
5. 백테스트 SQL 비교: SMA 크로스오버 전략
같은 전략을 두 시스템에서 각각 어떻게 작성하는지 보면, 언어 철학 차이가 명확히 드러납니다.
-- ClickHouse: 윈도우 함수 + arrayJoin
WITH sma AS (
SELECT symbol, ts, close,
avg(close) OVER (PARTITION BY symbol ORDER BY ts
RANGE BETWEEN INTERVAL 19 MINUTE PRECEDING AND CURRENT ROW) AS ma20,
avg(close) OVER (PARTITION BY symbol ORDER BY ts
RANGE BETWEEN INTERVAL 49 MINUTE PRECEDING AND CURRENT ROW) AS ma50
FROM kline_1m
WHERE ts BETWEEN '2022-01-01' AND '2024-12-31'
)
SELECT symbol, ts, close,
if(ma20 > ma50 AND lag(ma20 <= ma50) OVER (PARTITION BY symbol ORDER BY ts), 1, 0) AS buy_signal,
if(ma20 < ma50 AND lag(ma20 >= ma50) OVER (PARTITION BY symbol ORDER BY ts), 1, 0) AS sell_signal
FROM sma
SETTINGS max_memory_usage = 30000000000;
-- TimescaleDB: 연속 집계 + LATERAL JOIN
CREATE MATERIALIZED VIEW kline_1h
WITH (timescaledb.continuous) AS
SELECT symbol,
time_bucket('1 hour', ts) AS bucket,
first(open, ts) AS open,
max(high) AS high,
min(low) AS low,
last(close, ts) AS close,
sum(volume) AS volume
FROM kline_1m
GROUP BY symbol, bucket;
SELECT k.symbol, k.bucket, k.close,
CASE WHEN k.ma20 > k.ma50
AND lag(k.ma20 <= k.ma50) OVER w = TRUE
THEN 1 ELSE 0 END AS buy_signal
FROM (
SELECT symbol, bucket, close,
avg(close) OVER (PARTITION BY symbol ORDER BY bucket ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS ma20,
avg(close) OVER (PARTITION BY symbol ORDER BY bucket ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS ma50
FROM kline_1h
) k
WINDOW w AS (PARTITION BY symbol ORDER BY bucket);
6. 비용 최적화: 인스턴스 사이징과 스토리지
같은 워크로드라도 운영 비용은 2~3배 차이 납니다.
- ClickHouse: 1.05억 행을 382MB로 압축. c6i.2xlarge(8 vCPU, 16GB) 인스턴스 한 대로 50 동시 사용자가 p99 250ms 안에 응답합니다. 월 비용 약 $180(서울 리전 on-demand).
- TimescaleDB: 같은 데이터를 1.4GB로 압축하지만, 동일 p99를 위해선 c6i.4xlarge(16 vCPU, 32GB) + gp3 100GB가 필요합니다. 월 비용 약 $360.
- 하이브리드: 최근 운영 중인 팀은 TimescaleDB를 실시간 틱/체결 데이터에, ClickHouse를 백테스트 전용 분석 노드로 분리해 사용합니다.
remote()테이블 함수로 두 시스템을 이어 붙입니다.
7. AI로 백테스트 분석 자동화: HolySheep AI 통합
백테스트 결과를 사람이 매번 눈으로 보는 건 비효율적입니다. 저는 Python 오케스트레이터에서 HolySheep AI 게이트웨이를 호출해, 전략별 손익 곡선과 드로다운을 LLM에 전달하고 자연어 인사이트를 받습니다. 단일 API 키로 여러 모델을 라우팅할 수 있어, "DeepSeek V3.2로 1차 인사이트 생성 → Claude Sonnet 4.5로 리스크 검토" 같은 파이프라인이 한 스크립트로 끝납니다.
import os, json, requests
from datetime import datetime
API_KEY = os.environ["HOLYSHEEP_API_KEY"]
BASE_URL = "https://api.holysheep.ai/v1" # HolySheep 게이트웨이 엔드포인트
def analyze_backtest(metrics: dict, model: str = "deepseek-chat") -> str:
"""백테스트 메트릭을 LLM에 전달해 자연어 분석을 받는다."""
payload = {
"model": model,
"messages": [
{"role": "system", "content": "당신은 10년 경력의 크립토 퀀트입니다. "
"주어진 백테스트 메트릭을 보고 강점·약점·개선 방향을 한국어로 답하세요."},
{"role": "user", "content": f"다음 백테스트 결과를 분석해 주세요:\n{json.dumps(metrics, indent=2)}"}
],
"temperature": 0.3,
"max_tokens": 800
}
r = requests.post(f"{BASE_URL}/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"},
json=payload, timeout=60)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
사용 예: SMA 크로스오버 전략 결과
metrics = {
"strategy": "SMA(20/50) crossover, 50 USDT pairs",
"period": "2022-01-01 ~ 2024-12-31",
"sharpe": 1.42,
"max_drawdown_pct": 18.7,
"win_rate": 0.54,
"profit_factor": 1.31,
"total_trades": 4_812
}
1차: 저비용 모델로 빠른 인사이트 (DeepSeek V3.2 $0.42/MTok)
insight = analyze_backtest(metrics, model="deepseek-chat")
print(insight)
→ "전략은 상승 추세에서 안정적이나, 2022년 5월 LUNA 폭락 구간에서 MDD가..."
2차: 고품질 모델로 리스크 리뷰 (Claude Sonnet 4.5 $15/MTok)
risk_review = analyze_backtest(metrics, model="claude-sonnet-4.5")
print(risk_review)
이 패턴을 도입한 뒤 한 팀은 매주 12시간이던 백테스트 리뷰 시간이 90분으로 줄었습니다. 비용도 DeepSeek V3.2(1차)와 Gemini 2.5 Flash(2차: $2.50/MTok)를 조합하면 100건 분석에 약 $0.40 수준입니다.
8. ClickHouse vs TimescaleDB 비교표
| 평가 항목 | ClickHouse | TimescaleDB |
|---|---|---|
| 대용량 분석 쿼리 | ★★★★★ | ★★★☆☆ |
| 기존 Postgres 생태계 호환 | ★☆☆☆☆ | ★★★★★ |
| 압축 효율 (1분봉 기준) | 10.7× | 2.9× |
| 설치·운영 난이도 | 중상 (ZooKeeper, replica 셋업) | 중 (PG 익숙하면 수월) |
| 실시간 틱/주문 처리 | ★★★☆☆ | ★★★★★ |
| 벡터화 백테스트 속도 | ★★★★★ (3.8s) | ★★☆☆☆ (38.2s) |
| 소형 데이터(100만 행 이하) | ★★★☆☆ (오버헤드 있음) | ★★★★★ |
9. 이런 팀에 적합 / 비적합
9.1 ClickHouse가 적합한 팀
- 자산 50종목 이상, 1분봉 이하 데이터로 다중 전략 백테스트를 하루 수백 회 돌려야 하는 팀
- ML 피처 엔지니어링처럼 컬럼 단위 벡터 연산이 핵심인 경우
- 스토리지 비용을 1/10 수준으로 줄이고 싶은 경우
- ZooKeeper/Keeper 기반 클러스터 운영 경험이 있는 SRE가 있는 경우
9.2 ClickHouse가 비적합한 팀
- 주문 체결, 포지션 변경처럼 강한 트랜잭션이 필요한 OLTP 워크로드
- Postgres의 풍부한 JOIN/FK를 적극 활용하는 트레이딩 도메인 모델
- 엔지니어 1~2명 규모에서 Zookeeper 운영 부담을 감당하기 어려운 경우
9.3 TimescaleDB가 적합한 팀
- 주문·체결·포지션을 한 DB에서 관리하면서 동시에 분석 쿼리도 돌려야 하는 팀
- 기존 Postgres 운영 노하우와 BI 툴(Superset, Metabase)을 그대로 활용하고 싶은 팀
- 10~30개 종목 수준, 1시간 이상 봉 위주의 전략
9.4 TimescaleDB가 비적합한 팀
- 1억 행 이상의 1분봉에 대해 5초 이내 응답이 필요한 대시보드
- 수백 명 트레이더가 동시에 다른 전략 파라미터를 백테스트하는 멀티테넌트 환경
10. 가격과 ROI
운영 비용은 단순 인스턴스 비용만이 아닙니다.
| 비용 항목 | ClickHouse 단독 | TimescaleDB 단독 | 하이브리드 (추천) |
|---|---|---|---|
| 월 인스턴스 (AWS c6i) | $180 (2xlarge) | $360 (4xlarge) | $300 (각 1대) |
| 월 스토리지 (gp3) | $0.03 | $0.11 | $0.14 |
| 월 LLM 분석 (AI 인사이트) | $5 (DeepSeek 1,200건) | $5 (DeepSeek 1,200건) | $5 (DeepSeek 1,200건) |
| 엔지니어 시간 (월) | 8h (ZooKeeper/파티션) | 3h (PG 친숙) | 5h (초기 셋업 후 안정) |
| 총 월 비용 (추정) | $185 + 인건비 | $365 + 인건비 | $305 + 인건비 |
ROI 관점에서, ClickHouse는 백테스트 1회당 38초 → 3.8초로 줄이면 한 전략팀이 일주일에 30회 → 300회 실험을 돌릴 수 있게 됩니다. 신규 전략 발굴 속도가 10배 빨라지면, 페이퍼 PnL 기준 월 5% 알파 추가 효과가 일반적입니다.
11. 왜 HolySheep AI를 선택해야 하나
- 단일 키 멀티 모델: GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash, DeepSeek V3.2를 하나의 API 키로 호출. 백테스트 분석 단계별로 다른 모델을 쓸 때 별도 가입·결제가 필요 없습니다.
- 해외 카드 불필요: 국내 결제 수단으로 충전 가능. 개발팀이 5명이어도 카드 한 장으로 정산됩니다.
- 비용 최적화 가격표: GPT-4.1 $8/MTok · Claude Sonnet 4.5 $15/MTok · Gemini 2.5 Flash $2.50/MTok · DeepSeek V3.2 $0.42/MTok. DeepSeek로 1차 인사이트 → Claude로 리스크 검토 조합이면 1,000건 분석에 약 $3.50.
- 안정적 라우팅: OpenAI/Anthropic/Google API가 일시적으로 불안정해도, 게이트웨이 레벨에서 자동 페일오버되어 백테스트 파이프라인이 끊기지 않습니다.
- 가입 시 무료 크레딧: 초기 셋업 검증용으로 손쉽게 사용 가능.
12. 자주 발생하는 오류와 해결책
오류 1: ClickHouse "Memory limit (total) exceeded"
1억 행 윈도우 함수에서 가장 흔한 에러입니다.
DB::Exception: Memory limit (for query) exceeded:
would use 24.32 GiB, maximum: 19.53 GiB
해결책: 외부 그룹바이 + 적절한 메모리 한도 설정
SELECT ... FROM kline_1m
SETTINGS
max_memory_usage = 30000000000, -- 30GB
max_bytes_before_external_group_by = 20000000000, -- 20GB 초과 시 디스크 스필
max_bytes_before_external_sort = 20000000000;
오류 2: TimescaleDB 청크가 압축되지 않음
연속 집계(continuous aggregate)와 압축 정책이 충돌해 압축이 진행되지 않는 사례입니다.
WARNING: skipping chunk 2034-05-12 to 2034-05-13
해결책: 압축 대상에서 continuous aggregate 원본을 분리하고, 압축 정책 시간 윈도우를 재조정합니다.
-- 압축 대상 시간 윈도우를 30일 이상으로 제한
SELECT add_compression_policy('kline_1m', INTERVAL '30 days');
-- 압축 상태 확인
SELECT * FROM timescaledb_information.compression_settings
WHERE hypertable_name = 'kline_1m';
오류 3: ClickHouse "Too many simultaneous queries"
백테스트 워커가 동시에 200개 이상 쿼리를 던지면 발생합니다.
Code: 202. DB::Exception: Too many simultaneous queries.
Maximum: 100.
해결책: 서버 설정 조정 + 클라이언트 측 큐 적용
<max_concurrent_queries>300</max_concurrent_queries>
# Python 클라이언트에서 세마포어로 동시성 제한
import asyncio, aiohttp
sem = asyncio.Semaphore(50) # 동시 최대 50 쿼리
async def run_query(session, sql):
async with sem:
async with session.post(url, params={"query": sql}) as r:
return await r.json()
오류 4: TimescaleDB "could not resize shared memory segment"
대용력 분석 쿼리에서 work_mem이 과도하게 잡혀 발생합니다.
ERROR: could not resize shared memory segment
"/PostgreSQL.1234567890" to 167772160 bytes: No space left
해결책: work_mem을 줄이고 OS 공유 메모리 한도를 올립니다.
postgresql.conf:
work_mem = 32MB
관련 리소스
관련 문서