# StructureFlowStrategy v1.6 — 完整策略解析文档

> 写给自己看的版本，在下一次优化开始前彻底搞清楚这 436 行代码在做什么。

---

## 一、策略大纲

这是一个**纯价格行为策略**，零指标（没有 MACD、RSI、布林带……），只用 K 线结构做决策。

核心思想只有一句话：

> **在大趋势方向上，找到有效的支撑/阻力位，等价格出现反转形态信号时入场，让市场结构自动追踪止损。**

三层时间框架，各司其职：

```
D1（日线）  →  判断方向：现在是涨势还是跌势？
4H（四小时）→  找位置：支撑/阻力在哪里？价格在需求区还是供给区？
1H（一小时）→  找时机：此时此刻有没有反转信号？
```

---

## 二、整体代码结构图

```
StructureFlowStrategyV16
│
├── 工具函数（静态方法）
│   ├── _detect_swing_points()   ← 找 Swing High / Swing Low
│   ├── _build_structure()       ← 分析结构：趋势、支撑、阻力、供需区
│   └── _detect_candle_patterns() ← 识别 K 线形态：Pin Bar、吞没
│
├── 指标计算（按时间框架）
│   ├── populate_indicators_1d() ← D1：计算日线趋势方向
│   ├── populate_indicators_4h() ← 4H：计算中期结构 + 活支撑/阻力（v1.6新增）
│   └── populate_indicators()    ← 1H：识别 K 线形态
│
├── 信号逻辑
│   ├── populate_entry_trend()   ← 入场条件（6个条件全满足才进）
│   └── populate_exit_trend()    ← 出场条件（D1 趋势反转时退出）
│
└── 动态止损
    └── custom_stoploss()        ← 基于 4H Swing Point 的结构止损
```

---

## 三、基础参数说明

```python
can_short = True         # 允许做空（合约模式）
stoploss = -0.15         # 兜底止损：最大允许亏损 15%（custom_stoploss 的安全边界）
use_custom_stoploss = True  # 启用自定义止损逻辑
minimal_roi = {"0": 100} # 关闭固定止盈（100 = 永不触发）
max_open_trades = 1      # 同时最多 1 笔交易（专注单品种）
timeframe = "1h"         # 主时间框架：1小时
```

**关于 `stoploss = -0.15` 和 `custom_stoploss` 的关系：**

- `custom_stoploss` 是每根 K 线都会调用的动态止损逻辑
- `-0.15` 是**硬边界**：即使 `custom_stoploss` 返回了一个更宽的止损，freqtrade 也会把它截断到 -15%
- 这两个是"主控 + 保险"的关系，不是矛盾的

---

## 四、可优化参数

```python
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
# D1 识别 Swing Point 的回溯窗口：10 意味着一个高点必须是
# 左侧 10 根日线和右侧 10 根日线里的最高点，才算 Swing High。
# 数字越大 → Swing Point 越稀疏越重要
# 数字越小 → Swing Point 越密集越灵敏

swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
# 4H 级别的 Swing Point 回溯窗口，逻辑同上

pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
# Pin Bar 识别阈值：影线占整根 K 线的比例
# 60 表示 "影线占比 > 60% 才算 Pin Bar"
# 数字越大 → 只认最标准的 Pin Bar（更严格）
# 数字越小 → 稍微有点影线就算（更宽松）

max_stop_dist = IntParameter(20, 50, default=50, space="buy")
# 止损距离上限：入场价到支撑位的距离不能超过 50%
# 防止"支撑位太远、止损太大"的情况
# 实际上 default=50 几乎不过滤（等于没有限制）

cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v1.6 新增：冷却期（单位：1H K 线根数）
# 6 表示：上一个同方向信号出现后，6 小时内不再产生新信号
```

---

## 五、工具函数详解

### 5.1 `_detect_swing_points()` — 找摆动高低点

```python
for i in range(window, n - window):
    # 条件：第 i 根的 high > 左边 window 根的最高 AND > 右边 window 根的最高
    if high[i] > high[i-window:i].max() and high[i] > high[i+1:i+window+1].max():
        sh[i] = high[i]   # 这是一个 Swing High
    # 对称的逻辑找 Swing Low
    if low[i] < low[i-window:i].min() and low[i] < low[i+1:i+window+1].min():
        sl[i] = low[i]    # 这是一个 Swing Low
```

**视觉理解：**

```
                  ▲ Swing High（左右各 window 根都是低点）
                 /|\
                / | \
───────────────/  |  \───────────
```

**重要细节：** `range(window, n - window)` 意味着最后 `window` 根 K 线无法被识别为 Swing Point（因为需要看右边的数据）。这在实盘中意味着**最近的高低点要滞后 window 根才被确认**。

---

### 5.2 `_build_structure()` — 分析市场结构

这个函数是整个策略的大脑，逐根 K 线地维护 5 个状态：

#### （1）趋势判断

```python
# 记录最近 4 个 Swing High 和 4 个 Swing Low
sh_prices = []  # 最多保留 4 个
sl_prices = []

# 趋势判断逻辑（HH/HL vs LH/LL）
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
    trend_up = True    # HH (Higher High) + HL (Higher Low) = 上升趋势
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
    trend_down = True  # LH (Lower High) + LL (Lower Low) = 下降趋势
else:
    # 结构不明确时，继承上一根的趋势（趋势具有惯性）
    trend_up[i] = trend_up[i-1]
    trend_down[i] = trend_down[i-1]
```

**这就是价格行为学的核心：**
- 上升趋势 = Higher High + Higher Low（HH + HL）
- 下降趋势 = Lower High + Lower Low（LH + LL）

#### （2）最近支撑/阻力

```python
nearest_support[i] = sl_prices[-1]      # 最近的 Swing Low = 支撑
nearest_resistance[i] = sh_prices[-1]   # 最近的 Swing High = 阻力
```

**注意：** 这里直接取最后一个 Swing Low 作为支撑，没有考虑位置关系（支撑可能在当前价格之上）。这是一个潜在的逻辑弱点，后续优化可以考虑。

#### （3）供需区划分

```python
zone_range = resistance - support
pos_pct = (close - support) / zone_range

in_demand_zone = pos_pct < 0.35   # 价格在 support↔resistance 区间的底部 35%
in_supply_zone = pos_pct > 0.65   # 价格在 support↔resistance 区间的顶部 35%
```

**视觉理解：**

```
┌─────────────────────────────────┐
│ resistance ────────────────── │
│   供给区（Supply Zone, >65%）   │  ← 做空区域
│ ─────────────────────────── 65%│
│                                 │
│         中间区（35%~65%）        │  ← 不入场
│                                 │
│ ─────────────────────────── 35%│
│   需求区（Demand Zone, <35%）    │  ← 做多区域
│ support ──────────────────── │
└─────────────────────────────────┘
```

---

### 5.3 `_detect_candle_patterns()` — K 线形态识别

#### Pin Bar（Pin 柱/别针 K 线）

```python
body = abs(close - open_)             # 实体大小
total_range = high - low              # 整根 K 线的范围
upper_wick = high - max(close, open_) # 上影线长度
lower_wick = min(close, open_) - low  # 下影线长度

is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
# 当影线占比 > 60%，才算 Pin Bar

# 看涨 Pin Bar：阳线 + 下影线 > 上影线（价格被撑住了）
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)

# 看跌 Pin Bar：阴线 + 上影线 > 下影线（价格被压下来了）
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
```

**视觉理解：**

```
看涨 Pin Bar        看跌 Pin Bar
     │                  ██████
     │                    │
    ███                   │
     │                    │
  ███████              ███████
     │
  ███████
```

#### 吞没形态（Engulfing）

```python
# 看涨吞没：阳线，收盘 > 前根开盘，开盘 < 前根收盘
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)

# 看跌吞没：阴线，收盘 < 前根开盘，开盘 > 前根收盘
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
```

---

## 六、时间框架指标详解

### 6.1 D1 日线（`populate_indicators_1d`）

**只做一件事：确定宏观趋势方向**

- `trend_up_1d = True` → 日线是上升趋势（HH + HL）
- `trend_down_1d = True` → 日线是下降趋势（LH + LL）

回溯窗口 `swing_lookback_d1 = 10`，意味着确认一个日线 Swing Point 需要左右各 10 天，合计 20 天。这是有意为之——**日线趋势应该稳定，不应该频繁切换**。

---

### 6.2 4H 四小时（`populate_indicators_4h`）

**做五件事：**

1. `trend_up_4h` / `trend_down_4h`：4H 中期趋势（与 D1 配合，双重确认）
2. `support_4h`：最近的 4H Swing Low（止损基准点）
3. `resistance_4h`：最近的 4H Swing High（做空止损基准点）
4. `in_demand_4h`：当前 1H 价格是否在 4H 需求区（做多区域）
5. `in_supply_4h`：当前 1H 价格是否在 4H 供给区（做空区域）

**v1.6 新增：活支撑/阻力判断**

```python
# 支撑"活"的判断：
# 条件1：最近某根 4H K 线的 low，触及 support ± 0.5% 的范围
touched_support = (low <= support * 1.005) & (low >= support * 0.995)

# 条件2：那根 K 线的收盘价在 support 之上（撑住了，没跌穿）
held_support = close > support

# 两个条件都满足，才算"一次有效测试"
support_tested_and_held = touched_support & held_support

# 在最近 3 根 4H K 线内，至少发生过一次有效测试 → 支撑是"活"的
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
```

**为什么要"活支撑"？**

"死支撑"是指那些很久以前的 Swing Low，现在价格距离它很远，支撑位已经失效。例如：

```
3个月前的 Swing Low = 1500 美元（支撑）
但价格从那之后一直在 2000-2500 之间波动，从未回测过 1500
→ 1500 的支撑已经是"死"的，在那里做多没有意义
```

v1.6 要求：支撑必须是**最近 3 根 4H K 线内有价格来测试过的**，才算有效。

**"活支撑" 的问题（客观评价）：**
- `rolling(3)` = 最近 12 小时（3 × 4H）内测试过
- 这个窗口是否合适？12 小时可能太短，也可能太长
- 是未来优化的方向之一

---

### 6.3 1H 一小时（`populate_indicators`）

**只做一件事：识别 K 线反转形态**

```python
dataframe["bullish_signal"] = bullish_pin | bullish_engulf  # 两种看涨形态满足任一
dataframe["bearish_signal"] = bearish_pin | bearish_engulf  # 两种看跌形态满足任一
```

**NaN 安全处理：**
由于不同时间框架的数据合并（D1、4H → 1H）会产生空值，这里统一对布尔列做 `fillna(False)`，防止 `True & NaN = NaN` 导致信号丢失。

---

## 七、入场逻辑详解

### 做多条件（6 个条件全部满足）

```python
long_base = (
    dataframe["trend_up_1d"]           # 条件1：日线是上升趋势
    & dataframe["in_demand_4h"]        # 条件2：当前价格在 4H 需求区（支撑附近）
    & dataframe["bullish_signal"]      # 条件3：1H 出现看涨 K 线形态
    & (long_stop_dist <= max_dist)     # 条件4：止损距离 ≤ 50%（几乎不过滤）
    & (long_stop_dist > 0.003)         # 条件5：止损距离 > 0.3%（防止支撑=开盘价）
)

long_base = long_base & dataframe["support_alive_4h"]   # 条件6：支撑是"活"的 [v1.6]

long_recent = long_base.rolling(cooldown).max().shift(1) == 0  # 冷却期过滤 [v1.6]
long_conditions = long_base & long_recent
```

**条件5 的意义：** `long_stop_dist > 0.003` 防止"支撑位和开盘价几乎相同"的情况，那样止损太近，任何正常波动都会触发。

### 冷却期的工作原理

```python
# long_base 是"条件1-6都满足"的信号（不含冷却）
# rolling(6).max() → 过去6根1H bar内，是否有过满足条件的信号（1=有，0=没有）
# .shift(1) → 往后移一格，避免当前这根K线跟自己比较
# == 0 → 只有"过去6小时内没有信号"时，才允许当前入场

long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
```

**视觉理解（cooldown=6）：**

```
时间 →  1h  2h  3h  4h  5h  6h  7h  8h  9h  10h
信号       ✓                           ✓
冷却期         ←── 6小时冷却 ──→
允许入场   ✓                           ✓   （第10小时重新可以入场）
```

### 做空条件（对称）

```python
short_base = (
    dataframe["trend_down_1d"]          # 条件1：日线是下降趋势
    & dataframe["in_supply_4h"]         # 条件2：价格在 4H 供给区（阻力附近）
    & dataframe["bearish_signal"]       # 条件3：1H 出现看跌 K 线形态
    & (short_stop_dist <= max_dist)     # 条件4：止损距离合理
    & (short_stop_dist > 0.003)         # 条件5：止损距离不为零
)
short_base = short_base & dataframe["resistance_alive_4h"]  # 条件6：阻力是"活"的
```

---

## 八、出场逻辑详解

```python
exit_long = ~dataframe["trend_up_1d"].fillna(True)   # 当 D1 趋势不再上升时平多
exit_short = dataframe["trend_up_1d"].fillna(False)   # 当 D1 趋势转为上升时平空
```

**这个逻辑非常简单，也非常粗糙：**

- 只有 D1 趋势反转时才触发出场信号
- D1 趋势需要明确的 HH+HL 或 LH+LL 才会切换，这需要较长时间
- 结果：大多数交易不会触发 exit_signal，而是由止损（stop_loss 或 trailing_stop_loss）退出

这也是为什么回测中 `exit_signal` 只有 7-9 笔，而止损退出有 70-100 笔。

---

## 九、动态止损详解（最核心的部分）

```python
def custom_stoploss(self, pair, trade, current_time, current_rate, current_profit, ...):
    last = dataframe.iloc[-1]  # 取最新的 4H 数据

    if not trade.is_short:  # 多单
        support = last["support_4h"]          # 最近的 4H Swing Low
        sl_price = support * 0.999            # 止损价 = 支撑位下方 0.1%
        sl_ratio = (sl_price / current_rate) - 1.0  # 转换为相对于当前价格的比率
        return max(sl_ratio, -0.15)           # 不超过 -15% 的硬边界
    else:                   # 空单
        resistance = last["resistance_4h"]    # 最近的 4H Swing High
        sl_price = resistance * 1.001         # 止损价 = 阻力位上方 0.1%
        sl_ratio = 1.0 - (sl_price / current_rate)
        return min(sl_ratio, 0.15)            # 不超过 +15% 的硬边界
```

### 为什么这会产生"自然追踪止损效果"？

关键在于 `support_4h` 是**动态更新**的：

```
t=0  入场，support_4h = 1800（止损在 1798.2）
     价格从 1850 开始上涨
t=5h 价格涨到 2000，出现新 Swing Low = 1900
     → support_4h 更新为 1900
     → 止损自动上移到 1898.1
t=10h 价格涨到 2200，新 Swing Low = 2050
     → support_4h 更新为 2050
     → 止损上移到 2047.9
```

这就是为什么 `trailing_stop_loss` 出现那么多：每当价格创新高、产生新的 Swing Low，止损就自动追上去，锁住利润。

### 止损的数学计算示例

假设：
- 当前价格 = 2000 USDT
- support_4h = 1900 USDT

```python
sl_price = 1900 * 0.999 = 1898.1
sl_ratio = (1898.1 / 2000) - 1.0 = -0.0595（约 -5.95%）
```

即：止损触发点在当前价格的 5.95% 下方。

---

## 十、退出原因分类（理解 freqtrade 的逻辑）

| 退出原因 | 触发条件 | v1.6 ETH 全周期 |
|----------|----------|-----------------|
| `stop_loss` | 价格触及 `custom_stoploss` 返回的价格，且 current_profit < 0 | 73 笔，0% 胜率 |
| `trailing_stop_loss` | 价格从高点回落，触及之前计算的止损价，且有过盈利 | 71 笔，67.6% 胜率 |
| `exit_signal` | `populate_exit_trend` 返回 exit=1 | 7 笔，71.4% 胜率 |
| `force_exit` | 回测结束强制平仓 | 1 笔，100% |

**`stop_loss` 和 `trailing_stop_loss` 的本质区别：**

- `stop_loss`：交易从入场到退出**从未产生过盈利**。价格一直往不利方向走，直到触发止损。
- `trailing_stop_loss`：交易曾经**有过盈利**，后来价格回落，触及追踪止损位退出。

**这说明了什么？**

73 笔 `stop_loss` = 73 笔入场后价格立即向不利方向走的交易。

这些交易**入场位是错的**，或者**在错误的时机做了正确的方向**。这是 v1.6 最核心的问题，也是下一步优化的主攻方向。

---

## 十一、已知问题和局限性

### 问题 1：`stop_loss` 胜率 0%（最严重）
- 表现：73 笔 stop_loss 退出，全部亏损
- 根本原因：入场后价格立即反向，说明入场位质量不高
- 可能方向：更严格的入场过滤、更好的时机判断

### 问题 2：支撑位识别的逻辑漏洞
- `nearest_support = sl_prices[-1]` 直接取最近的 Swing Low，可能在当前价格之上
- 例如：价格在 2000，但最近 Swing Low 是 2100（价格刚从 2100 跌下来），这时支撑 = 2100 > 2000，止损价在当前价格上方，逻辑矛盾
- `long_stop_dist > 0.003` 一定程度上过滤了这种情况，但不完全

### 问题 3：出场逻辑太粗糙
- 只看 D1 趋势是否反转
- D1 趋势切换很慢，大多数交易靠止损退出，而非出场信号
- 可以考虑增加 4H 结构破坏作为出场信号

### 问题 4：市场环境依赖
- 趋势市（2023-2024）：表现优异
- 震荡市（2025 YTD BTC）：表现差
- 没有"市场环境过滤器"，在震荡市会频繁入场并被止损

### 问题 5：`fillna(False)` 的 FutureWarning
- 技术性问题，不影响结果，但需要修复（pandas 未来版本会报错）

---

## 十二、数据流的完整路径

一笔做多交易的完整生命周期：

```
1. D1 数据 → _detect_swing_points() → _build_structure()
   → 输出 trend_up_1d = True （日线上升趋势确认）

2. 4H 数据 → _detect_swing_points() → _build_structure()
   → 输出 support_4h = 1900, resistance_4h = 2100
   → 输出 in_demand_4h = True （价格在需求区）
   → v1.6: 检查 support_alive_4h = True （支撑最近被测试过）

3. 1H 数据 → _detect_candle_patterns()
   → 输出 bullish_signal = True （出现看涨 Pin Bar 或吞没）

4. populate_entry_trend() 汇总所有条件
   → 全部满足 + 冷却期过了 → enter_long = 1 → 下单做多

5. 持仓期间，每根 1H K 线：
   → custom_stoploss() 被调用
   → 读取最新 support_4h
   → 计算止损价 = support_4h * 0.999
   → 止损位随 support_4h 上移（追踪效果）

6. 退出：
   → 价格创新高后回落触及止损 → trailing_stop_loss 退出
   → 或价格入场后就下跌 → stop_loss 退出
   → 或 D1 趋势反转 → exit_signal 退出
```

---

## 十三、优化的可能方向（供后续参考）

基于以上分析，以下是最有价值的优化方向（按优先级排序）：

**优先级 A：解决 stop_loss 0% 胜率**
1. 分析 73 笔 stop_loss 交易的特征（何时入场？市场处于什么状态？）
2. 增加入场质量过滤（例如：要求 4H 也是上升趋势、要求成交量确认）
3. 改进 Swing Point 识别（例如：要求支撑位被测试 2 次以上）

**优先级 B：增加市场环境过滤**
1. 识别"震荡市"（例如：最近 N 根 4H bar 内，没有明确的 HH/HL 序列）
2. 在震荡市中停止交易，只在趋势市中工作

**优先级 C：改进出场逻辑**
1. 将出场信号细化到 4H 级别（4H 结构破坏时退出，而不等 D1 反转）
2. 可以大幅减少 stop_loss 次数，让更多交易变成 exit_signal 或 trailing_stop_loss

---

*文档基于 `structure_flow_strategy_v1_6.py` 代码，v1.6 版本（2026-06-07）*
