- Added BBRS strategy implementation, incorporating Bollinger Bands and RSI for trading signals. - Introduced multi-timeframe analysis support, allowing strategies to handle internal resampling. - Enhanced StrategyManager to log strategy initialization and unique timeframes in use. - Updated DefaultStrategy to support flexible timeframe configurations and improved stop-loss execution. - Improved plotting logic in BacktestCharts for better visualization of strategy outputs and trades. - Refactored strategy base class to facilitate resampling and data handling across different timeframes.
250 lines
9.1 KiB
Python
250 lines
9.1 KiB
Python
"""
|
|
Base classes for the strategy management system.
|
|
|
|
This module contains the fundamental building blocks for all trading strategies:
|
|
- StrategySignal: Represents trading signals with confidence and metadata
|
|
- StrategyBase: Abstract base class that all strategies must inherit from
|
|
"""
|
|
|
|
import pandas as pd
|
|
from abc import ABC, abstractmethod
|
|
from typing import Dict, Optional, List, Union
|
|
|
|
|
|
class StrategySignal:
|
|
"""
|
|
Represents a trading signal from a strategy.
|
|
|
|
A signal encapsulates the strategy's recommendation along with confidence
|
|
level, optional price target, and additional metadata.
|
|
|
|
Attributes:
|
|
signal_type (str): Type of signal - "ENTRY", "EXIT", or "HOLD"
|
|
confidence (float): Confidence level from 0.0 to 1.0
|
|
price (Optional[float]): Optional specific price for the signal
|
|
metadata (Dict): Additional signal data and context
|
|
|
|
Example:
|
|
# Entry signal with high confidence
|
|
signal = StrategySignal("ENTRY", confidence=0.8)
|
|
|
|
# Exit signal with stop loss price
|
|
signal = StrategySignal("EXIT", confidence=1.0, price=50000,
|
|
metadata={"type": "STOP_LOSS"})
|
|
"""
|
|
|
|
def __init__(self, signal_type: str, confidence: float = 1.0,
|
|
price: Optional[float] = None, metadata: Optional[Dict] = None):
|
|
"""
|
|
Initialize a strategy signal.
|
|
|
|
Args:
|
|
signal_type: Type of signal ("ENTRY", "EXIT", "HOLD")
|
|
confidence: Confidence level (0.0 to 1.0)
|
|
price: Optional specific price for the signal
|
|
metadata: Additional signal data and context
|
|
"""
|
|
self.signal_type = signal_type
|
|
self.confidence = max(0.0, min(1.0, confidence)) # Clamp to [0,1]
|
|
self.price = price
|
|
self.metadata = metadata or {}
|
|
|
|
def __repr__(self) -> str:
|
|
"""String representation of the signal."""
|
|
return (f"StrategySignal(type={self.signal_type}, "
|
|
f"confidence={self.confidence:.2f}, "
|
|
f"price={self.price}, metadata={self.metadata})")
|
|
|
|
|
|
class StrategyBase(ABC):
|
|
"""
|
|
Abstract base class for all trading strategies.
|
|
|
|
This class defines the interface that all strategies must implement:
|
|
- get_timeframes(): Specify required timeframes for the strategy
|
|
- initialize(): Setup strategy with backtester data
|
|
- get_entry_signal(): Generate entry signals
|
|
- get_exit_signal(): Generate exit signals
|
|
- get_confidence(): Optional confidence calculation
|
|
|
|
Attributes:
|
|
name (str): Strategy name
|
|
weight (float): Strategy weight for combination
|
|
params (Dict): Strategy parameters
|
|
initialized (bool): Whether strategy has been initialized
|
|
timeframes_data (Dict): Resampled data for different timeframes
|
|
|
|
Example:
|
|
class MyStrategy(StrategyBase):
|
|
def get_timeframes(self):
|
|
return ["15min"] # This strategy works on 15-minute data
|
|
|
|
def initialize(self, backtester):
|
|
# Setup strategy indicators using self.timeframes_data["15min"]
|
|
self.initialized = True
|
|
|
|
def get_entry_signal(self, backtester, df_index):
|
|
# Return StrategySignal based on analysis
|
|
if should_enter:
|
|
return StrategySignal("ENTRY", confidence=0.7)
|
|
return StrategySignal("HOLD", confidence=0.0)
|
|
"""
|
|
|
|
def __init__(self, name: str, weight: float = 1.0, params: Optional[Dict] = None):
|
|
"""
|
|
Initialize the strategy base.
|
|
|
|
Args:
|
|
name: Strategy name/identifier
|
|
weight: Strategy weight for combination (default: 1.0)
|
|
params: Strategy-specific parameters
|
|
"""
|
|
self.name = name
|
|
self.weight = weight
|
|
self.params = params or {}
|
|
self.initialized = False
|
|
self.timeframes_data = {} # Will store resampled data for each timeframe
|
|
|
|
def get_timeframes(self) -> List[str]:
|
|
"""
|
|
Get the list of timeframes required by this strategy.
|
|
|
|
Override this method to specify which timeframes your strategy needs.
|
|
The base class will automatically resample the 1-minute data to these timeframes
|
|
and make them available in self.timeframes_data.
|
|
|
|
Returns:
|
|
List[str]: List of timeframe strings (e.g., ["1min", "15min", "1h"])
|
|
|
|
Example:
|
|
def get_timeframes(self):
|
|
return ["15min"] # Strategy needs 15-minute data
|
|
|
|
def get_timeframes(self):
|
|
return ["5min", "15min", "1h"] # Multi-timeframe strategy
|
|
"""
|
|
return ["1min"] # Default to 1-minute data
|
|
|
|
def _resample_data(self, original_data: pd.DataFrame) -> None:
|
|
"""
|
|
Resample the original 1-minute data to all required timeframes.
|
|
|
|
This method is called automatically during initialization to create
|
|
resampled versions of the data for each timeframe the strategy needs.
|
|
|
|
Args:
|
|
original_data: Original 1-minute OHLCV data with DatetimeIndex
|
|
"""
|
|
self.timeframes_data = {}
|
|
|
|
for timeframe in self.get_timeframes():
|
|
if timeframe == "1min":
|
|
# For 1-minute data, just use the original
|
|
self.timeframes_data[timeframe] = original_data.copy()
|
|
else:
|
|
# Resample to the specified timeframe
|
|
resampled = original_data.resample(timeframe).agg({
|
|
'open': 'first',
|
|
'high': 'max',
|
|
'low': 'min',
|
|
'close': 'last',
|
|
'volume': 'sum'
|
|
}).dropna()
|
|
|
|
self.timeframes_data[timeframe] = resampled
|
|
|
|
def get_data_for_timeframe(self, timeframe: str) -> Optional[pd.DataFrame]:
|
|
"""
|
|
Get resampled data for a specific timeframe.
|
|
|
|
Args:
|
|
timeframe: Timeframe string (e.g., "15min", "1h")
|
|
|
|
Returns:
|
|
pd.DataFrame: Resampled OHLCV data or None if timeframe not available
|
|
"""
|
|
return self.timeframes_data.get(timeframe)
|
|
|
|
def get_primary_timeframe_data(self) -> pd.DataFrame:
|
|
"""
|
|
Get data for the primary (first) timeframe.
|
|
|
|
Returns:
|
|
pd.DataFrame: Data for the first timeframe in get_timeframes() list
|
|
"""
|
|
primary_timeframe = self.get_timeframes()[0]
|
|
return self.timeframes_data[primary_timeframe]
|
|
|
|
@abstractmethod
|
|
def initialize(self, backtester) -> None:
|
|
"""
|
|
Initialize strategy with backtester data.
|
|
|
|
This method is called once before backtesting begins.
|
|
The original 1-minute data will already be resampled to all required timeframes
|
|
and available in self.timeframes_data.
|
|
|
|
Strategies should setup indicators, validate data, and
|
|
set self.initialized = True when complete.
|
|
|
|
Args:
|
|
backtester: Backtest instance with data and configuration
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
|
|
"""
|
|
Generate entry signal for the given data index.
|
|
|
|
The df_index refers to the index in the backtester's working dataframe,
|
|
which corresponds to the primary timeframe data.
|
|
|
|
Args:
|
|
backtester: Backtest instance with current state
|
|
df_index: Current index in the primary timeframe dataframe
|
|
|
|
Returns:
|
|
StrategySignal: Entry signal with confidence level
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
|
|
"""
|
|
Generate exit signal for the given data index.
|
|
|
|
The df_index refers to the index in the backtester's working dataframe,
|
|
which corresponds to the primary timeframe data.
|
|
|
|
Args:
|
|
backtester: Backtest instance with current state
|
|
df_index: Current index in the primary timeframe dataframe
|
|
|
|
Returns:
|
|
StrategySignal: Exit signal with confidence level
|
|
"""
|
|
pass
|
|
|
|
def get_confidence(self, backtester, df_index: int) -> float:
|
|
"""
|
|
Get strategy confidence for the current market state.
|
|
|
|
Default implementation returns 1.0. Strategies can override
|
|
this to provide dynamic confidence based on market conditions.
|
|
|
|
Args:
|
|
backtester: Backtest instance with current state
|
|
df_index: Current index in the primary timeframe dataframe
|
|
|
|
Returns:
|
|
float: Confidence level (0.0 to 1.0)
|
|
"""
|
|
return 1.0
|
|
|
|
def __repr__(self) -> str:
|
|
"""String representation of the strategy."""
|
|
timeframes = self.get_timeframes()
|
|
return (f"{self.__class__.__name__}(name={self.name}, "
|
|
f"weight={self.weight}, timeframes={timeframes}, "
|
|
f"initialized={self.initialized})") |