保持敬畏之心
交易是一场持久战

Pine Script(244):限定交易方向与限制连续亏损天数

#Pine Script入门教学

限定策略只做多或只做空:strategy.risk.allow_entry_in()函数

大多数交易策略并非在所有市场环境下都能表现出色。为了提升策略的整体表现,我们可以通过过滤其交易时机或方式来进行优化。例如,我们可以限定策略只做多或只做空。本文将探讨如何利用TradingView的 strategy.risk.allow_entry_in() 函数来实现这一功能。

通过限定交易方向来控制风险

优秀交易策略的一个标志是在风险能够带来相应回报时才去承担。对于那些无法带来更高收益的交易风险,我们应将其最小化。如果我们的策略在多头或空头某个方向上表现不佳,那么阻止策略在该方向上开仓便是一个明智的选择。

我们可以通过 strategy.risk.allow_entry_in() 函数来阻止策略进行多头或空头交易。这个风险管理函数用法简单,其基本语法格式如下:

strategy.risk.allow_entry_in(value)

这个函数仅有的一个参数 value 是必需参数,用于指定策略允许的交易方向。可选值包括:strategy.direction.long(只允许做多)、strategy.direction.short(只允许做空),或 strategy.direction.all(双向交易,此为默认值)。

这里有个重要提示:strategy.risk.allow_entry_in() 函数会阻止 strategy.entry() 开立新的多头或空头仓位,但它不会影响 strategy.order() 函数。因此,即便你使用了此函数来限制交易方向,strategy.order() 依然能够提交并执行被禁止方向的订单。

让我们来看一个例子。假设我们希望策略只进行多头交易,那么可以在策略代码中加入:

// 只允许提交多头开仓订单
strategy.risk.allow_entry_in(strategy.direction.long)

如果策略只应进行空头交易,则应这样使用:

// 只允许建立空头仓位
strategy.risk.allow_entry_in(strategy.direction.short)

该函数的核心特性

为了更深入地理解这个风险管理函数,让我们来探讨它的具体行为和一些局限性。

第一,它仅影响当前策略。strategy.risk.allow_entry_in() 函数的作用范围仅限于调用了它的那个策略脚本。它对其他图表上的其他TradingView策略一无所知,也无法干预你在经纪商账户中的手动交易或已有持仓。例如,即便我们用此函数设定EUR/USD策略只能做多,我们仍然可以在TradingView界面上手动提交EUR/USD的空头订单。

第二,它全局且持续生效。strategy.risk.allow_entry_in() 会在策略运行的每一刻都对其行为产生影响。换句话说,策略的任何交易行为都必须先通过这道风控规则的审核,从而确保策略不会做出任何违反风险设定的操作。这也意味着,我们无法在策略运行时动态地关闭此规则;它一旦设定,便始终有效。要禁用它,你必须从代码中移除 strategy.risk.allow_entry_in() 这一行,或者使用Ctrl + /快捷键将其注释掉。目前,此风控规则没有任何对应的界面手动开关。因此,启用或禁用它都需要直接修改并保存策略代码。

第三,它无法条件化执行。目前,我们无法根据特定条件来动态地执行 strategy.risk.allow_entry_in()。例如,下面这种在 if 语句中调用的尝试是无效的:

// 错误示范:无法在if语句中调用
if close > ta.ema(close, 10)
    strategy.risk.allow_entry_in(strategy.direction.short)

同样,我们也不能通过条件运算符来动态设定其参数值:

// 错误示范:无法用条件运算符设定参数
strategy.risk.allow_entry_in(close > ta.ema(close, 10) ?
     strategy.direction.short :
     strategy.direction.long)

以上两个例子在保存脚本时都会导致TradingView报错。这一限制带来了一些后果:我们不能在特定时间段内启用或禁用此规则,它将在整个回测期间(以及随后的实盘信号中)持续生效;我们不能根据策略的净利润等动态变化的值来开启或关闭此规则;我们也无法通过输入选项(input)来控制此功能的开关,必须通过修改代码来完成。不过,我们可以通过编写自定义代码的方式,创建一个输入选项来间接实现让策略只做多或只做空的目的。

友情提示:如果你的策略代码很长,务必检查一下是否在某个不起眼的角落意外地留下了一行 strategy.risk.allow_entry_in() 语句。否则,你可能会花费大量时间来排查策略为何顽固地拒绝在某个方向上交易!

第四,这是一个非常有趣且重要的特性:strategy.risk.allow_entry_in() 可以将开仓指令转变为平仓交易。当一个开仓指令的方向被禁止时,这个指令并不会被简单地忽略,而是会转变为一个平掉现有反向仓位的交易。

假设我们设定了策略只能做多。此时,如果脚本中有一个 strategy.entry() 函数试图开立空头仓位,TradingView自然会阻止这个做空行为。但这个做空指令(本质上是一个卖出命令)会被自动转化为一个平掉多头仓位的订单。

换言之,strategy.entry() 函数原本具备的反转仓位(多转空,空转多)的功能,在 strategy.risk.allow_entry_in() 的作用下被抑制了。取而代之的是,strategy.entry() 的行为变成了平仓(从多头到无仓位,或从空头到无仓位)。本文稍后的示例策略将清晰地展示这一行为。

最后,该函数也可以被设置为允许双向交易,但这有些多此一举,因为双向交易本就是默认行为。如果你确实想这么写,可以使用 strategy.direction.all 参数:

// 允许在多头和空头方向上交易
strategy.risk.allow_entry_in(strategy.direction.all)

以这种方式使用时,策略的行为与完全不使用此函数时完全相同。因此,为了代码的简洁性,建议不要使用 strategy.direction.all

只交易多头仓位的策略范例

了解了该函数的特性后,让我们来看一个应用它的具体策略。下面的策略脚本基于20周期和50周期的简单移动平均线(SMA)的金叉死叉进行交易。当快线上穿慢线时做多;当快线下穿慢线时,我们用 strategy.entry() 去做空。然而,由于我们同时使用了 strategy.risk.allow_entry_in() 函数来限定只进行多头交易,那些做空的指令将不会真正地开立空仓。策略代码如下:

//@version=5
strategy(title="允许入场示例", overlay=true)

// 计算移动平均线
fastMA = ta.sma(close, 20)
slowMA = ta.sma(close, 50)

// 在图表上绘制均线
plot(fastMA, color=color.teal)
plot(slowMA, color=color.orange, linewidth=2)

// 设定只允许进行多头交易
strategy.risk.allow_entry_in(strategy.direction.long)

// 提交交易订单
if ta.crossover(fastMA, slowMA)
    strategy.entry("EL", strategy.long) // EL = Enter Long
if ta.crossunder(fastMA, slowMA)
    strategy.entry("ES", strategy.short) // ES = Enter Short

我们首先通过 strategy() 函数配置策略的基本信息。然后用 ta.sma() 计算均线,并用 plot() 将其显示在图表上。接下来是关键的一步,我们设定策略只做多:

// 设定只允许进行多头交易
strategy.risk.allow_entry_in(strategy.direction.long)

我们调用 strategy.risk.allow_entry_in(),并传入 strategy.direction.long 作为参数,以此来施加交易方向的限制。在代码的最后部分,我们根据均线的交叉情况提交订单:

// 提交交易订单
if ta.crossover(fastMA, slowMA)
    strategy.entry("EL", strategy.long)
if ta.crossunder(fastMA, slowMA)
    strategy.entry("ES", strategy.short)

第一个 if 语句使用 ta.crossover() 判断金叉。一旦发生金叉,就调用 strategy.entry() 开立多头仓位(strategy.long)。第二个 if 语句使用 ta.crossunder() 判断死叉。当死叉发生时,我们调用 strategy.entry() 意图开立空头仓位(strategy.short)。

通常情况下,如果策略已持有多仓,一个做空指令会使仓位反转。但由于我们已经禁止了做空交易,这里发生的情况将是:我们的”ES”(Enter Short)订单,实际上会平掉已有的多头仓位。当我们把这个策略应用到图表上时,就可以清晰地观察到这一行为:

图表上有两个名为”EL”的多头开仓订单,每一个之后都紧随着一个”ES”空头开仓订单。在正常情况下,后一个”ES”订单会直接将已有的多头仓位反转为空头仓位。但是,由于我们使用了 strategy.risk.allow_entry_in() 来禁止做空,情况就有所不同了。

实际发生的是,每一个”ES”订单的规模都恰好与前一个”EL”订单的规模相同。这使得这个做空指令的实际效果变成了平仓(即卖出)已有的多头头寸,而不会在此之后开立新的空头仓位。(要让一个卖出指令在平掉多仓后还能开立新的空仓,它的订单规模必须大于当前持有的多头仓位。订单中的第一部分合约会用于平掉多仓,而剩余的合约才会用于建立新的空仓。)

核心要点:当你的策略使用 strategy.entry() 函数来进行仓位反转时,一旦配合 strategy.risk.allow_entry_in() 使用,反转行为将不会发生。取而代之的是,策略将只会平掉当前的仓位。换言之,strategy.risk.allow_entry_in() 改变了 strategy.entry() 的核心行为,使其从一个可以反转仓位的开仓函数,变成了一个只能平仓的平仓函数。

只执行空头交易的策略范例

下面的示例策略交易的是20周期高点和低点的突破。具体来说,当价格收盘突破20周期最高高点时,产生一个多头信号;当价格收盘跌破20周期最低低点时,则产生一个空头信号。但是,我们并不会去实际执行那些向上突破的多头交易。因为我们通过 strategy.risk.allow_entry_in() 函数限定了策略只能进行空头交易。策略的完整代码如下:

//@version=5
strategy(title="允许入场 - 只做空", overlay=true)

// 计算前20周期的最高高点和最低低点
hiHighs = ta.highest(high, 20)[1]
loLows  = ta.lowest(low, 20)[1]

// 在图表上绘制高低点通道
plot(hiHighs, style=plot.style_circles, color=color.green, linewidth=2)
plot(loLows, style=plot.style_circles, color=color.red, linewidth=2)

// 设定只允许进行空头交易
strategy.risk.allow_entry_in(strategy.direction.short)

// 提交交易订单
if ta.crossover(close, hiHighs)
    strategy.entry("EL", strategy.long) // EL = Enter Long
if ta.crossunder(close, loLows)
    strategy.entry("ES", strategy.short) // ES = Enter Short

我们首先通过 strategy() 函数配置策略信息。然后用 ta.highest()ta.lowest() 函数计算20周期的高低点。接着用 plot() 函数将这些极值点以实心圆圈的形式绘制在图表上。然后,我们通过 strategy.risk.allow_entry_in() 来创建风控规则:

// 设定只允许进行空头交易
strategy.risk.allow_entry_in(strategy.direction.short)

由于我们传入了 strategy.direction.short 参数,策略现在只能进行空头交易。这也意味着,由 strategy.entry() 函数生成的多头买入指令,实际上会转变为一个平掉空头仓位(即买入回补)的订单。接下来,我们提交交易订单:

// 提交交易订单
if ta.crossover(close, hiHighs)
    strategy.entry("EL", strategy.long)
if ta.crossunder(close, loLows)
    strategy.entry("ES", strategy.short)

第一个 if 语句使用 ta.crossover() 判断价格是否向上突破了20周期最高点(hiHighs)。如果突破,我们便调用 strategy.entry() 意图开立多头仓位。另一个 if 语句则用 ta.crossunder() 判断价格是否向下跌破了20周期最低点(loLows)。在这种情况下,我们调用 strategy.entry() 来发起空头交易。

但是,别忘了我们已经通过 strategy.risk.allow_entry_in() 禁止了多头交易。所以,即便价格突破了最高点,strategy.entry() 也不会去开立多仓。相反,这个函数调用所做的,是发送一个买入指令来平掉任何当前持有的空头仓位。我们可以在下方的图表中观察到这一行为的实际效果:

图表中的第一个”ES”(做空)订单被随后的”EL”(做多)订单所平仓。但由于这个”EL”订单的规模并不比已有的空头仓位大,所以它所做的仅仅是平掉了空仓,使策略回归到无仓位状态。

优点与局限

在本文结束前,让我们总结一下 strategy.risk.allow_entry_in() 函数的优缺点。

优点方面:一是便捷性,此函数可以让你快速地禁用某一交易方向,而无需对策略的核心逻辑代码进行大的改动;二是功能转换,它能巧妙地将 strategy.entry() 的功能从开仓/反转转变为平仓,这样我们就不必为了单纯的平仓而去使用 strategy.exit() 函数重写逻辑。

局限方面:一是缺乏灵活性,此函数无法被条件化地设置或在回测期间动态更改,一旦设定,它将影响整个回测周期和所有实时信号,若想实现根据市场状况动态开关某一交易方向,我们需要编写自定义的逻辑代码;二是作用范围有限,此风控函数对 strategy.order() 函数发送的订单无效,因此即便我们禁止了空头入场,策略仍有可能通过 strategy.order() 建立空头仓位;三是无法手动配置,我们无法在策略设置界面中手动开关或配置此功能,要更改规则必须编辑代码,而每次保存修改后的代码,TradingView都会重置所有输入参数为默认值,这意味着每次调整此风控规则后,我们可能都需要重新配置策略的其他参数。

除了本文讨论的限定交易方向的函数,TradingView还提供了其他多种风险管理函数来帮助我们控制风险:strategy.risk.max_cons_loss_days() 限制最大连续亏损天数;strategy.risk.max_drawdown() 基于最大资金回撤来停止策略;strategy.risk.max_position_size() 限制策略的最大持仓规模;strategy.risk.max_intraday_filled_orders() 限制日内成交订单的数量,以控制交易频率;strategy.risk.max_intraday_loss() 限制策略的单日最大亏损额。

简单总结一下:strategy.risk.allow_entry_in() 函数可以使我们的TradingView策略只进行多头或空头交易。要只做多,我们使用 strategy.direction.long 参数;要只做空,则使用 strategy.direction.short 参数。这个风险管理函数会影响策略的每一个交易行为,从回测的第一个K线到最后一个实时K线,禁用它的唯一方法就是从代码中移除该函数。

一个非常有趣的特性是,它会改变 strategy.entry() 函数的行为模式。当只允许做多时,一个原本用于做空的 strategy.entry() 指令会自动转变为一个平掉多仓的订单,从而使 strategy.entry() 从一个开仓函数转变为一个平仓函数。同理,当只允许做空时,一个 strategy.entry() 的买入指令将只会用于回补空头仓位,而不会建立新的净多头头寸。当我们将它与其他风险管理函数结合使用时,TradingView会对每一条规则进行独立评估,最先被触发的那条规则将决定策略接下来的行为。

限制策略连续亏损天数:strategy.risk.max_cons_loss_days()函数

一个优秀的交易策略不仅能发出盈利信号,还应在市场状况不利时有效控制亏损。策略限制风险的一种有效方法,就是设定一个可容忍的最大连续亏损天数。本文将详细介绍如何在TradingView中,利用 strategy.risk.max_cons_loss_days() 函数来实现这一风控规则。

在经历一轮连败后停止交易

长期的连续亏损对交易而言是重大的风险。在此期间,一个交易策略可能看起来无论发出什么信号,结果都是错的。当这种情况发生时,停止交易、暂时离场,通常是比坚持交易更明智的选择。

为了给我们的TradingView策略脚本增加这一熔断功能,我们可以使用 strategy.risk.max_cons_loss_days()。这是一个内置的风险管理函数,它可以在策略经历了一定天数的连续亏损后,自动停止所有交易活动。该函数的基础语法格式如下:

strategy.risk.max_cons_loss_days(count)

其中,count 参数是一个必需的数值,用以指定策略所能承受的最大连续亏损天数。

请注意,strategy.risk.max_cons_loss_days() 会影响所有TradingView的订单函数。因此,一旦这个风险管理函数被触发,我们就无法再通过 strategy.entry()strategy.order() 函数来建立新的头寸。

举个例子,要让我们的策略在连续亏损5天后停止,代码可以这样写:

// 在策略权益连续5日下降后,停止交易
strategy.risk.max_cons_loss_days(count=5)

该函数的核心特性

让我们更深入地了解 strategy.risk.max_cons_loss_days() 的工作机制及其核心特性。

首先是亏损日的定义:基于策略权益的下降。要判断某一天是否为亏损日,该函数并不关心当天是否有平仓的亏损交易。它衡量的标准是策略的权益(Equity)。策略权益是初始资金、已平仓交易的累计盈亏以及当前持仓的浮动盈亏三者的总和。由于权益会随着持仓的市价而实时变动,因此,即使当天没有任何平仓操作,也可能构成一个亏损日。例如,如果我们持有的多头头寸在3天内价值缓慢下跌,那么即使仓位远未触及止损,TradingView依然会将其计为3个连续的亏损日。

其次是天的定义:基于日历日。对于这个函数而言,一天是指从午夜(00:00)开始的24小时周期。这意味着该函数是基于日历日而非交易时段来工作的。这个细微的差别,对于那些交易时段在同一日历日内开始和结束的品种(如多数股票)来说无关紧要。但对于那些交易时段跨越两个日历日的品种(如外汇),则至关重要。

假设我们交易EUR/USD,其交易时段为纽约时间17:05至次日17:00。该函数并不会在17:05那一刻去检查权益,以判断上一个交易日的盈亏。相反,它会在日历日结束的23:59那一刻来衡量权益的变化。这就解释了为何有时我们的策略在某个交易日内是盈利的,但从日历日的角度看却可能是亏损的。

再来看函数的比较机制:逐日对比。在每个日历日结束时,TradingView会将策略当前的权益与前一天结束时的权益进行比较。如果策略权益增加,连续亏损日的计数器将重置为0;如果策略权益减少,计数器将加1。一旦这个计数器的值等于函数所设定的天数,TradingView便会停止策略。

触发后的行为是取消订单并停止所有交易。当策略达到最大连续亏损天数时,所有待处理的挂单都将被取消;如果策略当前有持仓,该持仓将以市价单立即平仓;策略将不再提交任何新的订单。一旦被触发,策略在本次回测的剩余时间内将完全停止。在实盘中,它也将不再产生任何新的交易信号。

函数的作用范围是全局且持续有效的。只要我们将 strategy.risk.max_cons_loss_days() 函数加入到脚本中,它就会在每次脚本计算时影响策略的行为。换言之,它是始终有效的。这意味着,只要它出现在脚本的任何位置,就会作用于整个回测和所有实时信号。禁用此规则的唯一方法,就是从代码中移除该函数,或者使用快捷键Ctrl + /将其注释掉。

另外,函数的执行方式是不可条件化的。它的效果是无条件的,这意味着我们不能根据某些逻辑(例如,使用 if 语句)来选择性地执行它。以下尝试是错误的:

if close > ta.ema(close, 20)
    strategy.risk.max_cons_loss_days(count=3) // 错误!不能在if语句中调用

这种尝试也是不可能的:

// 错误!参数不能是动态的
strategy.risk.max_cons_loss_days(count= close > ta.ema(close, 20) ? 5 : 12)

这两个代码示例都会引发TradingView的编译错误。因此,我们的策略无法在回测过程中动态改变触发此规则所需的天数,也无法在特定时间段内启用或禁用这条规则。

不过,尽管该函数不能被动态调用,但我们可以使用自定义的输入选项来设置其参数。这使得调整风控规则变得更为便捷。示例如下:

// 创建一个整数输入选项
maxLossDays = input.int(10, title="最大连续亏损天数")

// 使用输入变量来设置最大亏损天数
strategy.risk.max_cons_loss_days(maxLossDays)

这段代码通过 input.int() 函数创建了一个整数输入框。我们将输入框的当前值存入 maxLossDays 变量,然后将此变量传递给 strategy.risk.max_cons_loss_days() 函数。这样,当我们需要调整最大连续亏损天数时,就无需直接修改代码,只需在策略设置界面中更改数值即可。

最后,该函数仅影响调用了它的那个策略。图表上的其他策略、任何手动交易或在经纪商处的持仓均不受其影响。因此,即使我们交易BTC/USD的策略因为此规则而停止,我们依然可以在TradingView中通过手动下单来交易比特币。

示例:在连续亏损5天后停止交易

了解了该函数的特性和行为后,让我们通过一个完整的策略脚本来看看如何应用它。以下策略基于两条移动平均线的交叉进行交易。一条是7周期的EMA快线,另一条是24周期的EMA慢线。当快线上穿慢线时,我们建立多头头寸;反之则建立空头头寸。

然而,我们不会无限期地运行此策略。通过 strategy.risk.max_cons_loss_days() 函数,我们设定了最多容忍连续5天的亏损。一旦触及此限制,策略就应停止交易。策略的完整代码如下:

//@version=5
strategy(title="最大连续亏损日 - 范例", overlay=true)

// 计算移动平均线
quickMA = ta.ema(close, 7)
slowMA  = ta.ema(close, 24)

// 在图表上绘制指标线
plot(quickMA, color=color.orange)
plot(slowMA, color=color.teal, linewidth=2)

// 在策略权益连续5日
// 下降后停止交易
strategy.risk.max_cons_loss_days(count=5)

// 提交订单
if ta.crossover(quickMA, slowMA)
    strategy.entry("EL", strategy.long, qty=1)

if ta.crossunder(quickMA, slowMA)
    strategy.entry("ES", strategy.short, qty=1)

我们首先通过 strategy() 函数配置策略的基本信息。然后使用 ta.ema() 计算移动平均线,并用 plot() 将它们绘制在图表上。接着,我们加入了核心的风控规则:

// 在策略权益连续5日
// 下降后停止交易
strategy.risk.max_cons_loss_days(count=5)

在这里,我们调用 strategy.risk.max_cons_loss_days() 并将 count 参数设为5。这使得策略在遭遇连续5个权益下降的日历日后便停止交易。代码的最后一部分负责提交多头和空头订单:

// 提交订单
if ta.crossover(quickMA, slowMA)
    strategy.entry("EL", strategy.long, qty=1)

if ta.crossunder(quickMA, slowMA)
    strategy.entry("ES", strategy.short, qty=1)

第一个 if 语句利用 ta.crossover() 函数判断7周期EMA是否上穿24周期EMA。若条件成立,则调用 strategy.entry() 建立一个多头仓位。第二个 if 语句则利用 ta.crossunder() 判断是否发生死叉,并建立空头仓位。

尽管在 if 语句中没有直接体现,但这两个订单的执行都隐性地受制于我们设置的风控规则。也就是说,一旦 strategy.risk.max_cons_loss_days() 触发并停止了策略,那么无论是否出现均线交叉信号,我们的 strategy.entry() 函数都将不会被执行。以下是该策略在图表上的行为方式:

如果一个策略没有真正的优势(edge),那么用不了多久它就会触及最大回撤的限制。在这张图表上,这种情况发生在本月的11号。一旦触及回撤限制,TradingView便会发出一个名为”Max Consecutive Loss days”(最大连续亏损日)的市价单来平掉仓位。从那一刻起,策略便停止了所有的多头和空头交易。

示例:设置策略权益最多连续下降3天

在下面的例子中,我们将在图表上绘制策略的权益曲线,并观察其相较于前一天的变化。这使得我们可以直观地跟踪最大回撤是如何形成的。策略本身基于20周期和50周期的简单移动平均线(SMA)交叉进行交易。我们将使用 strategy.risk.max_cons_loss_days() 函数来确保策略的连续亏损不会超过3天。策略的完整代码如下:

//@version=5
strategy(title="限制连续亏损日", overlay=false)

// 计算移动平均线
fastMA = ta.sma(close, 20)
slowMA = ta.sma(close, 50)

// 设定最多连续3个亏损日
strategy.risk.max_cons_loss_days(3)

// 提交交易订单
if ta.crossover(fastMA, slowMA)
    strategy.entry("EL", strategy.long)
if ta.crossunder(fastMA, slowMA)
    strategy.entry("ES", strategy.short)

// 计算策略权益的每日变化
newDay = dayofmonth != dayofmonth[1]

closeEquity = 0.0, prevEquity = 0.0

closeEquity := newDay ? strategy.equity[1] : closeEquity[1]
prevEquity  := newDay ? closeEquity[1] : prevEquity[1]

// 绘制权益的每日变化
plot(strategy.equity, color=color.blue, linewidth=2)

plotColour = closeEquity > prevEquity ? color.green :
     closeEquity < prevEquity ? color.red :
     color.orange
plot(newDay ? strategy.equity[1] : na,
     style=plot.style_line, color=plotColour, linewidth=3, offset=-1)

// 用背景色高亮每日的开始
bgcolor(newDay ? color.new(color.teal, 75) : na)

我们首先用 strategy() 函数配置策略的基本信息。然后用 ta.sma() 计算两条均线。接着,我们设定最多允许3个连续亏损日:

// 设定最多连续3个亏损日
strategy.risk.max_cons_loss_days(3)

为了限制策略的连续亏损,我们调用 strategy.risk.max_cons_loss_days() 函数,并将参数设为3。这将使得策略在出现连续亏损的第3天收盘后自动停止。然后,我们提交交易订单:

// 提交交易订单
if ta.crossover(fastMA, slowMA)
    strategy.entry("EL", strategy.long)
if ta.crossunder(fastMA, slowMA)
    strategy.entry("ES", strategy.short)

这部分是基于均线金叉死叉的常规交易逻辑。当20周期SMA上穿50周期SMA时做多,反之则做空。现在,我们进入代码中相对复杂的部分。为了追踪并可视化策略权益的每日变化,我们首先需要进行一些计算:

// 计算策略权益的每日变化
newDay = dayofmonth != dayofmonth[1]

closeEquity = 0.0, prevEquity = 0.0

closeEquity := newDay ? strategy.equity[1] : closeEquity[1]
prevEquity  := newDay ? closeEquity[1] : prevEquity[1]

我们创建的 newDay 变量通过比较当前K线的月份日期(dayofmonth)与上一根K线是否相同,来判断是否进入了新的一天。这个条件只在每个交易日的第一根K线上成立。

然后,我们定义了两个浮点变量 closeEquityprevEquity,分别用于记录昨日收盘权益和前日收盘权益。

这个逻辑的核心是,我们只在每天的第一根K线出现时,去回望并记录下历史的每日收盘权益状况。当 newDay 为真时,strategy.equity[1] 获取的就是昨天最后一根K线收盘时的权益,我们用它来更新 closeEquity。同时,我们将 closeEquity 的旧值(即前日的收盘权益)赋给 prevEquity。在当天剩下的时间里,这两个变量的值保持不变。

简单来说,closeEquity 始终保存着上一个交易日的收盘权益,而 prevEquity 则保存着再往前一个交易日的收盘权益。有了这两个每日的权益快照,我们就可以将它们的变化绘制出来了:

// 绘制权益的每日变化
plot(strategy.equity, color=color.blue, linewidth=2)

plotColour = closeEquity > prevEquity ? color.green :
     closeEquity < prevEquity ? color.red :
     color.orange
plot(newDay ? strategy.equity[1] : na,
     style=plot.style_line, color=plotColour, linewidth=3, offset=-1)

我们首先用 plot() 绘制了实时变化的权益曲线(蓝色)。接着,为了可视化每日的盈亏,我们定义了一个 plotColour 变量。通过条件运算符(?:),我们比较 closeEquityprevEquity:如果权益增加,颜色为绿色;如果减少,则为红色;不变则为橙色。然后,我们再次调用 plot(),但这次只在 newDay 为真时绘制昨天的收盘权益(strategy.equity[1]),并应用我们刚确定的颜色。这样,图上就会出现一条每日更新的、代表每日盈亏的彩色线段。

下面是这个策略在图表上的实际表现:

该脚本通过 strategy.risk.max_cons_loss_days() 函数设定了最多3天的连续亏损。在图表中,这三天发生在本月的18、19和20号。我们可以清晰地看到这三天的日终权益线段都是红色的,表示权益在持续下降。

在连续亏损的第三天(20号)收盘后,TradingView自动提交了一个”Close Position”(平仓)订单,将策略的仓位清空。从那一刻起,该策略便停止了所有后续的交易活动。

优点与局限

优点方面:一是简单有效,此函数提供了一种在策略陷入长期亏损时自动熔断的简单方法;二是易于配置,我们可以将连续亏损天数设置为一个输入选项,从而无需修改代码就能方便地调整风控规则。

局限方面则有几点值得注意。一是缺乏灵活性,此风控规则无法被条件化地设置,一旦启用,它将在整个回测期间持续有效,我们无法实现诸如”仅在高波动时期启用此规则”这样的动态风控。二是参数固定,在整个回测中只能使用一个固定的连续亏损天数阈值,这对于长期变化的市场环境来说并不理想。

三是它基于权益而非盈亏。该函数监控的是策略的总权益,对于长线持仓的策略来说,这可能产生误判。例如,一个盈利的持仓其浮动盈利连续几天回撤,也会导致权益下降,从而被计为亏损日,即便仓位本身仍是盈利的。

四是以天而非交易为单位。以天为单位衡量亏损可能具有误导性:在剧烈波动的市场中,策略可能在3天内产生50笔亏损交易;而在平静的市场中,同样时间内可能只有不到10笔。显然,前一种情况更有理由让我们停止交易。

五是它属于一刀切式止损。此规则是一种全有或全无的设置,策略要么全仓运行,要么在触及限制后完全停止。它不允许策略在出现亏损时通过减小仓位来应对,也完全剥夺了其从亏损中恢复的可能性。在某种意义上,这反而增加了风险,因为它将浮亏强制变成了实亏。

除了本文讨论的函数,TradingView Pine还提供了其他多种风险管理函数:strategy.risk.allow_entry_in() 限制策略的交易方向(只做多或只做空);strategy.risk.max_drawdown() 基于最大资金回撤来停止策略;strategy.risk.max_position_size() 限制策略的最大持仓规模;strategy.risk.max_intraday_filled_orders() 限制日内成交订单的数量;strategy.risk.max_intraday_loss() 限制策略的单日最大亏损额。

简单总结一下:strategy.risk.max_cons_loss_days() 函数用于限制策略的最大连续亏损天数。它只有一个参数,即我们愿意承受的最大连续亏损天数值。所谓的亏损日,指的是在某个日历日收盘时,策略的总权益低于前一日收盘时的权益。这个总权益包括了初始资本、已平仓交易的盈亏以及未平仓头寸的浮动盈亏。因此,即使没有平仓交易,一个盈利持仓的利润回撤也可能构成一个亏损日。

一旦该函数被触发,它会取消所有待处理的策略订单,然后发送一个市价单来平掉当前持有的仓位。此后,策略将不再提交任何新的订单。这个状态会贯穿整个回测期,并延续到随后的实时信号中。此函数在策略运行的每一刻都保持激活状态,我们无法通过代码来动态地启用或禁用它,要关闭此规则,只能从代码中移除或注释掉该函数。当我们在同一脚本中设置了多个风险管理规则时,TradingView会对每一条规则进行独立评估,最先被触发的那条规则将决定策略接下来的行为。

赞(0)
未经允许不得转载:图道交易 » Pine Script(244):限定交易方向与限制连续亏损天数
分享到

评论 抢沙发

登录

找回密码

注册