튜토리얼약 7분 읽기

모의투자(페이퍼 트레이딩) 환경 파이썬으로 직접 만들기 — 가상 체결·잔고 추적

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

백테스팅에서 멀쩡하던 코드도, 실시간으로 돌리는 순간에만 터지는 문제가 있다. 신호와 주문 사이의 지연, 재시작 후 상태 복원, 순간적인 데이터 끊김 — 과거 데이터를 한 번에 쭉 흘려보내는 백테스팅에서는 이런 상황이 아예 드러나지 않는다. 그래서 실전 계좌를 연결하기 전에, 실시간처럼 동작하는 모의투자 환경을 따로 만들어야 했다.

실제로 모의투자 단계에서 배운 것들에서 다뤘듯, "코드 리뷰로는 안 보이던 문제들"이 걸러진 곳은 백테스팅이 아니라 이 환경이었다. 이번 글에서는 그 환경을 어떤 구조로 만들었는지를 정리한다.

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

가장 큰 차이는 "시간이 흐르는 방식"이다. 백테스팅은 과거 데이터를 한 번에 다 갖고 계산하지만, 페이퍼 트레이딩은 실시간(혹은 실시간과 동일한 속도로 재생되는) 데이터를 한 틱씩 받으며 그때그때 판단해야 한다.

이 차이 때문에 페이퍼 트레이딩에서만 드러나는 문제들이 있다. 신호가 발생한 시점과 실제로 주문을 넣는 시점 사이의 지연, 시스템이 죽었다가 재시작됐을 때 이전 상태를 제대로 복원하는지, 데이터가 순간적으로 끊겼을 때 어떻게 반응하는지 같은 것들이다.

파이썬으로 가상 체결 엔진 만들기

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

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의 구현이 아닙니다. 이 글은 투자 조언이 아니며 특정 종목이나 매매기법을 추천하지 않습니다. 모의투자 환경에서의 결과가 실전 수익을 보장하지 않으며, 투자 손실에 대한 책임은 투자자 본인에게 있습니다.

← 전체 글 목록