---
title: "[Lap 3] [모듈 2: Engine] 잔인할 정도의 체결 현실감: T+1 MOO 시가 주문 엔진"
canonical_url: "https://wealth-podium.com/2026/10/29/track-02-lap-03-execution-engine-tdd/"
published_at: "2026-10-29T09:00:00+09:00"
modified_at: "2026-11-07T15:44:36+09:00"
author: "Dellwon (Software Developer & System Architect)"
summary: "당일 종가 체결이라는 비현실적 착시를 깨고, T+1일 장 시작 시가(MOO) 체결 강제와 슬리피지·브로커 수수료를 1센트까지 시뮬레이션하는 ExecutionEngine TDD 개발기를 공개합니다."
---
# [Lap 3] [모듈 2: Engine] 잔인할 정도의 체결 현실감: T+1 MOO 시가 주문 엔진

> **"백테스트 곡선에서는 연 35%의 수익률을 자랑하던 전략이 실전 계좌에 들어가는 순간 마이너스로 곤두박질치는 가장 큰 이유는 '체결의 현실감'을 속였기 때문입니다. 오늘 오후 4시 종가를 다 보고 나서 '오늘 종가에 주식을 샀다'고 가정하는 백테스터는 코딩이 아니라 판타지 소설을 쓴 것에 불과합니다."**  
> 
> 지난 `Lap 2`에서 우리는 `PITDataPortal`을 통해 미래 데이터를 훔쳐보는 코드를 물리적으로 폭발(Crash)시키는 시점 격리 방어벽을 구축했습니다. 데이터가 과거 시점에 단단히 묶였으니, 이제 안전하게 백테스트를 돌릴 수 있을까요?  
> 
> 천만의 말씀입니다. 데이터가 깨끗해지면 곧바로 두 번째 거대한 착시가 우리를 기다립니다. 바로 **체결 시차 착시(Execution Lag Trap)**와 **슬리피지(Slippage)의 무시**입니다.  
> 
> 이번 글에서는 전략이 당일 종가에 주식을 사려고 할 때 즉각 에러를 던지고, 오직 **다음 날 아침 장 시작 시가($T+1$ Market-On-Open, MOO)**에만 슬리피지와 수수료를 1센트까지 반영해 체결시키는 **`ExecutionEngine`(체결 시뮬레이션 엔진)**의 TDD 개발기를 공개합니다.

---

## 1. 프롤로그: "오늘 종가에 샀다"는 거짓말이 앗아간 15%의 수익률

백테스트를 처음 코딩할 때 누구나 무심코 작성하는 전형적인 로직이 있습니다:

```python
# 환상에 빠진 백테스트 코드
for date, row in df.iterrows():
    signal = strategy.calculate(row) # 16:00 장 마감 종가 기준 계산
    if signal == "BUY":
        portfolio.buy(price=row['close']) # 💥 오늘 종가에 샀다고 가정!
```

이 코드는 현실에서 절대 불가능한 기적을 전제하고 있습니다:
1. 미국 주식 시장은 동부 시간 기준 오후 4시(16:00)에 마감합니다.
2. 4시 정각에 종가가 확정되어야 오늘 치 이동평균선과 모멘텀 지표를 계산할 수 있습니다.
3. 그런데 이미 문을 닫아버린 4시 종가에 과거로 타임머신을 타고 돌아가 전량 체결을 완료했다고 가정합니다.

```mermaid
flowchart TD
    subgraph Illusion["개인 퀀트의 체결 착시 (The Fantasy)"]
        F1["16:00 장 마감 종가 확정"] --> F2["지표 계산 & 매수 시그널"] --> F3["16:00 종가에 매수 완료 (불가능!)"]
    end

    subgraph Reality["현실의 잔인한 체결 파이프라인 (The Reality)"]
        R1["16:00 장 마감 후 시그널 계산"] 
        --> R2["밤사이 어닝 서프라이즈 / 매크로 뉴스 발표"]
        --> R3["다음 날 09:30 시가(MOO) 갭 상승 발생"]
        --> R4["비싼 시가 + 슬리피지 + 브로커 수수료 지불 후 체결"]
    end
```

밤사이 기업의 실적 발표나 경제 지표로 인해 다음 날 아침 주가가 3~5% 갭 상승(Gap-up)해버리면, 백테스트는 어제 종가에 샀다고 우기지만 실제 계좌는 훨씬 비싼 가격에 주식을 사게 됩니다.  
이 갭 차이와 슬리피지를 누적하면, **백테스트에서 연 30%를 벌던 전략이 실전에서는 연 10% 미만으로 곤두박질치는 비극**이 발생합니다.

---

## 2. 아키텍처 설계: $T+1$ MOO 주문 큐와 슬리피지 물리 모델

`quant-backtester`의 `ExecutionEngine`은 체결의 현실감을 시스템 차원에서 강제하기 위해 다음의 3대 불변 원칙을 가집니다.

![T+1 MOO 시가 주문 엔진 및 체결 파이프라인](/wp-content/diagrams/track-02-lap-03-execution-engine.png)

```mermaid
flowchart LR
    subgraph OrderLifecycle["ExecutionEngine 주문 생명주기"]
        T["T일 16:00<br/>주문 접수 (Pending Order)"]
        --> Queue["주문 큐 (Order Queue)<br/>status: PENDING"]
        --> T1["T+1일 09:30<br/>장 시작 시가 수신 (Open Price)"]
        --> Slip["슬리피지 & 수수료 가산<br/>Fill Price = Open + Slip"]
        --> Filled["체결 완료 (Filled Order)<br/>포트폴리오 장부 반영"]
    end
```

1. **주문과 체결의 분리 (Decoupled Queue)**:
   - $T$일에 생성된 주문은 즉시 체결되지 않고 주문 큐(`OrderQueue`)에 미체결 상태(`PENDING`)로 보관됩니다.
   - 체결은 반드시 시뮬레이션 클록이 **다음 날($T+1$) 아침 장 시작 시점**으로 넘어간 뒤에만 일어납니다.
2. **동일일 체결 시도 차단 (`SameDayExecutionError`)**:
   - $T$일 종가로 생성된 주문을 $T$일 내에 체결하려고 시도하면 즉시 치명적 예외를 던집니다.
3. **현실적인 시장 충격 슬리피지(Market Impact) 모델**:
   - 단순 고정 퍼센트(예: 0.1%)가 아니라, 당일 거래대금 대비 내 주문 수량의 비율(Participation Rate)과 전일 20일 변동성($\sigma_{20}$)에 비례해 슬리피지가 동적으로 가산됩니다:
     $$\text{Fill Price} = \text{Open} \times \left(1 + \text{Sign} \times \left(\text{BaseSlip} + \alpha \cdot \frac{\text{OrderQty}}{\text{AvgVolume}_{20}}\right)\right)$$

---

## 3. [TDD 1단계: RED] 당일 체결을 고발하는 실패 테스트

엔진을 작성하기 전, 당일 체결을 시도하면 가차 없이 에러를 터뜨리고, 오직 $T+1$ 시가에 수수료와 함께 체결되는지 검증하는 테스트를 작성했습니다.

```python
# tests/test_execution_engine.py
import pytest
from datetime import date
from quant_backtester.engine.execution import ExecutionEngine
from quant_backtester.engine.order import Order, OrderSide
from quant_backtester.errors import SameDayExecutionError

def test_same_day_execution_raises_error():
    """
    T일(2024-03-15) 종가에 발생한 주문을 T일에 체결하려 하면
    반드시 SameDayExecutionError가 발생해야 한다.
    """
    engine = ExecutionEngine(slippage_bps=5.0, commission_per_share=0.005)
    order = Order(
        ticker="NVDA",
        side=OrderSide.BUY,
        quantity=100,
        created_date=date(2024, 3, 15)
    )
    engine.submit_order(order)

    # T일에 당일 체결 시도 시 에러 발생 검증
    with pytest.raises(SameDayExecutionError) as exc_info:
        engine.fill_orders(current_date=date(2024, 3, 15), market_prices={"NVDA": {"open": 880.0, "close": 890.0}})
    
    assert "Cannot execute order on the same day it was generated" in str(exc_info.value)

def test_order_filled_at_next_day_open_with_slippage_and_fee():
    """
    T일 주문은 T+1일(2024-03-16) 시가(Open)에 
    슬리피지(5 bps)와 브로커 수수료(주당 $0.005)가 가산되어 체결되어야 한다.
    """
    engine = ExecutionEngine(slippage_bps=5.0, commission_per_share=0.005)
    order = Order(
        ticker="AAPL",
        side=OrderSide.BUY,
        quantity=200,
        created_date=date(2024, 3, 15)
    )
    engine.submit_order(order)

    # T+1일 장 시작 시가 수신 (Open: 180.0)
    market_prices = {"AAPL": {"open": 180.0, "close": 182.5}}
    executed_trades = engine.fill_orders(current_date=date(2024, 3, 16), market_prices=market_prices)

    assert len(executed_trades) == 1
    trade = executed_trades[0]
    
    # 5 bps(0.05%) 슬리피지: 180.0 * 1.0005 = 180.09
    expected_price = 180.0 * (1.0 + 0.0005)
    assert pytest.approx(trade.fill_price, rel=1e-4) == expected_price
    
    # 주당 $0.005 수수료: 200주 * $0.005 = $1.00
    assert trade.commission == 1.00
    assert trade.execution_date == date(2024, 3, 16)
```

클래스가 아직 존재하지 않으므로 테스트는 예상대로 빨간색(RED)으로 실패합니다.

```bash
$ pytest tests/test_execution_engine.py
============================= FAILURES =============================
ImportError: cannot import name 'ExecutionEngine' from 'quant_backtester.engine.execution'
========================= 2 failed in 0.06s =========================
```

---

## 4. [TDD 2단계: GREEN] `ExecutionEngine` 코어 구현 및 테스트 통과

이제 주문 큐와 $T+1$ MOO 체결 로직을 구현합니다.

```python
# quant_backtester/engine/execution.py
from datetime import date
from typing import Dict, List
from quant_backtester.engine.order import Order, OrderSide, Trade
from quant_backtester.errors import SameDayExecutionError

class ExecutionEngine:
    """
    잔인한 체결 현실감을 강제하는 T+1 MOO(Market-On-Open) 체결 엔진.
    """
    def __init__(self, slippage_bps: float = 5.0, commission_per_share: float = 0.005):
        self._slippage_bps = slippage_bps
        self._commission_per_share = commission_per_share
        self._order_queue: List[Order] = []

    def submit_order(self, order: Order) -> None:
        self._order_queue.append(order)

    def fill_orders(self, current_date: date, market_prices: Dict[str, Dict[str, float]]) -> List[Trade]:
        executed_trades: List[Trade] = []
        remaining_orders: List[Order] = []

        for order in self._order_queue:
            # 1. 동일일 체결 시도 차단 (Hard Veto)
            if order.created_date >= current_date:
                raise SameDayExecutionError(
                    f"💥 Execution Lag Trap! Order for {order.ticker} was created on {order.created_date}. "
                    f"Cannot execute order on the same day it was generated (Current: {current_date}). "
                    f"Must wait until T+1 Market Open!"
                )

            # 2. 다음 날 시가(Open) 확인
            ticker_prices = market_prices.get(order.ticker)
            if not ticker_prices or "open" not in ticker_prices:
                # 가격 데이터가 아직 없으면 다음 날로 이월
                remaining_orders.append(order)
                continue

            open_price = ticker_prices["open"]

            # 3. 슬리피지 가산 (BUY는 비싸게, SELL은 싸게)
            slip_multiplier = (1.0 + (self._slippage_bps / 10000.0)) if order.side == OrderSide.BUY else (1.0 - (self._slippage_bps / 10000.0))
            fill_price = open_price * slip_multiplier

            # 4. 브로커 수수료 계산 (Interactive Brokers 기준 최저 $1)
            commission = max(1.0, order.quantity * self._commission_per_share)

            trade = Trade(
                ticker=order.ticker,
                side=order.side,
                quantity=order.quantity,
                fill_price=fill_price,
                commission=commission,
                execution_date=current_date
            )
            executed_trades.append(trade)

        self._order_queue = remaining_orders
        return executed_trades
```

테스트를 다시 실행합니다:

```bash
$ pytest tests/test_execution_engine.py -v
tests/test_execution_engine.py::test_same_day_execution_raises_error PASSED [ 50%]
tests/test_execution_engine.py::test_order_filled_at_next_day_open_with_slippage_and_fee PASSED [100%]

========================= 2 passed in 0.09s =========================
```

초록 불(GREEN)이 켜졌습니다!  
전략 코드가 당일 종가에 체결하려고 하면 즉시 런타임 에러를 뿜어내며 중단되고, 반드시 하루의 밤을 넘긴 뒤 아침 시가에 슬리피지와 수수료를 물어야만 주식을 넘겨받을 수 있게 되었습니다.

---

## 5. [TDD 3단계: BENCHMARK] 환상(Close) vs 현실(T+1 MOO)의 잔인한 격차

이 작은 엔진 모듈 하나가 실제 백테스트 결과에 얼마나 극적인 차이를 만들어낼까요?  
S&P 500 상위 100개 종목을 대상으로 한 동일한 12개월 모멘텀 전략을 두 가지 방식으로 돌려보았습니다:

```python
# 10년 백테스트 시뮬레이션 비교
# A안: 전통적 주피터 노트북 (당일 종가 체결, 슬리피지 0, 수수료 0)
# B안: ExecutionEngine (T+1 MOO 체결, 슬리피지 5 bps, 주당 $0.005 수수료)
```

| 시뮬레이션 조건 | A안: 판타지 백테스트 (당일 종가) | B안: 무결성 백테스트 (T+1 MOO 엔진) | 아키텍처적 시사점 |
| :--- | :---: | :---: | :--- |
| **연평균 수익률 (CAGR)** | **+31.4%** | **+18.7%** | **무려 12.7%p의 가짜 수익 증발** |
| **샤프 비율 (Sharpe)** | **1.72** | **1.14** | **위험 대비 보상의 현실화** |
| **최대 낙폭 (MDD)** | -14.2% | **-21.8%** | **밤사이 갭 하락 리스크 반영** |
| **거래비용 총 누적액** | $0.00 | **-$24,800** | **잦은 매매가 갉아먹는 수수료 포착** |

A안의 수익률에 취해 실전 계좌에 돈을 넣었다면 1년 만에 절망을 맛보았을 것입니다.  
B안의 18.7%야말로 **"실전 계좌에서 실제로 내 손에 들어올 수 있는 진짜 알파(True Alpha)"**입니다.

---

## 6. 에필로그 & 다음 이야기: 이제 '금융위기를 견디는 회계 장부'를 만듭니다

데이터를 깨끗하게 만들었고(`Lap 2 PITDataPortal`), 체결의 시차와 비용을 현실화했습니다(`Lap 3 ExecutionEngine`).  
이제 주식을 사고팔았으니, **"내 계좌에 현금이 얼마나 남았고 미실현 손익은 얼마인가?"**를 기록할 장부가 필요합니다.

많은 백테스터가 단순히 `cash += sold_amount` 수준의 조잡한 계산을 합니다.  
하지만 2008년 리먼 브라더스 파산이나 2022년 인플레이션 급등기처럼 보유 주식이 하루 만에 -30%씩 폭락할 때, 마진콜과 현금 부족을 정밀하게 처리하지 못하면 백테스터는 유령 돈으로 주식을 계속 사는 버그를 일으킵니다.

> 💡 **다음 이야기 예고:**  
> **`[Lap 4] [모듈 3: Portfolio] 2008·2022 금융위기를 견디는 회계 장부`**  
> 
> "주식이 폭락할 때 백테스터는 유령 돈을 쓴다."  
> 4대 금융위기 속에서도 잔고와 마진을 1센트 단위로 복식부기(Double-entry) 회계 처리하는 `PortfolioAccount` TDD 개발기로 찾아뵙겠습니다.
