캔들 API 페이지네이션으로 과거 시세 확보하기 | 토스 자동매매 개발기 #14

🌐 Read in English

📚 토스 자동매매 개발기 시리즈 (전체 14편 중 Phase 4)

← 이전 글: Phase 4 (백테스트 엔진 구현, #13) | 다음 글: Phase 5 (발행 전)

💡 이번 Phase 4 요약

  • 목적: 백테스팅에 필요한 과거 시세 데이터를 토스증권 캔들 API로 실제 수집·저장
  • 핵심 포인트: 응답 스키마가 확정되지 않은 상태에서 방어적으로 파싱 로직을 먼저 설계하고, 이후 실제 토큰으로 검증하는 순서로 리스크를 줄임
  • 검증: 신규 테스트 7개 추가(캔들 파싱 3개, 페이지네이션 연동 2개, DB 시각 보존 2개), 전체 78개 테스트 통과

백테스트 로직 자체는 지난 Phase 4 첫 글(#13)에서 이미 완성해뒀다. 매수-매도 시뮬레이션도 돌아가고, 성과 리포트도 나온다. 근데 정작 “과거 시세를 어디서 가져올 건데?”라는 질문에는 계속 답을 못 하고 있었다. 이번 글은 그 마지막 구멍을 메우는 작업이다.

토스증권 캔들 API, 진짜 있었다

제일 먼저 확인해야 했던 건 “토스증권 Open API에 과거 시세 조회 기능이 있긴 한가”였다. developers.tossinvest.com 공식 문서를 뒤져보니 GET /api/v1/candles 엔드포인트가 실제로 존재했다. 파라미터는 symbol, interval(1m/1d만 지원), count(최대 200), before(페이지네이션용), adjusted(수정주가 여부)로 구성되어 있었다.

눈여겨본 부분은 Rate Limit이 MARKET_DATA_CHART라는 별도 그룹으로 관리된다는 점이었다. 다른 API 그룹과 분리되어 있다는 뜻이라, 캔들 조회를 과도하게 반복해도 주문/잔고 조회 쪽에는 영향을 안 준다는 걸 미리 확인해둔 셈이다.

스키마 확정 전에, 방어적으로 먼저 짠다

공식 문서만으로는 응답 JSON의 정확한 래퍼 구조까지는 100% 확신할 수 없었다. 기존에 붙였던 다른 API들이 대체로 {"result": [...]} 패턴을 따랐던 걸 참고해서, 그 가정에 맞춰 toss_common.pyget_candles()를 우선 구현했다. 재시도 데코레이터도 그대로 적용했다.

동시에 toss_test.py에는 원본 응답을 눈으로 직접 까볼 수 있는 진단 함수 fetch_candles_raw()를 추가했다. 예전에 보유종목 API 스키마를 확인할 때 썼던 것과 같은 접근이다 — 확신 없는 부분은 짐작으로 밀어붙이지 않고, 실제 응답을 눈으로 보고 확정한다.

def get_candles(symbol: str, interval: str = "1d", count: int = 200, before: str | None = None):
    """토스증권 캔들 API 호출 (방어적 파싱)"""
    params = {"symbol": symbol, "interval": interval, "count": count}
    if before:
        params["before"] = before
    resp = _request_with_retry("GET", "/api/v1/candles", params=params)
    data = resp.json()
    result = data.get("result", {})
    return result.get("candles", []), result.get("nextBefore")

200개 제한, 페이지네이션으로 넘어서기

매도 시점이 빠져있던 예전 문제와 비슷한 맥락으로, 캔들 API도 한 번 호출만으론 충분한 과거 데이터를 모을 수 없었다. 최대 200개 제한이 있으니, backtest.pyfetch_and_store_historical_prices()를 만들어 before 파라미터로 반복 호출하면서 원하는 개수만큼 캔들을 모으고 시간순 정렬 후 price_snapshots에 저장하도록 했다.

여기서 신경 쓴 디테일 하나: 과거 데이터를 백필할 때 저장 시각이 “지금”이 되어버리면 안 된다는 점이다. 그 캔들이 실제로 발생한 시각이 보존돼야 나중에 시뮬레이션 정합성이 맞는다. 그래서 db.pyinsert_price_snapshot()captured_at 선택 파라미터를 추가해서 실제 캔들 시각을 그대로 넣을 수 있게 했다.

def insert_price_snapshot(conn, symbol: str, price: float, captured_at: str | None = None):
    ts = captured_at or datetime.utcnow().isoformat()
    conn.execute(
        "INSERT INTO price_snapshots (symbol, price, captured_at) VALUES (?, ?, ?)",
        (symbol, price, ts),
    )
    conn.commit()

모킹으로 먼저, 실제 토큰으로 나중에

실제 API 응답 스키마가 확정 전이었으니, 검증 순서는 항상 그렇듯 모킹이 먼저였다. 2페이지 페이지네이션 → 시간순 정렬 → DB 저장(실제 캔들 시각 보존) → 기존 run_backtest()와의 연계까지 파이프라인 전체를 모킹으로 통과시킨 뒤, 마지막에 실제 계정 토큰으로 toss_test.py를 실행해 원본 응답을 확인했다.

결과는 예상대로였다. 방어적으로 가정했던 {"result": {"candles": [...], "nextBefore": ...}} 구조가 실제 응답과 정확히 일치했다. 추가 수정 없이 그대로 쓸 수 있었고, 캔들이 최신순으로 내려온다는 것도 확인돼서 미리 넣어둔 정렬 로직이 실제로 필요했다는 게 실증됐다. (예시 종목코드는 마스킹: 005930, 000660 등 가상 코드로 대체해 테스트)

Phase 0~4, 여기까지 왔다

신규 테스트 7개(캔들 파싱 3개, 페이지네이션 연동 2개, DB 시각 보존 2개)를 추가해 전체 78개 테스트가 통과했다. 이걸로 Phase 4(백테스팅)의 세 항목 — 과거 시세 수집/저장, 시뮬레이션, 결과 리포트 — 이 전부 끝났고, Phase 0부터 이어온 큰 로드맵이 일단락됐다.

다음은 Phase 5(알림/모니터링 고도화) 차례다. 텔레그램 봇 명령어 인터페이스나 간단한 웹 대시보드 쪽을 검토할 예정이고, 이제 실제 과거 데이터를 모을 수단이 생겼으니 여러 전략(ThresholdBuyStrategy, MovingAverageStrategy)의 성과를 실데이터로 비교해보는 것도 다음 실험으로 흥미로울 것 같다.

💻 GitHub에서 전체 코드 확인하기

자주 묻는 질문

Q1. 토스증권 Open API로 과거 시세(캔들) 데이터를 받아올 수 있나요?

A. 네. GET /api/v1/candles 엔드포인트를 통해 일봉(1d)·분봉(1m) 데이터를 조회할 수 있습니다. 다만 한 번에 최대 200개까지만 제공되므로, 그 이상은 before 파라미터로 페이지네이션해야 합니다.

Q2. 캔들 조회도 다른 API처럼 Rate Limit에 걸리나요?

A. 별도의 MARKET_DATA_CHART 그룹으로 관리되기 때문에, 캔들 조회를 반복해도 주문·잔고 조회용 Rate Limit과는 분리되어 있습니다.

Q3. 응답 스키마를 미리 확신할 수 없을 땐 어떻게 접근했나요?

A. 기존 API들의 공통 패턴(result 래퍼)을 가정해 방어적으로 먼저 구현하고, 원본 응답을 그대로 찍어보는 진단 함수를 따로 만들어 실제 토큰으로 검증한 뒤 확정했습니다.

Q4. 과거 데이터를 백필할 때 저장 시각은 어떻게 처리하나요?

A. insert_price_snapshot()captured_at 선택 파라미터를 추가해, 저장 시점이 아니라 캔들이 실제로 발생한 시각을 그대로 기록하도록 했습니다.

“캔들 API 페이지네이션으로 과거 시세 확보하기 | 토스 자동매매 개발기 #14”에 대한 3개의 생각

댓글 남기기