217 lines
9.8 KiB
Python
217 lines
9.8 KiB
Python
import data_manager
|
|
import strategy
|
|
import pandas as pd
|
|
import numpy as np
|
|
import config
|
|
from typing import Optional, Dict, List, Any
|
|
|
|
# 비교에 사용할 티커 수 (기본: 전체 티커). 테스트 시간이 걸릴 수 있습니다.
|
|
# 이전에는 샘플 50으로 제한했으나, 전체 비교를 위해 제한을 제거합니다.
|
|
MAX_TICKERS = None
|
|
|
|
# Load tickers
|
|
tickers = {}
|
|
if config.TARGET_MARKET in ["KR", "BOTH"]:
|
|
tickers.update(data_manager.get_kr_tickers(config.KR_MARKET_CAP_START, config.KR_MARKET_CAP_END))
|
|
if config.TARGET_MARKET in ["US", "BOTH"]:
|
|
tickers.update(data_manager.get_us_tickers(config.US_MARKET_CAP_START, config.US_MARKET_CAP_END))
|
|
|
|
# limit
|
|
if MAX_TICKERS is None:
|
|
selected = list(tickers.keys())
|
|
else:
|
|
selected = list(tickers.keys())[:MAX_TICKERS]
|
|
print('Selected tickers:', len(selected))
|
|
|
|
# Helper: load data
|
|
def prepare_data(ticker: str) -> Optional[pd.DataFrame]:
|
|
"""티커의 주가 데이터를 로드하고 전처리합니다.
|
|
|
|
Args:
|
|
ticker: 종목 티커 코드
|
|
|
|
Returns:
|
|
전처리된 데이터프레임, 로드 실패 시 None
|
|
"""
|
|
s = tickers[ticker]
|
|
df = data_manager.get_stock_data(ticker, (pd.to_datetime(config.BACKTEST_START_DATE) - pd.DateOffset(years=config.DATA_HISTORY_YEARS)).strftime('%Y-%m-%d'), config.BACKTEST_END_DATE)
|
|
if df.empty:
|
|
return None
|
|
try:
|
|
df['LAST_PEAK'] = strategy.compute_last_peak_series(df, config.STOPLOSS_PEAK_FIND_PERIOD)
|
|
except (ValueError, KeyError) as e:
|
|
print(f'LAST_PEAK fail {ticker} (데이터 부족): {e}')
|
|
return None
|
|
except Exception as e:
|
|
print(f'LAST_PEAK 예상치 못한 오류 {ticker}: {type(e).__name__}: {e}')
|
|
raise
|
|
dfc = df.dropna()
|
|
return dfc
|
|
|
|
# Load all data
|
|
stock_data = {}
|
|
for t in selected:
|
|
df = prepare_data(t)
|
|
if df is not None and len(df) > 10:
|
|
stock_data[t] = df
|
|
|
|
print('Loaded data for', len(stock_data), 'tickers')
|
|
|
|
# Method A: current vectorized buy signals
|
|
buy_signals_history = {}
|
|
for t, df in stock_data.items():
|
|
buy_signals_history[t] = strategy.compute_buy_signals_vectorized(df)
|
|
|
|
# Simple backtest runner used for both methods
|
|
from datetime import datetime
|
|
|
|
def run_sim(use_vectorized: bool = True) -> Dict[str, Any]:
|
|
"""백테스트 시뮬레이션을 실행합니다.
|
|
|
|
Args:
|
|
use_vectorized: True이면 벡터화된 신호 사용, False이면 반복적 계산
|
|
|
|
Returns:
|
|
백테스트 결과 딕셔너리 (total_return, trades, trade_log)
|
|
"""
|
|
capital = config.BACKTEST_CAPITAL
|
|
portfolio = {}
|
|
trade_log = []
|
|
daily_portfolio_value = []
|
|
|
|
simulation_dates = pd.date_range(config.BACKTEST_START_DATE, config.BACKTEST_END_DATE)
|
|
|
|
tickers_list = list(stock_data.keys())
|
|
|
|
for today_date in simulation_dates:
|
|
today_str = today_date.strftime('%Y-%m-%d')
|
|
# sells
|
|
for ticker in list(portfolio.keys()):
|
|
df = stock_data[ticker]
|
|
try:
|
|
pos = df.index.get_loc(today_date)
|
|
except KeyError:
|
|
continue
|
|
if pos == 0:
|
|
continue
|
|
today_data = df.iloc[pos]
|
|
position = portfolio[ticker]
|
|
position['highest_price'] = max(position['highest_price'], today_data['High'])
|
|
sell_signal = None
|
|
sell_ratio = 0.0
|
|
# same inline sell logic as backtester
|
|
if config.USE_TECHNICAL_STOPLOSS and today_data['Low'] <= position['stop_loss_price']:
|
|
sell_signal = 'STOP_LOSS_TECH'; sell_ratio=1.0
|
|
elif config.USE_FIXED_PCT_STOPLOSS:
|
|
fixed_sl = position['buy_price'] * (1 - config.FIXED_STOPLOSS_PCT / 100)
|
|
if today_data['Low'] <= fixed_sl:
|
|
sell_signal = 'STOP_LOSS_FIXED'; sell_ratio=1.0
|
|
if not sell_signal and not position['partial_sold']:
|
|
profit_target = position['buy_price'] * (1 + config.PROFIT_TAKE_PCT)
|
|
if today_data['High'] >= profit_target:
|
|
sell_signal = 'PROFIT_TAKE'; sell_ratio = config.PROFIT_TAKE_SELL_RATIO
|
|
if not sell_signal and config.USE_TREND_EXIT_STRATEGY and pos>0:
|
|
yesterday_data = df.iloc[pos-1]
|
|
ma_short = f'MA_{config.TREND_EXIT_MA_SHORT}'; ma_long = f'MA_{config.TREND_EXIT_MA_LONG}'
|
|
if ma_short in df.columns and ma_long in df.columns:
|
|
is_dead_cross = (yesterday_data[ma_short] > yesterday_data[ma_long]) and (today_data[ma_short] < today_data[ma_long])
|
|
if is_dead_cross:
|
|
sell_signal = 'TREND_EXIT_DC'; sell_ratio = 1.0
|
|
if today_str == '2023-12-29':
|
|
sell_signal = 'EXPIRED'; sell_ratio = 1.0
|
|
if sell_signal:
|
|
sell_price = today_data['Close']
|
|
shares_to_sell = position['shares'] * sell_ratio
|
|
if sell_ratio < 1.0:
|
|
shares_to_sell = np.floor(shares_to_sell)
|
|
if shares_to_sell <= 0:
|
|
continue
|
|
sell_value = shares_to_sell * sell_price * (1 - config.TRANSACTION_FEE_PCT)
|
|
capital += sell_value
|
|
position['shares'] -= shares_to_sell
|
|
profit_pct = (sell_price - position['buy_price']) / position['buy_price']
|
|
trade_log.append({'ticker':ticker, 'entry_date': position['buy_date'], 'exit_date': today_date, 'entry_price': position['buy_price'], 'exit_price': sell_price, 'shares_sold': shares_to_sell, 'profit_pct': profit_pct, 'exit_reason': sell_signal, 'signal_type': position.get('signal_type')})
|
|
if sell_signal == 'PROFIT_TAKE':
|
|
position['partial_sold'] = True
|
|
if position['shares'] < 0.01:
|
|
del portfolio[ticker]
|
|
else:
|
|
current_val = position['shares'] * today_data['Close']
|
|
# buys
|
|
if len(portfolio) < config.MAX_PORTFOLIO_SIZE and capital > 0:
|
|
investment_per_stock = config.BACKTEST_CAPITAL * config.INVESTMENT_PER_STOCK_PCT
|
|
if capital >= investment_per_stock:
|
|
for ticker in tickers_list:
|
|
if ticker in portfolio or len(portfolio) >= config.MAX_PORTFOLIO_SIZE:
|
|
continue
|
|
if ticker not in stock_data:
|
|
continue
|
|
df = stock_data[ticker]
|
|
try:
|
|
pos = df.index.get_loc(today_date)
|
|
except KeyError:
|
|
continue
|
|
if pos == 0:
|
|
continue
|
|
if use_vectorized:
|
|
if ticker not in buy_signals_history:
|
|
continue
|
|
signals_df = buy_signals_history[ticker]
|
|
try:
|
|
row = signals_df.loc[today_date]
|
|
except KeyError:
|
|
continue
|
|
buy_signal = row['BUY_SIGNAL']; stop_loss = row['STOP_LOSS_PRICE']; signal_type = row['SIGNAL_TYPE']
|
|
else:
|
|
today = df.iloc[pos]; yesterday = df.iloc[pos-1]; history = df.iloc[:pos+1]
|
|
ok, stop_loss, signal_type = strategy.check_buy_signal(today, yesterday, history)
|
|
buy_signal = ok
|
|
if today_str == '2023-12-29':
|
|
buy_signal = False; stop_loss = None; signal_type = None
|
|
if buy_signal:
|
|
buy_price = df.iloc[pos]['Close']
|
|
if stop_loss is None or stop_loss >= buy_price:
|
|
continue
|
|
shares_to_buy = (investment_per_stock * (1 - config.TRANSACTION_FEE_PCT)) / buy_price
|
|
shares_to_buy = np.floor(shares_to_buy)
|
|
if shares_to_buy<=0: continue
|
|
buy_value = shares_to_buy * buy_price * (1 + config.TRANSACTION_FEE_PCT)
|
|
if capital < buy_value: continue
|
|
capital -= buy_value
|
|
portfolio[ticker] = {'buy_price': buy_price, 'shares': shares_to_buy, 'stop_loss_price': stop_loss, 'highest_price': buy_price, 'partial_sold': False, 'buy_date': today_date, 'signal_type': signal_type}
|
|
if len(portfolio) >= config.MAX_PORTFOLIO_SIZE:
|
|
break
|
|
# daily equity
|
|
final = capital
|
|
for t,p in portfolio.items():
|
|
df = stock_data[t]
|
|
if today_date in df.index:
|
|
final += p['shares'] * df.loc[today_date]['Close']
|
|
daily_portfolio_value.append((today_date, final))
|
|
# compute stats
|
|
df_eq = pd.DataFrame(daily_portfolio_value, columns=['date','equity']).set_index('date')
|
|
total_return = (df_eq['equity'].iloc[-1] / config.BACKTEST_CAPITAL) - 1
|
|
trades = len(trade_log)
|
|
return {'total_return': total_return, 'trades': trades, 'trade_log': trade_log}
|
|
|
|
print('Running vectorized method...')
|
|
res_vec = run_sim(use_vectorized=True)
|
|
print('Vectorized:', res_vec['total_return'], 'trades=', res_vec['trades'])
|
|
|
|
print('Running iterative method...')
|
|
res_iter = run_sim(use_vectorized=False)
|
|
print('Iterative:', res_iter['total_return'], 'trades=', res_iter['trades'])
|
|
|
|
# Compare trade lists (entry/exit pairs)
|
|
|
|
vec_set = set((t['ticker'], t['entry_date'].strftime('%Y-%m-%d'), t['exit_date'].strftime('%Y-%m-%d')) for t in res_vec['trade_log'])
|
|
iter_set = set((t['ticker'], t['entry_date'].strftime('%Y-%m-%d'), t['exit_date'].strftime('%Y-%m-%d')) for t in res_iter['trade_log'])
|
|
|
|
only_vec = vec_set - iter_set
|
|
only_iter = iter_set - vec_set
|
|
|
|
print('Only in vectorized trades:', len(only_vec))
|
|
for x in list(only_vec)[:10]: print(' ', x)
|
|
print('Only in iterative trades:', len(only_iter))
|
|
for x in list(only_iter)[:10]: print(' ', x)
|