""" Incremental Trader for backtesting incremental strategies. This module provides the IncTrader class that manages a single incremental strategy during backtesting, handling strategy execution, trade decisions, and performance tracking. """ import pandas as pd import numpy as np from typing import Dict, Optional, List, Any import logging # Use try/except for imports to handle both relative and absolute import scenarios try: from ..strategies.base import IncStrategyBase, IncStrategySignal from .position import PositionManager, TradeRecord except ImportError: # Fallback for direct execution import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from strategies.base import IncStrategyBase, IncStrategySignal from position import PositionManager, TradeRecord logger = logging.getLogger(__name__) class IncTrader: """ Incremental trader that manages a single strategy during backtesting. This class handles: - Strategy initialization and data feeding - Trade decision logic based on strategy signals - Risk management (stop loss, take profit) - Performance tracking and metrics collection The trader processes data points sequentially, feeding them to the strategy and executing trades based on the generated signals. Example: from IncrementalTrader.strategies import MetaTrendStrategy from IncrementalTrader.trader import IncTrader strategy = MetaTrendStrategy("metatrend", params={"timeframe": "15min"}) trader = IncTrader( strategy=strategy, initial_usd=10000, params={"stop_loss_pct": 0.02} ) # Process data sequentially for timestamp, ohlcv_data in data_stream: trader.process_data_point(timestamp, ohlcv_data) # Get results results = trader.get_results() """ def __init__(self, strategy: IncStrategyBase, initial_usd: float = 10000, params: Optional[Dict] = None): """ Initialize the incremental trader. Args: strategy: Incremental strategy instance initial_usd: Initial USD balance params: Trader parameters (stop_loss_pct, take_profit_pct, etc.) """ self.strategy = strategy self.initial_usd = initial_usd self.params = params or {} # Initialize position manager self.position_manager = PositionManager(initial_usd) # Current state self.current_timestamp = None self.current_price = None # Strategy state tracking self.data_points_processed = 0 self.warmup_complete = False # Risk management parameters self.stop_loss_pct = self.params.get("stop_loss_pct", 0.0) self.take_profit_pct = self.params.get("take_profit_pct", 0.0) # Performance tracking self.portfolio_history = [] logger.info(f"IncTrader initialized: strategy={strategy.name}, " f"initial_usd=${initial_usd}, stop_loss={self.stop_loss_pct*100:.1f}%") def process_data_point(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> None: """ Process a single data point through the strategy and handle trading logic. Args: timestamp: Data point timestamp ohlcv_data: OHLCV data dictionary with keys: open, high, low, close, volume """ self.current_timestamp = timestamp self.current_price = ohlcv_data['close'] self.data_points_processed += 1 try: # Feed data to strategy and get signal signal = self.strategy.process_data_point(timestamp, ohlcv_data) # Check if strategy is warmed up if not self.warmup_complete and self.strategy.is_warmed_up: self.warmup_complete = True logger.info(f"Strategy {self.strategy.name} warmed up after " f"{self.data_points_processed} data points") # Only process signals if strategy is warmed up if self.warmup_complete: self._process_trading_logic(signal) # Update performance tracking self._update_performance_tracking() except Exception as e: logger.error(f"Error processing data point at {timestamp}: {e}") raise def _process_trading_logic(self, signal: Optional[IncStrategySignal]) -> None: """Process trading logic based on current position and strategy signals.""" if not self.position_manager.is_in_position(): # No position - check for entry signals self._check_entry_signals(signal) else: # In position - check for exit signals self._check_exit_signals(signal) def _check_entry_signals(self, signal: Optional[IncStrategySignal]) -> None: """Check for entry signals when not in position.""" try: # Check if we have a valid entry signal if signal and signal.signal_type == "ENTRY" and signal.confidence > 0: self._execute_entry(signal) except Exception as e: logger.error(f"Error checking entry signals: {e}") def _check_exit_signals(self, signal: Optional[IncStrategySignal]) -> None: """Check for exit signals when in position.""" try: # Check strategy exit signals first if signal and signal.signal_type == "EXIT" and signal.confidence > 0: exit_reason = signal.metadata.get("type", "STRATEGY_EXIT") exit_price = signal.price if signal.price else self.current_price self._execute_exit(exit_reason, exit_price) return # Check stop loss if self.position_manager.check_stop_loss(self.current_price, self.stop_loss_pct): self._execute_exit("STOP_LOSS", self.current_price) return # Check take profit if self.position_manager.check_take_profit(self.current_price, self.take_profit_pct): self._execute_exit("TAKE_PROFIT", self.current_price) return except Exception as e: logger.error(f"Error checking exit signals: {e}") def _execute_entry(self, signal: IncStrategySignal) -> None: """Execute entry trade.""" entry_price = signal.price if signal.price else self.current_price try: entry_details = self.position_manager.execute_entry( entry_price, self.current_timestamp, self.strategy.name ) logger.info(f"ENTRY: {self.strategy.name} at ${entry_price:.2f}, " f"confidence={signal.confidence:.2f}, " f"fee=${entry_details['entry_fee']:.2f}") except Exception as e: logger.error(f"Error executing entry: {e}") raise def _execute_exit(self, exit_reason: str, exit_price: Optional[float] = None) -> None: """Execute exit trade.""" exit_price = exit_price if exit_price else self.current_price try: exit_details = self.position_manager.execute_exit( exit_price, self.current_timestamp, exit_reason, self.strategy.name ) logger.info(f"EXIT: {self.strategy.name} at ${exit_price:.2f}, " f"reason={exit_reason}, " f"profit={exit_details['profit_pct']*100:.2f}%, " f"fee=${exit_details['exit_fee']:.2f}") except Exception as e: logger.error(f"Error executing exit: {e}") raise def _update_performance_tracking(self) -> None: """Update performance tracking metrics.""" # Update position manager metrics self.position_manager.update_performance_metrics(self.current_price) # Track portfolio value over time current_balance = self.position_manager.get_current_balance(self.current_price) self.portfolio_history.append({ 'timestamp': self.current_timestamp, 'balance': current_balance, 'price': self.current_price, 'position': self.position_manager.position }) def finalize(self) -> None: """Finalize trading session (close any open positions).""" if self.position_manager.is_in_position(): self._execute_exit("EOD", self.current_price) logger.info(f"Closed final position for {self.strategy.name} at EOD") def get_results(self) -> Dict[str, Any]: """ Get comprehensive trading results. Returns: Dict containing performance metrics, trade records, and statistics """ # Get performance summary from position manager performance = self.position_manager.get_performance_summary() # Get trades as dictionaries trades = self.position_manager.get_trades_as_dicts() # Build comprehensive results results = { "strategy_name": self.strategy.name, "strategy_params": self.strategy.params, "trader_params": self.params, "data_points_processed": self.data_points_processed, "warmup_complete": self.warmup_complete, "trades": trades, "portfolio_history": self.portfolio_history, **performance # Include all performance metrics } # Add first and last trade info if available if len(trades) > 0: results["first_trade"] = { "entry_time": trades[0]["entry_time"], "entry": trades[0]["entry"] } results["last_trade"] = { "exit_time": trades[-1]["exit_time"], "exit": trades[-1]["exit"] } # Add final balance for compatibility results["final_balance"] = performance["final_usd"] return results def get_current_state(self) -> Dict[str, Any]: """Get current trader state for debugging.""" position_state = self.position_manager.get_current_state() return { "strategy": self.strategy.name, "current_price": self.current_price, "current_timestamp": self.current_timestamp, "data_points_processed": self.data_points_processed, "warmup_complete": self.warmup_complete, "strategy_state": self.strategy.get_current_state_summary(), **position_state # Include all position state } def get_portfolio_value(self) -> float: """Get current portfolio value.""" return self.position_manager.get_current_balance(self.current_price) def reset(self) -> None: """Reset trader to initial state.""" self.position_manager.reset() self.strategy.reset_calculation_state() self.current_timestamp = None self.current_price = None self.data_points_processed = 0 self.warmup_complete = False self.portfolio_history.clear() logger.info(f"IncTrader reset for strategy {self.strategy.name}") def __repr__(self) -> str: """String representation of the trader.""" return (f"IncTrader(strategy={self.strategy.name}, " f"position={self.position_manager.position}, " f"balance=${self.position_manager.get_current_balance(self.current_price or 0):.2f}, " f"trades={len(self.position_manager.trade_records)})")