From 15b9f90ae267a6038298c75b88c5127da464fda0 Mon Sep 17 00:00:00 2001 From: Fried-MK Date: Wed, 25 Mar 2026 21:16:23 +0800 Subject: [PATCH] Add TradingAgents backtesting strategy implementation Implement backtesting strategy using TradingAgents with data verification and caching. --- backtest | 276 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 backtest diff --git a/backtest b/backtest new file mode 100644 index 00000000..e3210bea --- /dev/null +++ b/backtest @@ -0,0 +1,276 @@ +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)