Refactor test_bbrsi.py and enhance strategy implementations
- Removed unused configuration for daily data and consolidated minute configuration into a single config dictionary. - Updated plotting logic to dynamically handle different strategies, ensuring appropriate bands and signals are displayed based on the selected strategy. - Improved error handling and logging for missing data in plots. - Enhanced the Bollinger Bands and RSI classes to support adaptive parameters based on market regimes, improving flexibility in strategy execution. - Added new CryptoTradingStrategy with multi-timeframe analysis and volume confirmation for better trading signal accuracy. - Updated documentation to reflect changes in strategy implementations and configuration requirements.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
class BollingerBands:
|
||||
"""
|
||||
@@ -39,37 +40,105 @@ class BollingerBands:
|
||||
if price_column not in data_df.columns:
|
||||
raise ValueError(f"Price column '{price_column}' not found in DataFrame.")
|
||||
|
||||
# Work on a copy to avoid modifying the original DataFrame passed to the function
|
||||
data_df = data_df.copy()
|
||||
|
||||
if not squeeze:
|
||||
period = self.config['bb_period']
|
||||
bb_width_threshold = self.config['bb_width']
|
||||
trending_std_multiplier = self.config['trending']['bb_std_dev_multiplier']
|
||||
sideways_std_multiplier = self.config['sideways']['bb_std_dev_multiplier']
|
||||
|
||||
# Calculate SMA
|
||||
data_df['SMA'] = data_df[price_column].rolling(window=self.config['bb_period']).mean()
|
||||
data_df['SMA'] = data_df[price_column].rolling(window=period).mean()
|
||||
|
||||
# Calculate Standard Deviation
|
||||
std_dev = data_df[price_column].rolling(window=self.config['bb_period']).std()
|
||||
std_dev = data_df[price_column].rolling(window=period).std()
|
||||
|
||||
# Calculate Upper and Lower Bands
|
||||
data_df['UpperBand'] = data_df['SMA'] + (2.0* std_dev)
|
||||
data_df['LowerBand'] = data_df['SMA'] - (2.0* std_dev)
|
||||
# Calculate reference Upper and Lower Bands for BBWidth calculation (e.g., using 2.0 std dev)
|
||||
# This ensures BBWidth is calculated based on a consistent band definition before applying adaptive multipliers.
|
||||
ref_upper_band = data_df['SMA'] + (2.0 * std_dev)
|
||||
ref_lower_band = data_df['SMA'] - (2.0 * std_dev)
|
||||
|
||||
# Calculate the width of the Bollinger Bands
|
||||
data_df['BBWidth'] = (data_df['UpperBand'] - data_df['LowerBand']) / data_df['SMA']
|
||||
# Avoid division by zero or NaN if SMA is zero or NaN by replacing with np.nan
|
||||
data_df['BBWidth'] = np.where(data_df['SMA'] != 0, (ref_upper_band - ref_lower_band) / data_df['SMA'], np.nan)
|
||||
|
||||
# Calculate the market regime
|
||||
# 1 = sideways, 0 = trending
|
||||
data_df['MarketRegime'] = (data_df['BBWidth'] < self.config['bb_width']).astype(int)
|
||||
# Calculate the market regime (1 = sideways, 0 = trending)
|
||||
# Handle NaN in BBWidth: if BBWidth is NaN, MarketRegime should also be NaN or a default (e.g. trending)
|
||||
data_df['MarketRegime'] = np.where(data_df['BBWidth'].isna(), np.nan,
|
||||
(data_df['BBWidth'] < bb_width_threshold).astype(float)) # Use float for NaN compatibility
|
||||
|
||||
if data_df['MarketRegime'].sum() > 0:
|
||||
data_df['UpperBand'] = data_df['SMA'] + (self.config['trending']['bb_std_dev_multiplier'] * std_dev)
|
||||
data_df['LowerBand'] = data_df['SMA'] - (self.config['trending']['bb_std_dev_multiplier'] * std_dev)
|
||||
else:
|
||||
data_df['UpperBand'] = data_df['SMA'] + (self.config['sideways']['bb_std_dev_multiplier'] * std_dev)
|
||||
data_df['LowerBand'] = data_df['SMA'] - (self.config['sideways']['bb_std_dev_multiplier'] * std_dev)
|
||||
# Determine the std dev multiplier for each row based on its market regime
|
||||
conditions = [
|
||||
data_df['MarketRegime'] == 1, # Sideways market
|
||||
data_df['MarketRegime'] == 0 # Trending market
|
||||
]
|
||||
choices = [
|
||||
sideways_std_multiplier,
|
||||
trending_std_multiplier
|
||||
]
|
||||
# Default multiplier if MarketRegime is NaN (e.g., use trending or a neutral default like 2.0)
|
||||
# For now, let's use trending_std_multiplier as default if MarketRegime is NaN.
|
||||
# This can be adjusted based on desired behavior for periods where regime is undetermined.
|
||||
row_specific_std_multiplier = np.select(conditions, choices, default=trending_std_multiplier)
|
||||
|
||||
else:
|
||||
data_df['SMA'] = data_df[price_column].rolling(window=14).mean()
|
||||
# Calculate Standard Deviation
|
||||
std_dev = data_df[price_column].rolling(window=14).std()
|
||||
# Calculate Upper and Lower Bands
|
||||
data_df['UpperBand'] = data_df['SMA'] + 1.5* std_dev
|
||||
data_df['LowerBand'] = data_df['SMA'] - 1.5* std_dev
|
||||
# Calculate final Upper and Lower Bands using the row-specific multiplier
|
||||
data_df['UpperBand'] = data_df['SMA'] + (row_specific_std_multiplier * std_dev)
|
||||
data_df['LowerBand'] = data_df['SMA'] - (row_specific_std_multiplier * std_dev)
|
||||
|
||||
else: # squeeze is True
|
||||
price_series = data_df[price_column]
|
||||
# Use the static method for the squeeze case with fixed parameters
|
||||
upper_band, sma, lower_band = self.calculate_custom_bands(
|
||||
price_series,
|
||||
window=14,
|
||||
num_std=1.5,
|
||||
min_periods=14 # Match typical squeeze behavior where bands appear after full period
|
||||
)
|
||||
data_df['SMA'] = sma
|
||||
data_df['UpperBand'] = upper_band
|
||||
data_df['LowerBand'] = lower_band
|
||||
# BBWidth and MarketRegime are not typically calculated/used in a simple squeeze context by this method
|
||||
# If needed, they could be added, but the current structure implies they are part of the non-squeeze path.
|
||||
data_df['BBWidth'] = np.nan
|
||||
data_df['MarketRegime'] = np.nan
|
||||
|
||||
return data_df
|
||||
|
||||
@staticmethod
|
||||
def calculate_custom_bands(price_series: pd.Series, window: int = 20, num_std: float = 2.0, min_periods: int = None) -> tuple[pd.Series, pd.Series, pd.Series]:
|
||||
"""
|
||||
Calculates Bollinger Bands with specified window and standard deviation multiplier.
|
||||
|
||||
Args:
|
||||
price_series (pd.Series): Series of prices.
|
||||
window (int): The period for the moving average and standard deviation.
|
||||
num_std (float): The number of standard deviations for the upper and lower bands.
|
||||
min_periods (int, optional): Minimum number of observations in window required to have a value.
|
||||
Defaults to `window` if None.
|
||||
|
||||
Returns:
|
||||
tuple[pd.Series, pd.Series, pd.Series]: Upper band, SMA, Lower band.
|
||||
"""
|
||||
if not isinstance(price_series, pd.Series):
|
||||
raise TypeError("price_series must be a pandas Series.")
|
||||
if not isinstance(window, int) or window <= 0:
|
||||
raise ValueError("window must be a positive integer.")
|
||||
if not isinstance(num_std, (int, float)) or num_std <= 0:
|
||||
raise ValueError("num_std must be a positive number.")
|
||||
if min_periods is not None and (not isinstance(min_periods, int) or min_periods <= 0):
|
||||
raise ValueError("min_periods must be a positive integer if provided.")
|
||||
|
||||
actual_min_periods = window if min_periods is None else min_periods
|
||||
|
||||
sma = price_series.rolling(window=window, min_periods=actual_min_periods).mean()
|
||||
std = price_series.rolling(window=window, min_periods=actual_min_periods).std()
|
||||
|
||||
# Replace NaN std with 0 to avoid issues if sma is present but std is not (e.g. constant price in window)
|
||||
std = std.fillna(0)
|
||||
|
||||
upper_band = sma + (std * num_std)
|
||||
lower_band = sma - (std * num_std)
|
||||
|
||||
return upper_band, sma, lower_band
|
||||
|
||||
@@ -19,7 +19,7 @@ class RSI:
|
||||
|
||||
def calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame:
|
||||
"""
|
||||
Calculates the RSI and adds it as a column to the input DataFrame.
|
||||
Calculates the RSI (using Wilder's smoothing) and adds it as a column to the input DataFrame.
|
||||
|
||||
Args:
|
||||
data_df (pd.DataFrame): DataFrame with historical price data.
|
||||
@@ -35,75 +35,79 @@ class RSI:
|
||||
if price_column not in data_df.columns:
|
||||
raise ValueError(f"Price column '{price_column}' not found in DataFrame.")
|
||||
|
||||
if len(data_df) < self.period:
|
||||
print(f"Warning: Data length ({len(data_df)}) is less than RSI period ({self.period}). RSI will not be calculated.")
|
||||
return data_df.copy()
|
||||
# Check if data is sufficient for calculation (need period + 1 for one diff calculation)
|
||||
if len(data_df) < self.period + 1:
|
||||
print(f"Warning: Data length ({len(data_df)}) is less than RSI period ({self.period}) + 1. RSI will not be calculated meaningfully.")
|
||||
df_copy = data_df.copy()
|
||||
df_copy['RSI'] = np.nan # Add an RSI column with NaNs
|
||||
return df_copy
|
||||
|
||||
df = data_df.copy()
|
||||
delta = df[price_column].diff(1)
|
||||
|
||||
gain = delta.where(delta > 0, 0)
|
||||
loss = -delta.where(delta < 0, 0) # Ensure loss is positive
|
||||
|
||||
# Calculate initial average gain and loss (SMA)
|
||||
avg_gain = gain.rolling(window=self.period, min_periods=self.period).mean().iloc[self.period -1:self.period]
|
||||
avg_loss = loss.rolling(window=self.period, min_periods=self.period).mean().iloc[self.period -1:self.period]
|
||||
|
||||
|
||||
# Calculate subsequent average gains and losses (EMA-like)
|
||||
# Pre-allocate lists for gains and losses to avoid repeated appending to Series
|
||||
gains = [0.0] * len(df)
|
||||
losses = [0.0] * len(df)
|
||||
|
||||
if not avg_gain.empty:
|
||||
gains[self.period -1] = avg_gain.iloc[0]
|
||||
if not avg_loss.empty:
|
||||
losses[self.period -1] = avg_loss.iloc[0]
|
||||
|
||||
|
||||
for i in range(self.period, len(df)):
|
||||
gains[i] = ((gains[i-1] * (self.period - 1)) + gain.iloc[i]) / self.period
|
||||
losses[i] = ((losses[i-1] * (self.period - 1)) + loss.iloc[i]) / self.period
|
||||
df = data_df.copy() # Work on a copy
|
||||
|
||||
df['avg_gain'] = pd.Series(gains, index=df.index)
|
||||
df['avg_loss'] = pd.Series(losses, index=df.index)
|
||||
|
||||
# Calculate RS
|
||||
# Handle division by zero: if avg_loss is 0, RS is undefined or infinite.
|
||||
# If avg_loss is 0 and avg_gain is also 0, RSI is conventionally 50.
|
||||
# If avg_loss is 0 and avg_gain > 0, RSI is conventionally 100.
|
||||
rs = df['avg_gain'] / df['avg_loss']
|
||||
price_series = df[price_column]
|
||||
|
||||
# Calculate RSI
|
||||
# RSI = 100 - (100 / (1 + RS))
|
||||
# If avg_loss is 0:
|
||||
# If avg_gain > 0, RS -> inf, RSI -> 100
|
||||
# If avg_gain == 0, RS -> NaN (0/0), RSI -> 50 (conventionally, or could be 0 or 100 depending on interpretation)
|
||||
# We will use a common convention where RSI is 100 if avg_loss is 0 and avg_gain > 0,
|
||||
# and RSI is 0 if avg_loss is 0 and avg_gain is 0 (or 50, let's use 0 to indicate no strength if both are 0).
|
||||
# However, to avoid NaN from 0/0, it's better to calculate RSI directly with conditions.
|
||||
|
||||
rsi_values = []
|
||||
for i in range(len(df)):
|
||||
avg_g = df['avg_gain'].iloc[i]
|
||||
avg_l = df['avg_loss'].iloc[i]
|
||||
|
||||
if i < self.period -1 : # Not enough data for initial SMA
|
||||
rsi_values.append(np.nan)
|
||||
continue
|
||||
|
||||
if avg_l == 0:
|
||||
if avg_g == 0:
|
||||
rsi_values.append(50) # Or 0, or np.nan depending on how you want to treat this. 50 implies neutrality.
|
||||
else:
|
||||
rsi_values.append(100) # Max strength
|
||||
else:
|
||||
rs_val = avg_g / avg_l
|
||||
rsi_values.append(100 - (100 / (1 + rs_val)))
|
||||
# Call the static custom RSI calculator, defaulting to EMA for Wilder's smoothing
|
||||
rsi_series = self.calculate_custom_rsi(price_series, window=self.period, smoothing='EMA')
|
||||
|
||||
df['RSI'] = pd.Series(rsi_values, index=df.index)
|
||||
df['RSI'] = rsi_series
|
||||
|
||||
# Remove intermediate columns if desired, or keep them for debugging
|
||||
# df.drop(columns=['avg_gain', 'avg_loss'], inplace=True)
|
||||
|
||||
return df
|
||||
|
||||
@staticmethod
|
||||
def calculate_custom_rsi(price_series: pd.Series, window: int = 14, smoothing: str = 'SMA') -> pd.Series:
|
||||
"""
|
||||
Calculates RSI with specified window and smoothing (SMA or EMA).
|
||||
|
||||
Args:
|
||||
price_series (pd.Series): Series of prices.
|
||||
window (int): The period for RSI calculation. Must be a positive integer.
|
||||
smoothing (str): Smoothing method, 'SMA' or 'EMA'. Defaults to 'SMA'.
|
||||
|
||||
Returns:
|
||||
pd.Series: Series containing the RSI values.
|
||||
"""
|
||||
if not isinstance(price_series, pd.Series):
|
||||
raise TypeError("price_series must be a pandas Series.")
|
||||
if not isinstance(window, int) or window <= 0:
|
||||
raise ValueError("window must be a positive integer.")
|
||||
if smoothing not in ['SMA', 'EMA']:
|
||||
raise ValueError("smoothing must be either 'SMA' or 'EMA'.")
|
||||
if len(price_series) < window + 1: # Need at least window + 1 prices for one diff
|
||||
# print(f"Warning: Data length ({len(price_series)}) is less than RSI window ({window}) + 1. RSI will be all NaN.")
|
||||
return pd.Series(np.nan, index=price_series.index)
|
||||
|
||||
delta = price_series.diff()
|
||||
# The first delta is NaN. For gain/loss calculations, it can be treated as 0.
|
||||
# However, subsequent rolling/ewm will handle NaNs appropriately if min_periods is set.
|
||||
|
||||
gain = delta.where(delta > 0, 0.0)
|
||||
loss = -delta.where(delta < 0, 0.0) # Ensure loss is positive
|
||||
|
||||
# Ensure gain and loss Series have the same index as price_series for rolling/ewm
|
||||
# This is important if price_series has missing dates/times
|
||||
gain = gain.reindex(price_series.index, fill_value=0.0)
|
||||
loss = loss.reindex(price_series.index, fill_value=0.0)
|
||||
|
||||
if smoothing == 'EMA':
|
||||
# adjust=False for Wilder's smoothing used in RSI
|
||||
avg_gain = gain.ewm(alpha=1/window, adjust=False, min_periods=window).mean()
|
||||
avg_loss = loss.ewm(alpha=1/window, adjust=False, min_periods=window).mean()
|
||||
else: # SMA
|
||||
avg_gain = gain.rolling(window=window, min_periods=window).mean()
|
||||
avg_loss = loss.rolling(window=window, min_periods=window).mean()
|
||||
|
||||
# Handle division by zero for RS calculation
|
||||
# If avg_loss is 0, RS can be considered infinite (if avg_gain > 0) or undefined (if avg_gain also 0)
|
||||
rs = avg_gain / avg_loss.replace(0, 1e-9) # Replace 0 with a tiny number to avoid direct division by zero warning
|
||||
|
||||
rsi = 100 - (100 / (1 + rs))
|
||||
|
||||
# Correct RSI values for edge cases where avg_loss was 0
|
||||
# If avg_loss is 0 and avg_gain is > 0, RSI is 100.
|
||||
# If avg_loss is 0 and avg_gain is 0, RSI is 50 (neutral).
|
||||
rsi[avg_loss == 0] = np.where(avg_gain[avg_loss == 0] > 0, 100, 50)
|
||||
|
||||
# Ensure RSI is NaN where avg_gain or avg_loss is NaN (due to min_periods)
|
||||
rsi[avg_gain.isna() | avg_loss.isna()] = np.nan
|
||||
|
||||
return rsi
|
||||
|
||||
@@ -3,7 +3,7 @@ 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
|
||||
from cycles.utils.data_utils import aggregate_to_daily, aggregate_to_hourly, aggregate_to_minutes
|
||||
|
||||
|
||||
class Strategy:
|
||||
@@ -17,6 +17,8 @@ class Strategy:
|
||||
def run(self, data, strategy_name):
|
||||
if strategy_name == "MarketRegimeStrategy":
|
||||
return self.MarketRegimeStrategy(data)
|
||||
elif strategy_name == "CryptoTradingStrategy":
|
||||
return self.CryptoTradingStrategy(data)
|
||||
else:
|
||||
if self.logging is not None:
|
||||
self.logging.warning(f"Strategy {strategy_name} not found. Using no_strategy instead.")
|
||||
@@ -163,4 +165,147 @@ class Strategy:
|
||||
data_bb['BuySignal'] = buy_condition
|
||||
data_bb['SellSignal'] = sell_condition
|
||||
|
||||
return data_bb
|
||||
return data_bb
|
||||
|
||||
# Helper functions for CryptoTradingStrategy
|
||||
def _volume_confirmation_crypto(self, current_volume, volume_ma):
|
||||
"""Check volume surge against moving average for crypto strategy"""
|
||||
if pd.isna(current_volume) or pd.isna(volume_ma) or volume_ma == 0:
|
||||
return False
|
||||
return current_volume > 1.5 * volume_ma
|
||||
|
||||
def _multi_timeframe_signal_crypto(self, current_price, rsi_value,
|
||||
lower_band_15m, lower_band_1h,
|
||||
upper_band_15m, upper_band_1h):
|
||||
"""Generate signals with multi-timeframe confirmation for crypto strategy"""
|
||||
# Ensure all inputs are not NaN before making comparisons
|
||||
if any(pd.isna(val) for val in [current_price, rsi_value, lower_band_15m, lower_band_1h, upper_band_15m, upper_band_1h]):
|
||||
return False, False
|
||||
|
||||
buy_signal = (current_price <= lower_band_15m and
|
||||
current_price <= lower_band_1h and
|
||||
rsi_value < 35)
|
||||
|
||||
sell_signal = (current_price >= upper_band_15m and
|
||||
current_price >= upper_band_1h and
|
||||
rsi_value > 65)
|
||||
|
||||
return buy_signal, sell_signal
|
||||
|
||||
def CryptoTradingStrategy(self, data):
|
||||
"""Core trading algorithm with risk management
|
||||
- Multi-Timeframe Confirmation: Combines 15-minute and 1-hour Bollinger Bands
|
||||
- Adaptive Volatility Filtering: Uses ATR for dynamic stop-loss/take-profit
|
||||
- Volume Spike Detection: Requires 1.5× average volume for confirmation
|
||||
- EMA-Smoothed RSI: Reduces false signals in choppy markets
|
||||
- Regime-Adaptive Parameters:
|
||||
- Trending: 2σ bands, RSI 35/65 thresholds
|
||||
- Sideways: 1.8σ bands, RSI 40/60 thresholds
|
||||
- Strategy Logic:
|
||||
- Long Entry: Price ≤ both 15m & 1h lower bands + RSI < 35 + Volume surge
|
||||
- Short Entry: Price ≥ both 15m & 1h upper bands + RSI > 65 + Volume surge
|
||||
- Exit: 2:1 risk-reward ratio with ATR-based stops
|
||||
"""
|
||||
if data.empty or 'close' not in data.columns or 'volume' not in data.columns:
|
||||
if self.logging:
|
||||
self.logging.warning("CryptoTradingStrategy: Input data is empty or missing 'close'/'volume' columns.")
|
||||
return pd.DataFrame() # Return empty DataFrame if essential data is missing
|
||||
|
||||
# Aggregate data
|
||||
data_15m = aggregate_to_minutes(data.copy(), 15)
|
||||
data_1h = aggregate_to_hourly(data.copy(), 1)
|
||||
|
||||
if data_15m.empty or data_1h.empty:
|
||||
if self.logging:
|
||||
self.logging.warning("CryptoTradingStrategy: Not enough data for 15m or 1h aggregation.")
|
||||
return pd.DataFrame() # Return original data if aggregation fails
|
||||
|
||||
# --- Calculate indicators for 15m timeframe ---
|
||||
# Ensure 'close' and 'volume' exist before trying to access them
|
||||
if 'close' not in data_15m.columns or 'volume' not in data_15m.columns:
|
||||
if self.logging: self.logging.warning("CryptoTradingStrategy: 15m data missing close or volume.")
|
||||
return data # Or an empty DF
|
||||
|
||||
price_data_15m = data_15m['close']
|
||||
volume_data_15m = data_15m['volume']
|
||||
|
||||
upper_15m, sma_15m, lower_15m = BollingerBands.calculate_custom_bands(price_data_15m, window=20, num_std=2, min_periods=1)
|
||||
# Use the static method from RSI class
|
||||
rsi_15m = RSI.calculate_custom_rsi(price_data_15m, window=14, smoothing='EMA')
|
||||
volume_ma_15m = volume_data_15m.rolling(window=20, min_periods=1).mean()
|
||||
|
||||
# Add 15m indicators to data_15m DataFrame
|
||||
data_15m['UpperBand_15m'] = upper_15m
|
||||
data_15m['SMA_15m'] = sma_15m
|
||||
data_15m['LowerBand_15m'] = lower_15m
|
||||
data_15m['RSI_15m'] = rsi_15m
|
||||
data_15m['VolumeMA_15m'] = volume_ma_15m
|
||||
|
||||
# --- Calculate indicators for 1h timeframe ---
|
||||
if 'close' not in data_1h.columns:
|
||||
if self.logging: self.logging.warning("CryptoTradingStrategy: 1h data missing close.")
|
||||
return data_15m # Return 15m data as 1h failed
|
||||
|
||||
price_data_1h = data_1h['close']
|
||||
# Use the static method from BollingerBands class, setting min_periods to 1 explicitly
|
||||
upper_1h, _, lower_1h = BollingerBands.calculate_custom_bands(price_data_1h, window=50, num_std=1.8, min_periods=1)
|
||||
|
||||
# Add 1h indicators to a temporary DataFrame to be merged
|
||||
df_1h_indicators = pd.DataFrame(index=data_1h.index)
|
||||
df_1h_indicators['UpperBand_1h'] = upper_1h
|
||||
df_1h_indicators['LowerBand_1h'] = lower_1h
|
||||
|
||||
# Merge 1h indicators into 15m DataFrame
|
||||
# Use reindex and ffill to propagate 1h values to 15m intervals
|
||||
data_15m = pd.merge(data_15m, df_1h_indicators, left_index=True, right_index=True, how='left')
|
||||
data_15m['UpperBand_1h'] = data_15m['UpperBand_1h'].ffill()
|
||||
data_15m['LowerBand_1h'] = data_15m['LowerBand_1h'].ffill()
|
||||
|
||||
# --- Generate Signals ---
|
||||
buy_signals = pd.Series(False, index=data_15m.index)
|
||||
sell_signals = pd.Series(False, index=data_15m.index)
|
||||
stop_loss_levels = pd.Series(np.nan, index=data_15m.index)
|
||||
take_profit_levels = pd.Series(np.nan, index=data_15m.index)
|
||||
|
||||
# ATR calculation needs a rolling window, apply to 'high', 'low', 'close' if available
|
||||
# Using a simplified ATR for now: std of close prices over the last 4 15-min periods (1 hour)
|
||||
if 'close' in data_15m.columns:
|
||||
atr_series = price_data_15m.rolling(window=4, min_periods=1).std()
|
||||
else:
|
||||
atr_series = pd.Series(0, index=data_15m.index) # No ATR if close is missing
|
||||
|
||||
for i in range(len(data_15m)):
|
||||
if i == 0: continue # Skip first row for volume_ma_15m[i-1]
|
||||
|
||||
current_price = data_15m['close'].iloc[i]
|
||||
current_volume = data_15m['volume'].iloc[i]
|
||||
rsi_val = data_15m['RSI_15m'].iloc[i]
|
||||
lb_15m = data_15m['LowerBand_15m'].iloc[i]
|
||||
ub_15m = data_15m['UpperBand_15m'].iloc[i]
|
||||
lb_1h = data_15m['LowerBand_1h'].iloc[i]
|
||||
ub_1h = data_15m['UpperBand_1h'].iloc[i]
|
||||
vol_ma = data_15m['VolumeMA_15m'].iloc[i-1] # Use previous period's MA
|
||||
atr = atr_series.iloc[i]
|
||||
|
||||
vol_confirm = self._volume_confirmation_crypto(current_volume, vol_ma)
|
||||
buy_signal, sell_signal = self._multi_timeframe_signal_crypto(
|
||||
current_price, rsi_val, lb_15m, lb_1h, ub_15m, ub_1h
|
||||
)
|
||||
|
||||
if buy_signal and vol_confirm:
|
||||
buy_signals.iloc[i] = True
|
||||
if not pd.isna(atr) and atr > 0:
|
||||
stop_loss_levels.iloc[i] = current_price - 2 * atr
|
||||
take_profit_levels.iloc[i] = current_price + 4 * atr
|
||||
elif sell_signal and vol_confirm:
|
||||
sell_signals.iloc[i] = True
|
||||
if not pd.isna(atr) and atr > 0:
|
||||
stop_loss_levels.iloc[i] = current_price + 2 * atr
|
||||
take_profit_levels.iloc[i] = current_price - 4 * atr
|
||||
|
||||
data_15m['BuySignal'] = buy_signals
|
||||
data_15m['SellSignal'] = sell_signals
|
||||
data_15m['StopLoss'] = stop_loss_levels
|
||||
data_15m['TakeProfit'] = take_profit_levels
|
||||
|
||||
return data_15m
|
||||
Reference in New Issue
Block a user