[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)로 백테스트를 짜본 개발자라면 누구나 한 번쯤 다음과 같은 코드를 작성해 보았을 것입니다.
# 흔히 저지르는 치명적 실수
def get_daily_signal(df, today):
# 오늘까지의 데이터를 슬라이싱한다고 믿음
history = df.loc[:today]
signal = calculate_momentum(history)
return signal
언뜻 보면 아무 문제가 없어 보입니다. today 날짜까지만 슬라이싱했으니 과거 데이터만 본 것처럼 보입니다.
하지만 실전 금융 데이터의 복잡성은 그리 단순하지 않습니다. 이 평범한 코드 한 줄 속에는 개인 퀀트를 파멸로 이끄는 3대 미래 데이터 누수 함정이 도사리고 있습니다.

📐 원본 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
- 재무제표 공시 시차(Announcement Lag): 애플(AAPL)의 3분기 회계 기준일은 9월 30일이지만, 실제 SEC 공시(Form 10-Q)는 11월 초에 발표됩니다. 기준일(Period End Date)을 기준으로 백테스트를 돌리면, 시장에 아직 발표되지도 않은 두 달 뒤의 실적을 미리 알고 매매하는 마법이 일어납니다.
- 수정주가 역산 누수(Corporate Action Leak): 일반적인 수정주가는 ‘현재 시점’을 기준으로 과거 주가를 할인합니다. 이 과정에서 미래의 대규모 배당락이나 주식분할 정보가 과거 데이터의 상대 가격 왜곡으로 누출됩니다.
- 체결 시차 무시: 15시 59분에 계산된 지표로 16시 정각 종가에 전량을 매수한다는 가정은 현실에서 불가능합니다.
이런 실수를 개발자의 ‘주의력’으로 막으려는 시도는 실패할 수밖에 없습니다.
“전략 알고리즘은 원본 데이터베이스에 직접 접근할 수 없고, 오직 엄격한 시점 격리 포털을 통해서만 데이터를 전달받아야 한다”는 아키텍처 원칙이 필요합니다.
2. 아키텍처 설계: 시점 고정 포털(PITDataPortal)의 원리
quant-backtester 엔진의 데이터 접근 계층은 Point-In-Time(특정 시점 격리) 철학을 따릅니다.
전략 객체(Strategy)가 데이터를 요청할 때, 전략은 자신이 현재 시뮬레이션 중인 날짜(as_of_date) 이상의 데이터를 물리적으로 조회할 수 없습니다.

📐 원본 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] 미래를 훔쳐보면 폭발하는 실패 테스트
모듈을 만들기 전, 가장 먼저 “미래를 조회하면 무조건 에러가 터져야 한다”는 실패 테스트를 작성했습니다.
# 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)이 들어옵니다.
$ 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 파라미터 바인딩과 날짜 검증 로직을 결합했습니다.
# 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))
이제 테스트를 다시 실행합니다.
$ 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 인메모리 엔진의 장점이 여기서 폭발적인 위력을 발휘합니다.
# 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) 및 브로커 수수료를 한 푼도 빼놓지 않고 시뮬레이션하는ExecutionEngineTDD 개발기로 찾아뵙겠습니다.