Files
stockTradingGNN/src/models/backtester.py
T
2026-05-26 12:43:47 +02:00

178 lines
6.4 KiB
Python

import logging
from datetime import datetime
from typing import Dict, List, Tuple
import pandas as pd
import torch
from config import config
from src.models.gnn_model import CorporateActionAwareGNN
logger = logging.getLogger(__name__)
class GNNBacktester:
def __init__(
self,
model: CorporateActionAwareGNN,
price_data: Dict,
initial_capital: float = config.INITIAL_CAPITAL,
):
self.model = model
self.price_data = price_data
self.initial_capital = initial_capital
self.portfolio_value = initial_capital
self.portfolio = {} # {ticker: shares}
self.trade_log = []
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def run_backtest(self, dataset: List) -> Tuple[pd.Series, pd.DataFrame]:
"""
Run backtest on the given dataset
Parameters:
dataset: List of PyG Data objects
Returns:
Tuple of (portfolio_values, trade_log)
"""
portfolio_values = []
dates = []
for data in dataset:
date = data.date
current_tickers = data.tickers
# Get current prices
current_prices = {}
for ticker in current_tickers:
if ticker in self.price_data and date in self.price_data[ticker].index:
current_prices[ticker] = self.price_data[ticker].loc[date][
"Adj Close"
]
# Calculate current portfolio value
current_value = sum(
shares * current_prices[ticker]
for ticker, shares in self.portfolio.items()
if ticker in current_prices
)
cash = self.portfolio_value - current_value
current_portfolio_value = current_value + cash
# Store portfolio value
portfolio_values.append(current_portfolio_value)
dates.append(date)
# Get predictions from GNN
with torch.no_grad():
data = data.to(self.device)
predictions = self.model(data).squeeze().cpu().numpy()
# Create trading signals
signals = {}
for i, ticker in enumerate(current_tickers):
if ticker in current_prices:
# Get corporate action flag (last feature)
corporate_action_flag = data.x[i, -1].item()
# Buy if predicted return > threshold and no upcoming corporate action
if predictions[i] > 0.005 and corporate_action_flag == 0:
signals[ticker] = "buy"
# Sell if predicted return < threshold or upcoming corporate action
elif predictions[i] < -0.005 or corporate_action_flag > 0:
signals[ticker] = "sell"
# Execute trades
for ticker, signal in signals.items():
if signal == "buy" and cash > 0:
# Buy with 10% of available cash
price = current_prices[ticker]
shares_to_buy = int(
(cash * 0.1) / (price * (1 + config.TRANSACTION_COST))
)
if shares_to_buy > 0:
cost = shares_to_buy * price * (1 + config.TRANSACTION_COST)
self.portfolio[ticker] = (
self.portfolio.get(ticker, 0) + shares_to_buy
)
cash -= cost
self.trade_log.append(
(date, ticker, "buy", shares_to_buy, price)
)
logger.debug(
f"Bought {shares_to_buy} shares of {ticker} at {price:.2f}"
)
elif signal == "sell" and ticker in self.portfolio:
# Sell all shares
shares = self.portfolio.pop(ticker)
proceeds = (
shares * current_prices[ticker] * (1 - config.TRANSACTION_COST)
)
cash += proceeds
self.trade_log.append(
(date, ticker, "sell", shares, current_prices[ticker])
)
logger.debug(
f"Sold {shares} shares of {ticker} at {current_prices[ticker]:.2f}"
)
# Update portfolio value
new_value = sum(
shares * current_prices[ticker]
for ticker, shares in self.portfolio.items()
if ticker in current_prices
)
self.portfolio_value = new_value + cash
# Create portfolio value series
portfolio_series = pd.Series(portfolio_values, index=dates)
# Create trade log DataFrame
if self.trade_log:
trade_log_df = pd.DataFrame(self.trade_log)
trade_log_df.columns = ["date", "ticker", "action", "shares", "price"]
else:
trade_log_df = pd.DataFrame()
trade_log_df.columns = ["date", "ticker", "action", "shares", "price"]
return portfolio_series, trade_log_df
def get_portfolio_composition(self, date: datetime) -> Dict[str, float]:
"""
Get portfolio composition at a specific date
Parameters:
date: Date to get composition for
Returns:
Dictionary of {ticker: weight} where weight is the percentage of portfolio
"""
# Get current prices
current_prices = {}
for ticker in self.portfolio:
if ticker in self.price_data and date in self.price_data[ticker].index:
current_prices[ticker] = self.price_data[ticker].loc[date]["Adj Close"]
# Calculate current value
current_value = sum(
shares * current_prices[ticker]
for ticker, shares in self.portfolio.items()
if ticker in current_prices
)
cash = self.portfolio_value - current_value
# Calculate weights
composition = {}
for ticker, shares in self.portfolio.items():
if ticker in current_prices:
composition[ticker] = (
shares * current_prices[ticker]
) / self.portfolio_value
# Add cash
composition["cash"] = cash / self.portfolio_value
return composition