From 7af8cdcb3290af424799a4b99f6befa3ddcaeb76 Mon Sep 17 00:00:00 2001 From: "Vasily.onl" Date: Fri, 23 May 2025 15:21:40 +0800 Subject: [PATCH] Enhance Bollinger Bands validation and add DatetimeIndex handling in strategies - Added validation to ensure the specified price column exists in the DataFrame for Bollinger Bands calculations. - Introduced a new method to ensure the DataFrame has a proper DatetimeIndex, improving time-series operations in strategy processing. - Updated strategy run method to call the new DatetimeIndex validation method before processing data. - Improved logging for better traceability of data transformations and potential issues. --- cycles/Analysis/boillinger_band.py | 5 +-- cycles/Analysis/strategies.py | 55 ++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/cycles/Analysis/boillinger_band.py b/cycles/Analysis/boillinger_band.py index 26a54da..02dce37 100644 --- a/cycles/Analysis/boillinger_band.py +++ b/cycles/Analysis/boillinger_band.py @@ -37,12 +37,13 @@ class BollingerBands: 'UpperBand', 'LowerBand'. """ - 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 price_column not in data_df.columns: + raise ValueError(f"Price column '{price_column}' not found in DataFrame.") + if not squeeze: period = self.config['bb_period'] bb_width_threshold = self.config['bb_width'] diff --git a/cycles/Analysis/strategies.py b/cycles/Analysis/strategies.py index d4c0696..74b3935 100644 --- a/cycles/Analysis/strategies.py +++ b/cycles/Analysis/strategies.py @@ -14,7 +14,56 @@ class Strategy: self.config = config self.logging = logging + def _ensure_datetime_index(self, data): + """ + Ensure the DataFrame has a DatetimeIndex for proper time-series operations. + If the DataFrame has a 'timestamp' column but not a DatetimeIndex, convert it. + + Args: + data (DataFrame): Input DataFrame + + Returns: + DataFrame: DataFrame with proper DatetimeIndex + """ + if data.empty: + return data + + # Check if we have a DatetimeIndex already + if isinstance(data.index, pd.DatetimeIndex): + return data + + # Check if we have a 'timestamp' column that we can use as index + if 'timestamp' in data.columns: + data_copy = data.copy() + # Convert timestamp column to datetime if it's not already + if not pd.api.types.is_datetime64_any_dtype(data_copy['timestamp']): + data_copy['timestamp'] = pd.to_datetime(data_copy['timestamp']) + # Set timestamp as index and drop the column + data_copy = data_copy.set_index('timestamp') + if self.logging: + self.logging.info("Converted 'timestamp' column to DatetimeIndex for strategy processing.") + return data_copy + + # If we have a regular index but it might be datetime strings, try to convert + try: + if data.index.dtype == 'object': + data_copy = data.copy() + data_copy.index = pd.to_datetime(data_copy.index) + if self.logging: + self.logging.info("Converted index to DatetimeIndex for strategy processing.") + return data_copy + except: + pass + + # If we can't create a proper DatetimeIndex, warn and return as-is + if self.logging: + self.logging.warning("Could not create DatetimeIndex for strategy processing. Time-based operations may fail.") + return data + def run(self, data, strategy_name): + # Ensure proper DatetimeIndex before processing + data = self._ensure_datetime_index(data) + if strategy_name == "MarketRegimeStrategy": result = self.MarketRegimeStrategy(data) return self.standardize_output(result, strategy_name) @@ -126,8 +175,8 @@ class Strategy: DataFrame: A unified DataFrame containing original data, BB, RSI, and signals. """ - # data = aggregate_to_hourly(data, 4) - data = aggregate_to_daily(data) + data = aggregate_to_hourly(data, 1) + # data = aggregate_to_daily(data) # Calculate Bollinger Bands bb_calculator = BollingerBands(config=self.config) @@ -263,6 +312,8 @@ class Strategy: 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 + + print(f"data: {data.head()}") # Aggregate data data_15m = aggregate_to_minutes(data.copy(), 15)