""" 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 """ import pandas as pd import numpy as np import logging from typing import Tuple, Optional 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. 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 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" } 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 initialize(self, backtester) -> None: """ Initialize BBRS strategy with signal processing. Sets up the strategy by: 1. Initializing empty signal series 2. Running the BBRS strategy processing if original data is available 3. Resampling signals from 15-minute to 1-minute resolution Args: backtester: Backtest instance with OHLCV data """ # Initialize empty signal series backtester.strategies["buy_signals"] = pd.Series(False, index=range(len(backtester.df))) backtester.strategies["sell_signals"] = pd.Series(False, index=range(len(backtester.df))) backtester.strategies["stop_loss_pct"] = self.params.get("stop_loss_pct", 0.05) # Run strategy processing if original data is available if hasattr(backtester, 'original_df'): 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 original dataframe and generate buy/sell signals based on Bollinger Bands, RSI, and market regime detection. Args: backtester: Backtest instance with original_df attribute """ from cycles.Analysis.strategies import Strategy # 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 strategy = Strategy(config=config_strategy, logging=logging) processed_data = strategy.run(backtester.original_df, 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) # Resample signals from 15-minute to 1-minute resolution self._resample_signals_to_1min(backtester, buy_signals_raw, sell_signals_raw) def _resample_signals_to_1min(self, backtester, buy_signals_raw, sell_signals_raw) -> None: """ Resample signals from 15-minute to 1-minute resolution. Takes the 15-minute signals and maps them to 1-minute timestamps using forward-fill to maintain signal consistency. Args: backtester: Backtest instance buy_signals_raw: Raw buy signals from strategy processing sell_signals_raw: Raw sell signals from strategy processing """ # Get the DatetimeIndex from the original 1-minute data original_datetime_index = backtester.original_df.index # Reindex signals from 15-minute to 1-minute resolution using forward-fill buy_signals_1min = buy_signals_raw.reindex(original_datetime_index, method='ffill').fillna(False) sell_signals_1min = sell_signals_raw.reindex(original_datetime_index, method='ffill').fillna(False) # Convert to integer index to match backtest DataFrame buy_condition = pd.Series(buy_signals_1min.values, index=range(len(buy_signals_1min))) sell_condition = pd.Series(sell_signals_1min.values, index=range(len(sell_signals_1min))) # Ensure same length as backtest DataFrame if len(buy_condition) != len(backtester.df): target_length = len(backtester.df) if len(buy_condition) > target_length: # Truncate if longer buy_condition = buy_condition[:target_length] sell_condition = sell_condition[:target_length] else: # Pad with False if shorter buy_values = buy_condition.values sell_values = sell_condition.values buy_values = np.pad(buy_values, (0, target_length - len(buy_values)), constant_values=False) sell_values = np.pad(sell_values, (0, target_length - len(sell_values)), constant_values=False) buy_condition = pd.Series(buy_values, index=range(target_length)) sell_condition = pd.Series(sell_values, index=range(target_length)) # Store the resampled signals backtester.strategies["buy_signals"] = buy_condition backtester.strategies["sell_signals"] = sell_condition 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. Args: backtester: Backtest instance with current state df_index: Current index in the 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 return StrategySignal("ENTRY", confidence=1.0) 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 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]: return StrategySignal("EXIT", confidence=1.0, metadata={"type": "SELL_SIGNAL"}) # Check for stop loss 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 is higher when signals are present and market conditions are favorable for the BBRS strategy. Args: backtester: Backtest instance with current state df_index: Current index in the dataframe Returns: float: Confidence level (0.0 to 1.0) """ if not self.initialized: return 0.0 # Check if we have processed data for confidence calculation if hasattr(backtester, 'processed_data') and not backtester.processed_data.empty: # Could analyze RSI levels, BB position, etc. for dynamic confidence # For now, return high confidence when signals are present if (df_index < len(backtester.strategies["buy_signals"]) and backtester.strategies["buy_signals"].iloc[df_index]): return 1.0 elif (df_index < len(backtester.strategies["sell_signals"]) and backtester.strategies["sell_signals"].iloc[df_index]): return 1.0 # Moderate confidence during neutral periods return 0.5 def _check_stop_loss(self, backtester) -> Tuple[bool, Optional[float]]: """ Check if stop loss is triggered using BBRS-specific logic. Similar to default strategy but uses BBRS-specific stop loss percentage and can be enhanced with additional BBRS-specific exit conditions. 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"]) # Get minute-level data for precise stop loss checking min1_df = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df min1_index = min1_df.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_df.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