v4.0 (Swing): 精简架构 - 单一框架震荡识别 + 快速入场

This commit is contained in:
2026-06-10 22:24:00 +08:00
parent 3da0b17725
commit 13616c1cd2

View File

@ -1,42 +1,36 @@
""" """
Structure Flow Swing Strategy v3.2 Structure Flow Swing Strategy v4.0
================================== ==================================
波段交易策略 — 基于4H震荡区间v3.1优化版 15m 震荡区间波段策略 — 基于价格聚集度检测
v3.2 改动基于v3.1诊断结果 — 三大市场感知不足 核心变革(相对于 v3.x
1. D1趋势强度过滤D1处于强趋势时拒绝入场防假区间陷阱 1. 时间框架从 4H → 15m直接在小周期检测和执行
- 计算 D1 EMA20/EMA50 间距作为趋势强度指标 2. 震荡判定从 "swing points 宽度稳定性""价格聚集度 + 边界测试次数"
- 趋势强度超过阈值 → 不交易即使4H出现区间形态 3. 检测周期 4-8 小时即可识别震荡,覆盖 1-3 天的 mini-震荡
2. 区间质量评分:从二分法升级为多维度评分
- 边界测试次数(测试越多越可靠)
- 区间持续时长(越长越成熟)
- 区间宽度适配度3-8%最优)
- 总分>=阈值才入场
3. 主动退出机制:确认转趋势后提前离场
- 3根连续K线收盘在入场时区间外 → 结构破坏
- 不等止损,主动离场(仅在损失<2%时)
- 避免浮盈变亏损
保留纯震荡定位、ATR×1.5止损、区间70%止盈、OR双边测试、冷却期1根 v3.1 诊断回顾2026-06-10 全周期回测):
- 122笔全部做空+76%CAGR 10.97%
- is_ranging 仅 13.7%,用 4H 判定只抓到大周期震荡
- 1-3 天的小震荡完全被漏掉,这才是手工交易的利润来源
版本历史: 版本历史:
v3.0 (2026-06-10): 初版,基于冯总波段交易新思路 v3.0 (2026-06-10): 初版,4H swing points + 双边测试
v3.1 (2026-06-10): 降低条件门槛,AND→OR等4项 v3.1 (2026-06-10): AND→OR,降门槛
v3.2 (2026-06-10): 三大市场感知改进 v4.0 (2026-06-10): 全面重写15m 价格聚集度检测
""" """
from datetime import datetime from datetime import datetime
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from pandas import DataFrame from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative from freqtrade.strategy import IStrategy, IntParameter
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class StructureFlowSwingV32(IStrategy): class StructureFlowSwingV40(IStrategy):
""" """
Structure Flow Swing Strategy v3.2 Structure Flow Swing Strategy v4.0
4H震荡区间波段交易 — 市场感知增强版 15m 震荡区间波段交易 — 价格聚集度检测
""" """
can_short = True can_short = True
@ -44,180 +38,21 @@ class StructureFlowSwingV32(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
minimal_roi = {"0": 100} minimal_roi = {"0": 100}
max_open_trades = 1 max_open_trades = 1
timeframe = "4h" timeframe = "15m"
# ===================== # =====================
# 核心参数沿用v3.1默认值) # 可优化参数
# ===================== # =====================
swing_lookback = IntParameter(4, 8, default=5, space="buy") lookback = IntParameter(24, 96, default=48, space="buy") # 检测窗口24~96根15m6h~24h
zone_stability_threshold = IntParameter(15, 40, default=25, space="buy") min_touches = IntParameter(1, 4, default=2, space="buy") # 边界至少测试次数
entry_zone_pct = IntParameter(1, 5, default=3, space="buy") zone_width_atr_mult = IntParameter(2, 6, default=4, space="buy") # 区间宽度上限 = ATR × N
atr_stop_mult = IntParameter(10, 25, default=15, space="buy") entry_zone_pct = IntParameter(2, 8, default=5, space="buy") # 入场范围距边界千分比0.5%
take_profit_pct = IntParameter(50, 80, default=70, space="sell") atr_stop_mult = IntParameter(10, 25, default=15, space="buy") # ATR止损倍数
take_profit_pct = IntParameter(50, 80, default=70, space="sell") # 区间高度止盈比例
# v3.2 新增参数
d1_trend_strength_max = IntParameter(6, 15, default=10, space="buy") # D1趋势强度上限%默认10%(极端趋势才触发)
zone_quality_min = IntParameter(20, 60, default=30, space="buy") # 区间质量最低分默认30
# 固定参数 # 固定参数
zone_touch_lookback = 10 breakout_bars = 2 # 连续几根K线突破才算真突破
breakout_bars = 2 cooldown = 4 # 入场后冷却 4 根15m1小时
early_exit_bars = 3 # v3.2新增连续N根在区间外触发主动退出
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
n = len(high)
sh = pd.Series(np.nan, index=high.index, dtype=float)
sl = pd.Series(np.nan, index=low.index, dtype=float)
for i in range(window, n - window):
if high.iloc[i] > high.iloc[i - window:i].max() and high.iloc[i] > high.iloc[i + 1:i + window + 1].max():
sh.iloc[i] = high.iloc[i]
if low.iloc[i] < low.iloc[i - window:i].min() and low.iloc[i] < low.iloc[i + 1:i + window + 1].min():
sl.iloc[i] = low.iloc[i]
return sh, sl
# =====================
# 工具:区间震荡检测(增强版:加入质量评分数据)
# =====================
def _detect_range(
self,
sh: pd.Series,
sl: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
) -> DataFrame:
n = len(high)
is_ranging = np.full(n, False)
support_arr = np.full(n, np.nan)
resistance_arr = np.full(n, np.nan)
zone_width_arr = np.full(n, np.nan)
touch_count_arr = np.full(n, 0) # v3.2新增
sh_prices = []
sl_prices = []
in_range = False
touch_count = 0
for i in range(n):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 5:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 5:
sl_prices.pop(0)
if len(sh_prices) < 3 or len(sl_prices) < 3:
# 不在区间中
if in_range:
in_range = False
touch_count = 0
continue
current_sh = sh_prices[-1]
current_sl = sl_prices[-1]
if current_sh <= current_sl:
if in_range:
in_range = False
touch_count = 0
continue
zone_width = (current_sh - current_sl) / current_sl
support_arr[i] = current_sl
resistance_arr[i] = current_sh
zone_width_arr[i] = zone_width
# 条件1区间宽度稳定性
widths = []
for j in range(min(len(sh_prices), len(sl_prices)) - 1, -1, -1):
w = (sh_prices[j] - sl_prices[j]) / sl_prices[j]
widths.append(w)
if len(widths) >= 3:
break
if len(widths) >= 3:
mean_width = np.mean(widths)
if mean_width > 0:
max_dev = max(abs(w - mean_width) / mean_width for w in widths)
stability_threshold = self.zone_stability_threshold.value / 100.0
is_stable = max_dev <= stability_threshold
else:
is_stable = False
else:
is_stable = False
if not is_stable:
if in_range:
in_range = False
touch_count = 0
continue
# 条件2价格测试过边界 — v3.1: AND→OR
start_idx = max(0, i - self.zone_touch_lookback)
support_zone_upper = current_sl * 1.01
touched_support = any(
low.iloc[j] <= support_zone_upper
for j in range(start_idx, i + 1)
)
resistance_zone_lower = current_sh * 0.99
touched_resistance = any(
high.iloc[j] >= resistance_zone_lower
for j in range(start_idx, i + 1)
)
if not (touched_support or touched_resistance):
if in_range:
in_range = False
touch_count = 0
continue
# 条件3无突破
consecutive_outside = 0
for j in range(i, max(0, i - self.breakout_bars) - 1, -1):
if close.iloc[j] > current_sh or close.iloc[j] < current_sl:
consecutive_outside += 1
else:
break
if consecutive_outside >= self.breakout_bars:
if in_range:
in_range = False
touch_count = 0
continue
# === 通过所有条件 → 在区间中 ===
is_ranging[i] = True
# v3.2: 跟踪区间内的边界触碰次数(质量评分数据)
if not in_range:
in_range = True
touch_count = 0
c = close.iloc[i]
if (c <= current_sl * 1.015) or (c >= current_sh * 0.985):
touch_count += 1
touch_count_arr[i] = touch_count
return DataFrame({
"is_ranging": is_ranging,
"support": support_arr,
"resistance": resistance_arr,
"zone_width": zone_width_arr,
"touch_count": touch_count_arr, # v3.2新增
}, index=high.index)
# ===================== # =====================
# 工具ATR计算 # 工具ATR计算
@ -232,206 +67,116 @@ class StructureFlowSwingV32(IStrategy):
}).max(axis=1) }).max(axis=1)
return tr.rolling(period).mean() return tr.rolling(period).mean()
# ================================================================ # =====================
# D1 信息时间框架 — v3.2: 新增趋势强度计算 # 时间框架 — 15m 指标
# ================================================================ # =====================
@informative("1d")
def populate_indicators_1d(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# 原有D1趋势方向swing point比较
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"], window=5
)
sh_vals = sh.dropna()
sl_vals = sl.dropna()
is_uptrend = pd.Series(False, index=dataframe.index)
is_downtrend = pd.Series(False, index=dataframe.index)
if len(sh_vals) >= 2 and len(sl_vals) >= 2:
if sh_vals.iloc[-1] > sh_vals.iloc[-2] and sl_vals.iloc[-1] > sl_vals.iloc[-2]:
is_uptrend[:] = True
elif sh_vals.iloc[-1] < sh_vals.iloc[-2] and sl_vals.iloc[-1] < sl_vals.iloc[-2]:
is_downtrend[:] = True
dataframe["d1_uptrend"] = is_uptrend
dataframe["d1_downtrend"] = is_downtrend
# v3.2新增D1趋势强度 = EMA20与EMA50的偏离程度
ema_20 = dataframe["close"].ewm(span=20, adjust=False).mean()
ema_50 = dataframe["close"].ewm(span=50, adjust=False).mean()
dataframe["trend_strength"] = abs(ema_20 - ema_50) / ema_50
return dataframe
# ================================================================
# 主时间框架 — 4H 指标v3.2: 新增区间质量评分)
# ================================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
sh, sl = self._detect_swing_points( lookback = self.lookback.value
dataframe["high"], dataframe["low"],
self.swing_lookback.value,
)
range_info = self._detect_range(sh, sl, dataframe["high"], dataframe["low"], dataframe["close"]) # ── 价格聚集度检测 ──
dataframe["is_ranging"] = range_info["is_ranging"] rolling_high = dataframe["high"].rolling(lookback).max()
dataframe["range_support"] = range_info["support"] rolling_low = dataframe["low"].rolling(lookback).min()
dataframe["range_resistance"] = range_info["resistance"]
dataframe["zone_width_pct"] = range_info["zone_width"]
dataframe["range_touch_count"] = range_info["touch_count"]
# 区间宽度(绝对值和百分比)
zone_width = rolling_high - rolling_low
zone_width_pct = zone_width / 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
# ATR
dataframe["atr"] = self._calc_atr(dataframe["high"], dataframe["low"], dataframe["close"], 14) dataframe["atr"] = self._calc_atr(dataframe["high"], dataframe["low"], dataframe["close"], 14)
# 价格在区间内的位置 # ── 边界测试计数 ──
denom = dataframe["range_resistance"] - dataframe["range_support"] # 价格在区间上边界 0.5% 范围内 → 算一次测试
touch_upper = dataframe["high"] >= rolling_high * 0.995
touch_lower = dataframe["low"] <= rolling_low * 1.005
# 滚动窗口内测试次数
dataframe["upper_touches"] = touch_upper.rolling(lookback).sum()
dataframe["lower_touches"] = touch_lower.rolling(lookback).sum()
# ── 震荡判定条件 ──
atr_mult = self.zone_width_atr_mult.value
min_touches = self.min_touches.value
# 条件1区间宽度合理不超过 ATR × N
is_compact = zone_width <= dataframe["atr"] * atr_mult
# 条件2上下边界都被测试过至少 min_touches 次
is_tested = (dataframe["upper_touches"] >= min_touches) & (dataframe["lower_touches"] >= min_touches)
# 条件3无突破最近 breakout_bars 根收盘价在边界内)
no_break_high = True
no_break_low = True
for i in range(1, self.breakout_bars + 1):
if i <= len(dataframe):
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
dataframe["is_ranging"] = is_ranging
# ── 价格在区间内的位置 ──
denom = rolling_high - rolling_low
dataframe["zone_position"] = np.where( dataframe["zone_position"] = np.where(
denom > 0, denom > 0,
(dataframe["close"] - dataframe["range_support"]) / denom, (dataframe["close"] - rolling_low) / denom,
np.nan, np.nan,
) )
# 距边界百分比 # 距边界百分比
dataframe["dist_to_support"] = np.where( dataframe["dist_to_low"] = np.where(
dataframe["range_support"] > 0, rolling_low > 0,
(dataframe["close"] - dataframe["range_support"]) / dataframe["close"], (dataframe["close"] - rolling_low) / dataframe["close"],
np.nan, np.nan,
) )
dataframe["dist_to_resistance"] = np.where( dataframe["dist_to_high"] = np.where(
dataframe["range_resistance"] > 0, rolling_high > 0,
(dataframe["range_resistance"] - dataframe["close"]) / dataframe["close"], (rolling_high - dataframe["close"]) / dataframe["close"],
np.nan, np.nan,
) )
# ── v3.2新增:区间质量评分 ── # ── 填充 ──
self._compute_zone_quality(dataframe) for col in ["is_ranging", "zone_position", "dist_to_low", "dist_to_high"]:
# ── v3.2新增:区间连续计数 ──
is_ranging_int = dataframe["is_ranging"].astype(int)
consecutive = np.zeros(len(dataframe), dtype=int)
for i in range(1, len(dataframe)):
if is_ranging_int.iloc[i] and is_ranging_int.iloc[i-1]:
consecutive[i] = consecutive[i-1] + 1
elif is_ranging_int.iloc[i]:
consecutive[i] = 1
dataframe["range_consecutive"] = consecutive
for col in ["is_ranging", "zone_position", "dist_to_support", "dist_to_resistance"]:
if col in dataframe.columns: if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False if col == "is_ranging" else 999) dataframe[col] = dataframe[col].fillna(False if col == "is_ranging" else 999)
return dataframe return dataframe
def _compute_zone_quality(self, dataframe: DataFrame) -> None:
"""
v3.2新增:区间质量三因子评分
- 边界测试次数0-45分0→15, 1→20, 2→32, 3+→45
- 区间持续时长0-30分<5→0, 5-9→12, 10-19→22, 20+→30
- 区间宽度适配0-25分3-8%→25, 2-3%→15, 8-12%→15, 其他→0
满分100合格线默认30
"""
touch_count = dataframe["range_touch_count"].fillna(0).values
zone_width = dataframe["zone_width_pct"].fillna(0).values
is_ranging = dataframe["is_ranging"].values
quality = np.zeros(len(dataframe))
# 因子1边界测试次数放宽0次触碰也有基础分
quality += np.where(
touch_count >= 3, 45,
np.where(touch_count >= 2, 32,
np.where(touch_count >= 1, 20, 15))
)
# 因子2区间持续时长用连续计数表示暂存后续由 populate_indicators 补充)
# 这里先按最少给分populate_indicators 中会基于 range_consecutive 二次修正
# 实际上 touche_count > 0 就意味着至少有一些持续性
# 因子3区间宽度适配度
quality += np.where(
(zone_width >= 0.03) & (zone_width <= 0.08), 25,
np.where(
((zone_width >= 0.02) & (zone_width < 0.03)) |
((zone_width > 0.08) & (zone_width <= 0.12)), 15, 0
)
)
# 只在区间内有效
quality = np.where(is_ranging, quality, 0)
dataframe["zone_quality_base"] = quality
# ================================================================ # ================================================================
# 入场信号 — v3.2: D1趋势强度 + 区间质量过滤 + 持续时间因子 # 入场信号
# ================================================================ # ================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
entry_zone = self.entry_zone_pct.value / 100.0 entry_zone = self.entry_zone_pct.value / 1000.0 # 千分比
d1_downtrend_col = "d1_downtrend_1d" if "is_ranging" not in dataframe.columns:
d1_uptrend_col = "d1_uptrend_1d" dataframe["is_ranging"] = False
d1_strength_col = "trend_strength_1d" # v3.2新增
for col in ["is_ranging", d1_uptrend_col, d1_downtrend_col, d1_strength_col]: # ── 做多:震荡中,价格靠近下边界 ──
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
else:
dataframe[col] = False
# ── v3.2: 计算完整区间质量评分(加入持续性因子) ──
range_consec = dataframe.get("range_consecutive", pd.Series(0, index=dataframe.index))
quality_base = dataframe.get("zone_quality_base", pd.Series(0, index=dataframe.index))
# 持续性因子:<5→0, 5-9→12, 10-19→22, 20+→30
duration_score = np.where(
range_consec >= 20, 30,
np.where(range_consec >= 10, 22,
np.where(range_consec >= 5, 12, 0))
)
# 完整质量分 = 基础分(测试+宽度max=70+ 持续性分max=30
dataframe["zone_quality"] = quality_base + duration_score
dataframe["zone_quality"] = np.where(dataframe["is_ranging"], dataframe["zone_quality"], 0)
# ── v3.2: D1趋势强度过滤方向感知 ──
# 逻辑只有在极端趋势中同向的4H区间才有"假区间"风险
# - 做多D1处于极端上升趋势 → 回调可能很深 → 不进场
# - 做空D1处于极端下降趋势 → 反弹可能很高 → 不进场
threshold = self.d1_trend_strength_max.value / 100.0
d1_strength_strong = dataframe[d1_strength_col] > threshold
long_d1_ok = ~(dataframe[d1_uptrend_col] & d1_strength_strong) # 极端上升趋势不做多
short_d1_ok = ~(dataframe[d1_downtrend_col] & d1_strength_strong) # 极端下降趋势不做空
# ── v3.2: 区间质量过滤 ──
quality_min = self.zone_quality_min.value
zone_quality_ok = dataframe["zone_quality"] >= quality_min
# ── 做多:震荡市中,价格靠近支撑位 ──
long_conds = ( long_conds = (
dataframe["is_ranging"] dataframe["is_ranging"]
& (dataframe["dist_to_support"] <= entry_zone) & (dataframe["dist_to_low"] < entry_zone)
& (dataframe["dist_to_support"] > 0) & (dataframe["dist_to_low"] > 0)
& (~dataframe[d1_downtrend_col]) # 原有D1不能是下降趋势
& long_d1_ok # v3.2新增:极端上升趋势不做多
& zone_quality_ok # v3.2新增:区间质量达标
) )
cooldown = 1 # 冷却
long_recent = long_conds.rolling(cooldown, min_periods=1).max().shift(1) == 0 long_recent = long_conds.rolling(self.cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_conds & long_recent, "enter_long"] = 1 dataframe.loc[long_conds & long_recent, "enter_long"] = 1
# ── 做空:震荡中,价格靠近阻力位 ── # ── 做空:震荡中,价格靠近上边界 ──
short_conds = ( short_conds = (
dataframe["is_ranging"] dataframe["is_ranging"]
& (dataframe["dist_to_resistance"] <= entry_zone) & (dataframe["dist_to_high"] < entry_zone)
& (dataframe["dist_to_resistance"] > 0) & (dataframe["dist_to_high"] > 0)
& (~dataframe[d1_uptrend_col]) # 原有D1不能是上升趋势
& short_d1_ok # v3.2新增:极端下降趋势不做空
& zone_quality_ok # v3.2新增:区间质量达标
) )
short_recent = short_conds.rolling(cooldown, min_periods=1).max().shift(1) == 0 short_recent = short_conds.rolling(self.cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_conds & short_recent, "enter_short"] = 1 dataframe.loc[short_conds & short_recent, "enter_short"] = 1
return dataframe return dataframe
@ -444,7 +189,7 @@ class StructureFlowSwingV32(IStrategy):
return dataframe return dataframe
# ================================================================ # ================================================================
# 自定义止损:支撑/阻力外侧ATR*1.5 缓冲v3.1逻辑保持不变) # 自定义止损:区间边界外侧 + ATR 缓冲
# ================================================================ # ================================================================
def custom_stoploss( def custom_stoploss(
@ -465,36 +210,36 @@ class StructureFlowSwingV32(IStrategy):
atr_mult = self.atr_stop_mult.value / 10.0 atr_mult = self.atr_stop_mult.value / 10.0
if not trade.is_short: if not trade.is_short:
support = last.get("range_support", np.nan) zone_low = last.get("zone_low", np.nan)
atr = last.get("atr", np.nan) atr = last.get("atr", np.nan)
if pd.isna(support) or support <= 0: if pd.isna(zone_low) or zone_low <= 0:
return -0.02 return -0.02
if pd.notna(atr) and atr > 0: if pd.notna(atr) and atr > 0:
sl_price = support - atr * atr_mult sl_price = zone_low - atr * atr_mult
else: else:
sl_price = support * 0.985 sl_price = zone_low * 0.985
sl_ratio = (sl_price / current_rate) - 1.0 sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.20) return max(sl_ratio, -0.20)
else: else:
resistance = last.get("range_resistance", np.nan) zone_high = last.get("zone_high", np.nan)
atr = last.get("atr", np.nan) atr = last.get("atr", np.nan)
if pd.isna(resistance) or resistance <= 0: if pd.isna(zone_high) or zone_high <= 0:
return 0.02 return 0.02
if pd.notna(atr) and atr > 0: if pd.notna(atr) and atr > 0:
sl_price = resistance + atr * atr_mult sl_price = zone_high + atr * atr_mult
else: else:
sl_price = resistance * 1.015 sl_price = zone_high * 1.015
sl_ratio = 1.0 - (sl_price / current_rate) sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.20) return min(sl_ratio, 0.20)
# ================================================================ # ================================================================
# 自定义止盈:区间70% + v3.2主动退出机制 # 自定义止盈:区间高度 × TP%
# ================================================================ # ================================================================
def custom_exit( def custom_exit(
@ -514,54 +259,25 @@ class StructureFlowSwingV32(IStrategy):
last = dataframe.iloc[-1] last = dataframe.iloc[-1]
# ── 原有区间70%止盈 ──
if not trade.is_short: if not trade.is_short:
support = last.get("range_support", np.nan) zone_low = last.get("zone_low", np.nan)
resistance = last.get("range_resistance", np.nan) zone_high = last.get("zone_high", np.nan)
if pd.notna(support) and pd.notna(resistance) and resistance > support: if pd.notna(zone_low) and pd.notna(zone_high) and zone_high > zone_low:
zone_height = (resistance - support) / support zone_height = (zone_high - zone_low) / zone_low
tp_target = zone_height * tp_pct tp_target = zone_height * tp_pct
if current_profit >= tp_target: if current_profit >= tp_target:
return "take_profit" return "take_profit"
else: else:
support = last.get("range_support", np.nan) zone_low = last.get("zone_low", np.nan)
resistance = last.get("range_resistance", np.nan) zone_high = last.get("zone_high", np.nan)
if pd.notna(support) and pd.notna(resistance) and resistance > support: if pd.notna(zone_low) and pd.notna(zone_high) and zone_high > zone_low:
zone_height = (resistance - support) / resistance zone_height = (zone_high - zone_low) / zone_high
tp_target = zone_height * tp_pct tp_target = zone_height * tp_pct
if current_profit >= tp_target: if current_profit >= tp_target:
return "take_profit" return "take_profit"
# ── v3.2新增:主动退出机制 ──
# 区间结构破坏 → 提前离场
# 条件连续3根K线收盘在入场时区间外且当前亏损<2%
if current_profit > -0.02:
# 找到入场时的K线取最后一根确认的K线不是当前正在形成的
entry_date = trade.open_date
entry_mask = dataframe["date"] <= entry_date
if entry_mask.any():
entry_idx = dataframe[entry_mask].index[-1]
entry_support = dataframe.loc[entry_idx, "range_support"]
entry_resistance = dataframe.loc[entry_idx, "range_resistance"]
if pd.notna(entry_support) and pd.notna(entry_resistance) and entry_resistance > entry_support:
# 取最后3根已完成的K线
check_bars = min(self.early_exit_bars, len(dataframe) - 1)
recent = dataframe.iloc[-(check_bars + 1):-1] # 排除当前正在形成的K线
if len(recent) >= self.early_exit_bars:
outside_count = 0
for _, bar in recent.iterrows():
c = bar["close"]
# 缓冲0.5%避免噪音触发
if c < entry_support * 0.995 or c > entry_resistance * 1.005:
outside_count += 1
if outside_count >= self.early_exit_bars:
return "early_exit_structure_broken"
return None return None
# ================================================================ # ================================================================
@ -572,18 +288,17 @@ class StructureFlowSwingV32(IStrategy):
def plot_config() -> dict: def plot_config() -> dict:
return { return {
"main_plot": { "main_plot": {
"range_support": {"color": "green", "type": "line"}, "zone_high": {"color": "red", "type": "line"},
"range_resistance": {"color": "red", "type": "line"}, "zone_low": {"color": "green", "type": "line"},
}, },
"subplots": { "subplots": {
"range": { "zone": {
"is_ranging": {"color": "blue", "type": "line"}, "is_ranging": {"color": "blue", "type": "line"},
"zone_width_pct": {"color": "purple", "type": "line"}, "zone_width_pct": {"color": "purple", "type": "line"},
"zone_quality": {"color": "orange", "type": "line"},
}, },
"position": { "touches": {
"dist_to_support": {"color": "green", "type": "line"}, "upper_touches": {"color": "red", "type": "line"},
"dist_to_resistance": {"color": "red", "type": "line"}, "lower_touches": {"color": "green", "type": "line"},
}, },
}, },
} }