Compare commits

9 Commits

12 changed files with 4999 additions and 371 deletions

32
README.md Normal file
View File

@ -0,0 +1,32 @@
# Beast Trader 策略仓库
ETH/USDT 永续合约量化交易策略版本管理,基于 freqtrade + Binance。
## 当前部署
**v2.2d** — 三层趋势共振D1+4H+1H震荡市自动休眠dry-run 运行中
## 版本演进
| 系列 | 版本范围 | 方向 | 状态 |
|------|---------|------|------|
| v0.x | v0.1 ~ v0.3 | 价格行为探索 | 已弃用 |
| v1.x | v1.0 ~ v1.9 | 结构流策略迭代 | 已弃用 |
| v2.x | v2.0 ~ v2.2d | 趋势跟踪(当前主线) | **v2.2d 运行中** |
| v3.x | v3.0 ~ v3.2 | 震荡波段Swing | 已验证/备用 |
| v4.x | v4.0 ~ v4.2 | 极简震荡 | 实验 |
| Scalp | v1.8, v2.0 | 剥头皮 | 已弃用 |
## 关键教训
- v1.1~v1.8 Scalp反向S/R交易 = 逆势接飞刀0%胜率)
- v2.3参数调优不是方向创建后10分钟删除
- v2.2b:当前最优回测基线(+4673%/+17%最大回撤)
- v2.2dD1趋势总闸门 — 震荡市不下单是保护机制不是bug
## 铁律
1. 只增不删 — 所有历史版本保留
2. 版本归档 — 每个版本独立 commit
3. 回测标准化 — 复用成功配置
4. 主任不越俎代庖 — 方案设计归主任,代码编写归交易部

454
ablation/ablation_1.py Normal file
View File

@ -0,0 +1,454 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21_Abl1(IStrategy):
"""
Ablation Variant 1: 移除条件 1
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

454
ablation/ablation_2.py Normal file
View File

@ -0,0 +1,454 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21_Abl2(IStrategy):
"""
Ablation Variant 2: 移除条件 2
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["bullish_signal"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["bearish_signal"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

454
ablation/ablation_3.py Normal file
View File

@ -0,0 +1,454 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21_Abl3(IStrategy):
"""
Ablation Variant 3: 移除条件 3
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

454
ablation/ablation_4.py Normal file
View File

@ -0,0 +1,454 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21_Abl4(IStrategy):
"""
Ablation Variant 4: 移除条件 4
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
& (long_stop_dist > 0.003)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
& (short_stop_dist > 0.003)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

454
ablation/ablation_5.py Normal file
View File

@ -0,0 +1,454 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21_Abl5(IStrategy):
"""
Ablation Variant 5: 移除条件 5
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
& (long_stop_dist <= max_dist)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
& (short_stop_dist <= max_dist)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

454
ablation/ablation_6.py Normal file
View File

@ -0,0 +1,454 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21_Abl6(IStrategy):
"""
Ablation Variant 6: 移除条件 6
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

454
ablation/ablation_7.py Normal file
View File

@ -0,0 +1,454 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21_Abl7(IStrategy):
"""
Ablation Variant 7: 移除条件 7
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

456
ablation/ablation_8.py Normal file
View File

@ -0,0 +1,456 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21_Abl8(IStrategy):
"""
Ablation Variant 8: 移除条件 8
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = True # cooldown removed
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = True # cooldown removed
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

View File

@ -0,0 +1,442 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21_Ablall(IStrategy):
"""
Ablation Variant all: 移除条件 1,2,3,4,5,6,7,8
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
)
long_recent = True # cooldown removed
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
)
short_recent = True # cooldown removed
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

456
ablation/v2_1_baseline.py Normal file
View File

@ -0,0 +1,456 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV21(IStrategy):
"""
Structure Flow Strategy v2.1 — D1: 趋势强度过滤
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

View File

@ -1,398 +1,270 @@
""" # structure_flow_momentum_scalp.py
Structure Flow Strategy v2.2c — 冷却期修复版 # 顺趋势剥头皮策略 v2.0
============================================== #
变更记录: # 核心思路不再在S/R处做反向交易接飞刀而是顺趋势方向等回调后入场。
v2.2c (2026-06-11): 1H S/R 替代 4H S/R #
v2.2c-coolfix (2026-06-11): 修复冷却期无限阻止下单 bug # ┌─────────────────────────────────────────────────────────────┐
""" # │ 15m趋势方向判断EMA20 vs EMA50
# │ ↓ │
# │ 上升趋势 → 只等5m回调到EMA20/支撑附近 → 止跌信号 → 做多 │
# │ 下降趋势 → 只等5m反弹到EMA20/阻力附近 → 止涨信号 → 做空 │
# │ ↓ │
# │ 止损ATR×1.0 | 止盈ATR×1.5 | 时间止损60分钟 │
# └─────────────────────────────────────────────────────────────┘
#
# v2.0 (2026-06-10): 初始版本,完全重写
from datetime import datetime from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative
import numpy as np
import pandas as pd
from pandas import DataFrame from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative import pandas as pd
import numpy as np
from datetime import datetime
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class StructureFlowStrategyV22d(IStrategy): class StructureFlowMomentumScalp(IStrategy):
"""
顺趋势剥头皮策略 v2.0
核心逻辑:
- 15m EMA趋势方向过滤只做顺趋势方向的单
- 5m 回调到EMA20或S/R支撑/阻力区域时等待K线信号确认后入场
- 止损 ATR×1.0,止盈 ATR×1.5,时间止损 60 分钟
- 不做方向猜测,不吃鱼头鱼尾,只吃回调结束那一小段
"""
# ── 时间框架 ──
timeframe = "5m"
# ── 交易参数 ──
can_short = True can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1 max_open_trades = 1
timeframe = "1h" stake_amount = "unlimited"
use_custom_stoploss = True
use_exit_signal = False # 出场完全由 custom_stoploss + custom_exit 管理
# ===================== # ── 合约参数 ──
# 可优化参数 margin_mode = "cross"
# ===================== trading_mode = "futures"
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy") # ── 可优化参数 ──
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy") # 趋势检测
swing_lookback_1h = IntParameter(3, 7, default=5, space="buy") # 新增1H swing参数 trend_ema_period = IntParameter(10, 30, default=20, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy") # 回调确认幅度
max_stop_dist = IntParameter(20, 50, default=50, space="buy") pullback_deviation = DecimalParameter(0.2, 1.0, default=0.5, decimals=1, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy") # 入场冷却期
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") cooldown_bars = IntParameter(2, 8, default=3, space="buy")
# K线形态灵敏度
pin_bar_wick_ratio = IntParameter(50, 80, default=60, space="buy")
# 止损ATR倍数
atr_mult_stop = DecimalParameter(0.8, 2.0, default=1.0, decimals=1, space="sell")
# 止盈ATR倍数
atr_mult_tp = DecimalParameter(1.0, 3.0, default=1.5, decimals=1, space="sell")
# ===================== # ── 常数 ──
# 工具Swing Point 检测 time_stop_minutes = 60 # 最大持仓时间
# =====================
@staticmethod # ── 保护性止损 ──
def _detect_swing_points( stoploss = -0.10 # 硬止损 10%
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 _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# =====================
# 工具:冷却期正确实现(修复 bug
# =====================
def _apply_cooldown(self, signal: pd.Series, cooldown_bars: int) -> pd.Series:
"""
正确应用冷却期:入场后才冷却,而非条件满足就冷却。
原逻辑 buglong_base.rolling(cooldown).max().shift(1) == 0
- 当市场持续满足入场条件时rolling window 里永远有 True
- 导致冷却期无限阻止下单
修复逻辑:遍历 K 线,模拟"入场 -> 冷却"过程。
- 满足条件 + 距离上次入场 > cooldown -> 允许入场
- 入场后 cooldown 根 K 线内不再入场
"""
n = len(signal)
result = [False] * n
last_entry = -99999 # 上次入场的 bar 索引
# 遍历(对 numpy array 操作O(n) 约几毫秒)
values = signal.values # numpy array快速访问
for i in range(n):
if values[i] and (i - last_entry) > cooldown_bars:
result[i] = True
last_entry = i
return pd.Series(result, index=signal.index)
# ================================================================ # ================================================================
# 信息时间框架 — D1 宏观结构 # 杠杆
# ================================================================ # ================================================================
@informative("1d") def leverage(
def populate_indicators_1d( self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs,
) -> float:
"""20x 杠杆起步,验证胜率后再上量"""
return min(20.0, max_leverage)
# ================================================================
# 信息时间框架 — 15m 趋势判断 + S/R
# ================================================================
@informative("15m")
def populate_indicators_15m(
self, dataframe: DataFrame, metadata: dict self, dataframe: DataFrame, metadata: dict
) -> DataFrame: ) -> DataFrame:
sh, sl = self._detect_swing_points( """15m级别EMA趋势方向 + swing point S/R。"""
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================ # ── EMA 趋势方向 ──
# 信息时间框架 — 4H 趋势强度(原版保留) ema_period = self.trend_ema_period.value
# ================================================================ dataframe["ema_fast"] = dataframe["close"].ewm(span=ema_period, adjust=False).mean()
dataframe["ema_slow"] = dataframe["close"].ewm(span=ema_period * 2.5, adjust=False).mean()
@informative("4h") dataframe["trend_up"] = dataframe["ema_fast"] > dataframe["ema_slow"]
def populate_indicators_4h( dataframe["trend_down"] = dataframe["ema_fast"] < dataframe["ema_slow"]
self, dataframe: DataFrame, metadata: dict
) -> DataFrame: # ── Swing Point 支撑/阻力 ──
sh, sl = self._detect_swing_points( high = dataframe["high"].tolist()
dataframe["high"], dataframe["low"], low = dataframe["low"].tolist()
self.swing_lookback_h4.value, close = dataframe["close"].tolist()
)
structure = self._build_structure( sh, sl = self._detect_swing_points(high, low, window=5)
dataframe["high"], dataframe["low"], dataframe["close"], trend_up_arr, trend_down_arr, support_arr, resistance_arr = self._build_structure(
sh, sl, high, low, close, sh, sl,
) )
# 趋势强度计算(原版逻辑) dataframe["trend_up_sp"] = trend_up_arr
sh_prices = [] dataframe["trend_down_sp"] = trend_down_arr
sl_prices = [] # EMA平滑S/R避免跳变
trend_strength_up = np.full(len(dataframe), np.nan) dataframe["support"] = self._ema_smooth(support_arr, alpha=0.3)
trend_strength_down = np.full(len(dataframe), np.nan) dataframe["resistance"] = self._ema_smooth(resistance_arr, alpha=0.3)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
trend_strength_up[i] = hh_dist + hl_dist
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
min_strength = self.trend_strength_min.value / 100.0
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe return dataframe
# ================================================================ # ================================================================
# 主时间框架 — 1H 指标(含 1H S/R + 活支撑/阻力) # 主框架 — 5m 级别指标
# ================================================================ # ================================================================
def populate_indicators( def populate_indicators(
self, dataframe: DataFrame, metadata: dict self, dataframe: DataFrame, metadata: dict
) -> DataFrame: ) -> DataFrame:
"""5m级别ATR + K线形态 + EMA趋势整合。"""
# ── ATR(14) ──
high = dataframe["high"]
low = dataframe["low"]
close = dataframe["close"]
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs(),
], axis=1).max(axis=1)
dataframe["atr"] = tr.rolling(14).mean()
atr_mean = dataframe["atr"].mean()
dataframe["atr"] = dataframe["atr"].fillna(atr_mean)
# ── K线形态 ── # ── K线形态 ──
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = ( bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns( self._detect_candle_patterns(
dataframe["open"], dataframe["open"], dataframe["high"], dataframe["low"],
dataframe["high"], dataframe["close"], self.pin_bar_wick_ratio.value / 100.0,
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
) )
) )
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# ── 1H级别 Swing Point + 结构替代原4H S/R ── # ── 5m EMA用于短期拉回确认 ──
sh_1h, sl_1h = self._detect_swing_points( dataframe["ema5"] = close.ewm(span=5, adjust=False).mean()
dataframe["high"], dataframe["low"], dataframe["ema8"] = close.ewm(span=8, adjust=False).mean()
self.swing_lookback_1h.value,
)
structure_1h = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh_1h, sl_1h,
)
dataframe["trend_up_1h"] = structure_1h["trend_up"]
dataframe["trend_down_1h"] = structure_1h["trend_down"]
dataframe["support"] = structure_1h["support"]
dataframe["resistance"] = structure_1h["resistance"]
dataframe["in_demand"] = structure_1h["in_demand"]
dataframe["in_supply"] = structure_1h["in_supply"]
# ── 1H 活支撑/阻力检查 ── # ── 布尔列NaN填充 ──
touched_support = ( for col in ["bullish_signal", "bearish_signal"]:
(dataframe["low"] <= dataframe["support"] * 1.005) & dataframe[col] = dataframe[col].fillna(False)
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ── NaN 安全处理 ──
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand", "in_supply",
"support_alive", "resistance_alive",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe return dataframe
# ===================== # ================================================================
# 入场信号(修复冷却期逻辑 # 入场逻辑
# ===================== # ================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(
max_dist = self.max_stop_dist.value / 100.0 self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""
入场逻辑。
只做顺趋势回调入场不做S/R反向交易
做多条件:
1. 15m 上升趋势EMA_fast > EMA_slow
2. 5m 价格回调到15m EMA_fast 或 支撑位附近
3. 5m K线止跌信号pinbar/engulfing
做空条件(对称):
1. 15m 下降趋势
2. 5m 价格反弹到15m EMA_fast 或 阻力位附近
3. 5m K线止涨信号
"""
cooldown = self.cooldown_bars.value cooldown = self.cooldown_bars.value
dev = self.pullback_deviation.value / 100.0 # 0.5% → 0.005
bool_cols = [ # ── 必要列检查 ──
"trend_up_1d", "trend_down_1d", required = [
"trend_up_4h", "trend_down_4h", "ema_fast_15m", "trend_up_15m", "trend_down_15m",
"in_demand", "in_supply", "support_15m", "resistance_15m",
"support_alive", "resistance_alive",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
] ]
for col in bool_cols: for col in required:
if col not in dataframe.columns:
return dataframe
# ── 布尔列填充 ──
for col in [
"bullish_signal", "bearish_signal",
"trend_up_15m", "trend_down_15m",
]:
if col in dataframe.columns: if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False) dataframe[col] = dataframe[col].fillna(False)
# ── 做多使用1H S/R ── # ═══════════════════════════════════════════════════════════
long_stop_dist = (dataframe["open"] - dataframe["support"]) / dataframe["open"] # 做多:上升趋势 + 回调到EMA/支撑 + 止跌信号
# ═══════════════════════════════════════════════════════════
long_base = ( # 条件115m 上升趋势
dataframe["trend_up_1d"] trend_up = dataframe["trend_up_15m"]
& dataframe["in_demand"]
& (long_stop_dist <= max_dist) # 条件2价格在EMA20或支撑位附近回调到顺趋势的支撑区
& (long_stop_dist > 0.003) near_ema = (
& dataframe["support_alive"] (dataframe["low"] <= dataframe["ema_fast_15m"] * (1.0 + dev * 0.5)) &
& dataframe["strong_uptrend_4h"] (dataframe["low"] >= dataframe["ema_fast_15m"] * (1.0 - dev * 2.0))
) )
near_support = (
# ✅ 修复:正确应用冷却期(基于实际入场,而非条件满足) (dataframe["low"] <= dataframe["support_15m"] * (1.0 + dev)) &
long_entries = self._apply_cooldown(long_base, cooldown) (dataframe["low"] >= dataframe["support_15m"] * (1.0 - dev))
dataframe.loc[long_entries, "enter_long"] = 1
# ── 做空使用1H S/R ──
short_stop_dist = (dataframe["resistance"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive"]
& dataframe["strong_downtrend_4h"]
) )
pullback_long = near_ema | near_support
# ✅ 修复:正确应用冷却期(基于实际入场,而非条件满足) # 条件3K线止跌信号
short_entries = self._apply_cooldown(short_base, cooldown) signal_long = dataframe["bullish_signal"]
dataframe.loc[short_entries, "enter_short"] = 1
# 综合入场
enter_long = trend_up & pullback_long & signal_long
long_recent = enter_long.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[enter_long & long_recent, "enter_long"] = 1
# ═══════════════════════════════════════════════════════════
# 做空:下降趋势 + 反弹到EMA/阻力 + 止涨信号
# ═══════════════════════════════════════════════════════════
# 条件115m 下降趋势
trend_down = dataframe["trend_down_15m"]
# 条件2价格在EMA20或阻力位附近反弹到顺趋势的阻力区
near_ema_short = (
(dataframe["high"] >= dataframe["ema_fast_15m"] * (1.0 - dev * 0.5)) &
(dataframe["high"] <= dataframe["ema_fast_15m"] * (1.0 + dev * 2.0))
)
near_resistance = (
(dataframe["high"] >= dataframe["resistance_15m"] * (1.0 - dev)) &
(dataframe["high"] <= dataframe["resistance_15m"] * (1.0 + dev))
)
pullback_short = near_ema_short | near_resistance
# 条件3K线止涨信号
signal_short = dataframe["bearish_signal"]
# 综合入场
enter_short = trend_down & pullback_short & signal_short
short_recent = enter_short.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[enter_short & short_recent, "enter_short"] = 1
return dataframe return dataframe
# ===================== # ================================================================
# 出场信号 # exit_trendfreqtrade 2025.11 强制要求,即使 use_exit_signal=False
# ===================== # ================================================================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
exit_long = ~dataframe["trend_up_1d"].fillna(True) """出场完全由 custom_stoploss + custom_exit 管理。"""
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe return dataframe
# ===================== # ================================================================
# 动态止损基于1H S/R # 出场 — 止损ATR动态
# ===================== # ================================================================
def custom_stoploss( def custom_stoploss(
self, self,
@ -404,48 +276,240 @@ class StructureFlowStrategyV22d(IStrategy):
after_fill: bool, after_fill: bool,
**kwargs, **kwargs,
) -> float: ) -> float:
"""
止损 = 入场价 ± ATR × atr_mult_stop
- ATR值从入场K线锁定持仓期间不变
- 做多entry_price - (locked_atr × mult)
- 做空entry_price + (locked_atr × mult)
- 配20x杠杆ATR×1.0 ≈ 对应约 $3.7 止损当前5m ATR~$3.74
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0: if dataframe is None or len(dataframe) == 0:
return -0.02 if not trade.is_short else 0.02 return -0.02 if not trade.is_short else 0.02
last = dataframe.iloc[-1] entry_row = self._get_entry_row(dataframe, trade)
if entry_row is None:
return -0.02 if not trade.is_short else 0.02
atr = entry_row.get("atr", np.nan)
if pd.isna(atr) or atr <= 0:
return -0.02 if not trade.is_short else 0.02
mult = self.atr_mult_stop.value
if not trade.is_short: if not trade.is_short:
support = last.get("support", np.nan) sl_price = trade.open_rate - (atr * mult)
if pd.isna(support) or support <= 0: sl_ratio = (sl_price / trade.open_rate) - 1.0
return -0.02 return max(sl_ratio, -self.stoploss)
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else: else:
resistance = last.get("resistance", np.nan) sl_price = trade.open_rate + (atr * mult)
if pd.isna(resistance) or resistance <= 0: sl_ratio = 1.0 - (sl_price / trade.open_rate)
return 0.02 return min(sl_ratio, self.stoploss)
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# ===================== # ================================================================
# Plot config # 出场 — 止盈ATR动态+ 时间止损
# ===================== # ================================================================
@staticmethod def custom_exit(
def plot_config() -> dict: self,
return { pair: str,
"main_plot": { trade: Trade,
"support": {"color": "green", "type": "line"}, current_time: datetime,
"resistance": {"color": "red", "type": "line"}, current_rate: float,
}, current_profit: float,
"subplots": { **kwargs,
"signals": { ) -> str | None:
"bullish_pinbar": {"color": "green", "type": "scatter"}, """
"bearish_pinbar": {"color": "red", "type": "scatter"}, 出场逻辑:
}, 1. ATR止盈利润达到入场时锁定的 ATR × atr_mult_tp → 止盈
"filters": { 2. 时间止损:持仓超过 time_stop_minutes → 强制出场
"support_alive": {"color": "green", "type": "line"}, """
"resistance_alive": {"color": "red", "type": "line"}, dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
"strong_uptrend_4h": {"color": "blue", "type": "line"}, if dataframe is None or len(dataframe) == 0:
"strong_downtrend_4h": {"color": "orange", "type": "line"}, return None
},
}, entry_row = self._get_entry_row(dataframe, trade)
} if entry_row is None:
return None
atr = entry_row.get("atr", np.nan)
if pd.isna(atr) or atr <= 0:
return None
# 1. ATR 止盈
tp_mult = self.atr_mult_tp.value
tp_ratio = (atr * tp_mult) / trade.open_rate
if current_profit >= tp_ratio:
return "atr_tp"
# 2. 时间止损
elapsed = (current_time - trade.open_date).total_seconds() / 60.0
if elapsed >= self.time_stop_minutes:
return "time_stop"
return None
# ================================================================
# 工具函数
# ================================================================
def _detect_swing_points(
self, highs: list, lows: list, window: int = 5
):
"""
Swing High / Swing Low 检测。
当一根K线的最高价高于其两侧window根K线的最高价时标记为Swing High。
Swing Low同理。
"""
n = len(highs)
swing_high = [np.nan] * n
swing_low = [np.nan] * n
for i in range(window, n - window):
# Swing High
is_high = True
for j in range(i - window, i + window + 1):
if j == i:
continue
if highs[j] >= highs[i]:
is_high = False
break
if is_high:
swing_high[i] = highs[i]
# Swing Low
is_low = True
for j in range(i - window, i + window + 1):
if j == i:
continue
if lows[j] <= lows[i]:
is_low = False
break
if is_low:
swing_low[i] = lows[i]
return swing_high, swing_low
def _build_structure(
self, highs: list, lows: list, closes: list,
swing_high: list, swing_low: list,
):
"""构建趋势结构和支撑/阻力位。"""
n = len(highs)
trend_up = [False] * n
trend_down = [False] * n
support = [np.nan] * n
resistance = [np.nan] * n
# 用最近4个swing point的位置判断
last_sh_idx = -1
last_sl_idx = -1
prev_sh = []
prev_sl = []
for i in range(n):
if not np.isnan(swing_high[i]):
prev_sh.append(swing_high[i])
last_sh_idx = i
if len(prev_sh) > 4:
prev_sh.pop(0)
if not np.isnan(swing_low[i]):
prev_sl.append(swing_low[i])
last_sl_idx = i
if len(prev_sl) > 4:
prev_sl.pop(0)
# 趋势判断最新的HH > 次新的HH = 上升趋势中的higher high
if len(prev_sh) >= 2 and prev_sh[-1] > prev_sh[-2]:
trend_up[i] = True
# 趋势判断最新的LL < 次新的LL = 下降趋势中的lower low
if len(prev_sl) >= 2 and prev_sl[-1] < prev_sl[-2]:
trend_down[i] = True
# 支撑 = 最近的有效Swing LowEMA平滑后在调用侧处理
if prev_sl:
support[i] = prev_sl[-1]
if prev_sh:
resistance[i] = prev_sh[-1]
return trend_up, trend_down, support, resistance
def _ema_smooth(self, values: list, alpha: float = 0.3):
"""对数组做EMA平滑避免跳变。"""
result = [np.nan] * len(values)
ema = None
for i, v in enumerate(values):
if pd.isna(v) or v is None:
if ema is not None:
result[i] = ema
continue
if ema is None:
ema = v
else:
ema = alpha * v + (1 - alpha) * ema
result[i] = ema
return np.array(result)
def _detect_candle_patterns(
self, opens, highs, lows, closes, wick_ratio=0.6,
):
"""检测K线形态pinbar锤子线/射击星)和吞没形态。"""
n = len(opens)
bullish_pin = [False] * n
bearish_pin = [False] * n
bullish_engulf = [False] * n
bearish_engulf = [False] * n
for i in range(n):
o, h, l, c = opens[i], highs[i], lows[i], closes[i]
total_range = h - l if h > l else 0.001
is_bullish = c > o
is_bearish = c < o
body = abs(c - o)
upper_wick = h - max(c, o)
lower_wick = min(c, o) - l
# Pinbar影线 > total_range × wick_ratio
if is_bullish and lower_wick / total_range > wick_ratio:
bullish_pin[i] = True
if is_bearish and upper_wick / total_range > wick_ratio:
bearish_pin[i] = True
# 吞没形态
if i > 0:
prev_o = opens[i - 1]
prev_c = closes[i - 1]
if is_bullish and c > prev_o and o < prev_c:
bullish_engulf[i] = True
if is_bearish and c < prev_o and o > prev_c:
bearish_engulf[i] = True
return (
pd.Series(bullish_pin),
pd.Series(bearish_pin),
pd.Series(bullish_engulf),
pd.Series(bearish_engulf),
)
def _get_entry_row(self, dataframe: DataFrame, trade: Trade):
"""查找入场K线行兼容live/backtesting两种模式。"""
if "date" in dataframe.columns:
entry_mask = pd.to_datetime(dataframe["date"]) <= trade.open_date
if not entry_mask.any():
return None
return dataframe[entry_mask].iloc[-1]
else:
try:
idx = dataframe.index.get_indexer([trade.open_date], method="pad")
if idx[0] < 0 or idx[0] >= len(dataframe):
return None
return dataframe.iloc[idx[0]]
except (TypeError, ValueError):
return None