- Introduced a comprehensive framework for incremental trading strategies, including modules for strategy execution, backtesting, and data processing. - Added key components such as `IncTrader`, `IncBacktester`, and various trading strategies (e.g., `MetaTrendStrategy`, `BBRSStrategy`, `RandomStrategy`) to facilitate real-time trading and backtesting. - Implemented a robust backtesting framework with configuration management, parallel execution, and result analysis capabilities. - Developed an incremental indicators framework to support real-time data processing with constant memory usage. - Enhanced documentation to provide clear usage examples and architecture overview, ensuring maintainability and ease of understanding for future development. - Ensured compatibility with existing strategies and maintained a focus on performance and scalability throughout the implementation.
301 lines
12 KiB
Python
301 lines
12 KiB
Python
"""
|
|
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)})") |