refactor to move inside strategy calculations
This commit is contained in:
parent
736b278ee2
commit
1107346594
@ -2,6 +2,8 @@ import pandas as pd
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from cycles.Analysis.boillinger_band import BollingerBands
|
from cycles.Analysis.boillinger_band import BollingerBands
|
||||||
|
from cycles.Analysis.rsi import RSI
|
||||||
|
from cycles.utils.data_utils import aggregate_to_daily
|
||||||
|
|
||||||
|
|
||||||
class Strategy:
|
class Strategy:
|
||||||
@ -65,45 +67,74 @@ class Strategy:
|
|||||||
Sell: Price ≥ Upper Band ∧ RSI ≥ 60
|
Sell: Price ≥ Upper Band ∧ RSI ≥ 60
|
||||||
|
|
||||||
Enhanced with RSI Bollinger Squeeze for signal confirmation when enabled.
|
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
|
# Initialize conditions as all False
|
||||||
buy_condition = pd.Series(False, index=data.index)
|
buy_condition = pd.Series(False, index=data_bb.index)
|
||||||
sell_condition = pd.Series(False, index=data.index)
|
sell_condition = pd.Series(False, index=data_bb.index)
|
||||||
|
|
||||||
# Create masks for different market regimes
|
# Create masks for different market regimes
|
||||||
sideways_mask = data['MarketRegime'] > 0
|
# MarketRegime is expected to be in data_bb from BollingerBands calculation
|
||||||
trending_mask = data['MarketRegime'] <= 0
|
sideways_mask = data_bb['MarketRegime'] > 0
|
||||||
valid_data_mask = ~data['MarketRegime'].isna() # Handle potential NaN values
|
trending_mask = data_bb['MarketRegime'] <= 0
|
||||||
|
valid_data_mask = ~data_bb['MarketRegime'].isna() # Handle potential NaN values
|
||||||
|
|
||||||
# Calculate volume spike (≥1.5× 20D Avg)
|
# Calculate volume spike (≥1.5× 20D Avg)
|
||||||
if 'volume' in data.columns:
|
# 'volume' column should be present in the input 'data', and thus in 'data_bb'
|
||||||
volume_20d_avg = data['volume'].rolling(window=20).mean()
|
if 'volume' in data_bb.columns:
|
||||||
volume_spike = data['volume'] >= 1.5 * volume_20d_avg
|
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
|
# Additional volume contraction filter for sideways markets
|
||||||
volume_30d_avg = data['volume'].rolling(window=30).mean()
|
volume_30d_avg = data_bb['volume'].rolling(window=30).mean()
|
||||||
volume_contraction = data['volume'] < 0.7 * volume_30d_avg
|
volume_contraction = data_bb['volume'] < 0.7 * volume_30d_avg
|
||||||
else:
|
else:
|
||||||
# If volume data is not available, assume no volume spike
|
# If volume data is not available, assume no volume spike
|
||||||
volume_spike = pd.Series(False, index=data.index)
|
volume_spike = pd.Series(False, index=data_bb.index)
|
||||||
volume_contraction = pd.Series(False, index=data.index)
|
volume_contraction = pd.Series(False, index=data_bb.index)
|
||||||
if self.logging is not None:
|
if self.logging is not None:
|
||||||
self.logging.warning("Volume data not available. Volume conditions will not be triggered.")
|
self.logging.warning("Volume data not available. Volume conditions will not be triggered.")
|
||||||
|
|
||||||
# Calculate RSI Bollinger Squeeze confirmation
|
# Calculate RSI Bollinger Squeeze confirmation
|
||||||
if 'RSI' in data.columns:
|
# RSI column is now part of data_bb
|
||||||
oversold_rsi, overbought_rsi = self.rsi_bollinger_confirmation(data['RSI'])
|
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:
|
else:
|
||||||
oversold_rsi = pd.Series(False, index=data.index)
|
oversold_rsi = pd.Series(False, index=data_bb.index)
|
||||||
overbought_rsi = pd.Series(False, index=data.index)
|
overbought_rsi = pd.Series(False, index=data_bb.index)
|
||||||
if self.logging is not None:
|
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. RSI Bollinger Squeeze will not be triggered.")
|
self.logging.warning("RSI data not available or all NaN. RSI Bollinger Squeeze will not be triggered.")
|
||||||
|
|
||||||
# Calculate conditions for sideways market (Mean Reversion)
|
# Calculate conditions for sideways market (Mean Reversion)
|
||||||
if sideways_mask.any():
|
if sideways_mask.any():
|
||||||
sideways_buy = (data['close'] <= data['LowerBand']) & (data['RSI'] <= 40)
|
sideways_buy = (data_bb['close'] <= data_bb['LowerBand']) & (data_bb['RSI'] <= 40)
|
||||||
sideways_sell = (data['close'] >= data['UpperBand']) & (data['RSI'] >= 60)
|
sideways_sell = (data_bb['close'] >= data_bb['UpperBand']) & (data_bb['RSI'] >= 60)
|
||||||
|
|
||||||
# Add enhanced confirmation for sideways markets
|
# Add enhanced confirmation for sideways markets
|
||||||
if self.config.get("SqueezeStrategy", False):
|
if self.config.get("SqueezeStrategy", False):
|
||||||
@ -116,8 +147,8 @@ class Strategy:
|
|||||||
|
|
||||||
# Calculate conditions for trending market (Breakout Mode)
|
# Calculate conditions for trending market (Breakout Mode)
|
||||||
if trending_mask.any():
|
if trending_mask.any():
|
||||||
trending_buy = (data['close'] < data['LowerBand']) & (data['RSI'] < 50) & volume_spike
|
trending_buy = (data_bb['close'] < data_bb['LowerBand']) & (data_bb['RSI'] < 50) & volume_spike
|
||||||
trending_sell = (data['close'] > data['UpperBand']) & (data['RSI'] > 50) & volume_spike
|
trending_sell = (data_bb['close'] > data_bb['UpperBand']) & (data_bb['RSI'] > 50) & volume_spike
|
||||||
|
|
||||||
# Add enhanced confirmation for trending markets
|
# Add enhanced confirmation for trending markets
|
||||||
if self.config.get("SqueezeStrategy", False):
|
if self.config.get("SqueezeStrategy", False):
|
||||||
@ -128,4 +159,8 @@ class Strategy:
|
|||||||
buy_condition = buy_condition | (trending_buy & trending_mask & valid_data_mask)
|
buy_condition = buy_condition | (trending_buy & trending_mask & valid_data_mask)
|
||||||
sell_condition = sell_condition | (trending_sell & trending_mask & valid_data_mask)
|
sell_condition = sell_condition | (trending_sell & trending_mask & valid_data_mask)
|
||||||
|
|
||||||
return buy_condition, sell_condition
|
# Add buy/sell conditions as columns to the DataFrame
|
||||||
|
data_bb['BuySignal'] = buy_condition
|
||||||
|
data_bb['SellSignal'] = sell_condition
|
||||||
|
|
||||||
|
return data_bb
|
||||||
@ -5,8 +5,6 @@ import pandas as pd
|
|||||||
|
|
||||||
from cycles.utils.storage import Storage
|
from cycles.utils.storage import Storage
|
||||||
from cycles.utils.data_utils import aggregate_to_daily
|
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
|
from cycles.Analysis.strategies import Strategy
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@ -59,44 +57,25 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
data = storage.load_data(config["data_file"], config["start_date"], config["stop_date"])
|
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)
|
strategy = Strategy(config=config_strategy, logging=logging)
|
||||||
data_bb = bb.calculate(df_to_plot.copy())
|
processed_data = strategy.run(data.copy(), config_strategy["strategy_name"])
|
||||||
|
|
||||||
rsi_calculator = RSI(config=config_strategy)
|
buy_condition = processed_data.get('BuySignal', pd.Series(False, index=processed_data.index)).astype(bool)
|
||||||
data_with_rsi = rsi_calculator.calculate(df_to_plot.copy(), price_column='close')
|
sell_condition = processed_data.get('SellSignal', pd.Series(False, index=processed_data.index)).astype(bool)
|
||||||
|
|
||||||
# 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.")
|
|
||||||
|
|
||||||
strategy = Strategy(config=config_strategy)
|
buy_signals = processed_data[buy_condition]
|
||||||
buy_condition, sell_condition = strategy.run(data_bb, config_strategy["strategy_name"])
|
sell_signals = processed_data[sell_condition]
|
||||||
|
|
||||||
buy_signals = data_bb[buy_condition]
|
|
||||||
sell_signals = data_bb[sell_condition]
|
|
||||||
|
|
||||||
# plot the data with seaborn library
|
# 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
|
# Create a figure with two subplots, sharing the x-axis
|
||||||
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 8), sharex=True)
|
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 8), sharex=True)
|
||||||
|
|
||||||
# Plot 1: Close Price and Bollinger Bands
|
# 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=processed_data.index, y='close', data=processed_data, 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=processed_data.index, y='UpperBand', data=processed_data, 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='LowerBand', data=processed_data, label='Lower Band (BB)', ax=ax1)
|
||||||
# Plot Buy/Sell signals on Price chart
|
# Plot Buy/Sell signals on Price chart
|
||||||
if not buy_signals.empty:
|
if not buy_signals.empty:
|
||||||
ax1.scatter(buy_signals.index, buy_signals['close'], color='green', marker='o', s=20, label='Buy Signal', zorder=5)
|
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)
|
ax1.grid(True)
|
||||||
|
|
||||||
# Plot 2: RSI
|
# Plot 2: RSI
|
||||||
if 'RSI' in data_bb.columns: # Check data_bb now as it should contain RSI
|
if 'RSI' in processed_data.columns:
|
||||||
sns.lineplot(x=data_bb.index, y='RSI', data=data_bb, label='RSI (' + str(config_strategy["rsi_period"]) + ')', ax=ax2, color='purple')
|
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"][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]) + ')')
|
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
|
# Plot Buy/Sell signals on RSI chart
|
||||||
@ -126,8 +105,8 @@ if __name__ == "__main__":
|
|||||||
logging.info("RSI data not available for plotting.")
|
logging.info("RSI data not available for plotting.")
|
||||||
|
|
||||||
# Plot 3: BB Width
|
# Plot 3: BB Width
|
||||||
sns.lineplot(x=data_bb.index, y='BBWidth', data=data_bb, label='BB Width', ax=ax3)
|
sns.lineplot(x=processed_data.index, y='BBWidth', data=processed_data, 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='MarketRegime', data=processed_data, label='Market Regime (Sideways: 1, Trending: 0)', ax=ax3)
|
||||||
ax3.set_title('Bollinger Bands Width')
|
ax3.set_title('Bollinger Bands Width')
|
||||||
ax3.set_ylabel('BB Width')
|
ax3.set_ylabel('BB Width')
|
||||||
ax3.legend()
|
ax3.legend()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user