Python 재현 가능한 백테스트 구현 방법
API key 없이 Python으로 재현 가능한 백테스트를 수행하는 방법을 설명합니다. 라이브러리 버전 고정과 번들 데이터셋 활용을 통해 동일한 결과를 보장하며, 단일 주식 곡선이 감추고 있는 시장 데이터의 이면을 분석합니다.
재현 가능한 백테스트의 조건
Python으로 작성된 재현 가능한 백테스트란, 낯선 사람이 깨끗한 환경의 컴퓨터에서 코드를 다시 실행했을 때 계정이나 API key 없이도 작성자와 정확히 동일한 수치를 얻을 수 있는 것을 의미합니다. 대부분의 튜토리얼은 첫 줄부터 이 기준을 충족하지 못합니다. 실시간 데이터를 내려받는 방식은 독자에게 작성자가 얻었던 것과 미세하게 다른 가격 이력을 제공하기 때문입니다. 본 가이드는 오픈소스 엔진인 quantjourney-bt 0.12.4 버전을 고정하여, 별도의 인증 정보 없이 번들로 제공된 예제를 실행합니다. 이어 실제 시장 데이터를 사용하여, 단 한 번의 깨끗한 실행만으로는 파악할 수 없는 정보가 무엇인지 설명합니다.
백테스트의 재현성을 결정하는 요소는 무엇인가
여기서 재현성은 좁고 검증 가능한 의미를 지닙니다. 다른 사람이 깨끗한 환경의 기기에서 명령어를 한 번 실행했을 때, 작성자와 소수점 단위까지 동일한 수치를 얻어야 한다는 뜻입니다. 이를 방해하는 일반적인 요인은 두 가지입니다.
첫 번째는 코드입니다. 0.x 버전의 라이브러리는 마이너 릴리스 간의 호환성을 보장하지 않습니다. 이름 변경, 기본값 수정, 열 순서 변경 등이 발생하면 스크립트는 오류 없이 실행되면서도 조용히 다른 값을 산출하게 됩니다.
두 번째는 데이터입니다. 첫 줄부터 실시간 다운로드를 수행하는 튜토리얼은 재현이 불가능합니다. 데이터 제공업체는 과거 기록을 수정하고, 주식 분할을 반영하며, 누락된 데이터를 채워 넣습니다. 따라서 한 달 뒤 같은 스크립트를 실행하면 다른 수치가 출력됩니다. 사후에는 코드 변경과 데이터 변경을 구분할 수 없습니다.
이 프로젝트에서 본받을 만한 패턴은 고정된 엔진 버전과 패키지 내부에 포함된 소규모 번들 데이터셋을 결합하는 방식입니다.
설치 고정: pip install quantjourney-bt==0.12.4
quantjourney-bt는 Apache License 2.0 하에 배포되며 Python 3.11 이상을 요구하는 QuantJourney 백테스터입니다. 0.12.4 버전은 2026년 7월 21일에 발표되었으며, 2026년 8월에 문서화된 바와 같이 아래의 모든 명령어는 이 버전을 기준으로 합니다.
독립된 환경에서 작업하십시오. python3 -m venv .venv는 환경을 생성하고, source .venv/bin/activate은 해당 환경으로 진입하며, python -m pip install -U pip은 그 안의 설치 도구를 업데이트합니다. 그런 다음 정확한 버전을 지정하십시오: pip install quantjourney-bt==0.12.4.
본 프로젝트는 고정되지 않은 형태인 pip install quantjourney-bt를 문서화하고 있습니다. ==0.12.4 부분은 사용자의 책임이며, 0.x 버전에서 그 역할을 충분히 수행합니다. 다음 작업자가 확인할 수 있도록 고정된 버전을 기록해 두십시오: pip freeze > requirements.txt은 사용자가 명시하지 않은 의존성을 포함하여 해결된 모든 의존성을 기록합니다.
두 가지 선택적 추가 기능이 존재합니다. pip install "quantjourney-bt[wf]"는 워크포워드(walk-forward) 및 최적화 예제를 위한 Optuna를 추가합니다. pip install "quantjourney-bt[data]"은 벤치마크에 사용되는 yfinance 대체 기능을 추가합니다.
Apache-2.0은 허용적인 라이선스입니다. 사용자는 상업적으로 코드를 사용 및 수정할 수 있으며, 재배포 시 라이선스 및 고지 사항 파일을 유지해야 합니다. 또한 기여자는 특허권을 명시적으로 부여합니다.
API 키 없이 번들 SMA 예제 실행하는 방법
이 저장소는 50개의 실행 가능한 전략 예제와 함께 런처 스크립트를 제공합니다. 전략은 가중치 기반 경로와 주문 기반 경로로 나뉘며, 그중에는 5개의 워크포워드(walk-forward) 워크플로우가 포함되어 있습니다. ./strategy.sh --list는 카탈로그를 출력합니다. ./strategy.sh example_weights_01_sma_daily --check는 단일 전략을 불러오며 데이터를 전혀 건드리지 않으므로, 설치가 정상인지 확인하는 가장 빠른 방법입니다.
데모 실행은 한 줄로 가능합니다: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
--sample-data 플래그가 핵심입니다. 이 프로젝트는 기반 데이터셋을 다음과 같이 설명합니다:
샘플 데이터셋은 의도적으로 작고 재현 가능하게 구성되었습니다. 이는 설치 확인, 보고서 생성, 계정 생성 없이 엔진 흐름을 파악하는 데 유용합니다.
출처: quantjourney-bt README, 버전 0.12.4, 2026년 8월 6일 열람.
실행 결과는 콘솔 메시지가 아닌 디렉토리 형태로 기록됩니다: summary.txt 및 summary.json, metrics.csv, equity_curve.png 옆의 equity_curve.csv, dashboard.html, plots/ 폴더, 그리고 실행 설정이 기록된 run_metadata.json가 생성됩니다. 마지막 파일은 대부분의 사용자가 간과하지만, 1년 뒤에도 결과를 감사(audit)할 수 있게 해주는 핵심 파일입니다.
생성된 지표를 객관적으로 해석하십시오. 번들 데이터셋은 작고 예시적인 수준이므로, summary.txt에 출력된 샤프 지수(Sharpe ratio)와 최대 낙폭(maximum drawdown)은 샘플 파일에 대한 설명일 뿐입니다. 이는 전략에 대한 증거가 아니며, 이를 실제 성과로 간주하는 것이 가장 흔히 범하는 첫 번째 실수입니다.
이 실행을 통해 확인할 수 있는 가치는 분명합니다. 설치가 정상적으로 작동하며, 신호 생성부터 목표 가중치 산정, 포트폴리오 가치 재구성까지 이어지는 전체 엔진 흐름이 어떠한 자격 증명(credential) 없이도 사용자 기기에서 결과물을 생성한다는 점입니다. 실제 과거 데이터를 활용한 백테스트를 위해 프로젝트 자체 데이터 서비스에 접속하는 경로도 존재합니다. 해당 경로는 문서화되어 있으며, 이 가이드는 외부의 그 어떤 것도 필요로 하지 않는 단계에서 마무리됩니다.
단일 표본 자기자본 곡선이 알려주지 않는 것
표본 실행은 하나의 자기자본 곡선(equity curve)을 그려냅니다. 데모 파일이 아닌 실제 시장 데이터를 기준으로 측정했을 때, 이 곡선이 보여주지 못하는 정보는 다음과 같습니다.
아래 패널은 예시 전략에서 사용한 아이디어, 즉 20거래일 이동평균선이 50거래일 이동평균선을 교차하는 전략을 SPY에 적용하여 2017년부터 2025년까지 각 역년(calendar year)별로 결과를 보고합니다. 각 세션의 포지션은 이전 세션의 종가를 기준으로 고정되므로, 해당 규칙은 아직 확보하지 못한 수치를 기반으로 거래하지 않습니다.
모든 수치 뒤에 숨겨진 정확한 SQL
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY year변경되지 않은 하나의 규칙을 9번 별도로 측정했습니다. 다른 항목을 읽기 전에 두 개의 퍼센트 열을 먼저 확인하십시오. 2017에 해당 규칙은 연간 16%의 수익률을 기록했으며, 같은 기간 SPY를 보유했을 때의 수익률은 19.4%였습니다. 2025에 동일한 두 열은 각각 10.4%와 16.4%를 나타냅니다. 두 행에 사용된 코드는 동일합니다. 단지 관측 기간(window)만 이동했을 뿐입니다.
교차(crossover) 열은 기초 데이터가 얼마나 빈약한지를 보여줍니다. 2025거래일 동안 4회의 포지션 변경이 있었다는 것은, 1년 치 자기자본 곡선이 소수의 결정에 의존하고 있음을 의미하며, 이는 결과를 도출하기에는 매우 작은 표본입니다.
동일한 규칙이 다른 종목에서도 같은 방식으로 작동하는가?
날짜 범위를 변경하는 것은 단일 곡선을 분석하는 한 가지 방법이다. 유니버스를 변경하는 것은 또 다른 방법이다. 아래 패널은 매개변수를 고정하고 2021년부터 2025년까지 5년 동안 5개의 유동성 높은 종목에 동일한 규칙을 적용한 결과이다.
모든 수치 뒤에 숨겨진 정확한 SQL
WITH daily AS
(
SELECT
ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
AND window_start >= '2020-07-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY ticker, d
),
averaged AS
(
SELECT
ticker,
d,
px,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (PARTITION BY ticker ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
ticker,
d,
px,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
ticker AS symbol,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
round(avg(long_prior) * 100, 0) AS days_long_pct
FROM positioned
WHERE px_prior > 0
AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESCQQQ는 패널 상단 40.2%에 위치하며, 하단 행인 KO은 3.8%를 기록한다. days_long_pct 열은 각 버전이 전체 기간 중 포지션을 보유한 비중을 나타내며, 상단 행의 경우 67%이다. 하나의 매개변수 세트와 다섯 개의 유니버스, 그리고 사후에 승자를 선택하는 것이 향후 실행할 전략에 대해 아무것도 보장하지 못할 만큼 넓은 스프레드(spread)가 존재한다.
이 내용은 크로스오버(crossover) 거래를 권장하는 것이 아니다. 크로스오버는 백테스트를 위한 측정 도구이며, 우리가 측정하는 대상은 바로 그 백테스트이다.
가중치 백테스트에서 미래 예측 편향(look-ahead bias)이 발생하는 지점
가중치 기반 엔진은 신호를 목표 가중치로 변환하고, 해당 가중치에 따른 체결을 시뮬레이션한 뒤, 결과 포지션으로부터 포트폴리오 가치를 재구성합니다. 오류는 신호와 가중치가 결합하는 지점에 숨어 있습니다. 만약 오늘의 가중치가 오늘의 종가(close)에서 도출되고 그 가중치로 오늘의 수익률을 얻는다면, 해당 백테스트는 주문이 실행될 당시에는 존재하지 않았던 정보를 이용해 거래한 셈이 됩니다. 이것이 바로 미래 예측 편향이며, 시스템은 이를 오류로 인식하지 않습니다. 단지 모든 성과를 실제보다 좋게 보이게 만들 뿐입니다.
아래 패널은 동일한 SPY(SPDR S&P 500 ETF Trust) 이력에 대해 한 가지 규칙의 두 가지 버전을 모두 실행한 결과입니다.
모든 수치 뒤에 숨겨진 정확한 SQL
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
- exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY year2017에서 이전 세션 버전은 16%를 기록한 반면, 동일 세션 버전은 17.1%을 기록하여 1.1 퍼센트포인트의 차이를 보였습니다. 2025에서 두 수치 간의 격차는 1.6 퍼센트포인트로 측정되었습니다. 이 중 한 열만이 해당 세션이 어디에서 마감되었는지 미리 알 수 없었던 기계에 의해 생성될 수 있으며, 두 수치 사이의 차이는 순수한 회계적 수치일 뿐, 어떠한 아이디어, 기술, 거래도 뒷받침되지 않은 결과입니다.
이 엔진은 타이밍 문제에 대한 자체적인 입장을 명시하고 있으며, 이는 단순한 약속보다 더 큰 가치를 지닙니다.
시가(open) 체결의 경우, 범위 민감형 슬리피지(range-sensitive slippage)는 이전에 완료된 바(bar)만을 참조하며, 거래량 수용 능력은 지연된 관측치로부터 예측됩니다. 엔진은 당일의 이후 고가, 저가, 종가 또는 당일 전체 거래량을 사용하지 않습니다.
출처: quantjourney-bt README, 버전 0.12.4, 2026년 8월 6일 열람.
문서화된 가정은 이미 설치된 소스 코드를 통해 검증할 수 있습니다. 문서화되지 않은 가정은 추측에 불과합니다.
워크포워드(walk-forward) 엑스트라가 존재하는 이유
워크포워드 예제인 WF01부터 WF05까지는 [wf] 엑스트라 및 그에 대한 Optuna 의존성과 함께 제공됩니다. 워크포워드는 과거 데이터의 특정 구간에서 매개변수를 최적화(fit)하고, 그 다음 구간에서 성과를 측정하며, 이 과정을 한 구간씩 이동하며 반복합니다. 롤링(rolling) 방식과 확장(expanding) 방식은 최적화 윈도우가 이동할 때 가장 오래된 데이터를 삭제하는지 여부에 따라 차이가 납니다. 또 다른 예제에서는 각 경계 지점에 퍼지(purge)와 엠바고(embargo)를 추가하여 경계선에 가장 가까운 관측치를 제거함으로써, 최적화된 구간의 데이터가 측정 대상 구간으로 유출(leak)되지 않도록 합니다.
이러한 과정이 부실한 아이디어를 성공적인 전략으로 바꿔주지는 않습니다. 이는 단일 수치를 논쟁 가능한 수치들의 분포로 대체하는 것이며, 그것이 이 업그레이드의 핵심입니다. 그 다음 단계는 여전히 실전 투자가 아닙니다. 실전 투자 전 모의 투자는 시뮬레이터가 가정한 가격 근처에서 주문이 체결되는지 여부를 시작으로 백테스트가 구조적으로 파악할 수 없는 요소들을 측정합니다. 또한 트레이딩 봇을 위한 서킷 브레이커는 시스템이 작동하지 않는 날 코드의 대응 방안을 다룹니다. 이 모든 과정의 기저에 깔린 통계적 원리에 대해서는 오픈소스 퀀트 트레이딩 서적에 관한 저희 노트를 통해 한 단계 더 깊이 살펴볼 수 있습니다.
자주 묻는 질문(FAQ)
API 키 없이 전략을 백테스트할 수 있습니까?
가능합니다. quantjourney-bt는 --sample-data 플래그를 통해 번들로 제공되는 샘플 데이터셋을 포함하고 있으며, 예제 전략은 계정이나 자격 증명 없이도 이를 기반으로 실행됩니다. 해당 데이터셋은 규모가 작고 예시용이므로, 이 실행 결과는 전략에 대한 검증보다는 설치 및 파이프라인 확인 용도로 활용하시기 바랍니다.
Python 백테스팅 패키지의 버전을 고정하는 이유는 무엇입니까?
0.x 버전의 패키지는 마이너 릴리스 간 호환성을 보장하지 않으며, 기본값 변경이나 지표 명칭 변경 시 별도의 공지가 없을 수 있습니다. pip install quantjourney-bt==0.12.4을 사용하여 버전을 고정하고 요구사항 파일(requirements file)에 환경을 기록하면, 오늘 생성한 결과를 내년에도 동일한 엔진으로 재현할 수 있습니다.
quantjourney-bt는 어떤 라이선스로 배포됩니까?
Apache License 2.0입니다. 이 라이선스는 상업적 이용과 수정을 허용하며, 재배포 시 라이선스 및 고지 사항 파일을 유지할 것을 요구하고, 기여자로부터의 명시적인 특허 사용 허가를 포함합니다. 0.12.4 버전은 2026년 7월 21일에 배포되었으며 Python 3.11 이상이 필요합니다.
강력한 백테스트 결과가 전략의 성공을 의미합니까?
아닙니다. 백테스트는 특정 기간, 특정 유니버스에서 수행된 하나의 측정치일 뿐입니다. 위 패널들은 동일한 규칙을 적용하더라도 하나의 티커(ticker)에서 연간 수치가 크게 달라질 수 있고, 다섯 개의 종목 간에도 결과가 상이할 수 있음을 보여줍니다. 이러한 차이는 워크포워드 검증(walk-forward validation)과 표본 외 테스트(out-of-sample testing)를 통해 확인해야 하는 간극입니다.
위 패널들의 계산 방식
일일 종가는 뉴욕 시간 기준 오전 9시 30분부터 오후 4시 사이 정규 세션의 마지막 1분간 체결된 거래(print)를 기준으로 하며, 이를 통해 조기 마감일의 세션 길이도 별도의 하드코딩 없이 정확하게 반영합니다. 단기 이동평균은 20세션, 장기 이동평균은 50세션을 기준으로 하는 단순 이동평균(simple)이며, 모든 시리즈의 초기 49세션은 포지션을 보유하지 않는 웜업(warm-up) 기간입니다. 연간 수치는 규칙에 따라 매수 포지션을 유지한 세션의 종가 대비 종가 변동을 복리로 계산하며, 보유(hold) 열은 비교를 위해 해당 연도의 모든 세션을 복리로 계산합니다. 횡단면 분석에 사용된 다섯 종목은 분석 기간 내 분할(split) 없이 연속적인 이력을 가진 종목들로 선정하였으므로 종가 시리즈에 대한 별도의 조정은 필요하지 않습니다. 분석 기간은 과거로 고정되어 있으므로, 이 패널들은 재실행 시 항상 동일한 수치를 반환합니다.
이곳의 모든 패널은 그 아래에 정확한 SQL을 포함하고 있으며, 이를 통해 페이지의 수치들은 고정된 설치 환경만큼이나 재실행이 가능합니다. 백테스트 코드를 작성하기 전에 직접 설정한 기간에 대해 규칙을 측정하려면, Strasmore 터미널에서 평이한 영어로 질문하십시오.