10 Commits
v2.2d ... main

33 changed files with 13998 additions and 371 deletions

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# 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
## 仓库结构
```
.
├── strategy.py # 当前最新策略v2.2d
├── indicators.py # Dashboard 后端指标计算
├── config.backtest.json # 标准化回测配置
├── ablation/ # v2.1 消融实验8组 + 基线)
├── backtest/ # 关键回测结果摘要
├── docs/ # 策略白皮书、技术文档
├── dashboard/ # Dashboard 后端
└── configs/ # 各版本配置文件
```
## 铁律
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"},
},
},
}

39
backtest/INDEX.md Normal file
View File

@ -0,0 +1,39 @@
# Backtest Results Index
## Today&apos;s Backtests (2026-06-08)
| 文件 | 策略 | 时间范围 | 周期 | 大小 | 备注 |
|------|------|----------|------|------|------|
| backtest-result-2026-06-08_00-04-25.meta.json | StructureFlowStrategyV16 | 2022-01-01~2025-08-17 | 2022-01-01~2025-08-17 | 511KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-04-50.meta.json | StructureFlowStrategyV16 | 2022-01-01~2025-08-17 | 2022-01-01~2025-08-17 | 517KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-08-01.meta.json | StructureFlowStrategyV16 | 2022-01-01~2025-08-17 | 2022-01-01~2025-08-17 | 511KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-10-04.meta.json | StructureFlowStrategyV16 | 2022-01-01~2025-08-17 | 2022-01-01~2025-08-17 | 517KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-10-14.meta.json | StructureFlowStrategyV16 | 2022-01-01~2025-08-17 | 2022-01-01~2025-08-17 | 511KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-07-46.meta.json | StructureFlowStrategyV16 | 2022-01-01~2023-01-01 | 2022年度 | 149KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-07-45.meta.json | StructureFlowStrategyV16 | 2023-01-01~2024-01-01 | 2023年度 | 146KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-07-48.meta.json | StructureFlowStrategyV16 | 2023-01-01~2024-01-01 | 2023年度 | 143KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-07-49.meta.json | StructureFlowStrategyV16 | 2024-01-01~2025-01-01 | 2024年度 | 149KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-00-50.meta.json | StructureFlowStrategyV16 | 2025-01-01~2025-08-17 | 2025-01-01~2025-08-17 | 98KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-09-34.meta.json | StructureFlowStrategyV16 | 2025-01-01~2025-08-17 | 2025-01-01~2025-08-17 | 99KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-09-36.meta.json | StructureFlowStrategyV16 | 2025-01-01~2025-08-17 | 2025-01-01~2025-08-17 | 98KB | ✅ 基线版本 |
| backtest-result-2026-06-08_00-25-02.meta.json | StructureFlowStrategyV18 | 2022-01-01~2026-06-07 | 全周期 | 611KB | ⚠️ 对比用 (2%缓冲) |
| backtest-result-2026-06-08_00-25-05.meta.json | StructureFlowStrategyV18 | 2022-01-01~2026-06-07 | 全周期 | 615KB | ⚠️ 对比用 (2%缓冲) |
| backtest-result-2026-06-08_00-17-35.meta.json | StructureFlowStrategyV17 | 2022-01-01~2026-06-07 | 全周期 | 607KB | ❌ 失败 (止损太宽) |
| backtest-result-2026-06-08_14-50-07.meta.json | StructureFlowStrategyV161 | 2022-01-01~2026-06-07 | 全周期 | - | ❌ v1.6.1 过度过滤 |
| backtest-result-2026-06-08_14-50-41.meta.json | StructureFlowStrategyV161 | 2022-01-01~2026-06-07 | 全周期 | - | ❌ v1.6.1 过度过滤 |
| backtest-result-2026-06-08_15-07-41.meta.json | StructureFlowStrategyV162 | 2022-01-01~2026-06-07 | 全周期 | - | ❌ v1.6.2 Brooks二次确认 |
| backtest-result-2026-06-08_07-22-32.zip | StructureFlowStrategyV163 | 2022-01-01~2026-06-07 | 全周期 | 622KB | ❌ v1.6.3 H4过滤器误杀盈利 |
| backtest-result-2026-06-08_08-45-17.zip | StructureFlowStrategyV21 | 2022-01-01~2026-06-07 | 全周期 (ETH, Binance Futures) | 627KB | ⭐ v2.1 D1趋势强度 ETH+304.31% |
| backtest-result-2026-06-08_08-46-37.zip | StructureFlowStrategyV21 | 2022-01-01~2026-06-07 | 全周期 (BTC, Binance Futures) | 632KB | ⭐ v2.1 D1趋势强度 BTC+95.96% |
| backtest-result-2026-06-08_08-45-58.zip | StructureFlowStrategyV16 | 2022-01-01~2026-06-07 | 全周期 (ETH, Binance Futures) | 628KB | v1.6 基线 ETH+282.27% (同环境对比) |
| backtest-result-2026-06-08_08-47-14.zip | StructureFlowStrategyV16 | 2022-01-01~2026-06-07 | 全周期 (BTC, Binance Futures) | 629KB | v1.6 基线 BTC+90.92% (同环境对比) |
**v2.1 vs v1.6 同环境对比 (Binance Futures, 2022-2026):**
- ETH: v1.6 +282.27% → v2.1 +304.31% (+7.8%相对提升)
- BTC: v1.6 +90.92% → v2.1 +95.96% (+5.5%相对提升)
- 两者PF/CAGR/DD均改善v2.1全面超越v1.6
**⚠️ 数据源说明:**
- 之前的v1.6回测(3659.63%)用的是OKX Spot数据fee结构不同
- 今日v2.1回测用的是Binance Futures数据0.05% fee
- 两种数据源下v2.1都超越v1.6,趋势一致

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,435 @@
"""
Structure Flow Strategy v1.6
=======================
变更记录:
v1.0 (2026-06-07): 纯价格结构策略D1定方向→4H定位→1H入场
v1.1 (2026-06-07): 1H futures结构止损首次回测成功(+61.52%)
v1.2 (2026-06-07): Entry Candle止损bug导致50笔硬止损全亏
v1.3 (2026-06-07): ATR动态止损结果-63.72%胜率20.2%
v1.4 (2026-06-07): 回归纯价格结构止损,+140.71%胜率38.7%
v1.5 (2026-06-07): 参数调优(stoploss -5%→-15%, max_stop_dist 3%→5%)+140.83%
v1.6 (2026-06-07): ===== 入场质量优化 =====
- 6-bar冷却期信号后6h内不重复入场防止连挨多刀
- 活支撑/阻力检查S/R必须被最近测试并守住才算有效
设计原则:不降频,只砍最差的那几笔重复入场
"""
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 StructureFlowStrategyV16(IStrategy):
"""
Structure Flow Strategy v1.6 — 纯价格结构,零指标
v1.6改动相对于v1.5
1. 6-bar冷却期同方向信号触发后6h内禁止同向再入场
→ 解决"同一天同一个价位挨两刀"的问题
2. 活支撑/阻力检查4H Swing Point 必须被价格测试并守住才有效
→ 解决"在死支撑上入场"的问题
"""
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")
# v1.6 新增
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# =====================
# 工具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 新增:活支撑/阻力检查
# ================================
# 支撑"活"的条件在最近3根4H bar内low 触及 support ±0.5%
# 并且收盘价在支撑之上(即测试后撑住了)
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
# 在过去3根4H bar内有至少一次"测试并守住"
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
# 阻力"活"的条件high 触及 resistance ±0.5% 且 close 在阻力之下
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
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",
"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 时间框架)。
做多条件:
1. D1 上升结构trend_up_1d
2. 4H 需求区域in_demand_4h
3. 1H 看涨 K 线形态bullish_signal
4. 止损距离 ≤ max_stop_dist%
5. [v1.6] 支撑位是""support_alive_4h
6. [v1.6] 6h内没有过同方向入场信号冷却期
做空条件对称。
"""
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",
"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)
)
# v1.6: 活支撑 — 支撑必须在最近3根4H内被测试并守住
long_base = long_base & dataframe["support_alive_4h"]
# v1.6: 冷却期 — 过去N根1H bar内没有过满足条件的做多信号
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
long_conditions = long_base & long_recent
dataframe.loc[long_conditions, "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)
)
# v1.6: 活阻力 — 阻力必须在最近3根4H内被测试并守住
short_base = short_base & dataframe["resistance_alive_4h"]
# v1.6: 冷却期 — 过去N根1H bar内没有过满足条件的做空信号
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
short_conditions = short_base & short_recent
dataframe.loc[short_conditions, "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:
"""
止损逻辑:完全基于价格结构,零指标。
止损位:
做多 → support_4h - 0.1%缓冲最近4H Swing Low下方
做空 → resistance_4h + 0.1%缓冲最近4H Swing High上方
support_4h / resistance_4h 随新Swing Point自动更新
天然形成追踪止损效果。
"""
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"},
},
},
}

View File

@ -0,0 +1 @@
{"max_open_trades":1,"stake_currency":"USDT","stake_amount":"unlimited","tradable_balance_ratio":0.99,"fiat_display_currency":"USD","dry_run":true,"dry_run_wallet":10000,"trading_mode":"futures","margin_mode":"cross","liquidation_buffer":0.05,"exchange":{"name":"binance","key":"REDACTED","secret":"REDACTED","password":"REDACTED","ccxt_config":{"enableRateLimit":true},"ccxt_async_config":{"enableRateLimit":true},"pair_whitelist":["ETH/USDT:USDT"],"pair_blacklist":[]},"pairlists":[{"method":"StaticPairList"}],"telegram":{"enabled":false,"token":"REDACTED","chat_id":"REDACTED"},"api_server":{"enabled":false,"listen_ip_address":"0.0.0.0","listen_port":8080,"username":"freqtrader","password":"REDACTED","jwt_secret_key":"somethingRandom123"},"bot_name":"backtest","entry_pricing":{"price_side":"same","use_order_book":true,"order_book_top":1,"price_last_balance":0.0,"check_depth_of_market":{"enabled":false,"bids_to_ask_delta":1}},"exit_pricing":{"price_side":"same","use_order_book":true,"order_book_top":1},"config_files":["user_data/config_backtest.json"],"internals":{}}

File diff suppressed because one or more lines are too long

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") # 扫描更宽范围
# =====================
# 工具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

@ -0,0 +1 @@
{"max_open_trades":3,"stake_currency":"USDT","stake_amount":"unlimited","tradable_balance_ratio":0.99,"fiat_display_currency":"USD","dry_run":true,"dry_run_wallet":1000,"cancel_open_orders_on_exit":false,"trading_mode":"futures","margin_mode":"isolated","unfilledtimeout":{"entry":10,"exit":10,"exit_timeout_count":0,"unit":"minutes"},"entry_pricing":{"price_side":"same","use_order_book":true,"order_book_top":1,"price_last_balance":0.0,"check_depth_of_market":{"enabled":false,"bids_to_ask_delta":1}},"exit_pricing":{"price_side":"same","use_order_book":true,"order_book_top":1},"exchange":{"name":"binance","key":"REDACTED","secret":"REDACTED","ccxt_config":{"proxies":{"http":"http://host.docker.internal:7890","https":"http://host.docker.internal:7890"}},"ccxt_async_config":{"proxies":{"http":"http://host.docker.internal:7890","https":"http://host.docker.internal:7890"}},"pair_whitelist":["ETH/USDT:USDT"],"pair_blacklist":[]},"pairlists":[{"method":"StaticPairList"}],"bot_name":"freqtrade","initial_state":"running","internals":{"process_throttle_secs":5},"config_files":["/tmp/futures_config.json"]}

File diff suppressed because one or more lines are too long

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") # 扫描更宽范围
# =====================
# 工具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

@ -0,0 +1 @@
{"max_open_trades":3,"stake_currency":"USDT","stake_amount":"unlimited","tradable_balance_ratio":0.99,"fiat_display_currency":"USD","dry_run":true,"dry_run_wallet":1000,"cancel_open_orders_on_exit":false,"trading_mode":"futures","margin_mode":"isolated","unfilledtimeout":{"entry":10,"exit":10,"exit_timeout_count":0,"unit":"minutes"},"entry_pricing":{"price_side":"same","use_order_book":true,"order_book_top":1,"price_last_balance":0.0,"check_depth_of_market":{"enabled":false,"bids_to_ask_delta":1}},"exit_pricing":{"price_side":"same","use_order_book":true,"order_book_top":1},"exchange":{"name":"binance","key":"REDACTED","secret":"REDACTED","ccxt_config":{"proxies":{"http":"http://host.docker.internal:7890","https":"http://host.docker.internal:7890"}},"ccxt_async_config":{"proxies":{"http":"http://host.docker.internal:7890","https":"http://host.docker.internal:7890"}},"pair_whitelist":["ETH/USDT:USDT"],"pair_blacklist":[]},"pairlists":[{"method":"StaticPairList"}],"bot_name":"freqtrade","initial_state":"running","internals":{"process_throttle_secs":5},"config_files":["/tmp/futures_config.json"]}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
# v2.2d 逐年回测结果
**回测时间**: 2026-06-11 13:08
**策略**: StructureFlowStrategyV22d
**配置**: Docker freqtrade, config_backtest.json, ETH/USDT:USDT, futures, 1x, $10,000起
## 逐年表现(独立运行,每年 $10,000 起)
| 年份 | 交易数 | 收益率 | 终值 | 市场涨跌 | 胜率 | 最大回撤 |
|------|--------|--------|------|----------|------|----------|
| 2021 | 172 | +251.16% | $35,116 | +406.63% | 27.3% | 11.28% |
| 2022 | 204 | +110.91% | $21,091 | -67.92% | 30.9% | 11.69% |
| 2023 | 182 | +49.35% | $14,935 | +92.43% | 26.9% | 10.04% |
| 2024 | 232 | +185.84% | $28,584 | +46.38% | 28.4% | 6.87% |
| 2025 | 221 | +608.24% | $70,824 | -11.43% | 27.6% | 13.92% |
| 2026 | 54 | -11.87% | $8,813 | -45.37% | 22.2% | 14.89% |
**逐年合计**: 1,065笔独立运行不跨年复合
## 全周期2021-2026 连续运行)
| 交易数 | 总收益率 | 终值 | CAGR | Sharpe | 最大回撤 |
|--------|----------|------|------|--------|----------|
| 1,375 | +205,684.36% | $20,578,436 | 309.01% | 1.03 | 20.58% |
## 关键观察
1. **熊市表现优异**: 2022市场-68%,策略+111%2026YTD市场-45%,策略仅-12%
2. **牛市相对逊色**: 2021市场+407%,策略+251%2023市场+92%,策略+49%
3. **2025是爆发年**: +608%主因2025年ETH波动大先跌后涨双向策略充分获利
4. **逐年回撤控制在7-15%**比全周期20.58%低,因逐年重置不积累
5. **逐年笔数稳定**: 172-232笔/年2026仅半年54笔日均0.5-0.6笔

385
dashboard/indicators.py Normal file
View File

@ -0,0 +1,385 @@
"""
指标计算模块 — 从 v2.2b 策略提取的独立版本
用于 APP 后端实时计算市场结构和信号诊断
"""
import numpy as np
import pandas as pd
def detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
"""检测 Swing High / Swing Low与 v2.2b 策略一致"""
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(
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> pd.DataFrame:
"""构建价格结构趋势方向、S/R 位、供需区),与 v2.2b 一致"""
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 pd.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)
def compute_trend_strength(
high: pd.Series,
low: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
min_strength: float = -0.20,
) -> pd.DataFrame:
"""计算趋势强度4H Swing Point 间距变化),与 v2.2b 一致"""
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(high), np.nan)
trend_strength_down = np.full(len(high), np.nan)
strong_uptrend = np.full(len(high), False)
strong_downtrend = np.full(len(high), False)
for i in range(len(high)):
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:
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)
strong_uptrend = pd.Series(trend_strength_up) > min_strength
strong_downtrend = pd.Series(trend_strength_down) > min_strength
return pd.DataFrame({
"trend_strength_up": pd.Series(trend_strength_up, index=high.index),
"trend_strength_down": pd.Series(trend_strength_down, index=high.index),
"strong_uptrend": strong_uptrend.values,
"strong_downtrend": strong_downtrend.values,
}, index=high.index)
def compute_support_alive(
low: pd.Series,
close: pd.Series,
support: pd.Series,
) -> pd.Series:
"""检查支撑是否活着(最近测试过没跌破),与 v2.2b 一致"""
touched = (
(low <= support * 1.005) &
(low >= support * 0.995)
)
held = close > support
tested_and_held = touched & held
return tested_and_held.rolling(3, min_periods=1).max() > 0
def compute_resistance_alive(
high: pd.Series,
close: pd.Series,
resistance: pd.Series,
) -> pd.Series:
"""检查阻力是否活着(最近测试过没突破),与 v2.2b 一致"""
touched = (
(high >= resistance * 0.995) &
(high <= resistance * 1.005)
)
held = close < resistance
tested_and_held = touched & held
return tested_and_held.rolling(3, min_periods=1).max() > 0
def detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> dict:
"""检测 K 线形态,与 v2.2b 一致"""
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_pinbar": bullish_pin,
"bearish_pinbar": bearish_pin,
"bullish_signal": bullish_pin | bullish_engulf,
"bearish_signal": bearish_pin | bearish_engulf,
}
def compute_all_indicators(
df_1h: pd.DataFrame,
df_4h: pd.DataFrame,
df_1d: pd.DataFrame,
params: dict = None,
) -> dict:
"""
完整计算所有指标的入口函数
返回 APP 需要的全部数据
params 可选参数:
swing_lookback_d1: int (default 5)
swing_lookback_h4: int (default 8)
pin_bar_wick_ratio: float (default 0.6)
max_stop_dist: float (default 0.50 = 5%)
trend_strength_min: float (default -0.20)
"""
if params is None:
params = {}
swing_lookback_d1 = params.get("swing_lookback_d1", 5)
swing_lookback_h4 = params.get("swing_lookback_h4", 8)
swing_lookback_1h = params.get("swing_lookback_1h", 5)
pin_bar_wick_ratio = params.get("pin_bar_wick_ratio", 0.6)
max_stop_dist = params.get("max_stop_dist", 0.50)
trend_strength_min = params.get("trend_strength_min", -0.20)
# ---- D1 结构 ----
sh_d1, sl_d1 = detect_swing_points(df_1d["high"], df_1d["low"], swing_lookback_d1)
struct_d1 = build_structure(df_1d["high"], df_1d["low"], df_1d["close"], sh_d1, sl_d1)
trend_up_1d = bool(struct_d1["trend_up"].iloc[-1]) if len(struct_d1) > 0 else False
trend_down_1d = bool(struct_d1["trend_down"].iloc[-1]) if len(struct_d1) > 0 else False
# ---- 4H 结构(仅趋势强度) ----
sh_4h, sl_4h = detect_swing_points(df_4h["high"], df_4h["low"], swing_lookback_h4)
struct_4h = build_structure(df_4h["high"], df_4h["low"], df_4h["close"], sh_4h, sl_4h)
strength_4h = compute_trend_strength(
df_4h["high"], df_4h["low"], sh_4h, sl_4h,
min_strength=trend_strength_min,
)
strong_uptrend_4h = bool(strength_4h["strong_uptrend"].iloc[-1]) if len(strength_4h) > 0 else False
strong_downtrend_4h = bool(strength_4h["strong_downtrend"].iloc[-1]) if len(strength_4h) > 0 else False
trend_strength_up_val = float(strength_4h["trend_strength_up"].iloc[-1]) if len(strength_4h) > 0 else 0
trend_strength_down_val = float(strength_4h["trend_strength_down"].iloc[-1]) if len(strength_4h) > 0 else 0
# ---- 1H 结构S/R 适配 v2.2c ----
sh_1h, sl_1h = detect_swing_points(df_1h["high"], df_1h["low"], swing_lookback_1h)
struct_1h = build_structure(df_1h["high"], df_1h["low"], df_1h["close"], sh_1h, sl_1h)
last_1h_s = struct_1h.iloc[-1] if len(struct_1h) > 0 else None
support_1h = float(last_1h_s["support"]) if last_1h_s is not None and pd.notna(last_1h_s["support"]) else None
resistance_1h = float(last_1h_s["resistance"]) if last_1h_s is not None and pd.notna(last_1h_s["resistance"]) else None
in_demand_1h = bool(last_1h_s["in_demand"]) if last_1h_s is not None else False
in_supply_1h = bool(last_1h_s["in_supply"]) if last_1h_s is not None else False
# 1H 活 S/R 检查
support_alive_1h = False
resistance_alive_1h = False
if support_1h is not None:
support_alive_1h = bool(compute_support_alive(df_1h["low"], df_1h["close"], struct_1h["support"]).iloc[-1])
if resistance_1h is not None:
resistance_alive_1h = bool(compute_resistance_alive(df_1h["high"], df_1h["close"], struct_1h["resistance"]).iloc[-1])
# ---- 1H K线形态 ----
last_1h_candle = df_1h.iloc[-1] if len(df_1h) > 0 else None
candle = detect_candle_patterns(
df_1h["open"], df_1h["high"], df_1h["low"], df_1h["close"],
pin_bar_wick_ratio,
)
bullish_signal = bool(candle["bullish_signal"].iloc[-1]) if len(df_1h) > 0 else False
bearish_signal = bool(candle["bearish_signal"].iloc[-1]) if len(df_1h) > 0 else False
bullish_pinbar = bool(candle["bullish_pinbar"].iloc[-1]) if len(df_1h) > 0 else False
bearish_pinbar = bool(candle["bearish_pinbar"].iloc[-1]) if len(df_1h) > 0 else False
# ---- 入场距离1H S/R ----
current_price = float(df_1h["open"].iloc[-1]) if len(df_1h) > 0 else 0
long_stop_dist = None
short_stop_dist = None
if support_1h is not None and support_1h > 0:
long_stop_dist = abs((current_price - support_1h) / current_price)
if resistance_1h is not None and resistance_1h > 0:
short_stop_dist = abs((resistance_1h - current_price) / current_price)
long_dist_ok = long_stop_dist is not None and long_stop_dist <= max_stop_dist and long_stop_dist > 0.003
short_dist_ok = short_stop_dist is not None and short_stop_dist <= max_stop_dist and short_stop_dist > 0.003
# ---- Swing Point 历史1H 用于画图) ----
swing_highs_1h = df_1h[sh_1h.notna()].index.strftime("%Y-%m-%d %H:%M").tolist() if len(sh_1h) > 0 else []
swing_high_prices_1h = [float(x) for x in sh_1h.dropna().tolist()]
swing_lows_1h = df_1h[sl_1h.notna()].index.strftime("%Y-%m-%d %H:%M").tolist() if len(sl_1h) > 0 else []
swing_low_prices_1h = [float(x) for x in sl_1h.dropna().tolist()]
# ---- 信号诊断结果1H S/R ----
diagnosis = {
"market_price": round(current_price, 2),
"support": round(support_1h, 2) if support_1h else None,
"resistance": round(resistance_1h, 2) if resistance_1h else None,
"zone_width_pct": round(abs(resistance_1h - support_1h) / support_1h * 100, 2) if support_1h and resistance_1h else None,
"price_position_in_zone": round((current_price - support_1h) / (resistance_1h - support_1h) * 100, 1) if support_1h and resistance_1h and resistance_1h > support_1h else None,
}
filters = {
"trend_up_1d": {
"pass": trend_up_1d,
"desc": "D1 上升趋势HH+HL 都在抬高)",
"value": "上升" if trend_up_1d else ("下降" if trend_down_1d else "震荡"),
},
"in_demand_1h": {
"pass": in_demand_1h,
"desc": "价格在 1H 需求区zone 底部 35%",
"value": f"位置 {diagnosis['price_position_in_zone']}%" if diagnosis['price_position_in_zone'] is not None else "N/A",
},
"long_stop_distance": {
"pass": long_dist_ok,
"desc": f"入场距支撑距离 ({'%.2f' % (long_stop_dist*100) if long_stop_dist else 'N/A'}%)",
"value": f"{'%.2f' % (long_stop_dist*100)}%" if long_stop_dist else "N/A",
},
"support_alive_1h": {
"pass": support_alive_1h,
"desc": "1H 支撑最近被测试过且未跌破",
"value": "有效" if support_alive_1h else "失效/未测试",
},
"strong_uptrend_4h": {
"pass": strong_uptrend_4h,
"desc": "4H 趋势强度(扩张中)",
"value": f"{'%.2f' % (trend_strength_up_val*100)}%",
},
"trend_down_1d": {
"pass": trend_down_1d,
"desc": "D1 下降趋势LH+LL 都在降低)",
"value": "下降" if trend_down_1d else ("上升" if trend_up_1d else "震荡"),
},
"in_supply_1h": {
"pass": in_supply_1h,
"desc": "价格在 1H 供给区zone 顶部 65%+",
"value": f"位置 {diagnosis['price_position_in_zone']}%" if diagnosis['price_position_in_zone'] is not None else "N/A",
},
"short_stop_distance": {
"pass": short_dist_ok,
"desc": f"入场距阻力距离 ({'%.2f' % (short_stop_dist*100) if short_stop_dist else 'N/A'}%)",
"value": f"{'%.2f' % (short_stop_dist*100)}%" if short_stop_dist else "N/A",
},
"resistance_alive_1h": {
"pass": resistance_alive_1h,
"desc": "1H 阻力最近被测试过且未突破",
"value": "有效" if resistance_alive_1h else "失效/未测试",
},
"strong_downtrend_4h": {
"pass": strong_downtrend_4h,
"desc": "4H 下降趋势强度(扩张中)",
"value": f"{'%.2f' % (trend_strength_down_val*100)}%",
},
}
# 入场可行性结论1H S/R + 4H 趋势)
long_ok = all([
trend_up_1d, in_demand_1h, long_dist_ok,
support_alive_1h, strong_uptrend_4h,
])
short_ok = all([
trend_down_1d, in_supply_1h, short_dist_ok,
resistance_alive_1h, strong_downtrend_4h,
])
return {
"timestamp": pd.Timestamp.now().isoformat(),
"current_price": round(current_price, 2),
"diagnosis": diagnosis,
"filters": filters,
"can_enter_long": long_ok,
"can_enter_short": short_ok,
"trend_1d": "up" if trend_up_1d else ("down" if trend_down_1d else "neutral"),
"candle_1h": {
"bullish_pinbar": bullish_pinbar,
"bearish_pinbar": bearish_pinbar,
"bullish_signal": bullish_signal,
"bearish_signal": bearish_signal,
},
"swing_points": {
"highs": [{"time": t, "price": p} for t, p in zip(swing_highs_1h[-10:], swing_high_prices_1h[-10:])],
"lows": [{"time": t, "price": p} for t, p in zip(swing_lows_1h[-10:], swing_low_prices_1h[-10:])],
},
"trend_strength_4h": {
"up": round(trend_strength_up_val * 100, 2),
"down": round(trend_strength_down_val * 100, 2),
},
}

498
docs/backtest-pitfalls.md Normal file
View File

@ -0,0 +1,498 @@
# Freqtrade 回测部署踩坑记录
> 记录于 2026-06-07 | PriceActionStrategy v0.2 首次回测
> 环境Docker freqtrade (d:\ft_userdata\)ETH/USDTBinance
---
## 一、配置文件 (config.json) 的隐性必需字段
Freqtrade 的配置校验非常严格,即使某些模块设为 `disabled`,也**必须提供完整的字段结构**。
以下字段在首次部署时缺失,逐一触发报错:
### 1. telegram 模块
**错误**:只写了 `"enabled": false`,缺少 `token``chat_id`
```json
// ❌ 错误(会报 schema 校验失败)
"telegram": {
"enabled": false
}
// ✅ 正确(即使 disabled 也要提供空值)
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
}
```
### 2. api_server 模块
**错误**:只写了 `"enabled": false`
```json
// ❌ 错误
"api_server": {
"enabled": false
}
// ✅ 正确
"api_server": {
"enabled": false,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"username": "freqtrader",
"password": "password",
"jwt_secret_key": "somethingRandom123"
}
```
### 3. entry_pricing / exit_pricing
回测配置文件**必须显式声明**这两个模块。
```json
// ✅ 回测 config 必须包含
"entry_pricing": {
"price_side": "same",
"use_order_book": false,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": false,
"order_book_top": 1
}
```
### 4. ccxt_config 中的 proxies
在 Docker 环境下,`proxies` 字段会导致配置解析异常。**不要**在 ccxt_config 中加入代理配置,除非明确知道 freqtrade 版本支持。
```json
// ❌ 导致配置解析错误
"ccxt_config": {
"enableRateLimit": true,
"proxies": { ... }
}
// ✅ 正确的极简配置
"ccxt_config": {
"enableRateLimit": true
},
"ccxt_async_config": {
"enableRateLimit": true
}
```
---
## 二、策略文件的强制属性
Freqtrade 要求每个策略必须声明以下属性,否则回测直接失败:
```python
class PriceActionStrategy(IStrategy):
# ── 必需属性 ──
timeframe = "5m" # 主时间框架
can_short = True # 是否支持做空spot 模式必须为 False
stoploss = -0.10 # 硬止损比例(即使有 custom_stoploss 也要声明)
use_custom_stoploss = True # 是否使用动态止损
minimal_roi = {"0": 100} # 时间止盈表(不用就写 {"0": 100}
max_open_trades = 1 # 最大同时持仓数
# ── 必需方法 ──
def populate_indicators(self, dataframe, metadata): ...
def populate_entry_trend(self, dataframe, metadata): ...
def populate_exit_trend(self, dataframe, metadata): ...
```
| 属性 | 说明 | 缺失后果 |
|------|------|---------|
| `stoploss` | 硬止损比例,如 `-0.10` = 10% | 回测直接报错退出 |
| `minimal_roi` | 时间止盈表,不用则写 `{"0": 100}` | 回测直接报错退出 |
| `use_custom_stoploss` | 有 `custom_stoploss()` 方法时必须设为 True | 方法不被调用 |
### Spot vs Futures 的 can_short 差异
| 模式 | `can_short` | 说明 |
|------|------------|------|
| `trading_mode: "spot"` | **必须是 False** | 现货不支持做空 |
| `trading_mode: "futures"` | 可以 True | 合约支持双向 |
**最佳实践**:策略中保持 `can_short = True`(你的实盘方向),创建单独的回测配置指定 `trading_mode: "futures"` 来测完整逻辑。如果必须用现货数据跑 spot 回测,临时改为 False。
---
## 三、多时间框架合并的数据清洗陷阱
### 问题merge_informative_pair 导致 NaN
当使用 `merge_informative_pair()` 合并 D1/1H 数据到 5M 主框架时,**前 N 根 K 线的衍生列布尔值、EMA、Swing 点)全是 NaN**。
```python
# merge_informative_pair 返回的 dataframe 前面有 NaN 行
dataframe = merge_informative_pair(
dataframe, daily, self.timeframe, "1d", ffill=True,
)
# 结果:前几十行中 trend_up_1d、bullish_pinbar_1h 等列为 NaN
```
**错误表现**`ValueError: The truth value of a Series is ambiguous` 或 NaN 通过布尔条件传递导致崩溃。
**修复方案**:在 `populate_indicators()` 返回前,显式填充所有布尔列:
```python
def populate_indicators(self, dataframe, metadata):
# ... 所有指标计算 ...
# ⚠️ 关键:填充合并后的 NaN 布尔列
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_1h", "trend_down_1h",
"bullish_pinbar", "bearish_pinbar",
"bullish_engulfing", "bearish_engulfing",
"volume_surge",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False).infer_objects(copy=False)
return dataframe
```
### 列名冲突问题
`merge_informative_pair` 默认 `append_timeframe=True`(给合并的列加 `_1d``_1h` 后缀)。**不要**传 `append_timeframe=False`,会导致源列被覆盖。后续引用时记得带后缀:
```python
# ✅ 正确引用
dataframe["trend_up_1d"] # 来自日线的趋势判断
dataframe["trend_up_1h"] # 来自1H的趋势判断
dataframe["swing_low_1h"] # 来自1H的Swing Low
```
---
## 四、回测执行命令模板
```bash
cd d:/ft_userdata
docker compose run --rm freqtrade backtesting \
--config user_data/config_backtest.json \
--strategy PriceActionStrategy \
--timerange 20250101-
```
### 必备前置步骤
1. **策略文件已复制到 Docker 目录**
```bash
cp user_data/strategies/price_action_strategy.py \
d:/ft_userdata/user_data/strategies/PriceActionStrategy.py
```
2. **历史数据已下载**`d:/ft_userdata/user_data/data/binance/` 下有对应的 JSON 文件)
3. **回测配置文件 `config_backtest.json` 已就绪**,包含所有必需字段
---
## 五、完整的最小可行回测配置
```json
{
"max_open_trades": 1,
"stake_currency": "USDT",
"stake_amount": 10000,
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD",
"dry_run": true,
"trading_mode": "spot",
"margin_mode": "",
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {
"enableRateLimit": true
},
"ccxt_async_config": {
"enableRateLimit": true
}
},
"pairlists": [
{"method": "StaticPairList"}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"username": "freqtrader",
"password": "password",
"jwt_secret_key": "somethingRandom123"
},
"bot_name": "backtest",
"entry_pricing": {
"price_side": "same",
"use_order_book": false,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": false,
"order_book_top": 1
}
}
```
---
## 六、错误顺序时间线
按实际触发顺序排列,方便快速定位:
| 序号 | 错误 | 根因 | 修复 |
|------|------|------|------|
| 1 | 配置解析错误 | `ccxt_config` 中含 `proxies` 字段 | 移除 proxies |
| 2 | Schema 校验失败 | `telegram` 缺少 `token`/`chat_id` | 补全空值字段 |
| 3 | Schema 校验失败 | `api_server` 缺少完整结构 | 补全所有必需字段 |
| 4 | Schema 校验失败 | 缺少 `entry_pricing`/`exit_pricing` | 添加两个模块 |
| 5 | 策略加载失败 | spot 模式不支持 `can_short=True` | 临时设 False |
| 6 | 策略校验失败 | 缺少 `stoploss` 属性 | 添加 stoploss |
| 7 | 策略校验失败 | 有 `custom_stoploss()` 但未声明 `use_custom_stoploss` | 设为 True |
| 8 | 策略校验失败 | 缺少 `minimal_roi` | 添加 `{"0": 100}` |
| 9 | NaN 导致崩溃 | `merge_informative_pair` 后布尔列为 NaN | fillna(False) 清洗 |
| **✅** | **回测成功** | — | — |
---
## 七、Freqtrade 2026.2 Binance Futures 回测专用坑
### 问题:`Ticker pricing not available for Binance`
**触发条件**`trading_mode: "futures"` + `use_order_book: false`
**根因**`freqtrade/exchange/binance.py` 第 57 行:
```python
_ft_has_futures = {
"tickers_have_price": False, # ← Binance futures 不支持 ticker.price
}
```
`validate_pricing()` 检查 `self._ft_has["tickers_have_price"]`,若为 False 且 `use_order_book=false`,直接抛异常。
**修复方案**futures 模式下**必须**设置:
```json
"entry_pricing": { "use_order_book": true },
"exit_pricing": { "use_order_book": true }
```
### 问题:`SampleStrategy` 也无法运行 futures 回测
**确认**:不是策略问题,是 freqtrade 2026.2 的 Binance futures 初始化 Bug。
**解决方案**
1. `use_order_book: true`(绕过 ticker 检查)
2. 下载 mark price 数据:`download-data --trading-mode futures`
3. pair 名称必须带结算货币后缀:`ETH/USDT:USDT`
### 最小可行 futures 回测配置
```json
{
"trading_mode": "futures",
"margin_mode": "cross",
"liquidation_buffer": 0.05,
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {
"enableRateLimit": true,
"options": {"defaultType": "swap"}
},
"pair_whitelist": ["ETH/USDT:USDT"],
"pair_blacklist": []
},
"entry_pricing": {
"use_order_book": true, // ← futures 必须 true
"order_book_top": 1
},
"exit_pricing": {
"use_order_book": true, // ← futures 必须 true
"order_book_top": 1
}
}
```
### v1.1 回测结果(首次 futures 成功)
| 指标 | 数值 |
|------|------|
| 交易笔数 | 65 |
| 总盈亏 | +61.52% |
| 做多利润 | +5,193 USDT |
| 做空利润 | +959 USDT |
| 硬止损问题 | 31 笔全部亏损,-8,839 USDT |
**教训**futures 可以做空熊市不再被动挨打。但硬止损位置不对放在结构失效点太远v1.2 改为 Entry Candle 失效点。
| **✅** | **v1.1 futures 回测成功** | — | — |
---
## 八、v1.2 止损逻辑 Bug 分析(关键经验)
### v1.2 回测结果回顾
| 指标 | 数值 |
|------|------|
| 交易笔数 | 100 |
| 硬止损出场 | **50 笔,全部亏损**-8,468 USDT |
| trailing_stop 出场 | 46 笔34.8% 胜率,+13,671 USDT |
**50 笔硬止损全部亏损 = 止损逻辑根本性失效。**
### 根因 #1致命`return None` 退回到 25% 宽止损
```python
# v1.2 custom_stoploss 末尾(第 559 行)
return None # ← 致命错误!
```
`custom_stoploss` 返回 `None` 时freqtrade **忽略自定义止损,使用类属性 `stoploss`**。
v1.2 的 `stoploss = -0.25`25% 硬止损),导致所有找不到 Entry Candle 的交易都用 25% 宽止损,"硬扛单"。
**修复**`custom_stoploss` **永远不要返回 `None`**,始终返回显式止损比率。
### 根因 #2Entry Candle 查找逻辑脆弱
```python
# v1.2 的查找方式(第 507-519 行)
entry_mask = (
(dataframe["potential_entry_low"].notna())
| (dataframe["potential_entry_high"].notna())
)
entry_candidates = dataframe[
entry_mask
& (dataframe["date"] <= trade.open_date_utc + timedelta(hours=1))
& (dataframe["date"] >= trade.open_date_utc - timedelta(hours=1))
]
```
问题:
1. `potential_entry_low` 标记了**所有**信号 K 线,不是触发这笔交易的那个
2. ±1 小时窗口内有多个信号时,`iloc[-1]` 取的是最后一个,不一定正确
3. 时区稍有偏差(纳秒级)就找不到,直接掉进 `return None`
**结论**:在 `custom_stoploss` 里查找 Entry Candle 是不可靠的,必须换方法。
### 根因 #3阶段二 `pass` 后没有设止损
```python
# v1.2 第 528-539 行
if current_profit > self.profit_to_structure_sl_pct.value:
pass # 继续到阶段二
else:
return max(sl_ratio, -0.25)
# ← 如果阶段二条件不满足,函数一路走到 return None
```
盈利超阈值后跳到阶段二,但如果 `resistance_4h` / `support_4h` 为 NaN阶段二没有任何 `return`,最终落到 `return None`。
### v1.3 修复方案
**弃用 Entry Candle 查找,改用 ATR 动态止损**
```python
# v1.3 custom_stoploss 核心逻辑
atr = last_candle['atr_1h']
if not trade.is_short:
if current_profit <= 0.01:
sl_price = open_rate - atr * 1.0 # 阶段一:紧止损
elif current_profit <= 0.02:
sl_price = open_rate * 0.999 # 阶段二:保本
else:
sl_price = current_rate - atr * 1.0 # 阶段三:追踪止损
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.05) # 永不返回 None
```
**为什么用 ATR 而不是 Entry Candle**
- ATR 来自当前 K 线数据100% 可靠,不需要查找历史
- ATR 是波动率的自适应指标,低波动时止损紧,高波动时止损宽
- 符合"早早认输"哲学:价格反向跑 1 ATR 就止损,不硬扛
### 经验总结
| 规则 | 说明 |
|------|------|
| ❌ 永不 `return None` | 始终返回显式止损比率,宁可紧不要松 |
| ✅ 用 ATR 做动态止损 | 可靠、自适应、不依赖历史 K 线查找 |
| ✅ 三阶段设计 | 紧止损 → 保本 → 追踪,符合趋势交易逻辑 |
| ✅ `stoploss` 属性设安全网 | `-0.05`5%)防止极端行情,但 `custom_stoploss` 应永远先于此触发 |
| **✅** | **v1.3 止损逻辑重写完成** | — | — |
---
## 九、Docker 网络与代理环境(高频踩坑区)
### 核心事实
**freqtrade 2026.2 在启动回测时强制执行 `reload_markets`**,即使回测使用的全部是本地 OHLCV 数据。这意味着 **Docker 容器必须有外网访问能力**,否则任何操作都会在 `_load_async_markets` 阶段失败。
```
freqtrade.exceptions.TemporaryError: Error in reload_markets due to ExchangeNotAvailable.
Message: binance GET https://api.binance.com/api/v3/exchangeInfo
```
### 已验证可工作的 docker-compose.yml 配置
```yaml
services:
freqtrade:
environment:
- HTTP_PROXY=http://127.0.0.1:7890
- HTTPS_PROXY=http://127.0.0.1:7890
- NO_PROXY=localhost,127.0.0.1
- TZ=Asia/Shanghai
```
**关键点**
- `HTTP_PROXY` 和 `HTTPS_PROXY` **必须同时设置**,缺一不可
- `NO_PROXY` 中**不要包含 `api.binance.com`**——Binance 必须走代理
- 端口号 `7890` 是用户 Clash 代理的默认端口
### 故障排查清单
| 现象 | 诊断方法 | 常见原因 |
|------|---------|---------|
| `ExchangeNotAvailable` | `docker run --rm curlimages/curl curl --proxy http://127.0.0.1:7890 https://api.binance.com/api/v3/ping` | 代理服务未运行 |
| 容器连不上代理 | 从宿主机 `curl http://127.0.0.1:7890` | 代理端口不对或服务挂了 |
| 配置正确但连不上 | 检查 `NO_PROXY` 是否误包含 `api.binance.com` | Binance 被排除在代理外 |
### 经验教训
1. **每次跑回测前**确认代理服务Clash/V2Ray在运行
2. **不要随意修改 docker-compose.yml 的代理配置**——它一旦配置正确就不该动
3. **区分服务不可用 vs 配置错误**:同样配置之前能跑通现在不能 → 服务的锅不是配置的锅
4. **在踩坑文档中固化已验证的 docker-compose.yml**,不要每次手动改
| **✅** | **网络/代理经验固化完成** | — | — |

View File

@ -0,0 +1,198 @@
# v1.6 止损交易深度分析报告
> 分析时间2026-06-08 | 数据ETH/USDT 2022-01-01 ~ 2025-08-17 全周期回测
> 总交易 152 笔,其中 stop_loss 退出 73 笔(全部亏损,胜率 0%
---
## 一、核心发现
### 发现 1**50.7% 的止损交易在入场时 H4 趋势方向就不对!**
这是最震撼的发现。73 笔止损交易中,有 37 笔50.7%)在入场时 H4 级别趋势方向与交易方向不一致:
| 方向 | H4趋势不一致 | 典型场景 |
|------|:----------:|---------|
| LONG 止损 | 16/37 (43%) | D1看涨但4H正在下跌——在下跌中的回调做多 |
| SHORT 止损 | 21/36 (58%) | D1看跌但4H正在上涨——在上涨中的反弹做空 |
**这意味着:** 策略的入场逻辑有结构性缺陷。当前入场条件只检查 D1 趋势方向,但**没有要求 H4 趋势与交易方向一致**。
### 案例说明
```
#2: 2022-04-07 LONG
D1趋势=UP ✓ → "大方向对"
H4趋势=DOWN ✗ → "但4H正在跌"
结果:支撑被击穿,-1.6%
#48: 2024-05-27 SHORT
D1趋势=UP ✗ → "D1就不对"
H4趋势=UP ✗ → "4H也在涨"
结果3小时止损-1.0%
```
### 发现 2**26 笔35.6%)止损是因为支撑/阻力被直接击穿**
- LONG: 13 笔价格跌破支撑PRICE_BROKE_SUPPORT
- SHORT: 13 笔价格突破阻力PRICE_BROKE_RESISTANCE
这些交易的特点是:入场时一切条件满足,但随后价格直接穿过 S/R 位。这是"真结构被否定"的情况——市场决定这个 S/R 不再有效。
### 发现 3**15 笔20.5%)止损是因为 S/R 位被新的 Swing Point 推着走**
- 支撑被新的 Swing Low 向下推移LONG 做多 → 止损位下移 → 更大的亏损空间)
- 阻力被新的 Swing High 向上推移SHORT 做空 → 止损位上移 → 更大的亏损空间)
典型如 #13 (2022-09-19):支撑位在交易期间**下移了 4.6%**,说明市场在持续创新低。
### 发现 4**"活支撑/阻力"过滤器漏了 4 笔死支撑**
v1.6 的 `support_alive` 过滤器只捕捉到最近 3 根 4H bar 内的测试,但仍漏了 4 笔5.5%)。这些支撑在入场时就没被验证过。
### 发现 5**快止损 vs 慢流血 — 两种不同的失败模式**
| 类型 | 数量 | 平均亏损 | 特征 |
|------|:----:|:-------:|------|
| 快止损 (<6h) | 11笔 | -1.14% | 入场即错价格立即反向 |
| 慢流血 (>72h) | 19笔 | -2.21% | 价格先小幅有利,然后趋势反转 |
快止损大部分是 H4 趋势不一致导致(如 #48#52)。慢流血则更复杂——有的是 S/R 位被慢慢侵蚀,有的是趋势反转。
---
## 二、分类统计
### 按根因分类
| 根因 | 数量 | 占比 | 可修复性 |
|------|:----:|:----:|:-------:|
| **H4趋势不一致** | 37 | 50.7% | ✅ 高 — 加入 H4 趋势过滤器 |
| **价格直接击穿S/R** | 26 | 35.6% | ⚠️ 中 — 需要更强的S/R确认 |
| **S/R位移** | 15 | 20.5% | ⚠️ 中 — 部分可提前识别 |
| **不在正确区域** | 12 | 16.4% | ✅ 高 — 收紧区域判断 |
| **死支撑/阻力** | 4 | 5.5% | ✅ 已过滤v1.6已解决大部分) |
| **不明原因** | 8 | 11.0% | 🔬 需要逐笔人工审查 |
> 注:一笔交易可能有多重根因,总和超过 100%。
### 按年度分布
| 年份 | 止损数 | 总亏损 | 平均亏损 | 市场环境 |
|------|:-----:|:------:|:------:|:-------:|
| 2022 | 21 | -$4,516 | -2.56% | 熊市 |
| 2023 | 20 | -$5,030 | -1.58% | 恢复期 |
| 2024 | 18 | -$21,772 | -2.16% | 牛市 |
| 2025 | 14 | -$36,062 | -2.14% | 震荡市 |
> 2025 年止损亏损金额最大,因为本金滚大了(复利效应),但平均亏损%与其他年份相当。
---
## 三、可修复性评估
### ✅ 高优先级(大概率能修复)
**1. 加入 H4 趋势一致性过滤器**
这是最大的单一改进机会。当前逻辑:
```python
# 当前:只检查 D1 趋势
long_base = trend_up_1d & in_demand_4h & bullish_signal
# 改进:同时要求 H4 趋势一致
long_base = trend_up_1d & trend_up_4h & in_demand_4h & bullish_signal
```
**预期效果:** 可能过滤掉 37 笔中的大部分(但需要评估是否会误杀盈利交易)。
**2. 收紧区域判断**
当前 `in_demand` = 价格在支撑-阻力区间的下 35%。12 笔止损交易不在正确区域就入场了。可以:
- 收紧到 25% 或 20%
- 或要求价格更接近支撑/阻力位
### ⚠️ 中优先级(部分可修复)
**3. 强化 S/R 确认**
26 笔"价格击穿 S/R"中,有些可能是支撑/阻力本身质量不高。可以:
- 要求 Swing Point 被测试 ≥2 次才有效
- 要求 S/R 位形成时间 > N 根 bar避免新形成的、未被验证的
- 考虑 S/R 位的"级别"——日线 Swing Point 比 4H Swing Point 更重要
**4. S/R 位移预警**
15 笔 S/R 位移中,如果在交易期间检测到"S/R 位正在移动"(新 Swing Point 形成),可以提前退出而非等到止损触发。
### 🔬 需要进一步研究
**5. "不明原因"的 8 笔**
这 8 笔交易(#5, #16, #19, #29, #36, #44, #66, #69看起来一切条件都对——H4 趋势对、D1 趋势对、在正确区域、支撑有效——但还是止损了。需要逐笔查看当时的 K 线图来理解。这可能是策略的"固有成本"。
---
## 四、UNFAIR 问题(结构性缺陷)
### 问题 1D1 和 H4 的脱节
当前策略的核心假设是"D1 定方向4H 找位置",但实际执行中:
- D1 趋势变化很慢(几天到几周)
- H4 趋势变化很快(几小时到几天)
- 在 D1 上涨但 H4 下跌的窗口期(回调),策略会做多 → 然后被 H4 下跌趋势吞噬
**本质:** D1 趋势对 1H 入场来说太"远"了,中间的 4H 趋势才是 1H 入场最近的上层结构。
### 问题 2S/R 识别只看最近 4 个 Swing Point
`_build_structure` 只保留最近 4 个 Swing High/Low 来判断趋势。这可能导致:
- 在更大时间框架的结构位(比如日线级别的关键支撑)被忽略
- 趋势判断过于短视
### 问题 3custom_stoploss 的动态更新是一把双刃剑
当新的 Swing Low 形成时,`support_4h` 会更新为新低点,止损位下移 → 持仓承受更大的浮亏。这就是 S/R 位移的根源。
---
## 五、改进路线图
### 阶段 1H4 趋势过滤器(预计影响最大)
```
v1.6.3: 加入 H4 趋势一致性要求
- LONG: D1趋势=UP AND H4趋势=UP
- SHORT: D1趋势=DOWN AND H4趋势=DOWN
- 回测验证:是否会误杀太多盈利交易?
```
### 阶段 2S/R 质量提升
```
v1.6.4: 增强 S/R 确认
- 要求 Swing Point 至少被回测 1 次
- 要求 S/R 形成时间 ≥ 2 根 4H bar
- 优先使用 D1 级别的 Swing Point如果存在
```
### 阶段 3动态退出优化
```
v1.6.5: S/R 位移时的提前退出
- 当新 Swing Point 形成且方向不利时 → 提前 exit
- 而非等待价格触及已移动的止损位
```
---
## 六、数据文件
| 文件 | 说明 |
|------|------|
| `bt_v16_stop_losses.csv` | 73 笔止损交易的基础数据 |
| `bt_v16_stop_loss_analysis.csv` | 含结构快照和分类的完整分析数据 |
---
*这份报告是接下来所有止损优化的基础。每个改进方向都可以独立验证、独立回测。*

View File

@ -0,0 +1,735 @@
# Structure Flow Strategy v2.1 — 策略白皮书
> **版本**: v2.1 | **日期**: 2026-06-08 | **状态**: 生产 dry-run
>
> 本文档面向策略使用者本人,用于深入理解策略逻辑、回测表现和设计思路。
---
## 目录
1. [策略概览](#1-策略概览)
2. [理论基础](#2-理论基础)
3. [时间框架架构](#3-时间框架架构)
4. [核心技术Swing Point 检测](#4-核心技术swing-point-检测)
5. [结构分析:趋势与支撑阻力](#5-结构分析趋势与支撑阻力)
6. [K线形态入场触发器](#6-k线形态入场触发器)
7. [入场逻辑详解](#7-入场逻辑详解)
8. [止损逻辑](#8-止损逻辑)
9. [出场逻辑](#9-出场逻辑)
10. [v2.1 核心创新:趋势强度过滤](#10-v21-核心创新趋势强度过滤)
11. [参数说明](#11-参数说明)
12. [回测表现](#12-回测表现)
13. [演进历史](#13-演进历史)
14. [已知局限](#14-已知局限)
15. [部署信息](#15-部署信息)
---
## 1. 策略概览
| 项目 | 值 |
|:------|:------|
| 策略名称 | StructureFlowStrategyV21 |
| 策略类型 | 中低频趋势跟踪(价格行为学) |
| 品种 | ETH/USDT 永续合约 (Binance Futures) |
| 杠杆 | 1x不上杠杆 |
| 主时间框架 | 1H |
| 辅助时间框架 | 4H, 1D |
| 持仓周期 | 数小时 ~ 数天 |
| 日均交易 | 0.1 ~ 0.15 笔 |
| 交易方向 | 多空双向 |
| 入场依据 | 顺势 + S/R位 + K线反转信号 |
| 止损方式 | 基于价格结构的动态追踪止损 |
| 出场方式 | D1趋势反转 + trailing stop |
### 核心理念
> **反转大多失败。在趋势中,于支撑/阻力位顺势入场,用追踪止损让利润奔跑。**
策略不预测市场方向,只做一件事:**在明确的趋势中,等待价格回到关键 S/R 位,确认反转信号后入场,然后用结构止损保护仓位,让趋势把利润带走。**
---
## 2. 理论基础
### 2.1 价格行为学Price Action
策略的核心框架来自经典价格行为学,尤其是 Dow Theory 和 Wyckoff 方法:
- **市场结构Market Structure**:通过 HH/HLHigher High / Higher Low和 LH/LLLower High / Lower Low判断趋势方向
- **Swing Point**:局部极值点,构成结构的"骨架"
- **支撑/阻力S/R**Swing Point 形成的价格水平,是市场参与者集体记忆的位置
- **价格区域Demand/Supply Zone**S/R 附近的区域,而非精确价位
### 2.2 为什么不用指标
策略刻意**不使用任何技术指标**EMA、MACD、RSI、ATR 等),原因:
1. **指标是价格的衍生品**——滞后于价格本身
2. **指标参数需要调优**——容易过拟合
3. **价格结构是本源信息**——HH/HL/LH/LL 直接告诉你市场在做什么
4. **指标在不同市场环境表现不一致**——牛市有效的参数熊市失效
唯一的例外是 K线形态Pin Bar、Engulfing它们本质上是价格行为的可视化表达不是衍生计算。
### 2.3 多时间框架分析
自上而下的分析框架:
```
D1 (日线) → 判断宏观趋势方向
4H (4小时) → 定位中期 S/R 位 + 评估趋势强度
1H (1小时) → 寻找入场信号K线形态 + S/R 位确认)
```
这种"大周期定方向、中周期定位置、小周期定时机"的方式,是专业交易员的标准做法,也是策略区别于单纯"看1H做1H"的核心优势。
---
## 3. 时间框架架构
策略使用 freqtrade 的 `@informative` 装饰器加载三个时间框架:
```
┌─────────────────────────────────────────┐
│ 1D (日线) │
│ populate_indicators_1d() │
│ → Swing Point 检测 │
│ → 结构分析trend_up / trend_down
│ → 输出到 1H: trend_up_1d, trend_down_1d │
└──────────────┬──────────────────────────┘
│ 宏观趋势方向
┌──────────────▼──────────────────────────┐
│ 4H (4小时) │
│ populate_indicators_4h() │
│ → Swing Point 检测 │
│ → 结构分析trend + S/R + zone
│ → 活支撑/阻力检查 │
│ → ★ v2.1 趋势强度评估 │
│ → 输出到 1H: *_4h 系列字段 │
└──────────────┬──────────────────────────┘
│ S/R位置 + 趋势强度
┌──────────────▼──────────────────────────┐
│ 1H (1小时) │
│ populate_indicators() │
│ → K线形态检测Pin Bar, Engulfing
│ → 合并所有时间框架数据 │
│ populate_entry_trend() │
│ → 综合判断入场信号 │
│ custom_stoploss() │
│ → 动态结构止损 │
└─────────────────────────────────────────┘
```
三个时间框架的数据通过 freqtrade 自动 merge 到 1H 主时间框架,列名会自动加后缀(如 `trend_up_1d`, `support_4h`)。
---
## 4. 核心技术Swing Point 检测
### 4.1 算法
`_detect_swing_points()` 函数实现了经典的 Swing Point 检测:
```python
def _detect_swing_points(high, low, window=5):
# Swing High: 当前bar的high大于前后各window根bar的所有high
# Swing Low: 当前bar的low小于前后各window根bar的所有low
```
**参数含义**
- `window=10`D1前后各10根日线约2周窗口
- `window=8`4H前后各8根4H线约1.3天窗口
**关键特性**
- 对称窗口——左右各 `window` 根 bar
- 严格比较——必须是严格大于/小于,等于不算
- 不依赖任何指标——纯价格极值比较
### 4.2 为什么窗口大小重要
| 窗口太小 | 窗口太大 |
|:------|:------|
| 噪音多,假 Swing Point 多 | 信号迟钝,错过转折 |
| 结构频繁切换 | 结构变化滞后 |
当前参数D1=10, 4H=8是回测调优的结果在灵敏度和稳定性之间取得了平衡。
### 4.3 可视化理解
```
价格
│ SH₁ SH₂
╲ SH₃
│╱ ╲╱ ╲ ╲___
│ SL₁ SL₂ SL₃
└─────────────────────────────────→ 时间
SH = Swing High局部高点
SL = Swing Low局部低点
趋势判断:
SH₂ > SH₁ 且 SL₂ > SL₁ → 上升趋势HH + HL
SH₃ < SH₂ 且 SL₃ < SL₂ → 下降趋势LH + LL
```
---
## 5. 结构分析:趋势与支撑阻力
### 5.1 `_build_structure()` 函数
这个函数是策略的"大脑",接收 Swing Point 数据,输出以下信息:
| 输出字段 | 含义 |
|:------|:------|
| `trend_up` | 当前是否处于上升趋势HH + HL |
| `trend_down` | 当前是否处于下降趋势LH + LL |
| `support` | 最近的 Swing Low 价格(支撑位) |
| `resistance` | 最近的 Swing High 价格(阻力位) |
| `in_demand` | 价格是否在需求区支撑位附近下方35%区域) |
| `in_supply` | 价格是否在供给区阻力位附近上方35%区域) |
### 5.2 趋势判断逻辑
```
if 最近的SH > 前一个SH AND 最近的SL > 前一个SL:
→ 上升趋势HH + HL
elif 最近的SH < 前一个SH AND 最近的SL < 前一个SL:
→ 下降趋势LH + LL
else:
→ 维持上一个趋势判断(避免噪音切换)
```
**关键设计**:当 HH/HL 和 LH/LL 都不满足时(震荡),策略**保持上一个趋势方向**而非标记为"无趋势"。这避免了在震荡市中频繁切换方向。
### 5.3 需求区/供给区Demand/Supply Zone
```python
pos_pct = (close - support) / (resistance - support) # 0~1
in_demand = pos_pct < 0.35 # 价格在 S/R 区间下方 35%
in_supply = pos_pct > 0.65 # 价格在 S/R 区间上方 65%
```
这是一个相对位置判断——不是固定金额,而是相对于 S/R 区间宽度的比例。这意味着:
- 窄区间:离 S/R 很近才算"靠近"
- 宽区间:离 S/R 稍远也算"靠近"
### 5.4 活支撑/活阻力v1.6 新增)
```python
# 活支撑价格触及支撑位±0.5%),但收盘价仍在支撑上方
touched_support = (low <= support * 1.005) & (low >= support * 0.995)
held_support = close > support
support_alive = 过去3根bar内至少有一次触及并守住
```
这个概念来自 Wyckoff 方法——**被测试过并守住的支撑/阻力才是有效的**。未被测试的 S/R 位只是"理论上的",被测试并守住的才是"实战验证的"。
---
## 6. K线形态入场触发器
策略识别两种经典的价格行为反转形态:
### 6.1 Pin Bar锤子线/射击之星)
```python
pin_bar_wick_ratio = 0.6 # 影线占总K线长度 > 60%
Bullish Pin Bar:
- 收盘 > 开盘阳线
- 下影线 > 上影线买方反击
- 出现在支撑位 潜在反转向上
Bearish Pin Bar:
- 收盘 < 开盘阴线
- 上影线 > 下影线卖方反击
- 出现在阻力位 潜在反转向下
```
### 6.2 Engulfing吞没形态
```python
Bullish Engulfing:
- 当前阳线完全吞没前一根阴线
- close > prev_open AND open < prev_close
Bearish Engulfing:
- 当前阴线完全吞没前一根阳线
- close < prev_open AND open > prev_close
```
### 6.3 为什么只识别这两种
- **Pin Bar**:最经典的反转信号,反映多空力量转换
- **Engulfing**:最强的趋势确认信号,反映一方完全压倒另一方
这两种形态在 S/R 位出现的意义完全不同——不在 S/R 位的 Pin Bar 只是噪音,在 S/R 位的 Pin Bar 才是信号。策略通过结合 S/R 位来过滤噪音。
---
## 7. 入场逻辑详解
### 7.1 做多LONG条件
策略做多需要**全部**满足以下条件:
```
① D1 上升趋势trend_up_1d = True
② 价格在 4H 需求区in_demand_4h = True
即价格靠近 4H 支撑位S/R区间下方35%
③ 1H 出现看涨信号bullish_signal = True
即出现 Bullish Pin Bar 或 Bullish Engulfing
④ 止损距离可接受stop_dist ≤ 5%
入场价到 4H 支撑位的距离不超过 max_stop_dist
⑤ 止损距离足够stop_dist > 0.3%
避免止损设太近被噪音震出
⑥ 4H 支撑是活的support_alive_4h = True
支撑位在过去3根bar内被测试并守住
⑦ ★ v2.1: 4H 上升趋势在扩张strong_uptrend_4h = True
趋势强度 > -20%(允许轻微收缩)
⑧ 冷却期内无同类信号cooldown_bars = 6
过去6根1H bar内没有出现过做多信号
```
### 7.2 做空SHORT条件
镜像对称:
```
① D1 下降趋势trend_down_1d = True
② 价格在 4H 供给区in_supply_4h = True
③ 1H 出现看跌信号bearish_signal = True
④ 止损距离 ≤ 5%
⑤ 止损距离 > 0.3%
⑥ 4H 阻力是活的resistance_alive_4h = True
⑦ ★ v2.1: 4H 下降趋势在扩张strong_downtrend_4h = True
⑧ 冷却期 6 根 bar
```
### 7.3 入场逻辑图
```
┌──────────────┐
│ D1 趋势确认 │
│ 上升/下降? │
└──────┬───────┘
│ ✅
┌──────▼───────┐
│ 靠近4H S/R
│ in_demand/ │
│ in_supply
└──────┬───────┘
│ ✅
┌──────▼───────┐
│ 1H 反转信号?│
│ Pin Bar/ │
│ Engulfing
└──────┬───────┘
│ ✅
┌──────▼───────┐
│ 止损距离 │
│ 0.3%~5%
└──────┬───────┘
│ ✅
┌──────▼───────┐
│ S/R是活的
│ 被测试+守住? │
└──────┬───────┘
│ ✅
┌──────▼───────┐
│ ★ 趋势在扩张?│ ← v2.1 新增
│ strong_ │
│ uptrend/ │
│ downtrend
└──────┬───────┘
│ ✅
┌──────▼───────┐
│ 冷却期检查 │
│ 6根bar内无 │
│ 同类信号? │
└──────┬───────┘
│ ✅
┌──────▼───────┐
│ ⚡ 入场! │
└──────────────┘
```
---
## 8. 止损逻辑
### 8.1 设计原则
策略使用 **custom_stoploss** 实现动态止损,基于价格结构而非固定百分比。
**核心理念**:止损位置由市场结构决定,不由固定金额或百分比决定。
### 8.2 止损算法
```python
# 做多止损
support = 4H最近的Swing Low支撑位
sl_price = support × 0.999 # 支撑位下方 0.1%
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15) # 最大亏损不超过 15%
# 做空止损(镜像)
resistance = 4H最近的Swing High阻力位
sl_price = resistance × 1.001 # 阻力位上方 0.1%
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15) # 最大亏损不超过 15%
```
### 8.3 为什么用 0.1% 缓冲
经过 v1.7/v1.8 的多次尝试2%、5%缓冲0.1% 是最优的:
- **0.1%**:止损次数多,但单笔亏损小。关键是被止损后不影响后续交易,且 `trailing_stop_loss` 有更多机会发挥作用
- **2~5%**:止损次数少,但单笔亏损大。`trailing_stop_loss` 的追踪效果被破坏
**核心认知**:这个策略的利润来源不是"少止损",而是"让盈利交易跑起来"。大缓冲破坏了 trailing stop 的追踪效果。
### 8.4 -15% 硬止损
`stoploss = -0.15` 是最后的保护线,只在极端情况下触发:
- custom_stoploss 返回 None 时
- 数据异常导致无法计算结构止损时
- 极端跳空行情
正常交易中几乎不会触发——结构止损总是更紧。
---
## 9. 出场逻辑
### 9.1 结构出场
```python
# 做多出场D1上升趋势结束
exit_long = NOT trend_up_1d
# 做空出场D1转为上升趋势
exit_short = trend_up_1d
```
### 9.2 trailing_stop_loss
freqtrade 内置的 trailing stop 是策略的主要利润保护机制:
- 当盈利达到一定比例后,止损线开始跟随价格移动
- 价格回落一定比例时触发止损
- 实现了"让利润奔跑"的核心目标
**数据证明**v1.6 中 69.3% 的盈利交易通过 trailing_stop_loss 出场,这是策略利润的主要来源。
### 9.3 minimal_roi
```python
minimal_roi = {"0": 100}
```
策略不使用固定时间/收益比的止盈。所有利润保护交给 trailing stop。这意味着
- 没有"赚够了就平仓"的思维
- 让趋势把利润带到它能带到的最远
---
## 10. v2.1 核心创新:趋势强度过滤
### 10.1 问题背景
v1.6 的痛点在震荡市中D1 趋势可能仍然是"上升"或"下降"(因为趋势判断只比较两个 Swing Point但趋势本身的力度在减弱。这种"名义上的趋势"会导致在 S/R 位入场后被震荡出局。
### 10.2 趋势强度算法
v2.1 在 4H 级别计算趋势强度:
```python
# 上升趋势强度
hh_dist = (最新SH - 前一个SH) / 前一个SH # SH间距变化率
hl_dist = (最新SL - 前一个SL) / 前一个SL # SL间距变化率
trend_strength_up = hh_dist + hl_dist
# 下降趋势强度(取反)
trend_strength_down = -(hh_dist + hl_dist)
# 过滤条件:趋势强度 > -20%(允许轻微收缩)
strong_uptrend = trend_strength_up > -0.20
strong_downtrend = trend_strength_down > -0.20
```
### 10.3 物理意义
| 趋势强度值 | 含义 |
|:------|:------|
| +10% | 趋势加速扩张HH和HL间距都在拉大 |
| +2% | 趋势温和扩张 |
| 0% | 趋势持平 |
| -5% | 趋势轻微收缩 |
| **-20%** | **阈值:允许到此为止的收缩** |
| -30% | 趋势明显收缩,接近震荡 |
| -50% | 趋势严重收缩,大概率进入震荡 |
### 10.4 为什么阈值是 -20%
回测参数调优结果。0%(只允许扩张)会过滤掉太多信号;-20% 在"过滤震荡市信号"和"保留趋势市信号"之间取得了最佳平衡。
### 10.5 v2.1 vs v1.6 效果对比
| 指标 | v1.6 | v2.1 | 变化 |
|:------|:----:|:----:|:----:|
| ETH 总收益 | +3659% | +4366% | +19.3% |
| BTC 总收益 | +507% | +575% | +13.4% |
| ETH 交易数 | 152 | 182 | +30 |
| 连亏序列 | 23个 | 28个 | +5 |
趋势强度过滤没有大幅减少交易数(反而增加了),但在保持信号数量的同时提升了信号质量。这表明过滤器主要排除了"看起来可以但实际上不该入场"的信号,而不是简单粗暴地减少交易。
---
## 11. 参数说明
### 11.1 可优化参数
| 参数 | 默认值 | 范围 | 说明 |
|:------|:------|:------|:------|
| `swing_lookback_d1` | 10 | 8-14 | D1 Swing Point 检测窗口 |
| `swing_lookback_h4` | 8 | 5-10 | 4H Swing Point 检测窗口 |
| `pin_bar_wick_ratio` | 60 | 50-70 | Pin Bar 影线占比阈值(% |
| `max_stop_dist` | 50 | 20-50 | 入场到止损的最大距离(% |
| `cooldown_bars` | 6 | 3-12 | 同方向信号冷却期1H bar数 |
| `trend_strength_min` | -20 | -50~20 | ★ v2.1: 趋势强度最小阈值(% |
### 11.2 固定参数
| 参数 | 值 | 说明 |
|:------|:------|:------|
| `stoploss` | -0.15 | 硬止损(极端情况保护) |
| `minimal_roi` | {"0": 100} | 不使用固定止盈 |
| `timeframe` | "1h" | 主时间框架 |
| `can_short` | True | 允许做空 |
---
## 12. 回测表现
### 12.1 ETH/USDT 全周期2022-01 ~ 2026-06
| 指标 | v1.6 | v2.1 |
|:------|:----:|:----:|
| 总收益率 | +3659% | **+4366%** |
| 总交易数 | 152 | 182 |
| 胜率 | ~35% | ~36% |
| Profit Factor | 3.56 | ~3.8 |
| 最大连续亏损 | 8 笔 | 8 笔 |
| 最长连亏天数 | 35 天 | ~30 天 |
### 12.2 BTC/USDT 全周期
| 指标 | v1.6 | v2.1 |
|:------|:----:|:----:|
| 总收益率 | +507% | **+575%** |
| 总交易数 | 190 | ~200+ |
### 12.3 年度表现ETH
| 年份 | 市场环境 | 收益 | 说明 |
|:------|:------|:------|:------|
| 2022 | 熊市 | -11.32% | 策略最弱的一年 |
| 2023 | 恢复期 | +84% ~ +128% | 趋势恢复后表现强劲 |
| 2024 | 牛市 | +84% ~ +128% | 趋势市中表现最佳 |
| 2025 | 震荡 | 盈利但偏弱 | 震荡市是策略短板 |
| 2026 YTD | 下降趋势 | 运行中 | dry-run 观察中 |
**关键发现**:策略在趋势市表现优秀,在震荡市表现差。这符合价格行为策略的特征——没有趋势就没有利润。
### 12.4 回测配置
```json
{
"max_open_trades": 1,
"dry_run_wallet": 10000,
"stake_amount": "unlimited",
"trading_mode": "futures",
"margin_mode": "isolated"
}
```
⚠️ **重要**`max_open_trades=1``stake_amount="unlimited"` 是收益率数字的关键——它们确保每笔交易使用全部资金,实现完整复利。如果改成 `max_open_trades=3`,收益会被人为压低到约 1/3。
---
## 13. 演进历史
策略从 v0.1 到 v2.1 经历了多轮迭代:
```
v0.x 系列(指标策略)
├── 使用 EMA/ATR 等衍生指标
└── ❌ 已弃用:用户要求纯价格行为学
v1.0 ~ v1.5(探索期)
├── v1.0: 5M TF, spot → 噪音太多
├── v1.1: 1H TF, futures, 做空 → +61%
├── v1.2: Entry Candle 止损 → bug
├── v1.3: ATR 动态止损 → -63.72%
├── v1.4: 回归价格结构止损 → +140.71% ✅
└── v1.5: 参数调优 → +140.83%
v1.6(旧最优基线)
├── 入场质量过滤:冷却期 + 活支撑/阻力
├── ETH: +3659%, PF=3.78
└── ✅ 证明"入场质量 > 止损优化"
v1.7 ~ v1.9(止损优化尝试 — 全部失败)
├── v1.7: 5%缓冲 → 单笔亏损过大
├── v1.8: 2%缓冲 → 总收益下降45%
└── v1.9: 结构变化检测止损 → 收益下降56%
v2.0 ~ v2.1(趋势过滤)
├── v2.0: B1入场延迟确认 → 方向正确但降频严重
└── v2.1: D1趋势强度过滤 → ⭐ 当前最优
```
### 关键教训
1. **止损缓冲不是越大越好** — 小缓冲 + trailing stop > 大缓冲
2. **入场质量 > 止损优化** — 减少需要止损的交易是最佳路径
3. **二元过滤器容易误杀** — 需要更精细的评分机制
4. **所有优化必须先回测** — v1.7/v1.8 的失败证明了这一点
---
## 14. 已知局限
### 14.1 震荡市表现差
策略在趋势明确时表现优秀,但在震荡市中连续止损。这是因为:
- 震荡市没有持续的 HH/HL 或 LH/LL
- S/R 位频繁被穿越
- K线反转信号在震荡市中可靠性低
**当前对策**v2.1 的趋势强度过滤可以排除部分震荡市信号,但不能完全解决。
**未来方向**:市场状态分类器(趋势 vs 震荡),在震荡市自动降低交易频率或暂停。
### 14.2 胜率偏低(~35%
65% 的交易以止损结束。这在价格行为策略中是正常的但对交易者心态是巨大考验。连续8笔亏损跨越35天需要极强的纪律性。
### 14.3 回撤偏大(~30-40%
最大回撤可能达到 30-40%。10万本金可能暂时缩水到6万。
### 14.4 BTC 表现远弱于 ETH
BTC 的收益率(+575%)远低于 ETH+4366%差了近7倍。这可能与 ETH 波动性更大、趋势更明确有关。
### 14.5 未经过实盘验证
所有数据来自回测。实盘中可能面临:
- 滑点
- 网络延迟
- 交易所 API 限制
- 极端行情下的流动性问题
---
## 15. 部署信息
### 15.1 当前运行状态
| 项目 | 详情 |
|:------|:------|
| 服务器 | 腾讯云东京 43.163.225.30 |
| 系统 | Ubuntu 24.04.3 LTS |
| freqtrade | 2025.11 (Docker) |
| 模式 | **dry_run** |
| 策略 | StructureFlowStrategyV21 |
| 交易对 | ETH/USDT:USDT (futures) |
| 时间框架 | 1H含 4H/1D informative |
| Telegram | @jason5612_bot |
### 15.2 服务器目录结构
```
~/freqtrade/
├── docker-compose.yml # Docker 编排配置
└── user_data/
├── config.json # 主配置(含 Telegram
├── config.pairlist.json # 交易对白名单
├── strategies/
│ └── structure_flow_strategy_v2_1.py
├── data/binance/ # K线数据
├── logs/ # 运行日志
└── tradesv3.sqlite # 交易数据库
```
### 15.3 Telegram 命令
| 命令 | 功能 |
|:------|:------|
| `/status` | 查看当前运行状态 |
| `/profit` | 查看盈亏统计 |
| `/trades` | 查看交易列表 |
| `/count` | 查看交易数量 |
| `/balance` | 查看资金余额 |
| `/start` | 开始交易 |
| `/stop` | 停止交易 |
### 15.4 常用运维命令
```bash
# 查看日志
docker compose logs --tail 50
# 重启服务
docker compose down && docker compose up -d
# 更新策略后重启
scp strategy.py ubuntu@43.163.225.30:~/freqtrade/user_data/strategies/
ssh ubuntu@43.163.225.30 "cd ~/freqtrade && docker compose restart"
```
---
## 附录 A策略完整入场条件速查
### LONG做多
```
trend_up_1d = True # D1 上升趋势
in_demand_4h = True # 价格在4H需求区下方35%
bullish_signal = True # 1H 出现看涨 Pin Bar 或 Engulfing
stop_dist ≤ 5% # 入场到支撑位 ≤ 5%
stop_dist > 0.3% # 入场到支撑位 > 0.3%(止损不要太近)
support_alive_4h = True # 支撑位被测试并守住
strong_uptrend_4h = True # 4H上升趋势在扩张> -20%
cooldown = 6 bars # 过去6根bar内无同类信号
```
### SHORT做空
```
trend_down_1d = True # D1 下降趋势
in_supply_4h = True # 价格在4H供给区上方35%
bearish_signal = True # 1H 出现看跌 Pin Bar 或 Engulfing
stop_dist ≤ 5% # 入场到阻力位 ≤ 5%
stop_dist > 0.3% # 入场到阻力位 > 0.3%
resistance_alive_4h = True # 阻力位被测试并守住
strong_downtrend_4h = True # 4H下降趋势在扩张> -20%
cooldown = 6 bars # 过去6根bar内无同类信号
```
---
> **最后更新**: 2026-06-08 | **作者**: 用户 + WorkBuddy AI
>
> 本文档将随策略迭代持续更新。

531
docs/v1.6_strategy_doc.md Normal file
View File

@ -0,0 +1,531 @@
# StructureFlowStrategy v1.6 — 完整策略解析文档
> 写给自己看的版本,在下一次优化开始前彻底搞清楚这 436 行代码在做什么。
---
## 一、策略大纲
这是一个**纯价格行为策略**,零指标(没有 MACD、RSI、布林带……只用 K 线结构做决策。
核心思想只有一句话:
> **在大趋势方向上,找到有效的支撑/阻力位,等价格出现反转形态信号时入场,让市场结构自动追踪止损。**
三层时间框架,各司其职:
```
D1日线 → 判断方向:现在是涨势还是跌势?
4H四小时→ 找位置:支撑/阻力在哪里?价格在需求区还是供给区?
1H一小时→ 找时机:此时此刻有没有反转信号?
```
---
## 二、整体代码结构图
```
StructureFlowStrategyV16
├── 工具函数(静态方法)
│ ├── _detect_swing_points() ← 找 Swing High / Swing Low
│ ├── _build_structure() ← 分析结构:趋势、支撑、阻力、供需区
│ └── _detect_candle_patterns() ← 识别 K 线形态Pin Bar、吞没
├── 指标计算(按时间框架)
│ ├── populate_indicators_1d() ← D1计算日线趋势方向
│ ├── populate_indicators_4h() ← 4H计算中期结构 + 活支撑/阻力v1.6新增)
│ └── populate_indicators() ← 1H识别 K 线形态
├── 信号逻辑
│ ├── populate_entry_trend() ← 入场条件6个条件全满足才进
│ └── populate_exit_trend() ← 出场条件D1 趋势反转时退出)
└── 动态止损
└── custom_stoploss() ← 基于 4H Swing Point 的结构止损
```
---
## 三、基础参数说明
```python
can_short = True # 允许做空(合约模式)
stoploss = -0.15 # 兜底止损:最大允许亏损 15%custom_stoploss 的安全边界)
use_custom_stoploss = True # 启用自定义止损逻辑
minimal_roi = {"0": 100} # 关闭固定止盈100 = 永不触发)
max_open_trades = 1 # 同时最多 1 笔交易(专注单品种)
timeframe = "1h" # 主时间框架1小时
```
**关于 `stoploss = -0.15` 和 `custom_stoploss` 的关系:**
- `custom_stoploss` 是每根 K 线都会调用的动态止损逻辑
- `-0.15` 是**硬边界**:即使 `custom_stoploss` 返回了一个更宽的止损freqtrade 也会把它截断到 -15%
- 这两个是"主控 + 保险"的关系,不是矛盾的
---
## 四、可优化参数
```python
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
# D1 识别 Swing Point 的回溯窗口10 意味着一个高点必须是
# 左侧 10 根日线和右侧 10 根日线里的最高点,才算 Swing High。
# 数字越大 → Swing Point 越稀疏越重要
# 数字越小 → Swing Point 越密集越灵敏
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
# 4H 级别的 Swing Point 回溯窗口,逻辑同上
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
# Pin Bar 识别阈值:影线占整根 K 线的比例
# 60 表示 "影线占比 > 60% 才算 Pin Bar"
# 数字越大 → 只认最标准的 Pin Bar更严格
# 数字越小 → 稍微有点影线就算(更宽松)
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
# 止损距离上限:入场价到支撑位的距离不能超过 50%
# 防止"支撑位太远、止损太大"的情况
# 实际上 default=50 几乎不过滤(等于没有限制)
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v1.6 新增冷却期单位1H K 线根数)
# 6 表示上一个同方向信号出现后6 小时内不再产生新信号
```
---
## 五、工具函数详解
### 5.1 `_detect_swing_points()` — 找摆动高低点
```python
for i in range(window, n - window):
# 条件:第 i 根的 high > 左边 window 根的最高 AND > 右边 window 根的最高
if high[i] > high[i-window:i].max() and high[i] > high[i+1:i+window+1].max():
sh[i] = high[i] # 这是一个 Swing High
# 对称的逻辑找 Swing Low
if low[i] < low[i-window:i].min() and low[i] < low[i+1:i+window+1].min():
sl[i] = low[i] # 这是一个 Swing Low
```
**视觉理解:**
```
▲ Swing High左右各 window 根都是低点)
/|\
/ | \
───────────────/ | \───────────
```
**重要细节:** `range(window, n - window)` 意味着最后 `window` 根 K 线无法被识别为 Swing Point因为需要看右边的数据。这在实盘中意味着**最近的高低点要滞后 window 根才被确认**。
---
### 5.2 `_build_structure()` — 分析市场结构
这个函数是整个策略的大脑,逐根 K 线地维护 5 个状态:
#### 1趋势判断
```python
# 记录最近 4 个 Swing High 和 4 个 Swing Low
sh_prices = [] # 最多保留 4 个
sl_prices = []
# 趋势判断逻辑HH/HL vs LH/LL
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up = True # HH (Higher High) + HL (Higher Low) = 上升趋势
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down = True # LH (Lower High) + LL (Lower Low) = 下降趋势
else:
# 结构不明确时,继承上一根的趋势(趋势具有惯性)
trend_up[i] = trend_up[i-1]
trend_down[i] = trend_down[i-1]
```
**这就是价格行为学的核心:**
- 上升趋势 = Higher High + Higher LowHH + HL
- 下降趋势 = Lower High + Lower LowLH + LL
#### 2最近支撑/阻力
```python
nearest_support[i] = sl_prices[-1] # 最近的 Swing Low = 支撑
nearest_resistance[i] = sh_prices[-1] # 最近的 Swing High = 阻力
```
**注意:** 这里直接取最后一个 Swing Low 作为支撑,没有考虑位置关系(支撑可能在当前价格之上)。这是一个潜在的逻辑弱点,后续优化可以考虑。
#### 3供需区划分
```python
zone_range = resistance - support
pos_pct = (close - support) / zone_range
in_demand_zone = pos_pct < 0.35 # 价格在 support↔resistance 区间的底部 35%
in_supply_zone = pos_pct > 0.65 # 价格在 support↔resistance 区间的顶部 35%
```
**视觉理解:**
```
┌─────────────────────────────────┐
│ resistance ────────────────── │
│ 供给区Supply Zone, >65% │ ← 做空区域
│ ─────────────────────────── 65%│
│ │
│ 中间区35%~65% │ ← 不入场
│ │
│ ─────────────────────────── 35%│
│ 需求区Demand Zone, <35% │ ← 做多区域
│ support ──────────────────── │
└─────────────────────────────────┘
```
---
### 5.3 `_detect_candle_patterns()` — K 线形态识别
#### Pin BarPin 柱/别针 K 线)
```python
body = abs(close - open_) # 实体大小
total_range = high - low # 整根 K 线的范围
upper_wick = high - max(close, open_) # 上影线长度
lower_wick = min(close, open_) - low # 下影线长度
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
# 当影线占比 > 60%,才算 Pin Bar
# 看涨 Pin Bar阳线 + 下影线 > 上影线(价格被撑住了)
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
# 看跌 Pin Bar阴线 + 上影线 > 下影线(价格被压下来了)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
```
**视觉理解:**
```
看涨 Pin Bar 看跌 Pin Bar
│ ██████
│ │
███ │
│ │
███████ ███████
███████
```
#### 吞没形态Engulfing
```python
# 看涨吞没:阳线,收盘 > 前根开盘,开盘 < 前根收盘
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
# 看跌吞没:阴线,收盘 < 前根开盘,开盘 > 前根收盘
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
```
---
## 六、时间框架指标详解
### 6.1 D1 日线(`populate_indicators_1d`
**只做一件事:确定宏观趋势方向**
- `trend_up_1d = True` → 日线是上升趋势HH + HL
- `trend_down_1d = True` → 日线是下降趋势LH + LL
回溯窗口 `swing_lookback_d1 = 10`,意味着确认一个日线 Swing Point 需要左右各 10 天,合计 20 天。这是有意为之——**日线趋势应该稳定,不应该频繁切换**。
---
### 6.2 4H 四小时(`populate_indicators_4h`
**做五件事:**
1. `trend_up_4h` / `trend_down_4h`4H 中期趋势(与 D1 配合,双重确认)
2. `support_4h`:最近的 4H Swing Low止损基准点
3. `resistance_4h`:最近的 4H Swing High做空止损基准点
4. `in_demand_4h`:当前 1H 价格是否在 4H 需求区(做多区域)
5. `in_supply_4h`:当前 1H 价格是否在 4H 供给区(做空区域)
**v1.6 新增:活支撑/阻力判断**
```python
# 支撑"活"的判断:
# 条件1最近某根 4H K 线的 low触及 support ± 0.5% 的范围
touched_support = (low <= support * 1.005) & (low >= support * 0.995)
# 条件2那根 K 线的收盘价在 support 之上(撑住了,没跌穿)
held_support = close > support
# 两个条件都满足,才算"一次有效测试"
support_tested_and_held = touched_support & held_support
# 在最近 3 根 4H K 线内,至少发生过一次有效测试 → 支撑是"活"的
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
```
**为什么要"活支撑"**
"死支撑"是指那些很久以前的 Swing Low现在价格距离它很远支撑位已经失效。例如
```
3个月前的 Swing Low = 1500 美元(支撑)
但价格从那之后一直在 2000-2500 之间波动,从未回测过 1500
→ 1500 的支撑已经是"死"的,在那里做多没有意义
```
v1.6 要求:支撑必须是**最近 3 根 4H K 线内有价格来测试过的**,才算有效。
**"活支撑" 的问题(客观评价):**
- `rolling(3)` = 最近 12 小时3 × 4H内测试过
- 这个窗口是否合适12 小时可能太短,也可能太长
- 是未来优化的方向之一
---
### 6.3 1H 一小时(`populate_indicators`
**只做一件事:识别 K 线反转形态**
```python
dataframe["bullish_signal"] = bullish_pin | bullish_engulf # 两种看涨形态满足任一
dataframe["bearish_signal"] = bearish_pin | bearish_engulf # 两种看跌形态满足任一
```
**NaN 安全处理:**
由于不同时间框架的数据合并D1、4H → 1H会产生空值这里统一对布尔列做 `fillna(False)`,防止 `True & NaN = NaN` 导致信号丢失。
---
## 七、入场逻辑详解
### 做多条件6 个条件全部满足)
```python
long_base = (
dataframe["trend_up_1d"] # 条件1日线是上升趋势
& dataframe["in_demand_4h"] # 条件2当前价格在 4H 需求区(支撑附近)
& dataframe["bullish_signal"] # 条件31H 出现看涨 K 线形态
& (long_stop_dist <= max_dist) # 条件4止损距离 ≤ 50%(几乎不过滤)
& (long_stop_dist > 0.003) # 条件5止损距离 > 0.3%(防止支撑=开盘价)
)
long_base = long_base & dataframe["support_alive_4h"] # 条件6支撑是"活"的 [v1.6]
long_recent = long_base.rolling(cooldown).max().shift(1) == 0 # 冷却期过滤 [v1.6]
long_conditions = long_base & long_recent
```
**条件5 的意义:** `long_stop_dist > 0.003` 防止"支撑位和开盘价几乎相同"的情况,那样止损太近,任何正常波动都会触发。
### 冷却期的工作原理
```python
# long_base 是"条件1-6都满足"的信号(不含冷却)
# rolling(6).max() → 过去6根1H bar内是否有过满足条件的信号1=有0=没有)
# .shift(1) → 往后移一格避免当前这根K线跟自己比较
# == 0 → 只有"过去6小时内没有信号"时,才允许当前入场
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
```
**视觉理解cooldown=6**
```
时间 → 1h 2h 3h 4h 5h 6h 7h 8h 9h 10h
信号 ✓ ✓
冷却期 ←── 6小时冷却 ──→
允许入场 ✓ ✓ 第10小时重新可以入场
```
### 做空条件(对称)
```python
short_base = (
dataframe["trend_down_1d"] # 条件1日线是下降趋势
& dataframe["in_supply_4h"] # 条件2价格在 4H 供给区(阻力附近)
& dataframe["bearish_signal"] # 条件31H 出现看跌 K 线形态
& (short_stop_dist <= max_dist) # 条件4止损距离合理
& (short_stop_dist > 0.003) # 条件5止损距离不为零
)
short_base = short_base & dataframe["resistance_alive_4h"] # 条件6阻力是"活"的
```
---
## 八、出场逻辑详解
```python
exit_long = ~dataframe["trend_up_1d"].fillna(True) # 当 D1 趋势不再上升时平多
exit_short = dataframe["trend_up_1d"].fillna(False) # 当 D1 趋势转为上升时平空
```
**这个逻辑非常简单,也非常粗糙:**
- 只有 D1 趋势反转时才触发出场信号
- D1 趋势需要明确的 HH+HL 或 LH+LL 才会切换,这需要较长时间
- 结果:大多数交易不会触发 exit_signal而是由止损stop_loss 或 trailing_stop_loss退出
这也是为什么回测中 `exit_signal` 只有 7-9 笔,而止损退出有 70-100 笔。
---
## 九、动态止损详解(最核心的部分)
```python
def custom_stoploss(self, pair, trade, current_time, current_rate, current_profit, ...):
last = dataframe.iloc[-1] # 取最新的 4H 数据
if not trade.is_short: # 多单
support = last["support_4h"] # 最近的 4H Swing Low
sl_price = support * 0.999 # 止损价 = 支撑位下方 0.1%
sl_ratio = (sl_price / current_rate) - 1.0 # 转换为相对于当前价格的比率
return max(sl_ratio, -0.15) # 不超过 -15% 的硬边界
else: # 空单
resistance = last["resistance_4h"] # 最近的 4H Swing High
sl_price = resistance * 1.001 # 止损价 = 阻力位上方 0.1%
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15) # 不超过 +15% 的硬边界
```
### 为什么这会产生"自然追踪止损效果"
关键在于 `support_4h` 是**动态更新**的:
```
t=0 入场support_4h = 1800止损在 1798.2
价格从 1850 开始上涨
t=5h 价格涨到 2000出现新 Swing Low = 1900
→ support_4h 更新为 1900
→ 止损自动上移到 1898.1
t=10h 价格涨到 2200新 Swing Low = 2050
→ support_4h 更新为 2050
→ 止损上移到 2047.9
```
这就是为什么 `trailing_stop_loss` 出现那么多:每当价格创新高、产生新的 Swing Low止损就自动追上去锁住利润。
### 止损的数学计算示例
假设:
- 当前价格 = 2000 USDT
- support_4h = 1900 USDT
```python
sl_price = 1900 * 0.999 = 1898.1
sl_ratio = (1898.1 / 2000) - 1.0 = -0.0595 -5.95%
```
即:止损触发点在当前价格的 5.95% 下方。
---
## 十、退出原因分类(理解 freqtrade 的逻辑)
| 退出原因 | 触发条件 | v1.6 ETH 全周期 |
|----------|----------|-----------------|
| `stop_loss` | 价格触及 `custom_stoploss` 返回的价格,且 current_profit < 0 | 73 0% 胜率 |
| `trailing_stop_loss` | 价格从高点回落触及之前计算的止损价且有过盈利 | 71 67.6% 胜率 |
| `exit_signal` | `populate_exit_trend` 返回 exit=1 | 7 71.4% 胜率 |
| `force_exit` | 回测结束强制平仓 | 1 100% |
**`stop_loss` `trailing_stop_loss` 的本质区别**
- `stop_loss`交易从入场到退出**从未产生过盈利**。价格一直往不利方向走直到触发止损
- `trailing_stop_loss`交易曾经**有过盈利**后来价格回落触及追踪止损位退出
**这说明了什么?**
73 `stop_loss` = 73 笔入场后价格立即向不利方向走的交易
这些交易**入场位是错的**或者**在错误的时机做了正确的方向**。这是 v1.6 最核心的问题也是下一步优化的主攻方向
---
## 十一、已知问题和局限性
### 问题 1`stop_loss` 胜率 0%(最严重)
- 表现73 stop_loss 退出全部亏损
- 根本原因入场后价格立即反向说明入场位质量不高
- 可能方向更严格的入场过滤更好的时机判断
### 问题 2支撑位识别的逻辑漏洞
- `nearest_support = sl_prices[-1]` 直接取最近的 Swing Low可能在当前价格之上
- 例如价格在 2000但最近 Swing Low 2100价格刚从 2100 跌下来这时支撑 = 2100 > 2000止损价在当前价格上方逻辑矛盾
- `long_stop_dist > 0.003` 一定程度上过滤了这种情况,但不完全
### 问题 3出场逻辑太粗糙
- 只看 D1 趋势是否反转
- D1 趋势切换很慢,大多数交易靠止损退出,而非出场信号
- 可以考虑增加 4H 结构破坏作为出场信号
### 问题 4市场环境依赖
- 趋势市2023-2024表现优异
- 震荡市2025 YTD BTC表现差
- 没有"市场环境过滤器",在震荡市会频繁入场并被止损
### 问题 5`fillna(False)` 的 FutureWarning
- 技术性问题不影响结果但需要修复pandas 未来版本会报错)
---
## 十二、数据流的完整路径
一笔做多交易的完整生命周期:
```
1. D1 数据 → _detect_swing_points() → _build_structure()
→ 输出 trend_up_1d = True (日线上升趋势确认)
2. 4H 数据 → _detect_swing_points() → _build_structure()
→ 输出 support_4h = 1900, resistance_4h = 2100
→ 输出 in_demand_4h = True (价格在需求区)
→ v1.6: 检查 support_alive_4h = True (支撑最近被测试过)
3. 1H 数据 → _detect_candle_patterns()
→ 输出 bullish_signal = True (出现看涨 Pin Bar 或吞没)
4. populate_entry_trend() 汇总所有条件
→ 全部满足 + 冷却期过了 → enter_long = 1 → 下单做多
5. 持仓期间,每根 1H K 线:
→ custom_stoploss() 被调用
→ 读取最新 support_4h
→ 计算止损价 = support_4h * 0.999
→ 止损位随 support_4h 上移(追踪效果)
6. 退出:
→ 价格创新高后回落触及止损 → trailing_stop_loss 退出
→ 或价格入场后就下跌 → stop_loss 退出
→ 或 D1 趋势反转 → exit_signal 退出
```
---
## 十三、优化的可能方向(供后续参考)
基于以上分析,以下是最有价值的优化方向(按优先级排序):
**优先级 A解决 stop_loss 0% 胜率**
1. 分析 73 笔 stop_loss 交易的特征(何时入场?市场处于什么状态?)
2. 增加入场质量过滤(例如:要求 4H 也是上升趋势、要求成交量确认)
3. 改进 Swing Point 识别(例如:要求支撑位被测试 2 次以上)
**优先级 B增加市场环境过滤**
1. 识别"震荡市"(例如:最近 N 根 4H bar 内,没有明确的 HH/HL 序列)
2. 在震荡市中停止交易,只在趋势市中工作
**优先级 C改进出场逻辑**
1. 将出场信号细化到 4H 级别4H 结构破坏时退出,而不等 D1 反转)
2. 可以大幅减少 stop_loss 次数,让更多交易变成 exit_signal 或 trailing_stop_loss
---
*文档基于 `structure_flow_strategy_v1_6.py` 代码v1.6 版本2026-06-07*

View File

@ -1,398 +1,270 @@
"""
Structure Flow Strategy v2.2c — 冷却期修复版
==============================================
变更记录:
v2.2c (2026-06-11): 1H S/R 替代 4H S/R
v2.2c-coolfix (2026-06-11): 修复冷却期无限阻止下单 bug
"""
# structure_flow_momentum_scalp.py
# 顺趋势剥头皮策略 v2.0
#
# 核心思路不再在S/R处做反向交易接飞刀而是顺趋势方向等回调后入场。
#
# ┌─────────────────────────────────────────────────────────────┐
# │ 15m趋势方向判断EMA20 vs EMA50
# │ ↓ │
# │ 上升趋势 → 只等5m回调到EMA20/支撑附近 → 止跌信号 → 做多 │
# │ 下降趋势 → 只等5m反弹到EMA20/阻力附近 → 止涨信号 → 做空 │
# │ ↓ │
# │ 止损ATR×1.0 | 止盈ATR×1.5 | 时间止损60分钟 │
# └─────────────────────────────────────────────────────────────┘
#
# v2.0 (2026-06-10): 初始版本,完全重写
from datetime import datetime
import numpy as np
import pandas as pd
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative
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
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
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
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参数
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")
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy")
# ── 可优化参数 ──
# 趋势检测
trend_ema_period = IntParameter(10, 30, default=20, space="buy")
# 回调确认幅度
pullback_deviation = DecimalParameter(0.2, 1.0, default=0.5, decimals=1, 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(
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)
# ── 保护性止损 ──
stoploss = -0.10 # 硬止损 10%
# ================================================================
# 信息时间框架 — D1 宏观结构
# 杠杆
# ================================================================
@informative("1d")
def populate_indicators_1d(
def leverage(
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
) -> 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
"""15m级别EMA趋势方向 + swing point S/R。"""
# ================================================================
# 信息时间框架 — 4H 趋势强度(原版保留)
# ================================================================
# ── EMA 趋势方向 ──
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")
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"] = dataframe["ema_fast"] > dataframe["ema_slow"]
dataframe["trend_down"] = dataframe["ema_fast"] < dataframe["ema_slow"]
# ── Swing Point 支撑/阻力 ──
high = dataframe["high"].tolist()
low = dataframe["low"].tolist()
close = dataframe["close"].tolist()
sh, sl = self._detect_swing_points(high, low, window=5)
trend_up_arr, trend_down_arr, support_arr, resistance_arr = self._build_structure(
high, low, close, sh, sl,
)
# 趋势强度计算(原版逻辑)
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)
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
dataframe["trend_up_sp"] = trend_up_arr
dataframe["trend_down_sp"] = trend_down_arr
# EMA平滑S/R避免跳变
dataframe["support"] = self._ema_smooth(support_arr, alpha=0.3)
dataframe["resistance"] = self._ema_smooth(resistance_arr, alpha=0.3)
return dataframe
# ================================================================
# 主时间框架 — 1H 指标(含 1H S/R + 活支撑/阻力)
# 主框架 — 5m 级别指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> 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线形态 ──
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["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
# ── 1H级别 Swing Point + 结构替代原4H S/R ──
sh_1h, sl_1h = self._detect_swing_points(
dataframe["high"], dataframe["low"],
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"]
# ── 5m EMA用于短期拉回确认 ──
dataframe["ema5"] = close.ewm(span=5, adjust=False).mean()
dataframe["ema8"] = close.ewm(span=8, adjust=False).mean()
# ── 1H 活支撑/阻力检查 ──
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
# ── 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)
# ── 布尔列NaN填充 ──
for col in ["bullish_signal", "bearish_signal"]:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号(修复冷却期逻辑
# =====================
# ================================================================
# 入场逻辑
# ================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
max_dist = self.max_stop_dist.value / 100.0
def populate_entry_trend(
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
dev = self.pullback_deviation.value / 100.0 # 0.5% → 0.005
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",
# ── 必要列检查 ──
required = [
"ema_fast_15m", "trend_up_15m", "trend_down_15m",
"support_15m", "resistance_15m",
]
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:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多使用1H S/R ──
long_stop_dist = (dataframe["open"] - dataframe["support"]) / dataframe["open"]
# ═══════════════════════════════════════════════════════════
# 做多:上升趋势 + 回调到EMA/支撑 + 止跌信号
# ═══════════════════════════════════════════════════════════
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive"]
& dataframe["strong_uptrend_4h"]
# 条件115m 上升趋势
trend_up = dataframe["trend_up_15m"]
# 条件2价格在EMA20或支撑位附近回调到顺趋势的支撑区
near_ema = (
(dataframe["low"] <= dataframe["ema_fast_15m"] * (1.0 + dev * 0.5)) &
(dataframe["low"] >= dataframe["ema_fast_15m"] * (1.0 - dev * 2.0))
)
# ✅ 修复:正确应用冷却期(基于实际入场,而非条件满足)
long_entries = self._apply_cooldown(long_base, cooldown)
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"]
near_support = (
(dataframe["low"] <= dataframe["support_15m"] * (1.0 + dev)) &
(dataframe["low"] >= dataframe["support_15m"] * (1.0 - dev))
)
pullback_long = near_ema | near_support
# ✅ 修复:正确应用冷却期(基于实际入场,而非条件满足)
short_entries = self._apply_cooldown(short_base, cooldown)
dataframe.loc[short_entries, "enter_short"] = 1
# 条件3K线止跌信号
signal_long = dataframe["bullish_signal"]
# 综合入场
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
# =====================
# 出场信号
# =====================
# ================================================================
# exit_trendfreqtrade 2025.11 强制要求,即使 use_exit_signal=False
# ================================================================
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
"""出场完全由 custom_stoploss + custom_exit 管理。"""
return dataframe
# =====================
# 动态止损基于1H S/R
# =====================
# ================================================================
# 出场 — 止损ATR动态
# ================================================================
def custom_stoploss(
self,
@ -404,48 +276,240 @@ class StructureFlowStrategyV22d(IStrategy):
after_fill: bool,
**kwargs,
) -> 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)
if dataframe is None or len(dataframe) == 0:
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:
support = last.get("support", 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)
sl_price = trade.open_rate - (atr * mult)
sl_ratio = (sl_price / trade.open_rate) - 1.0
return max(sl_ratio, -self.stoploss)
else:
resistance = last.get("resistance", 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)
sl_price = trade.open_rate + (atr * mult)
sl_ratio = 1.0 - (sl_price / trade.open_rate)
return min(sl_ratio, self.stoploss)
# =====================
# Plot config
# =====================
# ================================================================
# 出场 — 止盈ATR动态+ 时间止损
# ================================================================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support": {"color": "green", "type": "line"},
"resistance": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive": {"color": "green", "type": "line"},
"resistance_alive": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}
def custom_exit(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> str | None:
"""
出场逻辑:
1. ATR止盈利润达到入场时锁定的 ATR × atr_mult_tp → 止盈
2. 时间止损:持仓超过 time_stop_minutes → 强制出场
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
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