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

Pine Script(262):单边配对交易策略实战

#Pine Script入门教学

为TradingView Pine编写的单边配对交易策略

通过配对交易(pairs trading),我们试图通过买入一种资产、同时做空另一种资产来保持市场中性。但这种交易方法存在显著的风险。一种同样利用两种工具之间价差、但风险特征不同的替代方法,是单边配对交易(unilateral pairs trading)。本文将探讨我们如何在TradingView中编写这样一个交易策略。

詹姆斯·阿图彻的单边配对交易策略思想

在其著作《像对冲基金一样交易》(Trade Like a Hedge Fund)中,詹姆斯·阿图彻(James Altucher)讨论了多种策略思想。其中一些策略看起来很简单,但他坚信,基础的策略效果最好并且更具稳健性。他的核心理念是,一个由交易着不相关资产的、不相关的策略所组成的投资组合,并采用小头寸规模的模式,是获得良好且持续回报的最佳方式。

阿图彻分享的策略之一,便是单边配对交易策略。该策略是传统配对交易的一种替代方案。但配对交易并不像人们想象的那么安全。事实上,这种交易方法可能带来相当大的市场风险敞口。

传统的配对交易通常意味着交易者对市场方向保持中性,但它实际上是一个对两种工具之间价差的有偏见的赌注。也就是说,当导致价差偏离常态的基本面原因发生变化时,你最终会发现自己同时暴露在不是一个、而是两个品种的风险之下。

配对交易的其他问题还包括:由于价差的波动通常很小,需要大量的杠杆才能使交易有利可图;当价差变得更小时,则需要更大的杠杆。另一个问题是,大量的对冲基金都在交易价差,这不仅压缩了交易的盈利潜力,还导致当意外发生时,所有人都争相夺路而逃。

因此,阿图彻将单边配对交易视为一种更好的替代方案。这种方法只交易配对中的一边(unilateral意为单边的或一部分的)。为此,我们选择两种流动性极好且高度相关的工具,其中一个的波动性要大于另一个。

当这两个品种之间的价差变得极端时,我们便认为波动性更强的那个品种是犯错的一方,并只交易那个品种。这样,我们建立仓位时无需考虑市场的整体方向,我们只押注于该品种的价差会回归正常。

计算单边配对交易策略的价差

在讨论交易规则之前,让我们先看看阿图彻是如何计算两种工具之间的价差的。在他的书中,他使用S&P 500和纳斯达克100之间的价差来交易QQQ——跟踪纳斯达克100指数的ETF。

计算该价差分六步。第一步,计算比率:将纳斯达克100(QQQ)的价格除以S&P 500指数(SPY)的价格,计算出QQQ:SPY的比率。例如,SPY收于50.34,QQQ收于27.40,则比率为27.40 / 50.34 = 0.5443。第二步,计算比率的均值:计算该比率的20日移动平均线。第三步,计算离差(Delta):计算当前比率与其20日均值的差值,得出离差。第四步,计算离差的均值:计算这个离差值的20日移动平均线。第五步,计算离差的差值:用每个离差值(第三步的结果)减去其20日均值(第四步的结果)。第六步,标准化:用第五步得到的离差的差值,除以离差值(第三步的结果)的20日标准差,进行标准化处理。

顺便提一下,阿图彻是在日线图上交易此策略的。如果你使用其他时间框架,请将上述步骤中提到的日替换为你所用的周期。

单边配对交易的交易规则

计算出标准化的QQQ:SPY比率后,我们便可以应用策略的交易规则。做多入场规则:当QQQ:SPY的标准化比率小于-1.5时买入QQQ,并且QQQ的价格比前一日收盘价低2%。做多离场规则:当比率大于-0.5时,平掉多头仓位。做空入场规则:当QQQ:SPY的标准化比率大于1.5时做空QQQ,并且QQQ的价格比前一日收盘价高2%。做空离场规则:当比率小于0.5时,回补空头仓位。

这些规则旨在捕捉QQQ与SPY之间价差远超常规的时刻。当这种情况发生,并且波动性更强的QQQ本身也出现了大幅波动时,我们便将QQQ视为罪魁祸首。

因此,当QQQ:SPY比率错位且QQQ大幅下跌时,我们推断QQQ的下跌可能已经过度,价格很可能出现反弹。同理,当比率失常且QQQ在没有SPY跟随的情况下大幅上涨时,我们推断QQQ可能已经超前,价格很可能出现回调。

为TradingView编写单边配对交易策略

现在,让我们将上述交易规则转化为一个TradingView交易策略。使用模板能让编写过程更轻松,它提供了一个结构,并将编程任务分解成更小、更易于管理的工作区块。

这是我们将用于单边配对交易策略的模板:

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

如果你想跟随下方的代码讨论,请在TradingView的Pine编辑器中创建一个新的策略脚本,然后粘贴上述模板。(如果你只想看完成的代码,请跳转至本文末尾的完整策略代码。)

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

现在,让我们开始动手,将这个单边配对交易策略的构想,转化为一个规范的TradingView代码。

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

编写任何交易策略的第一步,都是配置脚本的整体属性并创建用户输入选项。我们使用 strategy() 函数来完成配置:

// 步骤1. 定义策略设置
strategy(title="单边配对交易", overlay=false,
     pyramiding=0, initial_capital=25000,
     default_qty_type=strategy.fixed,
     default_qty_value=100,
     commission_type=strategy.commission.cash_per_order,
     commission_value=8, slippage=2)

我们通过 title 参数为策略命名。overlay=false 使策略显示在独立的副图面板中。根据交易规则,我们通过 pyramiding=0 禁止加仓。策略的初始资金设为25,000(initial_capital=25000)。

我们设定订单数量为固定手数(default_qty_type=strategy.fixed),每笔交易固定为100股(default_qty_value=100)。在交易成本方面,我们设定每笔单边订单(commission_type)收取8个单位的固定佣金(commission_value=8),并假设市价单和止损单会产生2个最小跳动点的滑点(slippage=2)。这里的成本设置相对悲观,但高估交易成本总比低估要安全。

接下来,我们创建策略的输入选项。首先是用于设置QQQ:SPY比率入场和出场阈值的参数:

// 开仓触发阈值
longThres  = input.float(-1.5, title="多头开仓阈值")
shortThres = input.float(1.5, title="空头开仓阈值")

// 平仓触发阈值
exitLongThres  = input.float(-0.5, title="多头平仓阈值")
exitShortThres = input.float(0.5, title="空头平仓阈值")

我们使用 input.float() 函数创建了四个浮点数输入项。前两个分别设定了多头和空头开仓所需的QQQ:SPY比率水平,默认值分别为-1.5和1.5。后两个则设定了平仓的阈值,默认值为-0.5和0.5。

然后,我们为QQQ:SPY比率的计算周期创建一个设置:

// 比率计算周期
ratioLen = input.int(20, title="比率周期")

这是一个整数输入项,名为比率周期,默认值为20。

最后两个策略参数,用于指定在交易QQQ:SPY比率之前,QQQ本身需要出现的价格变动:

// 多头和空头所需的个股价格变动
chgLong  = input.float(-2, title="价格变动百分比 (多头)")
chgShort = input.float(2, title="价格变动百分比 (空头)")

价格变动百分比(多头)设定了开多仓前QQQ需要下跌的幅度,默认为-2%。另一个则设定了开空仓前QQQ需要上涨的幅度,默认为2%。

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

要计算策略所需的数据,我们分三步走:首先加载SPY的价格数据,然后计算QQQ与SPY的比率,最后计算QQQ本身的价格回报率。

首先,我们抓取S&P 500指数ETF(SPY)的价格:

// 获取SPY追踪基金的价格
spyData = request.security("SPY", timeframe.period, close)

我们使用 request.security() 函数来获取SPY在当前图表时间周期下的收盘价数据,并存入 spyData 变量。

然后,我们开始计算QQQ与SPY的比率:

// 计算价格比率及其均值
ratioSeries = close / spyData
ratioSMA    = ta.sma(ratioSeries, ratioLen)

我们先将当前品种(QQQ)的收盘价除以SPY的收盘价,得到原始比率序列 ratioSeries。然后,计算该序列的20周期SMA均线 ratioSMA

接着,我们观察这个比率与其均值的偏离程度:

// 计算价格比率的差值
deltaSeries = ratioSeries - ratioSMA
deltaSMA    = ta.sma(deltaSeries, ratioLen)
deltaDiff   = deltaSeries - deltaSMA

我们首先计算原始比率与其均值的差值 deltaSeries,然后计算这个差值序列本身的均线 deltaSMA,最后再计算差值与差值均线的离差 deltaDiff。这是一个二次平滑的过程。

最后一步,我们需要对比率的离差进行归一化处理:

// 使用标准差对差值进行归一化
deltaNormRatio = deltaDiff / ta.stdev(deltaSeries, ratioLen)

我们将二次平滑后的离差 deltaDiff 除以其20周期的标准差。这样,我们就得到了一个标准化的、围绕0上下波动的指标 deltaNormRatio,它反映了QQQ:SPY比率的相对强弱。

接下来,我们计算当前图表品种(QQQ)自身的价格回报率:

// 计算价格回报率
closeReturn = ((close - close[1]) / close[1]) * 100

closeReturn 变量计算的是当前K线的收盘价相对于上一根K线收盘价的百分比变化。

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

第三步,我们将策略的数据和参数绘制在图表上,以便追踪策略的值并验证其行为。首先,我们绘制归一化后的QQQ:SPY比率:

// 步骤3. 输出策略数据
// 绘制归一化的价格差异比率
plot(deltaNormRatio, color=color.navy, title="价格比率差异归一化指标")

我们用 plot() 函数将 deltaNormRatio 的值以海军蓝的线条绘制在副图上。

接下来,我们显示触发交易的比率水平:

// 显示多头和空头的触发水平线
hline(longThres, color=color.green, linestyle=hline.style_solid)
hline(shortThres, color=color.red, linestyle=hline.style_solid)

// 显示多头和空头的平仓水平线
hline(exitLongThres, color=color.new(color.green, 25), 
     linestyle=hline.style_dotted)
hline(exitShortThres, color=color.new(color.red, 25), 
     linestyle=hline.style_dotted)

我们使用 hline() 函数在图表上绘制了四条水平线,分别代表多空开仓(实线)和平仓(虚线)的阈值。

策略交易规则的另一个重要部分是图表品种的价格变动百分比。所以,让我们用背景色来高亮显示它们:

// 高亮显示超过阈值的价格变动百分比
bgColour = if closeReturn < chgLong
    color.new(color.green, 80)
else if closeReturn > chgShort
    color.new(color.red, 80)
bgcolor(bgColour)

我们定义一个 bgColour 变量。如果QQQ的日回报率低于设定的做多阈值(例如-2%),背景就染成绿色;如果高于做空阈值(例如+2%),则染成红色。最后调用 bgcolor() 函数应用颜色。

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

下一步,我们将策略的多头交易规则转化为TradingView代码。首先,我们定义多头入场条件:

// 步骤4. 定义多头交易条件
enterLong = deltaNormRatio < longThres and
     closeReturn <= chgLong

单边配对交易策略在做多前有两个要求:一是QQQ:SPY的归一化比率低于-1.5,二是QQQ自身收盘下跌超过2%。

我们通过 deltaNormRatio < longThres 来判断第一个条件是否满足。然后,通过 closeReturn <= chgLong 来判断第二个条件。我们使用 and 逻辑运算符将这两个条件连接起来,只有当两者同时为 true 时,enterLong 变量才为 true

接下来,我们编写平掉多头仓位的规则:

exitLong = deltaNormRatio > exitLongThres and
     strategy.position_size > 0

exitLong 变量在两个条件同时满足时为 true:一是QQQ:SPY比率(deltaNormRatio)回升至平仓阈值 exitLongThres(-0.5)之上;二是策略当前确实持有多头仓位(strategy.position_size > 0)。

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

接下来,我们需要将策略的空头条件转化为代码。首先是空头入场条件:

// 步骤5. 定义空头交易条件
enterShort = deltaNormRatio > shortThres and
     closeReturn >= chgShort

enterShort 变量的逻辑与多头对称。只有当QQQ:SPY比率高于开仓阈值 shortThres(1.5),并且QQQ自身收盘上涨超过 chgShort(2%)时,enterShort 才为 true

然后,我们定义平掉空仓的规则:

exitShort = deltaNormRatio < exitShortThres and
     strategy.position_size < 0

当QQQ:SPY比率回落至平仓阈值 exitShortThres(0.5)以下,并且策略当前正持有空仓时,exitShort 变量为 true

步骤6:提交开仓订单

在下一步,我们编写单边配对交易策略提交订单的代码:

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

enterLongtrue 时,我们调用 strategy.entry() 函数开立一个ID为”EL”的多头仓位。我们在这里不指定订单数量,策略将使用我们之前通过 strategy() 函数设定的默认值,即每笔交易100股。空头开仓的逻辑与此类似。

请注意,strategy.entry() 函数具备自动反转仓位的功能。因此,当策略持有多仓且空头入场信号出现时,TradingView会自动先平掉多仓,然后再开立空仓。

步骤7:提交平仓订单

最后一部分Pine Script代码用于平掉策略的仓位:

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

第一个 if 语句的条件包含两个表达式。第一个是 exitLong,判断是否达到了多头平仓条件。第二个是 not enterShort,我们加入这个额外条件,是为了处理一个特殊的边缘情况:防止在平掉多仓的同时又开立了新的空仓。如果平多信号和开空信号恰好在同一根K线上出现,而我们不加此限制,策略可能会同时发出两个卖出指令,导致最终的空头仓位是预期规模的两倍。这个检查确保了平仓和开仓是互斥的。

当两个条件都满足时,我们调用 strategy.close() 来平掉ID为”EL”的仓位。

第二个 if 语句的逻辑与此对称,用于安全地平掉空头仓位。

单边配对交易策略的表现

让我们来看看这个单边配对交易策略的表现。在市场存在一定波动时,该策略出人意料地擅长在短期波动的底部附近买入,并在顶部附近卖出。

下图就是一个例子。在这里,当市场整体处于横盘时,策略完成了两笔盈利的交易。正如副图中高亮的背景色所示,期间有数天QQQ的收盘涨跌幅超过了2%,但这种波动并未对策略的判断造成困扰:

该策略难以应对的一种情况是,市场在经历一段平静的横盘后开始走出趋势。当价格变化很小时,QQQ:SPY比率会在一个很窄的区间内波动。一旦价格开始形成趋势,这个比率可能会突然跳跃并触发信号。但这些信号并非有效的交易机会,而仅仅是由于策略基于过去20天平静的价格行为进行计算所产生的噪音。

在下方的图表中,价格首先以很小的波幅横向移动。随后当QQQ开始下跌时,本策略错误地在下跌过程中数次做多,导致了连续两笔亏损交易:

策略的资金曲线图如下所示。这张图看起来还不算太差。资金回撤相对温和,并且在没有经过任何优化和改进的情况下,策略的表现还算稳定。当策略亏损时,通常也是以小额亏损的形式出现——只有在回测的初期和末期才有比较显著的资金回撤。

下表展示了该策略在纳斯达克100指数ETF(QQQ)和道琼斯工业平均指数ETF(DIA)上的回测结果。单边配对交易策略在这两个品种上均取得了盈利。其胜率和盈利因子都相当不错,尤其是考虑到这些回测已经包含了佣金和滑点成本。

但其交易次数偏低。这使得我们很难对策略的结果抱有十足的信心。进行更多的测试,以更好地了解策略在各种市场条件下的表现,似乎是一个明智之举。

表现指标 纳斯达克100指数ETF (QQQ) 道琼斯工业平均指数ETF (DIA)
首次交易 1999-05-13 1998-06-16
最后交易 2018-11-13 2018-03-27
时间框架 日线 日线
净利润 $10,131 $2,230
总利润 $21,467 $7,078
总亏损 -$11,336 -$4,848
最大回撤 $1,834 $2,078
盈利因子 1.894 1.46
总交易数 143 43
胜率 59.44% 51.16%
平均每笔交易 $70.85 $51.86
平均盈利交易 $252.55 $321.73
平均亏损交易 -$195.45 -$230.86
平均盈亏比 1.292 1.394
支付佣金 $2,280 $688
每笔订单滑点 2个跳动点 2个跳动点

改进思路与新策略方向

虽然该策略的表现优于我们见过的其他TradingView示例策略,但仍有改进的空间。通过更多的测试,我们也能更好地理解该策略的行为。以下是一些你可能会觉得有价值去进一步探索的思路。

单边配对交易策略要求两种资产高度相关。然而,相关性究竟如何影响策略的表现,我们尚不清楚。或许,只有当相关性处于某个特定范围内时才进行交易,策略的结果会更好;或者当相关性低于某个值时便暂停交易。

我们不清楚阿图彻选择这些特定参数的理由。其中一些参数看起来像是为了方便而选择的:20日的观察期、2%的价格变动水平,以及1.5的比率阈值。或许,采用更符合当前市场状况的参数值,能让策略表现更佳。

通过单边配对交易策略,我们交易的是价差对中历史上波动性更强的一方。其基本思想是,波动性更强的一方通常是导致价差偏离常态的原因。策略或许能从波动率过滤器中受益,例如使用平均真实波幅(ATR),来确认犯错的一方确实是波动更剧烈的一方,或者基于两种工具之间的特定波动率差异来过滤交易。

策略的入场信号基于一个固定的2%价格变动。但这种变动发生的可能性,很大程度上取决于市场的波动性。在平静的市场中,2%的变动幅度很大;但在剧烈的熊市中,这可能只是寻常的一天。或许,当我们将市场的当前波动性纳入考量时,策略的表现会更好。

该策略不使用止损订单。虽然QQQ:SPY的比率很可能总会回归其历史均值(从而产生离场信号),但如果我们知道市场中有一个明确的止损来防范重大损失,策略可能会更容易被交易者执行。

当市场从横盘切换到趋势时,单边配对交易策略似乎难以适应。在这些情况下,其20日的QQQ:SPY比率仍然基于之前的横盘价格行为。或许,使用一个比慢速调整的SMA更具适应性的移动平均线,或者根据价格波动性使用动态的回看周期,能让策略变得更好。

该策略在比率刚刚越过0.5或-0.5时便了结盈利的交易。但或许通过追踪止损来尽可能地持有顺势的短期趋势,策略能获利更多,从而在均值回归的过程中攫取更大的利润。

改善策略表现的另一种方法是限制其所承担的风险。例如,我们可以限制策略的最大持仓规模,或者为其最大资金回撤设定一个上限。

完整代码:TradingView的单边配对交易策略

以下是单边配对交易策略的完整代码。关于代码的更多信息和解释,请参阅上文的讨论。

//@version=5
// 步骤1. 定义策略设置
strategy(title="单边配对交易", overlay=false,
     pyramiding=0, initial_capital=25000,
     default_qty_type=strategy.fixed,
     default_qty_value=100,
     commission_type=strategy.commission.cash_per_order,
     commission_value=8, slippage=2)

// --- 输入项 ---
// 入场触发阈值
longThres  = input.float(-1.5, title="做多入场阈值")
shortThres = input.float(1.5, title="做空入场阈值")

// 离场触发阈值
exitLongThres  = input.float(-0.5, title="平多离场阈值")
exitShortThres = input.float(0.5, title="平空离场阈值")

// 价差比率的平滑周期
ratioLen = input.int(20, title="比率周期")

// 多空入场所需的价格变动
chgLong  = input.float(-2, title="价格变动% (多头)")
chgShort = input.float(2, title="价格变动% (空头)")

// 步骤2. 计算策略所需指标值
// 获取S&P 500 ETF (SPY) 的价格数据
spyData = request.security("SPY", timeframe.period, close)

// --- 标准化价差计算 ---
// 1. 计算价格比率及其均值
ratioSeries = close / spyData
ratioSMA    = ta.sma(ratioSeries, ratioLen)

// 2. 计算价格比率的离差 (Delta)
deltaSeries = ratioSeries - ratioSMA
deltaSMA    = ta.sma(deltaSeries, ratioLen)
deltaDiff   = deltaSeries - deltaSMA

// 3. 用标准差来标准化离差
deltaNormRatio = deltaDiff / ta.stdev(deltaSeries, ratioLen)

// --- 其他计算 ---
// 计算价格回报率
closeReturn = ((close - close[1]) / close[1]) * 100

// 步骤3. 在图表上输出数据
// 绘制标准化的价差比率
plot(deltaNormRatio, color=color.navy, title="标准化价差比率")

// 显示多头和空头的入场阈值线
hline(longThres, color=color.green, linestyle=hline.style_solid)
hline(shortThres, color=color.red, linestyle=hline.style_solid)

// 显示多头和空头的离场阈值线
hline(exitLongThres, color=color.new(color.green, 25), 
     linestyle=hline.style_dotted)
hline(exitShortThres, color=color.new(color.red, 25), 
     linestyle=hline.style_dotted)

// 高亮显示超过阈值的价格变动
bgColour = if closeReturn < chgLong
    color.new(color.green, 80)
else if closeReturn > chgShort
    color.new(color.red, 80)
bgcolor(bgColour)

// 步骤4. 定义多头交易条件
enterLong = deltaNormRatio < longThres and
     closeReturn <= chgLong

exitLong = deltaNormRatio > exitLongThres and
     strategy.position_size > 0

// 步骤5. 定义空头交易条件
enterShort = deltaNormRatio > shortThres and
     closeReturn >= chgShort

exitShort = deltaNormRatio < exitShortThres 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")
赞(0)
未经允许不得转载:图道交易 » Pine Script(262):单边配对交易策略实战
分享到

评论 抢沙发

登录

找回密码

注册