Files
Chan/strategies/ChanLun_XGB.py
T
2025-04-25 11:00:20 +08:00

560 lines
28 KiB
Python

import logging
from functools import reduce
from typing import Dict
import numpy as np
import talib.abstract as ta
from pandas import DataFrame
from technical import qtpylib
from talib import MACD, RSI
from datetime import datetime
import pandas as pd
import uuid
from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__)
# freqtrade backtesting --config user_data/ChanLun_XGB.json --strategy ChanLun_XGB --freqaimodel XGBoostClassifier --timerange=20250401-20250421
class ChanLun_XGB1(IStrategy):
minimal_roi = {"0": 0.1, "240": -1}
plot_config = {
"main_plot": {},
"subplots": {
"&-s_close": {"&-s_close": {"color": "blue"}},
"do_predict": {"do_predict": {"color": "brown"}},
},
}
process_only_new_candles = True
stoploss = -0.05
use_exit_signal = True
startup_candle_count: int = 40
can_short = True
freqai_info = {
"feature_parameters": {
"label_period_candles": 24
}
}
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: Dict, **kwargs) -> DataFrame:
"""Basic technical indicators for various periods."""
logger.info("Starting feature_engineering_expand_all")
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=period, stds=2.2)
dataframe["bb_lowerband-period"] = bollinger["lower"]
dataframe["bb_middleband-period"] = bollinger["mid"]
dataframe["bb_upperband-period"] = bollinger["upper"]
dataframe["%-bb_width-period"] = (
(dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"]) / dataframe["bb_middleband-period"]
)
dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"]
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
dataframe["%-relative_volume-period"] = dataframe["volume"] / dataframe["volume"].rolling(period).mean()
logger.info("Completed feature_engineering_expand_all")
return dataframe.fillna(0)
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""Basic price and volume features."""
logger.info("Starting feature_engineering_expand_basic")
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
logger.info("Completed feature_engineering_expand_basic")
return dataframe.fillna(0)
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""Advanced feature engineering with Chan Lun and technical indicators."""
logger.info(f"Starting feature_engineering_standard for pair {metadata.get('pair', 'unknown')}")
if dataframe.empty:
return dataframe
try:
df = dataframe.copy()
# Time-based features
df["%-day_of_week"] = df["date"].dt.dayofweek
df["%-hour_of_day"] = df["date"].dt.hour
# Fractal detection
df = self.detect_fractals(df)
logger.info("Fractal detection completed")
# MACD and RSI
macd, signal, hist = MACD(df['close'], fastperiod=12, slowperiod=26, signalperiod=9)
df['macd'] = macd
df['macd_signal'] = signal
df['macd_hist'] = hist
df['macd_hist_sum'] = df['macd_hist'].rolling(5).sum()
df["%-rsi-14"] = RSI(df['close'], timeperiod=14)
logger.info("MACD and RSI calculated")
# Candlestick features
df["%-close_open_diff"] = (df["close"] - df["open"]) / df["open"].replace(0, np.nan)
df["%-body_length"] = abs(df["close"] - df["open"]) / df["close"].replace(0, np.nan)
df["%-upper_shadow"] = (df["high"] - df[["open", "close"]].max(axis=1)) / df["close"].replace(0, np.nan)
df["%-lower_shadow"] = (df[["open", "close"]].min(axis=1) - df["low"]) / df["close"].replace(0, np.nan)
# Candle color and trend
df["%-candle_color"] = (df["close"] > df["open"]).astype(int) * 2 - 1
df["%-consec_same_color"] = df["%-candle_color"].groupby((df["%-candle_color"] != df["%-candle_color"].shift()).cumsum()).cumcount() + 1
# Fractal strength
df["%-bottom_strength"], df["%-top_strength"] = self.calculate_fractal_strength(df)
logger.info("Fractal strength calculated")
# Price relationships
for i in [1, 2, 3]:
high_shift = df['high'].shift(i).replace(0, df['high'].mean())
low_shift = df['low'].shift(i).replace(0, df['low'].mean())
df[f"%-high_ratio_{i}"] = df['high'] / high_shift
df[f"%-low_ratio_{i}"] = df['low'] / low_shift
# MACD divergence
df["%-macd_bottom_div"] = ((df['low'] < df['low'].rolling(5).min().shift(1)) & (df['macd'] > df['macd'].rolling(5).min().shift(1))).astype(int)
df["%-macd_top_div"] = ((df['high'] > df['high'].rolling(5).max().shift(1)) & (df['macd'] < df['macd'].rolling(5).max().shift(1))).astype(int)
# Additional features
df["%-volume_change"] = df['volume'].pct_change()
df["%-volatility"] = df['close'].rolling(5).std()
df["%-price_range_20"] = (df['high'].rolling(5).max() - df['low'].rolling(5).min()) / df['close'].replace(0, np.nan)
df["%-potential_top"] = (df['is_top'] & (df["%-rsi-14"] > 70) & (df["%-macd_top_div"] == 1) &
(df['volume'] > df['volume'].rolling(20).mean()) & (df['close'] < df['open'])).astype(int)
df["%-potential_bottom"] = (df['is_bottom'] & (df["%-rsi-14"] < 30) & (df["%-macd_bottom_div"] == 1)).astype(int)
# Fractal distance
df["%-last_fractal_distance"] = self.calculate_fractal_distance(df)
# Advanced features
df["%-macd_hist_change"] = df['macd_hist_sum'].pct_change().replace([np.inf, -np.inf], 0)
df["%-volume_divergence"] = (df['close'].pct_change() - df['volume'].pct_change()).abs()
df["%-breakout_high"] = (df['high'] > df['high'].shift(1).rolling(20).max()).astype(int)
df["%-breakout_low"] = (df['low'] < df['low'].shift(1).rolling(20).min()).astype(int)
df["%-top_prominence"] = (df['high'] - df['high'].shift(1).rolling(5).mean()) / (df['high'].shift(1).rolling(5).std() + 1e-6)
df["%-top_prominence"] = df["%-top_prominence"].clip(-100, 100)
df["%-macd_hist_decline"] = df['macd_hist'].rolling(3).apply(
lambda x: 1 if all(x[i] > x[i+1] for i in range(len(x)-1)) else 0, raw=True)
df["%-top_candle_pattern"] = ((df['close'].shift(1) > df['open'].shift(1)) &
(df['close'] < df['open']) &
(df['close'] < df['open'].shift(1))).astype(int)
df["%-bottom_combo"] = (df['is_bottom'] & (df["%-rsi-14"] < 40) & (df['macd_hist'] > 0) &
(df['volume'] > df['volume'].rolling(20).mean())).astype(int)
df["%-top_combo"] = (df['is_top'] & (df["%-rsi-14"] > 60) & (df['macd_hist'] < 0) &
(df['volume'] > df['volume'].rolling(20).mean())).astype(int)
df["%-post_top_decline"] = self.calculate_post_top_decline(df)
df["%-resistance_distance"] = self.calculate_resistance_distance(df)
# Stroke and pivot features
strokes = self.detect_strokes(df)
df["%-macd_hist_dynamic"], df["%-pivot_distance"], df["%-buy_signal"], df["%-sell_signal"] = self.process_strokes_and_pivots(df, strokes)
logger.info("Stroke and pivot features completed")
# Clean up
num_columns = df.select_dtypes(include=[np.number]).columns
df[num_columns] = df[num_columns].replace([np.inf, -np.inf], 0).fillna(0)
logger.info(f"Completed feature_engineering_standard. Columns: {list(df.columns)}")
return df
except Exception as e:
logger.error(f"Error in feature_engineering_standard: {e}")
# 发生错误时返回原始数据框
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""Set prediction targets for FreqAI."""
logger.info("Setting FreqAI targets")
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
# Calculate future return
future_return = (
dataframe["close"].shift(-label_period).rolling(label_period).mean() / dataframe["close"] - 1
)
# Discretize into categorical labels: 1 (buy), 2 (sell)
dataframe["&-s_close"] = pd.Series(0, index=dataframe.index) # Default: no trade
dataframe.loc[future_return > 0.01, "&-s_close"] = 1 # Buy if return > 1%
dataframe.loc[future_return < -0.01, "&-s_close"] = 0 # Sell if return < -1%
return dataframe.fillna(0)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Populate indicators, leveraging FreqAI if available."""
logger.info(f"Starting populate_indicators for pair {metadata.get('pair', 'unknown')}")
# Always run feature_engineering_standard to ensure custom features
dataframe = self.feature_engineering_standard(dataframe, metadata)
if hasattr(self, "freqai"):
logger.info("Running FreqAI pipeline")
# Preserve custom features
custom_features = [col for col in dataframe.columns if col.startswith('%-')]
temp_df = dataframe[custom_features + ['date', 'close', 'open', 'high', 'low', 'volume']]
# Run FreqAI
freqai_df = self.freqai.start(dataframe, metadata, self)
# Merge back custom features
freqai_df = freqai_df.combine_first(temp_df)
dataframe = freqai_df
logger.info(f"Completed populate_indicators. Columns: {list(dataframe.columns)}")
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Define entry signals."""
logger.info(f"Starting populate_entry_trend for pair {metadata.get('pair', 'unknown')}")
df = self.ensure_columns(dataframe, ['%-buy_signal', '%-bottom_strength', '%-sell_signal', '%-top_strength', '%-macd_top_div', '%-rsi-14'])
enter_long_conditions = [
df["do_predict"] == 1,
df["&-s_close"] == 1, # 上涨预测
df["%-buy_signal"] > 0,
df["%-bottom_strength"] > 2.0,
df["%-rsi-14"] < 40,
]
if enter_long_conditions:
df.loc[reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"]] = (1, "long")
enter_short_conditions = [
df["do_predict"] == 1,
df["&-s_close"] == 0, # 下跌预测
df["%-sell_signal"] > 0,
df["%-top_strength"] > 2.5,
df["%-rsi-14"] > 60,
df["%-macd_top_div"] > 0,
]
if enter_short_conditions:
df.loc[reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"]] = (1, "short")
logger.info("Completed populate_entry_trend")
return df
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Define exit signals."""
logger.info(f"Starting populate_exit_trend for pair {metadata.get('pair', 'unknown')}")
df = self.ensure_columns(dataframe, ['%-top_combo', '%-top_candle_pattern', '%-bottom_combo', '%-bottom_strength', '%-rsi-14'])
exit_long_conditions = [
df["do_predict"] == 1,
df["&-s_close"] == 0, # 下跌预测时退出多头
(df["%-top_combo"] > 0) | (df["%-top_candle_pattern"] > 0)
]
if exit_long_conditions:
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
exit_short_conditions = [
df["do_predict"] == 1,
df["&-s_close"] == 1, # 上涨预测时退出空头
(df["%-bottom_combo"] > 0) | (df["%-bottom_strength"] > 3.0)
]
if exit_short_conditions:
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
logger.info("Completed populate_exit_trend")
return df
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str,
current_time, entry_tag, side: str, **kwargs) -> bool:
"""Confirm trade entry with additional checks."""
logger.info(f"Confirming trade entry for {pair}, side: {side}")
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = df.iloc[-1].squeeze()
df = self.ensure_columns(df, ['%-bottom_strength', '%-buy_signal', '%-top_strength', '%-sell_signal', '%-rsi-14'])
if side == "long":
if rate > (last_candle["close"] * 1.0025):
logger.debug(f"Long entry rejected: rate {rate} exceeds threshold")
return False
return last_candle["%-bottom_strength"] > 1.5 or last_candle["%-buy_signal"] > 0
else:
if rate < (last_candle["close"] * 0.9975):
logger.debug(f"Short entry rejected: rate {rate} below threshold")
return False
return last_candle["%-top_strength"] > 2.0 or last_candle["%-sell_signal"] > 0
def detect_fractals(self, df: DataFrame) -> DataFrame:
"""Detect top and bottom fractals."""
logger.debug("Detecting fractals")
df['is_top'] = (
(df['high'] > df['high'].shift(1)) & (df['high'] > df['high'].shift(2)) &
(df['high'] > df['high'].shift(-1)) & (df['high'] > df['high'].shift(-2))
)
df['is_bottom'] = (
(df['low'] < df['low'].shift(1)) & (df['low'] < df['low'].shift(2)) &
(df['low'] < df['low'].shift(-1)) & (df['low'] < df['low'].shift(-2))
)
return df.fillna({'is_top': False, 'is_bottom': False})
def calculate_fractal_strength(self, df: DataFrame) -> tuple:
"""Calculate strength of fractals."""
logger.debug("Calculating fractal strength")
bottom_strength = np.zeros(len(df))
top_strength = np.zeros(len(df))
for i in range(2, len(df) - 2):
if df['is_bottom'].iloc[i]:
strength = 0.0
pre_decline = (df['low'].iloc[i-2:i].min() - df['low'].iloc[i]) / df['low'].iloc[i]
post_rise = (df['high'].iloc[i+1:i+3].max() - df['high'].iloc[i]) / df['high'].iloc[i]
strength += min(pre_decline * 10, 1.0) + min(post_rise * 10, 1.0)
vol_surge = df['volume'].iloc[i] / df['volume'].iloc[i-3:i].mean() if df['volume'].iloc[i-3:i].mean() > 0 else 1
strength += min(vol_surge / 3, 1.0)
if any(abs(df['low'].iloc[j] - df['low'].iloc[i]) / df['low'].iloc[i] < 0.01 for j in range(max(0, i-20), i)):
strength += 1.0
bottom_strength[i] = min(strength, 5.0)
if df['is_top'].iloc[i]:
strength = 0.0
pre_rise = (df['high'].iloc[i] - df['high'].iloc[i-2:i].max()) / df['high'].iloc[i]
post_decline = (df['low'].iloc[i] - df['low'].iloc[i+1:i+3].min()) / df['low'].iloc[i]
strength += min(pre_rise * 10, 1.0) + min(post_decline * 10, 1.0)
vol_surge = df['volume'].iloc[i] / df['volume'].iloc[i-3:i].mean() if df['volume'].iloc[i-3:i].mean() > 0 else 1
strength += min(vol_surge / 3, 1.0)
if any(abs(df['high'].iloc[j] - df['high'].iloc[i]) / df['high'].iloc[i] < 0.01 for j in range(max(0, i-20), i)):
strength += 1.0
bearish_count = sum(df['close'].iloc[i:i+3] < df['open'].iloc[i:i+3])
strength += min(bearish_count * 0.5, 1.5)
top_strength[i] = min(strength, 6.0)
return bottom_strength, top_strength
def calculate_fractal_distance(self, df: DataFrame) -> pd.Series:
"""Calculate distance to last fractal."""
logger.debug("Calculating fractal distance")
fractal_indices = df[df['is_top'] | df['is_bottom']].index
distances = pd.Series(0, index=df.index)
for i in range(1, len(df)):
if fractal_indices[fractal_indices < df.index[i]].size > 0:
last_fractal_idx = fractal_indices[fractal_indices < df.index[i]][-1]
if isinstance(df.index[i], pd.Timestamp) and isinstance(last_fractal_idx, pd.Timestamp):
time_diff = (df.index[i] - last_fractal_idx).total_seconds() / 60
else:
time_diff = i - df.index.get_loc(last_fractal_idx)
distances.iloc[i] = time_diff
return distances
def calculate_post_top_decline(self, df: DataFrame) -> pd.Series:
"""Calculate post-top decline."""
logger.debug("Calculating post-top decline")
post_top_decline = pd.Series(0.0, index=df.index)
for i in range(2, len(df)-3):
if df['is_top'].iloc[i]:
decline = (df['high'].iloc[i] - df['low'].iloc[i+1:i+4].min()) / df['high'].iloc[i]
post_top_decline.iloc[i] = min(decline * 10, 5.0)
return post_top_decline
def calculate_resistance_distance(self, df: DataFrame) -> pd.Series:
"""Calculate resistance distance."""
logger.debug("Calculating resistance distance")
resistance_distance = pd.Series(0.0, index=df.index)
for i in range(20, len(df)):
if df['is_top'].iloc[i]:
price_high = df['high'].iloc[i]
resistance_levels = df['high'].iloc[i-20:i].rolling(5).max()
distance = (price_high - resistance_levels.min()) / price_high if resistance_levels.min() > 0 else 0
resistance_distance.iloc[i] = min(distance * 10, 5.0)
return resistance_distance
def detect_strokes(self, df: DataFrame) -> list:
"""Detect strokes based on fractals."""
logger.debug("Detecting strokes")
strokes = []
last_fractal, last_price, last_index = None, None, None
for i in range(len(df)):
if df['is_top'].iloc[i] or df['is_bottom'].iloc[i]:
current_fractal = 'top' if df['is_top'].iloc[i] else 'bottom'
current_price = df['high'].iloc[i] if current_fractal == 'top' else df['low'].iloc[i]
if last_fractal is None:
last_fractal, last_price, last_index = current_fractal, current_price, df.index[i]
continue
if not isinstance(current_price, (int, float)) or not isinstance(last_price, (int, float)):
continue
price_change = abs(current_price - last_price) / last_price
if price_change < 0.005:
continue
if (last_fractal == 'top' and current_fractal == 'bottom' and current_price < last_price) or \
(last_fractal == 'bottom' and current_fractal == 'top' and current_price > last_price):
strokes.append({
'start_time': last_index,
'end_time': df.index[i],
'start_price': last_price,
'end_price': current_price,
'type': 'down' if current_fractal == 'bottom' else 'up'
})
last_fractal, last_price, last_index = current_fractal, current_price, df.index[i]
logger.debug(f"Detected {len(strokes)} strokes")
return strokes
def detect_pivots(self, strokes: list) -> list:
"""Detect pivots based on strokes."""
logger.debug("Detecting pivots")
pivots = []
if len(strokes) < 3:
logger.debug("Insufficient strokes for pivot detection")
return pivots
for i in range(2, len(strokes)):
high1, low1 = max(strokes[i-2]['start_price'], strokes[i-2]['end_price']), min(strokes[i-2]['start_price'], strokes[i-2]['end_price'])
high2, low2 = max(strokes[i-1]['start_price'], strokes[i-1]['end_price']), min(strokes[i-1]['start_price'], strokes[i-1]['end_price'])
high3, low3 = max(strokes[i]['start_price'], strokes[i]['end_price']), min(strokes[i]['start_price'], strokes[i]['end_price'])
if max(low1, low2, low3) < min(high1, high2, high3):
pivots.append({
'start_time': strokes[i-2]['start_time'],
'end_time': strokes[i]['end_time'],
'high': min(high1, high2, high3),
'low': max(low1, low2, low3)
})
logger.debug(f"Detected {len(pivots)} pivots")
return pivots
def add_pivot_distance(self, df: DataFrame, pivots: list) -> DataFrame:
"""Add pivot distance feature."""
logger.debug("Adding pivot distance")
df = df.copy()
df['pivot_distance'] = 0.0
if not pivots:
logger.debug("No pivots detected, returning default pivot_distance")
return df
for pivot in pivots:
try:
start_time = pivot['start_time']
end_time = pivot['end_time']
if start_time not in df.index or end_time not in df.index:
logger.debug(f"Invalid pivot times: {start_time} to {end_time}")
continue
mask = (df.index >= start_time) & (df.index <= end_time)
denominator = pivot['high'] - pivot['low']
if denominator > 0:
df.loc[mask, 'pivot_distance'] = (df['close'] - pivot['low']) / denominator
else:
logger.debug(f"Zero denominator for pivot {pivot}")
except Exception as e:
logger.error(f"Error in pivot distance calculation: {e}")
continue
df['pivot_distance'] = df['pivot_distance'].clip(-10, 10).fillna(0.0)
logger.debug("Completed pivot distance calculation")
return df
def detect_back_divergence(self, df: DataFrame, strokes: list) -> DataFrame:
"""Detect back divergence for buy/sell signals."""
logger.debug("Detecting back divergence")
df = df.copy()
df['buy_signal'] = False
df['sell_signal'] = False
if len(strokes) < 2:
logger.debug("Insufficient strokes for divergence detection")
return df
for i in range(1, len(strokes)):
current_hist = self.safe_get_value(df, strokes[i]['end_time'], 'macd_hist_sum')
previous_hist = self.safe_get_value(df, strokes[i-1]['end_time'], 'macd_hist_sum')
if current_hist is None or previous_hist is None:
continue
if strokes[i]['type'] == strokes[i-1]['type'] == 'up':
if strokes[i]['end_price'] > strokes[i-1]['end_price'] and current_hist < previous_hist:
closest_time = df.index[df.index <= strokes[i]['end_time']]
if len(closest_time) > 0:
df.loc[closest_time[-1], 'sell_signal'] = True
elif strokes[i]['type'] == strokes[i-1]['type'] == 'down':
if strokes[i]['end_price'] < strokes[i-1]['end_price'] and current_hist < previous_hist:
closest_time = df.index[df.index <= strokes[i]['end_time']]
if len(closest_time) > 0:
df.loc[closest_time[-1], 'buy_signal'] = True
logger.debug("Completed back divergence detection")
return df
def process_strokes_and_pivots(self, df: DataFrame, strokes: list) -> tuple:
"""Process strokes and pivots for advanced features."""
logger.debug("Processing strokes and pivots")
macd_hist_dynamic = pd.Series(0.0, index=df.index)
pivot_distance = pd.Series(0.0, index=df.index)
buy_signal = pd.Series(0, index=df.index)
sell_signal = pd.Series(0, index=df.index)
if strokes:
try:
for stroke in strokes[1:]:
start_time, end_time = stroke['start_time'], stroke['end_time']
if start_time not in df.index or end_time not in df.index:
logger.debug(f"Invalid stroke times: {start_time} to {end_time}")
continue
window = self.calculate_window(df, start_time, end_time)
start_idx, end_idx = df.index.get_loc(start_time), df.index.get_loc(end_time) + 1
hist_values = df['macd_hist'].iloc[start_idx:end_idx].rolling(window, min_periods=1).sum().fillna(0)
macd_hist_dynamic.iloc[start_idx:end_idx] = hist_values
except Exception as e:
logger.warning(f"Dynamic MACD calculation error: {e}")
try:
pivots = self.detect_pivots(strokes)
if pivots:
df_with_pivot = self.add_pivot_distance(df, pivots)
pivot_distance = df_with_pivot['pivot_distance']
else:
logger.debug("No pivots detected")
except Exception as e:
logger.warning(f"Pivot detection error: {e}")
try:
df_with_divergence = self.detect_back_divergence(df, strokes)
buy_signal = df_with_divergence['buy_signal'].astype(int)
sell_signal = df_with_divergence['sell_signal'].astype(int)
except Exception as e:
logger.warning(f"Back divergence detection error: {e}")
logger.debug("Completed stroke and pivot processing")
return macd_hist_dynamic, pivot_distance, buy_signal, sell_signal
def ensure_columns(self, df: DataFrame, columns: list) -> DataFrame:
"""Ensure required columns exist with default values."""
logger.debug(f"Ensuring columns: {columns}")
for col in columns:
if col not in df.columns:
logger.warning(f"Column {col} missing, using default value")
df[col] = 50 if col == '%-rsi-14' else 0
return df
def safe_get_value(self, df: DataFrame, timestamp, column: str):
"""Safely get value from DataFrame."""
try:
if timestamp in df.index:
return df.loc[timestamp, column]
closest_idx = df.index[df.index <= timestamp]
return df.loc[closest_idx[-1], column] if len(closest_idx) > 0 else None
except Exception as e:
logger.debug(f"Error getting value for {column} at {timestamp}: {e}")
return None
def calculate_window(self, df: DataFrame, start_time, end_time) -> int:
"""Calculate window size for dynamic features."""
try:
if isinstance(start_time, pd.Timestamp) and isinstance(end_time, pd.Timestamp):
window = int((end_time - start_time).total_seconds() / 60)
else:
start_idx = df.index.get_loc(start_time)
end_idx = df.index.get_loc(end_time)
window = end_idx - start_idx
return max(window, 1)
except (TypeError, AttributeError, KeyError) as e:
logger.debug(f"Window calculation error: {e}")
return 1