功能

止损与止盈

回测止损、移动止损、止盈、时间止损和阶梯离场

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

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,  
    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()
比特币均线策略在 2% 至 16% 止损及 5% 至 40% 止盈下的夏普比率热图 图表数据(JSON)

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

止损与目标

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

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),  
    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

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

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

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

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

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

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

移动止盈

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

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

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% 止损。阈值改变移动离场何时接管。示例只检查给出的收盘价,因此离场在这些收盘价成交。

按时间离场

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

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

自适应止损

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

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)  
        target = sl_info["init_price"] + sl_info["stop"]
        if sl_info["stop"] > 0 and prev_close >= target:
            sl_info["stop"] = 0.0  

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

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

加仓

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

阶梯止损与部分离场

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

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

止损市价与止损限价离场

默认触发止损创建市价单。stop_order_type="limit" 改为创建限价单,stop_limit_delta 设置相对止损离场价的偏移。这样可比较触发后立即离场与等待指定价格。若始终未达到该价,限价可能无法成交。待成交订单和价格选择参见订单与执行。

真实止损成交

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

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 控制此行为:

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 线收盘衡量。

紧止损应使用更细数据

止损相对 K 线振幅较小时,许多根会触及两个水平,结果由悲观规则决定。应在日内数据上测试,此时同时包含两个水平的 K 线少得多。

模拟前计算止损信号

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

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 := {}),  
)
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

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

阶梯止损

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

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 持有交易,采用五级等量阶梯止盈离场 图表数据(JSON)

时间止损

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

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")  
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

版权所有 © 2021–2026 Oleg Polakow。保留所有权利。

本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。