Files
beast-trader-strategies/docs/v1.6_strategy_doc.md

532 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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*