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

Pine Script(261):外包月策略与市场跳空波段交易

#Pine Script入门教学

为TradingView编写的外包月策略

价格形态策略旨在寻找特定的K线组合,观察它们是否预示着一轮价格运动,或是标志着一段趋势的终结。其中一种形态便是月度外包线(Outside Month)。本文将探讨如何将这种价格行为编写成一个TradingView策略。

在TradingView中交易詹姆斯·阿图彻的月度外包线策略

在其著作《像对冲基金一样交易》(Trade Like a Hedge Fund)中,詹姆斯·阿图彻(James Altucher)描述了多种交易策略。其中一些策略看起来非常基础,但这是他有意为之:他坚信,简单的策略效果最好,也更具稳健性。他会将每一个策略都应用到一个更大的投资组合中。一个由交易着不相关资产的、不相关的策略所组成的投资组合,并采用小头寸规模,最有机会获得良好且持续的回报。

阿图彻的几个策略思路,都围绕着对极端走势进行反向交易。其中之一,便是月度外包线(Outside Month)策略,它基于一种价格形态。该策略蕴含的基本思想是:当市场经历了一个异常宽幅的月份,其最高点和最低点都超越了前一个月的范围时,这种加剧的波动性在接下来的月份中,有向均值回归的趋势。而这种后续波动性的降低,使得价格上涨的可能性更大。

接下来,让我们看看该策略具体的交易规则,然后将其转化为TradingView代码。

月度外包线策略的交易规则

该策略包含以下交易规则。做多入场规则:在每个月的第一天,如果上个月的最高价高于前一个月的最高价,并且上个月的最低价低于前一个月的最低价,则买入S&P 500指数(这个条件构成了经典的月度外包线或月度吞没形态)。做多离场规则:在该月的最后一个交易日收盘时,退出多头头寸。做空入场规则:在每个月的第一天,如果上个月的最高价低于前一个月的最高价,并且上个月的最低价高于前一个月的最低价,则做空S&P 500指数(这个条件构成了经典的月度内包线(Inside Month)或孕线形态)。做空离场规则:在该月的最后一个交易日收盘时,回补空头头寸。

顺便提一下,阿图彻本人并不从空头方向交易这个策略。他认为做空并非做多的简单反向操作。做空的交易风险(理论上)是无限的,而市场(理论上)又存在一种天然的上涨倾向。

尽管情况可能如此,但除非我们亲手编写并测试,否则我们无从知晓空头方向的表现究竟如何。因此,我自行决定在月度外包线策略中也加入了做空交易规则。如果回测显示空头头寸只会亏损,我们可以轻松地调整代码,让策略只从多头方向进行交易。

为TradingView编写月度外包线交易策略

现在,让我们基于以上交易规则,来编写一个TradingView策略。一个有效的方法是使用模板,它能将一项大任务分解成更小的部分,其增加的结构性也能让项目更容易上手,且不易遗漏环节。

这是我们将用来编写月度外包线策略的模板:

//@version=5
// 步骤1. 定义策略设置
// 步骤2. 计算策略所需指标值
// 步骤3. 在图表上输出数据
// 步骤4. 定义多头交易条件
// 步骤5. 定义空头交易条件
// 步骤6. 提交入场订单
// 步骤7. 提交出场订单

如果你想跟随本文一起操作,请在TradingView的Pine编辑器中创建一个新的策略脚本,并将上述模板粘贴进去。

为了让你对我们即将编写的代码有个直观的认识,以下是最终完成的月度外包线策略在图表上的样子:

现在,就让我们开始动手,将上述交易规则转化为一个功能完整的TradingView策略脚本。

步骤1:定义策略设置

首先,我们来配置策略的各项参数。我们使用 strategy() 函数来实现这一步:

// 步骤1. 定义策略设置
strategy(title="外包月策略", overlay=true,
     pyramiding=0, initial_capital=100000,
     default_qty_type=strategy.fixed,
     default_qty_value=100,
     commission_type=strategy.commission.cash_per_order,
     commission_value=8, slippage=2)

我们通过 title 参数为策略命名。overlay=true 使策略直接叠加在主图表上。根据交易规则,我们通过 pyramiding=0 禁止加仓。initial_capital 将策略的初始资金设为100,000。

订单数量(default_qty_type)设为固定手数(strategy.fixed),每笔多头或空头交易都固定为100股(default_qty_value=100)。

在交易成本方面,我们设定每笔单边订单(commission_type)收取8个单位的固定佣金(commission_value=8),并假设市价单和止损单会产生2个最小跳动点的滑点(slippage=2)。这里的成本设置相对悲观,这样做是为了让策略在更严苛的条件下证明其有效性。

步骤2:计算交易策略所需的值

在第二步,我们计算策略运行所需的数据。这主要包括两个动作:首先,我们需要加载月度级别的时间框架数据;然后,我们需要比较这些数据,以判断是否存在内包月或外包月形态。

下面是我们如何将月度数据加载到策略脚本中的方法:

// 步骤2. 计算策略所需值
highPrevMonth = request.security(syminfo.tickerid, "M", high)[1]
lowPrevMonth  = request.security(syminfo.tickerid, "M", low)[1]

high2MonthsBack = request.security(syminfo.tickerid, "M", high[1])[1]
low2MonthsBack  = request.security(syminfo.tickerid, "M", low[1])[1]

我们通过TradingView的 request.security() 函数来跨时间周期请求数据。

前两行代码调用 request.security() 函数来加载上个月的最高价和最低价。函数的第一个参数 syminfo.tickerid 表示我们请求的是当前图表品种的数据。第二个参数 "M" 指定了时间周期为月线。第三个参数 highlow 指定了所需的数据类型。最后的 [1] 是一个历史引用操作符,它确保我们获取的是已完成的上一个月份的数据,而不是当前正在形成的月份的数据。

接下来的两行代码逻辑类似,但我们通过 high[1]low[1] 的方式,请求的是上个月的上个月,即两个月前的月度高低点数据。我们将这些获取到的历史数据分别存储在相应的变量中。

现在,我们来判断这些月份是否构成了内包月或外包月形态。代码如下:

outsideMonth = highPrevMonth > high2MonthsBack and
     lowPrevMonth < low2MonthsBack

insideMonth = highPrevMonth < high2MonthsBack and
     lowPrevMonth > low2MonthsBack

我们在这里定义了两个布尔型(真/假)变量。第一个 outsideMonth 用于判断上个月是否为外包月。其条件是:上个月的最高价(highPrevMonth)高于两个月前的最高价,并且上个月的最低价(lowPrevMonth)低于两个月前的最低价。

另一个变量 insideMonth 用于判断上个月是否为内包月。其条件是:上个月的最高价低于两个月前的最高价,并且上个月的最低价高于两个月前的最低价。

步骤3:输出数据并可视化信号

计算完策略所需的值后,我们在图表上将识别出的内包月和外包月高亮显示出来:

// 步骤3. 输出策略数据
bgcolor(outsideMonth ? color.new(color.green, 90) : na)
bgcolor(insideMonth ? color.new(color.red, 90) : na)

我们使用 bgcolor() 函数来为图表背景上色。第一个 bgcolor() 调用会判断 outsideMonth 变量是否为 true,如果是,就将背景染成90%透明度的绿色;否则,不应用任何颜色。第二个调用的逻辑类似,用于在出现内包月时将背景染成红色。

由于 outsideMonthinsideMonth 变量是基于月度数据计算的,它们的值会在一个月内保持不变,因此我们会在日线图上看到持续数周的绿色或红色背景。

步骤4:编写多头交易规则

接下来,我们将策略的多头交易规则转化为Pine Script代码。我们为此定义了两个变量:

// 步骤4. 定义多头交易条件
enterLong = outsideMonth

exitLong = month != month[1] and
     strategy.position_size > 0

多头开仓的条件非常简单:只要上个月是外包月(outsideMonthtrue),enterLong 变量就为 true

多头平仓的条件 exitLong 则由两个条件共同决定:一是当前K线是本月的第一根K线(通过 month != month[1] 判断),二是当前策略确实持有多头仓位(strategy.position_size > 0)。我们加入后一个条件,是为了确保平仓指令只在持有多仓时才有效。

需要特别说明的是,我们在此处对平仓规则做了微调,与原作者阿图彻的描述有所不同。原始规则要求在当月的最后一个交易日收盘时平仓。但在TradingView中实现这一点存在两个技术难题:

首先,在回测中,我们无法确保订单能在K线的收盘价精确成交,在收盘时发出的订单最快也要在下一根K线的开盘价成交。其次,脚本在运行时无法预知当天是否为该月的最后一个交易日。

因此,我们选择在每个月的第一根K线开盘时平掉上个月的仓位。我们假设在一个月的交易周期中,这一两天的差异不会对策略的核心逻辑产生颠覆性影响。

步骤5:编写空头交易条件

接下来,我们以类似的方式编写策略的空头交易条件:

// 步骤5. 定义空头交易条件
enterShort = insideMonth

exitShort = month != month[1] and
     strategy.position_size < 0

此处的代码逻辑与我们定义多头条件时非常相似。我们将 enterShort(做空入场)布尔变量的值,直接设为 insideMonth(上一个月是内包线形态)。

对于 exitShort(做空离场)变量,我们设定了两个必须同时满足的条件:首先,当前K线必须是本月的第一天(month != month[1]);其次,策略当前必须持有着空头仓位(strategy.position_size < 0)。

步骤6:通过入场订单建立交易头寸

现在我们已经计算好了所有策略所需的数据,让我们来实际建立交易:

// 步骤6. 提交入场订单
if enterLong
    strategy.entry("EL", strategy.long)

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

第一个 if 语句检查 enterLong 变量是否为 true。当上一个月是外包线形态时,这种情况便会发生。此时,我们执行 strategy.entry() 函数,提交一个市价单来建立一个多头仓位(strategy.long)。我们将这个订单命名为”EL”。请注意,我们在这里没有指定订单数量;这是因为我们已在策略设置中将默认下单量设为100股。

第二个 if 语句则检查 enterShort 变量的值。当上一个月是内包线形态时,该变量为 true。此时,我们让 strategy.entry() 建立一个空头仓位,并将其命名为”ES”。

步骤7:通过出场订单平掉市场头寸

在最后一步,我们来处理平仓逻辑。我们通过以下TradingView代码来实现:

// 步骤7. 提交出场订单
if exitLong and not enterShort
    strategy.close("EL", comment="XL")

if exitShort and not enterLong
    strategy.close("ES", comment="XS")

这里有一个需要特别注意的逻辑冲突:平多仓的条件(exitLong,即新月份的第一天)与开空仓的条件(enterShort,即上个月是内包线)可能在同一根K线上同时触发。

我们需要知道,strategy.entry() 函数本身就具备反转仓位的功能。也就是说,如果策略当前持有多仓,而 enterShort 条件满足,strategy.entry() 会自动先平掉多仓,然后再开立一个空仓。

对于月度外包线策略,这可能会引发问题。如果平仓信号(exitLong)和反向开仓信号(enterShort)同时发生,strategy.entry() 会先将多仓反转为空仓,而紧接着的 strategy.close() 可能会认为它还需要再卖出一次才能平掉原来的多仓,最终导致我们持有一个比预期更大的空头仓位。

为了避免这种混乱,我们的 if 语句增加了一个额外的检查:if exitLong and not enterShort。我们检查 exitLong 的同时,也确保 enterShortfalse(通过 not enterShort)。这确保了 strategy.close() 只在单纯需要平仓、而不是需要反转开仓时才执行。此时,我们调用 strategy.close() 来平掉所有名为”EL”的订单。

第二个 if 语句的逻辑与此类似。它确保了平空仓的操作不会与新的多头开仓信号发生冲突。

TradingView的月度外包线策略的表现

我们先来看看这个策略表现好的一面。在某些月份,一个更宽的交易范围(即外包线)确实会引来一轮上升趋势;而一个更窄的交易范围(内包线)有时也确实预示着一轮短期的下跌。

例如,下图展示了策略在220点附近做多,并在一个月后以每股6美元的利润平仓的案例:

不幸的是,同样有足够多的月份,其价格走势与月度外包线策略的预期完全相反。也就是说,在外包线之后,价格下跌了(而非上涨);而在内包线之后,价格反而上涨了。

例如,下方的图表就展示了数笔交易,其趋势与策略的预测背道而驰:

下表展示了在标普500 ETF和纳斯达克100 ETF上的回测结果。鉴于上文的图表表现,这个负收益的结果或许并不令人意外。但这是否意味着该策略无效还很难说——策略每年只交易大约2到3次。这给我们留下的交易样本太小,无法自信地断定该策略存在缺陷。(当然,其初步迹象并不乐观。)

绩效指标 SPY (S&P 500 ETF) QQQ (Nasdaq 100 ETF)
首次交易 1993-03-02 1999-06-02
最后交易 2018-05-02 2017-09-05
时间周期 日线 日线
净利润 -$2,311 -$3,547
总盈利 $13,229 $4,404
总亏损 -$15,540 -$7,951
最大回撤 $7,341 $4,474
盈利因子 0.851 0.982
总交易数 66 40
胜率 50% 45%
平均每笔交易 -$35.02 -$88.68
平均盈利 $400.88 $244.67
平均亏损 -$470.91 -$361.41
盈亏比 0.851 0.677
支付佣金 $952 $600
滑点 2 跳 2 跳

你可能还记得,原作者阿图彻并未给外包月策略定义做空规则。他并不认为做空是做多的简单反向操作,并认为空头仓位的风险远大于多头。

事实证明,他的这种偏好或许有其道理。外包月策略的多头部分在SPY和QQQ上都是盈利的——而两个品种的空头部分都亏了钱。

在标普500 ETF上,共有35笔多头交易,胜率高达62.86%,平均每笔交易盈利$104.20,盈利因子为1.773。在纳斯达克100 ETF上,有23笔多头交易,其中47.83%盈利,平均每笔多头交易净赚$6.30,盈利因子为1.044。(但尽管有这些积极的数据,交易总数依然过少,不足以断定该策略的多头方面确实有效。)

改进思路与新策略方向

正如以上结果所示,外包月策略绝对有进一步优化的空间。以下是一些你可能会觉得有趣的探索方向。

时间框架的适应性:阿图彻未解释为何选择月度时间框架。虽然月线图上有更大的价格波动,但交易机会也相应稀少。如果能将外包月的核心思想应用到更低的时间框架(如日线),我们或许能获得更多的交易样本,从而更有信心地判断其基本逻辑是否有效。

引入风险控制:该策略目前没有任何形式的交易风险限制。尽管增加止损可能会降低胜率,但它通常能改善净利润,并且风险调整后的回报肯定会更好看。

优化退出机制:原作者也未说明为何持仓周期要长达一个月。虽然长达一个月的上升趋势是可能出现的,但价格持续上涨整整一个月的情况并不常见。因此,很可能在月中就有更好的退出时机。其他可行的退出方式包括使用移动止损,或者在出现连续两个更低的收盘价时(对多头而言)退出。

量化外包的强度:当前策略只看高低点是否突破,而不考虑突破的幅度。一个月线区间仅仅比前一个月大2个跳动点,也被算作外包月,但这可能并非原作者想要交易的那种剧烈波动的月份。也许我们可以通过衡量月线区间的增幅(用跳动点或ATR倍数)来过滤掉这些意义不大的突破。

整合其他风险管理:也许策略也能从更全面的风险管理中受益。例如,我们可以限制最大持仓规模以防范过度风险,或者在达到最大回撤时停止交易。

完整代码:TradingView的外包月策略

外包月策略的完整代码如下所示。关于代码的详细解释,请参阅上文的讨论。

//@version=5
// 步骤1. 定义策略设置
strategy(title="外包月策略", overlay=true,
     pyramiding=0, initial_capital=100000,
     default_qty_type=strategy.fixed,
     default_qty_value=100,
     commission_type=strategy.commission.cash_per_order,
     commission_value=8, slippage=2)

// 步骤2. 计算策略所需值
highPrevMonth = request.security(syminfo.tickerid, "M", high)[1]
lowPrevMonth  = request.security(syminfo.tickerid, "M", low)[1]

high2MonthsBack = request.security(syminfo.tickerid, "M", high[1])[1]
low2MonthsBack  = request.security(syminfo.tickerid, "M", low[1])[1]

outsideMonth = highPrevMonth > high2MonthsBack and
     lowPrevMonth < low2MonthsBack
insideMonth = highPrevMonth < high2MonthsBack and
     lowPrevMonth > low2MonthsBack

// 步骤3. 输出策略数据并可视化
bgcolor(outsideMonth ? color.new(color.green, 90) : na)
bgcolor(insideMonth ? color.new(color.red, 90) : na)

// 步骤4. 定义多头交易条件
enterLong = outsideMonth

exitLong = month != month[1] and
     strategy.position_size > 0

// 步骤5. 定义空头交易条件
enterShort = insideMonth

exitShort = month != month[1] and
     strategy.position_size < 0

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

// 步骤7. 提交平仓订单
if exitLong and not enterShort
    strategy.close("EL", comment="XL")
if exitShort and not enterLong
    strategy.close("ES", comment="XS")

TradingView中编写的波段交易市场跳空策略

交易跳空(gap)是许多日内交易者和对冲基金的看家本领之一。这些专业人士通常会反向交易跳空(fade the gap),即在价格回补至前一日收盘价时了结头寸。但如果我们利用价格的动能,将跳空行情作为一个波段交易来持有,并尽可能地榨取其全部价值,结果会如何呢?本文将探讨如何在TradingView中编写这样一个交易策略。

詹姆斯·阿图彻的市场跳空波段交易策略

在其著作《像对冲基金一样交易》(Trade Like a Hedge Fund)中,詹姆斯·阿图彻(James Altucher)描述了多种策略思想。虽然其中一些看起来略显基础,但阿图彻坚信,简单的策略效果最好。此外,他从不交易单一策略,而是将一个由交易着不相关资产的、不相关的策略所组成的投资组合,并采用小头寸规模的模式,视为理想状态。

他的大多数策略都与市场情绪有关,市场跳空波段交易策略便是其中之一。当一个交易品种的开盘价显著低于或高于其前一日的收盘价时,跳空便发生了。当一个品种回补跳空时,其价格会回落至前一日的收盘水平。这类行情非常适合进行反向交易,即在出现向下跳空时做多,在价格向上跳空时做空。

当跳空发生时,市场参与者可能会感到极度的焦虑。当公司出现坏消息时,投资者会试图在恐慌中抛售,这便在消息被正确传播和分析之前造成了向下的跳空。这种行为通常是非理性的,并且可以被有利可图地利用。

交易跳空的关键,是识别出那些有很大概率会被回补的情境。但是,如果我们能将头寸持有的时间,延长到不仅仅是回补缺口,我们的收益甚至可能更高。通过这种方式,我们能更久地持有顺势的仓位,这也能降低交易成本,并让我们能利用到隔夜发生的额外跳空机会。

接下来,让我们看看阿图彻的市场跳空波段交易策略思想所包含的具体交易规则,然后将其编写为TradingView代码。

市场跳空波段交易策略的交易规则

该策略包含以下交易规则。做多入场规则:当单个品种向下跳空超过5%时,建立多头仓位,并且该品种在前一日也是下跌的,并且作为市场整体的代表(即纳斯达克100指数的QQQ ETF),也向下跳空超过0.5%。做多离场规则:首先,必须持仓至少到次日早晨;然后,一旦价格跌破前一日的收盘价,便平掉仓位。做空入场规则:当单个品种向上跳空超过5%时,建立空头仓位,并且该品种在前一日也是上涨的,并且QQQ也向上跳空超过0.5%。做空离场规则:首先,必须持仓至少到次日早晨;然后,一旦价格涨过前一日的收盘价,便回补空头仓位。

需要注意的一点是,阿图彻本人并未为该策略定义做空交易的规则。他只交易股票,并认为做空股票并非做多的简单反向操作。这是因为空头头寸存在无限的交易风险,而市场长期来看又具有天然的上涨倾向。

虽然他的观点听起来合情合理,但在我们实际回测之前,并不知道做空向上跳空的表现是否真的不如做多向下跳空。因此,我自行决定在策略中也加入了做空交易规则。这使得测试策略的空头表现成为可能。如果结果证明做空向上跳空是个糟糕的主意,我们随时可以从策略中移除做空部分的代码。

为TradingView编写市场跳空波段交易策略

现在,让我们将上述交易规则转化为TradingView代码。一个能让过程更简单的方法是使用模板。它能将一项大任务分解成更小、更易于管理的工作区块,也能避免我们面对空白脚本时无从下手。

对于本文,我们将使用以下策略模板:

//@version=5
// 步骤1. 定义策略设置
// 步骤2. 计算策略所需指标值
// 步骤3. 定义多头交易条件
// 步骤4. 定义空头交易条件
// 步骤5. 在图表上输出数据
// 步骤6. 提交入场订单
// 步骤7. 提交出场订单

如果你想跟随本文一起操作,请在TradingView的Pine编辑器中创建一个新的策略脚本,并将上述模板粘贴进去。

为了让你对我们即将编写的代码有个直观的认识,以下是最终完成的市场跳空波段交易策略在图表上的样子:

现在,让我们开始动手,将上述交易规则转化为一个规范的TradingView策略脚本。

步骤1:定义策略设置和输入选项

在第一步,我们需要配置策略的整体属性,并创建用户可调的参数设置。首先,我们指定策略的基础设置:

// 步骤1. 定义策略设置
strategy(title="市场跳空摇摆交易", overlay=false,
     pyramiding=0, initial_capital=100000,
     default_qty_type=strategy.fixed,
     default_qty_value=100,
     commission_type=strategy.commission.cash_per_order,
     commission_value=8, slippage=2)

我们通过 strategy() 函数来配置脚本属性。title 参数为策略命名。overlay=false 使策略显示在独立的副图面板中。根据交易规则,我们通过 pyramiding=0 禁止加仓。initial_capital 将策略的初始资金设为100,000。订单数量(default_qty_type)设为固定手数(strategy.fixed),每笔交易固定为100股(default_qty_value=100)。在交易成本方面,我们设定每笔单边订单(commission_type)收取8个单位的固定佣金(commission_value=8),并假设市价单和止损单会产生2个最小跳动点的滑点(slippage=2)。这里的成本设置相对悲观,以避免高估策略表现。

我们还为策略创建了几个输入选项:

gapDownSize = input.float(-5, title="个股向下跳空幅度 %", step=0.25)
mktGapDown  = input.float(-0.5, title="市场向下跳空幅度 %", step=0.25)

gapUpSize = input.float(5, title="个股向上跳空幅度 %", step=0.25)
mktGapUp  = input.float(0.5, title="市场向上跳空幅度 %", step=0.25)

我们使用 input.float() 函数创建了四项浮点数输入,分别用于设定触发多头和空头交易的个股及市场(以QQQ为代表)的跳空幅度阈值。

步骤2:计算交易策略所需的值

接下来,我们计算策略生成订单所需的数据:

// 步骤2. 计算策略所需值
gapSize    = ((open - close[1]) / close[1]) * 100
marketGap  = request.security("BATS:QQQ", timeframe.period, gapSize)
prvDayDown = close[1] < open[1]
prvDayUp   = close[1] > open[1]

首先,我们计算当前交易品种的跳空幅度 gapSize。然后,我们使用 request.security() 函数来获取市场的跳空幅度 marketGap。这里有一个巧妙的用法:我们将 gapSize 这个表达式本身作为 request.security() 的第三个参数,这样该函数就会在QQQ的上下文中计算这个表达式,从而返回QQQ的跳空幅度。最后,我们定义了两个布尔变量,prvDayDownprvDayUp,用于判断前一个交易日是收阴还是收阳。

步骤3:编写多头交易规则

计算完所需值后,我们来定义交易条件。首先是多头交易规则:

// 步骤3. 定义多头交易条件
enterLong = gapSize < gapDownSize and
     prvDayDown and 
     marketGap < mktGapDown

exitLong = close < close[1] and
     strategy.position_size > 0 and
     ta.barssince(enterLong) > 1

enterLong 变量在以下三个条件同时满足时为 true:个股的向下跳空幅度低于设定的阈值、前一日收阴、并且市场的向下跳空幅度也低于设定的阈值。

exitLong 变量则定义了多头平仓的条件:当前收盘价低于前一日收盘价、策略当前持有多仓、并且距离开仓信号已超过一根K线(确保至少持有一天)。

步骤4:编写空头交易条件

接下来,我们编写策略的空头交易逻辑:

// 步骤4. 定义空头交易条件
enterShort = gapSize > gapUpSize and
     prvDayUp and 
     marketGap > mktGapUp

exitShort = close > close[1] and
     strategy.position_size < 0 and
     ta.barssince(enterShort) > 1

空头交易的逻辑与多头完全对称。enterShort 在个股和市场同时向上大幅跳空,且前一日收阳时触发。exitShort 则在持空仓、持仓时间超过一天且当前收盘价高于前一日收盘价时触发。

步骤5:输出数据并可视化信号

在这一步,我们输出三类信息:跳空幅度、跳空触发阈值以及交易信号。首先,我们显示跳空幅度:

// 步骤5. 输出策略数据
// 显示跳空幅度
plot(marketGap, color=color.orange, style=plot.style_columns,
     title="市场跳空幅度 %")
plot(gapSize, color=color.teal, style=plot.style_columns,
     title="个股跳空幅度 %")

我们用 plot() 函数将市场和个股的跳空幅度以橙色和青色的柱状图形式绘制出来。

然后,我们在图表上显示跳空的触发阈值:

// 高亮显示跳空触发水平
plot(gapDownSize, color=color.new(color.green, 70),
     title="向下跳空阈值 [多头入场]")
plot(gapUpSize, color=color.new(color.red, 70),
     title="向上跳空阈值 [空头入场]")

我们用两条带透明度的水平线,分别表示多头和空头入场的跳空幅度阈值。

接下来,我们高亮显示多空信号:

// 高亮显示多空信号
bgColour = enterLong ? color.new(color.green, 85) :
     enterShort ? color.new(color.red, 85) :
     na
bgcolor(bgColour)

我们通过 bgcolor() 函数,在 enterLong 信号出现时将背景染成绿色,在 enterShort 信号出现时染成红色。

步骤6:提交开仓订单

现在,让我们提交实际的交易订单。首先是开仓代码:

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

enterLong 为真时,我们调用 strategy.entry() 开立一个ID为”EL”的多头仓位。当 enterShort 为真时,则开立ID为”ES”的空头仓位。

步骤7:提交平仓订单

最后要实现的是平仓的逻辑。代码如下:

// 步骤7. 提交平仓订单
if exitLong
    strategy.close("EL", comment="XL")
if exitShort
    strategy.close("ES", comment="XS")

exitLong 为真时,我们调用 strategy.close() 来平掉ID为”EL”的多头仓位。当 exitShort 为真时,则平掉ID为”ES”的空头仓位。

市场跳空波段交易策略的表现

让我们先从这个策略表现积极的一面开始。它识别出的某些市场跳空,确实预示着可以盈利的、持续数日的动能行情。

例如,在这张亚马逊的日线图上,出现了一次显著的向下跳空。但在那之后,价格连续三日上涨,策略成功地捕捉了这次反转行情:

然而不幸的是,也有足够多的跳空行情,其走势与策略的预期完全相反。也就是说,向上跳空之后紧跟着的不是下跌,而是更强的上涨趋势;而向下跳空之后,价格非但没有反弹,反而持续疲软。

例如,下图中的策略在一个大的向上跳空后做空,但价格并未如期下跌,而是简单地延续了此前的上涨趋势:

下方表格展示了该策略在微软和亚马逊股票上的回测结果。其结果好坏参半,我们也见过表现更差的TradingView策略。市场跳空波段交易策略的一个问题是交易数量过少。例如,在微软股票上,该策略平均每年交易不到一次。

仅凭15笔交易,我们无法判断该策略是否有效。如此少的交易也无助于我们理解该策略在各种市场条件下的潜在表现。

表现指标 微软 (MSFT) 亚马逊 (AMZN)
首次交易 2000-10-20 1999-09-07
最后交易 2016-04-27 2018-05-02
时间框架 日线 日线
净利润 -$802 $1,181
总利润 $285 $5,522
总亏损 -$1,087 -$4,341
最大回撤 $802 $1,963
盈利因子 0.262 1.272
总交易数 15 31
胜率 33.33% 41.94%
平均每笔交易 -$53.47 $38.10
平均盈利交易 $57 $424.77
平均亏损交易 -$108.70 -$241.17
平均盈亏比 0.524 1.761
支付佣金 $240 $488
每笔订单滑点 2个跳动点 2个跳动点

在本文前面,我们讨论了阿图彻没有为该策略定义做空规则。他的偏好或许不无道理:在亚马逊股票上,该策略的多头交易是盈利的,但空头交易却是亏损的。策略总共进行了17笔多头交易,胜率为47%,平均每笔多头交易盈利$140,拥有3.04的健康盈亏比和2.698的盈利因子。

然而,在交易微软股票时,该策略的多头和空头两方面都亏了钱。此外,仅仅17笔亚马逊的交易,不足以让我们充满信心地断定,市场跳空波段交易策略的空头部分无效。

改进思路与新策略方向

从上表可以看出,市场跳空波段交易策略的表现并不理想。以下是一些你可能会觉得有趣的、值得进一步探索的思路。

阿图彻并未具体说明他选择这些参数设置的理由。考虑到他开发此策略已是多年前,即使这些参数是基于稳健的优化过程得出的,我们很可能也需要探索其他参数是否效果更好。

策略参数的一个潜在问题是它们基于固定的百分比。一个5%的跳空对于高波动的科技股来说可能不算什么,但对于其他股票,可能连3%的跳空都很少出现。此外,股票自身的价格也很重要:一支低价股可以轻易地产生10%以上的跳空。如果我们将跳空的大小与股票自身的波动性联系起来,策略的表现可能会得到改善。实现这一目标的方法之一是使用平均真实波幅(ATR)。

策略识别出的一些跳空确实是适合反向交易的。但另一些跳空则在跳空的方向上延续了趋势,因此从未回补缺口。如果我们能设计一个过滤器,来区分可以反向交易的普通跳空和应该顺势跟踪的持续性跳空,策略的表现将会变得更好。

该策略目前不使用止损。结果是,有几笔交易中,价格在与策略持仓相反的方向上移动了相当大的距离。虽然止损也可能降低策略的表现,但通过更好的风险管理,其经风险调整后的表现很可能会提升。

该策略的一个不足之处是缺乏风险管理措施。例如,我们可以为连续亏损的天数设置一个限制,或者限制策略的最大资金回撤。

完整代码:TradingView的市场跳空波段交易策略

以下是市场跳空波段交易策略的完整代码。关于代码的更多信息和细节,请参阅上文的讨论。

//@version=5
// 步骤1. 定义策略设置
strategy(title="波段交易市场跳空", overlay=false,
     pyramiding=0, initial_capital=100000,
     default_qty_type=strategy.fixed,
     default_qty_value=100,
     commission_type=strategy.commission.cash_per_order,
     commission_value=8, slippage=2)

// --- 输入项 ---
gapDownSize = input.float(-5, title="向下跳空大小%", step=0.25)
mktGapDown  = input.float(-0.5, title="市场向下跳空%", step=0.25)

gapUpSize = input.float(5, title="向上跳空大小%", step=0.25)
mktGapUp  = input.float(0.5, title="市场向上跳空%", step=0.25)

// 步骤2. 计算策略所需指标值
// 计算当前品种的跳空百分比
gapSize    = ((open - close[1]) / close[1]) * 100
// 使用 request.security 获取QQQ的市场跳空数据作为过滤器
marketGap  = request.security("BATS:QQQ", timeframe.period, gapSize)
// 判断前一日是上涨还是下跌
prvDayDown = close[1] < open[1]
prvDayUp   = close[1] > open[1]

// 步骤3. 定义多头交易条件
enterLong = gapSize < gapDownSize and
     prvDayDown and 
     marketGap < mktGapDown

// 离场条件:价格跌破昨日收盘价,且至少持仓到次日
exitLong = close < close[1] and
     strategy.position_size > 0 and
     ta.barssince(enterLong) > 1

// 步骤4. 定义空头交易条件
enterShort = gapSize > gapUpSize and
     prvDayUp and 
     marketGap > mktGapUp

// 离场条件:价格涨过昨日收盘价,且至少持仓到次日
exitShort = close > close[1] and
     strategy.position_size < 0 and
     ta.barssince(enterShort) > 1

// 步骤5. 在图表上输出数据
// 以柱状图形式显示市场和个股的跳空大小
plot(marketGap, color=color.orange, style=plot.style_columns,
     title="市场跳空%")
plot(gapSize, color=color.teal, style=plot.style_columns,
     title="个股跳空大小%")

// 以水平线形式高亮显示跳空触发阈值
plot(gapDownSize, color=color.new(color.green, 70),
     title="向下跳空阈值 [多头入场]")
plot(gapUpSize, color=color.new(color.red, 70),
     title="向上跳空阈值 [空头入场]")

// 以背景色高亮显示多头和空头入场信号
bgColour = enterLong ? color.new(color.green, 85) :
     enterShort ? color.new(color.red, 85) :
     na
bgcolor(bgColour)

// 步骤6. 提交入场订单
if enterLong
    strategy.entry("EL", strategy.long)

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

// 步骤7. 提交出场订单
if exitLong
    strategy.close("EL", comment="XL")

if exitShort
    strategy.close("ES", comment="XS")
赞(0)
未经允许不得转载:图道交易 » Pine Script(261):外包月策略与市场跳空波段交易
分享到

评论 抢沙发

登录

找回密码

注册