From d8b21219aef56ff51082258ba6311dab502b9e39 Mon Sep 17 00:00:00 2001 From: "zhangkun9038@dingtalk.com" Date: Sun, 28 Sep 2025 13:08:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=BA=86=E6=B3=A2=E5=8A=A8?= =?UTF-8?q?=E7=B3=BB=E6=95=B0=E9=80=BB=E8=BE=91,=20=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=88=B0=E5=8A=A8=E6=80=81=E9=80=80=E5=87=BA,=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E5=8A=A0=E4=BB=93=E7=AD=89=E7=8E=AF=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- calculate_position_adjustments.sh | 126 +++++++++++++ freqtrade/templates/freqaiprimer.py | 278 ++++++++++++++++++++++++++-- 2 files changed, 392 insertions(+), 12 deletions(-) create mode 100755 calculate_position_adjustments.sh diff --git a/calculate_position_adjustments.sh b/calculate_position_adjustments.sh new file mode 100755 index 00000000..88f1a7b8 --- /dev/null +++ b/calculate_position_adjustments.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# 设置默认值 +MAX_ENTRY_ADJUSTMENTS=3 +ADD_POSITION_CALLBACK=0.047 +STAKE_DIVISOR=2.0 +ADD_POSITION_MULTIPLIER=1.6 +ADD_POSITION_GROWTH=2.0 +INITIAL_STAKE=1000 + +# 显示帮助信息 +show_help() { + echo "用法: $0 [选项...]" + echo "" + echo "计算FreqAI策略中的加仓相对降幅间隔和加仓金额" + echo "" + echo "选项:" + echo " -h, --help 显示此帮助信息并退出" + echo " -m, --max-entry-adjustments 最大加仓次数 (默认: 3)" + echo " -c, --callback 基础加仓回调百分比 (默认: 0.047)" + echo " -d, --divisor 加仓金额分母 (默认: 2.0)" + echo " -p, --multiplier 加仓间隔系数 (默认: 1.6)" + echo " -g, --growth 加仓金额增长因子 (默认: 2.0)" + echo " -s, --initial-stake 初始入场金额 (默认: 1000)" + echo "" + echo "示例:" + echo " $0 使用所有默认参数运行" + echo " $0 -m 3 -c 0.047 -d 2.0 -p 1.6 -g 2.0 -s 1000 使用指定参数运行" + echo " $0 --max-entry-adjustments 3 --callback 0.03 --multiplier 1 使用等距间隔策略" + echo "" + echo "计算公式:" + echo " 相对降幅间隔 = callback * (multiplier ^ (加仓次数-1))" + echo " 加仓金额 = (initial_stake / divisor) * (growth ^ 加仓次数)" + exit 0 +} + +# 处理命令行参数 +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + show_help + ;; + -m|--max-entry-adjustments) + MAX_ENTRY_ADJUSTMENTS="$2" + shift 2 + ;; + -c|--callback) + ADD_POSITION_CALLBACK="$2" + shift 2 + ;; + -d|--divisor) + STAKE_DIVISOR="$2" + shift 2 + ;; + -p|--multiplier) + ADD_POSITION_MULTIPLIER="$2" + shift 2 + ;; + -g|--growth) + ADD_POSITION_GROWTH="$2" + shift 2 + ;; + -s|--initial-stake) + INITIAL_STAKE="$2" + shift 2 + ;; + -*|--*) + echo "错误: 未知选项 $1" + echo "使用 -h 或 --help 查看帮助信息" + exit 1 + ;; + *) + # 忽略任何非选项参数 + shift + ;; + esac +done + +# 打印当前设置 +printf "当前参数设置:\n" +printf "最大加仓次数: %d\n" "$MAX_ENTRY_ADJUSTMENTS" +printf "基础回调百分比: %.6f\n" "$ADD_POSITION_CALLBACK" +printf "加仓金额分母: %.2f\n" "$STAKE_DIVISOR" +printf "加仓间隔系数: %.2f\n" "$ADD_POSITION_MULTIPLIER" +printf "加仓金额增长因子: %.2f\n" "$ADD_POSITION_GROWTH" +printf "初始入场金额: %.2f\n\n" "$INITIAL_STAKE" + +# 打印表头 +printf "%-10s %-20s %-20s\n" "加仓次数" "相对降幅间隔" "加仓金额" +printf "%-10s %-20s %-20s\n" "-------" "------------" "--------" + +# 计算加仓(包括初始入场) +for i in $(seq 0 $MAX_ENTRY_ADJUSTMENTS); do + # 计算当前所需的加仓间隔百分比 + if [ $i -eq 0 ]; then + # 第一次是初始入场,没有回调要求 + callback="N/A" + else + # 计算当前回调百分比 = 基础间隔 * (系数 ^ (i-1)) + callback=$(echo "$ADD_POSITION_CALLBACK * ($ADD_POSITION_MULTIPLIER ^ ($i-1))" | bc -l) + callback=$(printf "%.6f" $callback) + fi + + # 计算加仓金额 + if [ $i -eq 0 ]; then + # 第一次是初始入场 + stake_amount=$INITIAL_STAKE + else + # 计算加仓金额: (initial_stake / stake_divisor) * (growth ^ i) + stake_amount=$(echo "($INITIAL_STAKE / $STAKE_DIVISOR) * ($ADD_POSITION_GROWTH ^ $i)" | bc -l) + stake_amount=$(printf "%.2f" $stake_amount) + fi + + # 打印结果 + printf "%-10d %-20s %-20s\n" "$i" "$callback" "$stake_amount" +done + +# 计算累计投入 +cumulative_stake=$(echo "$INITIAL_STAKE" | bc -l) +for i in $(seq 1 $MAX_ENTRY_ADJUSTMENTS); do + stake_amount=$(echo "($INITIAL_STAKE / $STAKE_DIVISOR) * ($ADD_POSITION_GROWTH ^ $i)" | bc -l) + cumulative_stake=$(echo "$cumulative_stake + $stake_amount" | bc -l) +done +cumulative_stake=$(printf "%.2f" $cumulative_stake) + +printf "\n%s\n" "累计投入金额: $cumulative_stake" \ No newline at end of file diff --git a/freqtrade/templates/freqaiprimer.py b/freqtrade/templates/freqaiprimer.py index ba0c6ecf..8b633e88 100644 --- a/freqtrade/templates/freqaiprimer.py +++ b/freqtrade/templates/freqaiprimer.py @@ -27,11 +27,165 @@ class FreqaiPrimer(IStrategy): # 用于跟踪市场状态的数据框缓存 _dataframe_cache = None + # 用于存储币对的波动系数 + _volatility_coefficients = {} + # 基准币对 (波动系数设为1.0) + _benchmark_pair = 'BTC/USDT' + # 稳定币列表 (波动系数设为0.0) + _stablecoins = ['USDT', 'USDC', 'BUSD', 'DAI', 'TUSD', 'USDP', 'GUSD', 'USTC'] + # 波动系数缓存有效期 (分钟) + _volatility_cache_ttl = 60 + # 上次计算波动系数的时间 + _last_volatility_calculation = 0 + def __init__(self, config=None): """初始化策略参数,调用父类初始化方法并接受config参数""" super().__init__(config) # 调用父类的初始化方法并传递config # 存储从配置文件加载的默认值 self._trailing_stop_positive_default = 0.004 # 降低默认值以更容易触发跟踪止盈 + # 初始化基准币对和稳定币的波动系数 + self._volatility_coefficients[self._benchmark_pair] = 1.0 + for stablecoin in self._stablecoins: + # 处理所有稳定币交易对,如USDT/USDC等 + for quote in self._stablecoins: + if stablecoin != quote: + pair = f'{stablecoin}/{quote}' + self._volatility_coefficients[pair] = 0.0 + + def _is_stablecoin_pair(self, pair: str) -> bool: + """ + 判断一个交易对是否为稳定币交易对 + 参数: + - pair: 交易对,如 BTC/USDT + 返回: + - bool: 是否为稳定币交易对 + """ + try: + base, quote = pair.split('/') + return base in self._stablecoins and quote in self._stablecoins + except ValueError: + return False + + def _calculate_volatility(self, pair: str, lookback_period: int = 200, timeframe: str = '1h') -> float: + """ + 计算一个币对的波动率 + 参数: + - pair: 交易对,如 BTC/USDT + - lookback_period: 回看期K线数量,默认200 + - timeframe: 时间框架,默认1h + 返回: + - float: 波动率值 + """ + try: + # 获取K线数据 + dataframe = self.dp.get_pair_dataframe(pair=pair, timeframe=timeframe) + + # 确保有足够的K线数据 + if len(dataframe) < lookback_period: + logger.warning(f"[{pair}] 没有足够的{timeframe} K线数据,需要{lookback_period}根,当前只有{len(dataframe)}根") + # 如果数据不足,返回0.0或默认值 + return 0.0 + + # 获取最近的K线数据 + recent_data = dataframe.iloc[-lookback_period:].copy() + + # 计算收益率 (收盘价变化百分比) + recent_data['returns'] = recent_data['close'].pct_change() + + # 计算对数收益率 (更适合波动率计算) + recent_data['log_returns'] = np.log(recent_data['close'] / recent_data['close'].shift(1)) + + # 使用对数收益率的标准差作为波动率指标 + volatility = recent_data['log_returns'].std() * np.sqrt(24) # 年化波动率 (假设1天24小时) + + return volatility + + except Exception as e: + logger.error(f"[{pair}] 计算波动率时出错: {str(e)}") + return 0.0 + + def _calculate_relative_volatility_coefficient(self, pair: str) -> float: + """ + 计算相对波动系数(以BTC/USDT为基准) + 参数: + - pair: 交易对,如 ETH/USDT + 返回: + - float: 相对波动系数 + """ + try: + # 检查是否为稳定币交易对 + if self._is_stablecoin_pair(pair): + return 0.0 + + # 检查是否为基准币对 + if pair == self._benchmark_pair: + return 1.0 + + # 计算当前币对的波动率 + pair_volatility = self._calculate_volatility(pair) + + # 计算基准币对的波动率 + benchmark_volatility = self._calculate_volatility(self._benchmark_pair) + + # 避免除以0的情况 + if benchmark_volatility == 0: + logger.warning(f"基准币对 {self._benchmark_pair} 的波动率为0,无法计算相对波动系数") + return 0.0 + + # 计算相对波动系数 + relative_coefficient = pair_volatility / benchmark_volatility + + return relative_coefficient + + except Exception as e: + logger.error(f"[{pair}] 计算相对波动系数时出错: {str(e)}") + return 0.0 + + def get_volatility_coefficient(self, pair: str) -> float: + """ + 获取币对的波动系数(带缓存机制) + 参数: + - pair: 交易对,如 ETH/USDT + 返回: + - float: 波动系数(稳定币为0.0,BTC/USDT为1.0,其他币对相对于BTC波动程度) + """ + try: + # 优先从缓存中获取,无需每次都检查时间戳 + # 这确保了在同一交易周期内多次调用时直接返回缓存值 + if pair in self._volatility_coefficients: + # 检查是否为基准币对或稳定币对(它们的波动系数固定) + if pair == self._benchmark_pair or self._is_stablecoin_pair(pair): + return self._volatility_coefficients[pair] + + # 获取当前时间戳(分钟) + current_time = int(datetime.datetime.now().timestamp() / 60) + + # 对于非固定波动系数的币对,检查缓存是否有效 + if current_time - self._last_volatility_calculation < self._volatility_cache_ttl: + return self._volatility_coefficients[pair] + + # 计算新的波动系数 + coefficient = self._calculate_relative_volatility_coefficient(pair) + + # 更新缓存 + self._volatility_coefficients[pair] = coefficient + + # 如果是首次计算或者缓存已过期,更新最后计算时间 + current_time = int(datetime.datetime.now().timestamp() / 60) + if current_time - self._last_volatility_calculation >= self._volatility_cache_ttl: + self._last_volatility_calculation = current_time + # 日志记录 + logger.info(f"更新了波动系数缓存,当前时间: {current_time}, 上一次计算时间: {self._last_volatility_calculation}") + + # 日志记录 - 仅在计算新值时记录 + logger.info(f"[{pair}] 波动系数: {coefficient:.4f}") + + return coefficient + + except Exception as e: + logger.error(f"[{pair}] 获取波动系数时出错: {str(e)}") + # 如果出错,返回默认值1.0(假设与BTC波动相当) + return 1.0 @property def protections(self): @@ -104,7 +258,7 @@ class FreqaiPrimer(IStrategy): # 定义可优化参数 # -m 4 -c 0.045 -g 4.5 -p 1.22 -d 9.3 -s 75 max_entry_adjustments = IntParameter(2, 5, default=3, optimize=True, load=True, space='buy') # 最大加仓次数 - add_position_callback = DecimalParameter(0.02, 0.06, decimals=3, default=0.045, optimize=False, load=True, space='buy') # 加仓回调百分比 + add_position_callback = DecimalParameter(0.02, 0.06, decimals=3, default=0.045, optimize=True, load=True, space='buy') # 加仓回调百分比 add_position_growth = DecimalParameter(1.5, 3.0, decimals=2, default=4.5, optimize=False, load=True, space='buy') # 加仓金额增长因子,保留2位小数用于hyperopt优化 add_position_multiplier = DecimalParameter(0.2, 10.5, decimals=2, default=1.22, optimize=False, load=True, space='buy') # 加仓间隔系数,保留2位小数用于hyperopt优化 stake_divisor = DecimalParameter(2.0, 12.0, decimals=2, default=9.3, optimize=False, load=True, space='buy') # 加仓金额分母(小数类型,保留2位小数) @@ -520,35 +674,44 @@ class FreqaiPrimer(IStrategy): last_candle = dataframe.iloc[-1] atr = last_candle['atr'] + # 获取当前币对的波动系数 + volatility_coef = self.get_volatility_coefficient(pair) + # 获取当前市场状态 current_state = dataframe['market_state'].iloc[-1] if 'market_state' in dataframe.columns else 'unknown' - # 更激进的渐进式止损策略 + # 基础止损倍数 + base_multiplier = 1.2 + + # 根据波动系数调整止损倍数 + adjusted_multiplier = base_multiplier * volatility_coef + + # 更激进的渐进式止损策略,同时考虑波动系数 if current_profit > 0.05: # 利润超过5%时 - return -3.0 * atr / current_rate # 更大幅扩大止损范围,让利润奔跑 + return -3.0 * adjusted_multiplier * atr / current_rate elif current_profit > 0.03: # 利润超过3%时 - return -2.5 * atr / current_rate # 更中等扩大止损范围 + return -2.5 * adjusted_multiplier * atr / current_rate elif current_profit > 0.01: # 利润超过1%时 - return -2.0 * atr / current_rate # 更轻微扩大止损范围 + return -2.0 * adjusted_multiplier * atr / current_rate # 在强劲牛市中,即使小亏损也可以容忍更大回调 if current_state == 'strong_bull' and current_profit > -0.01: - return -1.5 * atr / current_rate + return -1.5 * adjusted_multiplier * atr / current_rate # 动态调整止损范围 if current_profit > 0.05: # 利润超过5%时 - return -3.0 * atr / current_rate # 更大幅扩大止损范围,让利润奔跑 + return -3.0 * adjusted_multiplier * atr / current_rate elif current_profit > 0.03: # 利润超过3%时 - return -2.5 * atr / current_rate # 更中等扩大止损范围 + return -2.5 * adjusted_multiplier * atr / current_rate elif current_profit > 0.01: # 利润超过1%时 - return -2.0 * atr / current_rate # 更轻微扩大止损范围 + return -2.0 * adjusted_multiplier * atr / current_rate # 在强劲牛市中,即使小亏损也可以容忍更大回调 if current_state == 'strong_bull' and current_profit > -0.01: - return -1.8 * atr / current_rate + return -1.8 * adjusted_multiplier * atr / current_rate if atr > 0: - return -1.2 * atr / current_rate # 基础1.2倍ATR止损 + return -adjusted_multiplier * atr / current_rate return self.stoploss def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, @@ -560,6 +723,9 @@ class FreqaiPrimer(IStrategy): if trade_age_minutes < 0: trade_age_minutes = 0 + # 获取当前币对的波动系数 + volatility_coef = self.get_volatility_coefficient(pair) + # 使用可优化的线性函数: y = (a * (x + k)) + t a = self.roi_param_a.value # 系数a (可优化参数) k = self.roi_param_k.value # 偏移量k (可优化参数) @@ -588,6 +754,11 @@ class FreqaiPrimer(IStrategy): exit_ratio = 1.0 if entry_tag == 'strong_trend': exit_ratio *= 0.8 + + # 根据波动系数调整退出比例 + # 波动率低的币对可以更激进(降低退出比例,持有更长时间) + # 波动率高的币对需要更保守(增加退出比例,更快锁定利润) + exit_ratio = max(0.0, min(1.0, exit_ratio * volatility_coef)) if dynamic_roi_threshold < 0: exit_ratio = 1.0 @@ -630,7 +801,10 @@ class FreqaiPrimer(IStrategy): current_state = dataframe['market_state'].iloc[-1] if 'market_state' in dataframe.columns else 'neutral' # 计算当前所需的加仓间隔百分比 = 基础间隔 * (系数 ^ 已加仓次数) - current_callback = self.add_position_callback.value * (self.add_position_multiplier.value ** adjustment_count) + # 获取当前币对的波动系数,用于动态调整回调百分比 + volatility_coef = self.get_volatility_coefficient(pair) + # 回调百分比 = 基础回调 * (系数 ^ 已加仓次数) * 波动系数 + current_callback = self.add_position_callback.value * (self.add_position_multiplier.value ** adjustment_count) * volatility_coef if price_diff_pct <= -current_callback: # 计算初始入场金额 @@ -649,3 +823,83 @@ class FreqaiPrimer(IStrategy): # 不符合加仓条件,返回0 return 0.0 + + def custom_stake_amount(self, pair: str, current_time: datetime, **kwargs) -> float: + """ + 根据波动系数动态调整初始仓位大小 + - 波动率高的币对分配较小的仓位 + - 波动率低的币对可以分配较大的仓位 + """ + # 获取当前币对的波动系数 + volatility_coef = self.get_volatility_coefficient(pair) + + # 获取默认的基础仓位大小 + default_stake = self.stake_amount + + # 根据波动系数调整仓位大小 + # 波动率与仓位大小成反比关系 + # 基础逻辑:波动率高的币对仓位较小,波动率低的币对仓位较大 + if volatility_coef > 0: + # 使用平方根函数来降低极端波动的影响 + adjusted_stake = default_stake * min(1.5, max(0.5, 1.0 / (volatility_coef ** 0.5))) + else: + # 如果波动系数无法获取,使用默认仓位 + adjusted_stake = default_stake + + # 确保调整后的仓位在允许的范围内 + adjusted_stake = max(self.min_stake_amount, min(adjusted_stake, self.stake_amount)) + + #logger.info(f"[{pair}] 基于波动系数调整仓位: 波动系数={volatility_coef:.2f}, 默认仓位={default_stake:.2f}, 调整后仓位={adjusted_stake:.2f}") + + return adjusted_stake + + def confirm_trade_entry( + self, + pair: str, + order_type: str, + amount: float, + rate: float, + time_in_force: str, + current_time: datetime, + entry_tag: str | None, + side: str, + **kwargs, + ) -> bool: + """ + 交易买入前的确认函数,用于最终决定是否执行交易 + 此处实现剧烈拉升检查逻辑,并根据波动系数调整入场条件 + """ + # 默认允许交易 + allow_trade = True + + # 仅对多头交易进行检查 + if side == 'long': + # 获取当前币对的波动系数 + volatility_coef = self.get_volatility_coefficient(pair) + + # 检查是否处于剧烈拉升的不稳固区域 + is_unstable_region = self.detect_h1_rapid_rise(pair) + + # 基于波动系数调整入场条件的严格程度 + # 波动率高的币对,入场条件更加严格 + # 波动率低的币对,入场条件可以适当放宽 + if is_unstable_region: + #logger.info(f"[{pair}] 由于检测到剧烈拉升,取消入场交易") + allow_trade = False + + # 如果币对波动率非常高,可以增加额外的入场限制 + if volatility_coef > 2.0: + # 对于高波动币对,可以要求更严格的入场条件 + # 例如,检查当前价格是否高于某个移动平均线 + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if len(dataframe) > 20: + current_price = dataframe['close'].iloc[-1] + sma20 = dataframe['sma'].iloc[-1] if 'sma' in dataframe.columns else 0 + + # 对于高波动币对,要求当前价格必须显著高于均线才允许入场 + if sma20 > 0 and (current_price / sma20) < 1.01: + #logger.info(f"[{pair}] 由于高波动率且价格未显著高于均线,取消入场交易") + allow_trade = False + + # 如果没有阻止因素,允许交易 + return allow_trade