策略脚本的两个订单阶段
要让我们的策略脚本进行交易,只需调用一个TradingView的订单函数来提交订单即可。但在一段代码最终成为一个实际成交的订单之前,TradingView必须经过两个步骤。让我们来一探究竟,看看策略脚本的幕后到底发生了什么。
有几个TradingView订单函数可以让我们的策略脚本根据交易信号开仓、止盈和止损。这些订单看起来像是在图表上被TradingView自动执行了,但实际上,每一个订单在幕后都会经历两个不同的阶段。
理解TradingView是如何处理和执行订单的,能帮助我们更好地把握策略的行为,同时也能避免我们因一些看似bug的现象而感到困惑。
在我们策略脚本生成的订单被提交之前,它会经历以下两个阶段:
- 订单生成阶段(Order Generating Phase):在这个阶段,TradingView会检查订单的条件、策略的整体设置以及风险管理规则。根据这些审核的结果,TradingView会决定是批准还是拒绝这个订单请求。
- 订单执行阶段(Order Execution Phase):在这个阶段,TradingView会提交实际的订单并监督其执行过程。订单成交后,TradingView会将结果报告给策略。
这两个步骤适用于策略脚本提交的每一个订单,无论是在历史回测、实时模拟测试,还是在真实的实盘交易中。
虽然这两个阶段听起来很理论化,但它们会带来非常实际的后果。举个例子,你知道吗,如果你在同一时间发送多个订单,最终可能会开立一个远超预期的仓位?这个现象背后的原因,正是TradingView的这两个订单处理阶段。所以,让我们来深入了解一下。
订单生成阶段:条件审核与订单批准
当我们的策略脚本执行一个订单函数时,这个订单请求首先会进入订单生成阶段。在这个阶段,系统会审核订单的各项条件,以判断这个订单是否真的应该被创建。只有在这个阶段被批准的订单,才能进入下一阶段(订单执行阶段)。否则,这个订单命令就会被丢弃,我们的订单也就不会被提交到市场。
在订单生成阶段,TradingView除了会考虑我们代码中明确编写的订单条件外,还会审核其他多个方面:
- 当我们提交一个开仓订单时,策略的金字塔加仓(pyramiding)设置是否允许(再次)开仓?
- 是否有风险管理函数(例如
strategy.risk.*)禁止当前订单的执行? - 当我们使用一个平仓函数时,当前是否真的有持仓可供平掉?
- 市场中是否已经存在一个具有相同订单ID的待处理订单?如果有,TradingView会修改那个待处理的订单,而不是创建新订单。
- 当我们使用一个函数来平掉特定开仓单的仓位时(例如
strategy.exit()的from_entry参数),具有该ID的开仓单当前是否真的存在? - 当我们使用一个可以反转持仓的订单函数时,当前是否有持仓可供反转?还是应该直接开立一个新仓位?
注意:并非所有的策略订单函数都会受到相同的条件约束。例如,
strategy.order()函数就不受strategy.risk.allow_entry_in()风险管理函数的影响,策略的金字塔加仓设置对它也无效。因此,TradingView在批准订单前具体会检查哪些条件,也取决于我们使用了哪个订单函数(当然还有策略的整体设置和风险管理规则)。
只有当上述所有类似条件都满足时,订单才会被放行;只要有一个或多个条件不满足,订单请求就会被拒绝。
所以,在订单生成阶段,并不仅仅是我们的代码逻辑决定订单能否被发送,策略的设置(如金字塔加仓)和当前的上下文(如是否有持仓)同样至关重要。
订单生成阶段有几个非常重要的特性,我们来逐一分析。
TradingView对订单进行独立评估
订单生成阶段最重要的一个特性是:每一个订单都是被独立评估的。这意味着TradingView会拿起一个订单请求,单独检查它是否满足所有条件。如果满足,订单就被批准,然后进入订单执行阶段。
但TradingView不会做的是,评估同时提交的多个订单组合在一起会对策略设置产生什么影响。假设我们的策略中有以下代码,它会在同一时间执行两个开仓订单:
//@version=5
strategy(title="Strategy example", pyramiding=0)
if close > ta.ema(close, 10)
strategy.entry("EL1", strategy.long)
strategy.entry("EL2", strategy.long)
在这里,strategy() 函数设置了禁止金字塔加仓(pyramiding=0)。然后,一个 if 语句判断K线收盘价是否高于10周期EMA。如果条件满足,我们连续两次调用 strategy.entry() 来提交两个做多订单。
虽然我们调用了两次 strategy.entry(),但理论上只有一个订单应该成交,因为我们禁止了加仓。然而,实际情况并非如此。因为TradingView在订单生成阶段是独立评估每一个订单的。在评估第一个订单 EL1 时,策略没有持仓,满足开仓条件,所以 EL1 被批准。在评估第二个订单 EL2 时,由于 EL1 还未进入执行阶段,策略的持仓状态仍然是空仓,所以 EL2 也被批准了。最终,TradingView提交了两个订单。
当然,这两个订单的综合效果就是建立了一个比我们预期要大的仓位。然而,TradingView并不会在生成阶段就去考虑如果所有被批准的订单都成交了会怎样。
注意:如果我们的策略已经有了一个持仓,那么在
pyramiding=0的设置下,任何新的开仓订单都会在生成阶段被拒绝。我们上面描述的问题,主要发生在同时提交多个订单,且每个订单单独来看都符合条件,但它们的组合效果却违背了策略设置的场景中。
我们可以这样来理解这个特性:因为存在两个独立的阶段,一个在订单生成阶段被批准的订单,并不意味着它在进入订单执行阶段后仍然是合规的。
既然我们知道了TradingView会独立批准同时提交的多个订单,即便它们的组合效果会违反策略设置,我们该如何应对呢?
我们无法改变TradingView在订单生成阶段的评估机制。但我们能做到的是,避免在同一时间提交多个订单。例如,与其发送多个开仓订单,我们应该将它们的订单数量合并,通过一次 strategy.entry() 调用来完成。
对于平仓订单,我们需要确保它们的触发条件是互斥的。这样,在每一次脚本计算中,最多只会生成一个平仓订单。否则,我们可能会卖出或买入超预期的数量,导致最终持有一个非预期的反向仓位。
幸运的是,对于平仓订单,我们主要需要关注的是 strategy.order() 函数。其他的平仓函数,如 strategy.exit() 和 strategy.close(),只能从已有的持仓进行平仓,不会开立反向仓位。而 strategy.order() 则有能力在平仓的同时开立一个反向的新仓位。
订单执行阶段:执行订单并反馈结果
在第二个阶段,TradingView会执行订单。执行完毕后,它会将结果报告给策略。这样,我们的策略脚本就可以在下一次计算时使用这些最新的状态信息。
当一个订单到达订单执行阶段时,它已经是被TradingView批准过的。因此,几乎所有进入这个阶段的订单都会被执行。主要有两个例外:属于订单组的订单,以及被我们策略脚本后续取消的待处理订单。
当多个订单被绑定在一个订单组中时(例如OCA——One Cancels Another组),一旦其中一个订单成交,组内其他订单就会被取消或减小数量。当然,这个功能需要TradingView有足够的时间来反应。如果一个OCA组内的几个订单触发价非常接近(例如,一个在 high,另一个在 high + 2 ticks),当一个订单成交时,系统可能来不及在订单执行阶段取消另一个。
一个订单会通过以下两种方式离开订单执行阶段:
- 订单成交:成交后,TradingView会将成交详情(如价格、数量)报告给策略。
- 订单被取消:取消操作可以由我们的策略代码发起,也可以由TradingView基于订单组的规则自动执行。
如果我们先取消了一个订单,之后又重新提交了它,那么它会重新从订单生成阶段开始走一遍流程。
总结
当我们的策略脚本执行一个订单函数时,它会提交订单。但在订单最终成交前,它会经历两个阶段:订单生成阶段和订单执行阶段。
在订单生成阶段,TradingView会评估订单的特性、策略的整体设置以及风险管理规则。只有当所有条件都满足时,订单才会被批准进入下一阶段。
在订单执行阶段,TradingView执行订单并反馈结果。在这个阶段,TradingView也可以取消或修改属于订单组的订单。我们的脚本同样可以在这个阶段取消订单。
这些阶段中一个非常重要的特性是:在订单生成阶段,TradingView对每个订单进行独立评估。因此,如果我们同时提交多个开仓订单,而策略又禁止金字塔加仓,那么每个订单都可能会被单独批准。只有当TradingView在下一阶段执行它们时,我们才会发现最终的仓位比预期的要大。但到那时,TradingView已经批准并成交了这些订单。
策略持仓状态如何影响strategy.order()
Pine Script的 strategy.order() 函数用于开仓和平仓。准确地说,这个订单函数可以实现开立一个新仓位、对现有仓位进行加仓、部分平仓、全部平仓,以及将仓位反向。
现在,有趣的地方来了。strategy.order() 具体执行何种操作,并不取决于我们如何调用这个函数,而是完全由策略在调用它那一刻的当前持仓状态所决定。让我们来深入了解。
先看一个快速示例。假设策略当前是空仓状态,没有任何持仓。当策略执行一个 strategy.order() 买入(做多)订单时,Pine Script会开立一个新的多头仓位。
现在,假设策略已经持有多仓。此时,我们执行相同的 strategy.order() 买入订单。由于已存在多头仓位,这个函数的操作就变成了加仓(金字塔式),在原有基础上增加多头头寸。
尽管在这两种情况下,我们都告诉 strategy.order() 去买入,但结果却截然不同:一个是开立新仓,另一个是增加仓位。接下来,我们从多头和空头两个角度来详细分析。
多头strategy.order()订单
假设我们像下面这样执行 strategy.order() 函数:
// 当价格穿越20周期SMA时,买入(做多)5手合约
if ta.cross(close, ta.sma(close, 20))
strategy.order("Enter Long", strategy.long, qty=5)
这个 if 语句通过 ta.cross() 函数判断收盘价是否穿越了20周期的简单移动平均线。当交叉发生时,strategy.order() 函数会提交一个买入(strategy.long)订单。
这个订单会买入5手合约。但执行后策略的仓位会是怎样的呢?
答案是:我们无法仅从这段代码中得知。因为策略在执行这行代码时,可能已经持有多仓、空仓,或者完全没有仓位。
实际上,这同一个买入订单可以触发五种完全不同的操作:开立新多仓、对多仓加仓、平掉全部空仓、部分平掉空仓(减仓),以及把空仓反手成多仓。具体是哪一种,完全取决于策略当前的持仓状态。
下表详细分析了各种可能性。第一列是 strategy.order() 执行时的持仓状态,第二列是 strategy.order() 的指令,第三列则是最终的仓位变化结果:
| 当前持仓状态 | strategy.order() 指令 |
结果 |
|---|---|---|
| 空仓(无持仓) | 买入5手 | 开立一个全新的、数量为5手的多头仓位。 |
| 持有多仓3手 | 买入5手 | 在现有仓位上加仓5手,总仓位变为8手多仓。 |
| 持有空仓-5手 | 买入5手 | 平掉全部空仓。买入的5手正好对冲掉持有的-5手空仓,最终仓位变为空仓。 |
| 持有空仓-8手 | 买入5手 | 部分平掉空仓(减仓)。买入的5手使空仓数量从-8手减少到-3手。 |
| 持有空仓-3手 | 买入5手 | 反手开多仓。买入的5手中,3手用于平掉-3手的空仓,剩下的2手开立了一个新的多头仓位。 |
请注意,在上表中,strategy.order() 执行的指令始终是同一个:买入5手。但它对策略仓位造成的影响却截然不同,这完全是因为每次执行时的初始持仓状态不同。
空头strategy.order()订单
同样的原理也适用于空头订单:策略当前的持仓状态决定了一个 strategy.order() 卖出(做空)订单会产生什么样的结果。
假设我们的策略中有这样一段代码:
// 当K线最低价低于过去20根K线的最低点时,卖空250股
if low < ta.lowest(low, 20)[1]
strategy.order("Enter Short", strategy.short, qty=250)
这段代码判断价格是否创下了20周期新低。如果创了新低,strategy.order() 函数就会提交一个卖出(strategy.short)订单。
这个订单会卖空250股。但这会给我们的策略带来怎样的仓位变化呢?同样地,我们无法仅从代码本身得出结论,因为我们不知道策略的初始持仓状态。
这个卖出订单同样可能导致五种不同的结果:开立新空仓、对空仓加仓、平掉全部多仓、部分平掉多仓(减仓),以及把多仓反手成空仓。
下表列出了各种可能性:
| 当前持仓状态 | strategy.order() 指令 |
结果 |
|---|---|---|
| 空仓(无持仓) | 卖出250股 | 开立一个全新的、数量为-250股的空头仓位。 |
| 持有空仓-100股 | 卖出250股 | 在现有仓位上加仓-250股,总仓位变为-350股空仓。 |
| 持有多仓250股 | 卖出250股 | 平掉全部多仓。卖出的-250股正好对冲掉持有的250股多仓,最终仓位变为空仓。 |
| 持有多仓400股 | 卖出250股 | 部分平掉多仓(减仓)。卖出的-250股使多仓数量从400股减少到150股。 |
| 持有多仓100股 | 卖出250股 | 反手开空仓。卖出的-250股中,100股用于平掉多仓,剩下的-150股开立了一个新的空头仓位。 |
strategy.order() 执行的指令每次都完全相同:卖出250股。但最终的结果却因初始持仓状态的不同而千差万别。
获取初始持仓状态
既然策略的初始持仓状态决定了 strategy.order() 的行为,那么我们如何才能在代码中获知这个状态呢?答案是使用 strategy.position_size 变量。
通过检查 strategy.position_size 的值,我们就可以,例如,只在空仓时才执行 strategy.order()。当我们能够精确控制 strategy.order() 的执行情境时,我们就能可靠地预测这个函数将会如何改变我们的策略仓位了。
总结
strategy.order()函数可以发送买入(做多)或卖出(做空)订单。- 同一个
strategy.order()指令可能会产生5种不同的效果:开新仓、加仓、全部平仓、部分平仓(减仓),以及反手开仓。 - 具体会产生哪种效果,完全取决于
strategy.order()函数被调用时,策略的当前持仓状态。 - 我们可以通过内置变量
strategy.position_size来获取当前的持仓状态,从而在特定情境下才执行strategy.order(),以此来精确控制和预测它的行为。


