From 23ed71649d82601ff4d6ec9d9692dc88306d3745 Mon Sep 17 00:00:00 2001 From: Beast Trader Date: Wed, 10 Jun 2026 23:12:00 +0800 Subject: [PATCH] =?UTF-8?q?v4.1=20(Swing):=20=E5=85=A5=E5=9C=BA=E6=97=B6?= =?UTF-8?q?=E6=9C=BA=E4=BC=98=E5=8C=96=20+=20=E6=AD=A2=E6=8D=9F=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- strategy.py | 214 ++++++++++++++++++++++++++-------------------------- 1 file changed, 106 insertions(+), 108 deletions(-) diff --git a/strategy.py b/strategy.py index 0eda35a..001e1a8 100644 --- a/strategy.py +++ b/strategy.py @@ -1,22 +1,10 @@ """ -Structure Flow Swing Strategy v4.0 +Structure Flow Swing Strategy v4.1 ================================== -15m 震荡区间波段策略 — 基于价格聚集度检测 +15m 震荡波段 — 基于"碰壁验证"价格聚集度检测(短窗口版) -核心变革(相对于 v3.x): - 1. 时间框架从 4H → 15m:直接在小周期检测和执行 - 2. 震荡判定从 "swing points 宽度稳定性" → "价格聚集度 + 边界测试次数" - 3. 检测周期 4-8 小时即可识别震荡,覆盖 1-3 天的 mini-震荡 - -v3.1 诊断回顾(2026-06-10 全周期回测): - - 122笔全部做空,+76%,CAGR 10.97% - - is_ranging 仅 13.7%,用 4H 判定只抓到大周期震荡 - - 1-3 天的小震荡完全被漏掉,这才是手工交易的利润来源 - -版本历史: - v3.0 (2026-06-10): 初版,4H swing points + 双边测试 - v3.1 (2026-06-10): AND→OR,降门槛 - v4.0 (2026-06-10): 全面重写,15m 价格聚集度检测 +v4.1b (2026-06-10): 1H + 3K线验证(正收益但频率太低) +v4.1c (2026-06-10): 回到15m + 缩短lookback至24根(6h) + 保留min_rejections=2 """ from datetime import datetime @@ -27,10 +15,10 @@ from freqtrade.strategy import IStrategy, IntParameter from freqtrade.persistence import Trade -class StructureFlowSwingV40(IStrategy): +class StructureFlowSwingV41(IStrategy): """ - Structure Flow Swing Strategy v4.0 - 15m 震荡区间波段交易 — 价格聚集度检测 + Structure Flow Swing Strategy v4.1 + 15m 震荡区间波段交易 — 碰壁验证(短窗口) """ can_short = True @@ -43,16 +31,16 @@ class StructureFlowSwingV40(IStrategy): # ===================== # 可优化参数 # ===================== - lookback = IntParameter(24, 96, default=48, space="buy") # 检测窗口:24~96根15m(6h~24h) - min_touches = IntParameter(1, 4, default=2, space="buy") # 边界至少测试次数 - zone_width_atr_mult = IntParameter(2, 6, default=4, space="buy") # 区间宽度上限 = ATR × N - entry_zone_pct = IntParameter(2, 8, default=5, space="buy") # 入场范围:距边界千分比(0.5%) - atr_stop_mult = IntParameter(10, 25, default=15, space="buy") # ATR止损倍数 - take_profit_pct = IntParameter(50, 80, default=70, space="sell") # 区间高度止盈比例 + lookback = IntParameter(8, 48, default=12, space="buy") # 检测窗口:12根15m = 3h + min_rejections = IntParameter(1, 4, default=1, space="buy") # 碰壁验证1次即可 + rejection_window = IntParameter(1, 6, default=3, space="buy") # 碰壁验证窗口 + zone_width_atr_mult = IntParameter(2, 6, default=4, space="buy") + entry_zone_pct = IntParameter(2, 8, default=5, space="buy") + take_profit_pct = IntParameter(50, 80, default=70, space="sell") # 固定参数 - breakout_bars = 2 # 连续几根K线突破才算真突破 - cooldown = 4 # 入场后冷却 4 根15m(1小时) + breakout_bars = 2 + cooldown = 2 # 冷却 2 根15m(30分钟) # ===================== # 工具:ATR计算 @@ -73,43 +61,52 @@ class StructureFlowSwingV40(IStrategy): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: lookback = self.lookback.value + rej_threshold = 0.005 # 边界 0.5% 范围内算"碰边" - # ── 价格聚集度检测 ── + # ── 价格聚集范围 ── rolling_high = dataframe["high"].rolling(lookback).max() rolling_low = dataframe["low"].rolling(lookback).min() - - # 区间宽度(绝对值和百分比) - zone_width = rolling_high - rolling_low - zone_width_pct = zone_width / rolling_low + zone_width_raw = rolling_high - rolling_low dataframe["zone_high"] = rolling_high dataframe["zone_low"] = rolling_low - dataframe["zone_width_raw"] = zone_width - dataframe["zone_width_pct"] = zone_width_pct + dataframe["zone_width_raw"] = zone_width_raw + dataframe["zone_width_pct"] = zone_width_raw / rolling_low # ATR dataframe["atr"] = self._calc_atr(dataframe["high"], dataframe["low"], dataframe["close"], 14) - # ── 边界测试计数 ── - # 价格在区间上边界 0.5% 范围内 → 算一次测试 - touch_upper = dataframe["high"] >= rolling_high * 0.995 - touch_lower = dataframe["low"] <= rolling_low * 1.005 + # ── 碰壁验证检测 ── + # 支撑验证:价格到了低点附近 → 随后 rejection_window 根K线内有反弹 + near_low = dataframe["low"] <= rolling_low * (1 + rej_threshold) + bounced_up = pd.Series(False, index=dataframe.index) + for i in range(1, self.rejection_window.value + 1): + future_close = dataframe["close"].shift(-i).fillna(dataframe["close"]) + bounced_up = bounced_up | (future_close > dataframe["close"]) + support_rejection = near_low & bounced_up + dataframe["support_rejection_count"] = support_rejection.rolling(lookback).sum() - # 滚动窗口内测试次数 - dataframe["upper_touches"] = touch_upper.rolling(lookback).sum() - dataframe["lower_touches"] = touch_lower.rolling(lookback).sum() + # 阻力验证:价格到了高点附近 → 随后 rejection_window 根K线内有回落 + near_high = dataframe["high"] >= rolling_high * (1 - rej_threshold) + bounced_down = pd.Series(False, index=dataframe.index) + for i in range(1, self.rejection_window.value + 1): + future_close = dataframe["close"].shift(-i).fillna(dataframe["close"]) + bounced_down = bounced_down | (future_close < dataframe["close"]) + resistance_rejection = near_high & bounced_down + dataframe["resistance_rejection_count"] = resistance_rejection.rolling(lookback).sum() - # ── 震荡判定条件 ── + # ── 震荡判定 ── atr_mult = self.zone_width_atr_mult.value - min_touches = self.min_touches.value + min_rej = self.min_rejections.value # 条件1:区间宽度合理(不超过 ATR × N) - is_compact = zone_width <= dataframe["atr"] * atr_mult + is_compact = zone_width_raw <= dataframe["atr"] * atr_mult - # 条件2:上下边界都被测试过至少 min_touches 次 - is_tested = (dataframe["upper_touches"] >= min_touches) & (dataframe["lower_touches"] >= min_touches) + # 条件2:上下边界都经过碰壁验证(各至少 min_rej 次) + support_ok = dataframe["support_rejection_count"] >= min_rej + resistance_ok = dataframe["resistance_rejection_count"] >= min_rej - # 条件3:无突破(最近 breakout_bars 根收盘价在边界内) + # 条件3:无突破 no_break_high = True no_break_low = True for i in range(1, self.breakout_bars + 1): @@ -117,8 +114,7 @@ class StructureFlowSwingV40(IStrategy): no_break_high = no_break_high & (dataframe["close"].shift(i) <= rolling_high) no_break_low = no_break_low & (dataframe["close"].shift(i) >= rolling_low) - is_ranging = is_compact & is_tested & no_break_high & no_break_low - + is_ranging = is_compact & support_ok & resistance_ok & no_break_high & no_break_low dataframe["is_ranging"] = is_ranging # ── 价格在区间内的位置 ── @@ -129,7 +125,6 @@ class StructureFlowSwingV40(IStrategy): np.nan, ) - # 距边界百分比 dataframe["dist_to_low"] = np.where( rolling_low > 0, (dataframe["close"] - rolling_low) / dataframe["close"], @@ -153,7 +148,7 @@ class StructureFlowSwingV40(IStrategy): # ================================================================ def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - entry_zone = self.entry_zone_pct.value / 1000.0 # 千分比 + entry_zone = self.entry_zone_pct.value / 1000.0 if "is_ranging" not in dataframe.columns: dataframe["is_ranging"] = False @@ -164,8 +159,6 @@ class StructureFlowSwingV40(IStrategy): & (dataframe["dist_to_low"] < entry_zone) & (dataframe["dist_to_low"] > 0) ) - - # 冷却 long_recent = long_conds.rolling(self.cooldown, min_periods=1).max().shift(1) == 0 dataframe.loc[long_conds & long_recent, "enter_long"] = 1 @@ -175,7 +168,6 @@ class StructureFlowSwingV40(IStrategy): & (dataframe["dist_to_high"] < entry_zone) & (dataframe["dist_to_high"] > 0) ) - short_recent = short_conds.rolling(self.cooldown, min_periods=1).max().shift(1) == 0 dataframe.loc[short_conds & short_recent, "enter_short"] = 1 @@ -189,7 +181,7 @@ class StructureFlowSwingV40(IStrategy): return dataframe # ================================================================ - # 自定义止损:区间边界外侧 + ATR 缓冲 + # 自定义止损:固定入场价下方-3%(真固定,不追踪) # ================================================================ def custom_stoploss( @@ -202,41 +194,12 @@ class StructureFlowSwingV40(IStrategy): after_fill: bool, **kwargs, ) -> float: - dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) - if dataframe is None or len(dataframe) == 0: - return -0.02 if not trade.is_short else 0.02 - - last = dataframe.iloc[-1] - atr_mult = self.atr_stop_mult.value / 10.0 - + # 固定止损 = 入场价下方3%,不随当前价格移动 + # 不用current_rate,用trade.open_rate确保锚点固定 if not trade.is_short: - zone_low = last.get("zone_low", np.nan) - atr = last.get("atr", np.nan) - - if pd.isna(zone_low) or zone_low <= 0: - return -0.02 - - if pd.notna(atr) and atr > 0: - sl_price = zone_low - atr * atr_mult - else: - sl_price = zone_low * 0.985 - - sl_ratio = (sl_price / current_rate) - 1.0 - return max(sl_ratio, -0.20) + return max((trade.open_rate * 0.98 / current_rate) - 1.0, -0.20) else: - zone_high = last.get("zone_high", np.nan) - atr = last.get("atr", np.nan) - - if pd.isna(zone_high) or zone_high <= 0: - return 0.02 - - if pd.notna(atr) and atr > 0: - sl_price = zone_high + atr * atr_mult - else: - sl_price = zone_high * 1.015 - - sl_ratio = 1.0 - (sl_price / current_rate) - return min(sl_ratio, 0.20) + return min(1.0 - (trade.open_rate * 1.02 / current_rate), 0.20) # ================================================================ # 自定义止盈:区间高度 × TP% @@ -258,25 +221,61 @@ class StructureFlowSwingV40(IStrategy): return None last = dataframe.iloc[-1] + z_low = last.get("zone_low", np.nan) + z_high = last.get("zone_high", np.nan) + if pd.notna(z_low) and pd.notna(z_high) and z_high > z_low: + base = z_low if not trade.is_short else z_high + zone_height = (z_high - z_low) / base + if current_profit >= zone_height * tp_pct: + return "take_profit" + + return None + + # ================================================================ + # 自定义退出:锁定入场边界 + 收盘确认止损 + 区间高度止盈 + # ================================================================ + + def custom_exit( + self, + pair: str, + trade: Trade, + current_time: datetime, + current_rate: float, + current_profit: float, + **kwargs, + ) -> str | None: + tp_pct = self.take_profit_pct.value / 100.0 + + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if dataframe is None or len(dataframe) == 0: + return None + + last = dataframe.iloc[-1] + + # ── 锁定入场时的区间边界 ── + open_time = trade.open_date_utc.replace(tzinfo=None) + time_diff = (dataframe["date"] - open_time).abs() + open_idx = time_diff.idxmin() + open_row = dataframe.iloc[open_idx] + + z_low_open = open_row.get("zone_low", np.nan) + z_high_open = open_row.get("zone_high", np.nan) + + # ── 结构止损:收盘价确认跌破入场时的边界 ── if not trade.is_short: - zone_low = last.get("zone_low", np.nan) - zone_high = last.get("zone_high", np.nan) - - if pd.notna(zone_low) and pd.notna(zone_high) and zone_high > zone_low: - zone_height = (zone_high - zone_low) / zone_low - tp_target = zone_height * tp_pct - if current_profit >= tp_target: - return "take_profit" + if pd.notna(z_low_open) and z_low_open > 0 and last["close"] < z_low_open: + return "stop_loss" # 收盘跌破支撑 else: - zone_low = last.get("zone_low", np.nan) - zone_high = last.get("zone_high", np.nan) + if pd.notna(z_high_open) and z_high_open > 0 and last["close"] > z_high_open: + return "stop_loss" # 收盘涨破阻力 - if pd.notna(zone_low) and pd.notna(zone_high) and zone_high > zone_low: - zone_height = (zone_high - zone_low) / zone_high - tp_target = zone_height * tp_pct - if current_profit >= tp_target: - return "take_profit" + # ── 止盈:区间高度 × TP%(也用锁定边界) ── + if pd.notna(z_low_open) and pd.notna(z_high_open) and z_high_open > z_low_open: + base = z_low_open if not trade.is_short else z_high_open + zone_height = (z_high_open - z_low_open) / base + if current_profit >= zone_height * tp_pct: + return "take_profit" return None @@ -292,13 +291,12 @@ class StructureFlowSwingV40(IStrategy): "zone_low": {"color": "green", "type": "line"}, }, "subplots": { + "rejections": { + "support_rejection_count": {"color": "green", "type": "line"}, + "resistance_rejection_count": {"color": "red", "type": "line"}, + }, "zone": { "is_ranging": {"color": "blue", "type": "line"}, - "zone_width_pct": {"color": "purple", "type": "line"}, - }, - "touches": { - "upper_touches": {"color": "red", "type": "line"}, - "lower_touches": {"color": "green", "type": "line"}, }, }, }