가격 리스트 하나로 승률까지: 하이브리드 백테스트 엔진 만들기 | 토스 자동매매 개발기 #13 (Phase 4)

🌐 Read in English

💡 이번 Phase 4 요약

  • 목적: 전략 코드를 건드리지 않고 가격 리스트만으로 동작하는 백테스트 엔진을 붙여서, 승률·손익비·MDD를 자동 계산한다.
  • 핵심 포인트: 데이터 출처(자체 축적/외부/가상)와 백테스팅 로직을 분리하고, 매도 규칙(익절/손절)은 strategy.py가 아니라 backtest.py가 전담하도록 설계했다.
  • 검증: test_backtest.py 13개 신규 작성, 전체 pytest 71개 전부 통과.

Phase 0부터 Phase 3까지 기반·안정성·전략 모듈화·데이터 영속성을 순서대로 다졌으니, 로드맵상 다음은 백테스팅이었다. 그런데 막상 시작하려니 첫 질문부터 걸렸다. “과거 시세는 대체 어디서 가져오지?”

데이터 출처와 백테스팅 로직을 분리하기로 했다

토스증권 Open API에 과거 시세 조회 엔드포인트가 있는지부터 확인해야 했는데, 이건 다음 세션으로 미뤘다. 지금까지 쌓인 price_snapshots 테이블도 실행 이력이 짧아서 아직 백테스트용 데이터로 쓰기엔 부족했다.

그래서 결론을 냈다. backtest.py는 데이터가 어디서 왔는지 신경 쓰지 않는다. list[float] 형태의 가격 리스트만 받으면, 그게 자체 축적 데이터든 외부 공개 데이터든 테스트용 가상 데이터(005930, 000660 같은 가상 종목코드로 마스킹)든 똑같이 동작한다.

def run_backtest(strategy, prices: list[float], take_profit=0.05, stop_loss=0.03):
    trades = []
    i = 0
    while i < len(prices):
        if strategy.should_buy(prices[:i+1]):
            entry_price = prices[i]
            exit_i, reason = _find_exit(prices, i, entry_price, take_profit, stop_loss)
            trades.append({
                "entry": entry_price,
                "exit": prices[exit_i],
                "reason": reason,  # "take_profit" | "stop_loss" | "period_end"
            })
            i = exit_i + 1
        else:
            i += 1
    return trades

strategy.py는 그대로, 매도 규칙만 새로 만들었다

기존 strategy.pyshould_buy()는 “언제 살지”만 판단한다. 실전 매매에서는 매수 후 해당 종목 감시를 바로 종료하는 구조라 문제가 없었는데, 백테스팅에서 승률·손익비를 계산하려면 반드시 매도 시점이 있어야 했다.

전략 클래스를 건드리는 대신 config.py에 익절 +5%, 손절 -3% 기준을 추가하고, 매도 판단은 backtest.py가 전담하도록 설계했다. 전략은 매수 신호만 책임지고, 청산은 백테스트 엔진이 책임지는 구조다.

승률·손익비·MDD 계산 — compute_metrics()

거래 리스트가 나오면 compute_metrics(trades)가 승률, 손익비(평균이익/평균손실), 최대낙폭(MDD)을 계산한다. 손실 거래가 하나도 없는 경우 손익비를 억지로 무한대나 0으로 두지 않고 None으로 명확히 구분했다. 나중에 리포트에서 “손실 거래 없음”과 “손익비 0″을 헷갈릴 여지를 아예 없앤 것.

테스트 13개 중 1개가 실패했는데, 원인은 코드가 아니었다

test_backtest.py를 13개 작성해서 처음 pytest를 돌렸을 때 test_stop_loss_trigger가 FAILED로 떨어졌다. 로직 버그인 줄 알고 run_backtest 내부를 한참 들여다봤는데, 원인은 테스트 데이터 설계 실수였다. 손절 이후에도 가격이 여전히 매수 기준가 이하로 남아 있어서, 엔진이 정상적으로 재매수를 발생시킨 것뿐이었다. 테스트 쪽 가격 시퀀스를 수정하니 바로 통과했다.

전체 재실행 결과 기존 58개 + 신규 13개, 총 71개 전부 통과했고 안전장치 값도 변경되지 않은 걸 확인했다. 가상의 30일치 시세로 실제 백테스트를 돌려보니 거래 4건(익절 3, 손절 1), 승률 75%, 손익비 1.90, MDD 3.12%가 리포트로 나왔다.

남겨둔 한계 한 가지

내부 상태(가격 이력)를 갖는 MovingAverageStrategy는 문제가 하나 있다. 매도 후 다음 구간으로 건너뛰는 백테스트 루프 구조상, 건너뛴 구간이 이동평균 계산에 반영되지 않는다. 무상태인 ThresholdBuyStrategy는 문제없지만, 이건 코드 주석으로 명확히 남겨뒀다. 다음에 손댈 때 또 헤매지 않도록.

다음 단계

Phase 4의 3개 항목(과거 시세 수집, 백테스트 시뮬레이션, 결과 리포트) 중 시뮬레이션과 리포트는 끝났다. 남은 건 토스증권 Open API에 과거 시세(일봉/분봉) 조회 엔드포인트가 있는지 확인하는 것. 있다면 그게 가장 정확한 데이터 소스니까 우선 채택하고, backtest.py로 전략별 성과를 실제로 비교해볼 계획이다.

👉 이 시리즈의 전체 목차는 위쪽 네비게이션 박스에서 확인할 수 있습니다.

“가격 리스트 하나로 승률까지: 하이브리드 백테스트 엔진 만들기 | 토스 자동매매 개발기 #13 (Phase 4)”에 대한 3개의 생각

댓글 남기기