Cycles/cycles/strategies/bbrs_strategy.py
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

344 lines
14 KiB
Python

"""
Bollinger Bands + RSI Strategy (BBRS)
This module implements a sophisticated trading strategy that combines Bollinger Bands
and RSI indicators with market regime detection. The strategy adapts its parameters
based on whether the market is trending or moving sideways.
Key Features:
- Dynamic parameter adjustment based on market regime
- Bollinger Band squeeze detection
- RSI overbought/oversold conditions
- Market regime-specific thresholds
- Multi-timeframe analysis support
"""
import pandas as pd
import numpy as np
import logging
from typing import Tuple, Optional, List
from .base import StrategyBase, StrategySignal
class BBRSStrategy(StrategyBase):
"""
Bollinger Bands + RSI Strategy implementation.
This strategy uses Bollinger Bands and RSI indicators with market regime detection
to generate trading signals. It adapts its parameters based on whether the market
is in a trending or sideways regime.
The strategy works with 1-minute data as input and lets the underlying Strategy class
handle internal resampling to the timeframes it needs (typically 15min and 1h).
Stop-loss execution uses 1-minute precision.
Parameters:
bb_width (float): Bollinger Band width threshold (default: 0.05)
bb_period (int): Bollinger Band period (default: 20)
rsi_period (int): RSI calculation period (default: 14)
trending_rsi_threshold (list): RSI thresholds for trending market [low, high]
trending_bb_multiplier (float): BB multiplier for trending market
sideways_rsi_threshold (list): RSI thresholds for sideways market [low, high]
sideways_bb_multiplier (float): BB multiplier for sideways market
strategy_name (str): Strategy implementation name ("MarketRegimeStrategy" or "CryptoTradingStrategy")
SqueezeStrategy (bool): Enable squeeze strategy
stop_loss_pct (float): Stop loss percentage (default: 0.05)
Example:
params = {
"bb_width": 0.05,
"bb_period": 20,
"rsi_period": 14,
"strategy_name": "MarketRegimeStrategy",
"SqueezeStrategy": true
}
strategy = BBRSStrategy(weight=1.0, params=params)
"""
def __init__(self, weight: float = 1.0, params: Optional[dict] = None):
"""
Initialize the BBRS strategy.
Args:
weight: Strategy weight for combination (default: 1.0)
params: Strategy parameters for Bollinger Bands and RSI
"""
super().__init__("bbrs", weight, params)
def get_timeframes(self) -> List[str]:
"""
Get the timeframes required by the BBRS strategy.
BBRS strategy uses 1-minute data as input and lets the Strategy class
handle internal resampling to the timeframes it needs (15min, 1h, etc.).
We still include 1min for stop-loss precision.
Returns:
List[str]: List of timeframes needed for the strategy
"""
# BBRS strategy works with 1-minute data and lets Strategy class handle resampling
return ["1min"]
def initialize(self, backtester) -> None:
"""
Initialize BBRS strategy with signal processing.
Sets up the strategy by:
1. Using 1-minute data directly (Strategy class handles internal resampling)
2. Running the BBRS strategy processing on 1-minute data
3. Creating signals aligned with backtester expectations
Args:
backtester: Backtest instance with OHLCV data
"""
# Resample to get 1-minute data (which should be the original data)
self._resample_data(backtester.original_df)
# Get 1-minute data for strategy processing - Strategy class will handle internal resampling
min1_data = self.get_data_for_timeframe("1min")
# Initialize empty signal series for backtester compatibility
# Note: These will be populated after strategy processing
backtester.strategies["buy_signals"] = pd.Series(False, index=range(len(min1_data)))
backtester.strategies["sell_signals"] = pd.Series(False, index=range(len(min1_data)))
backtester.strategies["stop_loss_pct"] = self.params.get("stop_loss_pct", 0.05)
backtester.strategies["primary_timeframe"] = "1min"
# Run strategy processing on 1-minute data
self._run_strategy_processing(backtester)
self.initialized = True
def _run_strategy_processing(self, backtester) -> None:
"""
Run the actual BBRS strategy processing.
Uses the Strategy class from cycles.Analysis.strategies to process
the 1-minute data. The Strategy class will handle internal resampling
to the timeframes it needs (15min, 1h, etc.) and generate buy/sell signals.
Args:
backtester: Backtest instance with timeframes_data available
"""
from cycles.Analysis.strategies import Strategy
# Get 1-minute data for strategy processing - let Strategy class handle resampling
strategy_data = self.get_data_for_timeframe("1min")
# Configure strategy parameters with defaults
config_strategy = {
"bb_width": self.params.get("bb_width", 0.05),
"bb_period": self.params.get("bb_period", 20),
"rsi_period": self.params.get("rsi_period", 14),
"trending": {
"rsi_threshold": self.params.get("trending_rsi_threshold", [30, 70]),
"bb_std_dev_multiplier": self.params.get("trending_bb_multiplier", 2.5),
},
"sideways": {
"rsi_threshold": self.params.get("sideways_rsi_threshold", [40, 60]),
"bb_std_dev_multiplier": self.params.get("sideways_bb_multiplier", 1.8),
},
"strategy_name": self.params.get("strategy_name", "MarketRegimeStrategy"),
"SqueezeStrategy": self.params.get("SqueezeStrategy", True)
}
# Run strategy processing on 1-minute data - Strategy class handles internal resampling
strategy = Strategy(config=config_strategy, logging=logging)
processed_data = strategy.run(strategy_data, config_strategy["strategy_name"])
# Store processed data for plotting and analysis
backtester.processed_data = processed_data
if processed_data.empty:
# If strategy processing failed, keep empty signals
return
# Extract signals from processed data
buy_signals_raw = processed_data.get('BuySignal', pd.Series(False, index=processed_data.index)).astype(bool)
sell_signals_raw = processed_data.get('SellSignal', pd.Series(False, index=processed_data.index)).astype(bool)
# The processed_data will be on whatever timeframe the Strategy class outputs
# We need to map these signals back to 1-minute resolution for backtesting
original_1min_data = self.get_data_for_timeframe("1min")
# Reindex signals to 1-minute resolution using forward-fill
buy_signals_1min = buy_signals_raw.reindex(original_1min_data.index, method='ffill').fillna(False)
sell_signals_1min = sell_signals_raw.reindex(original_1min_data.index, method='ffill').fillna(False)
# Convert to integer index to match backtester expectations
backtester.strategies["buy_signals"] = pd.Series(buy_signals_1min.values, index=range(len(buy_signals_1min)))
backtester.strategies["sell_signals"] = pd.Series(sell_signals_1min.values, index=range(len(sell_signals_1min)))
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate entry signal based on BBRS buy signals.
Entry occurs when the BBRS strategy processing has generated
a buy signal based on Bollinger Bands and RSI conditions on
the primary timeframe.
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
StrategySignal: Entry signal if buy condition met, hold otherwise
"""
if not self.initialized:
return StrategySignal("HOLD", confidence=0.0)
if df_index >= len(backtester.strategies["buy_signals"]):
return StrategySignal("HOLD", confidence=0.0)
if backtester.strategies["buy_signals"].iloc[df_index]:
# High confidence for BBRS buy signals
confidence = self._calculate_signal_confidence(backtester, df_index, "entry")
return StrategySignal("ENTRY", confidence=confidence)
return StrategySignal("HOLD", confidence=0.0)
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate exit signal based on BBRS sell signals or stop loss.
Exit occurs when:
1. BBRS strategy generates a sell signal
2. Stop loss is triggered based on price movement
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
StrategySignal: Exit signal with type and price, or hold signal
"""
if not self.initialized:
return StrategySignal("HOLD", confidence=0.0)
if df_index >= len(backtester.strategies["sell_signals"]):
return StrategySignal("HOLD", confidence=0.0)
# Check for sell signal
if backtester.strategies["sell_signals"].iloc[df_index]:
confidence = self._calculate_signal_confidence(backtester, df_index, "exit")
return StrategySignal("EXIT", confidence=confidence,
metadata={"type": "SELL_SIGNAL"})
# Check for stop loss using 1-minute data for precision
stop_loss_result, sell_price = self._check_stop_loss(backtester)
if stop_loss_result:
return StrategySignal("EXIT", confidence=1.0, price=sell_price,
metadata={"type": "STOP_LOSS"})
return StrategySignal("HOLD", confidence=0.0)
def get_confidence(self, backtester, df_index: int) -> float:
"""
Get strategy confidence based on signal strength and market conditions.
Confidence can be enhanced by analyzing multiple timeframes and
market regime consistency.
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)
"""
if not self.initialized:
return 0.0
# Check for active signals
has_buy_signal = (df_index < len(backtester.strategies["buy_signals"]) and
backtester.strategies["buy_signals"].iloc[df_index])
has_sell_signal = (df_index < len(backtester.strategies["sell_signals"]) and
backtester.strategies["sell_signals"].iloc[df_index])
if has_buy_signal or has_sell_signal:
signal_type = "entry" if has_buy_signal else "exit"
return self._calculate_signal_confidence(backtester, df_index, signal_type)
# Moderate confidence during neutral periods
return 0.5
def _calculate_signal_confidence(self, backtester, df_index: int, signal_type: str) -> float:
"""
Calculate confidence level for a signal based on multiple factors.
Can consider multiple timeframes, market regime, volatility, etc.
Args:
backtester: Backtest instance
df_index: Current index
signal_type: "entry" or "exit"
Returns:
float: Confidence level (0.0 to 1.0)
"""
base_confidence = 1.0
# TODO: Implement multi-timeframe confirmation
# For now, return high confidence for primary signals
# Future enhancements could include:
# - Checking confirmation from additional timeframes
# - Analyzing market regime consistency
# - Considering volatility levels
# - RSI and BB position analysis
return base_confidence
def _check_stop_loss(self, backtester) -> Tuple[bool, Optional[float]]:
"""
Check if stop loss is triggered using 1-minute data for precision.
Uses 1-minute data regardless of primary timeframe to ensure
accurate stop loss execution.
Args:
backtester: Backtest instance with current trade state
Returns:
Tuple[bool, Optional[float]]: (stop_loss_triggered, sell_price)
"""
# Calculate stop loss price
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
# Use 1-minute data for precise stop loss checking
min1_data = self.get_data_for_timeframe("1min")
if min1_data is None:
# Fallback to original_df if 1min timeframe not available
min1_data = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
min1_index = min1_data.index
# Find data range from entry to current time
start_candidates = min1_index[min1_index >= backtester.entry_time]
if len(start_candidates) == 0:
return False, None
backtester.current_trade_min1_start_idx = start_candidates[0]
end_candidates = min1_index[min1_index <= backtester.current_date]
if len(end_candidates) == 0:
return False, None
backtester.current_min1_end_idx = end_candidates[-1]
# Check if any candle in the range triggered stop loss
min1_slice = min1_data.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
if (min1_slice['low'] <= stop_price).any():
# Find the first candle that triggered stop loss
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
# Use open price if it gapped below stop, otherwise use stop price
if stop_candle['open'] < stop_price:
sell_price = stop_candle['open']
else:
sell_price = stop_price
return True, sell_price
return False, None