Vasily.onl 5d0b707bc6 Implement BBRS strategy with multi-timeframe support and enhance strategy manager
- 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.
2025-05-23 16:56:53 +08:00

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})")