모의투자(페이퍼 트레이딩) 환경 파이썬으로 직접 만들기 — 가상 체결·잔고 추적
왜 백테스팅만으로는 부족한가
백테스팅 결과가 좋았다고 실전에 바로 연결해도 될까? 답은 "아니다"에 가깝다. 백테스팅에서 멀쩡하던 코드도, 실시간으로 돌리는 순간에만 터지는 문제가 따로 있기 때문이다. 신호와 주문 사이의 지연, 재시작 후 상태 복원, 순간적인 데이터 끊김 — 과거 데이터를 한 번에 쭉 흘려보내는 백테스팅에서는 이런 상황이 아예 드러나지 않는다. 그래서 실전 계좌를 연결하기 전에, 실시간처럼 동작하는 모의투자 환경을 따로 만들어야 했다.
실제로 모의투자 단계에서 배운 것들에서 다뤘듯, "코드 리뷰로는 안 보이던 문제들"이 걸러진 곳은 백테스팅이 아니라 이 환경이었다. 이번 글에서는 그 환경을 어떤 구조로 만들었는지를 정리한다.
페이퍼 트레이딩과 백테스팅의 차이
가장 큰 차이는 "시간이 흐르는 방식"이다. 백테스팅은 과거 데이터를 한 번에 다 갖고 계산하지만, 페이퍼 트레이딩은 실시간(혹은 실시간과 동일한 속도로 재생되는) 데이터를 한 틱씩 받으며 그때그때 판단해야 한다.
이 차이 때문에 페이퍼 트레이딩에서만 드러나는 문제들이 있다. 신호가 발생한 시점과 실제로 주문을 넣는 시점 사이의 지연, 시스템이 죽었다가 재시작됐을 때 이전 상태를 제대로 복원하는지, 데이터가 순간적으로 끊겼을 때 어떻게 반응하는지 같은 것들이다.
파이썬으로 가상 체결 엔진 만들기
실제 계좌 없이 "주문이 들어왔다"는 걸 흉내 내려면, 가상 체결 엔진이 필요하다. 개념적으로는 이런 형태다.
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여기서는 "가격 그대로 즉시 체결"이라는 가장 단순한 가정을 썼다. 실제 시장에서는 주문이 원하는 가격에 정확히 체결되지 않는 경우(슬리피지)가 흔하기 때문에, 더 정교한 환경에서는 이 가정 자체를 조정 가능한 파라미터로 빼두는 게 좋다. 이 PaperBroker는 이동평균 교차 전략처럼 단순한 전략을 실시간 흐름에서 검증할 때도 그대로 재사용할 수 있다.
가상 잔고·포지션 추적과 재시작 복원
가상 계좌라도 "지금 얼마 남았고, 무엇을 얼마나 들고 있는지"가 정확히 맞아야 의미가 있다. 특히 재시작 이후에도 상태가 꼬이지 않아야 한다는 게 003번 글에서 짚었던 체크리스트 항목 중 하나였다. 이걸 위해서는 매 체결마다 상태를 즉시 영속화해두는 구조가 필요하다.
python 코드 보기
def snapshot_state(broker: PaperBroker, store) -> None:
"""매 체결 후 현재 잔고·포지션을 저장소에 기록해, 재시작 시 복원 가능하게 한다."""
store.write(key="paper_account_state", record={
"cash": broker.cash,
"positions": dict(broker.positions),
})이 스냅샷이 없으면, 시스템이 중간에 재시작될 때 "지금까지 뭘 샀고 얼마가 남았는지"를 알 수 없게 된다. 실전 계좌라면 거래소 쪽에 실제 잔고가 남아있어 조회로 복구할 수 있지만, 가상 계좌는 이 상태를 스스로 책임지고 들고 있어야 한다는 점이 실전과 다른 부분이다. 원본과 정제본을 나눠 저장한 시세 데이터 파이프라인과 마찬가지로, 여기서도 "무엇을 언제 기록해두는가"가 나중에 문제를 복구할 수 있는지를 가른다.
실전과 다른 점 — 주문 지연·슬리피지 흉내 내기
가상 환경이 실전과 완전히 같을 수는 없다. 대표적으로 두 가지 차이를 의도적으로 흉내 내야 실전 감각에 가까워진다.
- 지연(latency): 신호 발생부터 주문 제출까지, 실제로는 통신·처리 지연이 존재한다. 가상 체결을 "즉시"로만 처리하면 이 지연이 문제가 되는 상황(신호와 체결 사이 가격이 크게 움직인 경우)을 못 잡아낸다.
- 슬리피지: 원하는 가격과 실제 체결 가격 사이의 차이. 특히 급변동 구간에서는 이 차이가 커질 수 있다. 가상 체결에 의도적으로 약간의 가격 오차를 반영해보면, 전략이 슬리피지에 얼마나 민감한지 미리 가늠해볼 수 있다.
오늘의 정리
- 페이퍼 트레이딩은 백테스팅과 달리 실시간 흐름·재시작·데이터 끊김 같은 상황을 그대로 겪을 수 있어, 백테스팅에서 드러나지 않는 문제를 잡아낸다.
- 가상 체결 엔진은 잔고·포지션 검증을 포함한 최소한의 로직만으로 시작할 수 있지만, 슬리피지 같은 실전과의 차이는 의도적으로 반영해야 한다.
- 가상 계좌 상태는 매 체결마다 영속화해둬야, 시스템 재시작 이후에도 상태가 꼬이지 않는다.
다음 글에서는 이렇게 만든 시스템을 실제로 24시간 무인으로 돌리면서 겪은 모니터링과 장애 대응 이야기를 다룬다.
여기 실린 코드는 학습용으로 단순화한 예제이며, 특정 실거래 시스템이나 특정 증권사 API의 구현이 아닙니다. 교육 목적의 글로 투자 조언이 아니고, 특정 종목이나 매매기법을 권하지 않습니다. 모의투자 환경의 결과가 실전 수익으로 이어진다는 보장은 없으며, 투자 손실의 책임은 투자자 본인에게 있습니다.