수정본
This commit is contained in:
@@ -5,6 +5,7 @@ us_stock_strategy의 매수/매도 신호를 과거 데이터에 적용하여
|
||||
"""
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
@@ -25,6 +26,27 @@ logger = logging.getLogger(__name__)
|
||||
US_COMMISSION_RATE = 0.001 # 해외 주식 수수료 약 0.1% (매수+매도 각각)
|
||||
RISK_FREE_RATE = 0.04 # 무위험 수익률 4% (연간, USD 기준)
|
||||
TRADING_DAYS_PER_YEAR = 252
|
||||
US_SLIPPAGE_PCT = 0.0005 # +/-0.05% (미국 주식은 유동성이 높아 슬리피지가 낮음)
|
||||
|
||||
|
||||
def _apply_us_buy_slippage(price: float, seed: Optional[int] = None) -> float:
|
||||
"""매수 시 슬리피지: 시장가 주문 시 약간 높은 가격에 체결
|
||||
|
||||
미국 주식은 유동성이 높아 국내(0.1%) 대비 낮은 0~0.05% 적용.
|
||||
"""
|
||||
rng = random.Random(seed)
|
||||
slip = rng.uniform(0, US_SLIPPAGE_PCT)
|
||||
return price * (1 + slip)
|
||||
|
||||
|
||||
def _apply_us_sell_slippage(price: float, seed: Optional[int] = None) -> float:
|
||||
"""매도 시 슬리피지: 시장가 주문 시 약간 낮은 가격에 체결
|
||||
|
||||
미국 주식은 유동성이 높아 국내(0.1%) 대비 낮은 0~0.05% 적용.
|
||||
"""
|
||||
rng = random.Random(seed)
|
||||
slip = rng.uniform(0, US_SLIPPAGE_PCT)
|
||||
return price * (1 - slip)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -229,6 +251,10 @@ async def run_us_backtest(
|
||||
indicators = _calculate_indicators(daily_data)
|
||||
|
||||
# 3) 시뮬레이션 실행
|
||||
#
|
||||
# [Look-ahead bias 수정]
|
||||
# T일 close로 신호 생성 -> T+1일 open + 슬리피지로 체결.
|
||||
# 마지막 봉 신호는 다음 봉이 없으므로 무시합니다.
|
||||
cash = initial_capital_usd
|
||||
position_qty = 0
|
||||
position_avg_price = 0.0
|
||||
@@ -236,96 +262,114 @@ async def run_us_backtest(
|
||||
trades: List[Dict] = []
|
||||
equity_curve: List[Dict] = []
|
||||
|
||||
# 전일 발생한 신호를 저장 (T일 close 신호 -> T+1일 open 체결)
|
||||
pending_buy_signal: Optional[Dict] = None
|
||||
pending_sell_signal: Optional[str] = None
|
||||
|
||||
for i in range(20, len(daily_data)): # 지표 계산에 최소 20일 필요
|
||||
bar = daily_data[i]
|
||||
ind = indicators[i]
|
||||
close = bar["close"]
|
||||
bar_date = bar["date"]
|
||||
bar_open = bar["open"]
|
||||
|
||||
# 포지션 보유 시: 매도 조건 체크
|
||||
# -- 1단계: 전일 신호를 오늘 open + 슬리피지로 체결 --
|
||||
if pending_sell_signal is not None and position_qty > 0:
|
||||
exec_price = _apply_us_sell_slippage(bar_open)
|
||||
amount = position_qty * exec_price
|
||||
commission = amount * US_COMMISSION_RATE
|
||||
pnl = (exec_price - position_avg_price) * position_qty - commission
|
||||
pnl_pct = ((exec_price - position_avg_price) / position_avg_price * 100) if position_avg_price > 0 else 0.0
|
||||
|
||||
trade = {
|
||||
"date": bar_date,
|
||||
"action": "SELL",
|
||||
"ticker": ticker,
|
||||
"price": round(exec_price, 4),
|
||||
"quantity": position_qty,
|
||||
"amount": round(amount, 2),
|
||||
"commission": round(commission, 2),
|
||||
"reason": pending_sell_signal,
|
||||
"pnl": round(pnl, 2),
|
||||
"pnl_pct": round(pnl_pct, 2),
|
||||
}
|
||||
trades.append(trade)
|
||||
cash += amount - commission
|
||||
position_qty = 0
|
||||
position_avg_price = 0.0
|
||||
position_high = 0.0
|
||||
pending_sell_signal = None
|
||||
|
||||
if pending_buy_signal is not None and position_qty == 0:
|
||||
exec_price = _apply_us_buy_slippage(bar_open)
|
||||
buy_amount = min(cash * 0.95, cash)
|
||||
if buy_amount > 10:
|
||||
quantity = int(buy_amount / exec_price)
|
||||
if quantity > 0:
|
||||
cost = quantity * exec_price
|
||||
commission = cost * US_COMMISSION_RATE
|
||||
total_cost = cost + commission
|
||||
|
||||
if total_cost <= cash:
|
||||
cash -= total_cost
|
||||
position_qty = quantity
|
||||
position_avg_price = exec_price
|
||||
position_high = exec_price
|
||||
|
||||
trade = {
|
||||
"date": bar_date,
|
||||
"action": "BUY",
|
||||
"ticker": ticker,
|
||||
"price": round(exec_price, 4),
|
||||
"quantity": quantity,
|
||||
"amount": round(cost, 2),
|
||||
"commission": round(commission, 2),
|
||||
"reason": pending_buy_signal.get("reason", "buy_signal"),
|
||||
"pnl": 0.0,
|
||||
"pnl_pct": 0.0,
|
||||
}
|
||||
trades.append(trade)
|
||||
pending_buy_signal = None
|
||||
|
||||
# -- 2단계: 오늘 close 기준으로 신호 생성 (체결은 내일 open) --
|
||||
is_last_bar = (i == len(daily_data) - 1)
|
||||
|
||||
# 트레일링 스탑용 최고가 갱신 (close 기준)
|
||||
if position_qty > 0:
|
||||
# 트레일링 스탑용 최고가 갱신
|
||||
if close > position_high:
|
||||
position_high = close
|
||||
|
||||
sell_signal = _check_backtest_sell(
|
||||
close=close,
|
||||
avg_price=position_avg_price,
|
||||
high_since_entry=position_high,
|
||||
rsi=ind.get("rsi", 50),
|
||||
bb_middle=ind.get("bb_middle", close),
|
||||
bb_upper=ind.get("bb_upper", close),
|
||||
)
|
||||
if not is_last_bar:
|
||||
# 매도 신호 체크
|
||||
if position_qty > 0:
|
||||
sell_signal = _check_backtest_sell(
|
||||
close=close,
|
||||
avg_price=position_avg_price,
|
||||
high_since_entry=position_high,
|
||||
rsi=ind.get("rsi", 50),
|
||||
bb_middle=ind.get("bb_middle", close),
|
||||
bb_upper=ind.get("bb_upper", close),
|
||||
)
|
||||
if sell_signal:
|
||||
pending_sell_signal = sell_signal
|
||||
|
||||
if sell_signal:
|
||||
amount = position_qty * close
|
||||
commission = amount * US_COMMISSION_RATE
|
||||
pnl = (close - position_avg_price) * position_qty - commission
|
||||
pnl_pct = ((close - position_avg_price) / position_avg_price * 100) if position_avg_price > 0 else 0.0
|
||||
# 매수 신호 체크
|
||||
if position_qty == 0 and pending_sell_signal is None:
|
||||
prev_rsi = indicators[i - 3].get("rsi") if i >= 3 else None
|
||||
|
||||
trade = {
|
||||
"date": bar_date,
|
||||
"action": "SELL",
|
||||
"ticker": ticker,
|
||||
"price": close,
|
||||
"quantity": position_qty,
|
||||
"amount": round(amount, 2),
|
||||
"commission": round(commission, 2),
|
||||
"reason": sell_signal,
|
||||
"pnl": round(pnl, 2),
|
||||
"pnl_pct": round(pnl_pct, 2),
|
||||
}
|
||||
trades.append(trade)
|
||||
cash += amount - commission
|
||||
position_qty = 0
|
||||
position_avg_price = 0.0
|
||||
position_high = 0.0
|
||||
|
||||
# 포지션 없을 때: 매수 조건 체크
|
||||
if position_qty == 0:
|
||||
prev_rsi = indicators[i - 3].get("rsi") if i >= 3 else None
|
||||
|
||||
buy_signal = check_us_buy_signal(
|
||||
rsi=ind.get("rsi", 50),
|
||||
close=close,
|
||||
bb_upper=ind.get("bb_upper", close),
|
||||
bb_middle=ind.get("bb_middle", close),
|
||||
bb_lower=ind.get("bb_lower", close),
|
||||
avg_volume_20d=ind.get("avg_volume_20d", 0),
|
||||
current_volume=bar.get("volume", 0),
|
||||
current_price_usd=close,
|
||||
rsi_prev=prev_rsi,
|
||||
)
|
||||
|
||||
if buy_signal:
|
||||
# 전체 자본의 최대 95% 투입 (여유 현금 확보)
|
||||
buy_amount = min(cash * 0.95, cash)
|
||||
if buy_amount > 10: # 최소 $10 이상
|
||||
quantity = int(buy_amount / close)
|
||||
if quantity > 0:
|
||||
cost = quantity * close
|
||||
commission = cost * US_COMMISSION_RATE
|
||||
total_cost = cost + commission
|
||||
|
||||
if total_cost <= cash:
|
||||
cash -= total_cost
|
||||
position_qty = quantity
|
||||
position_avg_price = close
|
||||
position_high = close
|
||||
|
||||
trade = {
|
||||
"date": bar_date,
|
||||
"action": "BUY",
|
||||
"ticker": ticker,
|
||||
"price": close,
|
||||
"quantity": quantity,
|
||||
"amount": round(cost, 2),
|
||||
"commission": round(commission, 2),
|
||||
"reason": buy_signal.get("reason", "buy_signal"),
|
||||
"pnl": 0.0,
|
||||
"pnl_pct": 0.0,
|
||||
}
|
||||
trades.append(trade)
|
||||
buy_signal = check_us_buy_signal(
|
||||
rsi=ind.get("rsi", 50),
|
||||
close=close,
|
||||
bb_upper=ind.get("bb_upper", close),
|
||||
bb_middle=ind.get("bb_middle", close),
|
||||
bb_lower=ind.get("bb_lower", close),
|
||||
avg_volume_20d=ind.get("avg_volume_20d", 0),
|
||||
current_volume=bar.get("volume", 0),
|
||||
current_price_usd=close,
|
||||
rsi_prev=prev_rsi,
|
||||
)
|
||||
if buy_signal:
|
||||
pending_buy_signal = buy_signal
|
||||
|
||||
# 일별 자산 기록
|
||||
position_value = position_qty * close if position_qty > 0 else 0.0
|
||||
@@ -337,19 +381,20 @@ async def run_us_backtest(
|
||||
"position_value": round(position_value, 2),
|
||||
})
|
||||
|
||||
# 4) 미청산 포지션 강제 청산 (백테스트 종료)
|
||||
# 4) 미청산 포지션 강제 청산 (백테스트 종료, 슬리피지 적용)
|
||||
if position_qty > 0 and daily_data:
|
||||
last_close = daily_data[-1]["close"]
|
||||
amount = position_qty * last_close
|
||||
exec_price = _apply_us_sell_slippage(last_close)
|
||||
amount = position_qty * exec_price
|
||||
commission = amount * US_COMMISSION_RATE
|
||||
pnl = (last_close - position_avg_price) * position_qty - commission
|
||||
pnl_pct = ((last_close - position_avg_price) / position_avg_price * 100) if position_avg_price > 0 else 0.0
|
||||
pnl = (exec_price - position_avg_price) * position_qty - commission
|
||||
pnl_pct = ((exec_price - position_avg_price) / position_avg_price * 100) if position_avg_price > 0 else 0.0
|
||||
|
||||
trades.append({
|
||||
"date": daily_data[-1]["date"],
|
||||
"action": "SELL",
|
||||
"ticker": ticker,
|
||||
"price": last_close,
|
||||
"price": round(exec_price, 4),
|
||||
"quantity": position_qty,
|
||||
"amount": round(amount, 2),
|
||||
"commission": round(commission, 2),
|
||||
|
||||
Reference in New Issue
Block a user