Cycles/cycles/strategies/bbrs_strategy.py
Vasily.onl 235098c045 Add strategy management system with multiple trading strategies
- 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.
2025-05-23 16:41:08 +08:00

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