三角裁定

Alexey Oreshkin | 15 12月, 2017


概念

三角裁定取引に特化したトピックは尽きることなくフォーラムに掲載されていますが、三角裁定取引とは正確に言うと何なのでしょうか。

「裁定取引」は市場に対する中立性を意味し、「三角」はポートフォリオが3つの製品で構成されていることを意味します。

最も一般的な例を取ってみましょう。 "EUR — GBP — USD"三角は、EURUSD+GBPUSD+EURGBPとして通貨ペアで記述することができます。必要とされる中立性は、利益を得ながら同じ製品を同時に売買しようとする試みにあります。 

つまり、この例のペアはすべて、他の2つで表されます。

EURUSD=GBPUSD*EURGBP

またはGBPUSD=EURUSD/EURGBP

またはEURGBP=EURUSD/GBPUSD

これらの変種はすべて同一であり、どれを選択するかについては以下でより詳細に説明します。その間に、最初のオプションについて考えてみましょう。

まず、買呼値と売呼値を見る必要があります。手順は次のとおりです。

  1. EURUSDを買います(売呼値を使います)。つまり、USDを取り除きながらバランスにEURを追加します。 
  2. 他の2つのペアを使ってEURUSDを評価します。
  3. GBPUSD: EURはありませんが、USDがあってこれは売るべきです。GBPUSDでUSDを売るには、このペアを買います(売呼値を使います)。買うときには、USDを取り除きながらバランスにGBPを追加します。
  4. EURGBP: EURを買い、必要のないGBPを売るべきです。EURGBPを買います(売呼値を使います)。GBPを取り除きながらバランスにEURを追加します。

全体的にみると(売呼値)EURUSD =(売呼値)GBPUSD *(売呼値)EURGBPです。これで必要なバランスが取れました。それを利益を得るために使用するには、片側を買い、もう片側を売るべきで、次の2つのオプションがあります。

  1. EURUSDを売れるよりも安く買います。言い換えれば(売呼値)EURUSD <(買呼値)GBPUSD *(売呼値) EURGBPです。
  2. EURUSDを買えるよりも安く売ります。言い換えれば(買呼値)EURUSD >(売呼値)GBPUSD *(売呼値) EURGBPです。

ここでしなければならないのは、このような場合を検出して利益を得ることだけです。

三角形は、3つのペアをすべて1方向に移動して1と比較するという別の方法でも作成できます。すべての変種は同じですが、上記の方が理解するのも説明するのも簡単なはずです。

状況を追跡することで、同時に売買するための瞬間を探すことができます。この場合、収益は瞬時になりますが、そのような瞬間はまれです。
より一般的なのは、片側を安く買うことができても現在は利益を持って売ることができない状態です。その場合、このアンバランスが消えるのを待ちます。私たちのポジションはほぼゼロである(市場を離れている)ので、取引に参加することは安全です。しかし、ここでは「ほとんど」という言葉に注意してください。取引量を完全に平準化するための精度は手に入りません。取引量は、ほとんどの場合、小数点第2位まで四捨五入されているので、その精度はこの戦略には十分ではありません

さて、理論を考察したので、EAを書く時です。EAは手続き型スタイルで開発されているので、初心者プログラマの方や何らかの理由でOOPが嫌いな方にも理解できるはずです。 


EAの簡単な説明

まず、すべての可能な三角形を作成して正しく配置し、各通貨ペアに必要なすべてのデータを取得します。

これらの情報はすべてMxThree構造体の配列に格納されます。各三角形にはstatusフィールドがあって、初期値は0です。ステータスは三角形を開く必要がある場合には1に設定され、三角形が完全に開いたことを確認した後に2に変わります。三角形が部分的に開いている場合、または三角形を閉じるときには、ステータスは3に変わります。三角形が正常に閉じられると、ステータスは0に戻ります。

三角形の閉開めはログファイルに保存され、アクションの正確性の確認と履歴の復元が可能になります。ログファイル名はThree Point Arbitrage Control YYYY.DD.MM.csvです。

テストを実行するには、必要なすべての通貨ペアをテスターにアップロードします。これを行うには、テスターを実行する前に、 "Create file with symbols"(銘柄付きファイルの作成)モードで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 /点、方程式で除算の代わりにこの値によって乗算する
      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;           // 3つのスプレッドの合計価格(手数料あり!)
      stThree(){status=0;magic=0;}
   };

  
// EA操作モード  
enum enMode
   {
      STANDART_MODE  =  0, /* 気配値表示からの銘柄 */                  // 標準操作モード気配値表示の銘柄
      USE_FILE       =  1, /* ファイルからの銘柄 */                          // 銘柄ファイルを使う
      CREATE_FILE    =  2, /* 銘柄を持つファイルを作成する */                   // テスターまたは作業用のファイルを作成する
      //END_ADN_CLOSE  =  3, /* 開かず、利益を待ち、閉じてエグジットする */      // 取引をすべて閉じて作業を終了する
      //CLOSE_ONLY     =  4  /* 開かず、利益を待たず、閉じてエグジットする */
   };


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 ";       // コメント

定義は簡単でコメントが付いているので初めにまわしますわかりやすいと思います。

stSmbstThreeの2つの構造体が続きます。論理は以下の通りです。三角形は3つの通貨ペアで構成されます。したがって、それらの1つを一度記述して3回使用すると、三角形が得られます。stSmbは、通貨ペアとその仕様(可能な取引量、_Digits及び_Point変数、始値など)を記述する構造体です。stThree構造体ではstSmbが3回使われます。これが三角形の形成方法です。また、三角形に関するいくつかのプロパティ(現在の利益、マジックナンバー、開く時刻など)がここに追加されます。次に、後述する動作モードと入力変数があります。入力はコメントにも記載されています。それらの2つを詳しく見ていきます。

inMaxThreeパラメータは、同時に開かれる三角形の最大許容数を格納します。0は未使用を示します。たとえば、パラメータが2に設定されている場合、同時に開くことができる三角形は最大2つです。

inProfitパラメータには手数料の値が含まれています(存在する場合)。


初期設定

インクルードファイルと使用変数について説明したのでOnInint()ブロックに進みましょう。

EAを起動する前に、入力したパラメータの正しさを確認し、必要に応じて初期データを受け取ります。これがうまくいたら、始めましょう。私は通常、EAに入力できる量をできるだけ少なく設定しているので、このロボットでもそうします。

6つの入力のうちEAの動作を妨げる可能性があるのは1つだけで、これは取引量です。負の数量で取引を開くことはできません。他のすべての設定は操作に影響しません。確認は最初のOnInit()ブロック機能で実行されます。

そのコードを見てみましょう。

void fnWarning(int &accounttype, double lot, int &fh)
   {   
      // 取引量は負であってはいけないので確認するhyjn
      if (lot<0)
      {
         Alert("Trade volume < 0");  
         ExpertRemove();         
      }      
      
      // 0の場合は、ロボットが最小限の取引量を使用することを警告する
      if (lot==0) Alert("Always use the same minimum trading volume");  

ロボットは手続き型で書かれているので、いくつかのグローバル変数を作成する必要があります。それらの1つはログファイルハンドルです。名前は固定部分とロボットの開始日で構成されています。これは制御を容易にするために行われているため、同じファイル内の特定の開始点のログがどこで開始されるのかは検索しません。名前は再起動のたびに変わり、同じ名前の前のファイルがあれば削除されます。

EAの作業には2つのファイルが使用されます。検出された三角形(ユーザの裁量で作成されたもの)を持つファイルと、三角の開閉時間が書き込まれたログファイル、始値、および制御の容易さのためのいくつかの追加データを持つファイルです。ログ付けは常にアクティブです。

      // 三角形ファイル作成モードが選択されていない場合にのみ、ログファイルを作成する                                  
      if(inMode!=CREATE_FILE)
      {
         string name=FILELOG+TimeToString(TimeCurrent(),TIME_DATE)+".csv";      
         FileDelete(name);      
         fh=FILEOPENWRITE(name);
         if (fh==INVALID_HANDLE) Alert("The log file is not created");      
      }   
      
      // ブローカーの通貨ペアの契約サイズは一般的には100000だが、時に例外がある
      // ただし、例外は非常に稀なので、起動時にこの値を確認して100000でない場合は報告するほうが簡単である
      // ユーザが自分のためにこれが重要かどうかを決める  
      // 三角形が異なる契約サイズを有するペアを有する場合、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. テスター内にいる場合は、気配値表示に銘柄をアップロードします。通常の家庭用PCは負荷に対処できないため、可能なものすべてをアップロードする理由はありません。事前に用意されたテスター銘柄を含むファイルを検索します。または、標準的な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);
         
         // 2番目のループのリストから次の銘柄を取得する
         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);
            // 最初のペアと2番目のペアは、いずれかの通貨に対して1つの一致を持つ必要がある
            // そうでなければ、三角形を作成できないので    
            // 完全一致テストを実行する意味はない  
            // 例えば、eurusdとeurusd.xxxの三角形を形成するのは不可能である
            if(sm1base==sm2base || sm1base==sm2prft || sm1prft==sm2base || sm1prft==sm2prft); else continue;
                  
            // 契約は同じサイズでなければならない            
            if (cs1!=cs2) continue;
            
            // 3番目のループの最後の三角形銘柄を検索する
            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);
               
               // 1番目と2番目の銘柄の通貨が共通していることはわかっている 
               //三角形を形成するには、3番目の通貨が1番目の通貨と一致し、その2番目の通貨が2番目の任意の通貨と一致するペアを見つけるべきである
               // 一致が見つからない場合は、ペアは三角形の形成には使用できない
               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;
               
               // この段階に達することは、すべての条件が満たされ、3つの検出された対が三角形を形成するのに適していることを意味する
               // 配列に書き入れる
               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    
   }

2番目に必要な関数は、ファイルから三角形を読み込むためのものです。

void fnGetThreeFromFile(stThree &MxSmb[])
   {
      // 銘柄付きのファイルが見つからない場合は、適切なメッセージを表示して動作を停止する
      int fh=FileOpen(FILENAME,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV);
      if(fh==INVALID_HANDLE)
      {
         Print("File with symbols not read!");
         ExpertRemove();
      }
      
      // キャリッジをファイルの先頭に移動する
      FileSeek(fh,0,SEEK_SET);
      
      // ヘッダー(ファイルの最初の行)を抜かす      
      while(!FileIsLineEnding(fh)) FileReadString(fh);
      
      
      while(!FileIsEnding(fh) && !IsStopped())
      {
         // 3つの三角形銘柄を取得し、データ可用性を基本的に確認する
         // ロボットは自動的に三角形でファイルを形成することができる 
         // ユーザが間違って変更した場合、これは意図的に行われたものとみなされる
         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);
      }
   }

このセクションで必要な最後の関数は、前の2つの関数のラッパーです。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);
   }

これで、三角形が形成されます。形成関数はfnSetThree.mqhインクルードファイルに配置されていますが、銘柄の制限を確認する関数は別の fnSmbCheck.mqhファイルに配置されています。

可能なすべての三角形を形成しました。それらのペアは任意の順序で並べ替えることができます。これによって、ある通貨ペアを他の通貨ペアでどのように表現するかを決定する必要が生じるため、多くの不便さがもたらされます。注文を確定するには、例としてEUR-USD-GBPを使用して可能なすべてのロケーションオプションを検討してみましょう。

# 銘柄1 銘柄2
銘柄3
1 EURUSD = GBPUSD  х EURGBP
2 EURUSD = EURGBP  х GBPUSD
3 GBPUSD = EURUSD  / EURGBP
4 GBPUSD = EURGBP  0 EURUSD
5 EURGBP = EURUSD  / GBPUSD
6 EURGBP = GBPUSD  0 EURUSD

'x' = 乗算、'/' = 除算'0' = アクションが取れない

上の表では三角形が6つの可能な方法で形成されていることがわかりますが、そのうちの2つ(4行目と6行目)は残りの2つの銘柄を通して最初の銘柄を表現できません。つまり、これらのオプションは破棄する必要があります。残りの4つのオプションは同じです。どの銘柄を表現したいのか、それをどのような銘柄で表現するのかは問題ではありません。ここで重要なのはスピードだけです。除算は乗算よりも遅いため、オプション3と5は破棄されます。残されたオプションは、行1と2です。

オプション2は認識が容易なので考慮します。したがって、1番目、2番目および3番目の銘柄に追加の入力フィールドを導入する必要はありません。単一のものではなくすべての可能な三角形を取引するので、これは不可能です。

この選択の利便性:裁定取引をするためこの戦略は中立的な立場を意味するので、同じ資産を売買しなければなりません。例: EURUSD0.7ロット買いEURGBP 0.7 ロット売ると、€70 000を売買したことになります。したがって、売りと買いの両方で同じ数量が存在するので(市場からエグジットしているという事実にもかかわらず)、ポジションを持っていることになります。これをGBPUSDの取引によって調整します。言い換えれば、銘柄1と銘柄2の取引量は同じで、方向が異なることをすぐにわかります。3番目のペアの数量が2番目のペアの価格と等しいことも事前に知られています。

下記は、ペアを三角形に正しく配置する関数です。

void fnChangeThree(stThree &MxSmb[])
   {
      int count=0;
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for         
         // まず、3番目の位置に何があるのかを見る 
         // これは、2つの他の基本通貨と一致しない基本通貨を持つペアである
         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;
            }
         }
         
         // ここで1番目と2番目を定義する 
         // 2番目は3番目の基本通貨と一致する収益通貨でペアを作る 
         // この場合は常にマルチプラットフォームを使用する
         sm3base=SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE);
         string sm2prft=SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_PROFIT);
         
         // 1番目と2番目のペアの位置を入れ替える 
         if(sm3base!=sm2prft)
         {
            string temp=MxSmb[i].smb1.name;
            MxSmb[i].smb1.name=MxSmb[i].smb2.name;
            MxSmb[i].smb2.name=temp;
         }
         
         // 処理された三角形のメッセージを表示する 
         Print("Use triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
         count++;
      }//
      // 作業に使用される三角形の総量の情報 
      Print("All used triangles: "+(string)count);
   }

関数はfnChangeThree.mqhファイルに全体的に配置されています。

三角形の準備を完了するために必要な最後のステップは、後でそれらを適用する時間を費やす必要がないように、使用されたペアのすべてのデータをすぐにアップロードすることです。下記が必要です。

  1. 各銘柄の最小取引量と最大取引量
  2. 丸めるのに使われる価格と取引量の桁数
  3. Point 及びTicksize変数。私は、これらが異なっていた状況に遭遇したことはありません。とにかく、すべてのデータを取得し、必要な場所で使用します。
void fnSmbLoad(double lot,stThree &MxSmb[])
   {
      
      // 簡単な印字マクロ   
      #define prnt(nm) {nm="";Print("NOT CORRECT LOAD: "+nm);continue;}
      
      // 形成された三角形のすべてを反復処理する。ここでは、同じ銘柄に対するデータ要求が繰り返されると、時間が過度に消費されるが、 
      // この操作はロボットの読み込み時にのみ実行されるため、コードの削減のために行うこともできる
      // 標準ライブラリを使用してデータを取得する 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // 銘柄をCSymbolInfoクラスにアップロードすることによって、可用性を確認しながら必要なすべてのデータのコレクションを初期化する
         // 何か問題があれば、三角形は非操作としてマークされる                   
         if (!csmb.Name(MxSmb[i].smb1.name))    prnt(MxSmb[i].smb1.name); 
         
         // 銘柄ごとのGet _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の値が分かっているが、3番目の取引量値はエントリの直前で計算される 
            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("Triangle: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - not correct the trading volume");
               continue;
            }            
         }
      }
   }

この関数は別のfnSmbLoad.mqhファイルにあります。

三角形の形成についてはこれで終わりです。次へ移りましょう。


EA操作モード

ロボットを起動するときには、使用可能な操作モードの1つを選択できます。
  1. 気配値表示の銘柄
  2. ファイルの銘柄
  3. 銘柄ファイルを作成

「気配値表示の銘柄」とは現在の銘柄でロボットを起動し、気配値表示ウィンドウから作業三角形を形成することを意味します。これは主な操作モードであり、追加の処理は必要ありません。

「ファイルの銘柄」は、前もって用意したファイルから三角形を取得する点だけで最初のものと異なります。

「銘柄ファイルを作成」は、将来、2番目の操作モードまたはテスターで使用される三角形のファイルを作成します。このモードでは、三角形の形成のみが前提とされEA操作はそれで完成です。

この論理について説明します。

      if(inMode==CREATE_FILE)
      {
         // 存在する場合ファイルを削除する
         FileDelete(FILENAME);  
         int fh=FILEOPENWRITE(FILENAME);
         if (fh==INVALID_HANDLE) 
         {
            Alert("File with symbols not created");
            ExpertRemove();
         }
         // 三角形といくつかの追加データをファイルに書き込む
         fnCreateFileSymbols(MxThree,fh);
         Print("File with symbols created");
         
         // ファイルを閉じてEA操作を完了する
         FileClose(fh);
         ExpertRemove();
      }

ファイルにデータを書き込む機能は簡単で、追加のコメントは必要ありません。

void fnCreateFileSymbols(stThree &MxSmb[], int filehandle)
   {
      // ファイルにヘッダを定義する
      FileWrite(filehandle,"Symbol 1","Symbol 2","Symbol 3","Contract Size 1","Contract Size 2","Contract Size 3",
      "Lot min 1","Lot min 2","Lot min 3","Lot max 1","Lot max 2","Lot max 3","Lot step 1","Lot step 2","Lot step 3",
      "Common min lot","Common max lot","Digits 1","Digits 2","Digits 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の初期設定はほぼ完了しました。しかし、依然としてクラッシュ後の回復をどう扱うかという質問があります。インターネット接続が短期的に失なわれることについては心配する必要はありません。ロボットはwebに再接続した後、操作を再開します。しかし、ロボットを再起動する場合は、ポジションを見つけて作業を再開する必要があります。

以下は、ロボットの再起動の問題を解決する関数です。

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:
            // ポジションをすべて調べて、マジックナンバーの一致を検出する 
            // 最初に検出されたポジションのマジックナンバーを記憶して、それを使って他の2つのポジションを検出する 

            
            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);
               
               // 同じマジックナンバーを持つ2番目のポジションを探す 
               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; 
                        
                        // これは「鈍く」行われている 
                        //  一見、同じ通貨ペアを何度も参照できるようだが 
                        //  別の通貨ペアを検出した後、検索はループの最初ではなく次のペアから再開するため、真実は異なる

                        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() 関数はこれを実行します。

      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; 
         
         // すべての3つのペアの可用性を再確認する
         //  少なくとも1つが使用できない場合は、三角形全体を計算する意味がない
         if (!fnSmbCheck(MxSmb[i].smb1.name)) continue;  
         if (!fnSmbCheck(MxSmb[i].smb2.name)) continue;  // 取引がペアの1つで閉じられる
         if (!fnSmbCheck(MxSmb[i].smb3.name)) continue;     
         
         // 各ティックの開始時に開いている三角形の数を計算するが
         // 三角形は言うまでもなくティック内で開くこともできるので継続的に数を追跡する
         if (lcMaxThree>0) {if (lcMaxThree>lcOpenThree); else continue;}     

         
         // その後、計算に必要なすべてのデータを取得し. 
         
         // 各ペアごとにティック価格を取得する
         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;
         
         // 3番目のペアの取引量を計算する。最初の2つのペアの取引量はわかっている(それらは同じで固定されている)
         // 3番目のペアの取引量は常に変化するが、初期変数でロットが0でない場合にのみ計算される
         // ロットがゼロの場合、最小(またはそれに似た)取引量が使用される
         // 取引量計算ロジックは単純である。EURUSD=EURGBP*GBPUSD三角形では、売買されたGBPの数は
         // EURGBPの見積もりに直接依存するが、3番目のペアでは、この3番目の通貨が最初に来る 
         // 2番目のペアの価格をボリュームとして使用して、いくつかの計算を取り除いた。ここでは売り呼び値と買い呼び値の平均を取った。
         // 入力取引量の訂正を忘れてはいけない.
         
         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.lot<MxSmb[i].smb3.lot_min || MxSmb[i].smb3.lot>MxSmb[i].smb3.lot_max)
         {
            Alert("The calculated lot for ",MxSmb[i].smb3.name," is out of range. 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("Triangle: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - DISABLED");
            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の買い呼び値である。このようなケースは数多くあるが、これでは成功するには不十分である
         // スプレッドコストを計算する。売り呼び値 < 買い呼び値の時に市場には機械的に参入するのではなく、
         // それらの差がスプレッドコストを超えるまで待つべきである          
         
         // 「買い」が最初の銘柄を買って残りの2つを売ることで
         // 「売り」が最初の銘柄を売って残りの2つを買うことであることに同意しよう
         
         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買い呼び値)にあるように、2番目と3番目銘柄価格を乗算する: 
         //    (MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)
         // 3. その後、売り呼び値と買い呼び値の差を計算する
         // 4. ポイント単位での差を通貨単位に変換する。これにはポイントの価格と取引量を乗算する 
         // これには1番目のペアの値を使う  
         // すべてのペアを片側に配置して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;
         
         // 三角形を売買することで得られるまたは失われる金額の計算を受け取ったので、 
         // 取引に入るかどうかを決めるべきかどうかを判断するためにコストと比較するだけである。すべてを小数点以下2桁まで正規化する
         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))  // 余剰証拠金を確認する
            {
               // 開く準備がほとんど整ったので、今度はここでの範囲から空いているマジックナンバーを見つける 
               // 初期のマジックはinMagic変数で指定されており、デフォルト値は300である 
               // マジックナンバーの範囲はMAGIC定義で指定されており、デフォルト値は200である
               MxSmb[i].magic=fnMagicGet(MxSmb,magic);   
               if (MxSmb[i].magic<=0)
               { // 0の場合、マジックナンバーはすべて使われているので、これをメッセージで通知して終了する
                  Print("Free magic ended\nNew triangles will not open");
                  break;
               }  
               
               // 検知されたマジックナンバーを設定する
               ctrade.SetExpertMagicNumber(MxSmb[i].magic); 
               
               // 三角形のコメントを書く
               cmnt_pos=cmnt+(string)MxSmb[i].magic+" Open";               
               
               // 三角形が開くために送信された時刻を記憶しながら開く 
               // これは待つのを避けるために必要である 
               // デフォルトでは、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("Open triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" magic: "+(string)MxSmb[i].magic);
            }
         }         
      }//for i
   }

この関数には、明確化のために詳細なコメントと説明が添付されていますが、適用された利用可能なマジックナンバーを選択するメカニズムと三角形のオープニングの2つが説明されずに残っています。

以下は、利用可能なマジックナンバーを選択する方法です。

ulong fnMagicGet(stThree &MxSmb[],ulong magic)
   {
      int mxsize=ArraySize(MxSmb);
      bool find;
      
      // 配列内のすべての空白の三角形を処理することもできるが、ここでは 
      // マジックナンバーの範囲を調べて選択されたものを配列内で移動するという
      // 別の方法を選択した 
      for(ulong i=magic;i<magic+MAGIC;i++)
      {
         find=false;
         
         // iはマジックナンバーなので、開いている三角形のいずれかに割り当てられているかどうかを確認する
         for(int j=0;j<mxsize;j++)
         if (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:
         
         // 注文が出された後に'false'が返された場合、残りの2ペアを開くために送信する必要がない 
         // 新しいティックで試みる。また、ロボットは部分的に三角形を開くことがない 
         // 注文を送信した後に一部が開かれない場合は、
         // 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の作業はこれで実際に終わります。しかし、ここではまだ三角形を開いた結果を追跡する必要があります。結果が成功したとは保証されていないので、これにはOnTrade及びOnTradeTransactionイベントは使用しません。代わりに、現在のポジションの数を調べます。 これは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("Not correct open: "+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);}                               // 3つの必要なポジションがある場合、三角形は正常に開かれているので、ステータスを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,"Open:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
      FileWrite(fh,"Tiket:",MxSmb[i].smb1.tkt,MxSmb[i].smb2.tkt,MxSmb[i].smb3.tkt);
      FileWrite(fh,"Lot",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,"Ask",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,"Bid",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,"Price open",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,"Tick value",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,"Spread point",DoubleToString(MxSmb[i].smb1.sppoint,0),DoubleToString(MxSmb[i].smb2.sppoint,0),DoubleToString(MxSmb[i].smb3.sppoint,0));
      FileWrite(fh,"Spread $",DoubleToString(MxSmb[i].smb1.spcost,3),DoubleToString(MxSmb[i].smb2.spcost,3),DoubleToString(MxSmb[i].smb3.spcost,3));
      FileWrite(fh,"Spread all",DoubleToString(MxSmb[i].spread,3));
      FileWrite(fh,"PL Buy",DoubleToString(MxSmb[i].PLBuy,3));
      FileWrite(fh,"PL Sell",DoubleToString(MxSmb[i].PLSell,3));      
      FileWrite(fh,"Magic",string(MxSmb[i].magic));
      FileWrite(fh,"Time open",TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_SECONDS));
      FileWrite(fh,"Time current",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); else continue;                             
         
         // 三角形の利益を計算する 
         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
            
            // 小数点以下2桁まで切り上げる
            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))          
      
      // すべてが利用可能な場合は、標準ライブラリを使用して3つのポジションをすべて決済する 
      // 決済後は、アクションが成功したかどうかを確認する 
      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;
      }       
   }  

作業はほぼ完了です。終了が成功したかどうかを確認して、メッセージを画面に表示するだけです。何も表示しないと、ロボットが動作してないようにみえるでしょう。

以下は、終了が成功したかどうかの確認です。取引方向を変えて開閉のする関数を1つ実装することもできますが、この2つの関数にわずかな手続きの違いがあるため、このオプションには気が引けます。 

終了が成功したかどうかを確認します。 

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: 
            
            // 三角形からペアが1つも選択できない場合、終了は成功しているので、ステータス0に戻る
            if (!PositionSelectByTicket(MxSmb[i].smb1.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb2.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb3.tkt))
            {  // 終了が成功した
               MxSmb[i].status=0;   
               
               Print("Close triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" magic: "+(string)MxSmb[i].magic+"  P/L: "+DoubleToString(MxSmb[i].pl,2));
               
               // 終了データをログファイルに書く 
               if (fh!=INVALID_HANDLE)
               {
                  FileWrite(fh,"============");
                  FileWrite(fh,"Close:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
                  FileWrite(fh,"Lot",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,"Tiket",string(MxSmb[i].smb1.tkt),string(MxSmb[i].smb2.tkt),string(MxSmb[i].smb3.tkt));
                  FileWrite(fh,"Magic",string(MxSmb[i].magic));
                  FileWrite(fh,"Profit",DoubleToString(MxSmb[i].pl,3));
                  FileWrite(fh,"Time current",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS));
                  FileFlush(fh);               
               }                   
            }                  
            break;
         }            
      }      
   }

最後に、視覚的な確認のために画面にコメントを表示しましょう。以下を表示します。

  1. 追跡された三角形の総数
  2. 開いている三角形
  3. 最も近い開くための5つの三角形
  4. もしあれば開いている三角形

関数コードは次のとおりです。

void fnCmnt(stThree &MxSmb[], ushort lcOpenThree)
   {     
      int total=ArraySize(MxSmb);
      
      string line="=============================\n";
      string txt=line+MQLInfoString(MQL_PROGRAM_NAME)+": ON\n";
      txt=txt+"Total triangles: "+(string)total+"\n";
      txt=txt+"Open triangles: "+(string)lcOpenThree+"\n"+line;
      
      // 画面に表示される三角形の最大数
      short max=5;
      max=(short)MathMin(total,max);
      
      // 最も近い開くための5つの三角形を表示する 
      short index[];                    // インデックスの配列
      ArrayResize(index,max);
      ArrayInitialize(index,-1);        // 未使用
      short cnt=0,num=0;
      while(cnt<max && num<total)       // 最初の最大の閉じた三角形のインデックスが開始時に取得される
      {
         if(MxSmb[num].status!=0)  {num++;continue;}
         index[cnt]=num;
         num++;cnt++;         
      }
      
      // 並び替えと検索は要素数が画面に表示できる数を超えている場合にのみ意味がある 
      if (total>max) 
      for(short i=max;i<total;i++)
      {
         // 開いている三角形は下に表示される
         if(MxSmb[i].status!=0) continue;
         
         for(short j=0;j<max;j++)
         {
            if (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;i<max;i++)
      {
         cnt=index[i];
         if (cnt<0) continue;
         if (flag)
         {
            txt=txt+"Smb1           Smb2           Smb3         P/L Buy        P/L Sell        Spread\n";
            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)+"\n";      
      }            
      
      // 開いている三角形を表示する 
      txt=txt+line+"\n";
      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+"  Time open: "+TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
         txt=txt+"\n";
      }   
      Comment(txt);
   }


テスト


テストをティックシミュレーションモードで行い、実際のティックでのテストと比較することが可能です。さらにライブアクションを持った実際のティックでのテスト結果を比較すると、マルチテスターはまだ現実とは遠いと結論づけることができます。 

結果は、平均して1週間に3-4の取引に頼ることができることを示しています。ほとんどの場合、夜にポジションが開かれ、通常、TRY、NOK、SEKなどの低流動性の通貨が表示されます。ロボットの利益は、取引されるボリュームに依存します。取引がまれであるため、EAは他のロボットと並行して動作して、大容量を容易に処理できます。

ロボットのリスクは計算が簡単です(3つのスプレッド * 開いている三角形の数)。

扱うことができる通貨ペアを準備するには、最初にすべての銘柄を表示し、取引を無効にしたものと通貨ペアでないものを隠すことをお勧めします。複数通貨戦略のファンにとって不可欠なスクリプト(https://www.mql5.com/en/market/product/25256)を使用すると、これはより迅速に実行できます。

また、テスターの履歴はブローカーのサーバーからアップロードされていないので、事前にクライアント端末にアップロードするべきなので、覚えておくべきです。これはテスト前に、単独で、または上記のスクリプトを再度利用して、実行する必要があります。


開発の見通し

結果は改善できるでしょうか。はい、もちろんです。改善には、流動資産アグリゲータを作らなければなりません。このアプローチの欠点は、複数のブローカーで口座を開設する必要があることです。

テスト結果をスピードアップすることもできます。これは、組み合わせることができる2つの方法で行うことができます。第1のステップは、エントリ確率が非常に高い三角形を常に追跡する離散的な計算を導入することです。2番目の方法は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 現在の三角形に使用できるマジックナンバーを検索します。
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 追加サポートクラスです。分数の小数点以下を数える関数の1つだけです。
19 head.mqh 上記のすべてのファイルのヘッダを記述します。