股票量化软件:三角套利

总体思路

开发可靠规律的三角套利话题经常出现在论坛上。那么它究竟是什么呢?

 

"套利" 意味着有些偏向行情的中立性。"三角" 是指投资组合由三个金融工具组成。

我们举一个最流行的例子: "欧元 — 英镑 — 美元" 三角。就货币对而言, 可以描述如下: EURUSD + GBPUSD + EURGBP。所需的中立性包括尝试同时买入和卖出相同的金融工具, 从而赚取利润。 赫兹股票量化软件

这看起来如下。这个例子中的任何一个货币对都可通过另外两个货币对来表示:

EURUSD=GBPUSD*EURGBP,

 GBPUSD=EURUSD/EURGBP,

或 EURGBP=EURUSD/GBPUSD

所有这些变体是相同的, 下面会更详细地讨论它们中的所有选择。同时, 我们来研究第一个选项。

首先, 我们需要看出竞买价和竞卖价。流程如下:

  1. 买入 EURUSD, 即使用 竞卖 价。这意味着, 我们在余额中增加 EUR 占比, 并消减 USD。 
  2. 我们来通过其它两个货币对评估 EURUSD。 赫兹股票量化软件
  3. GBPUSD: 这里面没有 EUR。代之, 我们需要抛售这里面的 USD。为了抛售 GBPUSD 当中的 USD, 我们需要买入这个货币对。意即, 我们使用 竞卖价。当买入时, 我们在余额中增加 GBP 占比, 同时消减 USD。
  4. EURGBP: 我们需要买入 EUR, 抛售我们不需要的 GBP。买入 EURGBP, 使用 竞卖价。我们在余额中增加 EUR 占比, 并消减 GBP。

总计我们拥有: (竞买价) EURUSD = (竞买价) GBPUSD * (竞买价) EURGBP。我们已获得了必要的等价。为了令其盈利, 我们应该一边买入一边卖出。这里有两种可能的选项:

  1. 比我们抛售 EURUSD 更便宜地买入, 但以不同的方式展现: (竞卖价) EURUSD < (竞买价) GBPUSD * (竞买价) EURGBP 
  2. 比我们买入 EURUSD 的更高价格抛售, 但以不同的方式展现: (竞买价) EURUSD > (竞卖价) GBPUSD * (竞卖价) EURGBP 

现在, 我们所要做的就是检测这种情况, 并从中获利。 赫兹股票量化软件

注意, 三角可以用另一种方式来移动, 这三个货币对在一个方向上移动, 并与 1 比较。所有变体都相同, 但我相信, 上面描述的变体更容易理解和解释。

通过形势跟踪, 我们可以寻找一个同时买入和卖出的时刻。在这种情况下, 会即时盈利, 但这样的时刻是罕见的。 赫兹股票量化软件
更常见的情况是, 当我们能够更便宜地买入一方时, 却无法在抛售另一方时盈利。那么我们只得等待这种不平衡消失。交易对我们来说是安全的, 因为我们的持仓相互抵消近乎为零, 意即我们游离于市场之外。虽然, 此处请注意 "近乎" 这个词。为了交易量的完美程度, 我们所需的精确度并未得到。交易量往往四舍五入到小数点后两位, 对于我们的策略来说这太粗糙了。

现在我们已经研究了这个理论, 现在是编写 EA 的时候了。EA 是以面向过程的风格开发的, 所以新入行的程序员, 以及那些因为某种原因不喜欢 OOP 的人都可以理解。 

简要的 EA 描述

首先, 我们创建所有可能的三角, 将它们正确放置, 并获得每个货币对的所有必要数据。

所有这些信息都存储在 MxThree 结构数组中。每个三角都有 status (状态) 字段。它的初始值是 0。如果需要三角开单, 状态设置为 1。确认三角完全开单后, 状态变为 2。如果三角形部分开单, 或者平单时间已到, 则状态变为 3。一旦三角成功平单, 状态将返回到 0。 赫兹股票量化软件

三角开单和平单均被保存到一个日志文件, 令我们能够检查动作的正确性并重温历史。日志文件名称为 Three Point Arbitrage Control YYYY.DD.MM.csv。

为了执行测试, 请将所有必要的货币对载入到测试器。为此, 在运行测试器之前, 在 "创建品种文件" 模式中启动 EA。如果不存在这样的文件, EA 将在默认的 EUR + GBP + USD 三角上运行测试。  赫兹股票量化软件

使用的变量

在我的开发过程中, 任何机器人的代码都是从包含头文件开始的。它会列出所有包含内容, 函数库, 等等。这个机器人也不例外: 说明模块之后紧随 #include "head.mqh" 等等:

#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>  
#include <Trade\TerminalInfo.mqh> 

#include "var.mqh"
#include "fnWarning.mqh"
#include "fnSetThree.mqh"
#include "fnSmbCheck.mqh"
#include "fnChangeThree.mqh"
#include "fnSmbLoad.mqh"
#include "fnCalcDelta.mqh"
#include "fnMagicGet.mqh"
#include "fnOpenCheck.mqh"
#include "fnCalcPL.mqh"
#include "fnCreateFileSymbols.mqh"
#include "fnControlFile.mqh"
#include "fnCloseThree.mqh"
#include "fnCloseCheck.mqh"
#include "fnCmnt.mqh"
#include "fnRestart.mqh"
#include "fnOpen.mqh"

此列表目前对您来说也许无法完全理解, 但本文会遵循这些代码, 因此程序结构在此并未被违反。往下一切都将变得清晰。所有函数, 类和代码单元都放在单独的文件中, 以方便使用。就我而言, 除了标准库之外, 每个包含文件也以 #include "head.mqh" 开头。允许在包含文件中使用 IntelliSense (智能感知), 因此不必在内存中保存所有必要实体的名称。 赫兹股票量化软件

之后, 为测试器连接文件。我们不能在任意地方进行这一步, 所以我们要在此声明。这个字符串是多币种测试器加载品种所需的:

#property tester_file FILENAME

接下来, 我们描述程序中使用的变量。描述可以在单独的 var.mqh 文件中找到:

// 宏定义
#define DEVIATION       3                                                                 // 最大可能的滑点
#define FILENAME        "Three Point Arbitrage.csv"                                       // 操作品种存储在这里
#define FILELOG         "Three Point Arbitrage Control "                                  // 日志文件名称部分
#define FILEOPENWRITE(nm)  FileOpen(nm,FILE_UNICODE|FILE_WRITE|FILE_SHARE_READ|FILE_CSV)  // 打开文件写入
#define FILEOPENREAD(nm)   FileOpen(nm,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV)   // 打开文件读取
#define CF              1.2                                                               // 提高保证金比例
#define MAGIC           200                                                               // 应用的魔幻数字范围
#define MAXTIMEWAIT     3                                                                 // 三角开单后的最长等待时间, 以秒为单位

// 货币对结构
struct stSmb
   {
      string            name;            // 货币对
      int               digits;          // 报价中的小数位数
      uchar             digits_lot;      // 手数的四舍五入小数位数
      int               Rpoint;          // 1/point, 以便在方程中乘以 (而不是除以)  该值
      double            dev;             // 可能的滑点。一次性转换成点数
      double            lot;             // 货币对的交易量
      double            lot_min;         // 最小交易量
      double            lot_max;         // 最大交易量
      double            lot_step;        // 手数增量
      double            contract;        // 合约大小
      double            price;           // 在三角中的货币对开单价。净持模式需要
      ulong             tkt;             // 交易开单所用的订单票号。对冲账户所需
      MqlTick           tick;            // 当前货币对价格
      double            tv;              // 当前分笔报价
      double            mrg;             // 当前用于开单的保证金
      double            sppoint;         // 点差, 单位为点数的整数值
      double            spcost;          // 当前开单的每手点差, 以资金为单位
      stSmb(){price=0;tkt=0;mrg=0;}   
   };

// 三角结构
struct stThree
   {
      stSmb             smb1;
      stSmb             smb2;
      stSmb             smb3;
      double            lot_min;          // 整个三角的最小交易量
      double            lot_max;          // 整个三角的最大交易量
      ulong             magic;            // 三角的魔幻数字
      uchar             status;           // 三角状态。0 - 未使用。1 - 发送开单。2 - 成功开单。3 - 发送平单
      double            pl;               // 三角盈利
      datetime          timeopen;         // 发送三角开单的时间
      double            PLBuy;            // 买入三角时的潜在利润
      double            PLSell;           // 抛售三角时的潜在利润
      double            spread;           // 所有三个点差的总价 (含佣金!)
      stThree(){status=0;magic=0;}
   };

  
// EA 操作模式  
enum enMode
   {
      STANDART_MODE  =  0, /*Symbols from Market Watch*/                  // 标准操作模式。市场观察品种
      USE_FILE       =  1, /*Symbols from file*/                          // 使用品种文件
      CREATE_FILE    =  2, /*Create file with symbols*/                   // 为测试器或操作创建文件
      //END_ADN_CLOSE  =  3, /*Not open, wait profit, close & exit*/      // 您的所有交易平单并结束操作
      //CLOSE_ONLY     =  4  /*Not open, not wait profit, close & exit*/
   };


stThree  MxThree[];           // 主数组存储正在操作的三角和所有必要的附加数据

CTrade         ctrade;        // 标准库的 CTrade 类
CSymbolInfo    csmb;          // 标准库的 CSymbolInfo 类
CTerminalInfo  cterm;         // 标准库的 CTerminalInfo 类

int         glAccountsType=0; // 账户类型: 对冲或净持
int         glFileLog=0;      // 日志文件句柄


// 输入

sinput      enMode      inMode=     0;          // 操作模式
input       double      inProfit=   0;          // 佣金
input       double      inLot=      1;          // 交易量
input       ushort	inMaxThree= 0;          // 三角已开单
sinput      ulong       inMagic=    300;        // EA 魔幻数字
sinput      string      inCmnt=     "R ";       // 注释

由于它们很简单并附有注释, 故先行定义。我相信, 它们很容易理解。

它们跟着两个结构 — stSmb 和 stThree。逻辑如下: 任何三角由三个货币对组成。因此, 一旦描述其一并使用三次之后, 我们得到一个三角。stSmb — 描述货币对的结构及其规格: 可能的交易量, _Digits 和 _Point 变量, 开单时的当前价格和一些其它值。在 stThree 结构当中, stSmb 使用了三次。这就是我们的三角的形成过程。此外, 还会添加一些与三角相关的属性 (当前利润, 魔幻数字, 开单时间等)。然后, 是我们将在稍后介绍的操作模式和输入变量。输入也在注释中说明了。我们要仔细看看其中两个: 赫兹股票量化软件

inMaxThree 参数中存储了可同时开单的最大三角可能数量。0 — 未用。例如, 如果参数设置为 2, 则不能有两个以上的三角同时开单。

inProfit 参数包含佣金值, 如果有的话。 赫兹股票量化软件

初始设置

在我们描述过包含文件和使用变量之后, 我们进入 OnInint() 模块。

在启动 EA 之前, 请务必检查输入参数的正确性, 并在必要时接收初始数据。如果一切顺利的话, 我们就开始吧。我通常在 EA 中设置尽可能少的输入量, 这个机器人也不例外。

六个输入中只有一个也许阻止 EA 操作, 这就是交易量。我们不能以负数交易量开单交易。所有其它设置不影响操作。这些检查在 OnInit() 模块函数中最先执行。

我们来看看它的代码。

void fnWarning(int &accounttype, double lot, int &fh)
   {   
      // 检查交易量, 不应该是负数
      if (lot<0)
      {
         Alert("交易量 < 0");  
         ExpertRemove();         
      }      
      
      // 如果为 0, 发出警告, 且机器人将使用尽可能低的交易量。
      if (lot==0) Alert("始终使用相同的最小交易量");  

由于机器人是以面向过程风格编写的, 所以我们必须创建几个全局变量。其中之一是日志文件句柄。该名称由一个固定部分和机器人开始日期组成 - 这是为了便于控制, 因此您不必在同一个文件中搜索特定日志的起始位置。请注意, 名称在每次重新启动时都会变更, 并删除前一个同名文件 (如果有的话)。

EA 在其操作中使用两个文件: 含有检测到三角的文件 (由用户自行决定), 和记录三角开单和平单时间的日志文件, 开单价格和一些方便控制的附加数据。日志记录始终处于活动状态。 赫兹股票量化软件

      // 仅在未选择三角文件创建模式时才创建日志文件。                                  
      if(inMode!=CREATE_FILE)
      {
         string name=FILELOG+TimeToString(TimeCurrent(),TIME_DATE)+".csv";      
         FileDelete(name);      
         fh=FILEOPENWRITE(name);
         if (fh==INVALID_HANDLE) Alert("日志文件未能创建");      
      }   
      
      // 通常, 货币对的经纪商合约大小= 100000, 但有时也有例外。
      // 然而, 这非常罕见, 在启动时很容易检查这个数值, 如果不是 10 万, 则报告,
      // 以便让用户自己决定重要与否。当三角中的货币对合约大小不同时 
      // EA 的处理未带有描述的时刻.
      for(int i=SymbolsTotal(true)-1;i>=0;i--)
      {
         string name=SymbolName(i,true);
         
         // 在形成三角时也要检查品种的可用性。
         // 我们会在稍后研究
         if(!fnSmbCheck(name)) continue;
         
         double cs=SymbolInfoDouble(name,SYMBOL_TRADE_CONTRACT_SIZE);
         if(cs!=100000) Alert("Attention: "+name+", contract size = "+DoubleToString(cs,0));      
      }
      
      // 获取账户类型, 对冲或净持
      accounttype=(int)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   }

形成三角

为了形成三角, 我们需要考虑以下几个方面:

  1. 数据来自市场观察窗口或预先准备的文件。
  2. 我们是否在测试器中?如果是的话, 则将品种上传到市场观察。上传所有可能的品种是没有意义的, 因为普通的家用电脑无法承受负载。搜索预先准备的包含测试器品种的文件。否则, 在标准三角: EUR + USD + GBP 上测试策略。
  3. 为了简化代码, 引入一个限制: 所有的三角品种应有相同的合约大小。
  4. 不要忘记, 三角只能以货币对构成。 赫兹股票量化软件

第一个必要的函数是利用来自市场观察的品种形成三角。

void fnGetThreeFromMarketWatch(stThree &MxSmb[])
   {
      // 获取品种总数
      int total=SymbolsTotal(true);
      
      // 用来比较合约大小的变量    
      double cs1=0,cs2=0;              
      
      // 使用第一次循环列表中的第一个品种
      for(int i=0;i<total-2 && !IsStopped();i++)    
      {//1
         string sm1=SymbolName(i,true);
         
         // 检查品种的各种限制
         if(!fnSmbCheck(sm1)) continue;      
              
         // 获取合约大小, 并将之常规化, 因为我们稍后会比较这个值 
         if (!SymbolInfoDouble(sm1,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue; 
         cs1=NormalizeDouble(cs1,0);
         
         // 获取基准货币和盈利货币, 因为它们要用来比较 (而非货币对名称)
         string sm1base=SymbolInfoString(sm1,SYMBOL_CURRENCY_BASE);     
         string sm1prft=SymbolInfoString(sm1,SYMBOL_CURRENCY_PROFIT);
         
         // 从第二次循环列表中取下一个品种
         for(int j=i+1;j<total-1 && !IsStopped();j++)
         {//2
            string sm2=SymbolName(j,true);
            if(!fnSmbCheck(sm2)) continue;
            if (!SymbolInfoDouble(sm2,SYMBOL_TRADE_CONTRACT_SIZE,cs2)) continue;
            cs2=NormalizeDouble(cs2,0);
            string sm2base=SymbolInfoString(sm2,SYMBOL_CURRENCY_BASE);
            string sm2prft=SymbolInfoString(sm2,SYMBOL_CURRENCY_PROFIT);
            // 在第一个和第二个货币对中应该有一种货币相匹配。
            // 否则, 它们不能形成一个三角。    
            // 进行全面的匹配测试没有意义。例如, 这是不可能的 
            // 形成 eurusd 和 eurusd.xxx 的三角.
            if(sm1base==sm2base || sm1base==sm2prft || sm1prft==sm2base || sm1prft==sm2prft); else continue;
                  
            // 合约应有相似的大小            
            if (cs1!=cs2) continue;
            
            // 搜索第三次循环中的最后一个三角品种
            for(int k=j+1;k<total && !IsStopped();k++)
            {//3
               string sm3=SymbolName(k,true);
               if(!fnSmbCheck(sm3)) continue;
               if (!SymbolInfoDouble(sm3,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue;
               cs1=NormalizeDouble(cs1,0);
               string sm3base=SymbolInfoString(sm3,SYMBOL_CURRENCY_BASE);
               string sm3prft=SymbolInfoString(sm3,SYMBOL_CURRENCY_PROFIT);
               
               // 我们知道第一个和第二个品种有一个共同的货币。若要形成一个三角, 我们应该找到
               // 第三个货币对内应有一种货币与第一个品种中的货币相匹配, 且其第二个货币匹配
               // 第二个品种中的货币如果没有匹配, 这个货币对不能用来形成一个三角。
               if(sm3base==sm1base || sm3base==sm1prft || sm3base==sm2base || sm3base==sm2prft);else continue;
               if(sm3prft==sm1base || sm3prft==sm1prft || sm3prft==sm2base || sm3prft==sm2prft);else continue;
               if (cs1!=cs2) continue;
               
               // 到达这个阶段, 意味着所有的检查都已经通过了, 且三个已检测货币对适于形成一个三角
               // 将其写入数组
               int cnt=ArraySize(MxSmb);
               ArrayResize(MxSmb,cnt+1);
               MxSmb[cnt].smb1.name=sm1;
               MxSmb[cnt].smb2.name=sm2;
               MxSmb[cnt].smb3.name=sm3;
               break;
            }//3
         }//2
      }//1    
   }

第二个必要的函数是从文件中读取三角

void fnGetThreeFromFile(stThree &MxSmb[])
   {
      // 如果没有找到含有品种的文件, 显示相应的消息并停止工作
      int fh=FileOpen(FILENAME,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV);
      if(fh==INVALID_HANDLE)
      {
         Print("未能读到品种文件!");
         ExpertRemove();
      }
      
      // 将指针移动到文件的开头
      FileSeek(fh,0,SEEK_SET);
      
      // 跳过标题行 (文件的第一行)      
      while(!FileIsLineEnding(fh)) FileReadString(fh);
      
      
      while(!FileIsEnding(fh) && !IsStopped())
      {
         // 得到三角的三个品种。我们来进行数据可用性的基本检查
         // 机器人能够自动形成三角文件。如果一位用户
         // 未能正确修改它, 我们假定这是故意的
         string smb1=FileReadString(fh);
         string smb2=FileReadString(fh);
         string smb3=FileReadString(fh);
         
         // 如果品种的数据可用, 在到达行尾后将它们写入我们的三角数组
         if (!csmb.Name(smb1) || !csmb.Name(smb2) || !csmb.Name(smb3)) {while(!FileIsLineEnding(fh)) FileReadString(fh);continue;}
         
         int cnt=ArraySize(MxSmb);
         ArrayResize(MxSmb,cnt+1);
         MxSmb[cnt].smb1.name=smb1;
         MxSmb[cnt].smb2.name=smb2;
         MxSmb[cnt].smb3.name=smb3;
         while(!FileIsLineEnding(fh)) FileReadString(fh);
      }
   }

本节所需的最后一个函数是前两个函数的包装。它负责根据 EA 输入来选择三角的来源。另外, 检查机器人的启动位置。如果在测试器当中, 无论用户选择什么, 都可以从文件中上传三角。如果没有文件, 下载默认的 EURUSD + GBPUSD + EURGBP 三角。 赫兹股票量化软件

void fnSetThree(stThree &MxSmb[],enMode mode)
   {
      // 重置我们的三角数组
      ArrayFree(MxSmb);
      
      // 检查我们是否在测试器中
      if((bool)MQLInfoInteger(MQL_TESTER))
      {
         // 如果是的话, 查找一个品种文件并从文件启动三角的上传
         if(FileIsExist(FILENAME)) fnGetThreeFromFile(MxSmb);
         
         // 如果未找到文件, 遍历所有可用品种查找其中默认的 EURUSD + GBPUSD + EURGBP 三角
         else{               
            char cnt=0;         
            for(int i=SymbolsTotal(false)-1;i>=0;i--)
            {
               string smb=SymbolName(i,false);
               if ((SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="GBP") ||
               (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD") ||
               (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="GBP" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD"))
               {
                  if (SymbolSelect(smb,true)) cnt++;
               }               
               else SymbolSelect(smb,false);
               if (cnt>=3) break;
            }  
            
            // 在市场观察中上载默认的三角之后, 启动三角         
            fnGetThreeFromMarketWatch(MxSmb);
         }
         return;
      }
      
      // 如果我们不在测试器当中, 查看用户选择的模式: 
      // 从市场观察或从文件中获取品种
      if(mode==STANDART_MODE || mode==CREATE_FILE) fnGetThreeFromMarketWatch(MxSmb);
      if(mode==USE_FILE) fnGetThreeFromFile(MxSmb);     
   }

此处我们使用一个辅助函数 — fnSmbCheck()。它检查所用品种是否有任何限制。若是, 则跳过。下面是它的代码。

bool fnSmbCheck(string smb)
   {
      // 三角只能由货币对组成
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX) return(false);
      
      // 如果有交易限制, 跳过此品种
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_FULL) return(false);   
      
      // 如果是合约的开始或结束, 也跳过该品种, 因为在处理货币时不使用该参数
      if(SymbolInfoInteger(smb,SYMBOL_START_TIME)!=0)return(false);
      if(SymbolInfoInteger(smb,SYMBOL_EXPIRATION_TIME)!=0) return(false);
      
      // 可用的订单类型。虽然机器人只进行市价订单交易, 但不应有限制
      int som=(int)SymbolInfoInteger(smb,SYMBOL_ORDER_MODE);
      if((SYMBOL_ORDER_MARKET&som)==SYMBOL_ORDER_MARKET); else return(false);
      if((SYMBOL_ORDER_LIMIT&som)==SYMBOL_ORDER_LIMIT); else return(false);
      if((SYMBOL_ORDER_STOP&som)==SYMBOL_ORDER_STOP); else return(false);
      if((SYMBOL_ORDER_STOP_LIMIT&som)==SYMBOL_ORDER_STOP_LIMIT); else return(false);
      if((SYMBOL_ORDER_SL&som)==SYMBOL_ORDER_SL); else return(false);
      if((SYMBOL_ORDER_TP&som)==SYMBOL_ORDER_TP); else return(false);
       
      // 为了数据可用性检查标准库         
      if(!csmb.Name(smb)) return(false);
      
      // 以下检查仅在实际操作中需要, 因为在某些情况下, 出于某些原因 SymbolInfoTick 接收价格 
      // 而竞卖价或竞买价依旧为 0。
      // 在测试器中禁用, 因为价格可能会在稍后出现。
      if(!(bool)MQLInfoInteger(MQL_TESTER))
      {
         MqlTick tk;      
         if(!SymbolInfoTick(smb,tk)) return(false);
         if(tk.ask<=0 ||  tk.bid<=0) return(false);      
      }

      return(true);
   }

所以, 三角就形成了。forming 函数置于 fnSetThree.mqh 包含文件中。检查品种限制的函数置于单独的 fnSmbCheck.mqh 文件中。

我们形成了所有可能的三角。它们当中的货币对可以按照任意顺序排列, 这会带来很多不便, 因为我们需要确定如何通过其它货币对来表示一个货币对。为了建立订单, 我们来研究使用 EUR-USD-GBP 所有可能的位置选项作为例子:

#品名 1品名 2品名 3
1EURUSD =GBPUSD хEURGBP
2EURUSD =EURGBP хGBPUSD
3GBPUSD =EURUSD /EURGBP
4GBPUSD =EURGBP 0EURUSD
5EURGBP =EURUSD /GBPUSD
6EURGBP =GBPUSD 0EURUSD

'x' = 乘以, '/' = 除以。'0' = 不可能动作

在上面的表格中, 我们可以看到, 三角可以用 6 种可能的方式来形成, 虽然其中的两个 — 第 4 行和第 6 行 — 不允许通过其余两个表示第一个品种。这意味着, 这些选项应该被丢弃。其余 4 个选项是相同的。无论我们想表达什么品种, 以及我们用什么品种来表达, 都无关紧要。唯一重要的是速度。除法比乘法慢, 因此选项 3 和 5 被丢弃。剩下的唯一选项是第 1 行和第 2 行。 赫兹股票量化软件

我们来研究方案 2, 因为它易于理解。因此, 我们不必为第一, 第二和第三个品种引入额外的字段。这是不可能的, 因为我们交易所有可能的三角而非单一的三角。

我们选择的便利性: 既然我们进行套利交易, 这个策略意味着一个中性的仓位, 我们应该买卖相同的资产。例如: 买入 0.7 手 EURUSD 并 抛售 0.7 手 EURGBP — 我们买卖 €70 000。因此, 我们有一笔仓位, 实际上我们已经游离在市场之外, 因为在买卖中 (虽然表达方式不同) 出现同样的数量。我们需要交易 GBPUSD 来调整它们。换句话说, 我们马上知道品种 1 和 2 应该有相似的交易量, 但方向不同。预先也知道, 第三对的交易量等于第二对的价格。

在三角中正确排列货币对的函数:

void fnChangeThree(stThree &MxSmb[])
   {
      int count=0;
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for         
         // 首先, 我们来确定第三个位置。 
         // 这一货币对的基准货币与其它两个基准货币不匹配
         string sm1base="",sm2base="",sm3base="";
         
         // 如果由于某种原因我们无法得到基准货币, 我们不会使用这个三角操作
         if(!SymbolInfoString(MxSmb[i].smb1.name,SYMBOL_CURRENCY_BASE,sm1base) ||
         !SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_BASE,sm2base) ||
         !SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE,sm3base)) {MxSmb[i].smb1.name="";continue;}
                  
         // 如果品种 1 和 2 的基准货币相同, 则跳过此步骤。否则, 交换货币对的位置
         if(sm1base!=sm2base)
         {         
            if(sm1base==sm3base)
            {
               string temp=MxSmb[i].smb2.name;
               MxSmb[i].smb2.name=MxSmb[i].smb3.name;
               MxSmb[i].smb3.name=temp;
            }
            
            if(sm2base==sm3base)
            {
               string temp=MxSmb[i].smb1.name;
               MxSmb[i].smb1.name=MxSmb[i].smb3.name;
               MxSmb[i].smb3.name=temp;
            }
         }
         
         // 现在, 我们来定义第一个和第二个位置。 
         // 第二个位置是与第三个基准货币匹配的利润货币对。 
         // 在这种情况下, 我们总是使用乘法。
         sm3base=SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE);
         string sm2prft=SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_PROFIT);
         
         // 交换第一和第二对的位置。 
         if(sm3base!=sm2prft)
         {
            string temp=MxSmb[i].smb1.name;
            MxSmb[i].smb1.name=MxSmb[i].smb2.name;
            MxSmb[i].smb2.name=temp;
         }
         
         // 显示已处理三角的消息。 
         Print("使用三角: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
         count++;
      }//
      // 通知操作中使用的三角总数。 
      Print("全部使用的三角: "+(string)count);
   }

该函数整个放在单独的 fnChangeThree.mqh 文件中。

完成三角准备所需的最后一步: 立即上传所用货币对的所有数据, 以便之后不必再花时间申请。我们需要以下:

  1. 每个品种的最小和最大交易量;
  2. 价格和交易量舍入的字符数;
  3. Point 和 Ticksize 变量。我从未遇到过它们不同时的情况。无论如何, 我们得到所有的数据, 并在必要时使用它们。
void fnSmbLoad(double lot,stThree &MxSmb[])
   {
      
      // 用来打印的简单宏定义   
      #define prnt(nm) {nm="";Print("不正确的上载: "+nm);continue;}
      
      // 循环遍历所有形成的三角。在此, 同一品种重复数据请求会过度消耗我们的时间 
      // 但由于这个操作只在加载机器人的时候执行, 所以为了减少代码, 我们仍然可以这样做。
      // 使用标准库来获取数据。 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // 通过上传品种到 CSymbolInfo 类, 我们初始化了所有必要数据的集合
         // 检查它们的可用性。如果出现问题, 三角标记为不可操作。                  
         if (!csmb.Name(MxSmb[i].smb1.name))    prnt(MxSmb[i].smb1.name); 
         
         // 获得每个品种的 _capacity
         MxSmb[i].smb1.digits=csmb.Digits();
         
         // 将滑点从整数转换为小数点。我们将需要这种格式进行进一步的计算
         MxSmb[i].smb1.dev=csmb.TickSize()*DEVIATION;         
         
         // 为了将报价转换为点数, 通常我们必须将价格除以 _Point 值。
         // 把这个值显示为 1/Point 是比较合理的, 这样我们就可以用乘法代替除法。 
         // 没有检查 csmb.Point() 是否为 0: 它不能等于 0, 但如果 
         //  由于某种原因没有收到参数, 三角按 if (!csmb.Name(MxSmb[i].smb1.name)) 这行排序。            
         MxSmb[i].smb1.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         
         // 我们要将手数舍入到小数位数。 
         MxSmb[i].smb1.digits_lot=csup.NumberCount(csmb.LotsStep());
         
         // 交易量限制 (一次性常规化)
         MxSmb[i].smb1.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb1.digits_lot);
         MxSmb[i].smb1.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb1.digits_lot);
         MxSmb[i].smb1.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb1.digits_lot); 
         
         // 合约大小 
         MxSmb[i].smb1.contract=csmb.ContractSize();
         
         // 同上, 但取自品种 2
         if (!csmb.Name(MxSmb[i].smb2.name))    prnt(MxSmb[i].smb2.name);
         MxSmb[i].smb2.digits=csmb.Digits();
         MxSmb[i].smb2.dev=csmb.TickSize()*DEVIATION;
         MxSmb[i].smb2.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         MxSmb[i].smb2.digits_lot=csup.NumberCount(csmb.LotsStep());
         MxSmb[i].smb2.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb2.digits_lot);
         MxSmb[i].smb2.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb2.digits_lot);
         MxSmb[i].smb2.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb2.digits_lot);         
         MxSmb[i].smb2.contract=csmb.ContractSize();
         
         // 同上, 但针对品种 3
         if (!csmb.Name(MxSmb[i].smb3.name))    prnt(MxSmb[i].smb3.name);
         MxSmb[i].smb3.digits=csmb.Digits();
         MxSmb[i].smb3.dev=csmb.TickSize()*DEVIATION;
         MxSmb[i].smb3.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         MxSmb[i].smb3.digits_lot=csup.NumberCount(csmb.LotsStep());
         MxSmb[i].smb3.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb3.digits_lot);
         MxSmb[i].smb3.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb3.digits_lot);
         MxSmb[i].smb3.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb3.digits_lot);           
         MxSmb[i].smb3.contract=csmb.ContractSize();   
         
         // 取齐交易量。对于 货币对和整个三角都有限制。
         // 货币对限制写在这里: MxSmb[i].smbN.lotN
         // 三角限制写在这里: MxSmb[i].lotN
         
         // 选择所有最低数值中的最高值。将它舍入到最大值。
         // 整个代码块仅适用于交易量大致如下的情况: 0.01 + 0.01 + 0.1。 
         // 在这种情况下, 尽可能少的交易量被设置为 0.1 并四舍五入到小数点后 1 位。
         double lt=MathMax(MxSmb[i].smb1.lot_min,MathMax(MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min));
         MxSmb[i].lot_min=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot)));
         
         // 另外, 最低交易量从最高交易量中取出并立即舍入。 
         lt=MathMin(MxSmb[i].smb1.lot_max,MathMin(MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max));
         MxSmb[i].lot_max=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot)));
         
         // 如果交易量输入参数为 0, 则使用尽可能少的交易量, 但并非取每对最少的,  
         // 而是所有对中最少的一个。 
         if (lot==0)
         {
            MxSmb[i].smb1.lot=MxSmb[i].lot_min;
            MxSmb[i].smb2.lot=MxSmb[i].lot_min;
            MxSmb[i].smb3.lot=MxSmb[i].lot_min;
         } else
         {
            // 如果您需要取齐交易量, 那么您知道货币对 1 和 2 的值, 而第三个交易量是在输入之前计算的。 
            MxSmb[i].smb1.lot=lot;  
            MxSmb[i].smb2.lot=lot;
            
            // 如果投入的交易量不在当前的限制范围内, 则三角不能在操作中使用。 
            // 使用警报通知这一点
            if (lot<MxSmb[i].smb1.lot_min || lot>MxSmb[i].smb1.lot_max || lot<MxSmb[i].smb2.lot_min || lot>MxSmb[i].smb2.lot_max) 
            {
               MxSmb[i].smb1.name="";
               Alert("三角: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - 交易量不正确");
               continue;
            }            
         }
      }
   }

函数可以在单独的 fnSmbLoad.mqh 文件中找到。

这就是有关形成三角的全部内容。我们继续前进。

EA 操作模式

启动机器人时, 我们可以选择一种可用的操作模式:

  1. 来自市场观察的品名。
  2. 来自文件的品名。
  3. 用品名创建文件。

"来自市场观察的品名" 意味着我们在当前品种上启动机器人, 并从市场观察窗口形成操作的三角。这是主要的操作模式, 不需要额外的处理。

"来自文件的品名" 不同于第一个仅从三角获得来源 — 从以前准备好的文件。

"用品名创建文件" 创建一个三角文件, 以备将来在第二种操作模式或测试器中使用。这种模式只假定形成三角。之后, EA 操作完成。

我们来描述一下这个逻辑:

      if(inMode==CREATE_FILE)
      {
         // 删除文件, 如果它存在。
         FileDelete(FILENAME);  
         int fh=FILEOPENWRITE(FILENAME);
         if (fh==INVALID_HANDLE) 
         {
            Alert("品种文件未创建");
            ExpertRemove();
         }
         // 将三角和一些其它数据写入文件
         fnCreateFileSymbols(MxThree,fh);
         Print("品种文件已创建");
         
         // 关闭文件并完成 EA 操作
         FileClose(fh);
         ExpertRemove();
      }

将数据写入文件的函数很简单, 不需要额外的注释:

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 || 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)); else continue;                                                  // 我们已经检测到这个三角。现在, 我们为其分配适当的状态                         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() 函数负责此任务:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值