backtest hyperopt都可以了

This commit is contained in:
Ubuntu 2025-05-17 18:48:33 +08:00
parent d748c37f6a
commit c232264487
5 changed files with 189 additions and 133 deletions

View File

@ -35,10 +35,7 @@
},
"pair_whitelist": [
"BTC/USDT",
"OKB/USDT",
"TON/USDT",
"SOL/USDT",
"DOT/USDT"
"SOL/USDT"
],
"pair_blacklist": []
},
@ -70,7 +67,7 @@
"freqaimodel": "CatboostClassifier",
"purge_old_models": 2,
"train_period_days": 15,
"identifier": "test77",
"identifier": "test58",
"train_period_days": 30,
"backtest_period_days": 10,
"live_retrain_hours": 0,

View File

@ -0,0 +1,32 @@
{
"strategy_name": "FreqaiPrimer",
"params": {
"max_open_trades": {
"max_open_trades": 4
},
"buy": {
"buy_rsi": 39.01486243151008
},
"sell": {
"sell_rsi": 69.01486243151008
},
"protection": {},
"roi": {
"0": 0.21500000000000002,
"8": 0.081,
"38": 0.028,
"57": 0
},
"stoploss": {
"stoploss": -0.029
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.246,
"trailing_stop_positive_offset": 0.33799999999999997,
"trailing_only_offset_is_reached": true
}
},
"ft_stratparam_v": 1,
"export_time": "2025-05-17 10:28:46.008125+00:00"
}

View File

@ -1,9 +1,7 @@
import logging
import numpy as np
import pandas as pd
from functools import reduce
import talib.abstract as ta
from typing import Dict, List, Optional
from pandas import DataFrame
from technical import qtpylib
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
@ -11,29 +9,32 @@ from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
logger = logging.getLogger(__name__)
class FreqaiPrimer(IStrategy):
minimal_roi = {}
stoploss = 0.0
minimal_roi = {
0: 0.135,
9: 0.052,
15: 0.007,
60: 0
}
stoploss = -0.263
trailing_stop = True
trailing_stop_positive = 0.324
trailing_stop_positive_offset = 0.411
trailing_only_offset_is_reached = False
max_open_trades = 4
process_only_new_candles = True
use_exit_signal = True
startup_candle_count: int = 40
can_short = False
# Hyperopt 参数
buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=False, load=True)
sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=False, load=True)
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.038, space="roi", optimize=True, load=True)
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.027, space="roi", optimize=True, load=True)
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.009, space="roi", optimize=True, load=True)
stoploss_param = DecimalParameter(low=-0.25, high=-0.05, default=-0.1, space="stoploss", optimize=True, load=True)
buy_rsi = IntParameter(low=10, high=50, default=30, space="buy", optimize=False, load=True)
sell_rsi = IntParameter(low=50, high=90, default=70, space="sell", optimize=False, load=True)
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.135, space="roi", optimize=True, load=True)
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.052, space="roi", optimize=True, load=True)
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.007, space="roi", optimize=True, load=True)
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)
# 保护机制
protections = [
{"method": "StoplossGuard", "stop_duration": 60, "lookback_period": 120},
{"method": "MaxDrawdown", "lookback_period": 120, "max_allowed_drawdown": 0.05}
]
# FreqAI 配置
freqai_info = {
"model": "LightGBMRegressor",
"feature_parameters": {
@ -47,9 +48,9 @@ class FreqaiPrimer(IStrategy):
"shuffle": False,
},
"model_training_parameters": {
"n_estimators": 100,
"learning_rate": 0.1,
"num_leaves": 15, # 降低以减少警告
"n_estimators": 200,
"learning_rate": 0.05,
"num_leaves": 31,
"verbose": -1,
},
}
@ -64,16 +65,6 @@ class FreqaiPrimer(IStrategy):
"do_predict": {"do_predict": {"color": "brown"}},
},
}
def __init__(self, config: Dict):
super().__init__(config)
# 初始化特征缓存
self.feature_cache = {}
# 设置日志级别
logger.setLevel(logging.DEBUG)
# 输出模型路径用于调试
freqai_model_path = self.config.get("freqai", {}).get("model_path", "/freqtrade/user_data/models")
logger.info(f"FreqAI 模型路径:{freqai_model_path}")
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
@ -96,7 +87,6 @@ class FreqaiPrimer(IStrategy):
dataframe = dataframe.replace([np.inf, -np.inf], 0)
dataframe = dataframe.ffill()
dataframe = dataframe.fillna(0)
logger.info(f"最终数据框列:\n{dataframe.columns.to_list()}")
return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
@ -109,11 +99,14 @@ class FreqaiPrimer(IStrategy):
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 = dataframe.replace([np.inf, -np.inf], 0)
dataframe = dataframe.ffill()
dataframe = dataframe.fillna(0)
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
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
@ -124,16 +117,22 @@ class FreqaiPrimer(IStrategy):
try:
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
logger.info(f"Generated %-volatility column: {dataframe['%-volatility'].head().to_string()}")
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14).shift(-label_period)
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["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["&-buy_rsi"] = dataframe["&-buy_rsi"].shift(-label_period).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)
if dataframe[col].isna().any():
logger.warning(f"目标列 {col} 仍包含 NaN检查数据生成逻辑")
logger.warning(f"目标列 {col} 仍包含 NaN数据预览:\n{dataframe[col].tail(10)}")
except Exception as e:
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
raise
@ -143,12 +142,10 @@ class FreqaiPrimer(IStrategy):
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.info(f"处理交易对:{metadata['pair']}")
logger.info(f"开始 FreqAI 处理,交易对:{metadata['pair']}")
logger.info(f"输入数据框列:\n{dataframe.columns.to_list()}")
logger.debug(f"输入特征列:{list(dataframe.columns)}")
dataframe = self.freqai.start(dataframe, metadata, self)
logger.info(f"FreqAI 处理后数据框列:\n{dataframe.columns.to_list()}")
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"]
@ -156,70 +153,58 @@ class FreqaiPrimer(IStrategy):
dataframe["bb_upperband"] = bollinger["upper"]
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
# 生成 up_or_down 信号
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
dataframe["up_or_down"] = np.where(
dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0
)
# 预填充 NaN
dataframe = dataframe.ffill()
dataframe = dataframe.fillna(0)
if "&-buy_rsi" in dataframe.columns:
if "%-volatility" not in dataframe.columns or dataframe["%-volatility"].isna().any():
logger.error("Critical column '%-volatility' is missing or contains NaN values.")
raise ValueError("Missing or invalid '%-volatility' column")
# 派生其他目标
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["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
dataframe["&-stoploss"] = -0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)
dataframe["&-stoploss"] = self.stoploss - (dataframe["%-volatility"] * 5).clip(-0.05, 0.05)
dataframe["&-roi_0"] = (dataframe["close"].shift(-label_period) / dataframe["close"] - 1).clip(0, 0.2)
# 计算预测值并减少 NaN
dataframe["buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(5, min_periods=1).mean().clip(10, 50)
dataframe["sell_rsi_pred"] = dataframe["&-sell_rsi"].rolling(5, min_periods=1).mean().clip(50, 90)
dataframe["stoploss_pred"] = dataframe["&-stoploss"].clip(-0.25, -0.05)
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)
# 处理 NaN
for col in ["buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred", "&-sell_rsi", "&-stoploss", "&-roi_0"]:
for col in ["buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred"]:
if dataframe[col].isna().any():
logger.warning(f"{col} 包含 NaN填充为默认值")
mean_value = dataframe[col].mean()
if pd.isna(mean_value):
logger.warning(f"{col} 均值仍为 NaN使用默认值")
mean_value = {
"buy_rsi_pred": 30,
"sell_rsi_pred": 70,
"stoploss_pred": -0.1,
"roi_0_pred": 0.05,
"&-sell_rsi": 70,
"&-stoploss": -0.1,
"&-roi_0": 0.05
}.get(col, 0)
dataframe[col] = dataframe[col].fillna(mean_value)
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.stoploss = float(dataframe["stoploss_pred"].iloc[-1])
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.0
60: 0
}
self.trailing_stop_positive = float(dataframe["trailing_stop_positive"].iloc[-1])
self.trailing_stop_positive_offset = float(dataframe["trailing_stop_positive_offset"].iloc[-1])
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"minimal_roi 键:{list(self.minimal_roi.keys())}")
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()
@ -227,12 +212,13 @@ class FreqaiPrimer(IStrategy):
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
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
enter_long_conditions = [
qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"] + (5 if metadata["pair"] == "BTC/USDT" else 0)),
qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]),
df["tema"] > df["tema"].shift(1),
df["volume"] > 0,
df["do_predict"] == 1,
@ -247,28 +233,45 @@ class FreqaiPrimer(IStrategy):
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.98) |
(df["close"] < df["bb_lowerband"]),
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
]
time_exit = (df["date"] >= df["date"].shift(1) + pd.Timedelta(days=1))
df.loc[
(reduce(lambda x, y: x & y, exit_long_conditions)) | time_exit,
"exit_long"
] = 1
if exit_long_conditions:
df.loc[
reduce(lambda x, y: x & y, exit_long_conditions),
"exit_long"
] = 1
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:
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = df.iloc[-1].squeeze()
if side == "long":
if rate > (last_candle["close"] * (1 + 0.001)):
try:
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if df is None or df.empty:
logger.warning(f"无法获取 {pair} 的分析数据,拒绝交易")
return False
return True
last_candle = df.iloc[-1].squeeze()
if "close" not in last_candle or np.isnan(last_candle["close"]):
logger.warning(f"{pair} 的最新 K 线缺少有效 close 价格,拒绝交易")
return False
if side == "long":
max_rate = last_candle["close"] * (1 + 0.0025) # 0.25% 滑点阈值
if rate > max_rate:
logger.debug(f"拒绝 {pair} 的买入,价格 {rate} 超过最大允许价格 {max_rate}")
return False
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

@ -34,6 +34,17 @@ rm result/*
hyperopt_config="${STRATEGY_NAME%.py}.json"
#docker-compose -f docker-compose_backtest.yml run --rm freqtrade >output.log 2>&1
echo "docker-compose run --rm freqtrade backtesting \
--logfile /freqtrade/user_data/logs/freqtrade.log \
--freqaimodel LightGBMRegressor \
--config /freqtrade/config_examples/$CONFIG_FILE \
--strategy-path /freqtrade/templates \
--strategy $STRATEGY_NAME \
--timerange $START_DATE-$END_DATE \
--fee 0.0008 \
--cache none >output.log"
docker-compose run --rm freqtrade backtesting \
--logfile /freqtrade/user_data/logs/freqtrade.log \
--freqaimodel LightGBMRegressor \
@ -42,6 +53,7 @@ docker-compose run --rm freqtrade backtesting \
--strategy $STRATEGY_NAME \
--timerange $START_DATE-$END_DATE \
--fee 0.0008 \
--breakdown day \
--cache none >output.log 2>&1
sed -i 's/\x1B\[[0-9;]*m//g' output.log

View File

@ -32,45 +32,57 @@ rm -rf ./freqtrade/user_data/data/backtest_results/*
rm -fr ./user_data/dryrun_results/*
#docker-compose -f docker-compose_backtest.yml run --rm freqtrade >output.log 2>&1
freqtrade hyperopt \
--logfile ./user_data/logs/freqtrade.log \
--freqaimodel XGBoostRegressor \
echo "docker-compose run --rm freqtrade hyperopt \
--logfile /freqtrade/user_data/logs/freqtrade.log \
--freqaimodel LightGBMRegressor \
--strategy $STRATEGY_NAME \
--config config_examples/$CONFIG_FILE \
--strategy-path ./freqtrade/templates \
--config /freqtrade/config_examples/$CONFIG_FILE \
--strategy-path /freqtrade/templates \
--timerange ${START_DATE}-${END_DATE} \
--epochs 100 \
-e 200 \
--hyperopt-loss ShortTradeDurHyperOptLoss \
--spaces stoploss \
--spaces roi stoploss trailing \
--fee 0.0016"
docker-compose run --rm freqtrade hyperopt \
--logfile /freqtrade/user_data/logs/freqtrade.log \
--freqaimodel LightGBMRegressor \
--strategy $STRATEGY_NAME \
--config /freqtrade/config_examples/$CONFIG_FILE \
--strategy-path /freqtrade/templates \
--timerange ${START_DATE}-${END_DATE} \
-e 200 \
--hyperopt-loss ShortTradeDurHyperOptLoss \
--spaces roi stoploss trailing \
--fee 0.0016
#>output.log 2>&1
#sed -i 's/\x1B\[[0-9;]*m//g' output.log
#python3 tools/filter.py
rm ./result/*.json -fr
rm ./result/*.py -fr
mv ./user_data/backtest_results/* ./result/
cd ./result
# 查找当前目录下的所有 zip 文件
zip_files=(*.zip)
# 检查是否只有一个 zip 文件
if [ ${#zip_files[@]} -eq 1 ]; then
# 解压缩该 zip 文件到当前目录
unzip "${zip_files[0]}"
rm *.zip
rm *.feather
else
echo "当前目录下没有 zip 文件或者有多个 zip 文件,无法操作。"
fi
cd -
sed -i 's/\x1B\[[0-9;]*m//g' output.log
#python3 ../filter.py
cp output.log result/ -f
cd tools/
python tradestocsv.py
python analytic.py >../result/analytic.log
cd ../
# rm ./result/*.json -fr
# rm ./result/*.py -fr
# mv ./user_data/backtest_results/* ./result/
#
# cd ./result
# # 查找当前目录下的所有 zip 文件
# zip_files=(*.zip)
#
# # 检查是否只有一个 zip 文件
# if [ ${#zip_files[@]} -eq 1 ]; then
# # 解压缩该 zip 文件到当前目录
# unzip "${zip_files[0]}"
# rm *.zip
# rm *.feather
# else
# echo "当前目录下没有 zip 文件或者有多个 zip 文件,无法操作。"
# fi
#
# cd -
# sed -i 's/\x1B\[[0-9;]*m//g' output.log
# #python3 ../filter.py
# cp output.log result/ -f
# cd tools/
# python tradestocsv.py
# python analytic.py >../result/analytic.log
# cd ../