English Русский 中文 Español Deutsch Português
preview
MetaTrader 5での取引の視覚的な評価と調整

MetaTrader 5での取引の視覚的な評価と調整

MetaTrader 5 |
91 16
Artyom Trishkin
Artyom Trishkin

内容



はじめに

次のような状況を想像してみましょう。ある口座では、比較的長期間にわたり、さまざまな金融商品で複数のエキスパートアドバイザー(EA)を使用して、場合によっては手動でも取引がおこなわれていました。そして現在、その成果を確認したいと考えています。もちろん、ターミナル上でAlt+Eキーを押すことで標準の取引レポートを確認することができます。また、チャート上に取引アイコンを表示し、ポジションのエントリーおよび決済時刻を確認することも可能です。しかし、取引の動きをより詳細に把握したい場合、ポジションがどのように開かれ、どのように閉じられたのかを見たい場合はどうでしょうか。各通貨ペアごと、またはすべてまとめて確認することもできます。開閉のタイミング、ストップ注文を置いたレベル、それが適切であったかどうかを把握することが可能です。さらに、「もしこうしていたら…」というシナリオを検討したい場合もあるでしょう。たとえば、異なるストップを設定した場合、異なるアルゴリズムや基準を用いた場合、トレーリングストップを使用した場合、あるいはストップをブレイクイーブンに移動した場合など、さまざまな「もし」を明確かつ視覚的にテストすることができます。

実は、このような課題を解決するための環境は既に整っています。必要なのは、口座の履歴、つまりすべての完了した取引をファイルに保存し、そのファイルを読み込んでポジションの開閉をおこなうEAをストラテジーテスターで実行するだけです。このEAに、ポジションの決済条件を変更するコードを追加すれば、取引がどのように変化するか、あるいは「もし…だったらどうなったか」を比較することができます。

この手法の利点は何でしょうか。口座上でしばらく稼働している取引の最適化や調整をおこなうための、もう一つのツールになるという点です。ビジュアルテストにより、特定の金融商品のポジションが正しく開かれたか、適切なタイミングで決済されたかを動的に確認できます。さらに、新しいアルゴリズムをEAに追加し、テストして結果を確認し、必要に応じて口座上で稼働しているEAを調整することも可能です。

以下のロジックをEAの動作として考えます。

  • 任意の金融商品のチャート上で起動された場合、現在の口座の取引履歴を収集し、すべての取引を1つのファイルに保存した後、何もせずに終了する。
  • ストラテジーテスターで起動された場合、ファイルに記録された取引履歴を読み込み、テスト中にそのファイルの取引を順番に再現し、ポジションを開閉する。

つまり、EAはまずチャート上で取引履歴のファイルを作成し、次にそのファイルから取引を実行することで、口座の取引を完全に再現する仕組みです。

次に、テスターで開かれるポジションに対して、異なるストップロスおよびテイクプロフィットの値を設定できるよう、EAに修正を加えていきます。



取引履歴をファイルに保存する

\MQL5\Experts\ターミナルディレクトリに、フォルダTradingByHistoryDealsを新規作成し、その中に新しいEAファイルTradingByHistoryDeals.mq5を作成します。

このEAは、どの銘柄とどのマジックナンバーをテストするかを選択できる機能を持たせる必要があります。口座上で複数のEAが異なる銘柄やマジックナンバーで稼働していた場合でも、設定で興味のある銘柄やマジックナンバー、あるいはすべてを一度に選択できるようにします。

//+------------------------------------------------------------------+
//|                                        TradingByHistoryDeals.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    string   InpTestedSymbol   =  "";      /* The symbol being tested in the tester        */ 
input    long     InpTestedMagic    =  -1;      /* The magic number being tested in the tester  */ 
sinput   bool     InpShowDataInLog  =  false;   /* Show collected data in the log               */ 

銘柄とマジックナンバーのデフォルト値はそれぞれ、空の文字列と-1です。この値を使用すると、EAは取引履歴を銘柄やマジックナンバーで分類せず、口座全体の取引履歴をテストします。3つ目の文字列は、EAがファイルに保存されたすべての取引の内容を操作ログに出力するかどうかを指定します。これにより、保存されたデータの正確性を明確に確認できます。

各取引は、さまざまな取引プロパティで構成される一連のパラメータです。最も簡単な方法は、すべての取引プロパティを1つの構造体にまとめて記録することです。大量の取引をファイルに書き込む場合は、構造体の配列を使用します。その配列をファイルに保存することで、すべての取引データを効率的に管理できます。MQL5言語はこの処理に必要な機能をすべて備えています。取引履歴をファイルに保存するロジックは以下の通りです。

  • 過去の取引履歴をループで順に処理する
  • 次の取引を取得し、そのデータを構造体に書き込む
  • 作成した取引構造体を取引配列に保存する
  • ループの最後で、準備した構造体の配列をファイルに保存する

すべての追加コード(構造体、クラス、列挙型など)は、別ファイルにまとめて記述します。このファイルは、将来の銘柄取引オブジェクトのクラス名に合わせて命名します。

同じフォルダ内に、インクルードファイル「SymbolTrade.mqh」を新規作成します。

このファイルでは、履歴ファイルを格納するフォルダ名、ファイル名、ファイルパスのマクロ置換を実装し、必要な標準ライブラリファイルをすべてインクルードします

//+------------------------------------------------------------------+
//|                                                  SymbolTrade.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define   DIRECTORY  "TradingByHistoryDeals"
#define   FILE_NAME  "HistoryDealsData.bin"
#define   PATH       DIRECTORY+"\\"+FILE_NAME

#include <Arrays\ArrayObj.mqh>
#include <Trade\Trade.mqh>

次に、取引構造体を記述します。

//+------------------------------------------------------------------+
//|  Deal structure. Used to create a deal history file              |
//+------------------------------------------------------------------+
struct SDeal
  {
   ulong             ticket;                 // Deal ticket
   long              order;                  // Order the deal is based on
   long              pos_id;                 // Position ID
   long              time_msc;               // Time in milliseconds
   datetime          time;                   // Time
   double            volume;                 // Volume
   double            price;                  // Price
   double            profit;                 // Profit
   double            commission;             // Deal commission
   double            swap;                   // Accumulated swap at closing
   double            fee;                    // Payment for the deal is accrued immediately after the deal is completed
   double            sl;                     // Stop Loss level
   double            tp;                     // Take Profit level
   ENUM_DEAL_TYPE    type;                   // Type
   ENUM_DEAL_ENTRY   entry;                  // Position change method
   ENUM_DEAL_REASON  reason;                 // Deal reason or source
   long              magic;                  // EA ID
   int               digits;                 // Symbol digits
   ushort            symbol[16];             // Symbol
   ushort            comment[64];            // Deal comment
   ushort            external_id[256];       // Deal ID in an external trading system (on the exchange)
   
//--- Set string properties
   bool              SetSymbol(const string deal_symbol)          { return(::StringToShortArray(deal_symbol, symbol)==deal_symbol.Length());                }
   bool              SetComment(const string deal_comment)        { return(::StringToShortArray(deal_comment, comment)==deal_comment.Length());             }
   bool              SetExternalID(const string deal_external_id) { return(::StringToShortArray(deal_external_id, external_id)==deal_external_id.Length()); }
                       
//--- Return string properties
   string            Symbol(void)                                 { return(::ShortArrayToString(symbol));                                                   }
   string            Comment(void)                                { return(::ShortArrayToString(comment));                                                  }
   string            ExternalID(void)                             { return(::ShortArrayToString(external_id));                                              }
  };

取引構造体をファイルに保存するため、そしてファイルに書き込めるのは単純型の構造体のみであるため(FileWriteArray()を参照)、すべての文字列型変数はushort配列に置き換える必要があります。また、構造体の文字列プロパティを書き込んだり、読み戻したりするためのメソッドも作成する必要があります。

作成する構造体は、取引履歴をファイルに保存し、記録された履歴をファイルから読み取るためにのみ使用されます。EA本体では、取引クラスのオブジェクトを格納するリストを作成します。リスト内で必要な取引を検索したり、配列を並べ替えたりする際には、検索対象となる取引プロパティを指定する必要があります。取引を検索するには、オブジェクトリストを目的のプロパティでソートしておく必要があります。

ここで、取引オブジェクトの検索に使用できるプロパティの一覧を示します。

//--- Deal sorting types
enum ENUM_DEAL_SORT_MODE
  {
   SORT_MODE_DEAL_TICKET = 0,          // Mode of comparing/sorting by a deal ticket
   SORT_MODE_DEAL_ORDER,               // Mode of comparing/sorting by the order a deal is based on
   SORT_MODE_DEAL_TIME,                // Mode of comparing/sorting by a deal time
   SORT_MODE_DEAL_TIME_MSC,            // Mode of comparing/sorting by a deal time in milliseconds
   SORT_MODE_DEAL_TYPE,                // Mode of comparing/sorting by a deal type
   SORT_MODE_DEAL_ENTRY,               // Mode of comparing/sorting by a deal direction
   SORT_MODE_DEAL_MAGIC,               // Mode of comparing/sorting by a deal magic number
   SORT_MODE_DEAL_REASON,              // Mode of comparing/sorting by a deal reason or source
   SORT_MODE_DEAL_POSITION_ID,         // Mode of comparing/sorting by a position ID
   SORT_MODE_DEAL_VOLUME,              // Mode of comparing/sorting by a deal volume
   SORT_MODE_DEAL_PRICE,               // Mode of comparing/sorting by a deal price
   SORT_MODE_DEAL_COMMISSION,          // Mode of comparing/sorting by commission
   SORT_MODE_DEAL_SWAP,                // Mode of comparing/sorting by accumulated swap on close
   SORT_MODE_DEAL_PROFIT,              // Mode of comparing/sorting by a deal financial result
   SORT_MODE_DEAL_FEE,                 // Mode of comparing/sorting by a deal fee
   SORT_MODE_DEAL_SL,                  // Mode of comparing/sorting by Stop Loss level
   SORT_MODE_DEAL_TP,                  // Mode of comparing/sorting by Take Profit level
   SORT_MODE_DEAL_SYMBOL,              // Mode of comparing/sorting by a name of a traded symbol
   SORT_MODE_DEAL_COMMENT,             // Mode of comparing/sorting by a deal comment
   SORT_MODE_DEAL_EXTERNAL_ID,         // Mode of comparing/sorting by a deal ID in an external trading system
   SORT_MODE_DEAL_TICKET_TESTER,       // Mode of comparing/sorting by a deal ticket in the tester
   SORT_MODE_DEAL_POS_ID_TESTER,       // Mode of comparing/sorting by a position ID in the tester 
  };

ここで、標準的な取引プロパティに加えて、さらに2つのプロパティがあります。それは、テスター内の取引チケットとテスター内のポジションIDです。ポイントは、テスター上で実際の取引データに基づいて取引をおこなう際、テスターで開かれるポジションおよびそれに対応する取引には、全く別のチケット番号とIDが付与されるという点です。実際の取引とテスター内の取引(およびID)を比較できるようにするためには、取引オブジェクトのプロパティにテスター内のチケット番号とポジションIDを保存しておき、保存したデータを使って、テスター内の取引と取引履歴上の実際の取引を比較できるようにする必要があります。

ここで一旦ファイルの話は置いておき、少し前に作成したEAファイルに移ります。次に、取引履歴のすべての取引構造体を格納するための構造体配列を追加しましょう。

//--- input parameters
input    string   InpTestedSymbol   =  "";      /* The symbol being tested in the tester        */ 
input    long     InpTestedMagic    =  -1;      /* The magic number being tested in the tester  */ 
sinput   bool     InpShowDataInLog  =  false;   /* Show collected data in the log               */ 

//--- global variables
SDeal          ExtArrayDeals[]={};

そして、過去の取引を操作するための関数を記述します。

以下は、取引履歴を配列に保存する関数です。

//+------------------------------------------------------------------+
//| Save deals from history into the array                           |
//+------------------------------------------------------------------+
int SaveDealsToArray(SDeal &array[], bool logs=false)
  {
//--- deal structure
   SDeal deal={};
   
//--- request the deal history in the interval from the very beginning to the current moment 
   if(!HistorySelect(0, TimeCurrent()))
     {
      Print("HistorySelect() failed. Error ", GetLastError());
      return 0;
     }
   
//--- total number of deals in the list 
   int total=HistoryDealsTotal(); 

//--- handle each deal 
   for(int i=0; i<total; i++) 
     { 
      //--- get the ticket of the next deal (the deal is automatically selected to get its properties)
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket==0)
         continue;
      
      //--- save only balance and trading deals
      ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket, DEAL_TYPE);
      if(deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL && deal_type!=DEAL_TYPE_BALANCE)
         continue;
      
      //--- save the deal properties in the structure
      deal.ticket=ticket;
      deal.type=deal_type;
      deal.order=HistoryDealGetInteger(ticket, DEAL_ORDER);
      deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket, DEAL_ENTRY);
      deal.reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(ticket, DEAL_REASON);
      deal.time=(datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
      deal.time_msc=HistoryDealGetInteger(ticket, DEAL_TIME_MSC);
      deal.pos_id=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
      deal.volume=HistoryDealGetDouble(ticket, DEAL_VOLUME);
      deal.price=HistoryDealGetDouble(ticket, DEAL_PRICE);
      deal.profit=HistoryDealGetDouble(ticket, DEAL_PROFIT);
      deal.commission=HistoryDealGetDouble(ticket, DEAL_COMMISSION);
      deal.swap=HistoryDealGetDouble(ticket, DEAL_SWAP);
      deal.fee=HistoryDealGetDouble(ticket, DEAL_FEE);
      deal.sl=HistoryDealGetDouble(ticket, DEAL_SL);
      deal.tp=HistoryDealGetDouble(ticket, DEAL_TP);
      deal.magic=HistoryDealGetInteger(ticket, DEAL_MAGIC);
      deal.SetSymbol(HistoryDealGetString(ticket, DEAL_SYMBOL));
      deal.SetComment(HistoryDealGetString(ticket, DEAL_COMMENT));
      deal.SetExternalID(HistoryDealGetString(ticket, DEAL_EXTERNAL_ID));
      deal.digits=(int)SymbolInfoInteger(deal.Symbol(), SYMBOL_DIGITS);
      
      //--- increase the array and
      int size=(int)array.Size();
      ResetLastError();
      if(ArrayResize(array, size+1, total)!=size+1)
        {
         Print("ArrayResize() failed. Error ", GetLastError());
         continue;
        }
      //--- save the deal in the array
      array[size]=deal;
      //--- if allowed, display the description of the saved deal to the journal
      if(logs)
         DealPrint(deal, i);
     }
//--- return the number of deals stored in the array
   return (int)array.Size();
  }

関数のコードには詳細なコメントが付けられています。この関数は、取引履歴全体を口座の最初から現在時点まで取得し、順次各履歴取引を取得します。その後、取引プロパティを構造体のフィールドに保存し、構造体変数を配列に格納します。取引履歴のループが終了した時点で、作成された取引構造体の配列のサイズを返します。また、配列への取引記録の進行状況を監視するために、処理した各取引を操作ログに出力することも可能です。その場合、関数を呼び出す際にlogsフラグをtrueに設定します。

以下は、取引の配列からすべての取引を操作ログに出力する関数です。

//+------------------------------------------------------------------+
//| Display deals from the array to the journal                      |
//+------------------------------------------------------------------+
void DealsArrayPrint(SDeal &array[])
  {
   int total=(int)array.Size();
//--- if an empty array is passed, report this and return 'false'
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return;
     }
//--- In a loop through the deal array, print out a description of each deal
   for(int i=0; i<total; i++)
     {
      DealPrint(array[i], i);
     }
  }

操作ログに取引の説明を表示するためのいくつかの関数を実装してみましょう。

以下は、トランザクションタイプの説明を返す関数です。

//+------------------------------------------------------------------+
//| Return the deal type description                                 |
//+------------------------------------------------------------------+
string DealTypeDescription(const ENUM_DEAL_TYPE type)
  {
   switch(type)
     {
      case DEAL_TYPE_BUY                     :  return "Buy";
      case DEAL_TYPE_SELL                    :  return "Sell";
      case DEAL_TYPE_BALANCE                 :  return "Balance";
      case DEAL_TYPE_CREDIT                  :  return "Credit";
      case DEAL_TYPE_CHARGE                  :  return "Additional charge";
      case DEAL_TYPE_CORRECTION              :  return "Correction";
      case DEAL_TYPE_BONUS                   :  return "Bonus";
      case DEAL_TYPE_COMMISSION              :  return "Additional commission";
      case DEAL_TYPE_COMMISSION_DAILY        :  return "Daily commission";
      case DEAL_TYPE_COMMISSION_MONTHLY      :  return "Monthly commission";
      case DEAL_TYPE_COMMISSION_AGENT_DAILY  :  return "Daily agent commission";
      case DEAL_TYPE_COMMISSION_AGENT_MONTHLY:  return "Monthly agent commission";
      case DEAL_TYPE_INTEREST                :  return "Interest rate";
      case DEAL_TYPE_BUY_CANCELED            :  return "Canceled buy deal";
      case DEAL_TYPE_SELL_CANCELED           :  return "Canceled sell deal";
      case DEAL_DIVIDEND                     :  return "Dividend operations";
      case DEAL_DIVIDEND_FRANKED             :  return "Franked (non-taxable) dividend operations";
      case DEAL_TAX                          :  return "Tax charges";
      default                                :  return "Unknown deal type: "+(string)type;
     }
  }

関数に渡される取引タイプに応じて、対応する文字列が表示されます。

以下は、ポジション変更メソッドの説明を返す関数です。

//+------------------------------------------------------------------+
//| Return position change method                                    |
//+------------------------------------------------------------------+
string DealEntryDescription(const ENUM_DEAL_ENTRY entry)
  {
   switch(entry)
     {
      case DEAL_ENTRY_IN      :  return "Entry In";
      case DEAL_ENTRY_OUT     :  return "Entry Out";
      case DEAL_ENTRY_INOUT   :  return "Entry InOut";
      case DEAL_ENTRY_OUT_BY  :  return "Entry OutBy";
      default                 :  return "Unknown entry: "+(string)entry;
     }
  }

関数に渡されるポジションの変更方法に応じて、対応する文字列が表示されます。

以下は、取引の説明を返す関数です。

//+------------------------------------------------------------------+
//| Return deal description                                          |
//+------------------------------------------------------------------+
string DealDescription(SDeal &deal, const int index)
  {
   string indexs=StringFormat("% 5d", index);
   if(deal.type!=DEAL_TYPE_BALANCE)
      return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f",
                          indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type),
                          deal.pos_id, deal.Symbol(), deal.magic, deal.digits, deal.price,
                          TimeToString(deal.time, TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.digits, deal.sl, deal.digits, deal.tp));
   else
      return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s",
                          indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type),
                          deal.profit, AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.time)));
  }

処理中の取引が残高調整用の取引である場合、その取引の説明は定型形式で表示されます。

    0: deal #190715988 Entry In, type Balance 3000.00 USD at 2024.09.13 21:48

それ以外の場合、取引の説明は別の形式で表示されます。

    1: deal #190724678 Entry In, type Buy, Position #225824633 USDCHF (magic 600), Price 0.84940 at 2024.09.13 23:49:03, sl 0.84811, tp 0.84983

以下は、取引の説明を操作ログに出力する関数です。

//+------------------------------------------------------------------+
//| Print deal data in the journal                                   |
//+------------------------------------------------------------------+
void DealPrint(SDeal &deal, const int index)
  {
   Print(DealDescription(deal, index));
  }

ここではすべてが明確です。DealDescription()関数から取得した文字列を出力するだけです。

取引の配列をファイルに書き込んだり、ファイルから読み取ったりするための関数を記述しましょう。

以下は、書き込み用にファイルを開く関数です。

//+------------------------------------------------------------------+
//| Open a file for writing, return a handle                         |
//+------------------------------------------------------------------+
bool FileOpenToWrite(int &handle)
  {
   ResetLastError();
   handle=FileOpen(PATH, FILE_WRITE|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
     {
      PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError());
      return false;
     }
//--- successful
   return true;
  }

以下は、読み取り用にファイルを開く関数です。

//+------------------------------------------------------------------+
//| Open a file for reading, return a handle                         |
//+------------------------------------------------------------------+
bool FileOpenToRead(int &handle)
  {
   ResetLastError();
   handle=FileOpen(PATH, FILE_READ|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
     {
      PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError());
      return false;
     }
//--- successful
   return true;
  }

これらの関数は、ファイルを読み書き用に開く役割を持っています。仮パラメータには、ファイルハンドル用の変数が参照渡しされます。ファイルが正常に開かれた場合はtrueを返し、エラーの場合はfalseを返します。

以下は、配列からファイルに取引データを保存する関数です。

//+------------------------------------------------------------------+
//| Save deal data from the array to the file                        |
//+------------------------------------------------------------------+
bool FileWriteDealsFromArray(SDeal &array[], ulong &file_size)
  {
//--- if an empty array is passed, report this and return 'false'
   if(array.Size()==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
     
//--- open the file for writing, get its handle
   int handle=INVALID_HANDLE;
   if(!FileOpenToWrite(handle))
      return false;
   
//--- move the file pointer to the end of the file
   bool res=true;
   ResetLastError();
   res&=FileSeek(handle, 0, SEEK_END);
   if(!res)
      PrintFormat("%s: FileSeek(SEEK_END) failed. Error %d",__FUNCTION__, GetLastError());
   
//--- write the array data to the end of the file 
   file_size=0;
   res&=(FileWriteArray(handle, array)==array.Size());
   if(!res)
      PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
   else
      file_size=FileSize(handle);

//--- close the file 
   FileClose(handle);
   return res;
  }

この関数は、ファイルに保存する必要がある構造体の配列を受け取ります。また、作成されるファイルのサイズを受け取る変数は、関数の仮パラメータで参照渡しされます。ファイルを開き、ファイルポインタをファイルの末尾に移動し、構造体の配列からデータをファイルに書き込み(ポインタ位置から開始)、書き込みが完了したらファイルを閉じます。

構造体の配列をファイルに保存した後は、このファイルから再び配列に読み戻すことが可能です。読み戻した配列をもとに、取引のリストを作成したり、ストラテジーテスター内での取引操作に利用したりできます。

以下は、ファイルから配列に取引データを読み込む関数です。

//+------------------------------------------------------------------+
//| Load the deal data from the file into the array                  |
//+------------------------------------------------------------------+
bool FileReadDealsToArray(SDeal &array[], ulong &file_size)
  {
//--- open the file for reading, get its handle
   int handle=INVALID_HANDLE;
   if(!FileOpenToRead(handle))
      return false;
   
//--- move the file pointer to the end of the file 
   bool res=true;
   ResetLastError();
   
//--- read data from the file into the array
   file_size=0;
   res=(FileReadArray(handle, array)>0);
   if(!res)
      PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
   else
      file_size=FileSize(handle);

//--- close the file 
   FileClose(handle);
   return res;
  }

上記で作成した関数に基づいて、取引の履歴を読み取ってファイルに書き込む関数を記述します。

以下は、過去の取引をファイルとして準備する関数です。

//+------------------------------------------------------------------+
//| Prepare a file with history deals                                |
//+------------------------------------------------------------------+
bool PreparesDealsHistoryFile(SDeal &deals_array[])
  {
//--- save all the account deals in the deal array
   int total=SaveDealsToArray(deals_array);
   if(total==0)
      return false;
      
//--- write the deal array data to the file
   ulong file_size=0;
   if(!FileWriteDealsFromArray(deals_array, file_size))
      return false;
      
//--- print in the journal how many deals were read and saved to the file, the path to the file and its size
   PrintFormat("%u deals were saved in an array and written to a \"%s\" file of %I64u bytes in size",
               deals_array.Size(), "TERMINAL_COMMONDATA_PATH\\Files\\"+ PATH, file_size);
   
//--- now, to perform a check, we will read the data from the file into the array
   ArrayResize(deals_array, 0, total);
   if(!FileReadDealsToArray(deals_array, file_size))
      return false;
      
//--- print in the journal how many bytes were read from the file and the number of deals received in the array
   PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, deals_array.Size());
   return true;
  }

コード内のコメントによりロジックが明確になります。この関数はOnInit()ハンドラで起動され、さらなる作業のために取引を含むファイルを準備します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- If the EA is not running in the tester
   if(!MQLInfoInteger(MQL_TESTER))
     {
      //--- prepare a file with all historical deals
      if(!PreparesDealsHistoryFile(ExtArrayDeals))
         return(INIT_FAILED);
         
      //--- print all deals in the journal after loading them from the file 
      if(InpShowDataInLog)
         DealsArrayPrint(ExtArrayDeals);
         
      //--- get the first balance deal, create the message text and display it using Alert
      SDeal    deal=ExtArrayDeals[0];
      long     leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
      double   start_money=deal.profit;
      datetime first_time=deal.time;
      string   start_time=TimeToString(deal.time, TIME_DATE);
      string   message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage);
      
      //--- notify via alert of the recommended parameters of the strategy tester for starting the test
      Alert(message);
     }
     
//--- All is successful
   return(INIT_SUCCEEDED);
  }

すべての過去取引をファイルに保存することに加えて、推奨されるテスター設定に関するアラートも表示されます。このアラートには、初期残高、レバレッジ、テスト開始時刻の情報が含まれます。たとえば、次のようになります。

Alert: Now you can run testing
Interval: 2024.09.13 - current date
Initial deposit: 3000.00, leverage 1:500

このようなテスター設定を使用することで、テスター上で得られる最終結果は、実際の取引結果に最も近いものになります。

\MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqhファイルに定義されている取引構造体は、ファイルへの取引保存および保存済み履歴の読み込みのみに使用されます。作業を続けるためには、取引クラスを作成する必要があります。このクラスのオブジェクトは、リスト内に格納されます。リスト自体は、テスター用の取引クラスオブジェクト内に保存されます。さらに、取引オブジェクトもクラスオブジェクトとして作成され、それぞれ独自のリストに格納されます。各取引オブジェクトは特定の銘柄に属することによって決定されます。取引に関与する銘柄の数が、取引オブジェクトの数を決定します。各取引オブジェクトは、自身の銘柄に対する取引のみを格納するリストと、標準ライブラリのCTradeクラスオブジェクトを持ちます。これにより、各CTradeクラスの取引オブジェクトを、取引対象の銘柄の条件に応じてカスタマイズすることが可能になります。

\MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqhファイルに取引クラスを作成しましょう。

//+------------------------------------------------------------------+
//| Deal class. Used for trading in the strategy tester              |
//+------------------------------------------------------------------+
class CDeal : public CObject
  {
protected:
//--- Integer properties
   ulong             m_ticket;            // Deal ticket. Unique number assigned to each deal
   long              m_order;             // Deal order number
   datetime          m_time;              // Deal execution time
   long              m_time_msc;          // Deal execution time in milliseconds since 01.01.1970
   ENUM_DEAL_TYPE    m_type;              // Deal type
   ENUM_DEAL_ENTRY   m_entry;             // Deal entry - entry in, entry out, reverse
   long              m_magic;             // Magic number for a deal (see ORDER_MAGIC)
   ENUM_DEAL_REASON  m_reason;            // Deal execution reason or source
   long              m_pos_id;            // The ID of the position opened, modified or closed by the deal
   
//--- Real properties
   double            m_volume;            // Deal volume
   double            m_price;             // Deal price
   double            m_commission;        // Deal commission
   double            m_swap;              // Accumulated swap when closing
   double            m_profit;            // Deal financial result
   double            m_fee;               // Fee for making a deal charged immediately after performing a deal
   double            m_sl;                // Stop Loss level
   double            m_tp;                // Take Profit level

//--- String properties
   string            m_symbol;            // Name of the symbol for which the deal is executed
   string            m_comment;           // Deal comment
   string            m_external_id;       // Deal ID in an external trading system (on the exchange)
   
//--- Additional properties
   int               m_digits;            // Symbol digits
   double            m_point;             // Symbol point
   ulong             m_ticket_tester;     // Position ticket in the tester
   long              m_pos_id_tester;     // Position ID in the tester
   
public:
//--- Set deal propertie
   void              SetTicket(const ulong ticket)             { this.m_ticket=ticket;          }
   void              SetOrder(const long order)                { this.m_order=order;            }
   void              SetTime(const datetime time)              { this.m_time=time;              }
   void              SetTimeMsc(const long value)              { this.m_time_msc=value;         }
   void              SetType(const ENUM_DEAL_TYPE type)        { this.m_type=type;              }
   void              SetEntry(const ENUM_DEAL_ENTRY entry)     { this.m_entry=entry;            }
   void              SetMagic(const long magic)                { this.m_magic=magic;            }
   void              SetReason(const ENUM_DEAL_REASON reason)  { this.m_reason=reason;          }
   void              SetPositionID(const long id)              { this.m_pos_id=id;              }
   void              SetVolume(const double volume)            { this.m_volume=volume;          }
   void              SetPrice(const double price)              { this.m_price=price;            }
   void              SetCommission(const double commission)    { this.m_commission=commission;  }
   void              SetSwap(const double swap)                { this.m_swap=swap;              }
   void              SetProfit(const double profit)            { this.m_profit=profit;          }
   void              SetFee(const double fee)                  { this.m_fee=fee;                }
   void              SetSL(const double sl)                    { this.m_sl=sl;                  }
   void              SetTP(const double tp)                    { this.m_tp=tp;                  }
   void              SetSymbol(const string symbol)            { this.m_symbol=symbol;          }
   void              SetComment(const string comment)          { this.m_comment=comment;        }
   void              SetExternalID(const string ext_id)        { this.m_external_id=ext_id;     }
   void              SetTicketTester(const ulong ticket)       { this.m_ticket_tester=ticket;   }
   void              SetPosIDTester(const long pos_id)         { this.m_pos_id_tester=pos_id;   }
   
//--- Return deal properties
   ulong             Ticket(void)                        const { return this.m_ticket;          }
   long              Order(void)                         const { return this.m_order;           }
   datetime          Time(void)                          const { return this.m_time;            }
   long              TimeMsc(void)                       const { return this.m_time_msc;        }
   ENUM_DEAL_TYPE    TypeDeal(void)                      const { return this.m_type;            }
   ENUM_DEAL_ENTRY   Entry(void)                         const { return this.m_entry;           }
   long              Magic(void)                         const { return this.m_magic;           }
   ENUM_DEAL_REASON  Reason(void)                        const { return this.m_reason;          }
   long              PositionID(void)                    const { return this.m_pos_id;          }
   double            Volume(void)                        const { return this.m_volume;          }
   double            Price(void)                         const { return this.m_price;           }
   double            Commission(void)                    const { return this.m_commission;      }
   double            Swap(void)                          const { return this.m_swap;            }
   double            Profit(void)                        const { return this.m_profit;          }
   double            Fee(void)                           const { return this.m_fee;             }
   double            SL(void)                            const { return this.m_sl;              }
   double            TP(void)                            const { return this.m_tp;              }
   string            Symbol(void)                        const { return this.m_symbol;          }
   string            Comment(void)                       const { return this.m_comment;         }
   string            ExternalID(void)                    const { return this.m_external_id;     }

   int               Digits(void)                        const { return this.m_digits;          }
   double            Point(void)                         const { return this.m_point;           }
   ulong             TicketTester(void)                  const { return this.m_ticket_tester;   }
   long              PosIDTester(void)                   const { return this.m_pos_id_tester;   }
   
//--- Compare two objects by the property specified in 'mode'
   virtual int       Compare(const CObject *node, const int mode=0) const
                       {
                        const CDeal *obj=node;
                        switch(mode)
                          {
                           case SORT_MODE_DEAL_TICKET          :  return(this.Ticket() > obj.Ticket()          ?  1  :  this.Ticket() < obj.Ticket()           ? -1  :  0);
                           case SORT_MODE_DEAL_ORDER           :  return(this.Order() > obj.Order()            ?  1  :  this.Order() < obj.Order()             ? -1  :  0);
                           case SORT_MODE_DEAL_TIME            :  return(this.Time() > obj.Time()              ?  1  :  this.Time() < obj.Time()               ? -1  :  0);
                           case SORT_MODE_DEAL_TIME_MSC        :  return(this.TimeMsc() > obj.TimeMsc()        ?  1  :  this.TimeMsc() < obj.TimeMsc()         ? -1  :  0);
                           case SORT_MODE_DEAL_TYPE            :  return(this.TypeDeal() > obj.TypeDeal()      ?  1  :  this.TypeDeal() < obj.TypeDeal()       ? -1  :  0);
                           case SORT_MODE_DEAL_ENTRY           :  return(this.Entry() > obj.Entry()            ?  1  :  this.Entry() < obj.Entry()             ? -1  :  0);
                           case SORT_MODE_DEAL_MAGIC           :  return(this.Magic() > obj.Magic()            ?  1  :  this.Magic() < obj.Magic()             ? -1  :  0);
                           case SORT_MODE_DEAL_REASON          :  return(this.Reason() > obj.Reason()          ?  1  :  this.Reason() < obj.Reason()           ? -1  :  0);
                           case SORT_MODE_DEAL_POSITION_ID     :  return(this.PositionID() > obj.PositionID()  ?  1  :  this.PositionID() < obj.PositionID()   ? -1  :  0);
                           case SORT_MODE_DEAL_VOLUME          :  return(this.Volume() > obj.Volume()          ?  1  :  this.Volume() < obj.Volume()           ? -1  :  0);
                           case SORT_MODE_DEAL_PRICE           :  return(this.Price() > obj.Price()            ?  1  :  this.Price() < obj.Price()             ? -1  :  0);
                           case SORT_MODE_DEAL_COMMISSION      :  return(this.Commission() > obj.Commission()  ?  1  :  this.Commission() < obj.Commission()   ? -1  :  0);
                           case SORT_MODE_DEAL_SWAP            :  return(this.Swap() > obj.Swap()              ?  1  :  this.Swap() < obj.Swap()               ? -1  :  0);
                           case SORT_MODE_DEAL_PROFIT          :  return(this.Profit() > obj.Profit()          ?  1  :  this.Profit() < obj.Profit()           ? -1  :  0);
                           case SORT_MODE_DEAL_FEE             :  return(this.Fee() > obj.Fee()                ?  1  :  this.Fee() < obj.Fee()                 ? -1  :  0);
                           case SORT_MODE_DEAL_SL              :  return(this.SL() > obj.SL()                  ?  1  :  this.SL() < obj.SL()                   ? -1  :  0);
                           case SORT_MODE_DEAL_TP              :  return(this.TP() > obj.TP()                  ?  1  :  this.TP() < obj.TP()                   ? -1  :  0);
                           case SORT_MODE_DEAL_SYMBOL          :  return(this.Symbol() > obj.Symbol()          ?  1  :  this.Symbol() < obj.Symbol()           ? -1  :  0);
                           case SORT_MODE_DEAL_COMMENT         :  return(this.Comment() > obj.Comment()        ?  1  :  this.Comment() < obj.Comment()         ? -1  :  0);
                           case SORT_MODE_DEAL_EXTERNAL_ID     :  return(this.ExternalID()  >obj.ExternalID()  ?  1  :  this.ExternalID()  <obj.ExternalID()   ? -1  :  0);
                           case SORT_MODE_DEAL_TICKET_TESTER   :  return(this.TicketTester()>obj.TicketTester()?  1  :  this.TicketTester()<obj.TicketTester() ? -1  :  0);
                           case SORT_MODE_DEAL_POS_ID_TESTER   :  return(this.PosIDTester() >obj.PosIDTester() ?  1  :  this.PosIDTester() <obj.PosIDTester()  ? -1  :  0);
                           default                             :  return(WRONG_VALUE);
                          }
                       }
   
//--- Constructors/destructor
                     CDeal(const ulong ticket, const string symbol) : m_ticket(ticket), m_symbol(symbol), m_ticket_tester(0), m_pos_id_tester(0)
                       { this.m_digits=(int)::SymbolInfoInteger(symbol, SYMBOL_DIGITS); this.m_point=::SymbolInfoDouble(symbol, SYMBOL_POINT); }
                     CDeal(void) {}
                    ~CDeal(void) {}
  };

このクラスは、以前作成した取引構造体をほぼ完全に再現しています。構造体の取引プロパティに加えて、追加のプロパティとして、取引がおこなわれた銘柄のDigits(小数桁数)とPoint(最小価格変動単位)が追加されています。これにより、取引の説明を出力する際の処理が簡略化されます。なぜなら、これらのデータは取引オブジェクトの生成時にコンストラクタで設定されるため、取引にアクセスする際に毎回プロパティを取得する必要がなくなるからです。
さらに、Compare()という仮想メソッドが作成されています。これは、2つの取引オブジェクトを比較するためのもので、リスト内の取引を指定プロパティで検索・ソートする際に使用されます。

次に、取引銘柄クラスを作成します。このクラスは、オブジェクトのプロパティに設定された銘柄を使用しておこなわれた取引のリストを保持します。ストラテジーテスターは、このクラスから取引を取得し、コピー用に使用します。一般的に、このクラスは、口座上でおこなわれた取引を銘柄ごとにテスターで再現するための基盤となります。

//+------------------------------------------------------------------+
//|  Class for trading by symbol                                     |
//+------------------------------------------------------------------+
CDeal DealTmp; // Temporary deal object for searching by properties

class CSymbolTrade : public CObject
  {
private:
   int               m_index_next_deal;                  // Index of the next deal that has not yet been handled
   int               m_deals_processed;                  // Number of handled deals
protected:
   MqlTick           m_tick;                             // Tick structure
   CArrayObj         m_list_deals;                       // List of deals carried out by symbol
   CTrade            m_trade;                            // Trading class
   string            m_symbol;                           // Symbol name
public:
//--- Return the list of deals
   CArrayObj        *GetListDeals(void)                  { return(&this.m_list_deals);       }
   
//--- Set a symbol
   void              SetSymbol(const string symbol)      { this.m_symbol=symbol;             }
   
//--- (1) Set and (2) returns the number of handled deals
   void              SetNumProcessedDeals(const int num) { this.m_deals_processed=num;       }
   int               NumProcessedDeals(void)       const { return this.m_deals_processed;    }
   
//--- Add a deal to the deal array
   bool              AddDeal(CDeal *deal);
   
//--- Return the deal (1) by time in seconds, (2) by index in the list,
//--- (3) opening deal by position ID, (4) current deal in the list
   CDeal            *GetDealByTime(const datetime time);
   CDeal            *GetDealByIndex(const int index);
   CDeal            *GetDealInByPosID(const long pos_id);
   CDeal            *GetDealCurrent(void);
   
//--- Return (1) the number of deals in the list, (2) the index of the current deal in the list
   int               DealsTotal(void)              const { return this.m_list_deals.Total(); }
   int               DealCurrentIndex(void)        const { return this.m_index_next_deal;    }
   
//--- Return (1) symbol and (2) object description
   string            Symbol(void)                  const { return this.m_symbol;             }
   string            Description(void) const
                       {
                        return ::StringFormat("%s trade object. Total deals: %d", this.Symbol(), this.DealsTotal() );
                       }

//--- Return the current (1) Bid and (2) Ask price, time in (3) seconds, (4) milliseconds
   double            Bid(void);
   double            Ask(void);
   datetime          Time(void);
   long              TimeMsc(void);
   
//--- Open (1) long, (2) short position, (3) close a position by ticket
   ulong             Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment);
   ulong             Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment);
   bool              ClosePos(const ulong ticket);

//--- Return the result of comparing the current time with the specified one
   bool              CheckTime(const datetime time)      { return(this.Time()>=time);        }
//--- Sets the index of the next deal
   void              SetNextDealIndex(void)              { this.m_index_next_deal++;         }
   
//--- OnTester handler. Returns the number of deals processed by the tester.
   double            OnTester(void)
                       {
                        ::PrintFormat("Symbol %s: Total deals: %d, number of processed deals: %d", this.Symbol(), this.DealsTotal(), this.NumProcessedDeals());
                        return this.m_deals_processed;
                       }

//--- Compares two objects to each other (comparison by symbol only)
   virtual int       Compare(const CObject *node, const int mode=0) const
                       {
                        const CSymbolTrade *obj=node;
                        return(this.Symbol()>obj.Symbol() ? 1 : this.Symbol()<obj.Symbol() ? -1 : 0);
                       }
//--- Constructors/destructor
                     CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {}
                     CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0)
                       {
                        this.m_trade.SetMarginMode();
                        this.m_trade.SetTypeFillingBySymbol(this.m_symbol);
                       }
                    ~CSymbolTrade(void) {}
  };

いくつかのメソッドを見てみましょう。

  • SetNumProcessedDeals()NumProcessedDeals():SetNumProcessedDeals()は、テスターがファイルから取得した取引リストですでに処理した履歴取引の数を設定します。NumProcessedDeals()は、その数を返すメソッドです。これらは、履歴取引の処理の有効性を確認し、テスターが処理した取引の最終統計を取得するために必要です。
  • GetDealCurrent():現在、テスターが処理する必要がある履歴取引へのポインタを返すメソッドです。返された取引は、処理済みとしてマークされます。
  • DealCurrentIndex():テスターが現在処理対象として選択している履歴取引のインデックスを返すメソッドです。
  • SetNextDealIndex():現在の履歴取引の処理が完了した後、次に処理する取引のインデックスを設定します。リスト内のすべての履歴取引はミリ秒単位の時刻順にソートされているため、テスターが前の取引の処理を終えた後に、次の取引のインデックスを設定できます。これにより、履歴内の取引を順次、指定された時刻に従って処理することができます。
  • CheckTime():現在の履歴取引のプロパティに設定された時刻が、テスターの時刻に達しているかをチェックします。ロジックは次の通りです。処理対象となる取引がテスターで選択されます。テスターの時刻が取引に記録された時刻よりも小さい間は、特に何もせず次のティックに進みます。テスターの時刻が現在選択されている取引の時刻と同じかそれ以上になった場合(テスターの時刻は取引の時刻と必ずしも一致しないため、「以上」のチェックもおこないます)、取引はそのタイプやポジションの変化方法に応じてテスターで処理されます。その後、この取引は処理済みとしてマークされ、次に処理すべき取引のインデックスが設定されます。こうして、メソッドが管理する待機状態は、次の取引に対しても継続されます。
  • OnTester()ハンドラは、標準EAのOnTester()ハンドラから呼び出されます。操作ログには銘柄名、履歴取引の総数、およびテスターが処理した取引数が表示され、取引オブジェクト銘柄ごとに処理済み取引数が返されます。

このクラスには、デフォルトとパラメトリックの2つのコンストラクタがあります。

パラメトリックコンストラクタの仮パラメータでは、オブジェクト作成時に使用する取引オブジェクトの銘柄名が渡されます。一方で、CTradeクラスの取引オブジェクトには、現在の口座設定に基づいた証拠金計算モードおよび、取引オブジェクト銘柄の設定に従った注文執行タイプが渡されます

//--- Constructors/destructor
                     CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {}
                     CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0)
                       {
                        this.m_trade.SetMarginMode();
                        this.m_trade.SetTypeFillingBySymbol(this.m_symbol);
                       }

以下は、取引を取引配列に追加するメソッドです。

//+------------------------------------------------------------------+
//| CSymbolTrade::Add a trade to the trades array                    |
//+------------------------------------------------------------------+
bool CSymbolTrade::AddDeal(CDeal *deal)
  {
//--- If the list already contains a deal with the deal ticket passed to the method, return 'true'
   this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET);
   if(this.m_list_deals.Search(deal)>WRONG_VALUE)
      return true;
   
//--- Add a pointer to the deal to the list in sorting order by time in milliseconds
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   if(!this.m_list_deals.InsertSort(deal))
     {
      ::PrintFormat("%s: Failed to add deal", __FUNCTION__);
      return false;
     }
//--- All is successful
   return true;
  }

取引オブジェクトへのポインタがメソッドに渡されます。もし同じチケット番号を持つ取引がすでにリスト内に存在する場合は、trueが返されます。存在しない場合は、リストをミリ秒単位の取引時刻でソートし、取引をミリ秒単位の時刻順に並べてリストに追加します。

以下は、秒単位の時間で取引オブジェクトへのポインタを返すメソッドです。

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the deal object by time in seconds          |
//+------------------------------------------------------------------+
CDeal* CSymbolTrade::GetDealByTime(const datetime time)
  {
   DealTmp.SetTime(time);
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   int index=this.m_list_deals.Search(&DealTmp);
   return this.m_list_deals.At(index);
  }

このメソッドは、必要な時刻を受け取ります。まず、渡された時刻を一時的な取引オブジェクトに設定します。次に、リストをミリ秒単位の時刻でソートし、一時オブジェクトに設定された時刻と同じ時刻を持つ取引のインデックスを検索します。検索が完了すると、見つかったインデックスに対応するリスト内の取引へのポインタが返されます。リスト内に該当する時刻の取引が存在しない場合は、インデックスは-1となり、リストからはNULLが返されます。

興味深いことに、取引は秒単位の時刻で検索されますが、リストはミリ秒単位でソートされています。テストによると、リストを秒単位でソートすると、存在するはずの取引が含まれない場合があります。これは、おそらく同じ秒の中に複数の取引が存在し、それぞれの時刻がミリ秒単位で異なるためです。そのため、同じ秒内に複数の取引がある場合は、以前に処理された取引へのポインタが返されることになります。

以下は、ポジションIDによって取引へのポインタを返すメソッドです。

//+------------------------------------------------------------------+
//|CSymbolTrade::Return the opening trade by position ID             |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealInByPosID(const long pos_id)
  {
   int total=this.m_list_deals.Total();
   for(int i=0; i<total; i++)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL || deal.PositionID()!=pos_id)
         continue;
      if(deal.Entry()==DEAL_ENTRY_IN)
         return deal;
     }
   return NULL;
  }

このメソッドは、開かれたポジションに対応する取引を見つけるためのポジションIDを受け取ります。次に、取引リストをループで順に検索し、ポジションIDがメソッドに渡されたIDと一致する取引を取得します。その中で、ポジションの変更方法がエントリー(DEAL_ENTRY_IN)に等しい取引のポインタを返します。

以下は、リスト内のインデックスによって取引オブジェクトへのポインタを返すメソッドです。

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the deal object by index in the list        |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealByIndex(const int index)
  {
   return this.m_list_deals.At(index);
  }

メソッドに渡されたインデックスによって、リスト内のオブジェクトへのポインタを返すだけです。インデックスが正しくない場合は、NULLが返されます。

以下は、現在の取引インデックスが指す取引へのポインタを返すメソッドです。

//+------------------------------------------------------------------+
//| Return the deal pointed to by the current deal index             |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealCurrent(void)
  {
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   return this.GetDealByIndex(this.m_index_next_deal);
  }

取引のリストはミリ秒単位の時間でソートされ、インデックスがm_index_next_dealクラス変数に書き込まれた取引へのポインタが返されます。

以下は、現在のBid価格を返すメソッドです

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current Bid price                       |
//+------------------------------------------------------------------+
double CSymbolTrade::Bid(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.bid;
  }

最後のティックのデータをm_tick価格構造体に取得し、そこからBid価格を返します。

以下は、現在のAsk価格を返すメソッドです。

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current Ask price                       |
//+------------------------------------------------------------------+
double CSymbolTrade::Ask(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.ask;
  }

最後のティックのデータをm_tick価格構造に取得し、そこからAsk価格を返します。

以下は、現在の時刻を秒単位で返すメソッドです。

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current time in seconds                 |
//+------------------------------------------------------------------+
datetime CSymbolTrade::Time(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.time;
  }

最後のティックのデータをm_tick価格構造体に取得し、そこから時間を返します。

以下は、現在の時刻をミリ秒単位で返すメソッドです。

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current time in milliseconds            |
//+------------------------------------------------------------------+
long CSymbolTrade::TimeMsc(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.time_msc;
  }

最後のティックのデータをm_tick価格構造に取得し、そこからミリ秒単位で時間を返します。

以下は、ロングポジションを開くメソッドです。

//+------------------------------------------------------------------+
//| CSymbolTrade::Open a long position                               |
//+------------------------------------------------------------------+
ulong CSymbolTrade::Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment)
  {
   this.m_trade.SetExpertMagicNumber(magic);
   if(!this.m_trade.Buy(volume, this.m_symbol, 0, sl, tp, comment))
     {
      return 0;
     }
   return this.m_trade.ResultOrder();
  }

このメソッドは、開くロングポジションのパラメータを受け取ります。必要なポジションのマジックナンバーは取引オブジェクトに設定され、指定されたパラメータでロングポジションを開く注文が送信されます。ポジションを開くのに失敗した場合は0が返され、成功した場合は、開いたポジションに基づく注文チケット番号が返されます。

以下は、ショートポジションを開くメソッドです。

//+------------------------------------------------------------------+
//| CSymbolTrade::Open a short position                              |
//+------------------------------------------------------------------+
ulong CSymbolTrade::Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment)
  {
   this.m_trade.SetExpertMagicNumber(magic);
   if(!this.m_trade.Sell(volume, this.m_symbol, 0, sl, tp, comment))
     {
      return 0;
     }
   return this.m_trade.ResultOrder();
  }

前のメソッドと似ていますが、ショートポジションが開かれます。

以下は、チケットでポジションを決済するメソッドです。

//+------------------------------------------------------------------+
//| CSymbolTrade::Close position by ticket                           |
//+------------------------------------------------------------------+
bool CSymbolTrade::ClosePos(const ulong ticket)
  {
   return this.m_trade.PositionClose(ticket);
  }

CTradeクラス取引オブジェクトのPositionClose()メソッドを呼び出した結果を返します。

これで、取引銘柄クラスの準備は完了です。次に、ファイルに保存された過去の取引の処理をEAに実装してみましょう。


テスター内のファイルから取引履歴を分析する

では、\MQL5\Experts\TradingByHistoryDeals\TradingByHistoryDeals.mq5 EAファイルに移り、新規作成した取引銘柄クラスの一時オブジェクトを追加しましょう。このオブジェクトは、該当するオブジェクトを保持しているリスト内から目的のオブジェクトを見つける際に必要になります。

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    string   InpTestedSymbol   =  "";      /* The symbol being tested in the tester        */ 
input    long     InpTestedMagic    =  -1;      /* The magic number being tested in the tester  */ 
sinput   bool     InpShowDataInLog  =  false;   /* Show collected data in the log               */ 

//--- global variables
CSymbolTrade   SymbTradeTmp;
SDeal          ExtArrayDeals[]={};
CArrayObj      ExtListSymbols;

私たちは、履歴取引の配列を持っています。この配列をもとに、取引オブジェクトのリストを作成できます。その中には、各取引オブジェクトの銘柄に属する取引のリストが格納されます。取引配列には、取引を記述する構造体が格納されます。取引オブジェクトは、取引オブジェクトのリストを格納するため、新しい取引オブジェクトを作成し、構造体で記述された取引のフィールドからプロパティを設定する関数を作成する必要があります。

//+------------------------------------------------------------------+
//| Create a deal object from the structure                          |
//+------------------------------------------------------------------+
CDeal *CreateDeal(SDeal &deal_str)
  {
//--- If failed to create an object, inform of the error in the journal and return NULL
   CDeal *deal=new CDeal(deal_str.ticket, deal_str.Symbol());
   if(deal==NULL)
     {
      PrintFormat("%s: Error. Failed to create deal object");
      return NULL;
     }
//--- fill in the deal properties from the structure fields
   deal.SetOrder(deal_str.order);               // Order the deal was based on
   deal.SetPositionID(deal_str.pos_id);         // Position ID
   deal.SetTimeMsc(deal_str.time_msc);          // Time in milliseconds
   deal.SetTime(deal_str.time);                 // Time
   deal.SetVolume(deal_str.volume);             // Volume
   deal.SetPrice(deal_str.price);               // Price
   deal.SetProfit(deal_str.profit);             // Profit
   deal.SetCommission(deal_str.commission);     // Deal commission
   deal.SetSwap(deal_str.swap);                 // Accumulated swap when closing
   deal.SetFee(deal_str.fee);                   // Fee for making a deal charged immediately after performing a deal
   deal.SetSL(deal_str.sl);                     // Stop Loss level
   deal.SetTP(deal_str.tp);                     // Take Profit level
   deal.SetType(deal_str.type);                 // Type
   deal.SetEntry(deal_str.entry);               // Position change method
   deal.SetReason(deal_str.reason);             // Deal execution reason or source
   deal.SetMagic(deal_str.magic);               // EA ID
   deal.SetComment(deal_str.Comment());         // Deal comment
   deal.SetExternalID(deal_str.ExternalID());   // Deal ID in an external trading system (on the exchange)
//--- Return the pointer to a created object
   return deal;
  }

この関数は取引構造体を受け取り、新しい取引オブジェクトが作成され、そのプロパティに構造フィールドの値が設定されます。
この関数は、新しく作成されたオブジェクトへのポインタを返します。オブジェクトの作成中にエラーが発生した場合は、NULLを返します。

以下は、取引銘柄オブジェクトのリストを作成する関数です。

//+------------------------------------------------------------------+
//| Create an array of used symbols                                  |
//+------------------------------------------------------------------+
bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols)
  {
   bool res=true;                      // result
   int total=(int)array_deals.Size();  // total number of deals in the array
   
//--- if the deal array is empty, return 'false'
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
   
//--- in a loop through the deal array
   CSymbolTrade *SymbolTrade=NULL;
   for(int i=0; i<total; i++)
     {
      //--- get the next deal and, if it is neither buy nor sell, move on to the next one
      SDeal deal_str=array_deals[i];
      if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL)
         continue;
      
      //--- find a trading object in the list whose symbol is equal to the deal symbol
      string symbol=deal_str.Symbol();
      SymbTradeTmp.SetSymbol(symbol);
      list_symbols.Sort();
      int index=list_symbols.Search(&SymbTradeTmp);
      
      //--- if the index of the desired object in the list is -1, there is no such object in the list
      if(index==WRONG_VALUE)
        {
         //--- we create a new trading symbol object and, if creation fails,
         //--- add 'false' to the result and move on to the next deal
         SymbolTrade=new CSymbolTrade(symbol);
         if(SymbolTrade==NULL)
           {
            res &=false;
            continue;
           }
         //--- if failed to add a symbol trading object to the list,
         //--- delete the newly created object, add 'false' to the result
         //--- and we move on to the next deal
         if(!list_symbols.Add(SymbolTrade))
           {
            delete SymbolTrade;
            res &=false;
            continue;
           }
        }
      //--- otherwise, if the trading object already exists in the list, we get it by index
      else
        {
         SymbolTrade=list_symbols.At(index);
         if(SymbolTrade==NULL)
            continue;
        }
         
      //--- if the current deal is not yet in the list of deals of the symbol trading object
      if(SymbolTrade.GetDealByTime(deal_str.time)==NULL)
        {
         //--- create a deal object according to its sample structure
         CDeal *deal=CreateDeal(deal_str);
         if(deal==NULL)
           {
            res &=false;
            continue;
           }
         //--- add the result of adding the deal object to the list of deals of a symbol trading object to the result value
         res &=SymbolTrade.AddDeal(deal);
        }
     }
//--- return the final result of creating trading objects and adding deals to their lists
   return res;
  }

関数のロジックはコメントで詳細に説明されています。まず、履歴取引のリストをループで順に解析します。それぞれの取引の銘柄を確認し、その銘柄に対応する取引オブジェクトがまだ存在しない場合は、新しい取引オブジェクトを作成してリストに保存します。すでに存在する場合は、リストからその取引の銘柄取引オブジェクトへのポインタを取得します。次に、同様にして、取引オブジェクトの取引リスト内にその取引が存在するかを確認し、存在しなければリストに追加します。こうして、すべての履歴取引をループ処理することで、銘柄ごとに取引オブジェクトのリストを作成できます。各取引オブジェクトは、その銘柄に属する取引のリストを保持しています。

取引オブジェクトのリストは、次の関数を使用して操作ログに送信されます。

//+------------------------------------------------------------------+
//| Display a list of symbol trading objects in the journal          |
//+------------------------------------------------------------------+
void SymbolsArrayPrint(CArrayObj *list_symbols)
  {
   int total=list_symbols.Total();
   if(total==0)
      return;
   Print("Symbols used in trading:");
   for(int i=0; i<total; i++)
     {
      string index=StringFormat("% 3d", i+1);
      CSymbolTrade *obj=list_symbols.At(i);
      if(obj==NULL)
         continue;
      PrintFormat("%s. %s",index, obj.Description());
     }
  }

取引銘柄オブジェクトのリストをループして、次のオブジェクトを取得し、その説明を操作ログに表示します。操作ログでは次のようになります。

Symbols used in trading:
  1. AUDUSD trade object. Total deals: 218
  2. EURJPY trade object. Total deals: 116
  3. EURUSD trade object. Total deals: 524
  4. GBPUSD trade object. Total deals: 352
  5. NZDUSD trade object. Total deals: 178
  6. USDCAD trade object. Total deals: 22
  7. USDCHF trade object. Total deals: 250
  8. USDJPY trade object. Total deals: 142
  9. XAUUSD trade object. Total deals: 118

これで、取引クラスのオブジェクトができました。取引の説明を返す関数を追加します。

//+------------------------------------------------------------------+
//| Return deal description                                          |
//+------------------------------------------------------------------+
string DealDescription(CDeal *deal, const int index)
  {
   string indexs=StringFormat("% 5d", index);
   if(deal.TypeDeal()!=DEAL_TYPE_BALANCE)
      return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f",
                          indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()),
                          deal.PositionID(), deal.Symbol(), deal.Magic(), deal.Digits(), deal.Price(),
                          TimeToString(deal.Time(), TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.Digits(), deal.SL(), deal.Digits(), deal.TP()));
   else
      return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s",
                          indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()),
                          deal.Profit(), AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.Time())));
  }

この関数は、取引構造体の説明を返すまったく同じ関数のロジックを完全に繰り返します。ただし、ここでは、取引構造体ではなく、取引オブジェクトへのポインタが関数に渡されます。

それでは、論理的な結論に至るまでOnInit()ハンドラを作成しましょう。

EAがストラテジーテスターで起動された場合の処理を追加します。具体的には、取引オブジェクトのリストを作成し、取引に使用された各銘柄にアクセスして、それぞれの履歴を読み込みます。また、必要に応じて、ストラテジーテスター内でその銘柄のチャートウィンドウを開くようにします。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- If the EA is not running in the tester
   if(!MQLInfoInteger(MQL_TESTER))
     {
      //--- prepare a file with all historical deals
      if(!PreparesDealsHistoryFile(ExtArrayDeals))
         return(INIT_FAILED);
         
      //--- print all deals in the journal after loading them from the file 
      if(InpShowDataInLog)
         DealsArrayPrint(ExtArrayDeals);
         
      //--- get the first balance deal, create the message text and display it using Alert
      SDeal    deal=ExtArrayDeals[0];
      long     leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
      double   start_money=deal.profit;
      datetime first_time=deal.time;
      string   start_time=TimeToString(deal.time, TIME_DATE);
      string   message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage);
      
      //--- notify via alert of the recommended parameters of the strategy tester for starting the test
      Alert(message);
     }
//--- The EA has been launched in the tester
   else
     {
      //--- read data from the file into the array
      ulong file_size=0;
      ArrayResize(ExtArrayDeals, 0);
      if(!FileReadDealsToArray(ExtArrayDeals, file_size))
        {
         PrintFormat("Failed to read file \"%s\". Error %d", FILE_NAME, GetLastError());
         return(INIT_FAILED);
        }
         
      //--- report the number of bytes read from the file and writing the deals array in the journal.
      PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, ExtArrayDeals.Size());
     }
     
//--- Create a list of trading objects by symbols from the array of historical deals
   if(!CreateListSymbolTrades(ExtArrayDeals, &ExtListSymbols))
     {
      Print("Errors found while creating symbol list");
      return(INIT_FAILED);
     }
//--- Print the created list of deals in the journal
   SymbolsArrayPrint(&ExtListSymbols);
   
//--- Access each symbol to start downloading historical data
//--- and opening charts of traded symbols in the strategy tester
   datetime array[];
   int total=ExtListSymbols.Total();

   for(int i=0; i<total; i++)
     {
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      CopyTime(obj.Symbol(), PERIOD_CURRENT, 0, 1, array);
     }
     
//--- All is successful
   return(INIT_SUCCEEDED);
  }

EAのOnDeinit()ハンドラで、EAを使用して作成された配列とリストをクリアします。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- clear the created lists and arrays
   ExtListSymbols.Clear();
   ArrayFree(ExtArrayDeals);
  }

テスターでEAのOnTick()ハンドラ内でファイルからの取引リストを処理します

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- work only in the strategy tester
   if(!MQLInfoInteger(MQL_TESTER))
      return;
      
//---  Handle the list of deals from the file in the tester
   TradeByHistory(InpTestedSymbol, InpTestedMagic);
  }

この関数についてさらに詳しく考えてみましょう。一般的に、過去の取引を処理するためのロジックは、当初次のように提示されました。

  1. ティック時刻を取得します。
  2. その時刻の取引を取得します。
  3. テスターで取引を処理します。

一見すると単純で論理的な構造は、実装してみると完全に機能しませんでした。理由は、ストラテジーテスター内では、ティックの時刻が必ずしも取引の時刻と一致しないからです。ミリ秒単位であっても同様です。その結果、全ティックに基づくテストをおこなった場合、同じサーバーから取得した実際のティックであっても、取引が見落とされることがありました。ティックの時刻はわかっていても、実際にその時刻に取引が存在することが確実でも、テスターにはそのティックが存在せず、取引を認識できません。しかし、取引時刻の前後にティックは存在しています。したがって、論理はティックとその時刻ではなく、取引を基準に構築できます。

  1. 取引はリスト内でミリ秒単位の発生時刻順にソートされます。最初の取引のインデックスを現在の取引インデックスとして設定します。
  2. 現在の取引インデックスで取引を選択し、その取引の時刻を取得します。
  3. この時刻のティックが到達するのを待ちます。
    1. ティック時刻が取引時刻よりも前の場合は、次のティックまで待ちます。
    2. ティック時刻が取引時刻と同じかそれ以上の場合は、取引を処理し、すでに処理済みであることを登録します。そして、次の取引のインデックスを現在の取引インデックスとして設定します。
  4. テストが終了するまで、上記の手順を繰り返します。

この構造により、各取引の時刻を待って順次実行することが可能になります。この場合、取引価格には注目せず、時刻が来たら取引をコピーするだけです。テスター内のティック時刻が実際の取引時刻より少し遅れても問題ありません。重要なのは、取引を正確にコピーすることです。テスターがすでに処理した取引は、取引オブジェクトの「テスター内ポジションチケット」プロパティが0以外の値で示されます。0の場合は、まだテスターで処理されていないことを意味します。この取引がテスターで実行されると、取引が属するポジションのチケット番号がこのプロパティに設定されます。

上記のロジックを実装する関数を追加しましょう。

//+------------------------------------------------------------------+
//| Trading by history                                               |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // number of trading objects in the list
   
//--- in a loop by all symbol trading objects
   for(int i=0; i<total; i++)
     {
      //--- get another trading object
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- sort the deal by magic number and symbol
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- sort the deal by type (only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>0)
         continue;
      
      //--- if the deal time has not yet arrived, move to the next trading object of the next symbol
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- in case of a market entry deal
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- open a position by deal type
         double sl=0;
         double tp=0;
         ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : 
                       type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0);
         
         //--- if a position is opened (we received its ticket)
         if(ticket>0)
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- get the position ID in the tester and write it to the properties of the deal object
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- in case of a market exit deal
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- get a deal a newly opened position is based on
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- get the position ticket in the tester from the properties of the opening deal
         //--- if the ticket is zero, then most likely the position in the tester is already closed
         ulong ticket_tester=deal_in.TicketTester();
         if(ticket_tester==0)
           {
            PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
            obj.SetNextDealIndex();
            continue;
           }
         //--- if the position is closed by ticket
         if(obj.ClosePos(ticket_tester))
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- if a ticket is now set in the deal object, then the deal has been successfully handled -
      //--- set the deal index in the list to the next deal
      if(deal.TicketTester()>0)
        {
         obj.SetNextDealIndex();
        }
     }
  }

このコードは、ファイルに記録されている口座の元の取引を完全にコピーします。すべてのポジションはストップ注文なしで開かれます。つまり、StopLossやTakeProfitの値は、実際の取引からポジションを開くメソッドにコピーされません。これにより、取引の追跡が簡略化されます。なぜなら、決済された取引もすべてリストに含まれており、テスターは取引を処理する際に、ポジションがどのように決済されたか(StopLossかTakeProfitか)に関係なく処理できるからです。

EAをコンパイルし、チャート上で起動します。その結果、HistoryDealsData.binファイルは、クライアントターミナルの共有フォルダ内に作成されます。具体的には、「C:\Users\[ユーザー名]\AppData\Roaming\MetaQuotes\Terminal\Common\Files」のパス下にあるTradingByHistoryDealsサブフォルダに保存されます。また、チャート上には、設定したテスターの条件に関するメッセージ付きのアラートが表示されます。

テスター設定で指定された日付範囲、初期入金額、レバレッジを選択し、テスターでEAを実行してみましょう。

取引されたすべての銘柄とマジックナンバーに対してテストを実行します。

結局、取引全体で550ドルの損失が発生しました。異なるストップ注文を設定したらどうなるのでしょうか。

これを確認してみましょう。


ストップ注文を調整する

EAを\MQL5\Experts\TradingByHistoryDeals\フォルダにTradingByHistoryDeals_SLTP.mq5として保存します。

テストメソッドの列挙を追加し、ストップ注文パラメータを設定するためのグループを追加して入力をグループ別に分割します。また、StopLossとTakeProfitの値をそれらを介して取引オブジェクトに渡すための2つの新しいグローバル変数も追加します。

//+------------------------------------------------------------------+
//|                                   TradingByHistoryDeals_SLTP.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "SymbolTrade.mqh"

enum ENUM_TESTING_MODE
  {
   TESTING_MODE_ORIGIN,    /* Original trading                          */ 
   TESTING_MODE_SLTP,      /* Specified StopLoss and TakeProfit values  */ 
  };

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    group             "Strategy parameters"
input    string            InpTestedSymbol   =  "";                  /* The symbol being tested in the tester        */ 
input    long              InpTestedMagic    =  -1;                  /* The magic number being tested in the tester  */ 
sinput   bool              InpShowDataInLog  =  false;               /* Show collected data in the log               */ 

input    group             "Stops parameters"
input    ENUM_TESTING_MODE InpTestingMode    =  TESTING_MODE_ORIGIN; /* Testing Mode                                 */ 
input    int               InpStopLoss       =  300;                 /* StopLoss in points                           */ 
input    int               InpTakeProfit     =  500;                 /* TakeProfit in points                         */ 

//--- global variables
CSymbolTrade   SymbTradeTmp;
SDeal          ExtArrayDeals[]={};
CArrayObj      ExtListSymbols;
int            ExtStopLoss;
int            ExtTakeProfit;


OnInit()ハンドラで、入力によって設定されたストップ注文の値を調整し、変数に書き込みます。

int OnInit()
  {
//--- Adjust the stops
   ExtStopLoss  =(InpStopLoss<1   ? 0 : InpStopLoss);
   ExtTakeProfit=(InpTakeProfit<1 ? 0 : InpTakeProfit);
   
//--- If the EA is not running in the tester

銘柄に設定されたStopLevelを基準としてStopLossとTakeProfitの価格の正しい値を計算する関数を追加します。

//+------------------------------------------------------------------+
//| Return correct StopLoss relative to StopLevel                    |
//+------------------------------------------------------------------+
double CorrectStopLoss(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int stop_loss, const int spread_multiplier=2)
  {
   if(stop_loss==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL))
      return 0;
   int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK));
   return
     (order_type==ORDER_TYPE_BUY ?
      NormalizeDouble(fmin(price-lv*pt, price-stop_loss*pt), dg) :
      NormalizeDouble(fmax(price+lv*pt, price+stop_loss*pt), dg)
     );
  }
//+------------------------------------------------------------------+
//| Return correct TakeProfit relative to StopLevel                  |
//+------------------------------------------------------------------+
double CorrectTakeProfit(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int take_profit, const int spread_multiplier=2)
  {
   if(take_profit==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL))
      return 0;
   int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK));
   return
     (order_type==ORDER_TYPE_BUY ?
      NormalizeDouble(fmax(price+lv*pt, price+take_profit*pt), dg) :
      NormalizeDouble(fmin(price-lv*pt, price-take_profit*pt), dg)
     );
  }
//+------------------------------------------------------------------+
//| Return StopLevel in points                                       |
//+------------------------------------------------------------------+
int StopLevel(const string symbol_name, const int spread_multiplier)
  {
   int spread=(int)SymbolInfoInteger(symbol_name, SYMBOL_SPREAD);
   int stop_level=(int)SymbolInfoInteger(symbol_name, SYMBOL_TRADE_STOPS_LEVEL);
   return(stop_level==0 ? spread*spread_multiplier : stop_level);
  }

StopLossおよびTakeProfitレベルを設定する際に、ストップ注文の価格は、現在の価格からStopLevelの距離よりも近くしてはいけません。銘柄のStopLevelの値が0の場合、使用されるStopLevelサイズは、銘柄に設定された2つまたは3つのスプレッドに等しくなります。これらの機能では、ダブルスプレッド乗数を使用します。この値は関数の仮パラメータで渡され、デフォルト値は2です。乗数の値を変更する必要がある場合は、関数を呼び出すときに、必要な別の値を関数に渡す必要があります。関数はStopLossとTakeProfitの正しい価格を返します。

TradeByHistory()取引履歴取引関数で、指定されたストップ注文値でのテストを選択した場合は、テスターでの取引モードを考慮し、StopLoss値とTakeProfit値を設定する新しいコードブロックを挿入します。ポジション決済ブロックでは、テストタイプが「オリジナル取引」の場合にのみポジションを決済する必要があります。指定されたストップ注文値でのテストを選択した場合、取引の決済は無視されます。テスターは指定されたStopLossとTakeProfitの値に基づいてポジションを自動的に決済します。ストップ注文で取引する場合、決済取引が処理されたら、それを処理済みとしてマークし、次の取引に進むだけで済みます。

//+------------------------------------------------------------------+
//| Trading by history                                               |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // number of trading objects in the list
   
//--- in a loop by all symbol trading objects
   for(int i=0; i<total; i++)
     {
      //--- get another trading object
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- sort the deal by magic number and symbol
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- sort the deal by type (only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>0)
         continue;
      
      //--- if the deal time has not yet arrived, move to the next trading object of the next symbol
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- in case of a market entry deal
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- set the sizes of stop orders depending on the stop setting method
         double sl=0;
         double tp=0;
         if(InpTestingMode==TESTING_MODE_SLTP)
           {
            ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
            sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
            tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
           }
         //--- open a position by deal type
         ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : 
                       type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0);
         
         //--- if a position is opened (we received its ticket)
         if(ticket>0)
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- get the position ID in the tester and write it to the properties of the deal object
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- in case of a market exit deal
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- get a deal a newly opened position is based on
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- get the position ticket in the tester from the properties of the opening deal
         //--- if the ticket is zero, then most likely the position in the tester is already closed
         ulong ticket_tester=deal_in.TicketTester();
         if(ticket_tester==0)
           {
            PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
            obj.SetNextDealIndex();
            continue;
           }
         //--- if we reproduce the original trading history in the tester,
         if(InpTestingMode==TESTING_MODE_ORIGIN)
           {
            //--- if the position is closed by ticket
            if(obj.ClosePos(ticket_tester))
              {
               //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object
               obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
               deal.SetTicketTester(ticket_tester);
              }
           }
         //--- otherwise, in the tester we work with stop orders placed according to different algorithms, and closing deals are skipped
         //--- accordingly, simply increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object
         else
           {
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- if a ticket is now set in the deal object, then the deal has been successfully handled -
      //--- set the deal index in the list to the next deal
      if(deal.TicketTester()>0)
        {
         obj.SetNextDealIndex();
        }
     }
  }

EAのOnTester()ハンドラで、テスターで処理された取引の合計数を計算して返します。

//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester(void)
  {
//--- calculate and return the total number of deals handled in the tester
   double ret=0.0;
   int total=ExtListSymbols.Total();
   for(int i=0; i<total; i++)
     {
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj!=NULL)
         ret+=obj.OnTester();
     }
   return(ret);
  }

さらに、各銘柄取引オブジェクトの独自のOnTester()ハンドラがここで呼び出され、そのデータを操作ログに出力します。テストの終了時に、テスターログに次のメッセージが表示されます。

2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol AUDUSD: Total deals: 218, number of processed deals: 216
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol EURJPY: Total deals: 116, number of processed deals: 114
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol EURUSD: Total deals: 524, number of processed deals: 518
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol GBPUSD: Total deals: 352, number of processed deals: 350
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol NZDUSD: Total deals: 178, number of processed deals: 176
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol USDCAD: Total deals: 22, number of processed deals: 22
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol USDCHF: Total deals: 250, number of processed deals: 246
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol USDJPY: Total deals: 142, number of processed deals: 142
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol XAUUSD: Total deals: 118, number of processed deals: 118
2025.01.22 23:49:15.951 Core 1  final balance 3591.70 pips
2025.01.22 23:49:15.951 Core 1  OnTester result 1902

EAをコンパイルし、同じテスト設定で実行しますが、テストの種類を[Specified StopLoss and TakeProfit values]として指定し、StopLossとTakeProfitの値をそれぞれ100ポイントと500ポイントに設定します。

前回のテストでは、元の取引をテストした結果、USD 550の損失が出ました。今回は、すべてのポジションのStopLossを100ポイント、TakeProfitを500ポイントに置き換えたところ、590ポイントの利益が得られました。これは、単純にストップ注文を置き換えただけで、取引対象の銘柄ごとの特性は考慮していません。もし、各取引シンボルごとに適切なストップ注文サイズを選択すれば、テストグラフをより平準化できる可能性が高いです。


結論

この記事では、「もしも…」のスタイルで取引履歴を使った小さな実験をおこないました。こうした実験は、自身の取引スタイルを変更するための興味深いアイデアにつながる可能性があります。次回の記事では、さまざまなポジションのトレーリングストップを組み込んだ別の実験をおこないます。さらに面白くなることが期待されます。

ここで説明したすべてのEAとクラスは以下に添付されています。ダウンロードして学習したり、自身の取引口座で実験することが可能です。アーカイブフォルダは、MQL5クライアントターミナルのディレクトリにそのまま解凍すれば、必要なサブフォルダにすべてのファイルが配置されます。

以下は本稿で使用されているプログラムです。

#
名前
 種類 詳細
1
SymbolTrade.mqh
クラスライブラリ
取引構造とクラスライブラリ、銘柄取引クラス
2
TradingByHistoryDeals.mq5
EA
テスターの口座で実行された取引を表示するためのEA
3
TradingByHistoryDeals_SLTP.mq5
EA
SLとTPを使用してテスターの口座で実行された取引と取引を表示および変更するためのEA
4
MQL5.zip
ZIPアーカイブ
上記のファイルのアーカイブは、クライアントターミナルのMQL5ディレクトリに解凍できます。


MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/16952

添付されたファイル |
SymbolTrade.mqh (53.86 KB)
MQL5.zip (22.3 KB)
最後のコメント | ディスカッションに移動 (16)
fxsaber
fxsaber | 31 1月 2025 において 15:32
Artyom Trishkin #:

何が間違いなのか?

読み込む前のファイルが配列より小さければ、配列のサイズは変わりません。

ArrayCopyを使っても同じようなエラーになることがあります。
Artyom Trishkin
Artyom Trishkin | 31 1月 2025 において 15:34
fxsaber #:
良い機能を無視している

利点は何ですか?

fxsaber
fxsaber | 31 1月 2025 において 15:36
Artyom Trishkin #:

どんな利点があるのか?

簡潔さと実行スピード(完全にMQ側)。

fxsaber
fxsaber | 31 1月 2025 において 15:52
Artyom Trishkin #:

標準的なセルフプリントとセルフフィリングの構造を示してください。

ほぼ標準的(MQフィールドは共有)。
Artyom Trishkin
Artyom Trishkin | 31 1月 2025 において 16:11
fxsaber #:

簡潔さと実行スピード(完全にMQ側)。

ありがとう。欠場
Pythonの価格変動離散化手法 Pythonの価格変動離散化手法
Python + MQL5を使用した価格離散化手法を見ていきます。本記事では、バー生成に関する幅広い手法を実装したPythonライブラリの開発経験についご紹介します。クラシックなボリュームバーやレンジバーから、よりエキゾチックな練行足やカギ足といった手法までを網羅します。スリーラインブレイクローソク足やレンジバーの統計分析をおこないながら、価格を離散的に表現する新たな方法を探っていきます。
循環単為生殖アルゴリズム(CPA) 循環単為生殖アルゴリズム(CPA)
本記事では、新しい集団最適化アルゴリズムである循環単為生殖アルゴリズム(CPA: Cyclic Parthenogenesis Algorithm)を取り上げます。本アルゴリズムは、アブラムシ特有の繁殖戦略に着想を得ています。CPAは、単為生殖と有性生殖という2つの繁殖メカニズムを組み合わせるほか、個体群のコロニー構造を活用し、コロニー間の移動も可能にしています。このアルゴリズムの主要な特徴は、異なる繁殖戦略間の適応的な切り替えと、飛行メカニズムを通じたコロニー間の情報交換システムです。
初級から中級まで:テンプレートとtypename(IV) 初級から中級まで:テンプレートとtypename(IV)
本記事では、前回の記事の最後で提示した問題の解決方法について詳しく解説します。そのために、データunionのテンプレートを作成できるタイプのテンプレートを設計しようという試みがおこなわれました。
初級から中級まで:テンプレートとtypename(III) 初級から中級まで:テンプレートとtypename(III)
本記事では、トピックの第一部について解説します。この内容は初心者にとって理解がやや難しい部分があります。さらなる混乱を避けて正しく理解していただくために、説明を段階的に分けて進めます。本記事ではその第一段階に焦点を当てます。ただし、記事の最後では行き詰まりに見えるかもしれませんが、実際には次の記事でより理解しやすくなる状況への一歩を踏み出す形になります。