277 lines
9.8 KiB
Plaintext
277 lines
9.8 KiB
Plaintext
import backtrader as bt
|
|
import pandas as pd
|
|
import yfinance as yf
|
|
import json
|
|
import os
|
|
import shutil
|
|
from datetime import datetime, timedelta
|
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
|
from tradingagents.default_config import DEFAULT_CONFIG
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
class TradingAgentsStrategy(bt.Strategy):
|
|
"""Strategy that uses TradingAgents for decision making"""
|
|
|
|
def __init__(self, trading_agent, ticker, backtest_config):
|
|
self.trading_agent = trading_agent
|
|
self.ticker = ticker
|
|
self.backtest_config = backtest_config
|
|
self.decisions = {}
|
|
self.trade_count = 0
|
|
self.data_checks = {}
|
|
|
|
def next(self):
|
|
# Get current date
|
|
current_date = self.datas[0].datetime.date(0)
|
|
date_str = current_date.strftime("%Y-%m-%d")
|
|
|
|
# Verify data range to avoid look-ahead bias
|
|
self.verify_data_range(date_str)
|
|
|
|
# Get decision from TradingAgents
|
|
if date_str not in self.decisions:
|
|
print(f"Processing date: {date_str}")
|
|
try:
|
|
_, decision = self.trading_agent.propagate(self.ticker, date_str)
|
|
self.decisions[date_str] = decision
|
|
print(f"Decision: {decision}")
|
|
except Exception as e:
|
|
print(f"Error getting decision for {date_str}: {e}")
|
|
self.decisions[date_str] = "HOLD"
|
|
|
|
decision = self.decisions[date_str]
|
|
|
|
# Execute trade based on decision
|
|
if decision == "BUY" and not self.position:
|
|
# Buy with 100% of available cash
|
|
size = int(self.broker.getcash() / self.data.close[0])
|
|
if size > 0:
|
|
self.buy(size=size)
|
|
self.trade_count += 1
|
|
print(f"BUY {self.ticker} on {date_str} at ${self.data.close[0]:.2f}")
|
|
|
|
elif decision == "SELL" and self.position:
|
|
# Sell all positions
|
|
self.sell(size=self.position.size)
|
|
self.trade_count += 1
|
|
print(f"SELL {self.ticker} on {date_str} at ${self.data.close[0]:.2f}")
|
|
|
|
def verify_data_range(self, date_str):
|
|
"""Verify that data range is correct to avoid look-ahead bias"""
|
|
current_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
|
|
# Check if we already verified this date
|
|
if date_str in self.data_checks:
|
|
return
|
|
|
|
# Verify data feed doesn't contain future data
|
|
data_end_date = self.datas[0].datetime.date(-1)
|
|
if data_end_date > current_date:
|
|
print(f"⚠️ Warning: Data feed contains future data beyond {date_str}")
|
|
|
|
self.data_checks[date_str] = True
|
|
|
|
def clean_cache():
|
|
"""Clean cache to avoid look-ahead bias"""
|
|
print("\n=== Cleaning cache to avoid look-ahead bias ===")
|
|
|
|
# Clean yfinance cache
|
|
yfinance_cache = "yfinance_cache"
|
|
if os.path.exists(yfinance_cache):
|
|
shutil.rmtree(yfinance_cache)
|
|
print(f"✓ Cleaned yfinance cache: {yfinance_cache}")
|
|
|
|
# Clean dataflows cache
|
|
dataflows_cache = "dataflows/data_cache"
|
|
if os.path.exists(dataflows_cache):
|
|
shutil.rmtree(dataflows_cache)
|
|
print(f"✓ Cleaned dataflows cache: {dataflows_cache}")
|
|
|
|
# Clean backtest results (optional)
|
|
# backtest_results = "backtest_results"
|
|
# if os.path.exists(backtest_results):
|
|
# shutil.rmtree(backtest_results)
|
|
# print(f"✓ Cleaned backtest results: {backtest_results}")
|
|
|
|
def run_backtest(ticker, start_date, end_date, initial_cash=100000, clean_cache_flag=True):
|
|
"""Run backtest for a given ticker and date range"""
|
|
|
|
# Clean cache to avoid look-ahead bias
|
|
if clean_cache_flag:
|
|
clean_cache()
|
|
|
|
# Create Cerebro engine
|
|
cerebro = bt.Cerebro()
|
|
|
|
# Set initial cash
|
|
cerebro.broker.setcash(initial_cash)
|
|
|
|
# Add strategy
|
|
config = DEFAULT_CONFIG.copy()
|
|
config["llm_provider"] = "openrouter"
|
|
config["deep_think_llm"] = "deepseek/deepseek-chat"
|
|
config["quick_think_llm"] = "openai/gpt-4o-mini"
|
|
config["max_debate_rounds"] = 2
|
|
|
|
# Verify date range
|
|
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
|
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
|
|
|
if start_dt >= end_dt:
|
|
raise ValueError("Start date must be before end date")
|
|
|
|
if end_dt > datetime.now():
|
|
raise ValueError("End date cannot be in the future")
|
|
|
|
trading_agent = TradingAgentsGraph(debug=False, config=config)
|
|
|
|
backtest_config = {
|
|
"ticker": ticker,
|
|
"start_date": start_date,
|
|
"end_date": end_date,
|
|
"initial_cash": initial_cash,
|
|
"clean_cache": clean_cache_flag
|
|
}
|
|
|
|
cerebro.addstrategy(TradingAgentsStrategy, trading_agent=trading_agent, ticker=ticker, backtest_config=backtest_config)
|
|
|
|
# Get historical data from yfinance
|
|
print("\n=== Fetching historical data ===")
|
|
data = yf.download(ticker, start=start_date, end=end_date)
|
|
|
|
# Verify data quality
|
|
if data.empty:
|
|
raise ValueError(f"No data found for {ticker} between {start_date} and {end_date}")
|
|
|
|
print(f"✓ Data fetched: {len(data)} trading days")
|
|
print(f"✓ Date range: {data.index.min().date()} to {data.index.max().date()}")
|
|
|
|
# Convert to backtrader data feed
|
|
data_feed = bt.feeds.PandasData(
|
|
dataname=data,
|
|
datetime=0,
|
|
high=1,
|
|
low=2,
|
|
open=3,
|
|
close=4,
|
|
volume=5,
|
|
openinterest=-1
|
|
)
|
|
|
|
# Add data feed to cerebro
|
|
cerebro.adddata(data_feed, name=ticker)
|
|
|
|
# Add analyzers
|
|
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
|
|
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
|
|
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
|
|
cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='annual')
|
|
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
|
|
cerebro.addanalyzer(bt.analyzers.PositionsValue, _name='positions')
|
|
|
|
# Run backtest
|
|
print(f"\n=== Starting Backtest for {ticker} ===")
|
|
print(f"Date range: {start_date} to {end_date}")
|
|
print(f"Initial cash: ${initial_cash:.2f}")
|
|
print(f"LLM Provider: {config['llm_provider']}")
|
|
print(f"Models: Deep={config['deep_think_llm']}, Quick={config['quick_think_llm']}")
|
|
|
|
results = cerebro.run()
|
|
|
|
# Get results
|
|
strategy = results[0]
|
|
final_value = cerebro.broker.getvalue()
|
|
total_return = ((final_value - initial_cash) / initial_cash) * 100
|
|
|
|
# Get analyzer results
|
|
sharpe = strategy.analyzers.sharpe.get_analysis()
|
|
drawdown = strategy.analyzers.drawdown.get_analysis()
|
|
trades = strategy.analyzers.trades.get_analysis()
|
|
annual = strategy.analyzers.annual.get_analysis()
|
|
returns = strategy.analyzers.returns.get_analysis()
|
|
|
|
# Calculate additional metrics
|
|
total_trades = trades.get('total', {}).get('total', 0)
|
|
won_trades = trades.get('won', {}).get('total', 0)
|
|
win_rate = won_trades / max(total_trades, 1) * 100
|
|
|
|
# Print results
|
|
print(f"\n=== Backtest Results ===")
|
|
print(f"Final portfolio value: ${final_value:.2f}")
|
|
print(f"Total return: {total_return:.2f}%")
|
|
print(f"Daily return: {returns.get('rnorm', 0) * 100:.4f}%")
|
|
print(f"Sharpe Ratio: {sharpe.get('sharperatio', 'N/A'):.2f}")
|
|
print(f"Max Drawdown: {drawdown.get('max', {}).get('drawdown', 'N/A'):.2f}%")
|
|
print(f"Total trades: {total_trades}")
|
|
print(f"Win rate: {win_rate:.2f}%")
|
|
print(f"Average trade duration: {trades.get('len', {}).get('average', 'N/A'):.1f} days")
|
|
|
|
# Save results
|
|
save_results(ticker, start_date, end_date, {
|
|
"initial_cash": initial_cash,
|
|
"final_value": final_value,
|
|
"total_return": total_return,
|
|
"daily_return": returns.get('rnorm', 0),
|
|
"sharpe_ratio": sharpe.get('sharperatio', None),
|
|
"max_drawdown": drawdown.get('max', {}).get('drawdown', None),
|
|
"total_trades": total_trades,
|
|
"won_trades": won_trades,
|
|
"win_rate": win_rate,
|
|
"average_trade_duration": trades.get('len', {}).get('average', None),
|
|
"decisions": strategy.decisions,
|
|
"config": backtest_config
|
|
})
|
|
|
|
# Plot results
|
|
print("\n=== Generating backtest chart ===")
|
|
cerebro.plot(style='candlestick')
|
|
|
|
def save_results(ticker, start_date, end_date, results):
|
|
"""Save backtest results to file"""
|
|
results_dir = f"backtest_results/{ticker}/"
|
|
os.makedirs(results_dir, exist_ok=True)
|
|
|
|
filename = f"backtest_{start_date}_{end_date}.json"
|
|
filepath = os.path.join(results_dir, filename)
|
|
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
json.dump(results, f, indent=4, default=str)
|
|
|
|
print(f"Results saved to: {filepath}")
|
|
|
|
def run_multiple_backtests(ticker_list, start_date, end_date, initial_cash=100000):
|
|
"""Run backtests for multiple tickers"""
|
|
all_results = {}
|
|
|
|
for ticker in ticker_list:
|
|
print(f"\n{'='*60}")
|
|
print(f"Running backtest for {ticker}")
|
|
print(f"{'='*60}")
|
|
|
|
try:
|
|
# Run backtest without cleaning cache for subsequent tickers
|
|
clean_cache_flag = (ticker == ticker_list[0])
|
|
run_backtest(ticker, start_date, end_date, initial_cash, clean_cache_flag)
|
|
except Exception as e:
|
|
print(f"Error running backtest for {ticker}: {e}")
|
|
all_results[ticker] = {"error": str(e)}
|
|
|
|
return all_results
|
|
|
|
if __name__ == "__main__":
|
|
# Define parameters
|
|
ticker = "NVDA"
|
|
start_date = "2024-01-01"
|
|
end_date = "2024-03-29"
|
|
initial_cash = 100000
|
|
|
|
# Run backtest
|
|
run_backtest(ticker, start_date, end_date, initial_cash)
|
|
|
|
# Example: Run multiple backtests
|
|
# tickers = ["NVDA", "AAPL", "MSFT"]
|
|
# run_multiple_backtests(tickers, start_date, end_date, initial_cash)
|