- Introduced a new strategies module containing the StrategyManager class to orchestrate multiple trading strategies. - Implemented StrategyBase and StrategySignal as foundational components for strategy development. - Added DefaultStrategy for meta-trend analysis and BBRSStrategy for Bollinger Bands + RSI trading. - Enhanced documentation to provide clear usage examples and configuration guidelines for the new system. - Established a modular architecture to support future strategy additions and improvements.
316 lines
13 KiB
Python
316 lines
13 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
|
|
"""
|
|
|
|
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 |