수정본
Some checks failed
CI/CD Pipeline / test-backend (push) Has been cancelled
CI/CD Pipeline / test-frontend (push) Has been cancelled
CI/CD Pipeline / build-and-push (push) Has been cancelled

This commit is contained in:
2026-03-27 14:47:49 +09:00
parent 844ee8944f
commit c818810667
25 changed files with 2708 additions and 266 deletions

View File

@@ -766,7 +766,12 @@ class KrBacktestEngine:
market: str,
random_seed: Optional[int],
) -> BacktestResult:
"""핵심 시뮬레이션 루프 (동기)"""
"""핵심 시뮬레이션 루프 (동기)
[Look-ahead bias 수정]
T일 close로 신호(on_bar) 생성 -> T+1일 open + 슬리피지로 체결.
마지막 봉 신호는 다음 봉이 없으므로 무시합니다.
"""
cash = initial_capital
positions: Dict[str, Position] = {}
all_trades: List[BacktestTrade] = []
@@ -784,6 +789,9 @@ class KrBacktestEngine:
peak_equity = initial_capital
seed_counter = random_seed if random_seed is not None else None
# 전일 신호를 저장 (T일 close 신호 -> T+1일 open 체결)
pending_orders: List[Dict[str, Any]] = []
for bar_idx in range(len(all_dates)):
current_date = all_dates[bar_idx]
@@ -795,23 +803,18 @@ class KrBacktestEngine:
position_value += stock_bars[bar_idx].close_price * pos.quantity
total_equity = cash + position_value
# 전략 호출
orders = strategy.on_bar(
bar_idx=bar_idx,
bars=bars,
positions=positions,
cash=cash,
total_equity=total_equity,
params=params,
)
# 주문 실행
for order in orders:
# -- 1단계: 전일 신호를 오늘 open + 슬리피지로 체결 --
for order in pending_orders:
action = order["action"]
code = order["stock_code"]
price = order["price"]
quantity = order["quantity"]
# T+1일 open 가격을 base price로 사용
stock_bars = bars.get(code, [])
if bar_idx >= len(stock_bars):
continue
next_open = float(stock_bars[bar_idx].open_price)
# 슬리피지용 시드
if seed_counter is not None:
seed_counter += 1
@@ -820,7 +823,7 @@ class KrBacktestEngine:
slip_seed = None
if action == "BUY":
exec_price = _apply_buy_slippage(price, slip_seed)
exec_price = _apply_buy_slippage(next_open, slip_seed)
total_cost, commission = _calc_buy_cost(exec_price, quantity)
if total_cost > cash:
@@ -831,7 +834,7 @@ class KrBacktestEngine:
total_cost, commission = _calc_buy_cost(exec_price, quantity)
cash -= total_cost
slippage_amount = abs(exec_price - price) * quantity
slippage_amount = abs(exec_price - next_open) * quantity
trade = BacktestTrade(
stock_code=code,
@@ -859,14 +862,13 @@ class KrBacktestEngine:
if pos is None:
continue
sell_pct = order.get("sell_pct", 1.0)
sell_qty = min(quantity, pos.quantity)
exec_price = _apply_sell_slippage(price, slip_seed)
exec_price = _apply_sell_slippage(next_open, slip_seed)
proceeds, commission, tax = _calc_sell_proceeds(exec_price, sell_qty, market)
cash += proceeds
slippage_amount = abs(price - exec_price) * sell_qty
slippage_amount = abs(next_open - exec_price) * sell_qty
# PnL 계산
entry_cost = pos.entry_price * sell_qty * (1 + BUY_COMMISSION_RATE)
@@ -899,6 +901,29 @@ class KrBacktestEngine:
pos.quantity = remaining_qty
pos.remaining_pct = remaining_qty / (pos.quantity + sell_qty)
pending_orders.clear()
# -- 2단계: 오늘 close 기준으로 신호 생성 (체결은 내일 open) --
is_last_bar = (bar_idx == len(all_dates) - 1)
if not is_last_bar:
# 체결 이후의 상태로 평가 자산 재계산 (전략에 전달)
position_value = 0.0
for code, pos in positions.items():
stock_bars_eval = bars.get(code, [])
if bar_idx < len(stock_bars_eval):
position_value += stock_bars_eval[bar_idx].close_price * pos.quantity
total_equity = cash + position_value
orders = strategy.on_bar(
bar_idx=bar_idx,
bars=bars,
positions=positions,
cash=cash,
total_equity=total_equity,
params=params,
)
pending_orders = list(orders)
# 일말 평가
position_value = 0.0
for code, pos in positions.items():