Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa5e177caf | |||
| bd598f0203 | |||
| 89174352e2 | |||
| 92fdf2c941 | |||
| 58ec67203f | |||
| 23ed71649d | |||
| 13616c1cd2 | |||
| 3da0b17725 | |||
| 77c3362dd5 | |||
| cf0c6d2677 | |||
| 688fe36e3b |
46
README.md
Normal file
46
README.md
Normal 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.2d:D1趋势总闸门 — 震荡市不下单是保护机制不是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
454
ablation/ablation_1.py
Normal 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
454
ablation/ablation_2.py
Normal 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
454
ablation/ablation_3.py
Normal 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
454
ablation/ablation_4.py
Normal 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
454
ablation/ablation_5.py
Normal 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
454
ablation/ablation_6.py
Normal 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
454
ablation/ablation_7.py
Normal 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
456
ablation/ablation_8.py
Normal 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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
442
ablation/ablation_all_removed.py
Normal file
442
ablation/ablation_all_removed.py
Normal 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
456
ablation/v2_1_baseline.py
Normal 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
39
backtest/INDEX.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Backtest Results Index
|
||||||
|
|
||||||
|
## Today'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
@ -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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -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":{}}
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -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"]}
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -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"]}
|
||||||
Binary file not shown.
2288
backtest/v2_2c_full_2021_2026.txt
Normal file
2288
backtest/v2_2c_full_2021_2026.txt
Normal file
File diff suppressed because it is too large
Load Diff
2926
backtest/v2_2d_full_2021_2026.txt
Normal file
2926
backtest/v2_2d_full_2021_2026.txt
Normal file
File diff suppressed because it is too large
Load Diff
32
backtest/v2_2d_yearly_summary.md
Normal file
32
backtest/v2_2d_yearly_summary.md
Normal 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
385
dashboard/indicators.py
Normal 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
498
docs/backtest-pitfalls.md
Normal file
@ -0,0 +1,498 @@
|
|||||||
|
# Freqtrade 回测部署踩坑记录
|
||||||
|
|
||||||
|
> 记录于 2026-06-07 | PriceActionStrategy v0.2 首次回测
|
||||||
|
> 环境:Docker freqtrade (d:\ft_userdata\),ETH/USDT,Binance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、配置文件 (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`**,始终返回显式止损比率。
|
||||||
|
|
||||||
|
### 根因 #2:Entry 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**,不要每次手动改
|
||||||
|
|
||||||
|
| **✅** | **网络/代理经验固化完成** | — | — |
|
||||||
198
docs/stop_loss_analysis_report.md
Normal file
198
docs/stop_loss_analysis_report.md
Normal 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 问题(结构性缺陷)
|
||||||
|
|
||||||
|
### 问题 1:D1 和 H4 的脱节
|
||||||
|
|
||||||
|
当前策略的核心假设是"D1 定方向,4H 找位置",但实际执行中:
|
||||||
|
- D1 趋势变化很慢(几天到几周)
|
||||||
|
- H4 趋势变化很快(几小时到几天)
|
||||||
|
- 在 D1 上涨但 H4 下跌的窗口期(回调),策略会做多 → 然后被 H4 下跌趋势吞噬
|
||||||
|
|
||||||
|
**本质:** D1 趋势对 1H 入场来说太"远"了,中间的 4H 趋势才是 1H 入场最近的上层结构。
|
||||||
|
|
||||||
|
### 问题 2:S/R 识别只看最近 4 个 Swing Point
|
||||||
|
|
||||||
|
`_build_structure` 只保留最近 4 个 Swing High/Low 来判断趋势。这可能导致:
|
||||||
|
- 在更大时间框架的结构位(比如日线级别的关键支撑)被忽略
|
||||||
|
- 趋势判断过于短视
|
||||||
|
|
||||||
|
### 问题 3:custom_stoploss 的动态更新是一把双刃剑
|
||||||
|
|
||||||
|
当新的 Swing Low 形成时,`support_4h` 会更新为新低点,止损位下移 → 持仓承受更大的浮亏。这就是 S/R 位移的根源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、改进路线图
|
||||||
|
|
||||||
|
### 阶段 1:H4 趋势过滤器(预计影响最大)
|
||||||
|
|
||||||
|
```
|
||||||
|
v1.6.3: 加入 H4 趋势一致性要求
|
||||||
|
- LONG: D1趋势=UP AND H4趋势=UP
|
||||||
|
- SHORT: D1趋势=DOWN AND H4趋势=DOWN
|
||||||
|
- 回测验证:是否会误杀太多盈利交易?
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 2:S/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` | 含结构快照和分类的完整分析数据 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*这份报告是接下来所有止损优化的基础。每个改进方向都可以独立验证、独立回测。*
|
||||||
735
docs/strategy-v2.1-whitepaper.md
Normal file
735
docs/strategy-v2.1-whitepaper.md
Normal 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/HL(Higher High / Higher Low)和 LH/LL(Lower 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
531
docs/v1.6_strategy_doc.md
Normal 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 Low(HH + HL)
|
||||||
|
- 下降趋势 = Lower High + Lower Low(LH + 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 Bar(Pin 柱/别针 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"] # 条件3:1H 出现看涨 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"] # 条件3:1H 出现看跌 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)*
|
||||||
780
strategy.py
780
strategy.py
@ -1,373 +1,270 @@
|
|||||||
"""
|
# structure_flow_momentum_scalp.py
|
||||||
Structure Flow Strategy v2.2b — 1H S/R 实验版
|
# 顺趋势剥头皮策略 v2.0
|
||||||
==============================================
|
#
|
||||||
变更记录:
|
# 核心思路:不再在S/R处做反向交易接飞刀,而是顺趋势方向,等回调后入场。
|
||||||
v2.2b (2026-06-09): 原版 — 4H级别S/R + 趋势强度
|
#
|
||||||
v2.2b-1h-sr (2026-06-10): 实验版 — 将S/R从4H改为1H级别,趋势强度仍用4H
|
# ┌─────────────────────────────────────────────────────────────┐
|
||||||
|
# │ 15m趋势方向判断(EMA20 vs EMA50) │
|
||||||
|
# │ ↓ │
|
||||||
|
# │ 上升趋势 → 只等5m回调到EMA20/支撑附近 → 止跌信号 → 做多 │
|
||||||
|
# │ 下降趋势 → 只等5m反弹到EMA20/阻力附近 → 止涨信号 → 做空 │
|
||||||
|
# │ ↓ │
|
||||||
|
# │ 止损:ATR×1.0 | 止盈:ATR×1.5 | 时间止损:60分钟 │
|
||||||
|
# └─────────────────────────────────────────────────────────────┘
|
||||||
|
#
|
||||||
|
# v2.0 (2026-06-10): 初始版本,完全重写
|
||||||
|
|
||||||
改动:
|
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative
|
||||||
support_alive/resistance_alive 从4H级别 → 1H级别
|
|
||||||
support/resistance 引用 从4H → 1H
|
|
||||||
in_demand/in_supply 从4H → 1H
|
|
||||||
趋势强度(strong_uptrend/downtrend)保持在4H
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from freqtrade.strategy import IStrategy, IntParameter, informative
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
class StructureFlowStrategyV22c(IStrategy):
|
class StructureFlowMomentumScalp(IStrategy):
|
||||||
|
"""
|
||||||
|
顺趋势剥头皮策略 v2.0
|
||||||
|
|
||||||
|
核心逻辑:
|
||||||
|
- 15m EMA趋势方向过滤,只做顺趋势方向的单
|
||||||
|
- 5m 回调到EMA20或S/R支撑/阻力区域时,等待K线信号确认后入场
|
||||||
|
- 止损 ATR×1.0,止盈 ATR×1.5,时间止损 60 分钟
|
||||||
|
- 不做方向猜测,不吃鱼头鱼尾,只吃回调结束那一小段
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ── 时间框架 ──
|
||||||
|
timeframe = "5m"
|
||||||
|
|
||||||
|
# ── 交易参数 ──
|
||||||
can_short = True
|
can_short = True
|
||||||
stoploss = -0.15
|
|
||||||
use_custom_stoploss = True
|
|
||||||
minimal_roi = {"0": 100}
|
|
||||||
max_open_trades = 1
|
max_open_trades = 1
|
||||||
timeframe = "1h"
|
stake_amount = "unlimited"
|
||||||
|
use_custom_stoploss = True
|
||||||
|
use_exit_signal = False # 出场完全由 custom_stoploss + custom_exit 管理
|
||||||
|
|
||||||
# =====================
|
# ── 合约参数 ──
|
||||||
# 可优化参数
|
margin_mode = "cross"
|
||||||
# =====================
|
trading_mode = "futures"
|
||||||
|
|
||||||
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
|
# ── 可优化参数 ──
|
||||||
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
|
# 趋势检测
|
||||||
swing_lookback_1h = IntParameter(3, 7, default=5, space="buy") # 新增:1H swing参数
|
trend_ema_period = IntParameter(10, 30, default=20, space="buy")
|
||||||
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
|
# 回调确认幅度
|
||||||
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
|
pullback_deviation = DecimalParameter(0.2, 1.0, default=0.5, decimals=1, space="buy")
|
||||||
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
|
# 入场冷却期
|
||||||
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy")
|
cooldown_bars = IntParameter(2, 8, default=3, space="buy")
|
||||||
|
# K线形态灵敏度
|
||||||
|
pin_bar_wick_ratio = IntParameter(50, 80, default=60, space="buy")
|
||||||
|
# 止损ATR倍数
|
||||||
|
atr_mult_stop = DecimalParameter(0.8, 2.0, default=1.0, decimals=1, space="sell")
|
||||||
|
# 止盈ATR倍数
|
||||||
|
atr_mult_tp = DecimalParameter(1.0, 3.0, default=1.5, decimals=1, space="sell")
|
||||||
|
|
||||||
# =====================
|
# ── 常数 ──
|
||||||
# 工具:Swing Point 检测
|
time_stop_minutes = 60 # 最大持仓时间
|
||||||
# =====================
|
|
||||||
|
|
||||||
@staticmethod
|
# ── 保护性止损 ──
|
||||||
def _detect_swing_points(
|
stoploss = -0.10 # 硬止损 10%
|
||||||
high: pd.Series,
|
|
||||||
low: pd.Series,
|
|
||||||
window: int = 5,
|
|
||||||
) -> tuple[pd.Series, pd.Series]:
|
|
||||||
n = len(high)
|
|
||||||
sh = pd.Series(np.nan, index=high.index, dtype=float)
|
|
||||||
sl = pd.Series(np.nan, index=low.index, dtype=float)
|
|
||||||
|
|
||||||
for i in range(window, n - window):
|
|
||||||
if high.iloc[i] > high.iloc[i - window : i].max() and high.iloc[i] > high.iloc[i + 1 : i + window + 1].max():
|
|
||||||
sh.iloc[i] = high.iloc[i]
|
|
||||||
if low.iloc[i] < low.iloc[i - window : i].min() and low.iloc[i] < low.iloc[i + 1 : i + window + 1].min():
|
|
||||||
sl.iloc[i] = low.iloc[i]
|
|
||||||
|
|
||||||
return sh, sl
|
|
||||||
|
|
||||||
# =====================
|
|
||||||
# 工具:结构分析
|
|
||||||
# =====================
|
|
||||||
|
|
||||||
def _build_structure(
|
|
||||||
self,
|
|
||||||
high: pd.Series,
|
|
||||||
low: pd.Series,
|
|
||||||
close: pd.Series,
|
|
||||||
swing_high: pd.Series,
|
|
||||||
swing_low: pd.Series,
|
|
||||||
) -> DataFrame:
|
|
||||||
n = len(high)
|
|
||||||
|
|
||||||
trend_up_arr = np.full(n, False)
|
|
||||||
trend_down_arr = np.full(n, False)
|
|
||||||
nearest_support = np.full(n, np.nan)
|
|
||||||
nearest_resistance = np.full(n, np.nan)
|
|
||||||
in_demand_zone = np.full(n, False)
|
|
||||||
in_supply_zone = np.full(n, False)
|
|
||||||
|
|
||||||
sh_prices = []
|
|
||||||
sl_prices = []
|
|
||||||
|
|
||||||
for i in range(n):
|
|
||||||
if pd.notna(swing_high.iloc[i]):
|
|
||||||
sh_prices.append(swing_high.iloc[i])
|
|
||||||
if len(sh_prices) > 4:
|
|
||||||
sh_prices.pop(0)
|
|
||||||
|
|
||||||
if pd.notna(swing_low.iloc[i]):
|
|
||||||
sl_prices.append(swing_low.iloc[i])
|
|
||||||
if len(sl_prices) > 4:
|
|
||||||
sl_prices.pop(0)
|
|
||||||
|
|
||||||
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
|
|
||||||
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
|
|
||||||
trend_up_arr[i] = True
|
|
||||||
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
|
|
||||||
trend_down_arr[i] = True
|
|
||||||
elif i > 0:
|
|
||||||
trend_up_arr[i] = trend_up_arr[i - 1]
|
|
||||||
trend_down_arr[i] = trend_down_arr[i - 1]
|
|
||||||
elif i > 0:
|
|
||||||
trend_up_arr[i] = trend_up_arr[i - 1]
|
|
||||||
trend_down_arr[i] = trend_down_arr[i - 1]
|
|
||||||
|
|
||||||
if sl_prices:
|
|
||||||
nearest_support[i] = sl_prices[-1]
|
|
||||||
if sh_prices:
|
|
||||||
nearest_resistance[i] = sh_prices[-1]
|
|
||||||
|
|
||||||
c = close.iloc[i]
|
|
||||||
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
|
|
||||||
zone_range = nearest_resistance[i] - nearest_support[i]
|
|
||||||
if zone_range > 0:
|
|
||||||
pos_pct = (c - nearest_support[i]) / zone_range
|
|
||||||
in_demand_zone[i] = pos_pct < 0.35
|
|
||||||
in_supply_zone[i] = pos_pct > 0.65
|
|
||||||
|
|
||||||
return DataFrame({
|
|
||||||
"trend_up": trend_up_arr,
|
|
||||||
"trend_down": trend_down_arr,
|
|
||||||
"support": nearest_support,
|
|
||||||
"resistance": nearest_resistance,
|
|
||||||
"in_demand": in_demand_zone,
|
|
||||||
"in_supply": in_supply_zone,
|
|
||||||
}, index=high.index)
|
|
||||||
|
|
||||||
# =====================
|
|
||||||
# 工具:K线形态检测
|
|
||||||
# =====================
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _detect_candle_patterns(
|
|
||||||
open_: pd.Series,
|
|
||||||
high: pd.Series,
|
|
||||||
low: pd.Series,
|
|
||||||
close: pd.Series,
|
|
||||||
pin_bar_wick_ratio: float = 0.6,
|
|
||||||
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
|
|
||||||
body = (close - open_).abs()
|
|
||||||
total_range = (high - low).replace(0, 0.0001)
|
|
||||||
|
|
||||||
upper_wick = high - close.where(close > open_, open_)
|
|
||||||
lower_wick = open_.where(close > open_, close) - low
|
|
||||||
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
|
|
||||||
|
|
||||||
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
|
|
||||||
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
|
|
||||||
|
|
||||||
prev_open = open_.shift(1)
|
|
||||||
prev_close = close.shift(1)
|
|
||||||
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
|
|
||||||
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
|
|
||||||
|
|
||||||
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
|
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# 信息时间框架 — D1 宏观结构
|
# 杠杆
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
||||||
@informative("1d")
|
def leverage(
|
||||||
def populate_indicators_1d(
|
self, pair: str, current_time: datetime, current_rate: float,
|
||||||
|
proposed_leverage: float, max_leverage: float, side: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> float:
|
||||||
|
"""20x 杠杆起步,验证胜率后再上量"""
|
||||||
|
return min(20.0, max_leverage)
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# 信息时间框架 — 15m 趋势判断 + S/R
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
@informative("15m")
|
||||||
|
def populate_indicators_15m(
|
||||||
self, dataframe: DataFrame, metadata: dict
|
self, dataframe: DataFrame, metadata: dict
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
sh, sl = self._detect_swing_points(
|
"""15m级别:EMA趋势方向 + swing point S/R。"""
|
||||||
dataframe["high"], dataframe["low"],
|
|
||||||
self.swing_lookback_d1.value,
|
|
||||||
)
|
|
||||||
structure = self._build_structure(
|
|
||||||
dataframe["high"], dataframe["low"], dataframe["close"],
|
|
||||||
sh, sl,
|
|
||||||
)
|
|
||||||
dataframe["trend_up"] = structure["trend_up"]
|
|
||||||
dataframe["trend_down"] = structure["trend_down"]
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
# ================================================================
|
# ── EMA 趋势方向 ──
|
||||||
# 信息时间框架 — 4H 趋势强度(原版保留)
|
ema_period = self.trend_ema_period.value
|
||||||
# ================================================================
|
dataframe["ema_fast"] = dataframe["close"].ewm(span=ema_period, adjust=False).mean()
|
||||||
|
dataframe["ema_slow"] = dataframe["close"].ewm(span=ema_period * 2.5, adjust=False).mean()
|
||||||
|
|
||||||
@informative("4h")
|
dataframe["trend_up"] = dataframe["ema_fast"] > dataframe["ema_slow"]
|
||||||
def populate_indicators_4h(
|
dataframe["trend_down"] = dataframe["ema_fast"] < dataframe["ema_slow"]
|
||||||
self, dataframe: DataFrame, metadata: dict
|
|
||||||
) -> DataFrame:
|
# ── Swing Point 支撑/阻力 ──
|
||||||
sh, sl = self._detect_swing_points(
|
high = dataframe["high"].tolist()
|
||||||
dataframe["high"], dataframe["low"],
|
low = dataframe["low"].tolist()
|
||||||
self.swing_lookback_h4.value,
|
close = dataframe["close"].tolist()
|
||||||
)
|
|
||||||
structure = self._build_structure(
|
sh, sl = self._detect_swing_points(high, low, window=5)
|
||||||
dataframe["high"], dataframe["low"], dataframe["close"],
|
trend_up_arr, trend_down_arr, support_arr, resistance_arr = self._build_structure(
|
||||||
sh, sl,
|
high, low, close, sh, sl,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 趋势强度计算(原版逻辑)
|
dataframe["trend_up_sp"] = trend_up_arr
|
||||||
sh_prices = []
|
dataframe["trend_down_sp"] = trend_down_arr
|
||||||
sl_prices = []
|
# EMA平滑S/R(避免跳变)
|
||||||
trend_strength_up = np.full(len(dataframe), np.nan)
|
dataframe["support"] = self._ema_smooth(support_arr, alpha=0.3)
|
||||||
trend_strength_down = np.full(len(dataframe), np.nan)
|
dataframe["resistance"] = self._ema_smooth(resistance_arr, alpha=0.3)
|
||||||
|
|
||||||
for i in range(len(dataframe)):
|
|
||||||
if pd.notna(sh.iloc[i]):
|
|
||||||
sh_prices.append(sh.iloc[i])
|
|
||||||
if len(sh_prices) > 4:
|
|
||||||
sh_prices.pop(0)
|
|
||||||
if pd.notna(sl.iloc[i]):
|
|
||||||
sl_prices.append(sl.iloc[i])
|
|
||||||
if len(sl_prices) > 4:
|
|
||||||
sl_prices.pop(0)
|
|
||||||
|
|
||||||
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
|
|
||||||
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
|
|
||||||
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
|
|
||||||
trend_strength_up[i] = hh_dist + hl_dist
|
|
||||||
trend_strength_down[i] = -(hh_dist + hl_dist)
|
|
||||||
|
|
||||||
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
|
|
||||||
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
|
|
||||||
|
|
||||||
min_strength = self.trend_strength_min.value / 100.0
|
|
||||||
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
|
|
||||||
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# 主时间框架 — 1H 指标(含 1H S/R + 活支撑/阻力)
|
# 主框架 — 5m 级别指标
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
||||||
def populate_indicators(
|
def populate_indicators(
|
||||||
self, dataframe: DataFrame, metadata: dict
|
self, dataframe: DataFrame, metadata: dict
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
|
"""5m级别:ATR + K线形态 + EMA趋势整合。"""
|
||||||
|
|
||||||
|
# ── ATR(14) ──
|
||||||
|
high = dataframe["high"]
|
||||||
|
low = dataframe["low"]
|
||||||
|
close = dataframe["close"]
|
||||||
|
prev_close = close.shift(1)
|
||||||
|
tr = pd.concat([
|
||||||
|
high - low,
|
||||||
|
(high - prev_close).abs(),
|
||||||
|
(low - prev_close).abs(),
|
||||||
|
], axis=1).max(axis=1)
|
||||||
|
dataframe["atr"] = tr.rolling(14).mean()
|
||||||
|
atr_mean = dataframe["atr"].mean()
|
||||||
|
dataframe["atr"] = dataframe["atr"].fillna(atr_mean)
|
||||||
|
|
||||||
# ── K线形态 ──
|
# ── K线形态 ──
|
||||||
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
|
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
|
||||||
self._detect_candle_patterns(
|
self._detect_candle_patterns(
|
||||||
dataframe["open"],
|
dataframe["open"], dataframe["high"], dataframe["low"],
|
||||||
dataframe["high"],
|
dataframe["close"], self.pin_bar_wick_ratio.value / 100.0,
|
||||||
dataframe["low"],
|
|
||||||
dataframe["close"],
|
|
||||||
self.pin_bar_wick_ratio.value / 100.0,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
dataframe["bullish_pinbar"] = bullish_pin
|
|
||||||
dataframe["bearish_pinbar"] = bearish_pin
|
|
||||||
dataframe["bullish_engulfing"] = bullish_engulf
|
|
||||||
dataframe["bearish_engulfing"] = bearish_engulf
|
|
||||||
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
|
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
|
||||||
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
|
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
|
||||||
|
|
||||||
# ── 1H级别 Swing Point + 结构(替代原4H S/R) ──
|
# ── 5m EMA(用于短期拉回确认) ──
|
||||||
sh_1h, sl_1h = self._detect_swing_points(
|
dataframe["ema5"] = close.ewm(span=5, adjust=False).mean()
|
||||||
dataframe["high"], dataframe["low"],
|
dataframe["ema8"] = close.ewm(span=8, adjust=False).mean()
|
||||||
self.swing_lookback_1h.value,
|
|
||||||
)
|
|
||||||
structure_1h = self._build_structure(
|
|
||||||
dataframe["high"], dataframe["low"], dataframe["close"],
|
|
||||||
sh_1h, sl_1h,
|
|
||||||
)
|
|
||||||
dataframe["trend_up_1h"] = structure_1h["trend_up"]
|
|
||||||
dataframe["trend_down_1h"] = structure_1h["trend_down"]
|
|
||||||
dataframe["support"] = structure_1h["support"]
|
|
||||||
dataframe["resistance"] = structure_1h["resistance"]
|
|
||||||
dataframe["in_demand"] = structure_1h["in_demand"]
|
|
||||||
dataframe["in_supply"] = structure_1h["in_supply"]
|
|
||||||
|
|
||||||
# ── 1H 活支撑/阻力检查 ──
|
# ── 布尔列NaN填充 ──
|
||||||
touched_support = (
|
for col in ["bullish_signal", "bearish_signal"]:
|
||||||
(dataframe["low"] <= dataframe["support"] * 1.005) &
|
dataframe[col] = dataframe[col].fillna(False)
|
||||||
(dataframe["low"] >= dataframe["support"] * 0.995)
|
|
||||||
)
|
|
||||||
held_support = dataframe["close"] > dataframe["support"]
|
|
||||||
support_tested_and_held = touched_support & held_support
|
|
||||||
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
|
|
||||||
|
|
||||||
touched_resistance = (
|
|
||||||
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
|
|
||||||
(dataframe["high"] <= dataframe["resistance"] * 1.005)
|
|
||||||
)
|
|
||||||
held_resistance = dataframe["close"] < dataframe["resistance"]
|
|
||||||
resistance_tested_and_held = touched_resistance & held_resistance
|
|
||||||
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
|
|
||||||
|
|
||||||
# ── NaN 安全处理 ──
|
|
||||||
bool_cols = [
|
|
||||||
"trend_up_1d", "trend_down_1d",
|
|
||||||
"trend_up_4h", "trend_down_4h",
|
|
||||||
"in_demand", "in_supply",
|
|
||||||
"support_alive", "resistance_alive",
|
|
||||||
"strong_uptrend_4h", "strong_downtrend_4h",
|
|
||||||
"bullish_signal", "bearish_signal",
|
|
||||||
]
|
|
||||||
for col in bool_cols:
|
|
||||||
if col in dataframe.columns:
|
|
||||||
dataframe[col] = dataframe[col].fillna(False)
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
# =====================
|
# ================================================================
|
||||||
# 入场信号
|
# 入场逻辑
|
||||||
# =====================
|
# ================================================================
|
||||||
|
|
||||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(
|
||||||
max_dist = self.max_stop_dist.value / 100.0
|
self, dataframe: DataFrame, metadata: dict
|
||||||
|
) -> DataFrame:
|
||||||
|
"""
|
||||||
|
入场逻辑。
|
||||||
|
|
||||||
|
只做顺趋势回调入场,不做S/R反向交易:
|
||||||
|
|
||||||
|
做多条件:
|
||||||
|
1. 15m 上升趋势(EMA_fast > EMA_slow)
|
||||||
|
2. 5m 价格回调到15m EMA_fast 或 支撑位附近
|
||||||
|
3. 5m K线止跌信号(pinbar/engulfing)
|
||||||
|
|
||||||
|
做空条件(对称):
|
||||||
|
1. 15m 下降趋势
|
||||||
|
2. 5m 价格反弹到15m EMA_fast 或 阻力位附近
|
||||||
|
3. 5m K线止涨信号
|
||||||
|
"""
|
||||||
cooldown = self.cooldown_bars.value
|
cooldown = self.cooldown_bars.value
|
||||||
|
dev = self.pullback_deviation.value / 100.0 # 0.5% → 0.005
|
||||||
|
|
||||||
bool_cols = [
|
# ── 必要列检查 ──
|
||||||
"trend_up_1d", "trend_down_1d",
|
required = [
|
||||||
"trend_up_4h", "trend_down_4h",
|
"ema_fast_15m", "trend_up_15m", "trend_down_15m",
|
||||||
"in_demand", "in_supply",
|
"support_15m", "resistance_15m",
|
||||||
"support_alive", "resistance_alive",
|
|
||||||
"strong_uptrend_4h", "strong_downtrend_4h",
|
|
||||||
"bullish_signal", "bearish_signal",
|
|
||||||
]
|
]
|
||||||
for col in bool_cols:
|
for col in required:
|
||||||
|
if col not in dataframe.columns:
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# ── 布尔列填充 ──
|
||||||
|
for col in [
|
||||||
|
"bullish_signal", "bearish_signal",
|
||||||
|
"trend_up_15m", "trend_down_15m",
|
||||||
|
]:
|
||||||
if col in dataframe.columns:
|
if col in dataframe.columns:
|
||||||
dataframe[col] = dataframe[col].fillna(False)
|
dataframe[col] = dataframe[col].fillna(False)
|
||||||
|
|
||||||
# ── 做多(使用1H S/R) ──
|
# ═══════════════════════════════════════════════════════════
|
||||||
long_stop_dist = (dataframe["open"] - dataframe["support"]) / dataframe["open"]
|
# 做多:上升趋势 + 回调到EMA/支撑 + 止跌信号
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
long_base = (
|
# 条件1:15m 上升趋势
|
||||||
dataframe["trend_up_1d"]
|
trend_up = dataframe["trend_up_15m"]
|
||||||
& dataframe["in_demand"]
|
|
||||||
& (long_stop_dist <= max_dist)
|
# 条件2:价格在EMA20或支撑位附近(回调到顺趋势的支撑区)
|
||||||
& (long_stop_dist > 0.003)
|
near_ema = (
|
||||||
& dataframe["support_alive"]
|
(dataframe["low"] <= dataframe["ema_fast_15m"] * (1.0 + dev * 0.5)) &
|
||||||
& dataframe["strong_uptrend_4h"]
|
(dataframe["low"] >= dataframe["ema_fast_15m"] * (1.0 - dev * 2.0))
|
||||||
)
|
)
|
||||||
|
near_support = (
|
||||||
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
|
(dataframe["low"] <= dataframe["support_15m"] * (1.0 + dev)) &
|
||||||
dataframe.loc[long_base & long_recent, "enter_long"] = 1
|
(dataframe["low"] >= dataframe["support_15m"] * (1.0 - dev))
|
||||||
|
|
||||||
# ── 做空(使用1H S/R) ──
|
|
||||||
short_stop_dist = (dataframe["resistance"] - dataframe["open"]) / dataframe["open"]
|
|
||||||
|
|
||||||
short_base = (
|
|
||||||
dataframe["trend_down_1d"]
|
|
||||||
& dataframe["in_supply"]
|
|
||||||
& (short_stop_dist <= max_dist)
|
|
||||||
& (short_stop_dist > 0.003)
|
|
||||||
& dataframe["resistance_alive"]
|
|
||||||
& dataframe["strong_downtrend_4h"]
|
|
||||||
)
|
)
|
||||||
|
pullback_long = near_ema | near_support
|
||||||
|
|
||||||
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
|
# 条件3:K线止跌信号
|
||||||
dataframe.loc[short_base & short_recent, "enter_short"] = 1
|
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/阻力 + 止涨信号
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# 条件1:15m 下降趋势
|
||||||
|
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
|
||||||
|
|
||||||
|
# 条件3:K线止涨信号
|
||||||
|
signal_short = dataframe["bearish_signal"]
|
||||||
|
|
||||||
|
# 综合入场
|
||||||
|
enter_short = trend_down & pullback_short & signal_short
|
||||||
|
short_recent = enter_short.rolling(cooldown, min_periods=1).max().shift(1) == 0
|
||||||
|
dataframe.loc[enter_short & short_recent, "enter_short"] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
# =====================
|
# ================================================================
|
||||||
# 出场信号
|
# exit_trend(freqtrade 2025.11 强制要求,即使 use_exit_signal=False)
|
||||||
# =====================
|
# ================================================================
|
||||||
|
|
||||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
exit_long = ~dataframe["trend_up_1d"].fillna(True)
|
"""出场完全由 custom_stoploss + custom_exit 管理。"""
|
||||||
dataframe.loc[exit_long, "exit_long"] = 1
|
|
||||||
|
|
||||||
exit_short = dataframe["trend_up_1d"].fillna(False)
|
|
||||||
dataframe.loc[exit_short, "exit_short"] = 1
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
# =====================
|
# ================================================================
|
||||||
# 动态止损(基于1H S/R)
|
# 出场 — 止损(ATR动态)
|
||||||
# =====================
|
# ================================================================
|
||||||
|
|
||||||
def custom_stoploss(
|
def custom_stoploss(
|
||||||
self,
|
self,
|
||||||
@ -379,49 +276,240 @@ class StructureFlowStrategyV22c(IStrategy):
|
|||||||
after_fill: bool,
|
after_fill: bool,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> float:
|
) -> float:
|
||||||
|
"""
|
||||||
|
止损 = 入场价 ± ATR × atr_mult_stop
|
||||||
|
|
||||||
|
- ATR值从入场K线锁定,持仓期间不变
|
||||||
|
- 做多:entry_price - (locked_atr × mult)
|
||||||
|
- 做空:entry_price + (locked_atr × mult)
|
||||||
|
- 配20x杠杆,ATR×1.0 ≈ 对应约 $3.7 止损(当前5m ATR~$3.74)
|
||||||
|
"""
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
if dataframe is None or len(dataframe) == 0:
|
if dataframe is None or len(dataframe) == 0:
|
||||||
return -0.02 if not trade.is_short else 0.02
|
return -0.02 if not trade.is_short else 0.02
|
||||||
|
|
||||||
last = dataframe.iloc[-1]
|
entry_row = self._get_entry_row(dataframe, trade)
|
||||||
|
if entry_row is None:
|
||||||
|
return -0.02 if not trade.is_short else 0.02
|
||||||
|
|
||||||
|
atr = entry_row.get("atr", np.nan)
|
||||||
|
if pd.isna(atr) or atr <= 0:
|
||||||
|
return -0.02 if not trade.is_short else 0.02
|
||||||
|
|
||||||
|
mult = self.atr_mult_stop.value
|
||||||
|
|
||||||
if not trade.is_short:
|
if not trade.is_short:
|
||||||
support = last.get("support", np.nan)
|
sl_price = trade.open_rate - (atr * mult)
|
||||||
if pd.isna(support) or support <= 0:
|
sl_ratio = (sl_price / trade.open_rate) - 1.0
|
||||||
return -0.02
|
return max(sl_ratio, -self.stoploss)
|
||||||
sl_price = support * 0.999
|
|
||||||
sl_ratio = (sl_price / current_rate) - 1.0
|
|
||||||
return max(sl_ratio, -0.15)
|
|
||||||
else:
|
else:
|
||||||
resistance = last.get("resistance", np.nan)
|
sl_price = trade.open_rate + (atr * mult)
|
||||||
if pd.isna(resistance) or resistance <= 0:
|
sl_ratio = 1.0 - (sl_price / trade.open_rate)
|
||||||
return 0.02
|
return min(sl_ratio, self.stoploss)
|
||||||
sl_price = resistance * 1.001
|
|
||||||
sl_ratio = 1.0 - (sl_price / current_rate)
|
|
||||||
return min(sl_ratio, 0.15)
|
|
||||||
|
|
||||||
# =====================
|
# ================================================================
|
||||||
# Plot config
|
# 出场 — 止盈(ATR动态)+ 时间止损
|
||||||
# =====================
|
# ================================================================
|
||||||
|
|
||||||
@staticmethod
|
def custom_exit(
|
||||||
def plot_config() -> dict:
|
self,
|
||||||
return {
|
pair: str,
|
||||||
"main_plot": {
|
trade: Trade,
|
||||||
"support": {"color": "green", "type": "line"},
|
current_time: datetime,
|
||||||
"resistance": {"color": "red", "type": "line"},
|
current_rate: float,
|
||||||
},
|
current_profit: float,
|
||||||
"subplots": {
|
**kwargs,
|
||||||
"signals": {
|
) -> str | None:
|
||||||
"bullish_pinbar": {"color": "green", "type": "scatter"},
|
"""
|
||||||
"bearish_pinbar": {"color": "red", "type": "scatter"},
|
出场逻辑:
|
||||||
},
|
1. ATR止盈:利润达到入场时锁定的 ATR × atr_mult_tp → 止盈
|
||||||
"filters": {
|
2. 时间止损:持仓超过 time_stop_minutes → 强制出场
|
||||||
"support_alive": {"color": "green", "type": "line"},
|
"""
|
||||||
"resistance_alive": {"color": "red", "type": "line"},
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
"strong_uptrend_4h": {"color": "blue", "type": "line"},
|
if dataframe is None or len(dataframe) == 0:
|
||||||
"strong_downtrend_4h": {"color": "orange", "type": "line"},
|
return None
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
entry_row = self._get_entry_row(dataframe, trade)
|
||||||
|
if entry_row is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
atr = entry_row.get("atr", np.nan)
|
||||||
|
if pd.isna(atr) or atr <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 1. ATR 止盈
|
||||||
|
tp_mult = self.atr_mult_tp.value
|
||||||
|
tp_ratio = (atr * tp_mult) / trade.open_rate
|
||||||
|
|
||||||
|
if current_profit >= tp_ratio:
|
||||||
|
return "atr_tp"
|
||||||
|
|
||||||
|
# 2. 时间止损
|
||||||
|
elapsed = (current_time - trade.open_date).total_seconds() / 60.0
|
||||||
|
if elapsed >= self.time_stop_minutes:
|
||||||
|
return "time_stop"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# 工具函数
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
def _detect_swing_points(
|
||||||
|
self, highs: list, lows: list, window: int = 5
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Swing High / Swing Low 检测。
|
||||||
|
|
||||||
|
当一根K线的最高价高于其两侧window根K线的最高价时,标记为Swing High。
|
||||||
|
Swing Low同理。
|
||||||
|
"""
|
||||||
|
n = len(highs)
|
||||||
|
swing_high = [np.nan] * n
|
||||||
|
swing_low = [np.nan] * n
|
||||||
|
|
||||||
|
for i in range(window, n - window):
|
||||||
|
# Swing High
|
||||||
|
is_high = True
|
||||||
|
for j in range(i - window, i + window + 1):
|
||||||
|
if j == i:
|
||||||
|
continue
|
||||||
|
if highs[j] >= highs[i]:
|
||||||
|
is_high = False
|
||||||
|
break
|
||||||
|
if is_high:
|
||||||
|
swing_high[i] = highs[i]
|
||||||
|
|
||||||
|
# Swing Low
|
||||||
|
is_low = True
|
||||||
|
for j in range(i - window, i + window + 1):
|
||||||
|
if j == i:
|
||||||
|
continue
|
||||||
|
if lows[j] <= lows[i]:
|
||||||
|
is_low = False
|
||||||
|
break
|
||||||
|
if is_low:
|
||||||
|
swing_low[i] = lows[i]
|
||||||
|
|
||||||
|
return swing_high, swing_low
|
||||||
|
|
||||||
|
def _build_structure(
|
||||||
|
self, highs: list, lows: list, closes: list,
|
||||||
|
swing_high: list, swing_low: list,
|
||||||
|
):
|
||||||
|
"""构建趋势结构和支撑/阻力位。"""
|
||||||
|
n = len(highs)
|
||||||
|
trend_up = [False] * n
|
||||||
|
trend_down = [False] * n
|
||||||
|
support = [np.nan] * n
|
||||||
|
resistance = [np.nan] * n
|
||||||
|
|
||||||
|
# 用最近4个swing point的位置判断
|
||||||
|
last_sh_idx = -1
|
||||||
|
last_sl_idx = -1
|
||||||
|
prev_sh = []
|
||||||
|
prev_sl = []
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
if not np.isnan(swing_high[i]):
|
||||||
|
prev_sh.append(swing_high[i])
|
||||||
|
last_sh_idx = i
|
||||||
|
if len(prev_sh) > 4:
|
||||||
|
prev_sh.pop(0)
|
||||||
|
|
||||||
|
if not np.isnan(swing_low[i]):
|
||||||
|
prev_sl.append(swing_low[i])
|
||||||
|
last_sl_idx = i
|
||||||
|
if len(prev_sl) > 4:
|
||||||
|
prev_sl.pop(0)
|
||||||
|
|
||||||
|
# 趋势判断:最新的HH > 次新的HH = 上升趋势中的higher high
|
||||||
|
if len(prev_sh) >= 2 and prev_sh[-1] > prev_sh[-2]:
|
||||||
|
trend_up[i] = True
|
||||||
|
|
||||||
|
# 趋势判断:最新的LL < 次新的LL = 下降趋势中的lower low
|
||||||
|
if len(prev_sl) >= 2 and prev_sl[-1] < prev_sl[-2]:
|
||||||
|
trend_down[i] = True
|
||||||
|
|
||||||
|
# 支撑 = 最近的有效Swing Low(EMA平滑后在调用侧处理)
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user