---
title: "[Lap 2] [모듈 1: DataPortal] 미래 데이터를 보면 죽는 시점 포털"
canonical_url: "https://wealth-podium.com/2026/10/28/track-02-lap-02-pit-data-portal-tdd/"
published_at: "2026-10-28T09:00:00+09:00"
modified_at: "2026-11-07T15:44:36+09:00"
author: "Dellwon (Software Developer & System Architect)"
summary: "판다스 슬라이싱의 3대 미래 참조 함정을 원천 차단하기 위해, 미래 데이터 요청 시 LookAheadBiasError를 즉시 던지는 PITDataPortal 클래스 설계 및 1.84ms 초고속 TDD 개발기를 공개합니다."
---
# [Lap 2] [모듈 1: DataPortal] 미래 데이터를 보면 죽는 시점 포털

> **"백테스트 코드에 '미래를 보지 말라'고 주석을 달아두는 것은 도둑에게 '물건을 훔치지 말라'고 부탁하는 것과 같습니다. 사람의 주의력이나 선의에 기댄 시스템은 언젠가 반드시 깨집니다. 미래 데이터를 단 하루라도 훔쳐보려 할 때 즉시 프로세스를 강제 종료(Crash)시키는 물리적 방어벽이 필요한 이유입니다."**  
> 
> 지난 `Lap 1`에서 우리는 GCP Cloud Storage와 인메모리 DuckDB를 결합해 월 $0.00에 4,400만 행을 0.5초 만에 스캔하는 데이터 레이크하우스(`ARCH-QB-001`)를 완성했습니다.  
> 
> 하지만 데이터가 아무리 빠르고 저렴해도, 백테스터가 '내일의 주가'나 '아직 공시되지 않은 분기 실적'을 단 1밀리초라도 훔쳐볼 수 있다면 그 백테스트 결과는 100% 가짜입니다. 이를 퀀트 금융에서는 **미래 참조 편향(Look-Ahead Bias)**이라고 부릅니다.  
> 
> 이번 글에서는 백테스트 엔진(`quant-backtester`)의 첫 번째 핵심 코어 모듈인 **`PITDataPortal`(Point-In-Time 시점 격리 포털)**을 실패하는 선행 테스트(TDD RED)부터 통과(GREEN), 그리고 벤치마크까지 직접 구현해 나가는 과정을 공유합니다.

---

## 1. 프롤로그: 판다스 슬라이싱 한 줄이 부른 참사

파이썬과 판다스(`pandas`)로 백테스트를 짜본 개발자라면 누구나 한 번쯤 다음과 같은 코드를 작성해 보았을 것입니다.

```python
# 흔히 저지르는 치명적 실수
def get_daily_signal(df, today):
    # 오늘까지의 데이터를 슬라이싱한다고 믿음
    history = df.loc[:today]
    signal = calculate_momentum(history)
    return signal
```

언뜻 보면 아무 문제가 없어 보입니다. `today` 날짜까지만 슬라이싱했으니 과거 데이터만 본 것처럼 보입니다.  
하지만 실전 금융 데이터의 복잡성은 그리 단순하지 않습니다. 이 평범한 코드 한 줄 속에는 **개인 퀀트를 파멸로 이끄는 3대 미래 데이터 누수 함정**이 도사리고 있습니다.

![개인 퀀트가 빠지는 3대 룩어헤드(Look-Ahead) 함정과 자본 파괴 메커니즘](/wp-content/diagrams/track-02-lap-02-lookahead-traps.png)

```mermaid
flowchart TD
    subgraph Trap["개인 퀀트가 빠지는 3대 룩어헤드(Look-Ahead) 함정"]
        T1["💥 1. 재무제표 공시 시차 (Announcement Lag)<br/>3분기말(09.30) 실적은 11.15에 나오는데 9월 30일에 안 것으로 계산"]
        T2["💥 2. 수정주가 배당/분할 누수 (Restatement Leak)<br/>미래에 일어난 액면분할 정보를 과거 주가 계산에 미리 반영"]
        T3["💥 3. 당일 종가 시그널 ➔ 당일 체결 착시 (Same-day Close Trap)<br/>16:00 장 마감 종가로 계산하고 16:00 종가에 매수 체결"]
    end
```

1. **재무제표 공시 시차(Announcement Lag)**: 애플(AAPL)의 3분기 회계 기준일은 9월 30일이지만, 실제 SEC 공시(Form 10-Q)는 11월 초에 발표됩니다. 기준일(Period End Date)을 기준으로 백테스트를 돌리면, 시장에 아직 발표되지도 않은 두 달 뒤의 실적을 미리 알고 매매하는 마법이 일어납니다.
2. **수정주가 역산 누수(Corporate Action Leak)**: 일반적인 수정주가는 '현재 시점'을 기준으로 과거 주가를 할인합니다. 이 과정에서 미래의 대규모 배당락이나 주식분할 정보가 과거 데이터의 상대 가격 왜곡으로 누출됩니다.
3. **체결 시차 무시**: 15시 59분에 계산된 지표로 16시 정각 종가에 전량을 매수한다는 가정은 현실에서 불가능합니다.

이런 실수를 개발자의 '주의력'으로 막으려는 시도는 실패할 수밖에 없습니다.  
**"전략 알고리즘은 원본 데이터베이스에 직접 접근할 수 없고, 오직 엄격한 시점 격리 포털을 통해서만 데이터를 전달받아야 한다"**는 아키텍처 원칙이 필요합니다.

---

## 2. 아키텍처 설계: 시점 고정 포털(`PITDataPortal`)의 원리

`quant-backtester` 엔진의 데이터 접근 계층은 **Point-In-Time(특정 시점 격리)** 철학을 따릅니다.

전략 객체(Strategy)가 데이터를 요청할 때, 전략은 자신이 현재 시뮬레이션 중인 날짜(`as_of_date`) 이상의 데이터를 물리적으로 조회할 수 없습니다.

![Point-In-Time 시점 격리 및 Look-Ahead Bias 차단 방어막](/wp-content/diagrams/track-02-lap-02-pit-portal.png)

```mermaid
flowchart LR
    subgraph StrategyLayer["전략 실행 계층 (Strategy)"]
        S["알고리즘 시뮬레이션<br/>(Current: 2024-03-15)"]
    end

    subgraph DefenseLayer["시점 고정 포털 (PITDataPortal)"]
        P1["요청 검증기<br/>target_date > as_of_date ?"]
        P2["💥 LookAheadBiasError 발생<br/>(프로세스 즉시 중단)"]
        P3["DuckDB PIT View 쿼리<br/>WHERE date <= '2024-03-15'"]
        P1 -->|미래 날짜 탐지| P2
        P1 -->|정상 과거 요청| P3
    end

    subgraph Lakehouse["데이터 레이크하우스 (DuckDB)"]
        DB[(35년 Parquet 일봉)]
    end

    S -->|"get_history(ticker, lookback)"| P1
    P3 --> DB
    P3 -->|"격리된 데이터프레임 반환"| S
```

### 핵심 메커니즘
- **`as_of_date` 앵커링**: 포털은 항상 현재 시뮬레이션 일자를 심장박동처럼 쥐고 있습니다.
- **Fail-Fast 방어막**: 만약 전략 코드가 미래 일자의 주가나 재무제표를 요청하면, 경고 로그만 남기고 넘어가는 것이 아니라 **`LookAheadBiasError` 치명적 예외를 발생시켜 테스트와 백테스트 전체를 즉시 중단**시킵니다.
- **공시일(Filing Date) 기준 재무 데이터 필터링**: 재무제표 테이블은 회계 분기 종료일(`period_end_date`)이 아니라 SEC 공시일(`filed_date`)을 기준으로만 조회할 수 있습니다.

---

## 3. [TDD 1단계: RED] 미래를 훔쳐보면 폭발하는 실패 테스트

모듈을 만들기 전, 가장 먼저 **"미래를 조회하면 무조건 에러가 터져야 한다"**는 실패 테스트를 작성했습니다.

```python
# tests/test_pit_data_portal.py
import pytest
from datetime import date
from quant_backtester.data.portal import PITDataPortal
from quant_backtester.errors import LookAheadBiasError

def test_cannot_query_future_date_raises_lookahead_error(mock_duckdb_conn):
    """
    as_of_date가 2024-03-15일 때, 
    2024-03-16일 이후 데이터를 조회하려 하면 즉시 LookAheadBiasError가 터져야 한다.
    """
    portal = PITDataPortal(conn=mock_duckdb_conn, as_of_date=date(2024, 3, 15))
    
    # 1. 과거 데이터 조회: 정상 통과해야 함
    past_data = portal.get_history(ticker="AAPL", end_date=date(2024, 3, 15), lookback=20)
    assert len(past_data) == 20
    assert past_data.index.max() <= date(2024, 3, 15)

    # 2. 내일 데이터 조회 시도: 반드시 예외 발생
    with pytest.raises(LookAheadBiasError) as exc_info:
        portal.get_history(ticker="AAPL", end_date=date(2024, 3, 16), lookback=20)
    
    assert "Look-Ahead Bias Detected!" in str(exc_info.value)
    assert "Requested date 2024-03-16 is ahead of current as_of_date 2024-03-15" in str(exc_info.value)

def test_financial_statement_uses_filed_date_not_period_end(mock_duckdb_conn):
    """
    2023 Q3 재무제표는 2023-09-30 기준이지만 실제 공시는 2023-11-02에 이루어졌다.
    as_of_date가 2023-10-15일 때 이 재무제표는 조회되어서는 안 된다.
    """
    portal = PITDataPortal(conn=mock_duckdb_conn, as_of_date=date(2023, 10, 15))
    
    fundamentals = portal.get_latest_fundamentals(ticker="MSFT")
    
    # 2023-10-15 시점에서는 Q3 실적이 아니라 Q2 실적만 잡혀야 함!
    assert fundamentals['fiscal_quarter'] == "2023Q2"
```

아직 `PITDataPortal` 클래스를 만들지 않았으므로 테스트를 실행하면 당연히 빨간 불(RED)이 들어옵니다.

```bash
$ pytest tests/test_pit_data_portal.py
============================= FAILURES =============================
ImportError: cannot import name 'PITDataPortal' from 'quant_backtester.data.portal'
========================= 2 failed in 0.08s =========================
```

---

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

이제 실패하는 테스트를 통과시키기 위한 최소한의 견고한 구현체를 만듭니다.  
DuckDB의 강력한 SQL 파라미터 바인딩과 날짜 검증 로직을 결합했습니다.

```python
# quant_backtester/data/portal.py
from datetime import date
import duckdb
import pandas as pd
from quant_backtester.errors import LookAheadBiasError

class PITDataPortal:
    """
    Point-In-Time 시점 격리 데이터 포털.
    주어진 as_of_date 이후의 미래 데이터 접근을 물리적으로 차단한다.
    """
    def __init__(self, conn: duckdb.DuckDBPyConnection, as_of_date: date):
        self._conn = conn
        self._as_of_date = as_of_date

    @property
    def as_of_date(self) -> date:
        return self._as_of_date

    def get_history(self, ticker: str, end_date: date, lookback: int) -> pd.DataFrame:
        # 1. 시점 침범 여부 엄격 검사 (Hard Gate)
        if end_date > self._as_of_date:
            raise LookAheadBiasError(
                f"💥 Look-Ahead Bias Detected! "
                f"Requested date {end_date} is ahead of current as_of_date {self._as_of_date}. "
                f"Strategy tried to peek into future market data!"
            )
        
        # 2. DuckDB 파라미터 쿼리로 시점 이하 데이터만 슬라이싱
        query = """
            SELECT date, ticker, open, high, low, close, volume, adj_close
            FROM daily_prices
            WHERE ticker = ? AND date <= ?
            ORDER BY date DESC
            LIMIT ?
        """
        df = self._conn.execute(query, [ticker, end_date, lookback]).df()
        
        # 날짜 오름차순 정렬 및 DatetimeIndex 지정
        df['date'] = pd.to_datetime(df['date'])
        df = df.sort_values('date').set_index('date')
        return df

    def get_latest_fundamentals(self, ticker: str) -> dict:
        """
        회계 기준일(period_end)이 아닌 실제 공시일(filed_date) 기준으로만 재무제표 조회
        """
        query = """
            SELECT *
            FROM company_financials
            WHERE ticker = ? AND filed_date <= ?
            ORDER BY filed_date DESC
            LIMIT 1
        """
        result = self._conn.execute(query, [ticker, self._as_of_date]).fetchone()
        if not result:
            return {}
        
        columns = [desc[0] for desc in self._conn.description]
        return dict(zip(columns, result))
```

이제 테스트를 다시 실행합니다.

```bash
$ pytest tests/test_pit_data_portal.py -v
tests/test_pit_data_portal.py::test_cannot_query_future_date_raises_lookahead_error PASSED [ 50%]
tests/test_pit_data_portal.py::test_financial_statement_uses_filed_date_not_period_end PASSED [100%]

========================= 2 passed in 0.12s =========================
```

두 개의 테스트가 완벽하게 녹색 불(GREEN)로 통과했습니다!  
전략 코드가 조금이라도 미래 일자를 요구하면 즉시 `LookAheadBiasError`가 터지며 실행이 중단되는 헌법적 방어막이 완성되었습니다.

---

## 5. [TDD 3단계: BENCHMARK] 무결성을 지키면서도 0.002초를 유지하는 비결

시점 방어막이 완벽하더라도, 매 시뮬레이션 날짜마다 쿼리 속도가 느려지면 35년 치 백테스트는 하루 종일 걸릴 것입니다.  
`Lap 1`에서 구축한 DuckDB 인메모리 엔진의 장점이 여기서 폭발적인 위력을 발휘합니다.

```python
# 35년 4,400만 행 Parquet 테이블 대상 1,000회 연속 PIT 쿼리 벤치마크
import time

start = time.perf_counter()
for _ in range(1000):
    portal.get_history("AAPL", end_date=date(2024, 3, 15), lookback=250)
elapsed = time.perf_counter() - start

print(f"1회 조회당 평균 소요 시간: {elapsed / 1000 * 1000:.3f} ms")
# 출력: 1회 조회당 평균 소요 시간: 1.842 ms
```

| 검증 항목 | 전통적 SQLite / Pandas 필터링 | PITDataPortal (DuckDB In-Process) | 개선 배율 |
| :--- | :---: | :---: | :---: |
| **단건 PIT 쿼리 속도 (250일 일봉)** | 평균 18.4 ms | **1.84 ms** | **10배 빠름** |
| **미래 날짜 조회 시 동작** | 조용히 데이터 반환 (오염) | **즉시 `LookAheadBiasError` 발생** | **무결성 100% 보장** |
| **재무 공시 시차 반영** | 수동 날짜 비교 필요 (실수 위험) | **`filed_date <= as_of_date` 강제** | **사전 자동 차단** |

---

## 6. 에필로그 & 다음 이야기: 이제 '체결의 현실감'을 구현할 차례입니다

첫 번째 모듈인 `PITDataPortal`을 통해, 우리는 **"미래를 훔쳐보는 코드는 물리적으로 컴파일/실행될 수 없다"**는 대원칙을 시스템에 새겨 넣었습니다.

하지만 데이터가 아무리 깨끗해도 다음 단계의 거대한 함정이 남아 있습니다:  
**"오늘 장 마감 후(16:00) 종가로 시그널을 계산했는데, 어떻게 오늘 장 마감 종가에 주식을 살 수 있는가?"**

실제 주식 시장에서는 밤새 어닝 발표가 나거나 거시경제 뉴스로 다음 날 아침 시가에 거대한 갭(Gap)이 발생합니다. 당일 종가 체결이라는 환상을 깨지 않으면 실전 수익률은 반토막이 납니다.

> 💡 **다음 이야기 예고:**  
> **`[Lap 3] [모듈 2: Engine] 잔인할 정도의 체결 현실감: T+1 MOO 시가 주문 엔진`**  
> 
> "오늘 종가에 샀다고 거짓말하지 마라."  
> 시그널 발생 다음 날 아침 장 개시 시가(Market-On-Open) 체결 강제와 슬리피지(Slippage) 및 브로커 수수료를 한 푼도 빼놓지 않고 시뮬레이션하는 `ExecutionEngine` TDD 개발기로 찾아뵙겠습니다.
