튜토리얼약 7분 읽기

모의투자(페이퍼 트레이딩) 환경 직접 만들기 — 개념과 구조

왜 백테스팅만으로는 부족한가

앞선 글에서 다룬 백테스팅은 과거 데이터를 한 번에 쭉 흘려보내 결과를 계산하는 방식이었다. 빠르고 반복 검증에 유리하지만, 한계도 뚜렷하다. 실시간으로 데이터가 들어오는 상황, 주문이 실제로 체결되기까지 걸리는 지연, 시스템이 재시작되거나 오류가 나는 상황은 백테스팅에서 제대로 드러나지 않는다. 003번 글에서 다룬 것처럼 "코드 리뷰로는 안 보이던 문제들"이 실제로 걸러진 곳은 백테스팅이 아니라 실시간처럼 동작하는 모의투자 환경이었다. 이번 글에서는 그 환경을 어떤 구조로 만들었는지를 정리한다.

페이퍼 트레이딩과 백테스팅의 차이

가장 큰 차이는 "시간이 흐르는 방식"이다. 백테스팅은 과거 데이터를 한 번에 다 갖고 계산하지만, 페이퍼 트레이딩은 실시간(혹은 실시간과 동일한 속도로 재생되는) 데이터를 한 틱씩 받으며 그때그때 판단해야 한다. 이 차이 때문에 페이퍼 트레이딩에서만 드러나는 문제들이 있다. 신호가 발생한 시점과 실제로 주문을 넣는 시점 사이의 지연, 시스템이 죽었다가 재시작됐을 때 이전 상태를 제대로 복원하는지, 데이터가 순간적으로 끊겼을 때 어떻게 반응하는지 같은 것들이다.

가상 체결 로직 설계

실제 계좌 없이 "주문이 들어왔다"는 걸 흉내내려면, 가상 체결 엔진이 필요하다. 개념적으로는 이런 형태다.

python 코드 보기
class PaperBroker:
    def __init__(self, initial_cash: float):
        self.cash = initial_cash
        self.positions = {}  # symbol -> quantity
        self.order_log = []

    def submit_order(self, symbol: str, side: str, quantity: int, price: float) -> dict:
        """실제 거래소 대신 가상으로 즉시 체결하는 단순화된 로직."""
        cost = price * quantity
        if side == "BUY":
            if cost > self.cash:
                return {"status": "REJECTED", "reason": "INSUFFICIENT_CASH"}
            self.cash -= cost
            self.positions[symbol] = self.positions.get(symbol, 0) + quantity
        elif side == "SELL":
            held = self.positions.get(symbol, 0)
            if quantity > held:
                return {"status": "REJECTED", "reason": "INSUFFICIENT_POSITION"}
            self.cash += cost
            self.positions[symbol] = held - quantity

        order = {"symbol": symbol, "side": side, "quantity": quantity, "price": price, "status": "FILLED"}
        self.order_log.append(order)
        return order

여기서는 "가격 그대로 즉시 체결"이라는 가장 단순한 가정을 썼다. 실제 시장에서는 주문이 원하는 가격에 정확히 체결되지 않는 경우(슬리피지)가 흔하기 때문에, 더 정교한 환경에서는 이 가정 자체를 조정 가능한 파라미터로 빼두는 게 좋다.

가상 잔고·포지션 추적

가상 계좌라도 "지금 얼마 남았고, 무엇을 얼마나 들고 있는지"가 정확히 맞아야 의미가 있다. 특히 재시작 이후에도 상태가 꼬이지 않아야 한다는 게 003번 글에서 짚었던 체크리스트 항목 중 하나였다. 이걸 위해서는 매 체결마다 상태를 즉시 영속화해두는 구조가 필요하다.

python 코드 보기
def snapshot_state(broker: PaperBroker, store) -> None:
    """매 체결 후 현재 잔고·포지션을 저장소에 기록해, 재시작 시 복원 가능하게 한다."""
    store.write(key="paper_account_state", record={
        "cash": broker.cash,
        "positions": dict(broker.positions),
    })

이 스냅샷이 없으면, 시스템이 중간에 재시작될 때 "지금까지 뭘 샀고 얼마가 남았는지"를 알 수 없게 된다. 실전 계좌라면 거래소 쪽에 실제 잔고가 남아있어 조회로 복구할 수 있지만, 가상 계좌는 이 상태를 스스로 책임지고 들고 있어야 한다는 점이 실전과 다른 부분이다.

실전과 다른 점 — 지연·슬리피지를 흉내내기

가상 환경이 실전과 완전히 같을 수는 없다. 대표적으로 두 가지 차이를 의도적으로 흉내내야 실전 감각에 가까워진다.

오늘의 정리

  1. 페이퍼 트레이딩은 백테스팅과 달리 실시간 흐름·재시작·데이터 끊김 같은 상황을 그대로 겪을 수 있어, 백테스팅에서 드러나지 않는 문제를 잡아낸다.
  2. 가상 체결 엔진은 잔고·포지션 검증을 포함한 최소한의 로직만으로 시작할 수 있지만, 슬리피지 같은 실전과의 차이는 의도적으로 반영해야 한다.
  3. 가상 계좌 상태는 매 체결마다 영속화해둬야, 시스템 재시작 이후에도 상태가 꼬이지 않는다.

다음 글에서는 이렇게 만든 시스템을 실제로 24시간 무인으로 돌리면서 겪은 모니터링과 장애 대응 이야기를 다룬다.


이 글의 코드는 학습 목적의 단순화된 예제이며 특정 실거래 시스템이나 특정 증권사 API의 구현이 아닙니다. 이 글은 투자 조언이 아니며 특정 종목이나 매매기법을 추천하지 않습니다. 모의투자 환경에서의 결과가 실전 수익을 보장하지 않으며, 투자 손실에 대한 책임은 투자자 본인에게 있습니다.

← 전체 글 목록