From 110734659403aa05045741a183b48c19008b3507 Mon Sep 17 00:00:00 2001 From: Ajasra Date: Thu, 22 May 2025 17:15:51 +0800 Subject: [PATCH] refactor to move inside strategy calculations --- cycles/Analysis/strategies.py | 83 +++++++++++++++++++++++++---------- test_bbrsi.py | 49 ++++++--------------- 2 files changed, 73 insertions(+), 59 deletions(-) diff --git a/cycles/Analysis/strategies.py b/cycles/Analysis/strategies.py index 3439700..a93ab1a 100644 --- a/cycles/Analysis/strategies.py +++ b/cycles/Analysis/strategies.py @@ -2,6 +2,8 @@ import pandas as pd import numpy as np from cycles.Analysis.boillinger_band import BollingerBands +from cycles.Analysis.rsi import RSI +from cycles.utils.data_utils import aggregate_to_daily class Strategy: @@ -65,45 +67,74 @@ class Strategy: Sell: Price ≥ Upper Band ∧ RSI ≥ 60 Enhanced with RSI Bollinger Squeeze for signal confirmation when enabled. - """ + Returns: + DataFrame: A unified DataFrame containing original data, BB, RSI, and signals. + """ + + data = aggregate_to_daily(data) + + # Calculate Bollinger Bands + bb_calculator = BollingerBands(config=self.config) + # Ensure we are working with a copy to avoid modifying the original DataFrame upstream + data_bb = bb_calculator.calculate(data.copy()) + + # Calculate RSI + rsi_calculator = RSI(config=self.config) + # Use the original data's copy for RSI calculation as well, to maintain index integrity + data_with_rsi = rsi_calculator.calculate(data.copy(), price_column='close') + + # Combine BB and RSI data into a single DataFrame for signal generation + # Ensure indices are aligned; they should be as both are from data.copy() + if 'RSI' in data_with_rsi.columns: + data_bb['RSI'] = data_with_rsi['RSI'] + else: + # If RSI wasn't calculated (e.g., not enough data), create a dummy column with NaNs + # to prevent errors later, though signals won't be generated. + data_bb['RSI'] = pd.Series(index=data_bb.index, dtype=float) + if self.logging: + self.logging.warning("RSI column not found or not calculated. Signals relying on RSI may not be generated.") + # Initialize conditions as all False - buy_condition = pd.Series(False, index=data.index) - sell_condition = pd.Series(False, index=data.index) + buy_condition = pd.Series(False, index=data_bb.index) + sell_condition = pd.Series(False, index=data_bb.index) # Create masks for different market regimes - sideways_mask = data['MarketRegime'] > 0 - trending_mask = data['MarketRegime'] <= 0 - valid_data_mask = ~data['MarketRegime'].isna() # Handle potential NaN values + # MarketRegime is expected to be in data_bb from BollingerBands calculation + sideways_mask = data_bb['MarketRegime'] > 0 + trending_mask = data_bb['MarketRegime'] <= 0 + valid_data_mask = ~data_bb['MarketRegime'].isna() # Handle potential NaN values # Calculate volume spike (≥1.5× 20D Avg) - if 'volume' in data.columns: - volume_20d_avg = data['volume'].rolling(window=20).mean() - volume_spike = data['volume'] >= 1.5 * volume_20d_avg + # 'volume' column should be present in the input 'data', and thus in 'data_bb' + if 'volume' in data_bb.columns: + volume_20d_avg = data_bb['volume'].rolling(window=20).mean() + volume_spike = data_bb['volume'] >= 1.5 * volume_20d_avg # Additional volume contraction filter for sideways markets - volume_30d_avg = data['volume'].rolling(window=30).mean() - volume_contraction = data['volume'] < 0.7 * volume_30d_avg + volume_30d_avg = data_bb['volume'].rolling(window=30).mean() + volume_contraction = data_bb['volume'] < 0.7 * volume_30d_avg else: # If volume data is not available, assume no volume spike - volume_spike = pd.Series(False, index=data.index) - volume_contraction = pd.Series(False, index=data.index) + volume_spike = pd.Series(False, index=data_bb.index) + volume_contraction = pd.Series(False, index=data_bb.index) if self.logging is not None: self.logging.warning("Volume data not available. Volume conditions will not be triggered.") # Calculate RSI Bollinger Squeeze confirmation - if 'RSI' in data.columns: - oversold_rsi, overbought_rsi = self.rsi_bollinger_confirmation(data['RSI']) + # RSI column is now part of data_bb + if 'RSI' in data_bb.columns and not data_bb['RSI'].isna().all(): + oversold_rsi, overbought_rsi = self.rsi_bollinger_confirmation(data_bb['RSI']) else: - oversold_rsi = pd.Series(False, index=data.index) - overbought_rsi = pd.Series(False, index=data.index) - if self.logging is not None: - self.logging.warning("RSI data not available. RSI Bollinger Squeeze will not be triggered.") + oversold_rsi = pd.Series(False, index=data_bb.index) + overbought_rsi = pd.Series(False, index=data_bb.index) + if self.logging is not None and ('RSI' not in data_bb.columns or data_bb['RSI'].isna().all()): + self.logging.warning("RSI data not available or all NaN. RSI Bollinger Squeeze will not be triggered.") # Calculate conditions for sideways market (Mean Reversion) if sideways_mask.any(): - sideways_buy = (data['close'] <= data['LowerBand']) & (data['RSI'] <= 40) - sideways_sell = (data['close'] >= data['UpperBand']) & (data['RSI'] >= 60) + sideways_buy = (data_bb['close'] <= data_bb['LowerBand']) & (data_bb['RSI'] <= 40) + sideways_sell = (data_bb['close'] >= data_bb['UpperBand']) & (data_bb['RSI'] >= 60) # Add enhanced confirmation for sideways markets if self.config.get("SqueezeStrategy", False): @@ -116,8 +147,8 @@ class Strategy: # Calculate conditions for trending market (Breakout Mode) if trending_mask.any(): - trending_buy = (data['close'] < data['LowerBand']) & (data['RSI'] < 50) & volume_spike - trending_sell = (data['close'] > data['UpperBand']) & (data['RSI'] > 50) & volume_spike + trending_buy = (data_bb['close'] < data_bb['LowerBand']) & (data_bb['RSI'] < 50) & volume_spike + trending_sell = (data_bb['close'] > data_bb['UpperBand']) & (data_bb['RSI'] > 50) & volume_spike # Add enhanced confirmation for trending markets if self.config.get("SqueezeStrategy", False): @@ -128,4 +159,8 @@ class Strategy: buy_condition = buy_condition | (trending_buy & trending_mask & valid_data_mask) sell_condition = sell_condition | (trending_sell & trending_mask & valid_data_mask) - return buy_condition, sell_condition \ No newline at end of file + # Add buy/sell conditions as columns to the DataFrame + data_bb['BuySignal'] = buy_condition + data_bb['SellSignal'] = sell_condition + + return data_bb \ No newline at end of file diff --git a/test_bbrsi.py b/test_bbrsi.py index d871de5..34f9575 100644 --- a/test_bbrsi.py +++ b/test_bbrsi.py @@ -5,8 +5,6 @@ import pandas as pd from cycles.utils.storage import Storage from cycles.utils.data_utils import aggregate_to_daily -from cycles.Analysis.boillinger_band import BollingerBands -from cycles.Analysis.rsi import RSI from cycles.Analysis.strategies import Strategy logging.basicConfig( @@ -59,44 +57,25 @@ if __name__ == "__main__": data = storage.load_data(config["data_file"], config["start_date"], config["stop_date"]) - if not IS_DAY: - data_daily = aggregate_to_daily(data) - storage.save_data(data, "btcusd_1-day_data.csv") - df_to_plot = data_daily - else: - df_to_plot = data - bb = BollingerBands(config=config_strategy) - data_bb = bb.calculate(df_to_plot.copy()) + strategy = Strategy(config=config_strategy, logging=logging) + processed_data = strategy.run(data.copy(), config_strategy["strategy_name"]) - rsi_calculator = RSI(config=config_strategy) - data_with_rsi = rsi_calculator.calculate(df_to_plot.copy(), price_column='close') - - # Combine BB and RSI data into a single DataFrame for signal generation - # Ensure indices are aligned; they should be as both are from df_to_plot.copy() - if 'RSI' in data_with_rsi.columns: - data_bb['RSI'] = data_with_rsi['RSI'] - else: - # If RSI wasn't calculated (e.g., not enough data), create a dummy column with NaNs - # to prevent errors later, though signals won't be generated. - data_bb['RSI'] = pd.Series(index=data_bb.index, dtype=float) - logging.warning("RSI column not found or not calculated. Signals relying on RSI may not be generated.") + buy_condition = processed_data.get('BuySignal', pd.Series(False, index=processed_data.index)).astype(bool) + sell_condition = processed_data.get('SellSignal', pd.Series(False, index=processed_data.index)).astype(bool) - strategy = Strategy(config=config_strategy) - buy_condition, sell_condition = strategy.run(data_bb, config_strategy["strategy_name"]) - - buy_signals = data_bb[buy_condition] - sell_signals = data_bb[sell_condition] + buy_signals = processed_data[buy_condition] + sell_signals = processed_data[sell_condition] # plot the data with seaborn library - if df_to_plot is not None and not df_to_plot.empty: + if processed_data is not None and not processed_data.empty: # Create a figure with two subplots, sharing the x-axis fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 8), sharex=True) # Plot 1: Close Price and Bollinger Bands - sns.lineplot(x=data_bb.index, y='close', data=data_bb, label='Close Price', ax=ax1) - sns.lineplot(x=data_bb.index, y='UpperBand', data=data_bb, label='Upper Band (BB)', ax=ax1) - sns.lineplot(x=data_bb.index, y='LowerBand', data=data_bb, label='Lower Band (BB)', ax=ax1) + sns.lineplot(x=processed_data.index, y='close', data=processed_data, label='Close Price', ax=ax1) + sns.lineplot(x=processed_data.index, y='UpperBand', data=processed_data, label='Upper Band (BB)', ax=ax1) + sns.lineplot(x=processed_data.index, y='LowerBand', data=processed_data, label='Lower Band (BB)', ax=ax1) # Plot Buy/Sell signals on Price chart if not buy_signals.empty: ax1.scatter(buy_signals.index, buy_signals['close'], color='green', marker='o', s=20, label='Buy Signal', zorder=5) @@ -108,8 +87,8 @@ if __name__ == "__main__": ax1.grid(True) # Plot 2: RSI - if 'RSI' in data_bb.columns: # Check data_bb now as it should contain RSI - sns.lineplot(x=data_bb.index, y='RSI', data=data_bb, label='RSI (' + str(config_strategy["rsi_period"]) + ')', ax=ax2, color='purple') + if 'RSI' in processed_data.columns: + sns.lineplot(x=processed_data.index, y='RSI', data=processed_data, label='RSI (' + str(config_strategy["rsi_period"]) + ')', ax=ax2, color='purple') ax2.axhline(config_strategy["trending"]["rsi_threshold"][1], color='red', linestyle='--', linewidth=0.8, label='Overbought (' + str(config_strategy["trending"]["rsi_threshold"][1]) + ')') ax2.axhline(config_strategy['trending']['rsi_threshold'][0], color='green', linestyle='--', linewidth=0.8, label='Oversold (' + str(config_strategy['trending']['rsi_threshold'][0]) + ')') # Plot Buy/Sell signals on RSI chart @@ -126,8 +105,8 @@ if __name__ == "__main__": logging.info("RSI data not available for plotting.") # Plot 3: BB Width - sns.lineplot(x=data_bb.index, y='BBWidth', data=data_bb, label='BB Width', ax=ax3) - sns.lineplot(x=data_bb.index, y='MarketRegime', data=data_bb, label='Market Regime (Sideways: 1, Trending: 0)', ax=ax3) + sns.lineplot(x=processed_data.index, y='BBWidth', data=processed_data, label='BB Width', ax=ax3) + sns.lineplot(x=processed_data.index, y='MarketRegime', data=processed_data, label='Market Regime (Sideways: 1, Trending: 0)', ax=ax3) ax3.set_title('Bollinger Bands Width') ax3.set_ylabel('BB Width') ax3.legend()