This commit is contained in:
zhangkun9038@dingtalk.com 2025-06-27 07:51:27 +00:00
parent 06fb2e8ead
commit e485c4b119
7 changed files with 791 additions and 74 deletions

View File

@ -64,14 +64,7 @@
},
"use_exit_signal": true,
"exit_profit_only": false,
"ignore_roi_if_entry_signal": false,
"stoploss": -0.05,
"minimal_roi": {
"0": 0.06,
"30": 0.04,
"60": 0.02,
"120": 0
},
"stake_currency": "USDT",
"stake_amount": 150,
"max_open_trades": 4,

View File

@ -4,12 +4,12 @@
"margin_mode": "isolated",
"max_open_trades": 5,
"stake_currency": "USDT",
"stake_amount": 100,
"stake_amount": 75,
"tradable_balance_ratio": 1,
"fiat_display_currency": "USD",
"dry_run": true,
"timeframe": "3m",
"dry_run_wallet": 1000,
"dry_run_wallet": 2000,
"cancel_open_orders_on_exit": true,
"stoploss": -0.05,
"max_entry_position_adjustment": 3,

View File

@ -0,0 +1,44 @@
{
"strategy_name": "FreqaiPrimer",
"params": {
"stoploss": {
"stoploss": -0.05
},
"max_open_trades": {
"max_open_trades": 5
},
"buy": {
"ADD_POSITION_THRESHOLD": -0.046,
"BUY_THRESHOLD_MAX": -0.01,
"BUY_THRESHOLD_MIN": -0.026,
"COOLDOWN_PERIOD_MINUTES": 9,
"MAX_ENTRY_POSITION_ADJUSTMENT": 3
},
"sell": {
"EXIT_POSITION_RATIO": 0.572,
"HOLD_TIME_EXTENSION": 1.975,
"HOLD_TIME_REDUCTION": 0.832,
"MAX_HOLD_TIME_MINUTES": 101,
"SELL_THRESHOLD_MAX": 0.085,
"SELL_THRESHOLD_MIN": 0.007,
"SLOPE_THRESHOLD": 0.0,
"TRAILING_STOP_DISTANCE": 0.005,
"TRAILING_STOP_START": 0.048
},
"protection": {},
"roi": {
"0": 0.128,
"12": 0.038,
"36": 0.023,
"53": 0
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.09,
"trailing_stop_positive_offset": 0.16499999999999998,
"trailing_only_offset_is_reached": false
}
},
"ft_stratparam_v": 1,
"export_time": "2025-06-27 03:30:20.329593+00:00"
}

727
freqtrade/templates/aa.py Normal file
View File

@ -0,0 +1,727 @@
import logging
import numpy as np
import datetime
import os
import json
import glob
from functools import reduce
from freqtrade.persistence import Trade
import talib.abstract as ta
from pandas import DataFrame
import pandas as pd
from typing import Dict
from freqtrade.strategy import (DecimalParameter, IStrategy, IntParameter)
logger = logging.getLogger(__name__)
class FreqaiPrimer(IStrategy):
"""
优化后的 FreqAI 动态阈值交易策略
- 主时间框架3m
- FreqAI 时间框架3m, 15m, 1h
- 动态最大持仓时间30-360 分钟基于市场趋势和利润率
- 改进趋势判定移除模拟 4h K线使用 1h 线性回归和 3m 短期趋势
- 新增 MACD Stochastic 指标增强特征工程
- 修改使用 FreqAI 优化 stake_amount
"""
# --- 🧪 Hyperopt Parameters ---
ROI_T0 = DecimalParameter(0.01, 0.05, default=0.02, space='roi', optimize=True)
ROI_T1 = DecimalParameter(0.005, 0.02, default=0.01, space='roi', optimize=True)
ROI_T2 = DecimalParameter(0.0, 0.01, default=0.0, space='roi', optimize=True)
TRAILING_STOP_START = DecimalParameter(0.01, 0.05, default=0.03, space='sell', optimize=True)
TRAILING_STOP_DISTANCE = DecimalParameter(0.005, 0.02, default=0.01, space='sell', optimize=True)
BUY_THRESHOLD_MIN = DecimalParameter(-0.1, -0.01, default=-0.05, space='buy', optimize=True)
BUY_THRESHOLD_MAX = DecimalParameter(-0.02, -0.001, default=-0.005, space='buy', optimize=True)
SELL_THRESHOLD_MIN = DecimalParameter(0.001, 0.02, default=0.005, space='sell', optimize=True)
SELL_THRESHOLD_MAX = DecimalParameter(0.02, 0.1, default=0.05, space='sell', optimize=True)
ADD_POSITION_THRESHOLD = DecimalParameter(-0.05, -0.01, default=-0.02, space='buy', optimize=True)
EXIT_POSITION_RATIO = DecimalParameter(0.2, 0.7, default=0.5, space='sell', optimize=True)
COOLDOWN_PERIOD_MINUTES = IntParameter(1, 10, default=5, space='buy', optimize=True)
MAX_ENTRY_POSITION_ADJUSTMENT = IntParameter(1, 3, default=2, space='buy', optimize=True)
# 新增:持仓时间和趋势斜率参数
MAX_HOLD_TIME_MINUTES = IntParameter(30, 360, default=120, space='sell', optimize=True)
HOLD_TIME_EXTENSION = DecimalParameter(1.0, 2.0, default=1.5, space='sell', optimize=True) # 牛市延长因子
HOLD_TIME_REDUCTION = DecimalParameter(0.5, 1.0, default=0.7, space='sell', optimize=True) # 熊市缩短因子
SLOPE_THRESHOLD = DecimalParameter(0.00005, 0.001, default=0.0001, space='sell', optimize=True) # 趋势斜率阈值
# --- 🛠️ 固定配置参数 ---
minimal_roi = {
"0": ROI_T0.value,
"30": ROI_T1.value,
"60": ROI_T2.value
}
stoploss = -0.015
timeframe = "3m"
use_custom_stoploss = True
position_adjustment_enable = True
startup_candle_count = 200
plot_config = {
"main_plot": {
"ema200": {"color": "blue"},
"bb_upperband": {"color": "gray"},
"bb_lowerband": {"color": "gray"},
"bb_middleband": {"color": "gray"}
},
"subplots": {
"Signals": {
"enter_long": {"color": "green"},
"exit_long": {"color": "red"}
},
"Price-Value Divergence": {
"&-price_value_divergence": {"color": "purple"}
},
"Volume Z-Score": {
"volume_z_score": {"color": "orange"}
},
"RSI": {
"rsi": {"color": "cyan"}
},
"MACD": {
"macd": {"color": "blue"},
"macdsignal": {"color": "orange"}
}
}
}
freqai_info = {
"identifier": "test58",
"model": "LightGBMRegressor",
"feature_parameters": {
"include_timeframes": ["3m", "15m", "1h"],
"label_period_candles": 12,
"include_shifted_candles": 3,
"include_corr_pairs": [],
"include_returned_api_pairs": False
},
"data_split_parameters": {
"test_size": 0.2,
"shuffle": False
},
"model_training_parameters": {
"n_estimators": 200,
"learning_rate": 0.05,
"num_leaves": 31,
"verbose": -1
},
"fit_live_predictions_candles": 100,
"live_retrain_candles": 100,
"target_parameters": { # 新增:支持多目标回归
"targets": ["&-price_value_divergence", "&-stake_amount"]
}
}
def __init__(self, config: dict, *args, **kwargs):
super().__init__(config, *args, **kwargs)
logger.debug("✅ 策略已初始化,日志级别设置为 DEBUG")
self.trailing_stop_enabled = False
self.pair_stats = {}
self.stats_logged = False
self.fit_live_predictions_candles = self.freqai_info.get("fit_live_predictions_candles", 100)
self.last_entry_time = {}
self.initial_stake = {} # 新增:记录每笔交易的首次入场金额
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
pair = metadata.get('pair', 'Unknown')
# 现有指标
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
real = ta.TYPPRICE(dataframe)
upperband, middleband, lowerband = ta.BBANDS(real, timeperiod=period, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_lowerband-period"] = lowerband
dataframe["bb_upperband-period"] = upperband
dataframe["bb_middleband-period"] = middleband
dataframe["%-bb_width-period"] = (dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"]) / dataframe["bb_middleband-period"]
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-relative_volume-period"] = dataframe["volume"] / dataframe["volume"].rolling(period).mean()
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["%-price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
# 新增指标
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["%-macd-period"] = macd['macd']
dataframe["%-macdsignal-period"] = macd['macdsignal']
dataframe["%-macdhist-period"] = macd['macdhist']
stoch = ta.STOCH(dataframe, fastk_period=14, slowk_period=3, slowd_period=3)
dataframe["%-slowk-period"] = stoch['slowk']
dataframe["%-slowd-period"] = stoch['slowd']
columns_to_clean = [
"%-rsi-period", "%-mfi-period", "%-sma-period", "%-ema-period", "%-adx-period",
"bb_lowerband-period", "bb_middleband-period", "bb_upperband-period",
"%-bb_width-period", "%-relative_volume-period", "%-price_value_divergence",
"%-macd-period", "%-macdsignal-period", "%-macdhist-period",
"%-slowk-period", "%-slowd-period"
]
for col in columns_to_clean:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
logger.debug(f"[{pair}] 特征工程完成,列:{list(dataframe.columns)}")
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
pair = metadata.get('pair', 'Unknown')
if len(dataframe) < 200:
logger.warning(f"[{pair}] 数据量不足({len(dataframe)}根K线需要至少200根K线进行训练")
return dataframe
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["&-price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
dataframe["volume_mean_20"] = dataframe["volume"].rolling(20).mean()
dataframe["volume_std_20"] = dataframe["volume"].rolling(20).std()
dataframe["volume_z_score"] = (dataframe["volume"] - dataframe["volume_mean_20"]) / dataframe["volume_std_20"]
# 新增:计算 FreqAI 目标 stake_amount
# 基于市场趋势、价格波动性和仓位调整逻辑动态计算
market_trend = self.get_market_trend(dataframe=dataframe, metadata=metadata)
base_stake = 60 # 默认基础下单金额
stake_multiplier = 1.0
if market_trend == 'bull':
stake_multiplier = 1.2 # 牛市增加下单金额
elif market_trend == 'bear':
stake_multiplier = 0.8 # 熊市减少下单金额
# 计算价格波动性(基于近期收盘价的标准差)
price_volatility = dataframe["close"].pct_change().rolling(20).std().iloc[-1]
if np.isnan(price_volatility):
price_volatility = 0.01 # 防止除零
volatility_factor = 1.0 + min(price_volatility * 10, 0.5) # 波动性调整因子,限制最大增幅
# 计算 &-stake_amount
dataframe["&-stake_amount"] = base_stake * stake_multiplier * volatility_factor
# 限制 stake_amount 在合理范围内(例如 7 到 200
dataframe["&-stake_amount"] = dataframe["&-stake_amount"].clip(lower=7, upper=200)
# 数据清理
dataframe["&-price_value_divergence"] = dataframe["&-price_value_divergence"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
dataframe["volume_z_score"] = dataframe["volume_z_score"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
dataframe["&-stake_amount"] = dataframe["&-stake_amount"].replace([np.inf, -np.inf], 0).ffill().fillna(base_stake)
logger.debug(f"[{pair}] 设置 FreqAI 目标 - "
f"&-price_value_divergence={dataframe['&-price_value_divergence'].iloc[-1]:.6f}, "
f"&-stake_amount={dataframe['&-stake_amount'].iloc[-1]:.2f}, "
f"market_trend={market_trend}, volatility_factor={volatility_factor:.2f}")
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata.get('pair', 'Unknown')
dataframe = self.freqai.start(dataframe, metadata, self)
logger.info(f"[{pair}] 当前可用列调用FreqAI前{list(dataframe.columns)}")
# 基本指标
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
if not hasattr(self, 'freqai') or self.freqai is None:
logger.error(f"[{pair}] FreqAI 未初始化,请确保回测命令中启用了 --freqai")
dataframe["&-price_value_divergence"] = dataframe["price_value_divergence"]
dataframe["&-stake_amount"] = 60 # 回退默认值
else:
logger.debug(f"self.freqai 类型:{type(self.freqai)}")
if "&-price_value_divergence" not in dataframe.columns:
logger.warning(f"[{pair}] 回归模型未生成 &-price_value_divergence回退到规则计算")
dataframe["&-price_value_divergence"] = dataframe["price_value_divergence"]
if "&-stake_amount" not in dataframe.columns:
logger.warning(f"[{pair}] 回归模型未生成 &-stake_amount回退到默认值 60")
dataframe["&-stake_amount"] = 60
upperband, middleband, lowerband = ta.BBANDS(dataframe["close"], timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_upperband"] = upperband
dataframe["bb_middleband"] = middleband
dataframe["bb_lowerband"] = lowerband
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["volume_mean_20"] = dataframe["volume"].rolling(20).mean()
dataframe["volume_std_20"] = dataframe["volume"].rolling(20).std()
dataframe["volume_z_score"] = (dataframe["volume"] - dataframe["volume_mean_20"]) / dataframe["volume_std_20"]
# 新增 MACD 和 Stochastic
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["macd"] = macd['macd']
dataframe["macdsignal"] = macd['macdsignal']
dataframe["macdhist"] = macd['macdhist']
stoch = ta.STOCH(dataframe, fastk_period=14, slowk_period=3, slowd_period=3)
dataframe["slowk"] = stoch['slowk']
dataframe["slowd"] = stoch['slowd']
# 数据清理
for col in ["ema200", "bb_upperband", "bb_middleband", "bb_lowerband", "rsi", "volume_z_score",
"&-price_value_divergence", "price_value_divergence", "macd", "macdsignal", "macdhist",
"slowk", "slowd", "&-stake_amount"]:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
logger.debug(f"[{pair}] 最新数据 - close{dataframe['close'].iloc[-1]:.6f}, "
f"rsi{dataframe['rsi'].iloc[-1]:.2f}, "
f"&-price_value_divergence{dataframe['&-price_value_divergence'].iloc[-1]:.6f}, "
f"volume_z_score{dataframe['volume_z_score'].iloc[-1]:.2f}, "
f"bb_lowerband{dataframe['bb_lowerband'].iloc[-1]:.6f}, "
f"macd{dataframe['macd'].iloc[-1]:.6f}, "
f"slowk{dataframe['slowk'].iloc[-1]:.2f}, "
f"&-stake_amount{dataframe['&-stake_amount'].iloc[-1]:.2f}")
# 获取 labels_mean 和 labels_std
labels_mean = None
labels_std = None
try:
model_base_dir = os.path.join(self.config["user_data_dir"], "models", self.freqai_info["identifier"])
pair_base = pair.split('/')[0] if '/' in pair else pair
sub_dirs = glob.glob(os.path.join(model_base_dir, f"sub-train-{pair_base}_*"))
if not sub_dirs:
logger.warning(f"[{pair}] 未找到任何子目录:{model_base_dir}/sub-train-{pair_base}_*")
else:
latest_sub_dir = max(sub_dirs, key=lambda x: int(x.split('_')[-1]))
pair_base_lower = pair_base.lower()
timestamp = latest_sub_dir.split('_')[-1]
metadata_file = os.path.join(latest_sub_dir, f"cb_{pair_base_lower}_{timestamp}_metadata.json")
if os.path.exists(metadata_file):
with open(metadata_file, "r") as f:
metadata_json = json.load(f)
labels_mean = metadata_json["labels_mean"]
labels_std = metadata_json["labels_std"]
logger.info(f"[{pair}] 从最新子目录 {latest_sub_dir} 读取 labels_mean{labels_mean}, labels_std{labels_std}")
else:
logger.warning(f"[{pair}] 最新的 metadata.json 文件 {metadata_file} 不存在")
except Exception as e:
logger.warning(f"[{pair}] 无法从子目录读取 labels_mean 和 labels_std{e},重新计算")
if labels_mean is None or labels_std is None:
logger.warning(f"[{pair}] 无法获取 labels_mean 和 labels_std重新计算")
dataframe["&-price_value_divergence_actual"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
dataframe["&-price_value_divergence_actual"] = dataframe["&-price_value_divergence_actual"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
recent_data = dataframe["&-price_value_divergence_actual"].tail(self.fit_live_predictions_candles)
labels_mean = {'&-price_value_divergence': recent_data.mean(), '&-stake_amount': 60}
labels_std = {'&-price_value_divergence': recent_data.std(), '&-stake_amount': 10}
if np.isnan(labels_std['&-price_value_divergence']) or labels_std['&-price_value_divergence'] == 0:
labels_std['&-price_value_divergence'] = 0.01
logger.warning(f"[{pair}] &-price_value_divergence labels_std 计算异常,使用默认值 0.01")
if np.isnan(labels_std['&-stake_amount']) or labels_std['&-stake_amount'] == 0:
labels_std['&-stake_amount'] = 10
logger.warning(f"[{pair}] &-stake_amount labels_std 计算异常,使用默认值 10")
# 动态调整买卖阈值
market_trend = self.get_market_trend(dataframe=dataframe, metadata=metadata)
k_buy = 1.0
k_sell = 1.2
if market_trend == 'bull':
k_buy = 0.8
k_sell = 1.0
elif market_trend == 'bear':
k_buy = 1.2
k_sell = 1.5
else:
k_buy = 1.0
k_sell = 1.2
if labels_mean['&-price_value_divergence'] > 0.015:
k_sell += 0.5
self.buy_threshold = labels_mean['&-price_value_divergence'] - k_buy * labels_std['&-price_value_divergence']
self.sell_threshold = labels_mean['&-price_value_divergence'] + k_sell * labels_std['&-price_value_divergence']
self.buy_threshold = max(self.buy_threshold, self.BUY_THRESHOLD_MIN.value)
self.buy_threshold = min(self.buy_threshold, self.BUY_THRESHOLD_MAX.value)
self.sell_threshold = min(self.sell_threshold, self.SELL_THRESHOLD_MAX.value)
self.sell_threshold = max(self.sell_threshold, self.SELL_THRESHOLD_MIN.value)
logger.info(f"[{pair}] 市场趋势:{market_trend}, labels_mean{labels_mean}, labels_std{labels_std}")
logger.info(f"[{pair}] k_buy{k_buy:.2f}, k_sell{k_sell:.2f}")
logger.info(f"[{pair}] 动态买入阈值:{self.buy_threshold:.4f}, 卖出阈值:{self.sell_threshold:.4f}")
if not self.stats_logged:
logger.info("===== 所有币对的 labels_mean 和 labels_std 汇总 =====")
for p, stats in self.pair_stats.items():
logger.info(f"[{p}] labels_mean{stats['labels_mean']}, labels_std{stats['labels_std']}")
logger.info("==============================================")
self.stats_logged = True
return dataframe
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
return {
"0": params.get("roi_t0", self.ROI_T0.value),
"30": params.get("roi_t1", self.ROI_T1.value),
"60": params.get("roi_t2", self.ROI_T2.value)
}
def roi_space(self):
return [self.ROI_T0, self.ROI_T1, self.ROI_T2]
def trailing_space(self):
return [self.TRAILING_STOP_START, self.TRAILING_STOP_DISTANCE]
def leverage_space(self):
return [
self.ADD_POSITION_THRESHOLD,
self.EXIT_POSITION_RATIO,
self.COOLDOWN_PERIOD_MINUTES,
self.MAX_ENTRY_POSITION_ADJUSTMENT
]
def buy_space(self):
return [
self.BUY_THRESHOLD_MIN,
self.BUY_THRESHOLD_MAX,
self.ADD_POSITION_THRESHOLD,
self.COOLDOWN_PERIOD_MINUTES,
self.MAX_ENTRY_POSITION_ADJUSTMENT
]
def sell_space(self):
return [
self.SELL_THRESHOLD_MIN,
self.SELL_THRESHOLD_MAX,
self.EXIT_POSITION_RATIO,
self.MAX_HOLD_TIME_MINUTES,
self.HOLD_TIME_EXTENSION,
self.HOLD_TIME_REDUCTION,
self.SLOPE_THRESHOLD
]
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float | None, max_stake: float,
leverage: float, entry_tag: str | None, side: str, **kwargs) -> float:
dataframe = self.dp.get_pair_dataframe(pair, self.timeframe)
dataframe = self.populate_indicators(dataframe, {'pair': pair})
stake_amount = dataframe["&-stake_amount"].iloc[-1] if "&-stake_amount" in dataframe.columns else 60
stake_amount = max(min(stake_amount, max_stake), min_stake or 7) # 确保在 min_stake 和 max_stake 范围内
logger.debug(f"[{pair}] 使用 FreqAI 预测的 stake_amount: {stake_amount:.2f}")
return stake_amount
def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float,
min_stake: float | None, max_stake: float,
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs) -> float | None | tuple[float | None, str | None]:
"""
动态调整仓位支持加仓减仓追踪止损和最大持仓时间限制
- 加仓逻辑第一次加仓为首次入场金额的2倍第二次4倍第三次8倍
- 增强日志记录首次入场金额和加仓计算
"""
pair = trade.pair
dataframe = self.dp.get_pair_dataframe(pair, self.timeframe)
dataframe = self.populate_indicators(dataframe, {'pair': pair})
stake_amount_value = dataframe["&-stake_amount"].iloc[-1] if "&-stake_amount" in dataframe.columns else 60
initial_stake_amount = self.initial_stake.get(pair, stake_amount_value)
market_trend = self.get_market_trend(dataframe=dataframe, metadata={'pair': pair})
hold_time = (current_time - trade.open_date_utc).total_seconds() / 60
profit_ratio = (current_rate - trade.open_rate) / trade.open_rate
# 获取首次入场金额
initial_stake_amount = self.initial_stake.get(pair, stake_amount_value)
logger.debug(f"{pair} 首次入场金额: {initial_stake_amount:.2f}, 当前持仓金额: {trade.stake_amount:.2f}, "
f"加仓次数: {trade.nr_of_successful_entries - 1}")
# 动态最大持仓时间
max_hold_time = self.MAX_HOLD_TIME_MINUTES.value
if market_trend == 'bull':
max_hold_time *= self.HOLD_TIME_EXTENSION.value
elif market_trend == 'bear':
max_hold_time *= self.HOLD_TIME_REDUCTION.value
if profit_ratio > 0.02:
max_hold_time *= 1.2
elif profit_ratio < -0.02:
max_hold_time *= 0.8
max_hold_time = int(max_hold_time)
# 加仓逻辑
max_entry_adjustments = self.MAX_ENTRY_POSITION_ADJUSTMENT.value
if trade.nr_of_successful_entries <= max_entry_adjustments + 1:
add_position_threshold = self.ADD_POSITION_THRESHOLD.value
if profit_ratio <= add_position_threshold and hold_time > 5 and market_trend in ['bear', 'sideways']:
# 根据加仓次数设置倍数
add_count = trade.nr_of_successful_entries - 1 # 当前加仓次数0, 1, 2
if add_count < 3: # 限制最多3次加仓
multiplier = 2 ** (add_count + 1) # 2^1=2, 2^2=4, 2^3=8
add_amount = initial_stake_amount * multiplier
logger.debug(f"{pair} 加仓计算: 第 {add_count + 1} 次加仓,倍数={multiplier}, "
f"金额 = {initial_stake_amount:.2f} * {multiplier} = {add_amount:.2f}")
# 检查最小和最大下单金额
if min_stake is not None and add_amount < min_stake:
logger.warning(f"{pair} 加仓金额 {add_amount:.2f} 低于最小下单金额 {min_stake:.2f},取消加仓")
return (None, f"Add amount {add_amount:.2f} below min_stake {min_stake:.2f}")
if add_amount > max_stake:
logger.warning(f"{pair} 加仓金额 {add_amount:.2f} 超出最大可用金额 {max_stake:.2f},调整为 {max_stake:.2f}")
add_amount = max_stake
logger.info(f"{pair} 价格下跌 {profit_ratio*100:.2f}%,触发第 {add_count + 1} 次加仓 {add_amount:.2f}")
return (add_amount, f"Price dropped {profit_ratio*100:.2f}%, add {add_amount:.2f}")
# 减仓逻辑
exit_position_ratio = self.EXIT_POSITION_RATIO.value
if profit_ratio >= 0.03:
if market_trend == 'bull':
reduce_amount = -exit_position_ratio * 0.6 * trade.stake_amount
logger.info(f"{pair} 牛市,利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}")
return (reduce_amount, f"Bull market, profit {profit_ratio*100:.2f}%")
else:
reduce_amount = -exit_position_ratio * trade.stake_amount
logger.info(f"{pair} 利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}")
return (reduce_amount, f"Profit {profit_ratio*100:.2f}%")
elif profit_ratio >= 0.05:
reduce_amount = -exit_position_ratio * 1.4 * trade.stake_amount
logger.info(f"{pair} 利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}")
return (reduce_amount, f"Profit {profit_ratio*100:.2f}%")
# 追踪止损逻辑
trailing_stop_start = self.TRAILING_STOP_START.value
trailing_stop_distance = self.TRAILING_STOP_DISTANCE.value
if market_trend == 'bull':
trailing_stop_distance *= 1.5
trailing_stop_start *= 1.2
elif market_trend == 'bear':
trailing_stop_distance *= 0.7
trailing_stop_start *= 0.8
if profit_ratio >= trailing_stop_start and not self.trailing_stop_enabled:
self.trailing_stop_enabled = True
trade.adjust_min_max_rates(current_rate, current_rate)
logger.info(f"{pair} 价格上涨超过 {trailing_stop_start*100:.1f}%,启动追踪止损")
return None
if self.trailing_stop_enabled:
max_rate = trade.max_rate or current_rate
trailing_stop_price = max_rate * (1 - trailing_stop_distance)
if current_rate < trailing_stop_price:
logger.info(f"{pair} 价格回落至 {trailing_stop_price:.6f},触发全部卖出")
return (-trade.stake_amount, f"Trailing stop at {trailing_stop_price:.6f}")
trade.adjust_min_max_rates(current_rate, trade.min_rate)
return None
# 最大持仓时间限制
if hold_time > max_hold_time:
logger.info(f"{pair} {market_trend} 市场,持仓时间超过 {max_hold_time} 分钟,强制清仓")
return (-trade.stake_amount, f"Max hold time ({max_hold_time} min) exceeded")
return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, **kwargs) -> bool:
dataframe = self.dp.get_pair_dataframe(pair, self.timeframe)
dataframe = self.populate_indicators(dataframe, {'pair': pair})
stake_amount_value = dataframe["&-stake_amount"].iloc[-1] if "&-stake_amount" in dataframe.columns else 60
self.initial_stake[pair] = stake_amount_value
market_trend = self.get_market_trend(dataframe=DataFrame(), metadata={'pair': pair})
cooldown_period_minutes = self.COOLDOWN_PERIOD_MINUTES.value if market_trend == 'bull' else self.COOLDOWN_PERIOD_MINUTES.value // 2
if pair in self.last_entry_time:
last_time = self.last_entry_time[pair]
if (current_time - last_time).total_seconds() < cooldown_period_minutes * 60:
logger.info(f"[{pair}] 冷却期内({cooldown_period_minutes} 分钟),跳过本次入场")
return False
self.initial_stake[pair] = stake_amount_value
self.last_entry_time[pair] = current_time
self.trailing_stop_enabled = False
logger.info(f"[{pair}] 确认入场,价格:{rate:.6f}, 首次入场金额:{self.initial_stake[pair]:.2f}")
return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, exit_reason: str,
current_time: datetime, **kwargs) -> bool:
adjusted_rate = rate * (1 + 0.00125)
logger.info(f"[{pair}] 退出交易,原因:{exit_reason}, 原始利润:{trade.calc_profit_ratio(rate):.2%}, "
f"调整后卖出价:{adjusted_rate:.6f}")
# 新增:清理 initial_stake 记录
if pair in self.initial_stake:
del self.initial_stake[pair]
return True
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
entry_tag: str | None, side: str, **kwargs) -> float:
adjusted_rate = proposed_rate * (1 - 0.00125)
logger.debug(f"[{pair}] 自定义买入价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f}")
return adjusted_rate
def custom_exit_price(self, pair: str, trade: Trade,
current_time: datetime, proposed_rate: float,
current_profit: float, exit_tag: str | None, **kwargs) -> float:
adjusted_rate = proposed_rate * (1 + 0.00125)
logger.debug(f"[{pair}] 自定义卖出价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f}")
return adjusted_rate
def get_market_trend(self, dataframe: DataFrame = None, metadata: dict = None) -> str:
"""
判断市场趋势基于1h或15m数据分析简单趋势
- 确保ma120列始终生成修复'ma120'错误
- 添加详细日志调试数据状态
- 简化逻辑降低数据要求
"""
pair = metadata.get('pair', 'Unknown')
try:
# 获取1h数据
df = self.dp.get_pair_dataframe(pair=pair, timeframe='1h')
timeframe = '1h'
logger.debug(f"{pair} 1h DataFrame 初始状态: shape={df.shape if df is not None else 'None'}, "
f"columns={df.columns.tolist() if df is not None else 'None'}, "
f"dtypes={df.dtypes if df is not None else 'None'}")
# 检查1h数据有效性
if df is None or df.empty or 'close' not in df.columns:
logger.warning(f"{pair} 1h 数据不可用或缺少 'close'尝试15m数据")
df = self.dp.get_pair_dataframe(pair=pair, timeframe='15m')
timeframe = '15m'
logger.debug(f"{pair} 15m DataFrame 状态: shape={df.shape if df is not None else 'None'}, "
f"columns={df.columns.tolist() if df is not None else 'None'}, "
f"dtypes={df.dtypes if df is not None else 'None'}")
if df is None or df.empty or 'close' not in df.columns:
logger.error(f"{pair} 15m 数据也不可用,返回默认趋势:震荡市")
return "sideways"
# 验证数据长度
available_candles = len(df)
logger.debug(f"{pair} 数据长度: {available_candles} 根K线")
if available_candles < 3:
logger.warning(f"{pair} 数据不足({available_candles}根K线需至少3根返回默认趋势震荡市")
return "sideways"
# 确保close列为数值型
df['close'] = pd.to_numeric(df['close'], errors='coerce')
if df['close'].isna().all():
logger.error(f"{pair} 'close' 列全为NaN或无效返回默认趋势震荡市")
return "sideways"
logger.debug(f"{pair} close列样本: {df['close'].tail(3).tolist()}")
# 计算ma120
sma_period = min(available_candles, 120 if timeframe == '1h' else 480)
if sma_period < 3:
sma_period = 3
logger.warning(f"{pair} 数据量不足使用最小SMA周期{sma_period}")
try:
df['ma120'] = df['close'].rolling(window=sma_period, min_periods=1).mean()
except Exception as e:
logger.error(f"{pair} rolling mean 计算失败:{e}使用close列作为回退")
df['ma120'] = df['close'].copy()
# 验证ma120列
if 'ma120' not in df.columns or df['ma120'].isna().all():
logger.error(f"{pair} ma120 列生成失败使用close列作为最终回退")
df['ma120'] = df['close'].copy()
logger.debug(f"{pair} ma120 计算后: exists={'ma120' in df.columns}, "
f"has_NaN={df['ma120'].isna().any()}, sample={df['ma120'].tail(3).tolist()}")
# 提取最近数据
last_n = df.tail(3 if timeframe == '1h' else 12) # 3根1h或12根15m ≈ 3小时
if len(last_n) < 3:
logger.warning(f"{pair} 最近数据不足({len(last_n)}根K线返回默认趋势震荡市")
return "sideways"
logger.debug(f"{pair} last_n 状态: shape={last_n.shape}, columns={last_n.columns.tolist()}")
# 计算斜率
try:
x = np.arange(len(last_n))
price_slope, _ = np.polyfit(x, last_n['close'].fillna(last_n['close'].mean()), 1)
ma_slope, _ = np.polyfit(x, last_n['ma120'].fillna(last_n['ma120'].mean()), 1)
except Exception as e:
logger.error(f"{pair} 斜率计算失败:{e},返回默认趋势:震荡市")
return "sideways"
current_price = last_n['close'].iloc[-1]
current_ma120 = last_n['ma120'].iloc[-1]
price_above_ma = current_price > current_ma120
logger.debug(f"{pair} 趋势计算: price_slope={price_slope:.6f}, ma_slope={ma_slope:.6f}, "
f"price_above_ma={price_above_ma}, current_price={current_price:.6f}, "
f"current_ma120={current_ma120:.6f}")
# 趋势判定
if price_slope > self.SLOPE_THRESHOLD.value and ma_slope > self.SLOPE_THRESHOLD.value and price_above_ma:
logger.info(f"{pair} 检测到牛市")
return "bull"
elif price_slope < -self.SLOPE_THRESHOLD.value and ma_slope < -self.SLOPE_THRESHOLD.value and not price_above_ma:
logger.info(f"{pair} 检测到熊市")
return "bear"
else:
logger.info(f"{pair} 检测到震荡市")
return "sideways"
except Exception as e:
logger.error(f"{pair} 获取市场趋势失败:{e}")
return "sideways"
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
生成买入信号放松条件以增加交易机会
- 移除 MACD 条件
- 放宽 RSI 阈值 (< 60)
- 放宽 Volume Z-Score (> 0.5)
- 详细日志记录每个条件的值
"""
pair = metadata.get('pair', 'Unknown')
conditions = []
if "&-price_value_divergence" in dataframe.columns:
cond1 = (dataframe["&-price_value_divergence"] < self.buy_threshold)
cond2 = (dataframe["volume_z_score"] > 0.5) # 放宽到 0.5
cond3 = (dataframe["rsi"] < 60) # 放宽到 60
cond4 = (dataframe["close"] <= dataframe["bb_lowerband"])
buy_condition = cond1 & cond2 & cond3 & cond4
conditions.append(buy_condition)
# 详细日志
divergence_value = dataframe['&-price_value_divergence'].iloc[-1] if not dataframe['&-price_value_divergence'].isna().all() else np.nan
logger.debug(f"[{pair}] 买入条件检查 - "
f"&-price_value_divergence={divergence_value:.6f} < {self.buy_threshold:.6f}: {cond1.iloc[-1]}, "
f"volume_z_score={dataframe['volume_z_score'].iloc[-1]:.2f} > 0.5: {cond2.iloc[-1]}, "
f"rsi={dataframe['rsi'].iloc[-1]:.2f} < 60: {cond3.iloc[-1]}, "
f"close={dataframe['close'].iloc[-1]:.6f} <= bb_lowerband={dataframe['bb_lowerband'].iloc[-1]:.6f}: {cond4.iloc[-1]}")
else:
logger.warning(f"[{pair}] ⚠️ &-price_value_divergence 列缺失,跳过买入信号生成")
if len(conditions) > 0:
combined_condition = reduce(lambda x, y: x & y, conditions)
if combined_condition.any():
dataframe.loc[combined_condition, 'enter_long'] = 1
logger.info(f"[{pair}] 入场信号触发,条件满足")
else:
logger.debug(f"[{pair}] 买入条件均不满足,未触发入场信号")
else:
logger.debug(f"[{pair}] 无有效买入条件")
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
生成卖出信号
- 放宽条件移除 MACD Stochastic
"""
pair = metadata.get('pair', 'Unknown')
conditions = []
if "&-price_value_divergence" in dataframe.columns:
cond1 = (dataframe["&-price_value_divergence"] > self.sell_threshold)
cond2 = (dataframe["rsi"] > 75)
sell_condition = cond1 | cond2
conditions.append(sell_condition)
logger.debug(f"[{pair}] 卖出条件检查 - "
f"&-price_value_divergence={dataframe['&-price_value_divergence'].iloc[-1]:.6f} > {self.sell_threshold:.6f}: {cond1.iloc[-1]}, "
f"rsi={dataframe['rsi'].iloc[-1]:.2f} > 75: {cond2.iloc[-1]}")
else:
logger.warning(f"[{pair}] ⚠️ &-price_value_divergence 列缺失,跳过卖出信号生成")
if len(conditions) > 0:
dataframe.loc[reduce(lambda x, y: x & y, conditions), 'exit_long'] = 1
logger.debug(f"[{pair}] 出场信号触发,条件满足")
else:
logger.debug(f"[{pair}] 无有效卖出条件")
return dataframe

View File

@ -1,6 +1,7 @@
{
"strategy_name": "FreqaiPrimer",
"params": {
"roi": {},
"stoploss": {
"stoploss": -0.05
},
@ -8,33 +9,27 @@
"max_open_trades": 5
},
"buy": {
"ADD_POSITION_THRESHOLD": -0.045,
"BUY_THRESHOLD_MAX": -0.006,
"BUY_THRESHOLD_MIN": -0.1,
"COOLDOWN_PERIOD_MINUTES": 2,
"MAX_ENTRY_POSITION_ADJUSTMENT": 3
"ADD_POSITION_THRESHOLD": -0.01,
"BUY_THRESHOLD_MAX": -0.014,
"BUY_THRESHOLD_MIN": -0.019,
"COOLDOWN_PERIOD_MINUTES": 1,
"MAX_ENTRY_POSITION_ADJUSTMENT": 1
},
"sell": {
"EXIT_POSITION_RATIO": 0.305,
"SELL_THRESHOLD_MAX": 0.053,
"SELL_THRESHOLD_MIN": 0.004,
"TRAILING_STOP_DISTANCE": 0.008,
"TRAILING_STOP_START": 0.039
"EXIT_POSITION_RATIO": 0.412,
"SELL_THRESHOLD_MAX": 0.082,
"SELL_THRESHOLD_MIN": 0.008,
"TRAILING_STOP_DISTANCE": 0.005,
"TRAILING_STOP_START": 0.02
},
"protection": {},
"roi": {
"0": 0.044,
"7": 0.034999999999999996,
"14": 0.009,
"68": 0
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.292,
"trailing_stop_positive_offset": 0.361,
"trailing_only_offset_is_reached": false
"trailing_stop_positive": 0.253,
"trailing_stop_positive_offset": 0.313,
"trailing_only_offset_is_reached": true
}
},
"ft_stratparam_v": 1,
"export_time": "2025-06-25 02:38:38.192887+00:00"
}
"export_time": "2025-06-27 07:47:09.914912+00:00"
}

View File

@ -19,10 +19,6 @@ class FreqaiPrimer(IStrategy):
"""
# --- 🧪 Hyperopt Parameters ---
ROI_T0 = DecimalParameter(0.01, 0.05, default=0.02, space='roi', optimize=True)
ROI_T1 = DecimalParameter(0.005, 0.02, default=0.01, space='roi', optimize=True)
ROI_T2 = DecimalParameter(0.0, 0.01, default=0.0, space='roi', optimize=True)
TRAILING_STOP_START = DecimalParameter(0.01, 0.05, default=0.03, space='sell', optimize=True)
TRAILING_STOP_DISTANCE = DecimalParameter(0.005, 0.02, default=0.01, space='sell', optimize=True)
@ -38,12 +34,6 @@ class FreqaiPrimer(IStrategy):
MAX_ENTRY_POSITION_ADJUSTMENT = IntParameter(1, 3, default=2, space='buy', optimize=True)
# --- 🛠️ 固定配置参数 ---
minimal_roi = {
"0": ROI_T0.value,
"30": ROI_T1.value,
"60": ROI_T2.value
}
stoploss = -0.015
timeframe = "3m"
use_custom_stoploss = True
@ -158,7 +148,6 @@ class FreqaiPrimer(IStrategy):
return dataframe
# 在 populate_indicators 中修改市场趋势相关逻辑
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata.get('pair', 'Unknown')
logger.info(f"[{pair}] 当前可用列调用FreqAI前{list(dataframe.columns)}")
@ -266,21 +255,6 @@ class FreqaiPrimer(IStrategy):
return dataframe
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
roi_table = {
"0": params.get("roi_t0", self.ROI_T0.value),
"30": params.get("roi_t1", self.ROI_T1.value),
"60": params.get("roi_t2", self.ROI_T2.value),
}
return roi_table
def roi_space(self):
return [
DecimalParameter(0.01, 0.05, name="roi_t0"),
DecimalParameter(0.005, 0.02, name="roi_t1"),
DecimalParameter(0.0, 0.01, name="roi_t2")
]
def trailing_space(self):
return [
DecimalParameter(0.01, 0.05, name="trailing_stop_start"),
@ -399,17 +373,6 @@ class FreqaiPrimer(IStrategy):
logger.debug(f"{pair} 首次入场金额: {initial_stake_amount:.2f}, 当前持仓金额: {trade.stake_amount:.2f}, "
f"加仓次数: {trade.nr_of_successful_entries - 1}")
# 动态最大持仓时间
# max_hold_time = self.MAX_HOLD_TIME_MINUTES.value
# # 线性映射持仓时间因子
# hold_factor = self.HOLD_TIME_REDUCTION.value + (self.HOLD_TIME_EXTENSION.value - self.HOLD_TIME_REDUCTION.value) * (trend_score / 100)
# max_hold_time *= hold_factor
# if profit_ratio > 0.02:
# max_hold_time *= 1.2
# elif profit_ratio < -0.02:
# max_hold_time *= 0.8
# max_hold_time = int(max_hold_time)
# 加仓逻辑
max_entry_adjustments = self.MAX_ENTRY_POSITION_ADJUSTMENT.value
if trade.nr_of_successful_entries <= max_entry_adjustments + 1:
@ -478,11 +441,6 @@ class FreqaiPrimer(IStrategy):
trade.adjust_min_max_rates(current_rate, trade.min_rate)
return None
# 最大持仓时间限制
# if hold_time > max_hold_time:
# logger.info(f"{pair} 趋势值 {trend_score:.2f},持仓时间超过 {max_hold_time} 分钟,强制清仓")
# return (-trade.stake_amount, f"Max hold time ({max_hold_time} min) exceeded")
return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,

View File

@ -65,7 +65,7 @@ echo "docker-compose run --rm freqtrade hyperopt \
--timerange ${START_DATE}-${END_DATE} \
-e 100 \
--hyperopt-loss ShortTradeDurHyperOptLoss \
--spaces buy sell roi trailing \
--spaces buy sell trailing \
--fee 0.0016"
docker-compose run --rm freqtrade hyperopt \
--logfile /freqtrade/user_data/logs/freqtrade.log \
@ -77,7 +77,7 @@ docker-compose run --rm freqtrade hyperopt \
--timerange ${START_DATE}-${END_DATE} \
-e 100 \
--hyperopt-loss SharpeHyperOptLoss \
--spaces buy sell roi trailing \
--spaces buy sell trailing \
--fee 0.0016
#>output.log 2>&1