第十四节 三角对冲指标编写

这节课我们要挑战一下稍微有点难度的三角对冲指标编写,相信大家认真学习完这次的指标编写后,其他的指标也基本上可以自己写了。

三角对冲交易策略

三角对冲可谓是相当出名的策略了,这个策略其实是基于三个货币对的关系来做交易的。什么意思呢,比如说现在EURUSD的价格是2,GBPUSD的价格是4,那么理论上EURGBP的价格应该是2/4=0.5,但是实际的价格是0.6,那么我们就发现有套利空间了,因为实际的价格是迟早会回归到理论价格的,我们只要做多理论价格做空实际价格就可以获利。于是我们做多1手EURUSD,做空0.5手GBPUSD,做空1手EURGBP,也就是说做多理论价格做空实际价格,等实际价格和理论价格相等的时候我们就平仓获利。

为什么要做多1手EURUSD并做空0.5手GBPUSD呢?这里详细说一下。对于一般的平台,EURUSD以及GBPUSD每手的合约数量是100000。那么这里的合约数量是什么意思呢?比如做多1手,那么我就是要用美元换100000的欧元,做空1手的话就是用100000欧元换成美元。那做多EURGBP是什么意思呢?意思就是我用英镑来买入100000欧元了。

按照以上的理论,我们做空0.5手的英镑,实际上就是把50000英镑换成了美元,由于GBPUSD的价格是4,那么我们换出了200000美元。然后我们再做多1手EURUSD,就是用美元换成了100000欧元,那么需要多少美元呢?由于EURUSD的价格是2,所以我们需要200000美元换成欧元。于是我们发现,此时做空0.5手的英镑换出的美元和做多1手EURUSD需要的美元是一样的,也就是说我们通过这一操作实现了将50000英镑换成了100000欧元效果,也就是说和在0.5的价格(也就是理论价格)做多1手EURGBP是等效的效果。

于是以此类推,我们就可以知道,假如EURUSD的价格是a,GBPUSD的价格是b,那么我做多1手EURUSD,再做空a/b手GBPUSD,就相当于在价格为a/b处做多1手EURGBP。

那么要实现在理论价格做多EURGBP和在实际价格做空EURGBP就很简单了,我只要按比例做这三个品种就行了,EURUSD:GBPUSD:EURGBP的具体手数比例为1:a/b:1。

我们在网上找三角对冲套利能够找到很多的内容,大部分人说三角对冲套利是无风险的套利方式,但是他们策略里这三个品种的手数比例是1:1:1,这样的做法实际上是做了一个三角对冲再加一个品种的单边仓单,如果这个品种突然暴涨暴跌还是有风险的,所以一定要注意不要被骗。

要实现这个策略能够盈利是有一个前提的,那就是市场会偶尔失效,也就是实际价格并不等于理论价格。实际上,我们可以用自己的MT4平台计算一下就可以发现确实EURGBP的理论价格和实际价格是不相等的,但是由于三个品种的点差相加后是大于这一差值的,所以我们并没有套利的空间。

那么有没有一个时间段,这个价格差可以比我们的成本大呢?接下来的三角对冲指标要完成的工作就是这个。

指标编写

有人会觉得这个很容易啊,只要引用K线对应的EURUSD、GBPUSD、EURGBP的价格,然后算一个理论价格和实际价格的差值,把这个差值用一个数组存下来,接着把这个数组和指标绑定一下就好了。那么我们试一下,很简单,把上节课的代码拿过来稍微改一下,如下:

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots 1
double hudge[];
string sym1='EURUSD';
string sym2='GBPUSD';
string sym3=“EURGBP';
int OnInit()
  {
      string name=“EURUSD/GBPUSD-EURGBP”;
      SetIndexBuffer(0,hudge,INDICATOR_DATA);
      SetIndexStyle(0,DRAW_LINE,STYLE_SOLID,1,clrRed);
      IndicatorShortName(name);
      return(INIT_SUCCEEDED);
  }
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   int i;
   ArrayResize(hudge,rates_total);
   if(prev_calculated==0)
   {
      for(i=0;i<=rates_total;i++)
      {  
         hudge[i]=(iClose(sym1,PERIOD_CURRENT,i)/iClose(sym2,PERIOD_CURRENT,i)-
	iClose(sym3,PERIOD_CURRENT,i))*10000;
      }
   }
   else
   {
      hudge[0]=iClose(sym1,PERIOD_CURRENT,0)/iClose(sym2,PERIOD_CURRENT,0)-			      iClose(sym3,PERIOD_CURRENT,0)*10000;
   }
   return(rates_total);
  }

以上代码中我们定义了三个货币对,然后把hudge这个数组和指标绑定,接下来我们把理论上的EURGBP价格和实际的EURGBP价格作差,赋值到hudge数组中,并把差值乘以10000,将其转换成点,于是我们的指标可以反应理论EURGBP的价格和实际EURGBP价格的差值,很直观地就可以看到是否存在套利的空间。我们将其运行在EURGBP 1min级别的图上,可以看到指标如下:

看到这我们会稍微欣喜一下,看来三角对冲还是有利可图的。

但是细想一下,不对,理论值和实际值怎么会相差100多个点,而且还出现了离现在越远,两者差值显著扩大的现象,肯定是哪里出了问题。

其实,出现这个问题的原因是一些数据的缺失导致的,比如我们要提取编号为100的K线的数据来做指标的计算,三个品种由于数据的缺失,会导致编号为100的K线的时间是不一样的,第100根K线对于EURGBP可能是昨天的数据,而对于GBPUSD来说可能是前天的数据,而离现在越远,这个时间差就越大,导致指标没有正确计算。

对冲指标的正确编写方式

为了解决这个问题,我们需要在这三个品种中拿出相同时间的数据来计算,而不是相同K线编号的数据来计算,于是,问题变得稍微复杂了,我们要对K线加入时间的鉴别。

试想一下,图中有动辄上万根K线,如果每一个时间对应的指标的计算都要从第一根K线开始找,找出三个品种对应改时间的收盘价,那么这个循环的工作量是非常大的,甚至有可能在加载指标的时候出现崩溃。这时我们就要用到全局变量来对已经计算的K线的根数进行统计,这样我们便可从已经计算出的K线开始往下找。具体的代码如下:

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
double hudge[];
string EURUSD='EURUSD';
string GBPUSD='GBPUSD';
string EURGBP='EURGBP';
int w2=0;
int w3=0;
int OnInit()
  {
string name=“EURUSD/GBPUSD-EURGBP”;
   SetIndexBuffer(0,hudge,INDICATOR_DATA);
   SetIndexStyle(0,DRAW_LINE,STYLE_SOLID,1,clrRed);
IndicatorShortName(name);
   return(INIT_SUCCEEDED);
  }
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
      int peri=PERIOD_CURRENT;
      int i=0;
      int i2;
      int i3;
      int k=0;
      int k1=0;
      int k2=0;
      int k3=0;
      int swit2=0;
      int swit3=0;
      while(iClose(EURUSD,peri,k3)!=0)
      {
         k3++;
      }
      while(iClose(GBPUSD,peri,k2)!=0)
      {
         k2++;
      }
      while(iClose(EURGBP,peri,k1)!=0)
      {
         k1++;
      }
      k=MathMin(k1,k2);
      k=MathMin(k,k3);
      ArrayResize(hudge,rates_total);
      if(prev_calculated==0)
      {
         for(i=0;i<=k-1;i++)
         {  
            for(i2=w2;i2<=k2-1;i2++)
            {
               if(iTime(EURUSD,peri,i2)==iTime(EURGBP,peri,i))
               {
                  swit2=1;
                  break;
               }
               if(iTime(EURUSD,peri,i2)<iTime(EURGBP,peri,i))
               {
                  swit2=0;
                  break;
               }
            }
            if(i2-2>=0)
            {
               w2=i2-2;
            }
            else
            {
               w2=0;
            }
            for(i3=w3;i3<=k3-1;i3++)
            {
               if(iTime(GBPUSD,peri,i3)==iTime(EURGBP,peri,i))
               {
                  swit3=1;
                  break;
               }
               if(iTime(GBPUSD,peri,i3)<iTime(EURGBP,peri,i))
               {
                  swit2=0;
                  break;
               }
               
            }
            if(i3-2>=0)
            {
               w3=i3-2;
            }
            else
            {
               w3=0;
            }
            if(swit2==1 && swit3==1 && iClose(EURGBP,peri,i)!=0 && iClose(EURUSD,peri,i2)!=0 && 
iClose(GBPUSD,peri,i3)!=0 &&iTime(EURGBP,peri,i)==iTime(EURUSD,peri,i2) && 
iTime(EURGBP,peri,i)==iTime(GBPUSD,peri,i3))
            {
               hudge[i]=(iClose(EURGBP,peri,i)-iClose(EURUSD,peri,i2)/iClose(GBPUSD,peri,i3))*10000;
            }
         }
      }
      else
      {
         ArrayResize(hudge,rates_total);
         if(iTime(EURGBP,peri,0)==iTime(EURUSD,peri,0) && 
iTime(EURGBP,peri,0)==iTime(GBPUSD,peri,0))
         {
            hudge[0]=(iClose(EURGBP,peri,0)-iClose(EURUSD,peri,0)/iClose(GBPUSD,peri,0))*10000;
            hudge[1]=(iClose(EURGBP,peri,1)-iClose(EURUSD,peri,1)/iClose(GBPUSD,peri,1))*10000;
         }
      }
   return(rates_total);
  }

下面分部讲解一下,第一部分是对指标的初始化,绑定数组等等。

第二部分定义了一系列变量,这些变量中,K1用来存储EURGBP的K线根数,K2用来存储GBPUSD的K线根数,K3用来存储EURUSD的K线根数,而K用来存储这三者之中的最小值,这是为了减少计算量,很多时候EURUSD或者GBPUSD的数据比EURGBP的数据多,这多出的那一部分由于EURGBP没有数据所以是不需要计算的,也就是说计算的范围是从编号为0的K线计算到编号为K-1的K线。

第三部分是最重要的一部分,当之前计算的K线根数为0,也就是说指标刚加载时,我们需要将0~k-1的K线对应的指标都计算一遍,所以我们做了一个循环,从0到k-1,那么为了计算指标,我们就需要找出与该K线的时间对应的EURUSD以及GBPUSD的价格,于是我们又做了一个循环来寻找与编号为i的EURGBP的K线时间相同的EURUSD的K线编号,为了避免每次计算从0开始找,这里用了全局变量w2。如果找到了,那么我们把开关2(swit2)赋值为1,如果没找到(当编号为i2的K线时间小于EURGBP的时间,说明没有找到,再找下去也不会有了,所以这根K线缺失了),那么就把开关2赋值为0,接下来把w2往回调2,以便为了下次再寻找时从w2开始找起。GBPUSD对应的K线寻找过程也是一样的。

找到对应的数据后我们就可以计算了,当开关2和开关3都等于1,也就是说EURUSD和GBPUSD都找到了与EURGBP对应的K线,而且时间上都相同,且三个价格都不为0,那么我们将(EURGBP-EURUSD/GBPUSD)*10000赋值给hudge这个被绑定的数组。

第四部分,如果指标都计算过一遍,那么就很简单了,只要计算当下和上一根K线的指标就行了。

我们将其加载到EURGBP的图表上,得出的结果如图:

于是我们发现,三角对冲策略似乎是真的有盈利的空间,可以看出,EURGBP理论和实际的差值已经达到了十几个点,哇,这还不快把策略搞起来。

但是先别急,三角对冲的事实是否是这样的呢?我们仔细观察一下其实就不难发现,当理论值和实际值的差值很大的时候都是在每天收盘和开盘的那段时间,而这段时间的点差会变得十分巨大,英镑有的时候会达到惊人的20个点的点差,而我们的理论值和实际值之间的差值只有十几个点,显然是不足以填补掉点差成本的。

关于这一结论大家可以在指标里面再加一条线,这条线反应了三个品种点差总和的走势,你会发现收盘和开盘那段时间三个品种总的点差会随着EURGBP理论与实际差值的变大而变大,可恨的是点差的值一直是大于这个差值的,也就是说根本没有套利的机会。这听起来让人沮丧,辛辛苦苦写了指标,结果证明是没有任何作用的,但是交易就是如此,很多时候会做无用功,但是正是这些无用功给了我们经验的积累,知道哪些东西或许是有用的,哪些东西确实没什么用,可以不用浪费时间,而这些成长也是每位交易者所必须的。

关于指标的编写就先说到这里,很多人对指标是比较反感的,觉得指标这个东西没什么用,实际上没有那么严重,交易系统对K线的分析实际上很多时候是计算得出来的,而指标只是把计算得出的结果更直观地反应在图上而已,所以没有必要妖魔化指标,也不用太过依赖指标,和EA一样,指标只是工具,效果取决于工具的好坏以及用工具的人。