refactor to move inside strategy calculations

This commit is contained in:
Ajasra
2025-05-22 17:15:51 +08:00
parent 736b278ee2
commit 1107346594
2 changed files with 73 additions and 59 deletions

View File

@@ -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
# Add buy/sell conditions as columns to the DataFrame
data_bb['BuySignal'] = buy_condition
data_bb['SellSignal'] = sell_condition
return data_bb