MetaTrader 5での取引の視覚的な評価と調整
内容
はじめに
次のような状況を想像してみましょう。ある口座では、比較的長期間にわたり、さまざまな金融商品で複数のエキスパートアドバイザー(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); }
この関数についてさらに詳しく考えてみましょう。一般的に、過去の取引を処理するためのロジックは、当初次のように提示されました。
- ティック時刻を取得します。
- その時刻の取引を取得します。
- テスターで取引を処理します。
一見すると単純で論理的な構造は、実装してみると完全に機能しませんでした。理由は、ストラテジーテスター内では、ティックの時刻が必ずしも取引の時刻と一致しないからです。ミリ秒単位であっても同様です。その結果、全ティックに基づくテストをおこなった場合、同じサーバーから取得した実際のティックであっても、取引が見落とされることがありました。ティックの時刻はわかっていても、実際にその時刻に取引が存在することが確実でも、テスターにはそのティックが存在せず、取引を認識できません。しかし、取引時刻の前後にティックは存在しています。したがって、論理はティックとその時刻ではなく、取引を基準に構築できます。
- 取引はリスト内でミリ秒単位の発生時刻順にソートされます。最初の取引のインデックスを現在の取引インデックスとして設定します。
- 現在の取引インデックスで取引を選択し、その取引の時刻を取得します。
- この時刻のティックが到達するのを待ちます。
- ティック時刻が取引時刻よりも前の場合は、次のティックまで待ちます。
- ティック時刻が取引時刻と同じかそれ以上の場合は、取引を処理し、すでに処理済みであることを登録します。そして、次の取引のインデックスを現在の取引インデックスとして設定します。
- テストが終了するまで、上記の手順を繰り返します。
この構造により、各取引の時刻を待って順次実行することが可能になります。この場合、取引価格には注目せず、時刻が来たら取引をコピーするだけです。テスター内のティック時刻が実際の取引時刻より少し遅れても問題ありません。重要なのは、取引を正確にコピーすることです。テスターがすでに処理した取引は、取引オブジェクトの「テスター内ポジションチケット」プロパティが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
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
Pythonの価格変動離散化手法
循環単為生殖アルゴリズム(CPA)
初級から中級まで:テンプレートとtypename(IV)
初級から中級まで:テンプレートとtypename(III)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
何が間違いなのか?
読み込む前のファイルが配列より小さければ、配列のサイズは変わりません。
ArrayCopyを使っても同じようなエラーになることがあります。良い機能を無視している
利点は何ですか?
どんな利点があるのか?
簡潔さと実行スピード(完全にMQ側)。
標準的なセルフプリントとセルフフィリングの構造を示してください。
簡潔さと実行スピード(完全にMQ側)。