English Русский 中文 Español Deutsch Português
preview
カスタムインジケーター:ネット口座の部分的なエントリー、エグジット、リバーサル取引のプロット

カスタムインジケーター:ネット口座の部分的なエントリー、エグジット、リバーサル取引のプロット

MetaTrader 5 | 26 2月 2025, 08:53
477 0
Daniel Santos
Daniel Santos

内容

  1. はじめに
  2. ネッティング口座について
  3. 取引イベントの操作
  4. 実際の使用例:導入
  5. インジケーターのプロパティ
  6. アルゴリズムの説明
  7. もう一つの実例
  8. 取引エキスパートアドバイザー(EA)との統合
  9. 結論


1.はじめに

インジケーターについて語る際、プロット(ヒストグラム、トレンドライン、矢印、バー)、価格やボリュームの動きを基にしたデータの計算、取引における統計パターンの観察など、さまざまな機能を思い浮かべることができます。しかし、この記事では、MQL5でインジケーターを構築する別の方法について検討します。具体的には、エントリーや部分的なエグジットを含めた、自身のポジション管理の方法を解説します。そのために、取引履歴やポジションに関連する動的マトリックス、およびいくつかの取引機能を多用します。 


2.ネッティング口座について

記事のタイトルが示すように、このインジケーターはネッティング会計システムを備えた口座での使用が適しています。このシステムでは、同じ銘柄のポジションは常に1つだけ許可されます。一方向に取引をおこなうと、ポジションのサイズが増加します。逆方向に取引を行うと、ポジションには以下の3つの変化が生じます。

  1. 新しい取引の取引量が少ない->ポジションが減少する
  2. ボリュームが同じ->ポジションがクローズされる
  3. 新しい取引の取引量が多い->ポジションが反転する


たとえば、ヘッジ口座では、1ロットのEUR/USD買い取引を2回おこなうことで、同じ金融商品に対して2つの異なるポジションを持つことが可能です。一方、ネッティング口座では、1ロットのEUR/USD買い取引を2回おこなうと、2つの取引の加重平均価格で1つの2ロットのポジションが作成されます。両方の取引の取引量が同じ場合、ポジションの価格は各取引の価格の算術平均となります。

この計算は次のように実行されます。

ネッティング計算

各取引のロット単位の取引量(N)に基づいて加重平均価格(P)が計算されます。

システム間の違いについて詳しく知りたい方は、MetaQuotesが執筆した「MetaTrader 5にはヘッジポジション会計システムが搭載されている」を読むことをお勧めします。ここから先は、ネッティング口座で実行されるすべての操作について説明します。まだそのような口座をお持ちでない場合は、以下の手順に従って、MetaQuotesで無料のデモ口座を開設できます。

MetaTrader5で[ファイル]>[口座開設]をクリックします。

口座を開設するサーバーを選択する

デモ口座を開設したら、[続行]ボタンをクリックし、[取引でヘッジを使用する]オプションのチェックを外したままにします。

デモ口座開設の完了


3.取引イベントの操作

注文、取引、ポジションを管理するには、取引イベントを利用した操作が必要です。取引注文には即時注文と指値注文があり、注文が実行されると同時に、ポジションを開く・閉じる・変更するといった取引が発生します。

インジケーター自体はOrderSend()などの関数を使用できませんが、取引履歴やポジションのプロパティとやり取りすることは可能です。OnCalculate関数を使用すれば、インジケーターは始値、終値、ボリュームなどの情報を取得できます。また、OnTrade()関数は主にEAで使用されますが、ストラテジーテスター外でトレードイベントを検出できるため、インジケーターにも適用可能です。これにより、チャートオブジェクトの更新をより高速かつ効率的におこなうことができます。


4.実際の使用例:はじめに

以下の画像は、開発中のカスタムインジケーターの実際の使用例を示しています。ここでは、部分的なエントリーやエグジット(さらには反転)を複数回繰り返した結果、3ロットの買いポジションが形成されています。このプロセスにより、チャート上に表示される平均価格と提示価格、およびロットサイズの間に明らかな矛盾が生じることがあります。しかし、取引イベントを監視することで、アルゴリズムは部分的な決済が発生した際にラインを削除し、画面上のロット数を更新するための基準を把握できます。

部分的なエントリとボリュームのチャート


5.インジケーターのプロパティ

説明の始めに、インジケーターのプロパティを指定する必要があります。このインジケーターには次のプロパティがあります。

#property indicator_chart_window               // Indicator displayed in the main window
#property indicator_buffers 0                  // Zero buffers
#property indicator_plots   0                  // No plotting
//--- plot Label1
#property indicator_label1  "Line properties"
#property indicator_type1   DRAW_LINE          // Line type for the first plot
#property indicator_color1  clrRoyalBlue       // Line color for the first plot
#property indicator_style1  STYLE_SOLID        // Line style for the first plot
#property indicator_width1  1                  // Line width for the first plot 

このコードは、初期パラメータが要求されるインジケーター読み込み画面に表示される情報に影響します。

インジケーター開始


6.アルゴリズムの説明

プログラムの先頭で実行されるOnInit()関数は、実際のインジケーターバッファとして機能するElementというdouble型の配列を初期化する役割を担います。この配列は3つの列で構成され、各インデックスには価格(0)、数量(1)、チケット番号(2)が格納されます。この配列の各行は、履歴内の何らかの取引に対応します。初期化が成功した場合、つまり口座がヘッジ口座ではないことが確認された場合、次にOnTrade()関数がトリガーされます。初期化中にエラーが発生した場合、インジケーターは閉じられ、チャートから削除されます。

以下をご覧ください。

int OnInit()
  {
   ArrayResize(Element,0,0);
   int res=INIT_SUCCEEDED;
   if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
      res=INIT_FAILED;
   else
      OnTrade();
   return(res);
  }

初期化後に取引イベントが発生すると、OnTrade()関数がOnCalculate()関数によってネイティブにトリガーされます。ただし、新しいローソク足が形成された場合に 1回だけ トリガーされるようにするため、isNewBar関数とisOldBarブール変数を使用してフィルター処理をおこないます。したがって、OnTrade関数は、初期化時、新しいローソク足がある場合、および各取引イベントの3つの場合にアクティブになります。このプロセスにより、Element配列内のイベントを 読み取り・処理・保存 し、最終的に線やテキストの形式で グラフィックオブジェクトとして画面に表示 されます。

OnTrade()関数は、取引アルゴリズムの主要な変数を更新します。その際、まず注文履歴の選択開始時刻を格納するdateという日時型変数を更新します。プログラムの開始時にポジションがない場合、date変数は現在のローソク足の開始時刻で更新されます。

取引が実行されると、PositionsTotal()関数はゼロより大きい値を返し、ループを通じて、インジケーターが実行されているチャートに対応する銘柄の位置をフィルター処理します。次に履歴が選択され、ポジションIDに対応する実行された注文が取得されます。date変数は、これらの注文の中で最も古い時刻、つまりIDが作成された時刻に更新されます。

2番目のポジションが異なるIDで表示される場合は、ClearRectangles()関数によって削除されるグラフィック要素があるかどうかを確認し、すべてが最新であることを確認する必要があります。その後、Element配列のサイズを0に設定し、そこに含まれるデータを削除します。ポジションがない場合、この関数はClearRectangnles()関数もアクティブ化し、要素配列をリセットします。date変数には、最後に確認されたサーバー時間、つまり現在の時刻の値が格納されます。最後に、date変数の残りの値がListOrdersPositions()関数に渡されます。

void
 OnTrade()
  {
//---
   static datetime date=0;
   if(date==0)
      date=lastTime;
   long positionId=-1,numberOfPositions=0;
   for(int i=PositionsTotal()-1; i>=0; i--)
      if(m_position.SelectByIndex(i))
         if(m_position.Symbol()==ativo000)
           {
            numberOfPositions++;
            positionId=m_position.Identifier();
            oldPositionId=positionId;
           }
   if(numberOfPositions!=0)
     {
      //Print("PositionId: "+positionId);
      HistorySelectByPosition(positionId);
      date=TimeCurrent();
      for(int j=0; j<HistoryDealsTotal(); j++)
        {
         ulong ticket = HistoryDealGetTicket(j);
         if(ticket > 0)
            if(HistoryDealGetInteger(ticket,DEAL_TIME)<date)
               date=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
        }
      if(HistoryDealsTotal()==1 && (ArraySize(Element)/3)>1)
         if(ClearRectangles())
            ArrayResize(Element,0,0);
     }
   else
     {
      bool isClean=ClearRectangles();
      ArrayResize(Element,0,0);
      if(isClean)
        date=TimeCurrent();        // Do not use the array until there is new open position
      ArrayPrint(Element);         // If there are no errors, this function will not be called here: the array with zero size
     }
   ListOrdersPositions(date);
  }

ListOrdersPositions()関数は、要素配列にエントリを追加または削除するAddValue()関数とRemoveValue()関数をアクティブ化する役割を担うため、重要な役割を果たします。int型パラメータdateStartを受け取る場合、2つのオプションが可能になります。HistorySelect(start,end)関数に指定された期間中に取引履歴がない場合、履歴の最後に直接ジャンプし、PlotRectangles()関数を呼び出します。この関数は、Element配列の内容に従って画面上のオブジェクトを更新します。ただし、履歴に取引がある場合、HistoryDealsTotal()関数はゼロ以外の値を返す必要があります。この場合、見つかった各取引を調査し、エントリータイプ別に分類し、価格、数量、チケット番号に関する情報を収集するために、新しいチェックが実行されます。可能な取引タイプは、DEAL_ENTRY_IN、DEAL_ENTRY_OUT、DEAL_ENTRY_INOUTです。

取引がエントリー取引の場合、AddValue関数がアクティブになります。エグジット取引の場合、価格、数量、以前に受信したチケット番号などのパラメータを使用してRemoveValue()がアクティブ化されます。反転がある場合、チケット番号が以前に配列に入力されていない場合は、AddVolume()関数もトリガーされます。さらに、価格とボリュームのパラメータが渡され、後者は収集されたボリュームと配列内にまだ存在する以前の取引のボリュームとの差として計算されます。

このプロセスは、履歴ポジションの再構築をシミュレートします。反転取引が発生すると、ポジションは新たに開かれたものとして扱われ、ロット数が調整された状態で配列に追加されます。同時に、それまで画面上に表示されていたラインは削除され、最新の取引状況が正確に反映されるようになります。Sort()関数は、Element配列を価格列の昇順に並べ替えます。その後、配列内のボリューム列(列1)の値がゼロのオブジェクトを特定し、それに対応するグラフィック要素をチャートから削除します。さらに、データの整合性を確保するため、配列の最初の2つのインデックス(価格と数量)がゼロである行を削除し、矛盾が生じないようにします。

void ListOrdersPositions(datetime dateInicio)
  {
//Analyze the history
   datetime inicio=dateInicio,fim=TimeCurrent();
   if(inicio==0)
      return;
   HistorySelect(inicio, fim);
   double deal_price=0, volume=0,newVolume;
   bool encontrouTicket;
   uint tamanhoElement=0;
   for(int j=0; j<HistoryDealsTotal(); j++)
     {
      ulong ticket = HistoryDealGetTicket(j);
      if(ticket <= 0)
         return;
      if(HistoryDealGetString(ticket, DEAL_SYMBOL)==_Symbol)
        {
         encontrouTicket=false;
         newVolume=0;            // Need to reset each 'for' loop
         volume=HistoryDealGetDouble(ticket,DEAL_VOLUME);
         deal_price=HistoryDealGetDouble(ticket,DEAL_PRICE);
         double auxArray[1][3] = {deal_price,volume,(double)ticket};
         if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_IN)
            AddValue(deal_price,volume,(double)ticket);
         if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT)
            RemoveValue(deal_price,volume,(double)ticket);
         if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT)
           {
            tamanhoElement = ArraySize(Element)/3; //Always check the array size, it can vary with the Add/RemoveValue() functions
            for(uint i=0; i<tamanhoElement; i++)
               if(Element[i][2]==ticket)
                 {
                  encontrouTicket=true;
                  break;
                 }
            if(!encontrouTicket) // If after the previous scanning we don't find mentioning of the ticket in the array
              {
               for(uint i=0; i<tamanhoElement; i++)
                 {
                  newVolume+=Element[i][1];
                  Element[i][1]=0;
                 }
               newVolume=volume-newVolume;
               AddValue(deal_price,newVolume,double(ticket));
              }
           }
        }
     }
   PlotRectangles();
  }


7.もう一つの実例

上記のアルゴリズムの説明は、その動作を理解するうえで十分な情報を提供しています。ここでは、関連する操作と最も重要な変数の内容を示す具体的な例を用いて、さらに詳しく検討してみましょう。取引はストラテジーテスターの外部で実行され、発生した取引イベントが検出されます。ネッティング口座では、すべての取引が同じポジション識別子(ID)に紐づいているため、このIDを基準にフィルタリングすることが可能です。以下に、特定のポジションに関連する取引イベントの例を示します。

 時間 銘柄
 取引
方向
数量
価格
2023.05.04  09:42:05
winm23
1352975

In
1
104035
2023.05.04  09:43:16
winm23
1356370

in/out
2
103900
2023.05.04 16:34:51
winm23
2193299

Out
1
103700
2023.05.04 16:35:05
winm23
2193395

In
1
103690
2023.05.04 16:35:24
winm23
2193543

In
1
103720
2023.05.04 16:55:00
winm23
2206914

Out
1
103470
2023.05.04 17:27:26
winm23
2214188

in/out
2
103620
2023.05.04 17:30:21
winm23
2215738

in/out
4
103675
2023.05.05 09:03:28
winm23
2229482

In
1
104175
2023.05.05 09:12:27
winm23
2236503

Out
1
104005
2023.05.05 09:19:18
winm23
2246014

Out
1
103970
2023.05.05 09:22:45
winm23
2250253

In
1
103950
2023.05.05 16:00:10
winm23
2854029

Out
1
106375
2023.05.05 16:15:40
winm23
2864767

Out
1
106275
2023.05.05 16:59:41
winm23
2884590

Out
1
106555

以前の操作に関係なく、この時点ではElement配列のサイズは0となり、ポジションは存在しません。2023年5月4日09:42:05に、1ロットの売りエントリー取引が実行され、プラットフォーム履歴にすでに記録されています。この取引がおこなわれた直後に、OnTrade()関数が呼び出されます。MetaTrader5がコンピューターで数分前(09:15h)に起動されたことを考慮すると、date変数は十分に更新され、2023.05.04 09:15:00という時刻が保存されます。それ以降、この変数はその値を保持します。OnTrade()関数内では、ポジションのリストが確認されます。使用している口座タイプでは、銘柄ごとに1つのポジションしか許可されないため、この場合は「WINM23」という銘柄です。numberOfPositions変数は1という値を取り、positionID変数は1352975という値を取ります。これは最初の取引のチケット番号であり、その取引を作成した注文の番号に一致します。これにより、date変数は取引時刻で更新され、取引番号2193299までのすべての今後の取引は、Identifier()関数を使用して同じ時刻を取得することになります。

関数ListOrdersPositions(date)がトリガーされ、09:42:05からTimeCurrent()までの期間を選択して履歴データを取得します。ループ内で、エントリタイプが「IN」と検出されると、パラメータprice=104035、volume=1、ticket=1352975を使用してAddValue()関数が呼び出されます。最初は空のElement配列内にこのチケットは存在しないため、提供された3つの値(価格、数量、チケット番号)を含む新しい行が挿入されます。最後に、ArrayPrint(Element)関数が呼び出され、この行列がターミナルに表示されます。

次に、PlotRectangles()関数が呼び出され、現在のローソク足と15本前のローソク足のタイムスタンプが保存されます。これにより、プロットされる線の長さが決まります。GetDigits()GetDigits()関数は、銘柄のティックサイズの小数点以下の桁数(この場合は0)を定義します。この情報は、Element配列に格納されている価格値とともに、オブジェクトの名前を生成する際に使用されます。配列内の対応する価格ボリュームがゼロ以外であり、かつオブジェクトがチャート上に存在しない場合、四角形とテキストオブジェクトが作成されます。もしオブジェクトがすでに存在する場合は、色、テキスト、位置などの属性が更新されます。これらの長方形は技術的には線として機能しますが(高さがないため)、チャートを削除する際にこのタイプのすべてのオブジェクトを削除する将来の機能を有効にするために、当初はOBJ_RECTANGLEが選択されました。この汎用的な削除メカニズムは実装されませんでしたが、高さゼロの長方形を使用する方法は保持されています。そのため、買い取引104035に対応する配列の行が処理され、ボリュームがゼロでなく、「104035text」という名前のオブジェクトがまだ存在しない場合、関連付けられたテキストオブジェクトと四角形オブジェクトが作成されます。

次の1分で、2ロットの売り取引が実行されます。既に1ロットの買いポジションがあったため、ポジションは反転し、1ロットのショートポジションが残ります。MetaTraderはすぐにこの取引を履歴レコードに追加します。以前と同じ処理ロジックが適用され、注文履歴ループが再度実行されます。チケット番号1352975の取引は、選択した期間内で再度表示され、AddValue()関数に渡されます。関数は、このチケットを唯一の既存エントリとして配列内で見つけたため、新しいエントリを追加することなく終了します。次に検出された取引は「INOUT」タイプであり、配列内に存在する唯一の取引のElement[0][1](ボリューム)値はnewVolumeに格納され、その後ゼロに設定されます。

取引量は、HistoryDealGetDouble(ticket, DEAL_PRICE) - newVolumeとして計算され、newVolume = 2 - 1 = 1となります。その結果、同じロジックに従って、AddValue(103900, 1, 135370)が実行されます。次に、PlotRectangles()関数が再度実行され、Sort()関数を使用して配列を昇順に並べ替えると、配列の最初の価格は103900になります。この価格のオブジェクトはチャート上にまだ存在していないため、新しく作成されます。2番目の配列要素(価格104035)にはすでにオブジェクトが描画されているため、その属性が更新されます。この段階では、要素配列には{{103900, 1, 1356370}, {104035, 0, 1352975}}が含まれています。

プロセスが続行されると、価格=103700、ボリューム=1、チケット=2193299のエグジット取引として識別される3番目の取引が表示されます。エグジット取引は、これらのパラメータを使用してRemoveValue()関数をトリガーします。RemoveValue()は、ボリュームがゼロの場合、または同じチケットを持つ既存の行が検出された場合に終了します。これらの条件が満たされていないため、関数はArrayBsearch()を使用して削除する価格の検索を続けます。ArrayBsearch()は、ソートされた配列(Sort()によって保証される)を必要とするバイナリ検索アルゴリズムです。103700に最も近いインデックスは、配列の最初のエントリです。この行のボリュームも1であるため、ゼロに設定され、RemoveRectangle()関数がトリガーされ、価格103900に関連付けられたグラフィカルオブジェクトが削除されます。その後、AddValue()は行{103700, 0, 219299}を挿入しますが、これはSort()によって変更されません。ポジションはクローズされます。この段階では、要素配列には{{103700, 0, 219299}, {103900, 0, 1356370}, {104035, 0, 1352975}}が含まれています。

ポジションが完全にクローズされると、numberOfPositions変数はゼロに設定され、ClearRectangles()が正常に実行されると、isClean変数はtrueに設定されます。これにより、配列がクリアされ、日付が現在の時刻に更新されます。これにより、新しい取引が開始された場合、その取引が配列に追加され、後続のアクションの処理が続行されるまでシステムは待機状態に戻ります。この時点では、要素配列は空です:{}。これによって、システムはこの例の冒頭で説明した状態と同様の状態に戻ります。後続の取引におけるインジケーターの動作を理解するためには、同じロジックが適用されることを確認する必要があります。現在の例では、「5」で参照されるように、操作は価格103690から開始されます。最初の例で示された動作がなぜ発生するのかを明確にするために、各ステップを注意深く追うことが重要です。説明は、エグジット取引価格と、アルゴリズムが「DEAL_ENTRY_OUT」取引の価格に最も近い価格の行を順番に削除する方法にリンクされています。


8.取引エキスパートアドバイザー(EA)との統合 

ストラテジーテスターでこのカスタムインジケーターを使用するには、2つの方法があります。最初の方法では、カスタムインジケーターを呼び出すEAまたは別のインジケーターをコンパイルします。そのためには、コンパイルされたファイル「PlotagemdeEntradasParciais.ex5」が「Indicators」フォルダ内に配置されていることを確認してください。次に、呼び出し元のOnInit()関数に以下のコード行を挿入します。その前に、グローバル変数handlePlotagemEntradasParciaisをint型として宣言することを忘れないでください。

   iCustom(_Symbol,PERIOD_CURRENT,"Plotagem de Entradas Parciais");
//--- if the handle is not created
   if(handlePlotagemEntradasParciais ==INVALID_HANDLE)
     {
      //--- Print an error message and exit with an error code
      PrintFormat("Failed to create indicator handle for symbol %s/%s, error code %d",
                  _Symbol,
                  EnumToString(_Period),
                  GetLastError());
      //--- The indicator is terminated prematurely
      return(INIT_FAILED);
     }

2番目のアプローチでは、EA内のこれらの行を変更する必要がないため、テストの際にはより便利なオプションとなります。標準的な方法でインジケーターをチャートにロードし、テンプレートを「Tester.tpl」として保存します(必要に応じて、同じ名前の既存のファイルを上書きします)。これにより、EAがテストされるたびにインジケーターが自動的に読み込まれるようになります。この方法は、ストラテジーテスターでチャート表示付きのビジュアルモードが有効な場合にのみ有効であることに注意してください。


9.結論

MetaTrader5向けの最も先進的で最新のプログラミング言語の1つであるMQL5を使用して、インジケーターを作成および利用する新しい方法を模索するために、部分的なエントリをプロットするカスタムインジケーターを作成しました。 


MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12576

添付されたファイル |
リプレイシステムの開発(第59回):新たな未来 リプレイシステムの開発(第59回):新たな未来
さまざまなアイデアを適切に理解することで、より少ない労力でより多くのことを実現できます。この記事では、サービスがチャートと対話する前にテンプレートを構成する必要がある理由について説明します。また、マウスポインタを改良し、より多くの機能を持たせることについても考察します。
リプレイシステムの開発(第58回):サービスへの復帰 リプレイシステムの開発(第58回):サービスへの復帰
リプレイ/シミュレーターサービスの開発と改良を一時中断していましたが、再開することにしました。ターミナルグローバルのようなリソースの使用をやめたため、いくつかの部分を完全に再構築しなければなりません。ご心配なく。このプロセスを詳細に説明することで、誰もが私たちのサービスの進展についていけるようにします。
初級から中級へ:変数(I) 初級から中級へ:変数(I)
多くの初心者プログラマーは、自分のコードが期待どおりに動作しない理由を理解するのに苦労します。コードを正しく機能させるためには、さまざまな要素が関わります。ただ関数や操作を組み合わせるだけでは、コードが適切に動作するとは限りません。今日は、単にコードをコピー&ペーストするのではなく、実際に正しくコードを書く方法を学んでみましょう。ここで提供される資料は教育目的のみに使用されるべきです。いかなる状況においても、提示された概念を学習し習得する以外の目的でアプリケーションを閲覧することは避けてください。
リプレイシステムの開発(第57回):テストサービスについて リプレイシステムの開発(第57回):テストサービスについて
注意点が1つあります。この記事にはサービスコードは含まれておらず、次の記事でのみ提供されます。ただし、実際の開発の出発点として同じコードを使用するため、この記事ではその説明をおこないます。ですので、注意深く、そして忍耐強く読んでください。毎日、すべてがさらに面白くなっていきますので、次の記事を楽しみにお待ちください。