クロスプラットフォームグラインドEAの開発

Roman Klymenko | 13 6月, 2019

イントロダクション

このウェブサイトをよく訪れているユーザーなら、MQL5 がカスタムEAを開発するための最良の選択肢だということにお気づきでしょう。 しかし残念ながら、すべてのブローカーが MetaTrader5 で利用可能なアカウントを作成できるわけではありません。 現在MT5が可能なブローカーでも、将来的にMetaTrader4 のみになる可能性もあります。 その場合、開発したすべての MQL5EAをどうするつもりでしょうか。 MQL4 に収まるように手直しするために膨大な時間を費やすつもりでしょうか。 おそらく、MetaTrader5 と MetaTrader4 の両方で稼働することができるEAを開発する方が合理的でしょう。

この記事では、そのようなEAを開発し、オーダーグリッドに基づくトレーディングシステムが使用可能かどうかを確認します。

条件付きコンパイルについての用語

条件付きコンパイルを使用すると、MetaTrader4 と MetaTrader5 の両方で機能するEAを開発することができます。 適用される構文は次のとおりです。

   #ifdef __MQL5__ 
      //MQL5 コード
   #else 
      //MQL4 コード
   #endif 

条件付きコンパイルでは、MQL5EAでコンパイルが行われた場合にのみ、特定のブロックをコンパイルするように指定することができます。 MQL4 および他の言語バージョンでコンパイルするとき、このコードブロックは廃棄されます。 代わりに、 #else演算子に続くコードブロックを使用します (set の場合)。

したがって、MQL4 と MQL5 で異なる関数が実装されている場合、両方の方法で実装しますが、条件付きコンパイルでは特定の言語に必要なオプションを選択することができます。

他のケースでは、MQL4 と MQL5 の両方で動作する構文を使用します。

グリッドトレードシステム

EA開発を始める前に、グリッドトレード戦略の基本について説明しましょう。

グラインダーズ(グリッドトレード)は、現在の価格の上に複数の指値オーダーを配置し、同時に現在の価格の下にリミットオーダーを同じ数オーダーするEAです。

指値オーダーは、1つの価格ではなく、特定のステップで設定されます。 言い換えれば、最初の指値オーダーは現在の価格よりも一定の距離で設定されます。 2番目の指値オーダーは、同じ距離にある最初のリミットオーダーの上に設定されます。 それを続けます。 オーダー数と適用されるステップは異なります。

1方向のオーダーは現在の価格の上に配置され、別の方向のオーダーは現在の価格の下に配置されます。 次のことが考えられます。

ストップレベルを適用することも、なしで稼働することもできます。

SLとTPを使用しない場合、すべてのオープンポジション (収益性の高い方と損失を生んでいる方) は、全体的な利益が一定のレベルに達するまで存在します。 その後、すべてのオープンポジション、および価格の影響を受けない指値オーダーがクローズされ、新しいグリッドが設定されます。

次のスクリーンショットは、開いているグリッドを示します。

グリッドサンプル

したがって、理論的には、グリッドトレードシステムは、任意のインジケータを使用することなく、任意のユニークなエントリポイントを待つことなく、任意の相場で利益を上げることができます。

SLとTPを使用する場合、価格が一方向に動く場合、1つのポジションの損失が残りの部分の全体的な利益によってカバーされるという事実に利益が得られます。

ストップレベルなしで、利益は正しい方向により多くのオーダーを開くことによって得られます。 最初に価格が一方向にポジションに触れ、その後超える場合でも、最終的には多くなるように、右方向の新しいポジションは、以前に開いたポジションの損失を補います。

griderEAの動作原理

上記の最もシンプルなグリッドトレードの動作原理を説明しました。 オープンオーダーの方向を変えるグリッドの独自のオプションを用意し、同じ価格で複数のオーダーを開く関数を追加したり、インジケータを追加したりすることができます。

この記事では、基づいているという考えが魅力的であるので、SLなしで最もシンプルなグリッドトレードバージョンを実装します。

実際には、ポジションが間違った方向に最初に開かれた場合でも、価格が一方向に移動すると利益が得られるという考えは合理的です。 当初、価格が推移し、2つのオーダーに触れたと仮定します。 その後、価格は反対 (メイントレンド) 方向に移動し始めます。 この場合、遅かれ早かれ2つ以上のオーダーが正しい方向に開かれ、最初の損失はしばらくすると利益に変わります。

トレードシステムが損失を引き起こす可能性がある唯一のケースは、価格が最初に1つのオーダーに触れ、その後戻って反対に触れ、再び方向を変えて別のオーダーに触れ、その方向を何度もタッチするということです。 しかし、そのような価格行動は、実際の状況で起こりうるでしょうか。

EAテンプレート

テンプレートからEAの開発を開始します。 これより、どの標準 MQL 関数が関与するかをすぐに確認することができます。

#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://www.mql5.com/ja/users/needtome"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
  }

void OnChartEvent(const int id,         //イベント ID
                  const long& lparam,   //long 型のイベントパラメータ
                  const double& dparam, //double 型のイベントパラメータ
                  const string& sparam) //文字列型のイベントパラメータ
   {
   }

MQL5 ウィザードを使用してEAを作成するときに生成される標準テンプレートとの唯一の差は#property strictの文字列です。 EAが MQL4 でも動作するように追加します。

ボタンのクリックに応答できるようにするには、 OnChartEvent() 関数が必要です。 次に、目的の資金に達した場合、またはEAをストップしたい場合は、すべてのシンボルのポジションとオーダーを手動で閉じることができるように、すべて閉じるボタンを実装します。

ポジションオープン関数

おそらく、あらゆるEAの最も重要な関数はオーダーを行う能力です。 最初の問題があるところでもあります。 MQL5 と MQL4 では、オーダーはまったく異なって配置されます。 この関数を何らかの形で統一するためには、オーダーを出すためのカスタム関数を開発する必要があります。

各オーダーには独自のタイプがあります: 買いオーダー、売りオーダー、指値買いまたは売りオーダー。 この型がオーダー時に設定される変数は、MQL5 と MQL4 でも異なっています。

MQL4 では、オーダータイプは int 型変数によって指定されますが、MQL5 では ENUM_ORDER_TYPE 列挙が使われます。 MQL4 にはそのような列挙はありません。 したがって、両方の方法を組み合わせるには、オーダータイプを設定するためのカスタム列挙体を作成する必要があります。 このため、将来的に作成する関数は、MQL のバージョンに依存しません。

enum TypeOfPos{
   MY_BUY,
   MY_SELL,
   MY_BUYSTOP,
   MY_BUYLIMIT,
   MY_SELLSTOP,
   MY_SELLLIMIT,
}; 

オーダーを配置するためのカスタム関数を作成することができます。 PdxSendOrder()という名前にしましょう。 オーダーに必要なものをすべて渡します: オーダータイプ、始値、SL(設定されていない場合は 0)、TP(未設定の場合は 0)、ボリューム、オープンポジションチケット (オープンポジションが MQL5 で変更する必要がある場合)、コメントとシンボル:

//オーダー送信関数
bool pdxSendOrder(TypeOfPos mytype, double price, doubleSL, doubleTP, double volume, ulong position=0, string comment="", string sym=""){
   // check passed values
   if( !StringLen(sym) ){
      sym=_Symbol;
   }
   int curDigits=(int) SymbolInfoInteger(sym, SYMBOL_DIGITS);
   if(sl>0){
      sl=NormalizeDouble(sl,curDigits);
   }
   if(tp>0){
      tp=NormalizeDouble(tp,curDigits);
   }
   if(price>0){
      price=NormalizeDouble(price,curDigits);
   }
   
   #ifdef __MQL5__ 
      ENUM_TRADE_REQUEST_ACTIONS action=TRADE_ACTION_DEAL;
      ENUM_ORDER_TYPE type=ORDER_TYPE_BUY;
      switch(mytype){
         case MY_BUY:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_BUY;
            break;
         case MY_BUYSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_STOP;
            break;
         case MY_BUYLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_LIMIT;
            break;
         case MY_SELL:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_SELL;
            break;
         case MY_SELLSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_STOP;
            break;
         case MY_SELLLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_LIMIT;
            break;
      }
      
      MqlTradeRequest mrequest;
      MqlTradeResult mresult;
      ZeroMemory(mrequest);
      
      mrequest.action = action;
      mrequest.SL=SL;
      mrequest.TP=TP;
      mrequest.symbol = sym;
      if(position>0){
         mrequest.position = position;
      }
      if(StringLen(comment)){
         mrequest.comment=comment;
      }
      if(action!=TRADE_ACTION_SLTP){
         if(price>0){
            mrequest.price = price;
         }
         if(volume>0){
            mrequest.volume = volume;
         }
         mrequest.type = type;
         mrequest.magic =EA_Magic;
         switch(useORDER_FILLING_RETURN){
            case FOK:
               mrequest.type_filling = ORDER_FILLING_FOK;
               break;
            case RETURN:
               mrequest.type_filling = ORDER_FILLING_RETURN;
               break;
            case IOC:
               mrequest.type_filling = ORDER_FILLING_IOC;
               break;
         }
         mrequest.deviation=100;
      }
      if(OrderSend(mrequest,mresult)){
         if(mresult.retcode==10009 || mresult.retcode==10008){
            return true;
         }else{
            msgErr(GetLastError(), mresult.retcode);
         }
      }
   #else 
      int type=OP_BUY;
      switch(mytype){
         case MY_BUY:
            type=OP_BUY;
            break;
         case MY_BUYSTOP:
            type=OP_BUYSTOP;
            break;
         case MY_BUYLIMIT:
            type=OP_BUYLIMIT;
            break;
         case MY_SELL:
            type=OP_SELL;
            break;
         case MY_SELLSTOP:
            type=OP_SELLSTOP;
            break;
         case MY_SELLLIMIT:
            type=OP_SELLLIMIT;
            break;
      }
      if(OrderSend(sym, type, volume, price, 100,SL,TP, comment,EA_Magic, 0)<0){
         msgErr(GetLastError());
      }else{
         return true;
      }
   
   #endif 
   return false;
}

まず、関数に渡された値を確認し、価格を正規化します。

インプット. その後、条件付きコンパイルを使用して、現在の MQL バージョンを定義し、そのルールに従ってオーダーを設定します。 追加のuseORDER_FILLING_RETURNインプットパラメータは MQL5 に使用します。 その助けを借りて、ブローカーによってサポートされているモードに従ってオーダー実行モードを設定します。 UseORDER_FILLING_RETURNインプットパラメータは MQL5EAに対してのみ必要であるため、条件付きコンパイルを再度使用して追加します。

#ifdef __MQL5__ 
   enum TypeOfFilling //Filling Mode
     {
      FOK,//ORDER_FILLING_FOK
      RETURN,//ORDER_FILLING_RETURN
      IOC,//ORDER_FILLING_IOC
     }; 
   input TypeOfFilling  useORDER_FILLING_RETURN=FOK; //Filling Mode
#endif 

また、オーダー時に、EAのマジックナンバーを含むEA_Magicのインプットパラメータを使用します。

EA設定でこのパラメータが設定されていない場合、EAが起動された銘柄のポジションはEAが所有しているものとみなされます。 したがって、EAはそれらを完全に制御します。

マジックナンバーが設定されている場合、EAはこのマジックナンバーを持つポジションのみを考慮します。

エラーの表示. オーダーが正常に設定されている場合は、 trueが返されます。 それ以外の場合は、適切なエラーコードがmsgErr() 関数に渡され、さらに分析し、わかりやすいエラーメッセージを表示します。 この関数は、詳細なエラー記述を含むローカライズされたメッセージを表示します。 ここに完全なコードを提供することに意味はありません。 よって一部だけを表示します:

void msgErr(int err, int retcode=0){
   string curErr="";
   switch(err){
      case 1:
         curErr=langs.err1;
         break;
//      case N:
//         curErr=langs.errN;
//         break;
      default:
         curErr=langs.err0+": "+(string) err;
   }
   if(retcode>0){
      curErr+=" ";
      switch(retcode){
         case 10004:
            curErr+=langs.retcode10004;
            break;
//         case N:
//            curErr+=langs.retcodeN;
//            break;
      }
   }
   
   Alert(curErr);
}

次のセクションでは、ローカリゼーションについて詳しく説明します。

EAのローカリゼーション

EAの開発を再開する前に、バイリンガルにしましょう。 EAメッセージの言語を選択する関数を追加してみましょう。 英語とロシア語の2つの言語にします。

可能な言語オプションを使用して列挙体を作成し、言語を選択するための適切なパラメータを追加します。

enum TypeOfLang{
   MY_ENG, //英語
   MY_RUS, // Русский
}; 

input TypeOfLang  LANG=MY_RUS; // Language

次に、EAで使用するすべてのテキスト文字列を格納するために使用する構造を作成します。 その後、作成した型の変数を宣言します。

struct translate{
   string err1;
   string err2;
//   ... other strings
};
translate langs;

文字列を含む変数が既に存在します。 しかし、まだそこには文字列はありません。 Lauguageインプットで選択された言語で文字列を格納する関数を作成します。 関数の名前をinit_lang()にしましょう。 そのコードの一部が下に表示されます。

void init_lang(){
   switch(LANG){
      case MY_ENG:
         langs.err1="No error, but unknown result. (1)";
         langs.err2="General error (2)";
         langs.err3="Incorrect parameters (3)";
//         ... other strings
         break;
      case MY_RUS:
         langs.err0="Во время выполнения запроса произошла ошибка";
         langs.err1="Нет ошибки, но результат неизвестен (1)";
         langs.err2="Общая ошибка (2)";
         langs.err3="Неправильные параметры (3)";
//         ... other strings
         break;
   }
}

あとは、文字列が必要な値でいっぱいになるようにinit_lang()関数を呼び出すだけです。 これを呼び出すのに最適な場所は、EAの起動中に呼び出されるため、まさに必要とするものであり、標準的なOnInit()関数です。

メインインプット

EAにメインのインプットを追加する時間です。 すでに記述されているEA_MagicLANGとは別に、以下のものがあります。

input double      Lot=0.01;     //ロットサイズ
input uint        maxLimits=7;  //一方向のグリッド内の指値オーダーの数
input int         Step=10;      //ポイント内のグリッドステップ
input double      takeProfit=1; //指定された利益に達すると取引をクローズ

言い換えれば、 maxLimitsオーダーを1つの方向に開き、同じオーダー数を反対のオーダーにオープンします。 最初のオーダーは、現在の価格のステップポイントに配置されます。 2番目のものは、最初のオーダーなどのステップポイントに配置されます。

利益はテイクプロフィット値 ($) に達するとすぐに確定されます。 この場合、すべてのオープンポジションが決済され、すべてのオーダーがキャンセルされます。 その後、EAはそのグリッドをリセットします。

すべてが負ける可能性を考慮していないので、TPはポジションを閉じるための唯一の条件です。

OnInit 関数のインプット

すでに述べたように、 OnInit()関数は最初のEA起動時に1回呼び出されます。 Init_lang()関数の呼び出しが既に追加されています。 もう戻らなくてもいいように、最後まで埋めていきましょう。

EAの枠内で、 OnInit()関数の唯一の目的は、価格が3または5のデジタルの場所がある場合、ステップインプットの補正です。 言い換えれば、1つの追加のデジタル・プレースが、ブローカーによってシンボルに使用する場合:

   ST=Step;
   if(_Digits==5 || _Digits==3){
      ST*=10;
   }

したがって、EA自体のステップインプットの代わりに補正された ST パラメータを使用します。 Double 型を指定して関数を呼び出す前に宣言します。

グリッドを形成するためにポイントではなくシンボル価格で距離が必要になるため、すぐに変換を実行してみましょう:

   ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT);

また、この関数において、EAのトレードが許可されているかどうかを確認することができます。 トレードが無効になっている場合は、すぐにユーザーに通知することをお勧めします。

このチェックは、この小さなコードを使用して行うことができます:

   if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
      Alert(langs.noBuy+" ("+(string)EA_Magic+")");
      ExpertRemove();
   }   

トレードが無効になっている場合は、ユーザーが選択した言語で通知します。 その後、EAの操作は完了です。

結果として、 OnInit()関数の最終的な外観は次のようになります。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   init_lang();
   
   if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
      Alert(langs.noBuy+" ("+(string)EA_Magic+")");
      ExpertRemove();
   }

   ST=Step;
   if(_Digits==5 || _Digits==3){
      ST*=10;
   }
   ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   
   return(INIT_SUCCEEDED);
  }

[すべて閉じる] ボタンを追加する

EAでのタスクの利便性は、選択されたトレード戦略への準拠と同様に重要です。

今回のケースでは、ロングとショートがどのようにポジションとして開いているかを一目で見る利便性が必要であり、また、現在開いているすべてのポジションの総利益が必要です。

利益に満足しているか、何かが間違っている場合にも、すぐにすべてのオープンオーダーやポジションを閉じることができるはずです。

したがって、すべての必要なデータを表示するボタンを追加し、クリックするとすべてのポジションとオーダーを決済するようにしましょう。

グラフィカルオブジェクトプレフィックス メタトレーダーのすべてのグラフィックオブジェクトには名前が必要です。 1つのEAによって作成されたオブジェクトの名前は、チャート上で手動または他のEAによって作成されたオブジェクトの名前と一致してはなりません。 そのため、まず、すべてのグラフィカルオブジェクトの名前に追加するプレフィックスを定義しましょう。

string prefix_graph="grider_";

ポジションと利益を計算. オープンロングとショートポジションの数を計算する関数を作成することができますし、その総利益、およびそのようなボタンが既に存在する場合、取得したデータとボタンを表示したり、その上のテキストを更新します。 関数の名前をgetmeinfo_btn()にしましょう。

void getmeinfo_btn(string symname){
   double posPlus=0;
   double posMinus=0;
   double profit=0;
   double positionExist=false;

   //オープンロングとショートポジションの数をカウントし、
   //総利益を出す
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=symname) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         positionExist=true;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
         
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY){
            posPlus+=PositionGetDouble(POSITION_VOLUME);
         }else{
            posMinus+=PositionGetDouble(POSITION_VOLUME);
         }
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=symname) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            positionExist=true;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
            
            if(OrderType()==OP_BUY){
               posPlus+=OrderLots();
            }else{
               posMinus+=OrderLots();
            }
         }
      }
   #endif 
   
   //オープンポジションがある場合は、
   //閉じるボタンを追加する
   if(positionExist){
      createObject(prefix_graph+"delall", 233, langs.closeAll+" ("+DoubleToString(profit, 2)+") L: "+(string) posPlus+" S: "+(string) posMinus);
   }else{
      //それ以外の場合は、ポジションを閉じるボタンを削除します
      if(ObjectFind(0, prefix_graph+"delall")>0){
         ObjectDelete(0, prefix_graph+"delall");
      }
   }
   
   //表示する現在のチャートを更新します。
   //実装された変更
   ChartRedraw(0);
}

ここでは、MQL5 でのオープンポジションの操作の関数が MQL4 のものと異なるため、2回目に条件付きコンパイルを使用しました。 同様の理由から、この記事の後半では、条件付きコンパイルを複数回使用します。

ボタンを表示. また、チャート上にボタンを表示するために、 createObject()カスタム関数を使用していることにも注意してください。 この関数は、最初の関数引数として渡された名前を持つボタンがチャート上に存在するかどうかをチェックします。

ボタンが既に作成されている場合は、3番目の関数引数で渡されるテキストに従って、そのテキストを更新するだけです。

ボタンがない場合は、チャートの右上隅に作成します。 この場合、2番目の関数引数はボタンの幅を設定します。

void createObject(string name, int weight, string title){
   //チャートに「name」ボタンがない場合は作成します。
   if(ObjectFind(0, name)<0){
      //ボタンが表示されるチャートの右上に対するシフトを定義します。
      long offset= ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87;
      long offsetY=0;
      for(int ti=0; ti<ObjectsTotal((long) 0); ti++){
         string objName= ObjectName(0, ti);
         if( StringFind(objName, prefix_graph)<0 ){
            continue;
         }
         long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset>offsetY){
            offsetY=tmpOffset;
         }
      }
      
      for(int ti=0; ti<ObjectsTotal((long) 0); ti++){
         string objName= ObjectName(0, ti);
         if( StringFind(objName, prefix_graph)<0 ){
            continue;
         }
         long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset!=offsetY ){
            continue;
         }
         
         tmpOffset=ObjectGetInteger(0, objName, OBJPROP_XDISTANCE);
         if( tmpOffset>0 && tmpOffset<offset){
            offset=tmpOffset;
         }
      }
      offset-=(weight+1);
      if(offset<0){
         offset=ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87;
         offsetY+=25;
         offset-=(weight+1);
      }
  
     ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
     ObjectSetInteger(0,name,OBJPROP_XDISTANCE,offset); 
     ObjectSetInteger(0,name,OBJPROP_YDISTANCE,offsetY); 
     ObjectSetString(0,name,OBJPROP_TEXT, title); 
     ObjectSetInteger(0,name,OBJPROP_XSIZE,weight); 
     ObjectSetInteger(0,name,OBJPROP_FONTSIZE, 8);
     ObjectSetInteger(0,name,OBJPROP_COLOR, clrBlack);
     ObjectSetInteger(0,name,OBJPROP_YSIZE,25); 
     ObjectSetInteger(0,name,OBJPROP_BGCOLOR, clrLightGray);
     ChartRedraw(0);
  }else{
     ObjectSetString(0,name,OBJPROP_TEXT, title);
  }
}

ボタンクリックに対するレスポンス. これで、 getmeinfo_btn()関数を呼び出すと、チャート上に [すべて閉じる] ボタンが表示されます (ポジションを開いている場合)。 ただし、このボタンをクリックしても何も起こりません。

ボタンをクリックする応答を追加するには、 OnChartEvent()標準関数でクリックをインターセプトする必要があります。 OnChartEvent()関数の唯一の目的であるため、最終的なコードを提供することができます。

void OnChartEvent(const int id,         //イベント ID
                  const long& lparam,   //long 型のイベントパラメータ
                  const double& dparam, //double 型のイベントパラメータ
                  const string& sparam) //文字列型のイベントパラメータ
{
   string text="";
   switch(id){
      case CHARTEVENT_OBJECT_CLICK:
         //クリックしたボタンの名前が prefix_graph +  "delall" の場合は、
         if (sparam==prefix_graph+"delall"){
            closeAllPos();
         }
         break;
   }
}

今ポジションを閉じるボタンをクリックすると、 closeAllPos()関数が呼び出されます。 この関数はまだ実装されていません。 次のセクションで行います。

追加アクション. 必要なデータを計算し、ポジションを閉じるボタンを表示するgetmeinfo_btn()関数が既にあります。 また、ボタンをクリックすると発生するアクションを実装します。 ただし、 getmeinfo_btn()関数自体は、EAではまだどこにも呼び出されません。 したがって、現時点ではチャートには表示されません。

標準のOnTick()関数のコードを扱うときに、 getmeinfo_btn()関数を使用します。

その間に、 OnDeInit()標準関数に注目してみましょう。 今回のEAはグラフィカルオブジェクトを作成するので、EAを閉じるときに、よって作成されたすべてのグラフィックオブジェクトがチャートから削除されるようにしてください。 OnDeInit()関数が必要なのはこのためです。 EAを修了すると自動的に呼び出されます。

その結果、 OnDeInit()関数本体は次のようになります。

void OnDeinit(const int reason)
  {
      ObjectsDeleteAll(0, prefix_graph);
  }

この文字列は、EAを閉じるときに、名前に指定されたプレフィックスを含むすべてのグラフィックオブジェクトを削除します。 これまでのところ、このようなオブジェクトは1つしかありません。

すべてのポジションをクローズするための関数の実装

すでにcloseAllPos()関数の使用を開始しているので、そのコードを実装しましょう。

CloseAllPos()関数は、現在オープンしているすべてのポジションをクローズし、すべての配置オーダーを削除します。

しかし、そうシンプルではありません。 この関数は、現在開いているすべてのポジションを削除するだけではありません。 もし、オープンロングポジションと短いものがあれば、ポジションの1つを反対側でクローズします。 ブローカーは、現在のツールでこの操作をサポートしている場合、2つのポジションを開くために支払ったスプレッドを取り戻します。 これよりEAの収益性が向上します。 TPによってすべてのポジションをクローズすると、テイクプロフィットインプットパラメータで指定されたものを少し超える利益が実際にあります。

したがって、 closeAllPos()関数の最初の文字列には、さらに別の関数の呼び出しが含まれています: closeByPos()

CloseByPos()関数は、反対のポジションでポジションをクローズします。 すべての反対のポジションがクローズされた後、 closeAllPos()関数は、通常の方法で残りのポジションを閉じます。 その後、配置されたオーダーを閉じます。

通常、MQL5 でポジションをクローズするために CTrade オブジェクトを使用します。 したがって、2つのカスタム関数を実装する前に、クラスをインクルードして、すぐにそのオブジェクトを作成してみましょう。

#ifdef __MQL5__ 
   #include <Trade\Trade.mqh>
   CTrade Trade;
#endif 

これで、すべてのポジションを決済する関数の開発を開始することができます。

void closeByPos(){
   bool repeatOpen=false;
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         if( PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ){
            long closefirst=PositionGetInteger(POSITION_TICKET);
            double closeLots=PositionGetDouble(POSITION_VOLUME);
            
            for(int ti2=cntMyPos-1; ti2>=0; ti2--){
               if(PositionGetSymbol(ti2)!=_Symbol) continue;
               if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
               if( PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_SELL ) continue;
               if( PositionGetDouble(POSITION_VOLUME)!=closeLots ) continue;
               
               MqlTradeRequest request;
               MqlTradeResult  result;
               ZeroMemory(request);
               ZeroMemory(result);
               request.action=TRADE_ACTION_CLOSE_BY;
               request.position=closefirst;
               request.position_by=PositionGetInteger(POSITION_TICKET);
               if(EA_Magic>0) request.magic=EA_Magic;
               if(OrderSend(request,result)){
                  repeatOpen=true;
                  break;
               }
            }
            if(repeatOpen){
               break;
            }
         }
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; 
            if( OrderSymbol()!=_Symbol ) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            if( OrderType()==OP_BUY ){
               int closefirst=OrderTicket();
               double closeLots=OrderLots();
               
               for(int ti2=cntMyPos-1; ti2>=0; ti2--){
                  if(OrderSelect(ti2,SELECT_BY_POS,MODE_TRADES)==false) continue; 
                  if( OrderSymbol()!=_Symbol ) continue;
                  if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
                  if( OrderType()!=OP_SELL ) continue;
                  if( OrderLots()<closeLots ) continue;
                  
                  if( OrderCloseBy(closefirst, OrderTicket()) ){
                     repeatOpen=true;
                     break;
                  }
               }
               if(repeatOpen){
                  break;
               }
            }
                        
         }
      }
   #endif 
   //反対のポジションをクローズすると、
   //closeByPos 関数を再度起動する
   if(repeatOpen){
      closeByPos();
   }
}

Close 操作が成功した場合、この関数は自分自身を呼び出します。 ポジションが異なるボリュームを持つ可能性があるため、2つのポジションを閉じることは必ずしも必要な結果をもたらさない場合があるためです。 ボリュームが異なる場合、ポジションのボリュームの1つは減少し、次の関数起動時に反対のポジションで閉じられるようになります。

すべての反対のポジションを閉じた後、 closeAllPos()関数は残りのものを閉じます:

void closeAllPos(){
   closeByPos();
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;

         Trade.PositionClose(PositionGetInteger(POSITION_TICKET));
      }
      int cntMyPosO=OrdersTotal();
      for(int ti=cntMyPosO-1; ti>=0; ti--){
         ulong orderTicket=OrderGetTicket(ti);
         if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue;
         if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue;
         
         Trade.OrderDelete(orderTicket);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; 
            if( OrderSymbol()!=_Symbol ) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            if( OrderType()==OP_BUY ){
               MqlTick latest_price;
               if(!SymbolInfoTick(OrderSymbol(),latest_price)){
                  Alert(GetLastError());
                  return;
               }
               if(!OrderClose(OrderTicket(), OrderLots(),latest_price.bid,100)){
               }
            }else if(OrderType()==OP_SELL){
               MqlTick latest_price;
               if(!SymbolInfoTick(OrderSymbol(),latest_price)){
                  Alert(GetLastError());
                  return;
               }
               if(!OrderClose(OrderTicket(), OrderLots(),latest_price.ask,100)){
               }
            }else{
               if(!OrderDelete(OrderTicket())){
               }
            }
                        
         }
      }
   #endif 
   //ポジション決済ボタンを削除する
   if(ObjectFind(0, prefix_graph+"delall")>0){
      ObjectDelete(0, prefix_graph+"delall");
   }

}

OnTick 関数の実装

すでにほぼすべてのEA関数を実装しました。 さて次に、最も重要な部分を開発しましょう。つまり、グリッドオーダーの配置です。

各シンボルティックの到着時に、標準のOnTick()関数が呼び出されます。 この関数を使用して、グリッドのオーダーが存在するかどうかを確認し、そうでない場合は作成します。

バースタートチェック. ただし、ティックごとにチェックを行うのは冗長です。 たとえば、5分ごとに1回など、グリッドの存在を確認するだけで十分です。 これを行うには、足の開始をチェックするコードをOnTick()関数に追加します。 足の開始からの最初のティックでない場合は、何もせずに関数の操作を完了します。

   if( !pdxIsNewBar() ){
      return;
   }

PdxIsNewBar()関数は次のようになります。

bool pdxIsNewBar(){
   static datetime Old_Time;
   datetime New_Time[1];

   if(CopyTime(_Symbol,_Period,0,1,New_Time)>0){
      if(Old_Time!=New_Time[0]){
         Old_Time=New_Time[0];
         return true;
      }
   }
   return false;
}

EAが条件を5分ごとに1回チェックするためには、M5 時間枠で起動する必要があります。

テイクプロフィットのチェック. グリッドの可用性を確認する前に、現在開いているすべてのグリッドポジションのTPに達しているかどうかを確認する必要があります。 TPに達した場合は、前述のcloseAllPos()関数を呼び出します。

   if(checkTakeProfit()){
      closeAllPos();
   }

TPを確認するには、 checkTakeProfit()関数を呼び出します。 現在開いているすべてのポジションの利益を計算し、テイクプロフィットインプットパラメータの値と比較します。

bool checkTakeProfit(){
   if( takeProfit<=0 ) return false;
   double curProfit=0;
   double profit=0;
   
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
         }
      }
   #endif 
   if(profit>takeProfit){
      return true;
   }
   return false;
}

全決済のボタンの表示. 実装していますが、まだ表示されていないClose Allボタンを忘れないでください。 さて、この関数呼び出しを追加しましょう。

getmeinfo_btn(_Symbol);

次のように表示されます。

Close Allボタン

グリッドの配置. 最後に、EAの最も重要な部分にアプローチします。 すべてのコードは関数の後ろに隠されているので、シンプルに見えます:

   //シンボルがオープンポジションまたは配置オーダーを特徴とする場合、
   if( existLimits() ){
   }else{
   //それ以外の場合は、グリッドを配置
      initLimits();
   }

シンボルがオープンポジションまたは配置されたオーダーがある場合、 existLimits()関数は ' true ' を返します。

bool existLimits(){
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         return true;
      }
      int cntMyPosO=OrdersTotal();
      for(int ti=cntMyPosO-1; ti>=0; ti--){
         ulong orderTicket=OrderGetTicket(ti);
         if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue;
         if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue;
         return true;
      }
   #else 
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            return true;
         }
      }
   #endif 
   
   return false;
}

この関数が ' true ' を返す場合、何もしません。 それ以外の場合は、 initLimits()関数を使用して新しいオーダーグリッドを配置します。

void initLimits(){
   //グリッドオーダーを設定するための価格
   double curPrice;
   //現在のシンボル価格
   MqlTick lastme;
   SymbolInfoTick(_Symbol, lastme);
   //現在の価格が取得されない場合は、グリッドの配置をキャンセルします。
   if( lastme.bid==0 ){
      return;
   }

   //SLを配置するために利用可能な価格からの最小距離、
   //おそらく、予約注文
   double minStop=SymbolInfoDouble(_Symbol, SYMBOL_POINT)*SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   
   //ロングオーダーを配置
   curPrice=lastme.bid;
   for(uint i=0; i<maxLimits; i++){
      curPrice+=ST;

      if( curPrice-lastme.ask < minStop ) continue;
      if(!pdxSendOrder(MY_BUYSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){
      }
   }
   //ショートオーダーを行う
   curPrice=lastme.ask;
   for(uint i=0; i<maxLimits; i++){
      curPrice-=ST;
      if( lastme.bid-curPrice < minStop ) continue;
      if(!pdxSendOrder(MY_SELLSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){
      }
   }
}

EAのテスト

EAの準備が整いました。 テストし、トレード戦略のパフォーマンスに関する結論を引き出す必要があります。

このEAは MetaTrader4 と MetaTrader5 の両方で動作するため、テストを実行するターミナルバージョンを選択することができます。 選択はここではかなり明白ですが。 MetaTrader5 はより分かりやすく、より良いと考えられています。

まず、最適化を行わずにテストを実行してみましょう。 妥当な値を使用する場合、EAはインプットの値に完全に依存すべきではありません。 みてみましょう:

インプットのデフォルト値はそのまま残ります (ロット0.01、ステップ10ポイント、グリッドあたり7オーダー、TP$1)。

結果を以下に示します。

最初のEAテスト中のバランスチャート

チャートからわかるように、すべてが月全体と1週間にうまくいきました。 $30 のドローダウンでほぼ $100 を稼ぐことができました。 そして、一見してありえない出来事が起こりました。 ビジュアライザを見て、9月の価格の動きを確認します。

可視化結果

9月13日に始まった、16:15 の少し後。 まず、価格がロングオーダーに触れました。 次に、2つのショートオーダー、2よりロングオーダー、最後に残りの5つのショートオーダーをアクティブにしました。 その結果、3のロングオーダーと7つのショートオーダーがあります。

画像上では見ることができませんが、価格は下に移動しませんでした。 9月20日までに、一番上のポイントに戻り、残りの4つのロングオーダーをアクティブにしました。

その結果、7つのショートオーダーと7つのロングオーダーがすべてオープンになりました。 これは、これ以上利益を達成しないことを意味します。

さらなる価格変動を見てみると、約80ポイント上昇しています。 もし一連の流れで、13のオーダーをしたとすれば、おそらく状況を反転させて利益を得ることができます。

十分でなかったとしても、後で価格は200ポイント下がるので、チェーン内の30のオーダーで、理論的にはTPを得ることができます。 これには、おそらく数ヶ月かかり、ドローダウンは巨大になります。

グリッドで新しいオーダー数をテスト. 仮定を確認してみましょう。 グリッド内の13のオーダーは何も変更しませんでしたが、20のオーダーは無傷でした。

EURUSD のテスト、グリッドでの20オーダー

しかし、ドローダウンは約 $300 で構成されていますが、合計利益は $100 をわずかに超えています。 おそらく、トレード戦略は完全な失敗ではありませんが、間違いなく大きな改善が必要です。

したがって、現在最適化することに意味はありません。 しかし、とにかくやってみましょう。

最適化. 最適化は、次のパラメータを使用して実行されます。

グリッド内のオーダー数は16であるのに対し、13ポイントのステップは最高であることが判明しました。

EURUSD のテスト、グリッドでの16オーダー、13ポイントのステップ

"実際のティックに基づくすべてのティック"モードでのテストの結果です。 結果がポジティブであるという事実それにも関わらず、$221 のドローダウンで5ヶ月の $119 は最良の結果ではありません。 これはトレード戦略は本当に改善が必要であることを意味します。

トレード戦略を改善するための方法

明らかに、TPのみを使用するのは不十分です。 時には、価格が両方向のオーダーのすべてまたはほとんどを突破する状況があります。 この場合、何ヶ月も利益を待つことになるかもしれませんが、無限ではありません。

検出された問題を解決するために何ができるか考えてみましょう。

裁量コントロール. もちろん、簡単な方法は、随時EAを手動で制御することです。 潜在的な問題が発生している場合, 追加のオーダーを置くか、すべてのポジションを閉じます。

追加のグリッドを設置する. たとえば、ある方向のオーダーの 70%、別の方向のオーダーの 70% が影響を受ける場合は、別のグリッドを設定しようとすることがあります。 追加のグリッドからのオーダーは、1方向のオープンポジション数を迅速に増加させることができるため、TPに早く到達することができます。

オープンポジション数とは別に、最終ポジションオープン日をチェックすることがあります。 たとえば、直近のポジションを開いてから1週間以上経過した場合、新しいグリッドが設定されます。

両方のオプションを使用すると、すでに大きなドローダウンを増加させる状況をさらに悪化するリスクがあります。

グリッド全体を終わらせ、新しくする. 追加のグリッドを設定することから離れて、現在のグリッドに属するすべてのポジションと配置されたオーダーを閉じることも一つの手です。

これを行うには、複数の方法があります。

例として、リストから直近の項目を実装してみましょう。 現在のグリッド上のポジションを閉じて、新しいものを開く必要があるときに、$ で損失のサイズを設定するパラメータを追加します。 0未満の数値は、損失を設定するために使用します。

input double      takeLoss=0; //損失の場合は決済$

次に、 checkTakeProfit()関数を書き直して、' true ' または ' false ' ではなく、すべてのオープンポジションの利益を返すようにする必要があります。

double checkTakeProfit(){
   double curProfit=0;
   double profit=0;
   
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
         }
      }
   #endif 
   return profit;
}

変更は黄色で表示されます。

これで、 OnTick()関数を変更して、TPに加えてすべてのポジションで ストップロス をチェックできるようになりました。

   if(takeProfit>0 && checkTakeProfit()>takeProfit){
      closeAllPos();
   }else if(takeLoss<0 && checkTakeProfit()<takeLoss){
      closeAllPos();
   }

追加のテスト

改善がどのような用途であったかを見てみましょう。

-$5 から-$100 までの範囲内で、$ 1 の ストップロス のみを最適化します。 残りのパラメータは、直近のテスト中に選択されたレベル (グリッド内の13ポイント、16オーダーのステップ) のままです。

ほとんどの利益は、-$56 の ストップロス で終わっています。 5ヶ月以内の利益は $83 の最大ドローダウンで $156 です。:

EURUSD のテスト-$56 のSL

チャートを分析すると、SLは5ヶ月間一度だけ起こったされたことがわかります。 この結果は、もちろん、利益のドローダウン比の面で優れています。

しかし、最終的な結論を出す前に、EAが選択されたパラメータで長期的にいくらかの利益を得ることができるかどうかを確認しましょう。 過去5年間の期間に試してみましょう。

SLで EURUSD をテストします, 5 年

結果は、がっかりなものです。 おそらく、追加の最適化が改善する可能性としてあります。 いずれにせよ、このグリッドトレード戦略の使用は、抜本的な改善を必要とします。 追加のオープンポジションは、遅かれ早かれ損失を克服するという考えは、長期的な自動売買の点で間違っています。

オーダーのSLおよびTPの追加

残念なことに, 上記の他のEA改善オプションはどちらも良い結果につながりません。. しかし、別々のトレードのSLはどうでしょうか。 場合によっては、SLを追加することで、長期的な自動売買に対するEAを改善できます。

5年のヒストリー上の最適化は、上記と比較してより良い結果を示しました。

140ポイントの ストップロス と50ポイントのTPは最も効率的でしました。 1つのポジションが30日以内に現在のグリッド上で開かれていない場合、いったん閉じられ、新しいグリッドが開かれます。

最終的な結果を以下に示します。

オーダーに ストップロス とTPを使用

利益は $226 のドローダウンで $351 です。 もちろん、 ストップロス を使用せずに得られたトレード結果よりも優れています。 しかし、直近のトレードを実行した後、30日以内に現在のグリッドを閉じるときに得られたすべての結果が損失を作ることに気づくことはできません。 また、30を超える日数の場合は、ほとんどが損失に終わります。 よって、この結果は、必然よりも偶然です。

結論

この記事の主な目的は、MetaTrader4 と MetaTrader5 の両方で稼働するトレーディングEAを書くことでしました。 そのことには成功しました。

また、週ごとにパラメータを調整することができない場合、数ヶ月のヒストリーでEAをテストするのが不十分であることもわかりました。

残念ながら、シンプルなグリッドトレードに基づくアイデアは現実的ではありません。 多分何かを見落としています。 実際に勝てるグリッドトレードを開発する方法を知っていれば、コメントに提案を書いてください。

とにかく、今回の調査結果は、グリッドベースのトレーディング戦略がまったく利益を生むことができないということではありません。 たとえば、次のシグナルを見てください。

このシグナルは、ここで説明したものよりも複雑な単一のグリッドトレードに基づいています。 そのグリッドは、実際に月あたりの利益の 100% まで得ることができます。 これについては、グリッドトレードに関する次の記事で詳しく説明します。