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