최초 프로젝트 업로드 (Script Auto Commit)

This commit is contained in:
2025-12-03 22:36:00 +09:00
commit 4745ab3c28
33 changed files with 4251 additions and 0 deletions

View File

@@ -0,0 +1,216 @@
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)