# 止损与止盈 (/zh/features/backtesting/stop-loss-and-take-profit)

离场决定了策略的大部分风险。VBT 可将止损、移动止损、止盈和时间止损作为 `vbt.PF.from_signals` 参数，在 Python
中回测，并一次测试完整参数网格。止损对每根开盘、最高、最低和收盘价检查，明确规定谁先成交。

```python title="Test 64 stop loss and take profit combinations at once"
>>> data = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2024-01-01")
>>> fast = data.close.rolling(20).mean()
>>> slow = data.close.rolling(50).mean()
>>> entries = fast.vbt.crossed_above(slow)

>>> pf = vbt.PF.from_signals(
...     data,
...     entries,  # (1)
...     sl_stop=vbt.Param(np.arange(2, 17, 2) / 100),
...     tp_stop=vbt.Param(np.arange(5, 45, 5) / 100),
...     fees=0.001,
... )
>>> pf.sharpe_ratio.sort_values(ascending=False).head(3)
sl_stop  tp_stop
0.08     0.4        0.941107
0.12     0.4        0.917926
0.08     0.1        0.910485
Name: sharpe_ratio, dtype: float64

>>> pf.sharpe_ratio.vbt.heatmap(x_level="sl_stop", y_level="tp_stop").show()
```

1.  没有离场信号，每个持仓都由止损或止盈关闭。

比特币均线策略在 2% 至 16% 止损及 5% 至 40% 止盈下的夏普比率热图. [Figure data (JSON)](/assets/figures/features/backtesting/stop-loss-take-profit-grid.45ce7fc98a45.json)

最佳单元位于网格边缘，与邻近值差异很大。热图让您在依据单个最优值决策前，就能看到这一点。

## 止损与目标 \[#stops-and-targets]

内置四种价格止损机制。`sl_stop` 为止损，`tsl_stop` 为跟随入场以来最佳价格的移动止损，`tp_stop` 为止盈，`tsl_th`
将移动止损变成移动止盈，只有价格变动达到阈值后才开始跟随。默认都以入场价格的比例衡量：

```python title="Exit by take profit or by trailing stop"
>>> index = pd.date_range("2026-01-01", periods=5)
>>> ohlc = pd.DataFrame({
...     "open": [100.0, 101.0, 104.0, 108.0, 103.0],
...     "high": [101.0, 105.0, 111.0, 109.0, 104.0],
...     "low": [99.0, 100.0, 103.0, 102.0, 94.0],
...     "close": [100.0, 104.0, 108.0, 103.0, 95.0],
... }, index=index)
>>> entries = pd.Series([True, False, False, False, False], index=index)

>>> pf = vbt.PF.from_signals(
...     ohlc["close"],
...     entries,
...     open=ohlc["open"],
...     high=ohlc["high"],
...     low=ohlc["low"],
...     sl_stop=0.05,
...     tsl_stop=vbt.Param([np.nan, 0.04], level=0),  # (1)
...     tp_stop=vbt.Param([0.1, np.nan], level=0),
... )
>>> pf.orders.readable[["Column", "Fill Index", "Side", "Price", "Stop Type"]]
        Column Fill Index  Side   Price Stop Type
0   (nan, 0.1) 2026-01-01   Buy  100.00      None
1   (nan, 0.1) 2026-01-03  Sell  110.00        TP
2  (0.04, nan) 2026-01-01   Buy  100.00      None
3  (0.04, nan) 2026-01-04  Sell  106.56       TSL
```

1.  共用 `level=0` 将数值配对，而非构建笛卡尔积：先单独 10% 止盈，再单独 4% 移动止损。

止盈在第三根以 110 美元成交。移动止损从 111 美元最高价向下跟随至 106.56 美元，在第四根成交。`delta_format="absolute"`
将止损值解释为价格距离而非比例，`"target"` 则解释为价格水平。

止损值与其他参数一样可广播，因此能按资产、K 线和方向不同。多空使用不同止损时，可在多头入场位置填多头值，在空头入场位置填空头值。止损在开仓时读取，之后保持固定，除非回调修改。

止损从入场后一根才检查，即使入场在开盘成交，因为无法判断本根最高与最低在成交之前还是之后。触发时平仓。`stop_exit_type="reverse"` 使同一订单还开立反向持仓。

将止损与止盈结合，可为每个持仓测试一组保护边界。任一规则平仓后，VBT 清除其他止损，旧目标不会影响后续持仓。

风险收益比设置只需两个数组。这里风险是到过去五根最低价的距离，目标为该距离的两倍：

```python title="Set the stop below recent lows and the target at 2R"
>>> data = vbt.YFData.pull("BTC-USD", start="2023-01-01", end="2024-01-01")
>>> entries = data.close.vbt.crossed_above(data.close.rolling(20).max().shift(1))
>>> risk = data.close - data.low.rolling(5).min()

>>> pf = vbt.PF.from_signals(
...     data,
...     entries,
...     sl_stop=risk,
...     tp_stop=2 * risk,
...     delta_format="absolute",
... )
>>> cols = ["Entry Index", "Avg Entry Price", "Avg Exit Price", "Return"]
>>> pf.trades.readable[cols].head(4)
                Entry Index  Avg Entry Price  Avg Exit Price    Return
0 2023-01-23 00:00:00+00:00     22934.431641    20685.380859 -0.098064
1 2023-03-14 00:00:00+00:00     24746.074219    34981.714844  0.413627
2 2023-10-29 00:00:00+00:00     34538.480469    36781.667969  0.064947
3 2023-11-15 00:00:00+00:00     37880.582031    43744.746094  0.154807
```

## 移动止盈 \[#trailing-take-profit]

固定目标到达即离场；移动止盈允许走势继续，回调时再离场。`tsl_th` 设置激活所需涨幅，`tsl_stop` 设置跟随距离。达到阈值前，可保留独立 `sl_stop`。

同一价格路径先回落，再上涨超过 10%。一个组合从一开始跟随，另一个等达到该涨幅后才启用 5% 移动离场：

```python title="Start trailing only after a 10% gain"
>>> index = pd.date_range("2026-01-01", periods=6)
>>> close = pd.Series([100.0, 104.0, 98.0, 112.0, 120.0, 113.0], index=index)
>>> pf = vbt.PF.from_signals(
...     close,
...     entries=[True, False, False, False, False, False],
...     sl_stop=0.1,
...     tsl_stop=0.05,
...     tsl_th=vbt.Param([np.nan, 0.1], keys=["trail immediately", "trail after 10%"]),
... )
>>> pf.orders.side_sell.readable[["Column", "Fill Index", "Price", "Stop Type"]]
              Column Fill Index  Price Stop Type
0  trail immediately 2026-01-03   98.0       TSL
1    trail after 10% 2026-01-06  113.0       TTP
```

两个组合都有 10% 止损。阈值改变移动离场何时接管。示例只检查给出的收盘价，因此离场在这些收盘价成交。

## 按时间离场 \[#time-based-exits]

时间止损在持有一定时长后或指定时刻离场，不受价格影响。`td_stop` 接受 `"7D"` 等时长，或配合 `time_delta_format="rows"` 接受 K 线数量。`dt_stop`
接受日期、`"M"`（月末）等周期，或 `"18:00"` 等日内时间。持有期从入场 K 线开盘计起，因此时间止损在到期 K
线开盘成交。下方[时间止损](#time-stops)在月末前关闭每个持仓。

```python title="Exit five bars after each entry"
>>> pf = vbt.PF.from_signals(data, entries, td_stop=5, time_delta_format="rows")
>>> pf.orders.readable[["Fill Index", "Side", "Stop Type"]].head(4)
                 Fill Index  Side Stop Type
0 2023-01-23 00:00:00+00:00   Buy      None
1 2023-01-28 00:00:00+00:00  Sell        TD
2 2023-01-29 00:00:00+00:00   Buy      None
3 2023-02-03 00:00:00+00:00  Sell        TD
```

## 自适应止损 \[#adaptive-stops]

按波动率缩放的止损也是数组。传入 ATR 倍数，配合 `delta_format="absolute"`，每笔交易就会获得符合入场市场状况的距离。距离只读取一次，因此止损序列不会让移动止损跟随当前
ATR。若要在持仓期间移动止损，应编写调整函数。它每根开始运行，可修改 `c.last_sl_info` 中的活动止损。这里在上一收盘已盈利一个风险单位后，将止损移到入场价：

```python title="Move an ATR stop to breakeven after 1R"
>>> @njit
... def breakeven_nb(c):
...     sl_info = c.last_sl_info[c.col]
...     if c.last_position[c.col] > 0 and vbt.pf_nb.is_stop_info_active_nb(sl_info):
...         prev_close = vbt.pf_nb.select_nb(c, c.close, i=c.i - 1)  # (1)
...         target = sl_info["init_price"] + sl_info["stop"]
...         if sl_info["stop"] > 0 and prev_close >= target:
...             sl_info["stop"] = 0.0  # (2)

>>> atr = data.run("atr", window=14).atr
>>> kwargs = dict(sl_stop=2 * atr, tp_stop=6 * atr, delta_format="absolute")
>>> fixed = vbt.PF.from_signals(data, entries, **kwargs)
>>> breakeven = vbt.PF.from_signals(data, entries, adjust_func_nb=breakeven_nb, **kwargs)
>>> pd.DataFrame({
...     "entry": fixed.trades.readable["Entry Index"],
...     "fixed": fixed.trades.readable["Return"],
...     "breakeven": breakeven.trades.readable["Return"],
... }).iloc[3:6]
                      entry     fixed  breakeven
3 2023-05-28 00:00:00+00:00 -0.056052  -0.056052
4 2023-06-20 00:00:00+00:00 -0.060298   0.000000
5 2023-10-01 00:00:00+00:00 -0.041770  -0.041770
```

1.  函数在当前 K 线已知前运行，因此基于上一收盘决策。
2.  距离为零，将止损放在入场价。

六月交易上涨超过初始风险后回落。固定止损亏损 6%，保本规则则平价离场。同一模式可按更高周期收紧止损、使用吊灯止损距离跟随，或加仓后按平均入场价重新设置止损。

!!! info "加仓"
    每种止损每列保留一个活动水平。入场增加持仓时，`upon_stop_update` 决定保留已有止损，还是用新止损替换。

## 阶梯止损与部分离场 \[#stop-ladders-and-partial-exits]

止损通常关闭全部持仓。配合 `stop_ladder` 传入止损值列表，则在连续水平分批离场。`"uniform"` 每级退出相同比例，`"weighted"`
根据距上一级的距离确定每次数量，自适应变体则根据剩余持仓重新计算。下方[阶梯止损](#stop-laddering)并列测试两种阶梯止盈。

部分止损离场后，阶梯机制保持余下止损有效，将分批止盈与剩余持仓保护结合。若需控制每一级，可在回调中设置止损记录的 `exit_size` 和 `exit_type`，例如反转而非平仓。

## 止损市价与止损限价离场 \[#stop-market-and-stop-limit-exits]

默认触发止损创建市价单。`stop_order_type="limit"` 改为创建限价单，`stop_limit_delta`
设置相对止损离场价的偏移。这样可比较触发后立即离场与等待指定价格。若始终未达到该价，限价可能无法成交。待成交订单和价格选择参见[订单与执行](/zh/features/backtesting/orders-and-execution/#pending-limit-and-stop-entry-orders)。

## 真实止损成交 \[#realistic-stop-fills]

日线隐藏了日内事件顺序。VBT 将每根拆为开盘、开收盘之间及收盘，先成交先触及者。若止损与止盈在同一部分同时达到，止损优先：

```python title="Resolve a bar that hits both the stop and the target"
>>> index = pd.date_range("2026-01-01", periods=2)
>>> pf = vbt.PF.from_signals(
...     pd.Series([100.0, 100.0], index=index),
...     pd.Series([True, False], index=index),
...     open=pd.Series([100.0, 100.0], index=index),
...     high=pd.Series([100.0, 112.0], index=index),
...     low=pd.Series([100.0, 94.0], index=index),
...     sl_stop=0.05,
...     tp_stop=0.1,
... )
>>> pf.orders.readable[["Fill Index", "Side", "Price", "Stop Type"]]
  Fill Index  Side  Price Stop Type
0 2026-01-01   Buy  100.0      None
1 2026-01-02  Sell   95.0        SL
```

同样的悲观假设也用于移动止损：每根先检查止损，再允许新高提高止损位。若止损在本根不同部分触及，较早者成交。

跳空是另一陷阱。价格开盘越过止损时，真实订单按开盘而非止损水平成交。`stop_exit_price` 控制此行为：

```python title="Fill a stop after a gap down"
>>> pf = vbt.PF.from_signals(
...     pd.Series([100.0, 92.0], index=index),
...     pd.Series([True, False], index=index),
...     open=pd.Series([100.0, 90.0], index=index),
...     high=pd.Series([100.0, 93.0], index=index),
...     low=pd.Series([100.0, 89.0], index=index),
...     sl_stop=0.05,
...     stop_exit_price=vbt.Param(["stop", "hardstop", "close"]),
... )
>>> pf.orders.readable[["Column", "Side", "Price"]]
     Column  Side  Price
0      stop   Buy  100.0
1      stop  Sell   90.0
2  hardstop   Buy  100.0
3  hardstop  Sell   95.0
4     close   Buy  100.0
5     close  Sell   92.0
```

默认 `"stop"` 在跳空后按开盘成交；`"hardstop"` 总按止损水平成交，属于乐观假设；`"close"` 按本根收盘。入场侧，`stop_entry_price`
设置止损参考价。默认是订单价格，例如 `price="nextopen"` 时为开盘。`"fillprice"` 将滑点计入参考价，`"close"` 则以入场 K 线收盘衡量。

!!! warning "紧止损应使用更细数据"
    止损相对 K
    线振幅较小时，许多根会触及两个水平，结果由悲观规则决定。应在[日内数据](/zh/features/backtesting/intraday-and-tick-backtesting/)上测试，此时同时包含两个水平的
    K 线少得多。

## 模拟前计算止损信号 \[#stop-signals-before-simulation]

止损也可在没有投资组合时计算为离场信号，适合信号研究、标签生成，以及在确定仓位前检查止损触发频率：

```python title="Generate stop exits as signals"
>>> exits = entries.vbt.signals.generate_ohlc_stop_exits(
...     entry_price=data.close,
...     open=data.open,
...     high=data.high,
...     low=data.low,
...     close=data.close,
...     sl_stop=0.05,
...     tp_stop=0.1,
...     out_dict=(out := {}),  # (1)
... )
>>> stop_names = dict(enumerate(vbt.sig_enums.StopType._fields))
>>> pd.DataFrame({
...     "stop price": out["stop_price"][exits],
...     "stop type": out["stop_type"][exits].map(stop_names),
... }).head(4)
                             stop price stop type
Date
2023-02-09 00:00:00+00:00  22585.838086        SL
2023-02-24 00:00:00+00:00  23587.691016        SL
2023-03-17 00:00:00+00:00  27558.067969        TP
2023-03-22 00:00:00+00:00  26767.025586        SL
```

1.  收集每次触发止损的价格和类型。

默认仅在两个入场之间放置一次离场，因此止损仍等待时到达的新入场，不会启动新止损。

## 阶梯止损 \[#stop-laddering]

新增于 1.12.0

✅
阶梯止损是一种逐步退出持仓的技术。无需提供单一止损值一次平仓，而可提供止损数组，各水平触发时退出一定数量。选择不同阶梯模式即可控制数量。新的广播功能允许数组仅沿一个轴广播，因此止损值无需与数据形状相同，甚至可将不同形状的止损数组作为参数！

```python title="Test two TP ladders"
>>> data = vbt.YFData.pull("BTC-USD", end="2017-01")
>>> pf = vbt.PF.from_holding(
...     data,
...     stop_ladder="uniform",
...     tp_stop=vbt.Param([
...         [0.1, 0.2, 0.3, 0.4, 0.5],
...         [0.4, 0.5, 0.6],
...     ], keys=["tp_ladder_1", "tp_ladder_2"])
... )
>>> pf.trades.plot(column="tp_ladder_1").show()
```

BTC-USD 持有交易，采用五级等量阶梯止盈离场. [Figure data (JSON)](/assets/figures/features/portfolio/stop-laddering.e265b961a2c9.json)

## 时间止损 \[#time-stops]

新增于 1.11.0

✅ 除其他止损单外，时间止损可在经过一定时长或指定日期时平仓。

```python title="Enter randomly, exit before the end of the month"
>>> data = vbt.YFData.pull("BTC-USD", start="2022-01", end="2022-04")
>>> entries = vbt.pd_acc.signals.generate_random(data.symbol_wrapper, n=10, seed=42)
>>> pf = vbt.PF.from_signals(data, entries, dt_stop="M")  # (1)
>>> pf.orders.readable[["Fill Index", "Side", "Stop Type"]]
                 Fill Index  Side Stop Type
0 2022-01-19 00:00:00+00:00   Buy      None
1 2022-01-31 00:00:00+00:00  Sell        DT
2 2022-02-25 00:00:00+00:00   Buy      None
3 2022-02-28 00:00:00+00:00  Sell        DT
4 2022-03-11 00:00:00+00:00   Buy      None
5 2022-03-31 00:00:00+00:00  Sell        DT
```

1.  日期时间止损使用 `dt_stop`，时间差止损使用 `td_stop`。日期时间止损可为周期（"D"）、时间戳（"2023-01-01"），甚至指定时间（"18:00"）。
