495 lines
15 KiB
Python
495 lines
15 KiB
Python
"""
|
|
Integration with TradingAgents framework.
|
|
|
|
This module provides integration between the backtesting framework
|
|
and TradingAgentsGraph, allowing backtesting of multi-agent strategies.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Dict, List, Optional, Any
|
|
from decimal import Decimal
|
|
|
|
import pandas as pd
|
|
|
|
from .strategy import BaseStrategy, Signal, Position
|
|
from .backtester import Backtester, BacktestResults
|
|
from .config import BacktestConfig
|
|
from .exceptions import IntegrationError
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TradingAgentsStrategy(BaseStrategy):
|
|
"""
|
|
Wrapper strategy for TradingAgentsGraph.
|
|
|
|
This class adapts TradingAgentsGraph to work with the backtesting framework.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
trading_graph: Any,
|
|
lookback_days: int = 30,
|
|
):
|
|
"""
|
|
Initialize TradingAgents strategy.
|
|
|
|
Args:
|
|
trading_graph: TradingAgentsGraph instance
|
|
lookback_days: Number of days of historical data to provide
|
|
"""
|
|
super().__init__(name="TradingAgents")
|
|
self.trading_graph = trading_graph
|
|
self.lookback_days = lookback_days
|
|
self.last_signals: Dict[str, str] = {} # ticker -> last action
|
|
|
|
logger.info("TradingAgentsStrategy initialized")
|
|
|
|
def generate_signals(
|
|
self,
|
|
timestamp: datetime,
|
|
data: Dict[str, pd.DataFrame],
|
|
positions: Dict[str, Position],
|
|
portfolio_value: Decimal,
|
|
) -> List[Signal]:
|
|
"""
|
|
Generate signals using TradingAgentsGraph.
|
|
|
|
Args:
|
|
timestamp: Current timestamp
|
|
data: Historical data for all tickers
|
|
positions: Current positions
|
|
portfolio_value: Current portfolio value
|
|
|
|
Returns:
|
|
List of signals
|
|
"""
|
|
signals = []
|
|
|
|
for ticker, df in data.items():
|
|
try:
|
|
# Run TradingAgentsGraph
|
|
final_state, processed_signal = self.trading_graph.propagate(
|
|
company_name=ticker,
|
|
trade_date=timestamp.strftime('%Y-%m-%d'),
|
|
)
|
|
|
|
# Parse the processed signal
|
|
action = self._parse_signal(processed_signal)
|
|
|
|
# Only generate signal if action changed or is new
|
|
last_action = self.last_signals.get(ticker, 'hold')
|
|
|
|
if action != last_action:
|
|
# Get confidence from final state if available
|
|
confidence = self._extract_confidence(final_state)
|
|
|
|
signal = Signal(
|
|
ticker=ticker,
|
|
timestamp=timestamp,
|
|
action=action,
|
|
confidence=confidence,
|
|
metadata={
|
|
'final_decision': final_state.get('final_trade_decision', ''),
|
|
'investment_plan': final_state.get('investment_plan', ''),
|
|
}
|
|
)
|
|
|
|
signals.append(signal)
|
|
self.last_signals[ticker] = action
|
|
|
|
logger.debug(f"{ticker}: {action} (confidence: {confidence:.2f})")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate signal for {ticker}: {e}")
|
|
continue
|
|
|
|
return signals
|
|
|
|
def _parse_signal(self, processed_signal: str) -> str:
|
|
"""
|
|
Parse the processed signal from TradingAgentsGraph.
|
|
|
|
Args:
|
|
processed_signal: Processed signal string
|
|
|
|
Returns:
|
|
Action ('buy', 'sell', or 'hold')
|
|
"""
|
|
# Convert TradingAgents signal to backtest action
|
|
signal_lower = processed_signal.lower()
|
|
|
|
if 'buy' in signal_lower or 'long' in signal_lower:
|
|
return 'buy'
|
|
elif 'sell' in signal_lower or 'short' in signal_lower:
|
|
return 'sell'
|
|
else:
|
|
return 'hold'
|
|
|
|
def _extract_confidence(self, final_state: Dict[str, Any]) -> float:
|
|
"""
|
|
Extract confidence level from final state.
|
|
|
|
Args:
|
|
final_state: Final state from TradingAgentsGraph
|
|
|
|
Returns:
|
|
Confidence level (0.0 to 1.0)
|
|
"""
|
|
# This is a placeholder - you might want to parse the actual
|
|
# confidence from the judge's decision or other metrics
|
|
try:
|
|
# Look for confidence indicators in the decision
|
|
decision = final_state.get('final_trade_decision', '').lower()
|
|
|
|
if 'high confidence' in decision or 'strong' in decision:
|
|
return 0.9
|
|
elif 'moderate' in decision or 'medium' in decision:
|
|
return 0.7
|
|
elif 'low' in decision or 'weak' in decision:
|
|
return 0.5
|
|
else:
|
|
return 0.7 # Default moderate confidence
|
|
|
|
except Exception:
|
|
return 0.7
|
|
|
|
def on_fill(self, fill: Any) -> None:
|
|
"""
|
|
Called when an order is filled.
|
|
|
|
Can be used to update TradingAgents memories with outcomes.
|
|
|
|
Args:
|
|
fill: Fill information
|
|
"""
|
|
# TODO: Implement reflection mechanism
|
|
# This could call trading_graph.reflect_and_remember()
|
|
pass
|
|
|
|
def finalize(self) -> None:
|
|
"""Called at end of backtest."""
|
|
logger.info("TradingAgents strategy finalized")
|
|
|
|
|
|
def backtest_trading_agents(
|
|
trading_graph: Any,
|
|
tickers: List[str],
|
|
start_date: str,
|
|
end_date: str,
|
|
initial_capital: float = 100000.0,
|
|
commission: float = 0.001,
|
|
slippage: float = 0.0005,
|
|
benchmark: str = 'SPY',
|
|
**kwargs
|
|
) -> BacktestResults:
|
|
"""
|
|
Backtest a TradingAgentsGraph strategy.
|
|
|
|
Args:
|
|
trading_graph: TradingAgentsGraph instance
|
|
tickers: List of tickers to trade
|
|
start_date: Start date (YYYY-MM-DD)
|
|
end_date: End date (YYYY-MM-DD)
|
|
initial_capital: Starting capital
|
|
commission: Commission rate
|
|
slippage: Slippage rate
|
|
benchmark: Benchmark ticker
|
|
**kwargs: Additional config parameters
|
|
|
|
Returns:
|
|
BacktestResults
|
|
|
|
Example:
|
|
>>> from tradingagents.graph.trading_graph import TradingAgentsGraph
|
|
>>> from tradingagents.backtest.integration import backtest_trading_agents
|
|
>>>
|
|
>>> # Create strategy
|
|
>>> graph = TradingAgentsGraph()
|
|
>>>
|
|
>>> # Run backtest
|
|
>>> results = backtest_trading_agents(
|
|
... trading_graph=graph,
|
|
... tickers=['AAPL', 'MSFT'],
|
|
... start_date='2023-01-01',
|
|
... end_date='2023-12-31',
|
|
... )
|
|
>>>
|
|
>>> print(f"Total Return: {results.total_return:.2%}")
|
|
>>> print(f"Sharpe Ratio: {results.sharpe_ratio:.2f}")
|
|
"""
|
|
logger.info("Starting TradingAgents backtest")
|
|
|
|
# Create configuration
|
|
config = BacktestConfig(
|
|
initial_capital=Decimal(str(initial_capital)),
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
commission=Decimal(str(commission)),
|
|
slippage=Decimal(str(slippage)),
|
|
benchmark=benchmark,
|
|
**kwargs
|
|
)
|
|
|
|
# Create strategy wrapper
|
|
strategy = TradingAgentsStrategy(trading_graph)
|
|
|
|
# Create backtester
|
|
backtester = Backtester(config)
|
|
|
|
# Run backtest
|
|
results = backtester.run(
|
|
strategy=strategy,
|
|
tickers=tickers,
|
|
)
|
|
|
|
logger.info("TradingAgents backtest complete")
|
|
|
|
return results
|
|
|
|
|
|
def compare_strategies(
|
|
strategies: Dict[str, BaseStrategy],
|
|
tickers: List[str],
|
|
start_date: str,
|
|
end_date: str,
|
|
initial_capital: float = 100000.0,
|
|
**kwargs
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Compare multiple strategies.
|
|
|
|
Args:
|
|
strategies: Dictionary of strategy_name -> strategy
|
|
tickers: List of tickers to trade
|
|
start_date: Start date
|
|
end_date: End date
|
|
initial_capital: Starting capital
|
|
**kwargs: Additional config parameters
|
|
|
|
Returns:
|
|
DataFrame comparing strategy metrics
|
|
|
|
Example:
|
|
>>> from tradingagents.backtest.strategy import BuyAndHoldStrategy, SimpleMovingAverageStrategy
|
|
>>> from tradingagents.backtest.integration import compare_strategies
|
|
>>>
|
|
>>> strategies = {
|
|
... 'Buy & Hold': BuyAndHoldStrategy(),
|
|
... 'SMA Crossover': SimpleMovingAverageStrategy(50, 200),
|
|
... }
|
|
>>>
|
|
>>> comparison = compare_strategies(
|
|
... strategies=strategies,
|
|
... tickers=['AAPL'],
|
|
... start_date='2020-01-01',
|
|
... end_date='2023-12-31',
|
|
... )
|
|
>>>
|
|
>>> print(comparison)
|
|
"""
|
|
logger.info(f"Comparing {len(strategies)} strategies")
|
|
|
|
results_dict = {}
|
|
|
|
for name, strategy in strategies.items():
|
|
logger.info(f"Running backtest for: {name}")
|
|
|
|
# Create configuration
|
|
config = BacktestConfig(
|
|
initial_capital=Decimal(str(initial_capital)),
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
**kwargs
|
|
)
|
|
|
|
# Create backtester
|
|
backtester = Backtester(config)
|
|
|
|
try:
|
|
# Run backtest
|
|
results = backtester.run(strategy=strategy, tickers=tickers)
|
|
|
|
# Extract metrics
|
|
results_dict[name] = {
|
|
'Total Return': results.metrics.total_return,
|
|
'Annualized Return': results.metrics.annualized_return,
|
|
'Sharpe Ratio': results.metrics.sharpe_ratio,
|
|
'Sortino Ratio': results.metrics.sortino_ratio,
|
|
'Max Drawdown': results.metrics.max_drawdown,
|
|
'Volatility': results.metrics.volatility,
|
|
'Win Rate': results.metrics.win_rate,
|
|
'Total Trades': results.metrics.total_trades,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to backtest {name}: {e}")
|
|
results_dict[name] = {k: None for k in [
|
|
'Total Return', 'Annualized Return', 'Sharpe Ratio',
|
|
'Sortino Ratio', 'Max Drawdown', 'Volatility',
|
|
'Win Rate', 'Total Trades'
|
|
]}
|
|
|
|
# Create comparison DataFrame
|
|
comparison_df = pd.DataFrame(results_dict).T
|
|
|
|
logger.info("Strategy comparison complete")
|
|
|
|
return comparison_df
|
|
|
|
|
|
def parallel_backtest(
|
|
strategy_configs: List[Dict[str, Any]],
|
|
tickers: List[str],
|
|
start_date: str,
|
|
end_date: str,
|
|
n_jobs: int = -1,
|
|
) -> List[BacktestResults]:
|
|
"""
|
|
Run multiple backtests in parallel.
|
|
|
|
Args:
|
|
strategy_configs: List of dictionaries with strategy configurations
|
|
tickers: List of tickers
|
|
start_date: Start date
|
|
end_date: End date
|
|
n_jobs: Number of parallel jobs (-1 = all CPUs)
|
|
|
|
Returns:
|
|
List of BacktestResults
|
|
|
|
Example:
|
|
>>> configs = [
|
|
... {'strategy': SimpleMovingAverageStrategy(50, 200)},
|
|
... {'strategy': SimpleMovingAverageStrategy(20, 50)},
|
|
... ]
|
|
>>>
|
|
>>> results = parallel_backtest(
|
|
... strategy_configs=configs,
|
|
... tickers=['AAPL'],
|
|
... start_date='2020-01-01',
|
|
... end_date='2023-12-31',
|
|
... )
|
|
"""
|
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
|
|
logger.info(f"Running {len(strategy_configs)} backtests in parallel")
|
|
|
|
def run_single_backtest(config_dict):
|
|
"""Run a single backtest."""
|
|
strategy = config_dict['strategy']
|
|
|
|
backtest_config = BacktestConfig(
|
|
initial_capital=config_dict.get('initial_capital', Decimal("100000")),
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
commission=config_dict.get('commission', Decimal("0.001")),
|
|
slippage=config_dict.get('slippage', Decimal("0.0005")),
|
|
)
|
|
|
|
backtester = Backtester(backtest_config)
|
|
return backtester.run(strategy, tickers)
|
|
|
|
# Determine number of workers
|
|
if n_jobs == -1:
|
|
import multiprocessing
|
|
n_jobs = multiprocessing.cpu_count()
|
|
|
|
results = []
|
|
|
|
# Note: ProcessPoolExecutor may have issues with complex objects
|
|
# For TradingAgentsGraph, you might need to use ThreadPoolExecutor instead
|
|
# or implement proper serialization
|
|
|
|
# For now, run sequentially to avoid pickling issues
|
|
for config in strategy_configs:
|
|
try:
|
|
result = run_single_backtest(config)
|
|
results.append(result)
|
|
except Exception as e:
|
|
logger.error(f"Backtest failed: {e}")
|
|
results.append(None)
|
|
|
|
logger.info("Parallel backtests complete")
|
|
|
|
return results
|
|
|
|
|
|
class BacktestingPipeline:
|
|
"""
|
|
Pipeline for running comprehensive backtesting workflows.
|
|
|
|
Combines backtesting, walk-forward analysis, Monte Carlo simulation,
|
|
and reporting into a single workflow.
|
|
"""
|
|
|
|
def __init__(self, config: BacktestConfig):
|
|
"""
|
|
Initialize pipeline.
|
|
|
|
Args:
|
|
config: Backtest configuration
|
|
"""
|
|
self.config = config
|
|
self.backtester = Backtester(config)
|
|
|
|
def run_full_analysis(
|
|
self,
|
|
strategy: BaseStrategy,
|
|
tickers: List[str],
|
|
monte_carlo: bool = True,
|
|
generate_report: bool = True,
|
|
output_dir: str = './backtest_results',
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Run full backtesting analysis.
|
|
|
|
Args:
|
|
strategy: Trading strategy
|
|
tickers: List of tickers
|
|
monte_carlo: Whether to run Monte Carlo simulation
|
|
generate_report: Whether to generate HTML report
|
|
output_dir: Output directory for results
|
|
|
|
Returns:
|
|
Dictionary with all analysis results
|
|
"""
|
|
from pathlib import Path
|
|
|
|
logger.info("Running full backtesting analysis")
|
|
|
|
output_path = Path(output_dir)
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Run backtest
|
|
results = self.backtester.run(strategy, tickers)
|
|
|
|
analysis = {
|
|
'backtest_results': results,
|
|
'metrics': results.metrics,
|
|
}
|
|
|
|
# Monte Carlo simulation
|
|
if monte_carlo:
|
|
logger.info("Running Monte Carlo simulation")
|
|
from .monte_carlo import MonteCarloConfig
|
|
mc_config = MonteCarloConfig(n_simulations=10000)
|
|
mc_results = results.monte_carlo(mc_config)
|
|
analysis['monte_carlo'] = mc_results
|
|
|
|
# Generate report
|
|
if generate_report:
|
|
logger.info("Generating HTML report")
|
|
report_path = output_path / 'backtest_report.html'
|
|
results.generate_report(str(report_path))
|
|
analysis['report_path'] = str(report_path)
|
|
|
|
# Export to CSV
|
|
results.export_to_csv(str(output_path))
|
|
|
|
logger.info(f"Analysis complete. Results saved to {output_dir}")
|
|
|
|
return analysis
|