欢迎进入汇天下HuiTXZN EA官方网站,EA购买专业安全可靠!

三角套利的探索-策略研究-MT4外汇EA,mt4量化交易,mt4智能交易-汇天下智能科技有限公司

一站式EA提供商

国家版权局软著登字第4574767、2693581、4270965号

客服中心:3338149422

行业资讯

汇天下智能科技有限公司
邮箱:3338149422@qq.com
Q Q:3338149422
地址:广东省兴宁市毅德城二号交易广场

策略研究
您的位置: 首页>>行业资讯>>策略研究
三角套利的探索
发布时间:2020-07-26 08:10:12浏览次数:679
class="code" style="-webkit-tap-highlight-color:transparent;background-color:#FBF9F5;border:1px solid #B3B3B3;color:#000000;overflow:auto;padding:8px 8px 8px 20px;font-size:13px;font-family:"Courier New", Courier, monospace !important;margin-bottom:12px;margin-top:12px;">void fnCreateFileSymbols(stThree &MxSmb[], int filehandle) { // 在文件中定义头文件 FileWrite(filehandle,"品名 1","品名 2","品名 3","合约大小 1","合约大小 2","合约大小 3", "最小手数 1","最小手数 2","最小手数 3","最大手数 1","最大手数 2","最大手数 3","手数增量 1","手数增量 2","手数增量 3", "公用最小手数","公用最大手数","小数位 1","小数位 2","小数位 3"); // 根据上面指定的头文件填写文件 for(int i=ArraySize(MxSmb)-1;i>=0;i--) { FileWrite(filehandle,MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name, MxSmb[i].smb1.contract,MxSmb[i].smb2.contract,MxSmb[i].smb3.contract, MxSmb[i].smb1.lot_min,MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min, MxSmb[i].smb1.lot_max,MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max, MxSmb[i].smb1.lot_step,MxSmb[i].smb2.lot_step,MxSmb[i].smb3.lot_step, MxSmb[i].lot_min,MxSmb[i].lot_max, MxSmb[i].smb1.digits,MxSmb[i].smb2.digits,MxSmb[i].smb3.digits); } FileWrite(filehandle,"");            // 在所有品名之后留下一个空字符串 // 操作完成后, 出于安全原因将所有数据写入磁盘 FileFlush(filehandle); }

除了三角之外, 我们还会写入额外的数据: 允许交易量, 合约大小, 报价单数量。我们只需要从文件中获取这些数据来直观地检查品种的属性。

该函数置于一个单独的 fnCreateFileSymbols.mqh 文件中。

重新启动机器人

我们已近乎完成了 EA 的初始设置。不过, 我们仍然有一个问题需要回答: 如何处理崩溃后的恢复?我们不必担心短时间的互联网连接断线。重新连接到网络后, 机器人恢复运行。但如果我们必须重新启动机器人, 那么我们需要记住当前位置, 并从此处继续操作。

下面是解决机器人重新启动问题的函数:

void fnRestart(stThree &MxSmb[],ulong magic,int accounttype)
   {
      string   smb1,smb2,smb3;
      long     tkt1,tkt2,tkt3;
      ulong    mg;
      uchar    count=0;    // 还原三角的计数器
      
      switch(accounttype)
      {
         // 在对冲账户中恢复仓位非常容易: 遍历所有未平仓位, 使用魔幻数字定义持仓 
         // 并将它们组合为三角
         // 如果净持账户, 情况会变得更加复杂 - 首先, 我们需要参考保存的仓位数据库, 这些仓位是由机器人打单的。 
         
         // 搜索必要仓位并将其恢复为三角的算法已经以相当直接的方式实现了, 没有任何装饰和 
         // 优化。但是, 由于这个阶段是不经常需要的, 我们可能会忽略其性能
         // 以便简化代码。 
         
         case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
            // 遍历所有已开仓位, 并检测魔幻数字匹配。 
            // 记住第一个检测到的仓位的魔幻数字: 用它来检测另外两个。 

            
            for(int i=PositionsTotal()-1;i>=2;i--)
            {//for i
               smb1=PositionGetSymbol(i);
               mg=PositionGetInteger(POSITION_MAGIC);
               if (mg(magic+MAGIC)) continue;
               
               // 记住票号, 以便更方便地访问这个仓位。 
               tkt1=PositionGetInteger(POSITION_TICKET);
               
               // 寻找具有相同魔幻数字的第二个仓位。 
               for(int j=i-1;j>=1;j--)
               {//for j
                  smb2=PositionGetSymbol(j);
                  if (mg!=PositionGetInteger(POSITION_MAGIC)) continue;  
                  tkt2=PositionGetInteger(POSITION_TICKET);          
                    
                  // 查找最后的仓位。
                  for(int k=j-1;k>=0;k--)
                  {//for k
                     smb3=PositionGetSymbol(k);
                     if (mg!=PositionGetInteger(POSITION_MAGIC)) continue;
                     tkt3=PositionGetInteger(POSITION_TICKET);
                     
                     // 如果您到达这个阶段, 已经找齐了已开单三角。数据已下载。机器人在下一笔分笔报价时计算其余数据。
                     
                     for(int m=ArraySize(MxSmb)-1;m>=0;m--)
                     {//for m
                        // 遍历三角数组, 忽略已开三角。
                        if (MxSmb[m].status!=0) continue; 
                        
                        // "bluntly" 完成。起初, 我们似乎可以                         // 多次参考相同 货币对若干次。但事实并非如此, 因为在检测到另一种货币对之后,
                        // 我们从下一对继续我们的搜索, 而不是从搜索循环的开始

 

if ( (MxSmb[m].smb1.name==smb1 || MxSmb[m].smb1.name==smb2 || MxSmb[m].smb1.name==smb3) && (MxSmb[m].smb2.name==smb1 || MxSmb[m].smb2.name==smb2 || MxSmb[m].smb2.name==smb3) && (MxSmb[m].smb3.name==smb1 || MxSmb[m].smb3.name==smb2 || MxSmb[m].smb3.name==smb3)); elsecontinue; // 我们已经检测到这个三角。现在, 我们为其分配适当的状态 MxSmb[m].status=2; MxSmb[m].magic=magic; MxSmb[m].pl=0; // 按所需顺序排列单号。三角已经还原了。 if (MxSmb[m].smb1.name==smb1) MxSmb[m].smb1.tkt=tkt1; if (MxSmb[m].smb1.name==smb2) MxSmb[m].smb1.tkt=tkt2; if (MxSmb[m].smb1.name==smb3) MxSmb[m].smb1.tkt=tkt3; if (MxSmb[m].smb2.name==smb1) MxSmb[m].smb2.tkt=tkt1; if (MxSmb[m].smb2.name==smb2) MxSmb[m].smb2.tkt=tkt2; if (MxSmb[m].smb2.name==smb3) MxSmb[m].smb2.tkt=tkt3; if (MxSmb[m].smb3.name==smb1) MxSmb[m].smb3.tkt=tkt1; if (MxSmb[m].smb3.name==smb2) MxSmb[m].smb3.tkt=tkt2; if (MxSmb[m].smb3.name==smb3) MxSmb[m].smb3.tkt=tkt3; count++; break; }//for m              }//for k              }//for j          }//for i          break; default: break; } if (count>0) Print("Restore "+(string)count+" triangles"); }

和以前一样, 这个函数在一个单独的文件中: fnRestart.mqh

最后一步:

      ctrade.SetDeviationInPoints(DEVIATION);
      ctrade.SetTypeFilling(ORDER_FILLING_FOK);
      ctrade.SetAsyncMode(true);
      ctrade.LogLevel(LOG_LEVEL_NO);
      
      EventSetTimer(1);

注意发送订单的异步模式。策略假定最大的操作行为, 所以我们使用这种安置模式。还有一些复杂的情况: 我们需要额外的代码来跟踪其是否成功开单。我们在下面研究这一切。

OnInit() 模块已经完成。是进入机器人实体的时候了。

OnTick

首先, 我们来看看设置中是否对最大允许的三角数量有限制。如果存在这样的限制, 并且我们已经达到了这个限制, 那么可以跳过此分笔报价时刻的大部分代码:

      ushort OpenThree=0;                          // 开单的三角数量
      for(int j=ArraySize(MxThree)-1;j>=0;j--)
      if (MxThree[j].status!=0) OpenThree++;       // 未平单的也被考虑在内         

检查很简单。我们声明了一个局部变量来计数已开单的三角, 并在一个循环中遍历我们的主要数组。如果三角状态不为 0, 那么它是激活的。

计算已开单三角后 (如果限制允许), 查看所有剩余的三角并跟踪其状态。fnCalcDelta() 函数负责此任务:

      if (inMaxThree==0 || (inMaxThree>0 && inMaxThree>OpenThree))
         fnCalcDelta(MxThree,inProfit,inCmnt,inMagic,inLot,inMaxThree,OpenThree);   // 计算偏离并立即开单

我们来详细分析代码:

void fnCalcDelta(stThree &MxSmb[],double prft, string cmnt, ulong magic,double lot, ushort lcMaxThree, ushort &lcOpenThree)
   {     
      double   temp=0;
      string   cmnt_pos="";
      
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for i
         // 如果三角已激活, 则跳过它
         if(MxSmb[i].status!=0) continue; 
         
         // 重新检查所有三个货币对的可用性: 如果至少有一个不可用,
         // 那么计算整个三角就没有意义
         if (!fnSmbCheck(MxSmb[i].smb1.name)) continue;  
         if (!fnSmbCheck(MxSmb[i].smb2.name)) continue;  // 其中一个货币对平单
         if (!fnSmbCheck(MxSmb[i].smb3.name)) continue;     
         
         // 在每次分笔报价伊始计算开单三角的数量,
         // 但是它们也可在分笔报价之内开单。所以, 要持续跟踪它们的数量
         if (lcMaxThree>0) {if (lcMaxThree>lcOpenThree); elsecontinue;}     

         
         // 之后, 获取所有必要的数据进行计算。 
         
         // 获取分笔报价的价格。
         if(!SymbolInfoDouble(MxSmb[i].smb1.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb1.tv)) continue;
         if(!SymbolInfoDouble(MxSmb[i].smb2.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb2.tv)) continue;
         if(!SymbolInfoDouble(MxSmb[i].smb3.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb3.tv)) continue;
         
         // 获取当前价格。
         if(!SymbolInfoTick(MxSmb[i].smb1.name,MxSmb[i].smb1.tick)) continue;
         if(!SymbolInfoTick(MxSmb[i].smb2.name,MxSmb[i].smb2.tick)) continue;
         if(!SymbolInfoTick(MxSmb[i].smb3.name,MxSmb[i].smb3.tick)) continue;
         
         // 检查竞买价和竞卖价是否为 0。
         if(MxSmb[i].smb1.tick.ask<=0 || MxSmb[i].smb1.tick.bid<=0 || MxSmb[i].smb2.tick.ask<=0 || MxSmb[i].smb2.tick.bid<=0 || MxSmb[i].smb3.tick.ask<=0 || MxSmb[i].smb3.tick.bid<=0) continue;
         
         // 计算第三对的交易量。我们知道前两对的交易量 — 它相同且固定// 第三对的交易量总是在变化。但只在初始变量中手数不为 0 时才会计算。
         // 如果手数为零, 则使用最小 (相似) 交易量。
         // 交易量计算逻辑很简单。我们回到我们的三角: EURUSD=EURGBP*GBPUSD。买入或卖出 GBP 数量
         // 直接取决于 EURGBP 的报价, 而在第三个货币对中, 这第三种货币先到。通过使用第二个货币对的价格作为一个交易量,
         // 我们消减了一些计算。我已经取得了竞卖价和竞买价之间的平均价格。
         // 不要忘记调整输入交易量。
         
         if (lot>0)
         MxSmb[i].smb3.lot=NormalizeDouble((MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.tick.bid)/2*MxSmb[i].smb1.lot,MxSmb[i].smb3.digits_lot);
         
         // 如果计算的交易量超过允许的边界, 通知用户。
         // 三角标记为非操作
         if (MxSmb[i].smb3.lotMxSmb[i].smb3.lot_max)
         {
            Alert("计算 ",MxSmb[i].smb3.name," 超界。Min/Max/Calc: ",
            DoubleToString(MxSmb[i].smb3.lot_min,MxSmb[i].smb3.digits_lot),"/",
            DoubleToString(MxSmb[i].smb3.lot_max,MxSmb[i].smb3.digits_lot),"/",
            DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot)); 
            Alert("三角: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - 禁用");
            MxSmb[i].smb1.name="";   
            continue;  
         }
         
         // 计算我们的成本, 即点差+佣金。pr = 点差的整数形式点数。
         // 点差妨碍我们使用这种策略赚钱, 因此, 应随时考虑到这一点。 
         // 您可用以点数为单位的点差替代差价乘以相反的点数。

         
         MxSmb[i].smb1.sppoint=NormalizeDouble(MxSmb[i].smb1.tick.ask-MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits)*MxSmb[i].smb1.Rpoint;
         MxSmb[i].smb2.sppoint=NormalizeDouble(MxSmb[i].smb2.tick.ask-MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits)*MxSmb[i].smb2.Rpoint;
         MxSmb[i].smb3.sppoint=NormalizeDouble(MxSmb[i].smb3.tick.ask-MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits)*MxSmb[i].smb3.Rpoint;
         if (MxSmb[i].smb1.sppoint<=0 || MxSmb[i].smb2.sppoint<=0 || MxSmb[i].smb3.sppoint<=0) continue;
         
         // 现在, 我们来计算存款币种的点差。 
         // 在货币中, 1 个分笔报价的价格总是等于 SYMBOL_TRADE_TICK_VALUE。
         // 另外, 不要忘记交易量
         MxSmb[i].smb1.spcost=MxSmb[i].smb1.sppoint*MxSmb[i].smb1.tv*MxSmb[i].smb1.lot;
         MxSmb[i].smb2.spcost=MxSmb[i].smb2.sppoint*MxSmb[i].smb2.tv*MxSmb[i].smb2.lot;
         MxSmb[i].smb3.spcost=MxSmb[i].smb3.sppoint*MxSmb[i].smb3.tv*MxSmb[i].smb3.lot;
         
         // 那么, 这里是指定交易量加上用户指定佣金后的成本
         MxSmb[i].spread=MxSmb[i].smb1.spcost+MxSmb[i].smb2.spcost+MxSmb[i].smb3.spcost+prft;
         
         // 我们可以跟踪投资组合的竞卖价<竞买价的情况, 但这种情况很少见 
         // 且可单独考虑。同时, 时间上的套利空间也能够处理这样的情况。
         // 仓位是没有风险的, 这就是为什么:假设您已经买入了 eurusd,         // 并立即抛售 eurgbp 和 gbpusd。 
         // 换言之, 我们看到 eurusd 竞买价 < eurgbp 竞买价 * gbpusd 竞买价。这种情况很多, 但是这对于一个成功的入场是不够的。
         // 计算点差成本。当竞卖价<竞买价时, 不要机械地入场, 而要等到它们之间的差值
         // 超过点差成本。          
         
         // 我们同意买入意味着买入第一个品种, 然后卖出剩下的两个品种,
         // 而卖出意味着卖出第一个货币双并买入其余的两个货币双。
         
         temp=MxSmb[i].smb1.tv*MxSmb[i].smb1.Rpoint*MxSmb[i].smb1.lot;
         
         // 我们仔细看看计算公式。 
         // 1. 在括号内, 每个价格都针对较差方向的滑点进行了调整: MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev
         // 2. 如上面等式所示, eurgbp 竞买价 * gbpusd 竞买价 - 乘以第二和第三个品种的价格:
         //    (MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)
         // 3. 然后, 计算竞卖价和竞买价之间的差值
         // 4. 我们收到的以点数为单位的差价现在应该转换为币值: 乘以 
         // 点数价格和交易量。取第一个货币对的数值。 
         // 如果我们通过把所有的货币对放在一侧来构建一个三角, 并与 1 进行比较, 就会有更多的计算。 

         MxSmb[i].PLBuy=((MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)-(MxSmb[i].smb1.tick.ask+MxSmb[i].smb1.dev))*temp;
         MxSmb[i].PLSell=((MxSmb[i].smb1.tick.bid-MxSmb[i].smb1.dev)-(MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.ask+MxSmb[i].smb3.dev))*temp;
         
         // 我们已得到买入或卖出三角后可盈利或亏损的汇总计算。 
         // 现在, 我们只需将其与成本进行比较, 以便做出是否入场交易的决策。我们将所有数值均常规化到小数点后两位。
         MxSmb[i].PLBuy=   NormalizeDouble(MxSmb[i].PLBuy,2);
         MxSmb[i].PLSell=  NormalizeDouble(MxSmb[i].PLSell,2);
         MxSmb[i].spread=  NormalizeDouble(MxSmb[i].spread,2);                  
         
         // 如果有潜在的利润, 检查资金是否足够开单。         
         if (MxSmb[i].PLBuy>MxSmb[i].spread || MxSmb[i].PLSell>MxSmb[i].spread)
         {
            // 我只简单计算了入场买入的保证金。由于它比卖出稍高, 我们不必考虑交易方向。   
            // 还要注意递增因子。如果保证金不足, 我们不能为三角开单。默认的递增因子是 20%

            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb1.name,MxSmb[i].smb1.lot,MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.mrg))
            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb2.name,MxSmb[i].smb2.lot,MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.mrg))
            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb3.name,MxSmb[i].smb3.lot,MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.mrg))
            if(AccountInfoDouble(ACCOUNT_MARGIN_FREE)>((MxSmb[i].smb1.mrg+MxSmb[i].smb2.mrg+MxSmb[i].smb3.mrg)*CF))  //check the free margin
            {
               // 我们几乎已为开单做好了各种准备。现在只需从我们的范围内找到一个可用的魔幻数字。 
               // 初始的魔幻数字是在 inMagic 变量中指定的。默认值是 300。 
               // 魔幻数字的范围在 MAGIC 定义中指定, 默认值是 200。
               MxSmb[i].magic=fnMagicGet(MxSmb,magic);   
               if (MxSmb[i].magic<=0)
               { // 若为 0, 则所有的魔幻数字都被占用。通知此消息并退出。
                  Print("可用魔幻数字结束
新三角不会开单");
                  break;
               }  
               
               // 设置检测到的魔幻数字
               ctrade.SetExpertMagicNumber(MxSmb[i].magic); 
               
               // 写一个三角的注释
               cmnt_pos=cmnt+(string)MxSmb[i].magic+" 开单";               
               
               // 开单, 同时记住三角开单的时间。 
               // 这是避免等待所必需的。 
               // 默认情况下, 完全开单的等待时间在 MAXTIMEWAIT 定义中设置为 3 秒。
               // 如果在这段时间内我们未能完全开单, 则所有已开单均要平单。                              MxSmb[i].timeopen=TimeCurrent();                              if (MxSmb[i].PLBuy>MxSmb[i].spread)    fnOpen(MxSmb,i,cmnt_pos,true,lcOpenThree);               if (MxSmb[i].PLSell>MxSmb[i].spread)   fnOpen(MxSmb,i,cmnt_pos,false,lcOpenThree);                                             // 打印有关三角开单的消息。                if (MxSmb[i].status==1) Print("开单三角: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" 魔幻数字: "+(string)MxSmb[i].magic);            }         }               }//for i   }

该函数带有详细的注释和解释, 令一切都很清楚。有两件事情已经被遗忘了: 我已应用的可用魔幻数字选择机制和三角开单。

以下是我们如何选择可用魔幻数字:

ulong fnMagicGet(stThree &MxSmb[],ulong magic)
   {
      int mxsize=ArraySize(MxSmb);
      bool find;
      
      // 我们可以遍历数组中的所有开单三角。 
      // 但我已选择了另外一个选项 - 遍历魔幻数字的范围,
      // 然后沿数组移动选定的一个。 
      for(ulong i=magic;ifalse;
         
         // 魔幻数字 i。我们来看看它是否被分配到了任何一个已开单三角。
         for(int j=0;jif (MxSmb[j].status>0 && MxSmb[j].magic==i)
         {
            find=true;
            break;   
         }   
         
         // 如果不使用魔幻数字, 则不等待其完成即退出循环。    
         if (!find) return(i);            
      }  
      return(0);  
   }

此处是我们如何为三角开单:

bool fnOpen(stThree &MxSmb[],int i,string cmnt,bool side, ushort &opt)
   {
      // 首个订单开单标志。 
      bool openflag=false;
      
      // 无授权则不能交易。 
      if (!cterm.IsTradeAllowed())  return(false);
      if (!cterm.IsConnected())     return(false);
      
      switch(side)
      {
         case  true:
         
         // 如果在发送开单后返回 "假", 则发送剩余的两个货币对没有意义。          // 我们在下一次分笔报价时再次尝试。另外, 机器人不能部分进行三角开单。          // 如果某些部分在发送订单后未开单, 等待
         // MAXTIMEWAIT 定义中设置的时间, 然后将部分开单的三角平单。 
         if(ctrade.Buy(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt))
         {
            openflag=true;
            MxSmb[i].status=1;
            opt++;
            // 进一步的逻辑是相同的: 如果无法开单, 则该三角平单。 
            if(ctrade.Sell(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt))
            ctrade.Sell(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt);               
         }            
         break;
         case  false:
         
         if(ctrade.Sell(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt))
         {
            openflag=true;
            MxSmb[i].status=1;  
            opt++;        
            if(ctrade.Buy(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt))
            ctrade.Buy(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt);         
         }           
         break;
      }      
      return(openflag);
   }

像往常一样, 上面的函数位于单独的 fnCalcDelta.mqh, fnMagicGet.mqh 和 fnOpen.mqh 文件中。

所以, 我们已经找到了必要的三角, 并将其送出开单。在 MetaTrader 4 以及 MetaTrader 5 对冲账户中, 这实际上意味着 EA 操作的结束。但是我们仍然需要跟踪三角开单的结果。我不打算使用 OnTradeOnTradeTransaction 事件, 因为它们不能保证获得成功。代之, 我要检查当前仓位的数量 — 一个 100% 的指标。

我们来看看开仓管理函数:

void fnOpenCheck(stThree &MxSmb[], int accounttype, int fh)
   {
      uchar cnt=0;       // 三角中开仓计数器
      ulong   tkt=0;     // 当前单号
      string smb="";     // 当前品种
      
      // 检查我们的三角阵列
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // 只考虑具有状态 1 的三角, 即被送出用于开单
         if(MxSmb[i].status!=1) continue;
                          
         if ((TimeCurrent()-MxSmb[i].timeopen)>MAXTIMEWAIT)
         {     
            // 如果超出开单时间, 请将三角标记为准备平单         
            MxSmb[i].status=3;
            Print("未正确开单: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
            continue;
         }
         
         cnt=0;
         
         switch(accounttype)
         {
            case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
            
            // 检查所有未结仓位。针对每个三角执行此检查。 

            for(int j=PositionsTotal()-1;j>=0;j--)
            if (PositionSelectByTicket(PositionGetTicket(j)))
            if (PositionGetInteger(POSITION_MAGIC)==MxSmb[i].magic)
            {
               // 获取品种并考虑仓位的单号。 

 

tkt=PositionGetInteger(POSITION_TICKET); smb=PositionGetString(POSITION_SYMBOL); // 检查在所考虑的三角中是否有我们需要的当前仓位。 // 如果是, 增加计数, 并记住单号和开单价格。 if (smb==MxSmb[i].smb1.name){ cnt++; MxSmb[i].smb1.tkt=tkt; MxSmb[i].smb1.price=PositionGetDouble(POSITION_PRICE_OPEN);} else if (smb==MxSmb[i].smb2.name){ cnt++; MxSmb[i].smb2.tkt=tkt; MxSmb[i].smb2.price=PositionGetDouble(POSITION_PRICE_OPEN);} else if (smb==MxSmb[i].smb3.name){ cnt++; MxSmb[i].smb3.tkt=tkt; MxSmb[i].smb3.price=PositionGetDouble(POSITION_PRICE_OPEN);} // 如果有三个必需的仓位, 我们的三角已经成功开单。将其状态更改为 2 (开单)。 // 将开单数据写入日志文件 if (cnt==3) { MxSmb[i].status=2; fnControlFile(MxSmb,i,fh); break; } } break; default: break; } } }

写日志文件的函数很简单:

void fnControlFile(stThree &MxSmb[],int i, int fh)
   {
      FileWrite(fh,"============");
      FileWrite(fh,"开单:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
      FileWrite(fh,"单号:",MxSmb[i].smb1.tkt,MxSmb[i].smb2.tkt,MxSmb[i].smb3.tkt);
      FileWrite(fh,"手数",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot));
      FileWrite(fh,"Margin",DoubleToString(MxSmb[i].smb1.mrg,2),DoubleToString(MxSmb[i].smb2.mrg,2),DoubleToString(MxSmb[i].smb3.mrg,2));
      FileWrite(fh,"竞卖价",DoubleToString(MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.digits));
      FileWrite(fh,"竞买价",DoubleToString(MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits));               
      FileWrite(fh,"开单价格",DoubleToString(MxSmb[i].smb1.price,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.price,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.price,MxSmb[i].smb3.digits));
      FileWrite(fh,"点值",DoubleToString(MxSmb[i].smb1.tv,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tv,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tv,MxSmb[i].smb3.digits));
      FileWrite(fh,"点差点数",DoubleToString(MxSmb[i].smb1.sppoint,0),DoubleToString(MxSmb[i].smb2.sppoint,0),DoubleToString(MxSmb[i].smb3.sppoint,0));
      FileWrite(fh,"点差 $",DoubleToString(MxSmb[i].smb1.spcost,3),DoubleToString(MxSmb[i].smb2.spcost,3),DoubleToString(MxSmb[i].smb3.spcost,3));
      FileWrite(fh,"所有点差",DoubleToString(MxSmb[i].spread,3));
      FileWrite(fh,"买入盈亏",DoubleToString(MxSmb[i].PLBuy,3));
      FileWrite(fh,"卖出盈亏",DoubleToString(MxSmb[i].PLSell,3));      
      FileWrite(fh,"魔幻数字",string(MxSmb[i].magic));
      FileWrite(fh,"开单时间",TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_SECONDS));
      FileWrite(fh,"当前时间",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS));
      
      FileFlush(fh);       
   }

所以, 我们找到了一个已入场并相应开仓的三角。现在, 我们需要计算我们赚了多少。

void fnCalcPL(stThree &MxSmb[], int accounttype, double prft)
   {
      // 再次遍历我们的三角数组。 
      // 开单和平单的速度是这一策略的重要组成部分。 
      // 因此, 只要我们发现已平单的三角, 立即将之平单。
      
      bool flag=cterm.IsTradeAllowed() & cterm.IsConnected();      
      
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for
         // 我们只对状态为 2 或 3 的三角感兴趣。
         // 如果三角只是部分开单, 我们会得到状态 3 (三角平单)
         if(MxSmb[i].status==2 || MxSmb[i].status==3); elsecontinue;                             
         
         // 我们来计算三角形的盈利 
         if (MxSmb[i].status==2)
         {
            MxSmb[i].pl=0;         // 盈利清零
            switch(accounttype)
            {//switch
               case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:  
                
               if (PositionSelectByTicket(MxSmb[i].smb1.tkt)) MxSmb[i].pl=PositionGetDouble(POSITION_PROFIT);
               if (PositionSelectByTicket(MxSmb[i].smb2.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT);
               if (PositionSelectByTicket(MxSmb[i].smb3.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT);                           
               break;
               default:
               break;
            }//switch
            
            // 四舍五入到小数点后两位
            MxSmb[i].pl=NormalizeDouble(MxSmb[i].pl,2);
            
            // 我们来近距离查看平单。我使用以下逻辑:
            // 套利的情况不正常, 不应该发生。当它出现时, 我们可以期待返回 
            // 到没有套利的状态。我们可以赚钱吗?换句话说, 我们不知道,             // 是否我们能够继续获得利润。因此, 我希望在点差和佣金被抹平之后立即平仓。 
            // 三角套利是以点数计算的。您不能在此依赖大走势。 
            // 尽管您可以等待输入中 Commission 变量的期望利润。 
            // 如果我们赚得比我们花费的多, 则将 "送出平单" 状态分配给该仓位。

            if (flag && MxSmb[i].pl>prft) MxSmb[i].status=3;                    
         }
         
         // 仅在允许交易的情况下将三角平单。
         if (flag && MxSmb[i].status==3) fnCloseThree(MxSmb,accounttype,i); 
      }//for         
   }

负责三角平单的函数很简单:

void fnCloseThree(stThree &MxSmb[], int accounttype, int i)
   {
      // 在平单之前, 检查三角中所有货币对的可用性。 
      // 裂解三角是错误的, 极端危险。在净持账户上操作时,
      // 这可能会导致以后的仓位混乱。 
      
      if(fnSmbCheck(MxSmb[i].smb1.name))
      if(fnSmbCheck(MxSmb[i].smb2.name))
      if(fnSmbCheck(MxSmb[i].smb3.name))          
      
      // 如果全部可用, 则使用标准库将全部三个仓位平仓。 
      // 平仓后, 检查操作是否成功。 
      switch(accounttype)
      {
         case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:     
         
         ctrade.PositionClose(MxSmb[i].smb1.tkt);
         ctrade.PositionClose(MxSmb[i].smb2.tkt);
         ctrade.PositionClose(MxSmb[i].smb3.tkt);              
         break;
         default:
         break;
      }       
   }  

我们的工作近乎完成。现在, 我们只需检查平仓是否成功, 并在屏幕上显示一条消息。如果机器人什么都不写, 好似它未工作。

以下是我们对成功平仓的检查。我们可以实现一个单独的函数, 简单地通过改变交易方向来开仓和平仓, 但是我不喜欢这个选项, 因为这两个操作之间存在轻微的程序差异。

检查是否平仓成功:

void fnCloseCheck(stThree &MxSmb[], int accounttype,int fh)
   {
      // 遍历三角数组。 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // 我们只对那些状态为 3 的感兴趣, 即已平仓或送出平仓的那些。 
         if(MxSmb[i].status!=3) continue;
         
         switch(accounttype)
         {
            case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: 
            
            // 如果从三角中不能选择一个货币对, 则平仓成功。返回状态 0
            if (!PositionSelectByTicket(MxSmb[i].smb1.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb2.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb3.tkt))
            {  // 意味着平仓已成功
               MxSmb[i].status=0;   
               
               Print("三角平仓: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" 魔幻数字: "+(string)MxSmb[i].magic+"   P/L: "+DoubleToString(MxSmb[i].pl,2));
               
               // 将平仓数据写入日志文件。 
               if (fh!=INVALID_HANDLE)
               {
                  FileWrite(fh,"============");
                  FileWrite(fh,"平单:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
                  FileWrite(fh,"手数",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot));
                  FileWrite(fh,"单号",string(MxSmb[i].smb1.tkt),string(MxSmb[i].smb2.tkt),string(MxSmb[i].smb3.tkt));
                  FileWrite(fh,"魔幻数字",string(MxSmb[i].magic));
                  FileWrite(fh,"盈利",DoubleToString(MxSmb[i].pl,3));
                  FileWrite(fh,"当前时间",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS));
                  FileFlush(fh);               
               }                   
            }                  
            break;
         }            
      }      
   }

最后, 我们在屏幕上显示一条注释以供直观确认。我们显示以下内容:

  1. 跟踪的三角总数

  2. 开单三角

  3. 最近五个开单三角

  4. 开单三角, 如果有的话

以下是函数代码:

void fnCmnt(stThree &MxSmb[], ushort lcOpenThree)
   {     
      int total=ArraySize(MxSmb);
      
      string line="=============================
";
      string txt=line+MQLInfoString(MQL_PROGRAM_NAME)+": ON
";
      txt=txt+"三角总计: "+(string)total+"
";
      txt=txt+"开单三角: "+(string)lcOpenThree+"
"+line;
      
      // 屏幕上显示的最大三角数量
      short max=5;
      max=(short)MathMin(total,max);
      
      // 显示最近五个开单三角 
      short index[];                    // 索引数字
      ArrayResize(index,max);
      ArrayInitialize(index,-1);        // 未使用
      short cnt=0,num=0;
      while(cnt// 第一个最大平仓三角索引作为开始
      {
         if(MxSmb[num].status!=0)  {num++;continue;}
         index[cnt]=num;
         num++;cnt++;         
      }
      
      // 只有当元素的数量超过了屏幕上可以显示的数量时, 才能进行排序和搜索。 
      if (total>max) 
      for(short i=max;i// 开单三角显示在下面。
         if(MxSmb[i].status!=0) continue;
         
         for(short j=0;jif (MxSmb[i].PLBuy>MxSmb[index[j]].PLBuy)  {index[j]=i;break;}
            if (MxSmb[i].PLSell>MxSmb[index[j]].PLSell)  {index[j]=i;break;}
         }   
      }
      
      // 显示最近开单的三角。
      bool flag=true;
      for(short i=0;iif (cnt<0) continue;
         if (flag)
         {
            txt=txt+"品种1           品种2           品种3         买入盈亏        卖出盈亏        点差
";
            flag=false;
         }         
         txt=txt+MxSmb[cnt].smb1.name+" + "+MxSmb[cnt].smb2.name+" + "+MxSmb[cnt].smb3.name+":";
         txt=txt+"      "+DoubleToString(MxSmb[cnt].PLBuy,2)+"          "+DoubleToString(MxSmb[cnt].PLSell,2)+"            "+DoubleToString(MxSmb[cnt].spread,2)+"
";      
      }            
      
      // 显示开单三角。 
      txt=txt+line+"
";
      for(int i=total-1;i>=0;i--)
      if (MxSmb[i].status==2)
      {
         txt=txt+MxSmb[i].smb1.name+"+"+MxSmb[i].smb2.name+"+"+MxSmb[i].smb3.name+" P/L: "+DoubleToString(MxSmb[i].pl,2);
         txt=txt+"   开单时间: "+TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
         txt=txt+"
";
      }   
      Comment(txt);
   }

 

 

 

 

 

 

 

 

有可能在分笔报价模拟模式下进行测试, 并与真实分笔报价测试进行比较。我们可以更进一步比较基于真实分笔报价的实际行动的测试结果, 并得出结论: 多元测试器距现实尚远。

结果表明, 您平均每周可以进行 3-4 次交易。大多数情况下, 在夜间开仓, 三角通常含有 TRY, NOK, SEK 等低流动性货币。机器人的利润取决于交易量。由于交易不频繁, EA 可以轻松处理大交易量, 并与其它机器人并行工作。

机器人的风险很容易计算: 3 个点差 * 开单三角的数量。

为了准备我们可以使用的货币对, 我建议首先显示所有的品种, 然后隐藏那些禁止交易和非货币对的品种。可以使用多货币策略粉丝所不可或缺的脚本来快速完成: https://www.mql5.com/zh/market/product/25256

我还应提醒您, 测试器的历史不会从经纪商的服务器上传 - 应该预先上传到客户终端。因此, 这应该在测试之前单独完成, 或者再次使用上述脚本。

发展前景

我们能改善结果吗? 当然可以。要做到这一点, 我们需要做流动性汇聚。这种方法的缺点是需要在多个经纪商开户。

我们也可以加速测试结果。这可以通过两种方式来完成。第一步是引入一个离散计算, 持续跟踪三角, 其入场概率非常高。第二种方法是使用 OpenCL, 对于这个机器人来说这非常合理。

文章中使用的文件

# 文件名 描述
1 var.mqh 描述所有应用的变量, 定义和输入。
2 fnWarning.mqh 检查 EA 正确操作的初始条件: 输入, 环境, 设置。
3 fnSetThree.mqh 形成货币对三角。货币对的来源也可以在这里选择—- 市场观察或预先准备的文件。
4 fnSmbCheck.mqh 检查品种的可用性和其它限制的函数。注意: 机器人不会检查交易和报价时段。
5 fnChangeThree.mqh 改变三角中的货币对位置, 以统一的方式形成它们。
6 fnSmbLoad.mqh 上传各品种, 价格, 点数, 交易量限制等数据。
7 fnCalcDelta.mqh 考虑三角中的所有分量, 以及所有的附加成本: 点差, 佣金, 滑点。
8 fnMagicGet.mqh 搜索可用于当前三角的魔幻数字。
9 fnOpenCheck.mqh 检查三角是否成功开单。
10 fnCalcPL.mqh 计算三角利润/亏损。
11 fnCreateFileSymbols.mqh 用三角创建交易文件的函数。该文件还含有其它数据 (更多信息)。
12 fnControlFile.mqh 维护日志文件的函数。它包含所有开单和平单的必要数据。
13 fnCloseThree.mqh 三角平仓。
14 fnCloseCheck.mqh 检查三角是否完全平仓。
15 fnCmnt.mqh 在屏幕上显示注释。
16 fnRestart.mqh 当启动机器人时, 检查是否有以前打开的三角。如果有的话, 恢复并跟踪它们。
17 fnOpen.mqh 三角开单。
18 Support.mqh 额外的支持类。它只有一个函数 — 计算分数的小数位数。
19 head.mqh 描述所有上述文件的头文件。