回测结果不太理想, 优化了很多点, dryrun试一下
This commit is contained in:
parent
b4b647dc4d
commit
6cd743522d
1
.gitignore
vendored
1
.gitignore
vendored
@ -126,3 +126,4 @@ data/
|
||||
|
||||
tools/.env
|
||||
result/
|
||||
output.log
|
||||
|
||||
3101
chat/result9.md
Normal file
3101
chat/result9.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
},
|
||||
"protection": {},
|
||||
"roi": {
|
||||
"0": 0.20400000000000001,
|
||||
"18": 0.07100000000000001,
|
||||
"0": 0.20,
|
||||
"18": 0.07,
|
||||
"47": 0.03,
|
||||
"102": 0
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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 \
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user