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

Pine Script(245):限制策略最大回撤与最大仓位规模

#Pine Script入门教学

根据最大亏损停止交易:strategy.risk.max_drawdown()函数

无论策略多么优秀,资金回撤(Drawdown)都是交易中不可避免的一部分。限制这种风险的有效方法之一,便是在回撤达到某个预设的上限时,果断停止交易。本文将详细介绍如何在TradingView中,利用 strategy.risk.max_drawdown() 函数来实现这一关键的风控逻辑。

基于最大回撤来停止一个TradingView策略

一个造成巨额亏损的策略是极其危险的。即便我们的策略仅仅亏损了40%,我们也需要在此后实现超过66%的惊人回报,才能勉强回本。毫无疑问,防范于未然,远比从重大亏损中恢复要容易得多。

通过 strategy.risk.max_drawdown() 函数,我们的TradingView策略可以在达到预设的最大回撤值时,自动停止所有交易活动。该函数的基础语法格式如下:

strategy.risk.max_drawdown(value, type)

其中,value 是一个必需的浮点数,用以设定最大回撤的阈值。这个值可以是基于图表交易品种的固定货币金额,也可以是基于账户权益的百分比(允许范围为0到100)。type 也是一个必需的参数,用以指定回撤的衡量方式,可选值为 strategy.cash(按亏损的货币金额计算)或 strategy.percent_of_equity(按亏损的权益百分比计算)。

请注意,一旦达到最大回撤,strategy.risk.max_drawdown() 将会停止所有交易。这意味着我们不仅无法通过 strategy.entry() 函数建立新仓位,也无法使用 strategy.order() 函数来提交任何交易。(这一点值得特别留意,因为并非所有风险管理函数都会阻止 strategy.order() 的执行。)

如果我们选择按权益百分比来衡量回撤,那么当策略的权益变为负数时,该函数同样会停止交易。因此,使用 strategy.percent_of_equity 设置时,允许的最大亏损就是-100%(即全部初始资金)。而使用 strategy.cash 选项时,理论上允许策略权益变为负值。

举个例子,假设我们希望将亏损上限设为5,000个货币单位,可以这样使用该风控函数:

strategy.risk.max_drawdown(value=5000, type=strategy.cash)

如果我们不希望亏损超过账户权益的10%,则可以这样使用:

strategy.risk.max_drawdown(value=10, type=strategy.percent_of_equity)

该函数的核心特性

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

首先,回撤的计算基础是策略权益(Equity)。TradingView是基于策略权益来衡量最大回撤的。策略权益是初始资金、已平仓交易的累计盈亏(已包含佣金和滑点)以及当前持仓的浮动盈亏三者的总和。这意味着,回撤的计算部分是基于我们尚未兑现的账面盈亏。因此,一个策略完全有可能在没有任何一笔平仓亏损的情况下,就陷入了严重的回撤。

其次,实际亏损可能比设定的最大回撤更严重,这是一个至关重要的风险提示。一旦策略达到最大回撤阈值,该函数会通过一个市价单来强制平掉当前持仓。在平仓的瞬间,这个市价单可能会遭遇滑点,从而导致我们最终的实际亏损比设定的最大回撤值更为严重。此外,如果市场发生对我们持仓不利的跳空,我们同样会遭受超出预期的亏损。因此,一个明智的做法是,将函数中设置的最大回撤值,定得比你心理上或实际上能承受的最大亏损更小一些,以为这些不可控因素预留缓冲空间。

触发后的行为是停止一切交易。一旦策略的回撤达到了我们定义的上限,所有待处理的挂单都将被取消;如果策略当前有持仓,TradingView会提交一个市价单来平掉全部仓位;此后,该策略将不再提交任何新的交易。

函数的作用范围是全局且持续有效的。只要我们将该函数加入到脚本中,它就会在每次脚本计算时影响策略的行为。因此,策略将始终受到该函数所定义的最大回撤的约束。这也意味着,整个回测过程和所有实时信号都会受到影响。没有捷径可以临时禁用此风控规则,要关闭它,你必须从代码中移除这一行,或者使用快捷键Ctrl + /将其注释掉。

此外,函数的执行方式不可条件化。目前,我们无法有条件地执行 strategy.risk.max_drawdown()。这意味着我们不能基于某个条件来动态启用此函数:

// 错误!不能在if语句中调用
if close > ta.ema(close, 20)
    strategy.risk.max_drawdown(value=2000, type=strategy.cash)

同样,我们也无法根据条件来动态设置函数的参数:

// 错误!参数不能是动态的
strategy.risk.max_drawdown(value=close > ta.ema(close, 20) ? 2000 : 5000, type=strategy.cash)

这两个代码示例都会引发TradingView的编译错误。因此,一旦使用了该函数,它在整个回测期间都会采用相同的最大回撤设置,无法根据策略状况或市场波动性进行自适应调整。

尽管TradingView目前没有提供手动的界面设置来调整策略的最大回撤,但我们可以创建自己的输入选项。这样,我们就可以在不修改代码的情况下,方便地配置该函数所使用的最大回撤值。

要基于固定货币金额来设置最大回撤,我们可以创建一个整数或浮点数输入选项,然后将其值传递给函数。示例如下:

// 创建一个输入项,用于设置策略的最大回撤(以货币金额计)
maxDd = input.int(2500, title="最大回撤(金额)", minval=1)

// 使用该输入项的值来调用最大回撤函数
strategy.risk.max_drawdown(maxDd, type=strategy.cash)

这里,我们用 input.int() 函数创建了一个整数输入框,默认值为2,500,并将其值存入 maxDd 变量。然后,我们将 maxDd 变量作为最大回撤值,并指定类型为 strategy.cash

要将最大回撤配置为权益的百分比,我们同样可以创建一个输入选项。例如:

// 创建一个输入项,用于设置策略的最大回撤(以权益百分比计)
maxPercDd = input.int(10, title="最大回撤 (%)", minval=1, maxval=100)

// 使用该输入项的值来调用最大回澈函数
strategy.risk.max_drawdown(maxPercDd, type=strategy.percent_of_equity)

这里,我们通过 input.int()minvalmaxval 参数,将输入值的范围限制在1到100之间。然后,我们将输入变量 maxPercDdtype=strategy.percent_of_equity 一起使用,以实现基于权益百分比的回撤控制。

最后,该函数的作用对象仅限当前策略脚本。虽然它对整个策略脚本都有效,但其影响力也仅限于此。该风控函数不会感知其他图表上运行的策略,也不会影响你在TradingView内进行的手动交易,更不会干预你在经纪商处的实际持仓。因此,即使它触发并停止了你的策略,你仍然可以通过手动下单的方式交易该品种。其他图表上运行的策略,即使交易的是同一品种,也将保持活跃。

示例:在回撤达到$2,500后停止交易

了解了该函数的特性后,让我们通过一个完整的TradingView策略脚本来看看如何应用它。下方案例脚本交易的是20周期的最高高点和最低低点的突破。我们同时也会设置一个基于价格通道中线的止损。我们使用 strategy.risk.max_drawdown() 来确保策略在亏损达到2,500个货币单位后便停止运行。策略的完整代码如下:

//@version=5
strategy(title="最大回撤示例", overlay=true)

// 计算最高高点和最低低点
hiHighs  = ta.highest(high, 20)[1]
loLows   = ta.lowest(low, 20)[1]
midPoint = (hiHighs + loLows) / 2

// 在图表上显示20周期的高点、低点和中线
plot(hiHighs, style=plot.style_circles, color=color.green, linewidth=2)
plot(loLows, style=plot.style_circles, color=color.red, linewidth=2)
plot(midPoint, color=color.orange)

// 在亏损超过2,500个货币单位后停止交易
strategy.risk.max_drawdown(value=2500, type=strategy.cash)

// 提交入场订单
if ta.crossover(close, hiHighs)
    strategy.entry("EL", strategy.long, qty=50000)

if ta.crossunder(close, loLows)
    strategy.entry("ES", strategy.short, qty=50000)

// 提交止损订单
if strategy.position_size > 0
    strategy.exit("XL", stop=midPoint)

if strategy.position_size < 0
    strategy.exit("XS", stop=midPoint)

首先,我们定义策略设置,然后计算并绘制出价格通道的高点、低点和中线。接下来,我们加入核心的风控规则:

// 在亏损超过2,500个货币单位后停止交易
strategy.risk.max_drawdown(value=2500, type=strategy.cash)

为了实现这一资金管理规则,我们执行 strategy.risk.max_drawdown() 函数,将 value 设为2500,type 设为 strategy.cash。这会使策略在亏损达到或超过2,500个货币单位时便停止运行。然后我们提交入场订单:

// 提交入场订单
if ta.crossover(close, hiHighs)
    strategy.entry("EL", strategy.long, qty=50000)

if ta.crossunder(close, loLows)
    strategy.entry("ES", strategy.short, qty=50000)

第一个 if 语句判断价格是否上穿通道上轨,若是则做多。第二个则判断是否下穿下轨,若是则做空。

这里需要注意的是,虽然这两个 if 语句的条件只涉及价格和通道线的比较,但它们的执行隐性地依赖于策略的回撤状态。这是因为,一旦 strategy.risk.max_drawdown() 函数被触发,这些 strategy.entry() 订单将不再被提交。因此,如果你发现策略的 if 语句没有按预期工作,除了检查条件本身,也需要考虑它们执行的代码是否可能已被某个风险管理函数禁用了。

以上示例策略在图表上的表现如下:

在本例中,策略在本月的22日达到了其设定的最大回撤。从那一刻起,便再也没有产生任何EUR/USD的交易。这一风控措施成功阻止了策略的进一步亏损,但同时也意味着那2,500美元的亏损被锁定,成为了最终结果。

示例:在图表上直观地观察最大回撤的形成过程

下方的示例策略基于20周期和50周期的简单移动平均线(SMA)进行交易。当快线上穿慢线时做多,反之则做空。同时,我们利用 strategy.risk.max_drawdown() 函数,将策略的最大回撤限制在10,000个货币单位以内。我们还将在图表上绘制策略的实时回撤曲线,以便可以直观地观察回撤随时间的变化情况。策略的完整代码如下:

//@version=5
strategy(title="限制策略回撤", overlay=false)

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

// 将最大回撤限制在10,000货币单位
strategy.risk.max_drawdown(value=10000, type=strategy.cash)

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

// 计算策略的实时回撤
maxEquity = 0.0
maxEquity := math.max(strategy.equity, nz(maxEquity[1]))

drawdown = strategy.equity - maxEquity

// 绘制策略的回撤曲线
plot(drawdown, linewidth=2,
     color=drawdown > drawdown[1] ? color.green : color.red)

我们首先通过 strategy() 函数配置策略信息,然后用 ta.sma() 计算两条均线。接着,我们实施核心的风控规则:

// 将最大回撤限制在10,000货币单位
strategy.risk.max_drawdown(value=10000, type=strategy.cash)

我们调用 strategy.risk.max_drawdown() 函数,将其 value 参数设为10000,并通过 type=strategy.cash 指定以绝对的货币金额来计算回撤。这样,一旦累计亏损达到10,000,策略便会自动停止。然后是基于均线交叉的交易逻辑:

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

这部分是常规的均线交叉系统,当金叉发生时做多,死叉时做空。现在,我们来编写用于追踪和可视化策略回撤的代码。首先,我们需要计算一些关键值:

// 计算策略的实时回撤
maxEquity = 0.0
maxEquity := math.max(strategy.equity, nz(maxEquity[1]))

drawdown = strategy.equity - maxEquity

这里,我们定义了一个 maxEquity 变量,用于实现一个高水位线(high-water mark)机制。在每一根K线上,我们都将其值更新为当前的策略权益和它自身在上一根K线的值之间的较大者。通过这种方式,maxEquity 能够持续追踪并记录历史最高权益点。

然后,用当前的策略权益(strategy.equity)减去这个历史最高权益(maxEquity),得到的结果就是当前的回撤值。我们将其存入 drawdown 变量。接下来,我们将回撤值绘制在图表上:

// 绘制策略的回撤曲线
plot(drawdown, linewidth=2,
     color=drawdown > drawdown[1] ? color.green : color.red)

我们用 plot() 函数将 drawdown 变量的值以折线图的形式展示出来,并适当加粗了线条。为了便于解读,我们还对线条的颜色做了条件化处理:如果当前的回撤值大于上一根K线的值(意味着回撤在减小,资金在恢复),线条显示为绿色;反之,如果回撤不变或加深,则显示为红色。

下面是这个策略及其回撤曲线在图表上的实际表现:

由于我们的策略缺乏真正的优势(edge),回撤曲线毫不意外地缓慢下行。当回撤值最终触及$10,000的阈值时(本例中的EuroStoxx CFD以美元计价),TradingView便自动发送了一个平仓订单,关闭了当时持有的多头仓位。

但值得注意的是,这个市价平仓单的成交价格可能并不理想,从而导致策略的最终亏损略微超过我们设定的$10,000上限。一旦仓位被清空,策略便停止了所有后续的交易活动。

优点与局限

优点方面:一是简单可靠,它提供了一种简单明了的方式来根据资金或权益百分比限制策略的总亏损;二是执行保障,该函数几乎可以保证在达到最大回撤时交易活动会确实停止(如果我们自己编写逻辑,总有可能因代码错误而失效);三是易于配置,我们可以将最大回撤值设置为一个输入选项,从而无需修改代码就能方便地进行调整。

局限方面:一是缺乏灵活性,此风控规则无法被条件化地设置,一旦设定,它将在整个回测期间以固定的阈值持续有效,无法根据市场波动性或策略状态的变化进行动态调整。二是一刀切式止损,该函数会永久性地停止策略在当前回测(及后续实盘)中的所有交易,不给策略任何恢复的机会,直接将最大回撤变成了最终亏损,当策略只是因为暂时的意外市场状况而受挫时,这种处理方式可能过于严苛。三是不考虑交易背景,该函数只看亏损金额,不看亏损的质量:亏损$2,500发生在3笔交易中,可能只是由闪崩或央行干预等异常事件导致;但如果这$2,500是分散在75笔交易中累积起来的,那几乎可以肯定策略的优势已经消失了。

除了本文讨论的函数,TradingView还提供了其他多种风险管理函数:strategy.risk.allow_entry_in() 限制策略的交易方向(只做多或只做空);strategy.risk.max_cons_loss_days() 限制最大连续亏损天数;strategy.risk.max_position_size() 限制策略的最大持仓规模;strategy.risk.max_intraday_filled_orders() 限制日内成交订单的数量;strategy.risk.max_intraday_loss() 限制策略的单日最大亏损额。我们可以单独使用这些规则,也可以将它们组合起来。

简单总结一下:strategy.risk.max_drawdown() 函数会在策略达到预设的最大回撤时自动停止策略。具体来说,当回撤限制被触发时,TradingView会首先取消所有待处理的订单,然后提交一个市价单来平掉当前持有的仓位。此后,在当前的回测及后续的实盘信号中,策略将被禁止生成任何新的交易。

该函数有两个参数:value 用于设定我们能承受的最大亏损额;type 则用于定义衡量方式,可以是基于策略权益的百分比(strategy.percent_of_equity),也可以是基于图表品种计价货币的绝对金额(strategy.cash)。它基于策略的总权益来衡量回撤,该权益包括了初始资本、已平仓交易的盈亏以及未平仓头寸的浮动盈亏。正因如此,即使没有平仓交易,一个持仓的浮亏增加也可能导致回撤扩大并触发此风控规则。此函数无法被条件化地设置,它在整个回测期间都保持激活状态,并且始终使用同一个固定的最大回撤阈值。

限制策略的最大仓位规模:strategy.risk.max_position_size()函数

理想情况下,我们策略所承担的每一分风险,都应获得与之相称的回报。然而,并非所有风险都能带来更高收益。特别是当我们交易的头寸规模过大时,一旦亏损,其后果可能严重到几乎无法挽回。本文将详细介绍如何利用TradingView的 strategy.risk.max_position_size() 函数来有效限制策略的持仓规模。

用代码限制TradingView策略的持仓规模

即便是最顶尖的交易策略,也难免会经历资金回撤。应对这些亏损期的一个常用方法,就是减小交易规模。因为在这些时期,哪怕持仓规模只比平时大一点点,都可能造成巨大的亏损。

strategy.risk.max_position_size() 函数能够将一个未平仓头寸的规模,限制在预设的合约、股数、单位或手数之内。通过这种方式,我们的策略就不会建立起比我们指定规模更大的多头或空头仓位。该函数的基础语法格式如下:

strategy.risk.max_position_size(contracts)

其中,contracts 参数是一个必需的数值,用以设定在单个未平仓市场头寸中,所允许的最大合约、股数、单位或手数。

因此,如果我们不希望单方向的持仓超过15个合约,我们可以这样使用该函数:

// 设置单方向持仓规模不得超过15个合约、股数、单位或手数
strategy.risk.max_position_size(contracts=15)

请注意,只有通过 strategy.entry() 函数发送的入场订单,才会受到 strategy.risk.max_position_size() 的限制。而通过 strategy.order() 函数提交的入场订单,则可以创建任意大的头寸。因此,如果你的策略中使用了 strategy.order(),你将需要自己编写额外的代码逻辑来限制持仓规模。

该函数的核心特性

了解了如何调用该函数后,让我们来深入探究它的一些重要特性。

先看此风险规则如何影响入场订单。strategy.risk.max_position_size() 通过以下方式影响由 strategy.entry() 函数提交的入场订单:如果一笔新订单成交后,总持仓不会超过上限,那么该订单将被正常下达;如果一笔新订单完全成交后,将会导致总持仓超过上限,那么TradingView会自动调低这笔订单的数量,以确保最终的总持仓恰好等于上限;如果策略的单向持仓已经达到了上限,那么TradingView将不会下达任何新的同向入场订单。

需要注意的是,该函数在达到持仓上限后,并不会完全停止策略交易。策略依然可以执行以下操作:通过 strategy.exit() 发送的平仓订单;通过 strategy.entry() 发送的反转持仓的订单(例如,从多头转为空头);通过 strategy.order() 发送的任何入场和出场订单——这是因为 strategy.risk.max_position_size() 不会对 strategy.order() 函数施加任何限制。

strategy.order() 函数是一个例外,这是一个至关重要的警示:strategy.risk.max_position_size() 无法限制所有类型的入场订单。通过 strategy.order() 函数提交的交易,完全不受此风险规则的约束。因此,如果希望你的策略严格遵守最大持仓规模的限制,最好不要使用 strategy.order() 来建立仓位。或者,你必须自己编写代码来监控和限制持仓规模。

另外,同时发送多个入场订单可能导致持仓超限。在TradingView中,每个订单的生命周期都分为订单生成和订单执行两个阶段。在订单生成阶段,TradingView会检查该订单是否符合策略逻辑和风险管理设置。一旦获批,订单便进入执行阶段。这个订单生成阶段有一个非常重要的特性:TradingView是孤立地、分别地评估每一笔订单的。在大多数情况下,这不成问题,但当我们在同一时刻发送多个订单时,问题就出现了。

假设我们用 strategy.risk.max_position_size() 将持仓上限设为10个合约,然后用以下代码开仓:

// 当5周期EMA上穿15周期EMA时做多
if ta.crossover(ta.ema(close, 5), ta.ema(close, 15))
    strategy.entry("EL", strategy.long, qty=10)
    strategy.entry("EL 2", strategy.long, qty=10)

当均线交叉时,我们同时提交了两个各10个合约的入场订单。问题就在于,TradingView会独立地审查这两个订单,由于它们各自的规模(10)都没有超过上限(10),因此两个订单都会被批准。

但这显然不是我们期望的结果。因为当这两个订单都成交后,我们的实际持仓将达到20个合约。TradingView之所以会这样处理,是因为它在审查时,是孤立地看待每一笔订单的。

为了确保TradingView能严格遵守 strategy.risk.max_position_size() 的设置,我们需要确保在同一时刻只发送一个入场订单。因此,我们应该将上述两个入场合并为一笔:

// 当5周期EMA上穿15周期EMA时做多
if ta.crossover(ta.ema(close, 5), ta.ema(close, 15))
    strategy.entry("EL", strategy.long, qty=20)

请注意,只有当我们在同一次脚本计算周期内(例如,在同一根K线的同一个 if 代码块中)提交多个入场订单时,才会出现这种持仓超限的问题。如果你的策略虽然有多个入场逻辑,但它们不会在同一时刻被触发,那么就不会出现持仓规模超出设置的情况。

此风险函数始终有效。strategy.risk.max_position_size() 风险规则在每次脚本计算时都处于激活状态。由于策略的任何行为都需要进行脚本计算,因此策略的任何建仓活动都无法绕开此函数的限制。其结果是,整个回测和所有实时信号都会受到影响。要禁用此风控规则,没有捷径可走,只能从代码中移除该行,或者使用Ctrl + /快捷键将其注释掉。

此风险函数不会主动减小已有的超额持仓。它主要做两件事:当持仓达到上限时,它会阻止新的同向入场;如果新订单会导致超仓,它会减小该订单的规模。但是,如果策略的持仓由于某种原因(例如,前述的同时下单问题)已经超过了上限,它不会采取任何措施来减小这个已有的超额持仓。换句话说,此函数只向前看(管理新订单),不向后管(管理已有持仓)。这一点与其他风险管理函数不同。例如,strategy.risk.max_cons_loss_days()strategy.risk.max_drawdown() 在达到其限制时,确实会主动发送平仓订单。因此,如果你的策略要求在任何情况下都不能持有超大仓位,你将需要编写自定义代码来自动缩减超额的持仓。

还有一点:图表上显示的订单规模可能看起来过大。我们通过该函数设定的,是单方向上的最大持仓规模。如果你的策略会进行头寸反转(例如,从多头直接转为空头),那么你在图表上看到的订单规模可能会大于你设定的最大持仓规模。但这并不意味着你的代码或函数出了问题。

举个例子:我们设定最大持仓为10个合约,并且当前正好持有多头10个合约。此时,策略生成了一个做空10个合约的入场信号。为了执行这个操作,TradingView必须总共卖出20个合约:前10个用于平掉现有的多头仓位,后10个用于建立新的空头仓位。因此,虽然我们的最大持仓规模是10,但图表上显示的却是一笔20个合约的订单。它看起来是这样的:

这张图表上的交易以20个合约的规模进行,实现了从多头到空头,或从空头到多头的反转。但策略本身设定的最大持仓规模是10个合约,并且这个上限从未被突破。我们可以在图表下方的指标面板中清晰地看到这一点,该面板绘制了策略的实时持仓规模。

有两种方法可以验证你的策略是否严格遵守了 strategy.risk.max_position_size() 设定的最大持仓限制:一是在图表上绘制策略的持仓规模(strategy.position_size);二是在策略测试器窗口的表现摘要标签页中,查看最大持仓合约这一项的数值。

此风险规则无法被条件化设置。目前,我们无法根据某个特定条件来动态地执行该函数,也无法根据代码逻辑来动态设定其参数。因此,我们不能在 if 语句中来设定最大持仓规模:

// 错误示范:不能在if语句中调用
if close < ta.ema(close, 10)
    strategy.risk.max_position_size(contracts=10)

同样,我们也不能用条件运算符来动态设定合约数量:

// 错误示范:不能用条件运算符设定参数
strategy.risk.max_position_size(contracts=
     close > ta.highest(high, 20)[1] ? 10 : 2)

以上两个例子在保存脚本时都会引发TradingView的编译错误。这意味着,一旦策略中使用了该函数,它将在整个回测及所有后续的实盘信号中持续生效,并且其设定的最大持仓规模也是固定不变的。所以,我们无法根据市场状况的变化来使用不同的最大持仓规模,也无法根据策略的内部逻辑来启用或禁用这一风控规则。

不过,可以通过输入选项来设置最大持仓规模。尽管TradingView目前没有为该函数提供手动的界面设置,但我们可以自己编写一个输入选项来控制它。这样,每次我们想调整最大持仓规模时,就无需再编辑策略代码了。实现这一功能的代码如下:

// 创建一个输入项,用于配置策略的最大持仓规模
maxPosSize = input.int(10, title="最大持仓规模", minval=1)

// 使用输入项的值来设定最大持仓规模
strategy.risk.max_position_size(maxPosSize)

这里,我们通过 input.int() 函数创建了一个整数输入框。我们将其当前值保存在 maxPosSize 变量中。然后,在调用 strategy.risk.max_position_size() 时,将其参数设为我们刚刚创建的这个输入变量。如此一来,策略的最大持仓规模就由用户在界面上设定的值来决定了。

最后,此风险规则仅对当前脚本生效。我们用该函数定义的最大持仓规模,其作用范围仅限于调用了它的这一个策略脚本。可以说,该函数存在三个认知盲区:它不知道其他图表上的策略是如何交易的,不知道我们手动开立了哪些仓位,也不知道我们在经纪商的真实账户中有哪些持仓。因此,理论上,我们仍然可能拥有一个比它所设定的更大的总头寸。但这只会在交易是由该TradingView策略之外的途径产生时才会发生。

示例:设定单次交易持仓不超过500股

了解了该函数的内部机制后,让我们通过一个完整的TradingView策略脚本来看看如何实际应用它。下面的策略基于两条移动平均线的交叉进行交易。当快线上穿慢线时做多,反之则做空。此外,在初始开仓后,我们还会进行加仓操作。

为了防止仓位规模失控,我们使用 strategy.risk.max_position_size() 函数将最大持仓限制在多头或空头500股以内。策略的完整代码如下:

//@version=5
strategy(title="最大头寸大小示例", overlay=true,
     pyramiding=10, default_qty_type=strategy.percent_of_equity,
     default_qty_value=10)

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

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

// 设定多头或空头持仓最多不超过500股
strategy.risk.max_position_size(500)

// 定义交易条件
enterLong  = ta.crossover(quickMA, slowMA)
enterShort = ta.crossunder(quickMA, slowMA)

// 提交初始开仓订单
if enterLong
    strategy.entry("EL", strategy.long)
if enterShort
    strategy.entry("ES", strategy.short)

// 从初始开仓后,每隔5根K线提交一次加仓订单
if strategy.position_size > 0 and ta.barssince(enterLong) % 5 == 0
    strategy.entry("EL+", strategy.long)
if strategy.position_size < 0 and ta.barssince(enterShort) % 5 == 0
    strategy.entry("ES+", strategy.short)

我们首先通过 strategy() 函数配置策略的基本信息。然后用 ta.ema() 计算两条指数移动平均线(EMA),并用 plot() 将它们绘制在图表上。接着,我们设定策略的持仓规模上限:

// 设定多头或空头持仓最多不超过500股
strategy.risk.max_position_size(500)

我们调用 strategy.risk.max_position_size() 函数,并将其参数设为500。这样,我们的策略所持有的任何多头或空头仓位都不会超过500个合约、股、单位或手数。之后,我们定义策略的入场条件:

// 定义交易条件
enterLong  = ta.crossover(quickMA, slowMA)
enterShort = ta.crossunder(quickMA, slowMA)

我们使用 ta.crossover() 来判断金叉,并将返回的布尔值(true/false)存入 enterLong 变量。同样,我们用 ta.crossunder() 来监控死叉,并将结果存入 enterShort 变量。然后,我们提交初始的开仓订单:

// 提交初始开仓订单
if enterLong
    strategy.entry("EL", strategy.long)
if enterShort
    strategy.entry("ES", strategy.short)

enterLong 为真时,我们调用 strategy.entry() 开立多仓;当 enterShort 为真时,则开立空仓。策略的最后一部分代码,实现了金字塔式加仓的逻辑,即每隔5根K线加仓一次:

// 从初始开仓后,每隔5根K线提交一次加仓订单
if strategy.position_size > 0 and ta.barssince(enterLong) % 5 == 0
    strategy.entry("EL+", strategy.long)
if strategy.position_size < 0 and ta.barssince(enterShort) % 5 == 0
    strategy.entry("ES+", strategy.short)

第一个 if 语句检查两个条件:首先,策略是否持有多仓(即 strategy.position_size > 0);其次,距离最初的多头信号(enterLong)是否已经过去了5的倍数根K线。我们使用模数运算符(%)来判断 ta.barssince() 的返回值是否能被5整除。当两个条件都满足时,我们便调用 strategy.entry() 进行一次多头加仓。第二个 if 语句的逻辑与此类似,用于空头方向的加仓。

有了这两段加仓逻辑,我们的策略可以迅速地累积一个较大的仓位。但幸运的是,我们预设的 strategy.risk.max_position_size() 会像一个安全天花板一样,限制住策略的总持仓规模。下面是这个策略在图表上的实际表现:

一旦策略开仓,它会每隔5根K线就加仓一次。但策略并不会无休止地这样做。因为在某个时刻,策略会达到其500股的最大持仓规模。

即使在这张小图上,我们也能看到这条风控规则的效果。每个头寸的最后两笔加仓交易(”ES+”和”EL+”)的规模,都明显小于之前的订单。这正是因为如果不这样做,我们的持仓就会超出 strategy.risk.max_position_size() 所允许的上限。

示例:限制并追踪最大持仓规模

以下策略交易的是两条指数移动平均线(EMA)的金叉或死叉。当策略持有多头或空头仓位时,我们会进行分批加仓。为了防止持仓规模变得过大,我们通过 strategy.risk.max_position_size() 将仓位上限设为1,000股。

同时,我们也会追踪并绘制策略的实时持仓规模。这样,我们便可以在回测过程中直观地看到持仓规模的演变,并清晰地定位该函数开始生效的时刻。完整的策略代码如下:

//@version=5
strategy(title="示例:最大持仓规模", overlay=false,
     pyramiding=15, default_qty_type=strategy.fixed,
     default_qty_value=100, precision=0)

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

// 设定单方向最大持仓为1,000股
strategy.risk.max_position_size(1000)

// 绘制持仓规模及其上限
plot(strategy.position_size, style=plot.style_areabr, color=
     strategy.position_size > 0 ? color.green : color.red)
hline(1000, color=color.orange, linestyle=hline.style_solid)
hline(-1000, color=color.orange, linestyle=hline.style_solid)

// 定义交易条件
enterLong  = ta.crossover(quickMA, slowMA)
enterShort = ta.crossunder(quickMA, slowMA)

// 提交初始入场订单
if enterLong
    strategy.entry("EL", strategy.long)

if enterShort
    strategy.entry("ES", strategy.short)

// 自初始入场后,每隔5根K线提交一次加仓订单
if strategy.position_size > 0 and ta.barssince(enterLong) % 5 == 0
    strategy.entry("EL+", strategy.long)

if strategy.position_size < 0 and ta.barssince(enterShort) % 5 == 0
    strategy.entry("ES+", strategy.short)

我们首先通过 strategy() 函数配置策略设置,其中包括将默认订单数量设为100股。然后,我们用 ta.ema() 函数计算两条移动平均线。接着,我们限制策略的最大持仓规模:

// 设定单方向最大持仓为1,000股
strategy.risk.max_position_size(1000)

这里,我们执行 strategy.risk.max_position_size() 函数,并将其参数值设为1000。这使得我们的策略在多头或空头方向上的最大持仓不会超过1,000股。随后,我们绘制出策略的实际持仓规模:

// 绘制持仓规模及其上限
plot(strategy.position_size, style=plot.style_areabr, color=
     strategy.position_size > 0 ? color.green : color.red)
hline(1000, color=color.orange, linestyle=hline.style_solid)
hline(-1000, color=color.orange, linestyle=hline.style_solid)

这里,我们首先用 plot() 函数来绘制 strategy.position_size 变量的值。该变量在策略持多仓时返回正的持仓数,持空仓时返回负的持仓数,无持仓时返回零。

我们用面积图来展示这个规模。图表的颜色是根据条件动态设定的:持多仓时为绿色(color.green),否则为红色(color.red)。我们还使用 hline() 函数绘制了两条水平线,分别位于+1,000和-1,000的位置。这使得我们可以轻松地识别出允许的最大多头和空头持仓上限。我们将这两条线设为橙色(color.orange)的实线。

然后,我们定义策略的交易条件:

// 定义交易条件
enterLong  = ta.crossover(quickMA, slowMA)
enterShort = ta.crossunder(quickMA, slowMA)

我们创建了两个布尔变量 enterLongenterShort。当25周期EMA上穿200周期EMA时,enterLongtrue;否则为 false。同理,当发生死叉时,enterShorttrue。我们分别使用 ta.crossover()ta.crossunder() 函数来判断这些条件。之后,我们提交策略的初始入场订单:

// 提交初始入场订单
if enterLong
    strategy.entry("EL", strategy.long)

if enterShort
    strategy.entry("ES", strategy.short)

这两个 if 语句分别在 enterLongenterShorttrue 时,通过 strategy.entry() 函数建立初始的多头或空头仓位。最后,我们通过一小段代码来实现分批加仓:

// 自初始入场后,每隔5根K线提交一次加仓订单
if strategy.position_size > 0 and ta.barssince(enterLong) % 5 == 0
    strategy.entry("EL+", strategy.long)

if strategy.position_size < 0 and ta.barssince(enterShort) % 5 == 0
    strategy.entry("ES+", strategy.short)

这段代码的逻辑是:只要策略持有多仓,就从初始入场信号(enterLong)算起,每隔5根K线就加仓一次。同理,当持空仓时,也从初始入场信号(enterShort)算起,每隔5根K线加仓一次。

当然,这种加仓行为并不会无限持续下去。毕竟,我们已经使用了 strategy.risk.max_position_size() 来限制最大持仓规模。以下是该策略在图表上的行为方式。这里展示了微软股票的大量交易,每笔订单都是100股。在主图下方,我们能看到策略的持仓规模变化。在初始的多头或空头入场后,持仓规模稳步增加。

但这种增加只持续到某个点。当策略的持仓达到1,000股(无论是多头还是空头)时,便不再有新的加仓交易,持仓规模也保持在水平线上。这正是 strategy.risk.max_position_size() 函数在起作用。

优点与局限性

为了总结本文,让我们来看看这个风险管理函数的长处与不足。优点方面:一是便捷性,这是一个快速限制策略最大持仓规模的方法,为我们省去了自己编写和测试相关代码的麻烦;二是自动调整,当持仓接近上限时,该函数会自动计算并调整新订单的规模,以确保不超过限制,这样我们就不必自己进行复杂的订单数量计算。

局限方面:一是无法条件化启用,我们不能根据条件来启用或禁用此函数,因此它在整个回测和所有实时信号期间都将保持激活状态。二是无法条件化设置,我们不能根据条件来动态设置它的值,这意味着无论市场状况或策略表现如何,策略都将使用同一个固定的持仓上限。三是多空同限,我们无法为多头和空头分别定义不同的最大持仓规模,该设置会同时应用于多头和空头两个方向。四是 strategy.order() 的例外,此风控函数无法限制由 strategy.order() 函数发送的订单,因此,如果我们使用该订单函数,仍然有可能建立起超额的仓位。

本文我们讨论了 strategy.risk.max_position_size() 如何限制最大持仓。但TradingView还提供了更多风险管理工具:strategy.risk.allow_entry_in() 可以禁用多头或空头交易,使策略只做单边;strategy.risk.max_cons_loss_days() 限制连续亏损的天数,以防止长期的连败;strategy.risk.max_drawdown() 限制策略的最大资金回撤,一旦触及上限便停止策略;strategy.risk.max_intraday_filled_orders() 限制日内的成交订单总数,防止策略交易过于频繁;strategy.risk.max_intraday_loss() 定义最大日内交易亏损,一旦触及,便停止当日交易,次日再战。我们还可以在同一个策略中组合使用多个风险函数,从而精确地定义我们希望策略承担的风险。

简单总结一下:通过 strategy.risk.max_position_size() 函数,我们可以将策略的未平仓头寸规模限制在预设的合约、股数、单位或手数之内。这个风险函数有两个实用的功能:当一笔入场订单可能导致持仓超限时,它会自动缩减该订单的规模;当策略已经达到最大持仓时,它会阻止新的同向入场。

但有几点需要特别注意:该函数在整个回测和实盘期间都使用同一个固定的上限值;虽然它能阻止新订单,但当持仓因故变得过大时,它并不会主动发送平仓订单来减仓。此外,该函数只对 strategy.entry() 函数发送的入场订单有效,而会忽略 strategy.order() 函数生成的入场。并且,当我们在同一时刻提交多个 strategy.entry() 订单时,仍有可能导致最终的持仓规模超出限制。

赞(0)
未经允许不得转载:图道交易 » Pine Script(245):限制策略最大回撤与最大仓位规模
分享到

评论 抢沙发

登录

找回密码

注册