Files
Chan/strategies/XGB.py

571 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_XGB(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 = 100 # Ensure sufficient data for pivots
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.debug("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()
return dataframe.fillna(0)
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""Basic price and volume features."""
logger.debug("Starting feature_engineering_expand_basic")
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
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 or len(dataframe) < self.startup_candle_count:
logger.error(f"Input DataFrame is empty or too small: {len(dataframe)} candles")
return self.ensure_columns(dataframe, [
'%-buy_signal', '%-bottom_strength', '%-sell_signal', '%-top_strength',
'%-macd_top_div', '%-rsi-14', '%-top_combo', '%-top_candle_pattern', '%-bottom_combo'
])
df = dataframe.copy()
if df['close'].isna().any():
logger.warning(f"Missing data: {df[['open', 'high', 'low', 'close', 'volume']].isna().sum()}")
# 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.debug("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.debug("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.debug("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.debug("Stroke and pivot features completed")
# Validate required columns
required_columns = [
'%-buy_signal', '%-bottom_strength', '%-sell_signal', '%-top_strength',
'%-macd_top_div', '%-rsi-14', '%-top_combo', '%-top_candle_pattern', '%-bottom_combo'
]
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
logger.error(f"Missing required columns: {missing_columns}")
df = self.ensure_columns(df, missing_columns)
# 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
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""Set prediction targets for FreqAI."""
logger.debug(f"Setting FreqAI targets for pair {metadata.get('pair', 'unknown')}")
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
)
# 二分类标签:1(买入/上涨),0(卖出/下跌) - 更简单且不易出错
dataframe["&-s_close"] = np.where(future_return > 0.01, 1, 0).astype(int)
logger.debug(f"Label distribution: {dataframe['&-s_close'].value_counts().to_dict()}")
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('%-')]
base_columns = ['date', 'close', 'open', 'high', 'low', 'volume']
# 仅保存确定存在的列
preserve_columns = custom_features + [col for col in base_columns if col in dataframe.columns]
temp_df = dataframe[preserve_columns]
# 设置FreqAI目标
dataframe = self.set_freqai_targets(dataframe, metadata)
# Run FreqAI
try:
freqai_df = self.freqai.start(dataframe, metadata, self)
# Merge back custom features
freqai_df = freqai_df.combine_first(temp_df)
dataframe = freqai_df
except Exception as e:
logger.error(f"FreqAI pipeline failed: {e}")
# Fallback to custom features
logger.info(f"Completed populate_indicators. Columns: {list(dataframe.columns)}")
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Generate entry signals for the strategy."""
dataframe.loc[:, "enter_long"] = 0
dataframe.loc[:, "enter_short"] = 0
# 确保数据框中的列存在
dataframe = self.ensure_columns(dataframe)
# 只有当do_predict列存在时才使用它
if "do_predict" in dataframe.columns:
# 根据AI预测值设置交易信号 - 使用二分类结果
mask = (dataframe["do_predict"] == 1)
dataframe.loc[mask, "enter_long"] = 1
# 设置空头信号 (如果策略支持空头)
if self.can_short:
mask = (dataframe["do_predict"] == 0)
dataframe.loc[mask, "enter_short"] = 1
# 将NaN值替换为0
dataframe = dataframe.fillna(0)
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Generate exit signals for the strategy."""
dataframe.loc[:, "exit_long"] = 0
dataframe.loc[:, "exit_short"] = 0
# 确保数据框中的列存在
dataframe = self.ensure_columns(dataframe)
# 只有当do_predict列存在时才使用它
if "do_predict" in dataframe.columns:
# 多头平仓信号
mask = (dataframe["do_predict"] == 0)
dataframe.loc[mask, "exit_long"] = 1
# 空头平仓信号
if self.can_short:
mask = (dataframe["do_predict"] == 1)
dataframe.loc[mask, "exit_short"] = 1
# 将NaN值替换为0
dataframe = dataframe.fillna(0)
return dataframe
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.debug(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
logger.debug("Dynamic MACD calculated")
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)
logger.debug("Back divergence signals calculated")
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, dataframe: DataFrame) -> DataFrame:
"""确保数据框中包含必要的列"""
# 不要尝试创建do_predict和&-s_close列,它们由FreqAI生成
for col in ["enter_long", "enter_short", "exit_long", "exit_short"]:
if col not in dataframe.columns:
dataframe[col] = 0
return dataframe
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