docs: 补充回测结果摘要 + 策略文档 + Dashboard 后端 + 整理目录结构

This commit is contained in:
2026-06-11 23:32:27 +08:00
parent bd598f0203
commit fa5e177caf
22 changed files with 8999 additions and 0 deletions

View File

@ -24,6 +24,20 @@ ETH/USDT 永续合约量化交易策略版本管理,基于 freqtrade + Binance
- v2.2b:当前最优回测基线(+4673%/+17%最大回撤)
- v2.2dD1趋势总闸门 — 震荡市不下单是保护机制不是bug
## 仓库结构
```
.
├── strategy.py # 当前最新策略v2.2d
├── indicators.py # Dashboard 后端指标计算
├── config.backtest.json # 标准化回测配置
├── ablation/ # v2.1 消融实验8组 + 基线)
├── backtest/ # 关键回测结果摘要
├── docs/ # 策略白皮书、技术文档
├── dashboard/ # Dashboard 后端
└── configs/ # 各版本配置文件
```
## 铁律
1. 只增不删 — 所有历史版本保留

39
backtest/INDEX.md Normal file
View 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

View File

@ -0,0 +1,435 @@
"""
Structure Flow Strategy v1.6
=======================
变更记录:
v1.0 (2026-06-07): 纯价格结构策略D1定方向→4H定位→1H入场
v1.1 (2026-06-07): 1H futures结构止损首次回测成功(+61.52%)
v1.2 (2026-06-07): Entry Candle止损bug导致50笔硬止损全亏
v1.3 (2026-06-07): ATR动态止损结果-63.72%胜率20.2%
v1.4 (2026-06-07): 回归纯价格结构止损,+140.71%胜率38.7%
v1.5 (2026-06-07): 参数调优(stoploss -5%→-15%, max_stop_dist 3%→5%)+140.83%
v1.6 (2026-06-07): ===== 入场质量优化 =====
- 6-bar冷却期信号后6h内不重复入场防止连挨多刀
- 活支撑/阻力检查S/R必须被最近测试并守住才算有效
设计原则:不降频,只砍最差的那几笔重复入场
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV16(IStrategy):
"""
Structure Flow Strategy v1.6 — 纯价格结构,零指标
v1.6改动相对于v1.5
1. 6-bar冷却期同方向信号触发后6h内禁止同向再入场
→ 解决"同一天同一个价位挨两刀"的问题
2. 活支撑/阻力检查4H Swing Point 必须被价格测试并守住才有效
→ 解决"在死支撑上入场"的问题
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
# v1.6 新增
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
n = len(high)
sh = pd.Series(np.nan, index=high.index, dtype=float)
sl = pd.Series(np.nan, index=low.index, dtype=float)
for i in range(window, n - window):
if high.iloc[i] > high.iloc[i - window : i].max() and high.iloc[i] > high.iloc[i + 1 : i + window + 1].max():
sh.iloc[i] = high.iloc[i]
if low.iloc[i] < low.iloc[i - window : i].min() and low.iloc[i] < low.iloc[i + 1 : i + window + 1].min():
sl.iloc[i] = low.iloc[i]
return sh, sl
# =====================
# 工具:结构分析
# =====================
def _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
n = len(high)
trend_up_arr = np.full(n, False)
trend_down_arr = np.full(n, False)
nearest_support = np.full(n, np.nan)
nearest_resistance = np.full(n, np.nan)
in_demand_zone = np.full(n, False)
in_supply_zone = np.full(n, False)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
elif i > 0:
trend_up_arr[i] = trend_up_arr[i - 1]
trend_down_arr[i] = trend_down_arr[i - 1]
if sl_prices:
nearest_support[i] = sl_prices[-1]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
"in_demand": in_demand_zone,
"in_supply": in_supply_zone,
}, index=high.index)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_d1.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
return dataframe
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["trend_up"] = structure["trend_up"]
dataframe["trend_down"] = structure["trend_down"]
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
dataframe["in_demand"] = structure["in_demand"]
dataframe["in_supply"] = structure["in_supply"]
# ================================
# v1.6 新增:活支撑/阻力检查
# ================================
# 支撑"活"的条件在最近3根4H bar内low 触及 support ±0.5%
# 并且收盘价在支撑之上(即测试后撑住了)
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
# 在过去3根4H bar内有至少一次"测试并守住"
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
# 阻力"活"的条件high 触及 resistance ±0.5% 且 close 在阻力之下
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态零指标"""
bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
self._detect_candle_patterns(
dataframe["open"],
dataframe["high"],
dataframe["low"],
dataframe["close"],
self.pin_bar_wick_ratio.value / 100.0,
)
)
dataframe["bullish_pinbar"] = bullish_pin
dataframe["bearish_pinbar"] = bearish_pin
dataframe["bullish_engulfing"] = bullish_engulf
dataframe["bearish_engulfing"] = bearish_engulf
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
做多条件:
1. D1 上升结构trend_up_1d
2. 4H 需求区域in_demand_4h
3. 1H 看涨 K 线形态bullish_signal
4. 止损距离 ≤ max_stop_dist%
5. [v1.6] 支撑位是""support_alive_4h
6. [v1.6] 6h内没有过同方向入场信号冷却期
做空条件对称。
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
)
# v1.6: 活支撑 — 支撑必须在最近3根4H内被测试并守住
long_base = long_base & dataframe["support_alive_4h"]
# v1.6: 冷却期 — 过去N根1H bar内没有过满足条件的做多信号
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
long_conditions = long_base & long_recent
dataframe.loc[long_conditions, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
)
# v1.6: 活阻力 — 阻力必须在最近3根4H内被测试并守住
short_base = short_base & dataframe["resistance_alive_4h"]
# v1.6: 冷却期 — 过去N根1H bar内没有过满足条件的做空信号
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
short_conditions = short_base & short_recent
dataframe.loc[short_conditions, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑:完全基于价格结构,零指标。
止损位:
做多 → support_4h - 0.1%缓冲最近4H Swing Low下方
做空 → resistance_4h + 0.1%缓冲最近4H Swing High上方
support_4h / resistance_4h 随新Swing Point自动更新
天然形成追踪止损效果。
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return -0.02 if not trade.is_short else 0.02
last = dataframe.iloc[-1]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
},
},
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

385
dashboard/indicators.py Normal file
View File

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

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

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

View File

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

View File

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

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

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