如何设置基于百分比的追踪止损
移动止损(Trailing Stop)能以一个随着价格向有利方向移动而不断优化的价格来平仓。虽然TradingView支持移动止损,但它无法直接基于百分比来设置。不过幸运的是,我们可以自己编写代码来实现这一功能。让我们看看具体怎么做。
移动止损的原理是,先在市价下方(对于多头)或上方(对于空头)设置一个初始的止损价。然后,当市场价格向对我们有利的方向移动时,只要能获得一个更优的执行价格,止损位就会随之更新。这不仅能限制交易风险,理想情况下,止损位还能移动足够多的距离来锁定部分利润。
我们通常使用 strategy.order() 或 strategy.exit() 函数来提交平仓的止损单。虽然 strategy.exit() 功能最灵活,但它也只能发送基于固定价格和固定点数的移动止损。然而,基于百分比的止损在很多时候也同样非常有用。
试想一下,如果我们进行一个长期的历史回测,期间交易品种的价格已经发生了巨大的涨跌。在这种情况下,一个固定点数的移动止损就显得不太实用了。因为当品种价格很高时,这个固定点数的止损可能离市价太近,容易被正常波动扫掉;而当品种价格很低时,同样点数的止损又可能离市价太远,起不到有效的保护作用。
一个可行的解决方案就是使用基于百分比的移动止损。这种止损能始终与市价保持一个固定的相对距离。下面我们来看看如何在PineScript中实现它。
实现百分比移动止损的步骤
要让策略脚本能够提交基于百分比的移动止损订单,我们需要执行三步:先(可选地)创建一个用户输入框,用于灵活配置移动止损的百分比;然后使用这个百分比计算出实际的移动止损价格;最后根据计算出的价格,提交具体的止损订单。让我们来逐一审视这些步骤及其所需的代码。
第一步,通过输入选项设置移动止损百分比。创建输入选项,可以让我们在策略设置界面中方便地调整移动止损的百分比,而无需每次都去修改策略的源代码。为了能够独立地配置多头和空头的止损,我们分别为它们创建输入选项:
// 通过输入选项配置移动止损水平(可选)
longTrailPerc = input.float(3, title="多头移动止损 (%)",
minval=0.0, step=0.1) * 0.01
shortTrailPerc = input.float(3, title="空头移动止损 (%)",
minval=0.0, step=0.1) * 0.01
我们使用 input.float() 函数创建了两个浮点数输入框。第一个名为”多头移动止损 (%)”,默认值为3(即3%),其最小值设为0以防意外输入负值。我们将这个输入框的当前值存储在 longTrailPerc 变量中。另一个输入框”空头移动止损 (%)”也同样默认从3%开始,且最小值也为0,它的值由 shortTrailPerc 变量来跟踪。
请注意,我们将每个输入框的值都乘以了0.01。这是因为,虽然我们在界面上设置的是百分比(如3%或17.5%),但在后续的计算中,使用小数形式(如0.03和0.175)会更加方便。因此,我们通过乘以0.01将百分比转换为了小数。
(如果你的策略不需要这些输入,只需知道在本文后续部分,longTrailPerc 和 shortTrailPerc 分别代表多头和空头的移动止损百分比(小数形式)即可。)
第二步,确定多头和空头的移动止损价格。有了以变量形式存储的移动止损百分比,我们就可以用它们来计算出移动止损单应该设置的具体价格了。为此,我们分两段代码来处理。首先计算多头的移动止损价格:
// 确定移动止损价格
longStopPrice = 0.0
longStopPrice := if strategy.position_size > 0
stopValue = close * (1 - longTrailPerc)
math.max(stopValue, longStopPrice[1])
else
0
这段代码的逻辑有些复杂,我们来分解一下。首先,我们声明了 longStopPrice 变量,然后通过一个 if/else 语句为其赋予实际的值。
if 的条件是判断策略当前是否持有多头仓位(strategy.position_size > 0)。如果是,我们就需要确定多头的止损价,这分为两步:
首先,我们计算出一个临时的止损价 stopValue,它的值是当前K线的收盘价乘以 (1 - longTrailPerc)。用1减去止损百分比,是因为多头止损的初始位置应该在当前市价的下方。例如,如果 longTrailPerc 是0.07(7%),那么 stopValue 就是 close * 0.93。
当然,我们需要的不仅仅是一个静态的百分比止损,它还需要能够移动。math.max() 函数的作用就在于此。它会比较刚刚计算出的 stopValue 和 longStopPrice 在前一根K线上的值(longStopPrice[1]),并返回两者中较大的那个。
通过这种方式,与前一根K线相比,我们的止损价只能提高或保持不变,绝不会降低。由于这个结果在每根K线上都会被重新赋值给 longStopPrice 变量,就实现了多头止损只上不下的移动效果。
当策略没有多头仓位时,else 部分的代码会执行,将 longStopPrice 重置为0,以便下一次开多仓时能重新开始计算。
第二段代码用于计算空头的移动止损价格:
// 确定移动空头价格
shortStopPrice = 0.0
shortStopPrice := if strategy.position_size < 0
stopValue = close * (1 + shortTrailPerc)
math.min(stopValue, shortStopPrice[1])
else
999999
这段代码的逻辑与多头非常相似。if 条件判断策略是否持有空头仓位。如果是,我们计算的 stopValue 是 close * (1 + shortTrailPerc),因为空头止损的初始位置应在市价的上方。
接着,我们使用 math.min() 函数来比较当前的 stopValue 和前一根K线的 shortStopPrice,并取其较小值。这样,空头的移动止损就只会向下移动或保持不变。
当策略没有空头仓位时,else 部分会将 shortStopPrice 重置为一个非常大的数999999。这样做是为了确保在下一次开空仓时,math.min() 能够正确地取到新计算出的 stopValue 作为初始止损价,而不是被一个过小的前期值(如0)所干扰。
注意:在本文中,我们是基于收盘价(
close)来移动止损的。你也可以根据自己的需要,使用K线的最低价(low)来移动多头止损,或使用最高价(high)来移动空头止损,只需替换相应的变量即可。
第三步,提交策略的移动止损订单。计算出多空移动止损的具体价格后,我们就可以通过 strategy.exit() 函数来提交实际的止损订单了:
// 为移动止损价格提交平仓订单
if strategy.position_size > 0
strategy.exit("XL TRL STP", stop=longStopPrice)
if strategy.position_size < 0
strategy.exit("XS TRL STP", stop=shortStopPrice)
第一个 if 语句检查策略是否持有多头仓位。如果是,就调用 strategy.exit() 发送一个名为 “XL TRL STP” 的止损单,其价格就是我们动态计算的 longStopPrice。第二个 if 语句则处理空头仓位,提交一个价格为 shortStopPrice 的止损单。
通过这种方式,虽然 strategy.exit() 函数本身不支持基于百分比的移动止损,但我们通过自己计算出百分比对应的具体价格,然后利用该函数基于价格的 stop 参数,巧妙地实现了这一功能。
注意:我们也可以使用
strategy.order()函数来提交止损单。但该函数需要我们自己指定订单数量,并且可能会意外地开立反向仓位。为了方便起见,本文统一使用strategy.exit()函数。
示例策略:使用基于百分比的移动止损进行交易
下面的示例策略整合了我们上面讨论的所有三个步骤。该脚本基于两条移动平均线的交叉进行交易,当快线上穿慢线时做多,下穿时则做空。
该策略有两种平仓方式:一是当均线反向交叉时,仓位会反转;二是通过移动止损退出。我们初始将移动止损设在距离市价3%的位置。当价格向不利方向移动时,止损位会触发,以防止更大的亏损。
策略的完整代码如下:
//@version=5
strategy(title="Trailing stop loss (% of instrument price)",
overlay=true, pyramiding=3)
// 步骤 1:
// 通过输入选项配置移动止损水平(可选)
longTrailPerc = input.float(3, title="多头移动止损 (%)",
minval=0.0, step=0.1) * 0.01
shortTrailPerc = input.float(3, title="空头移动止损 (%)",
minval=0.0, step=0.1) * 0.01
// 计算移动平均线
fastSMA = ta.sma(close, 20)
slowSMA = ta.sma(close, 60)
// 计算交易条件
enterLong = ta.crossover(fastSMA, slowSMA)
enterShort = ta.crossunder(fastSMA, slowSMA)
// 绘制移动平均线
plot(fastSMA, color=color.teal)
plot(slowSMA, color=color.orange)
// 步骤 2:
// 确定移动止损价格
longStopPrice = 0.0
shortStopPrice = 0.0
longStopPrice := if strategy.position_size > 0
stopValue = close * (1 - longTrailPerc)
math.max(stopValue, longStopPrice[1])
else
0
shortStopPrice := if strategy.position_size < 0
stopValue = close * (1 + shortTrailPerc)
math.min(stopValue, shortStopPrice[1])
else
999999
// 绘制止损位以供确认
plot(strategy.position_size > 0 ? longStopPrice : na,
color=color.fuchsia, style=plot.style_cross,
linewidth=2, title="多头移动止损位")
plot(strategy.position_size < 0 ? shortStopPrice : na,
color=color.fuchsia, style=plot.style_cross,
linewidth=2, title="空头移动止损位")
// 提交开仓订单
if enterLong
strategy.entry("EL", strategy.long)
if enterShort
strategy.entry("ES", strategy.short)
// 步骤 3:
// 为移动止损价格提交平仓订单
if strategy.position_size > 0
strategy.exit("XL TRL STP", stop=longStopPrice)
if strategy.position_size < 0
strategy.exit("XS TRL STP", stop=shortStopPrice)
让我们看看这个策略的实际表现。下图中的标普500 CFD空头交易发生在下跌趋势接近尾声时。但随着价格继续下跌,移动止损也随之向下移动。
在交易过程中,市场的最低收盘价达到了2,804.3。此后价格开始缓慢回升,移动止损在2,888.5被触发。从那个最低收盘价算起,这次止损导致了84.2点的回撤(或-3.0025%)。幸运的是,止损位已经向对我们有利的方向移动了不少,使得最终的平仓价位仍在开仓价附近。
再来看另一个例子。下图中,策略使用了2%的移动止损。在一个漂亮的上涨趋势开始前,脚本建立了多头仓位。在趋势消退后,价格出现了显著的回调。幸运的是,移动止损保护了我们的一部分利润。
交易过程中的最高收盘价是2,816.8,而止损最终在2,760.4成交。虽然相对于开仓价来说这笔交易是盈利的,但与最高点相比,利润回撤了56.4点,即2.0022%。




