""" 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.bb_rsi import BollingerBandsStrategy # 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 = BollingerBandsStrategy(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