튜토리얼약 7분 읽기

파이썬으로 시세 데이터 다루기 — 수집·정제·저장 기초

자동매매의 입력단, 시세 데이터

앞선 글들에서 백테스팅과 이동평균 교차 전략을 다루면서 계속 전제로 깔았던 게 있다. "과거 종가 목록(prices)이 이미 깨끗하게 준비돼 있다"는 전제다. 실제로는 그 목록을 만드는 과정 자체가 자동매매 시스템에서 가장 손이 많이 가는 부분 중 하나다. 전략 로직이 아무리 정교해도, 입력으로 들어가는 데이터가 지저분하면 결과는 신뢰할 수 없다. 이번 글은 전략보다 한 층 아래, 시세 데이터를 수집하고 정제해서 저장하는 기초 구조를 다룬다.

OHLCV란 무엇인가

시세 데이터를 다룰 때 가장 기본이 되는 단위가 OHLCV다. 특정 기간(1분, 1일 등) 동안의 시가(Open)·고가(High)·저가(Low)·종가(Close)·거래량(Volume)을 묶은 것이다.

candle = {
    "timestamp": "2026-07-16T09:00:00",
    "open": 100.0,
    "high": 102.5,
    "low": 99.0,
    "close": 101.0,
    "volume": 15234,
}

앞선 글들에서 쓴 "종가 목록"은 이 OHLCV 구조에서 close 값만 시간순으로 뽑아낸 것이다. 실무에서는 종가만 쓰기도 하지만, 고가·저가·거래량까지 함께 봐야 판단할 수 있는 로직(변동성 계산, 거래량 기반 필터 등)도 많아서 원본은 OHLCV 전체를 보관해두는 편이 낫다.

결측치는 왜 생기고 어떻게 처리하는가

데이터 수집 과정에서 특정 구간의 캔들이 통째로 비는 경우가 있다. 수집 서버가 잠시 멈췄거나, 수신 과정에서 유실됐거나, 원본 소스 자체에 공백이 있는 경우다. 이걸 그냥 무시하고 다음 캔들로 넘어가면, 시간 간격이 불규칙한 데이터가 만들어지고 이동평균 같은 계산이 조용히 틀어진다.

def fill_missing_candles(candles: list[dict], expected_interval_sec: int) -> list[dict]:
    """시간 간격을 검사해 비어있는 구간을 명시적으로 표시한다."""
    filled = []
    for i, c in enumerate(candles):
        filled.append(c)
        if i == len(candles) - 1:
            continue
        gap = to_epoch(candles[i + 1]["timestamp"]) - to_epoch(c["timestamp"])
        if gap > expected_interval_sec:
            missing_count = gap // expected_interval_sec - 1
            for _ in range(missing_count):
                filled.append({"timestamp": None, "close": None, "is_missing": True})
    return filled

핵심은 "빈 구간을 조용히 스킵"하는 게 아니라 "빈 구간이 있었다는 사실 자체를 데이터에 남기는 것"이다. 이후 단계(전략, 리스크 관리)에서 is_missing 플래그를 보고 판단을 유보하도록 만들 수 있기 때문이다. 003번 글에서 다룬 "데이터 끊김을 마지막 값으로 때우던 문제"가 바로 이 단계를 건너뛰었을 때 생기는 결과다.

이상치는 다르게 다뤄야 한다

결측치가 "없는 데이터"라면, 이상치는 "있지만 의심스러운 데이터"다. 예를 들어 특정 캔들의 종가가 직전 대비 수십 배 뛰거나, 고가가 저가보다 낮게 찍혀 있는 경우다. 통신 오류나 소스의 순간적 오류로 이런 값이 섞여 들어올 수 있다.

이상치는 결측치처럼 "빈 자리로 표시"하고 끝낼 수 없다. 진짜 급변동(뉴스, 이벤트)과 데이터 오류를 구분하기가 어렵기 때문이다. 그래서 이상치는 삭제하기보다 의심 플래그를 붙여 별도로 표시하고, 실제 판단(진짜 급변동인지 오류인지)은 사람이 로그를 보고 확인하는 절차로 남겨두는 편이 안전하다. 자동으로 "이상하니까 삭제"해버리면 진짜 중요한 신호를 놓칠 수도 있다.

정제된 데이터를 어떻게 저장할 것인가

정제까지 마친 데이터는 재사용 가능한 형태로 저장해둬야 한다. 매번 원본 소스에서 다시 받아오면 느리고, 소스 쪽 문제(속도 제한, 일시 장애)에 매번 영향을 받는다. 개념적으로는 이런 구조면 충분하다.

def save_candles(symbol: str, candles: list[dict], store) -> None:
    """종목별로 시간순 정렬된 캔들을 저장소에 적재한다."""
    candles_sorted = sorted(candles, key=lambda c: c["timestamp"] or "")
    store.write(key=f"candles:{symbol}", records=candles_sorted)

저장소는 파일이든 데이터베이스든 상관없다. 중요한 건 (1) 종목·기간별로 조회하기 쉬운 구조, (2) 정제 이전 원본과 정제 이후 결과를 구분해서 남기는 것이다. 정제 로직에 버그가 있었다는 걸 나중에 발견했을 때, 원본이 남아있어야 다시 정제해서 복구할 수 있다.

이 단계를 대충 넘기면 생기는 문제

시세 데이터 파이프라인은 화려한 부분이 아니라서 건너뛰고 싶은 유혹이 크다. 하지만 이 단계의 결함은 전략 로직의 결함보다 발견하기 어렵다. 전략이 잘못되면 결과가 눈에 띄게 이상하지만, 데이터가 미묘하게 틀리면 전략은 "그럴듯하게 이상한" 결과를 낸다. 백테스팅 결과가 유독 좋거나 나쁘게 나올 때, 전략보다 먼저 데이터 파이프라인을 의심해봐야 하는 이유다.

오늘의 정리

  1. OHLCV는 시세 데이터의 기본 단위이며, 전략 계산에는 종가만 써도 원본은 전체를 보관하는 게 안전하다.
  2. 결측치는 조용히 스킵하지 않고 "비어있었다"는 사실 자체를 명시적으로 남겨야 이후 단계에서 안전하게 판단할 수 있다.
  3. 이상치는 삭제가 아니라 의심 플래그로 표시해, 실제 오류인지 진짜 급변동인지는 사람이 확인하는 절차를 남겨야 한다.
  4. 정제된 데이터는 원본과 구분해서 저장해야, 정제 로직에 문제가 생겼을 때 복구할 수 있다.

다음 글에서는 이렇게 정제한 데이터를 입력으로 삼아, 실전 전 필수 단계인 모의투자(페이퍼 트레이딩) 환경을 직접 만드는 구조를 다뤄본다.


이 글의 코드는 학습 목적의 단순화된 의사코드이며 특정 실거래 시스템이나 특정 데이터 제공사의 구현이 아닙니다. 이 글은 투자 조언이 아니며 특정 종목이나 매매기법을 추천하지 않습니다. 시세 데이터 처리 방식은 사용하는 소스와 목적에 따라 달라질 수 있으며, 투자 손실에 대한 책임은 투자자 본인에게 있습니다.

← 전체 글 목록