回测结果不太理想, 优化了很多点, dryrun试一下

This commit is contained in:
zhangkun9038@dingtalk.com 2025-05-21 14:36:23 +00:00
parent b4b647dc4d
commit 6cd743522d
8 changed files with 3414 additions and 209 deletions

1
.gitignore vendored
View File

@ -126,3 +126,4 @@ data/
tools/.env
result/
output.log

3101
chat/result9.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -61,7 +61,7 @@
"pairlists": [
{
"method": "StaticPairList"
}
}
],
"freqai": {
"enabled": true,
@ -70,11 +70,14 @@
},
"freqaimodel": "CatboostClassifier",
"purge_old_models": 2,
"train_period_days": 15,
"identifier": "test58",
"train_period_days": 30,
"backtest_period_days": 10,
"backtest_period_days": 7,
"live_retrain_hours": 0,
"outlier_detection": {
"method": "IsolationForest",
"contamination": 0.1
},
"feature_selection": {
"method": "recursive_elimination"
},
@ -88,8 +91,9 @@
"BTC/USDT",
"SOL/USDT"
],
"label_period_candles": 20,
"include_shifted_candles": 2,
"outlier_protection_percentage": 0.1,
"label_period_candles": 12,
"include_shifted_candles": 3,
"DI_threshold": 0.9,
"weight_factor": 0.9,
"principal_component_analysis": false,
@ -102,13 +106,15 @@
"plot_feature_importances": 0
},
"data_split_parameters": {
"test_size": 0.2
"test_size": 0.2,
"shuffle": false,
},
"model_training_parameters": {
"n_estimators": 100,
"learning_rate": 0.05,
"max_depth": 5,
"num_leaves": 31
"learning_rate": 0.01,
"num_leaves": 64,
"verbose": -1,
"max_depth": 8,
}
},
"api_server": {

View File

@ -12,8 +12,8 @@
},
"protection": {},
"roi": {
"0": 0.20400000000000001,
"18": 0.07100000000000001,
"0": 0.20,
"18": 0.07,
"47": 0.03,
"102": 0
},

View File

@ -5,6 +5,9 @@ import talib.abstract as ta
from pandas import DataFrame
from technical import qtpylib
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
from pandas.core.dtypes.common import is_scalar
import pandas as pd
logger = logging.getLogger(__name__)
@ -34,9 +37,10 @@ class FreqaiPrimer(IStrategy):
stoploss_param = DecimalParameter(low=-0.35, high=-0.1, default=-0.263, space="stoploss", optimize=True, load=True)
trailing_stop_positive_param = DecimalParameter(low=0.1, high=0.5, default=0.324, space="trailing", optimize=True, load=True)
trailing_stop_positive_offset_param = DecimalParameter(low=0.2, high=0.6, default=0.411, space="trailing", optimize=True, load=True)
tema_period = IntParameter(low=5, high=20, default=9, space="buy", optimize=True, load=True)
freqai_info = {
"model": "LightGBMRegressor",
"model": "LightGBMRegressorMultiTarget",
"feature_parameters": {
"include_timeframes": ["5m", "15m", "1h"],
"include_corr_pairlist": [],
@ -48,9 +52,10 @@ class FreqaiPrimer(IStrategy):
"shuffle": False,
},
"model_training_parameters": {
"n_estimators": 200,
"learning_rate": 0.05,
"num_leaves": 31,
"n_estimators": 500,
"learning_rate": 0.01,
"num_leaves": 64,
"max_depth": 8,
"verbose": -1,
},
}
@ -66,187 +71,283 @@ class FreqaiPrimer(IStrategy):
},
}
def __init__(self, config: dict):
super().__init__(config)
self.cycle_trade_pair = None # 跟踪当前周期的下单币对
self.last_cycle_time = None # 记录上一个周期时间
def bot_loop_start(self, current_time, **kwargs) -> None:
"""
Reset cycle_trade_pair at the start of each new cycle.
"""
try:
# 检查是否进入新周期(基于 timeframe如 5m
if self.last_cycle_time is None or current_time >= self.last_cycle_time + pd.Timedelta(self.timeframe):
self.cycle_trade_pair = None
self.last_cycle_time = current_time
logger.debug(f"New cycle started at {current_time}, reset cycle_trade_pair")
except Exception as e:
logger.error(f"Error in bot_loop_start: {str(e)}")
def clean_dataframe(self, dataframe: DataFrame, columns: list = None) -> DataFrame:
try:
if columns:
valid_columns = [col for col in columns if col in dataframe.columns]
if not valid_columns:
logger.warning(f"None of the specified columns {columns} exist in DataFrame. Skipping cleaning.")
return dataframe
for col in valid_columns:
logger.debug(f"Cleaning column {col}: NaN count before = {dataframe[col].isna().sum()}")
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
logger.debug(f"NaN count after = {dataframe[col].isna().sum()}")
else:
logger.debug(f"Cleaning entire DataFrame: NaN count before = {dataframe.isna().sum().sum()}")
dataframe = dataframe.replace([np.inf, -np.inf], 0).ffill().fillna(0)
logger.debug(f"NaN count after = {dataframe.isna().sum().sum()}")
return dataframe
except Exception as e:
logger.error(f"Data cleaning failed: {str(e)}")
return dataframe # 返回原始 DataFrame避免策略崩溃
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(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()
)
dataframe = dataframe.replace([np.inf, -np.inf], 0)
dataframe = dataframe.ffill()
dataframe = dataframe.fillna(0)
return dataframe
try:
dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"])
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(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()
)
dataframe = self.clean_dataframe(dataframe, columns=["%-rsi-period", "%-mfi-period", "%-sma-period",
"%-ema-period", "%-adx-period", "bb_lowerband-period",
"bb_middleband-period", "bb_upperband-period",
"%-bb_width-period", "%-close-bb_lower-period",
"%-roc-period", "%-relative_volume-period"])
return dataframe
except Exception as e:
logger.error(f"Error in feature_engineering_expand_all for {metadata['pair']}: {str(e)}")
return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
dataframe = dataframe.replace([np.inf, -np.inf], 0)
dataframe = dataframe.ffill()
dataframe = dataframe.fillna(0)
return dataframe
try:
dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"])
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
dataframe = self.clean_dataframe(dataframe, columns=["%-pct-change", "%-raw_volume", "%-raw_price"])
return dataframe
except Exception as e:
logger.error(f"Error in feature_engineering_expand_basic for {metadata['pair']}: {str(e)}")
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
if len(dataframe["close"]) < 20:
logger.warning(f"数据不足 {len(dataframe)} 根 K 线,%-volatility 可能不完整")
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
return dataframe
try:
dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"])
if len(dataframe["close"]) < 20:
logger.warning(f"数据不足 {len(dataframe)} 根 K 线,%-volatility 可能不完整")
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
dataframe = self.clean_dataframe(dataframe, columns=["%-day_of_week", "%-hour_of_day", "%-volatility"])
return dataframe
except Exception as e:
logger.error(f"Error in feature_engineering_standard for {metadata['pair']}: {str(e)}")
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
if "close" not in dataframe.columns:
logger.error("数据框缺少必要的 'close'")
raise ValueError("数据框缺少必要的 'close'")
try:
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
if "close" not in dataframe.columns:
logger.error("数据框缺少必要的 'close'")
raise ValueError("数据框缺少必要的 'close'")
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
if "%-volatility" not in dataframe.columns:
logger.warning("缺少 %-volatility 列,强制重新生成")
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
# 移除 shift(-label_period),改为使用当前及过去的数据
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["&-buy_rsi"] = dataframe["&-buy_rsi"].rolling(window=label_period).mean().ffill().bfill()
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14).rolling(window=label_period).mean().ffill().bfill()
for col in ["&-buy_rsi", "%-volatility"]:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
dataframe[col] = dataframe[col].ffill()
dataframe[col] = dataframe[col].fillna(0)
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
if dataframe[col].isna().any():
logger.warning(f"目标列 {col} 仍包含 NaN数据预览\n{dataframe[col].tail(10)}")
logger.info(f"目标列预览:\n{dataframe[['&-buy_rsi']].head().to_string()}")
return dataframe
except Exception as e:
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
raise
logger.info(f"目标列预览:\n{dataframe[['&-buy_rsi']].head().to_string()}")
return dataframe
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.info(f"处理交易对:{metadata['pair']}")
logger.debug(f"输入特征列:{list(dataframe.columns)}")
dataframe = self.freqai.start(dataframe, metadata, self)
logger.debug(f"FreqAI 输出特征列:{list(dataframe.columns)}")
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe["bb_lowerband"] = bollinger["lower"]
dataframe["bb_middleband"] = bollinger["mid"]
dataframe["bb_upperband"] = bollinger["upper"]
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
# 使用滚动窗口而非未来函数来生成 up_or_down 列
dataframe["up_or_down"] = np.where(
dataframe["close"].rolling(window=label_period).mean() > dataframe["close"], 1, 0
)
if "&-buy_rsi" in dataframe.columns:
if "%-volatility" not in dataframe.columns:
logger.warning("缺少 %-volatility 列,强制重新生成")
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30
dataframe["&-stoploss"] = self.stoploss - (dataframe["%-volatility"] * 5).clip(-0.05, 0.05)
dataframe["&-roi_0"] = (dataframe["close"].rolling(window=label_period).mean() / dataframe["close"] - 1).clip(0, 0.2)
for col in ["&-buy_rsi", "&-sell_rsi", "&-stoploss", "&-roi_0"]:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
dataframe[col] = dataframe[col].ffill()
dataframe[col] = dataframe[col].fillna(0)
dataframe["buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(5).mean().clip(10, 50)
dataframe["sell_rsi_pred"] = dataframe["&-sell_rsi"].rolling(5).mean().clip(50, 90)
dataframe["stoploss_pred"] = dataframe["&-stoploss"].clip(-0.35, -0.1)
dataframe["roi_0_pred"] = dataframe["&-roi_0"].clip(0.01, 0.2)
for col in ["buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred"]:
if dataframe[col].isna().any():
logger.warning(f"{col} 包含 NaN填充为默认值")
dataframe[col] = dataframe[col].ffill()
dataframe[col] = dataframe[col].fillna(dataframe[col].mean())
dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3)
dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.75).clip(0.02, 0.4)
self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1])
self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1])
self.stoploss = float(self.stoploss_param.value)
self.minimal_roi = {
0: float(self.roi_0.value),
15: float(self.roi_15.value),
30: float(self.roi_30.value),
60: 0
}
self.trailing_stop_positive = float(self.trailing_stop_positive_param.value)
self.trailing_stop_positive_offset = float(self.trailing_stop_positive_offset_param.value)
logger.info(f"动态参数buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, "
f"stoploss={self.stoploss}, trailing_stop_positive={self.trailing_stop_positive}")
else:
logger.warning(f"&-buy_rsi 列缺失,跳过 FreqAI 预测逻辑,检查 freqai.start 输出")
dataframe = dataframe.replace([np.inf, -np.inf], 0)
dataframe = dataframe.ffill()
dataframe = dataframe.fillna(0)
logger.info(f"up_or_down 值统计:\n{dataframe['up_or_down'].value_counts().to_string()}")
logger.info(f"do_predict 值统计:\n{dataframe['do_predict'].value_counts().to_string()}")
logger.debug(f"最终特征列:{list(dataframe.columns)}")
return dataframe
try:
logger.debug(f"Pre-cleaning input DataFrame for {metadata['pair']}: columns={list(dataframe.columns)}")
dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"])
logger.info(f"Processing pair: {metadata['pair']}")
logger.debug(f"Input columns before FreqAI: {list(dataframe.columns)}, shape={dataframe.shape}")
# FreqAI 预测
dataframe = self.freqai.start(dataframe, metadata, self)
logger.debug(f"Columns after FreqAI: {list(dataframe.columns)}, shape={dataframe.shape}")
# 技术指标
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe["bb_lowerband"] = bollinger["lower"]
dataframe["bb_middleband"] = bollinger["mid"]
dataframe["bb_upperband"] = bollinger["upper"]
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=self.tema_period.value)
dataframe["atr"] = ta.ATR(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
# 标签生成
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
dataframe["up_or_down"] = np.where(
dataframe["close"].rolling(window=label_period).mean() > dataframe["close"], 1, 0
)
# FreqAI 预测后处理
if "&-buy_rsi" in dataframe.columns:
if "%-volatility" not in dataframe.columns:
logger.warning("Missing %-volatility column, regenerating")
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30
dataframe["&-stoploss"] = self.stoploss - (dataframe["%-volatility"] * 5).clip(-0.05, 0.05)
dataframe["&-roi_0"] = (dataframe["close"].rolling(window=label_period).mean() / dataframe["close"] - 1).clip(0, 0.2)
dataframe = self.clean_dataframe(dataframe, columns=["&-buy_rsi", "&-sell_rsi", "&-stoploss", "&-roi_0", "%-volatility"])
dataframe["buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(5).mean().clip(10, 50)
dataframe["sell_rsi_pred"] = dataframe["&-sell_rsi"].rolling(5).mean().clip(50, 90)
dataframe["stoploss_pred"] = dataframe["&-stoploss"].clip(-0.35, -0.1)
dataframe["roi_0_pred"] = dataframe["&-roi_0"].clip(0.01, 0.2)
# 处理缺失值
for col in ["buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred"]:
if dataframe[col].isna().any():
logger.warning(f"Column {col} contains NaN values, filling with mean")
dataframe[col] = dataframe[col].ffill().fillna(dataframe[col].mean())
dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3)
dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.75).clip(0.02, 0.4)
# 更新动态参数
self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1])
self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1])
self.stoploss = float(self.stoploss_param.value)
self.minimal_roi = {
0: float(self.roi_0.value),
15: float(self.roi_15.value),
30: float(self.roi_30.value),
60: 0
}
self.trailing_stop_positive = float(self.trailing_stop_positive_param.value)
self.trailing_stop_positive_offset = float(self.trailing_stop_positive_offset_param.value)
logger.info(f"Dynamic parameters: buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, "
f"stoploss={self.stoploss}, trailing_stop_positive={self.trailing_stop_positive}")
else:
logger.warning(f"&-buy_rsi column missing, skipping FreqAI prediction logic. Check model training or prediction files.")
dataframe["buy_rsi_pred"] = self.buy_rsi.default
dataframe["sell_rsi_pred"] = self.sell_rsi.default
dataframe["stoploss_pred"] = self.stoploss_param.default
dataframe["roi_0_pred"] = self.roi_0.default
dataframe["trailing_stop_positive"] = self.trailing_stop_positive_param.default
dataframe["trailing_stop_positive_offset"] = self.trailing_stop_positive_offset_param.default
# 清理生成的列
generated_columns = [
"rsi", "bb_lowerband", "bb_middleband", "bb_upperband", "tema", "atr",
"up_or_down", "buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred",
"trailing_stop_positive", "trailing_stop_positive_offset"
]
dataframe = self.clean_dataframe(dataframe, columns=generated_columns)
logger.info(f"up_or_down value counts:\n{dataframe['up_or_down'].value_counts().to_string()}")
logger.info(f"do_predict value counts:\n{dataframe['do_predict'].value_counts().to_string()}")
logger.debug(f"Final columns: {list(dataframe.columns)}, shape={dataframe.shape}")
return dataframe
except Exception as e:
logger.error(f"Error processing indicators for {metadata['pair']}: {str(e)}")
return dataframe # 返回未修改的 DataFrame避免中断
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
enter_long_conditions = [
qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]),
df["tema"] > df["tema"].shift(1),
df["volume"] > 0,
df["do_predict"] == 1,
df["up_or_down"] == 1
]
if enter_long_conditions:
df.loc[
reduce(lambda x, y: x & y, enter_long_conditions),
["enter_long", "enter_tag"]
] = (1, "long")
return df
try:
# 验证输入列
required_columns = ["rsi", "tema", "volume", "buy_rsi_pred", "do_predict", "bb_middleband", "close", "&-buy_rsi"]
missing_cols = [col for col in required_columns if col not in df.columns]
if missing_cols:
logger.error(f"Missing columns for {metadata['pair']}: {missing_cols}")
return df
# 处理 NaN
if df[required_columns].isna().any().any():
logger.warning(f"NaN values in {metadata['pair']}: {df[required_columns].isna().any()}")
df[required_columns] = df[required_columns].ffill().fillna(0)
# 计算 entry_score
df["entry_score"] = (
(50 - df["rsi"]) * 0.5 + # RSI 超卖权重
(df["volume"] / df["volume"].rolling(20, min_periods=1).mean()) * 0.3 + # 成交量活跃
df["&-buy_rsi"] * 0.2 # FreqAI 预测
)
# 确保 entry_score 是标量
if not df["entry_score"].apply(is_scalar).all():
logger.error(f"Non-scalar values in entry_score for {metadata['pair']}: {df['entry_score'].head()}")
df["entry_score"] = df["entry_score"].apply(lambda x: x[0] if isinstance(x, (list, np.ndarray)) else x).astype(float)
# 买入条件(优化为 5 个,触发率 0.35%-0.42%
enter_long_conditions = [
qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]), # RSI 上穿 FreqAI 预测
df["tema"] > df["tema"].shift(1), # TEMA 上行(趋势确认)
df["volume"] > df["volume"].rolling(20, min_periods=1).mean() * 1.1, # 成交量略高于均量
df["do_predict"] == 1, # FreqAI 预测有效
df["rsi"] < 45 # RSI 放宽到 < 45
]
# 合并条件
if enter_long_conditions:
condition = reduce(lambda x, y: x & y, enter_long_conditions)
df.loc[condition, "enter_long"] = 1
df.loc[condition, "enter_tag"] = "long"
df.loc[condition, "entry_score"] = df.loc[condition, "entry_score"]
df["enter_long"] = df["enter_long"].fillna(0).astype(int)
df["enter_tag"] = df["enter_tag"].fillna("")
df["entry_score"] = df["entry_score"].fillna(0.0).astype(float)
else:
logger.warning(f"No valid entry conditions for {metadata['pair']}")
df["enter_long"] = 0
df["enter_tag"] = ""
df["entry_score"] = 0.0
logger.debug(f"Entry signals for {metadata['pair']}: {df['enter_long'].sum()} signals")
return df
except Exception as e:
logger.error(f"Error in populate_entry_trend for {metadata['pair']}: {str(e)}")
return df
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
exit_long_conditions = [
qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"]),
(df["close"] < df["close"].shift(1) * 0.97),
df["volume"] > 0,
df["do_predict"] == 1,
df["up_or_down"] == 0
]
if exit_long_conditions:
df.loc[
reduce(lambda x, y: x & y, exit_long_conditions),
"exit_long"
] = 1
return df
try:
exit_long_conditions = [
qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"]),
(df["close"] < df["close"].shift(1) * 0.97) | (df["rsi"] > 80),
df["volume"] > 0,
(df["do_predict"] == 1) | (df["do_predict"] == 0),
(df["up_or_down"] == 0) | (df["rsi"] > 70)
]
if exit_long_conditions:
df.loc[
reduce(lambda x, y: x & y, exit_long_conditions),
"exit_long"
] = 1
return df
except Exception as e:
logger.error(f"Error in populate_exit_trend for {metadata['pair']}: {str(e)}")
return df
def confirm_trade_entry(
self, pair: str, order_type: str, amount: float, rate: float,
@ -264,16 +365,44 @@ class FreqaiPrimer(IStrategy):
return False
if side == "long":
# 检查当前周期是否已有下单
if self.cycle_trade_pair is not None:
logger.debug(f"周期内已有下单 {self.cycle_trade_pair},拒绝 {pair} 的买入")
return False
# 收集所有交易对的信号和分数
signals = []
for p in self.config["exchange"]["pair_whitelist"]:
p_df, _ = self.dp.get_analyzed_dataframe(p, self.timeframe)
if p_df is not None and not p_df.empty and "enter_long" in p_df.columns:
if p_df["enter_long"].iloc[-1] == 1:
score = p_df["entry_score"].iloc[-1] if "entry_score" in p_df.columns else -float('inf')
signals.append((p, score))
# 如果有信号,选择得分最高的币对
if signals:
signals = sorted(signals, key=lambda x: x[1], reverse=True)
top_pair, top_score = signals[0]
if pair != top_pair:
logger.debug(f"{pair} 不是最高得分币对 {top_pair} (score={top_score:.2f}),拒绝买入")
return False
else:
logger.debug(f"无有效信号,拒绝 {pair} 的买入")
return False
# 滑点检查
max_rate = last_candle["close"] * (1 + 0.0025) # 0.25% 滑点阈值
if rate > max_rate:
logger.debug(f"拒绝 {pair} 的买入,价格 {rate} 超过最大允许价格 {max_rate}")
return False
# 记录下单币对
self.cycle_trade_pair = pair
logger.info(f"确认 {pair} 的买入,周期内最优信号 (score={last_candle['entry_score']:.2f})")
return True
elif side == "short":
logger.warning(f"{pair} 尝试做空,但策略不支持做空 (can_short={self.can_short})")
return False
logger.debug(f"确认 {pair} 的交易side={side}, rate={rate}, close={last_candle['close']}")
return True
except Exception as e:
logger.error(f"确认 {pair} 交易时出错:{str(e)}")
return False

View File

@ -1,32 +0,0 @@
{
"strategy_name": "FreqaiPrimer",
"params": {
"max_open_trades": {
"max_open_trades": 4
},
"buy": {
"buy_rsi": 45.2500884290867
},
"sell": {
"sell_rsi": 75.2500884290867
},
"protection": {},
"roi": {
"0": 0.20400000000000001,
"18": 0.07100000000000001,
"47": 0.03,
"102": 0
},
"stoploss": {
"stoploss": -0.07
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.237,
"trailing_stop_positive_offset": 0.267,
"trailing_only_offset_is_reached": false
}
},
"ft_stratparam_v": 1,
"export_time": "2025-05-17 11:42:41.193338+00:00"
}

View File

@ -37,7 +37,7 @@ hyperopt_config="${STRATEGY_NAME%.py}.json"
echo "docker-compose run --rm freqtrade backtesting \
--logfile /freqtrade/user_data/logs/freqtrade.log \
--freqaimodel LightGBMRegressor \
--freqaimodel LightGBMRegressorMultiTarget \
--config /freqtrade/config_examples/$CONFIG_FILE \
--strategy-path /freqtrade/templates \
--strategy $STRATEGY_NAME \