
MQL5取引ツールキット(第4回):履歴管理EX5ライブラリの開発
はじめに
この魅力的な連載では、2つの包括的なEX5ライブラリを開発しました。ポジションを処理および管理するPositionsManager.ex5と、予約注文を処理するPendingOrdersManager.ex5です。これらに加えて、これらのライブラリの実装を効果的に実証するために、グラフィカルユーザーインターフェイスを備えた実用的な例も作成しました。
この記事では、完了した注文、取引、ポジション取引の履歴を取得して処理するために設計された、もう1つの重要なEX5ライブラリを紹介します。さらに、さまざまな柔軟な基準に基づいて取引システム、エキスパートアドバイザー(EA)、または特定の銘柄のパフォーマンスを評価する取引レポートを生成する分析モジュールを開発します。
この記事は、ポジション、注文、取引履歴の操作が難しいと感じる初心者のMQL5開発者向けの実用的なガイドとして役立ちます。また、取引履歴の処理を効率化し、作業を強化するライブラリを探しているMQL5プログラマーにとっても貴重なリソースとなるでしょう。
まず、MQL5プログラマー、特にMetaTrader 5で取引履歴を処理するのが初めてのプログラマーにとって、理解が難しいと感じることが多い重要な質問をいくつか取り上げます。
MQL5における取引トランザクションのライフサイクルとは
MQL5では、取引トランザクションのライフサイクルは注文の実行から始まります。注文は、直接成行注文と予約注文の2つの主なタイプに分類できます。
直接成行注文のエントリー
直接成行注文は、現在の市場価格(AskまたはBid)で資産を購入または売却するためのリアルタイムのリクエストです。これらの注文を処理する方法については、ライブラリの開発中に、第1回と第2回の記事で詳しく説明しました。
直接成行注文は即座に実行されるため、手動取引にも自動取引戦略にも最適です。実行された注文はアクティブなポジションに移行し、一意のチケット番号と別のポジション識別子(POSITION_ID)が割り当てられます。これにより、ポジションのライフサイクルを通じて、さまざまな段階の追跡と管理がより信頼性高くおこなうことができます。
予約注文エントリー
一方、予約注文(BUYSTOP、BUYLIMIT、SELLSTOP、SELLLIMIT、BUYSTOPLIMIT、SELLSTOPLIMIT)は、指定された価格レベルに達したときにトリガーされる遅延注文です。これらの種類の注文の処理に関する詳細なガイドは、本連載第3回でPending Orders Managerライブラリを開発したときに掲載しています。
市場価格が事前に定義された予約注文のトリガー価格と一致するまで、予約注文は非アクティブのままになります。トリガーされると、成行注文に変換されて実行され、直接成行注文と同様、一意のチケット番号とポジション識別子(POSITION_ID)を受け取ります。
ポジションのライフサイクル中にステータスがどのように変化するか
ポジションのステータスは、そのライフサイクル中にさまざまな要因によって変化する可能性があります。
- 部分決済:ポジションの一部が決済されると、対応するエグジット(アウト)取引が取引履歴に記録されます。
- ポジションの逆転:「closeby」取引などのポジションの反転もエグジット取引として記録されます。
- 完全決済:ポジション全体がテイクプロフィット、ストップロス、またはマージンコールによるストップアウトイベントを通じて手動または自動で決済されると、最終的な決済取引が取引履歴に記録されます。
MQL5における取引操作のライフサイクルを理解することは非常に重要です。すべての取引は取引サーバーに送られる注文から始まります。これは、予約注文を出すリクエストであっても、成行注文の売買実行であっても、既存ポジションの部分決済リクエストであっても同じです。その種類に関係なく、すべての取引操作はまず注文として記録されます。
正常に実行された注文は次の段階に移行し、取引として履歴データベースに保存されます。注文や取引に利用可能なさまざまなプロパティや関数を使用して、各取引がどの注文から発生したかを追跡し、それを対応するポジションにリンクすることができます。このプロセスにより、取引のライフサイクルに関する明確で体系的な経路が作成されます。
この「パンくずリスト」アプローチにより、MQL5環境内でのすべての取引またはトランザクションの起源、進行状況、および結果を追跡できます。それにより、取引を開始した注文、実行時刻、途中で加えられた変更、最終的な取引結果(ポジション)など、詳細な監査履歴を提供します。この追跡は、透明性を高めるだけでなく、MQL5プログラマーとして取引戦略を分析し、改善すべき領域を特定し、パフォーマンスを最適化するアルゴリズムを開発する力を提供します。
MQL5では、ポジションは現在市場で保持しているアクティブな進行中取引(ポジション)を表します。特定の銘柄の買いまたは売りのポジションを反映するオープンな状態です。一方、取引は、ポジションが完全決済された、完了済みの取引を指します。 アクティブなポジションと予約注文は、MetaTrader 5のツールボックスウィンドウの[取引]タブに表示されます。
クローズ済みのポジション(取引)は、注文および取引とともに、ツールボックスウィンドウの[履歴]タブに表示されます。
ポジションの完全な履歴にアクセスするには、プラットフォームのメニューオプションを使用して、[ポジション]メニュー項目を選択します。同じメニューオプションを使用して、注文と取引の履歴にもアクセスできます。
特にプラットフォームの標準履歴機能を使用する場合、ポジションと取引の区別は初心者のMQL5プログラマーにとって紛らわしいものです。この記事と、これから作成するライブラリの詳細なコードを読めば、MQL5でポジションと取引がどのように分類され、追跡されるかを明確に理解できるようになります。もしお急ぎですぐに使える履歴ライブラリが必要な場合は、次の記事の包括的なドキュメントに従って、プロジェクトに直接実装できます。
履歴マネージャライブラリのソースコードファイル(.mq5)を作成する
開始するには、MetaEditor IDEを開き、メニューから[新規作成]を選択してMQLウィザードにアクセスします。ウィザードで、新しいライブラリソースファイルを作成することを選択し、HistoryManager.mq5という名前を付けます。このファイルは、口座の取引履歴の管理と分析に特化したコア関数の基盤となります。新しいHistoryManager.mq5ライブラリを作成するときは、最初の記事で作成したLibraries\Toolkitフォルダに保存します。この新しいファイルをPositionsManagerおよびPendingOrdersManager EX5ライブラリと同じディレクトリに保存することで、プロジェクトの明確で一貫した組織構造を維持できます。このアプローチにより、ツールキットが拡張されるにつれて、各コンポーネントの検索と管理が容易になります。
新しく作成されたHistoryManager.mq5ソースファイルは次のようになります。まず、#propertyディレクティブの下にある「My function」コメントを削除します。ファイル内のcopyrightおよびlinkプロパティのディレクティブは異なる場合がありますが、コードの動作やパフォーマンスには影響しません。copyrigihtおよびlinkのディレクティブは任意の情報でカスタマイズできますが、libraryプロパティのディレクティブは変更しないでください。
//+------------------------------------------------------------------+ //| HistoryManager.mq5 | //| Copyright 2024, Wanateki Solutions Ltd. | //| https://www.wanateki.com | //+------------------------------------------------------------------+ #property library #property copyright "Copyright 2024, Wanateki Solutions Ltd." #property link "https://www.wanateki.com" #property version "1.00" //+------------------------------------------------------------------+ //| My function | //+------------------------------------------------------------------+ // int MyCalculator(int value,int value2) export // { // return(value+value2); // } //+------------------------------------------------------------------+
データ構造体、プリプロセッサディレクティブ、グローバル変数
新しく作成したHistoryManager.mq5ライブラリソースファイルで、まず次のコンポーネントを定義します。
- プリプロセッサディレクティブ:さまざまな種類の取引履歴の並べ替えや照会に役立ちます。
- データ構造体:注文、取引、ポジション、予約注文の履歴データが保存されます。
- グローバル動的構造体配列:ライブラリ内のすべての関連する履歴データが保持されます。
これらのコンポーネントをグローバルスコープで定義すると、ライブラリ全体でアクセスできるようになり、ライブラリ内のすべての異なるモジュールまたは関数で利用できるようになります。
プリプロセッサディレクティブ
履歴管理ライブラリはさまざまな種類のリクエストを処理するため、各リクエストに必要な特定の履歴データのみを取得するように設計することが重要です。このモジュール式でターゲットを絞ったアプローチにより、さまざまなユースケースに対する柔軟性を維持しながら、ライブラリのパフォーマンスが向上します。
これを実現するために、特定の種類の履歴データの識別子として機能する整数定数を定義します。これらの定数により、ライブラリは必要なデータのみをターゲットにすることができるため、リソースの消費が最小限に抑えられ、処理が高速化されます。
履歴データを5つの主要カテゴリに分類します。
- 注文履歴
- 取引履歴
- ポジション履歴
- 予約注文履歴
- すべての履歴データ
これらの定数を使用することで、ライブラリ内の関数は処理する履歴の種類を指定できます。メインの履歴取得関数は、要求されたデータのみを照会して返すため、時間と計算リソースが節約されます。まず、これらの整数定数をコード内の最後の#propertyディレクティブのすぐ下に配置して定義します。
#define GET_ORDERS_HISTORY_DATA 1001 #define GET_DEALS_HISTORY_DATA 1002 #define GET_POSITIONS_HISTORY_DATA 1003 #define GET_PENDING_ORDERS_HISTORY_DATA 1004 #define GET_ALL_HISTORY_DATA 1005
データ構造体
私たちのEX5ライブラリは、グローバルに宣言されたデータ構造にさまざまな履歴データを保存します。これらの構造体は、照会された際に、取引、注文、ポジション、予約注文の履歴を効率的に保持します。
//- Data structure to store deal properties struct DealData { ulong ticket; ulong magic; ENUM_DEAL_ENTRY entry; ENUM_DEAL_TYPE type; ENUM_DEAL_REASON reason; ulong positionId; ulong order; string symbol; string comment; double volume; double price; datetime time; double tpPrice; double slPrice; double commission; double swap; double profit; }; //- Data structure to store order properties struct OrderData { datetime timeSetup; datetime timeDone; datetime expirationTime; ulong ticket; ulong magic; ENUM_ORDER_REASON reason; ENUM_ORDER_TYPE type; ENUM_ORDER_TYPE_FILLING typeFilling; ENUM_ORDER_STATE state; ENUM_ORDER_TYPE_TIME typeTime; ulong positionId; ulong positionById; string symbol; string comment; double volumeInitial; double priceOpen; double priceStopLimit; double tpPrice; double slPrice; }; //- Data structure to store closed position/trade properties struct PositionData { ENUM_POSITION_TYPE type; ulong ticket; ENUM_ORDER_TYPE initiatingOrderType; ulong positionId; bool initiatedByPendingOrder; ulong openingOrderTicket; ulong openingDealTicket; ulong closingDealTicket; string symbol; double volume; double openPrice; double closePrice; datetime openTime; datetime closeTime; long duration; double commission; double swap; double profit; double tpPrice; double slPrice; int tpPips; int slPips; int pipProfit; double netProfit; ulong magic; string comment; }; //- Data structure to store executed or canceled pending order properties struct PendingOrderData { string symbol; ENUM_ORDER_TYPE type; ENUM_ORDER_STATE state; double priceOpen; double tpPrice; double slPrice; int tpPips; int slPips; ulong positionId; ulong ticket; datetime timeSetup; datetime expirationTime; datetime timeDone; ENUM_ORDER_TYPE_TIME typeTime; ulong magic; ENUM_ORDER_REASON reason; ENUM_ORDER_TYPE_FILLING typeFilling; string comment; double volumeInitial; double priceStopLimit; };
グローバル動的構造体配列
グローバルスコープ内の最後の宣言は、前に定義した構造体の動的データ構造体配列で構成されます。これらの配列は、ライブラリによって管理されるすべてのコアデータのプライマリストレージとして機能します。
OrderData orderInfo[]; DealData dealInfo[]; PositionData positionInfo[]; PendingOrderData pendingOrderInfo[];
GetHistoryData関数
GetHistoryData関数は、EX5ライブラリの中核として機能し、その機能のバックボーンを形成します。ライブラリ内の他のほとんどの関数は、指定された期間と履歴タイプに基づいて取引履歴を取得するためにこの関数に依存します。この関数は内部使用のみを目的としているため、エクスポート可能として定義されません。
この関数は、指定された期間と履歴タイプに対して要求された履歴データを取得するように設計されています。これはbool型関数であり、履歴が正常に取得された場合はtrueを返し、操作が失敗した場合はfalseを返します。
GetHistoryData関数は3つの入力パラメータを受け入れます。
- 目的の期間の開始と終了を指定する2つのdatetime変数、fromDateTimeとtoDateTime
- ファイルの先頭にある定義済み定数の1つに対応するunsigned整数型のdataToGet
これらの入力を組み合わせることで、関数は必要な履歴データを効率的に照会および処理できます。まず関数を定義しましょう。
bool GetHistoryData(datetime fromDateTime, datetime toDateTime, uint dataToGet) { return(true); //-- Our function's code will go here }
この関数の最初のタスクは、指定された日付範囲が有効かどうかを確認することです。MQL5のdatetimeデータ型は、本質的にはUnixエポック形式(つまり、1970年1月1日00:00:00UTCからの経過秒数)で時間を表すlong整数型であるため、これらの値を直接比較して正確性を確認できます。また、MQL5で履歴データを要求する場合、時間はローカルマシンの時間ではなく、取引サーバーの時間に基づいていることに注意してください。
日付範囲を検証するには、fromDateTime値がtoDateTime値より小さいことを確認します。開始日を終了日以降にすることはできないため、fromDateTimeがtoDateTime以上の場合は期間が無効です。指定された期間が検証に失敗した場合は、falseを返して関数を終了します。
if(fromDateTime >= toDateTime) { //- Invalid time period selected Print("Invalid time period provided. Can't load history!"); return(false); }
日付と期間が確認されると、問題が発生した場合に正確なエラーコードを確保するために、MQL5のエラーキャッシュがリセットされます。次に、if-else文内でHistorySelect関数を呼び出し、検証されたdatetime値を渡して、指定された期間の取引と注文の履歴を取得します。HistorySelectはブール値を返すため、処理する履歴が正常に見つかった場合はtrueを返し、エラーが発生した場合やデータの取得に失敗した場合はfalseを返します。
ResetLastError(); if(HistorySelect(fromDateTime, toDateTime)) //- History selected ok { //-- Code to process the history data will go here } else //- History selecting failed { Print("Selecting the history failed. Error code = ", GetLastError()); return(false); }
if-else文のelse部分内に、関数を終了してブール値falseを返す前に、履歴選択に失敗したことを示すメッセージとエラーコードをログに記録するコードを追加しました。if部分では、switch文を使用して、dataToGetの値に基づいて、読み込まれた取引履歴データを処理するための適切な関数を呼び出します。
switch(dataToGet) { case GET_DEALS_HISTORY_DATA: //- Get and save only the deals history data SaveDealsData(); break; case GET_ORDERS_HISTORY_DATA: //- Get and save only the orders history data SaveOrdersData(); break; case GET_POSITIONS_HISTORY_DATA: //- Get and save only the positions history data SaveDealsData(); //- Needed to generate the positions history data SaveOrdersData(); //- Needed to generate the positions history data SavePositionsData(); break; case GET_PENDING_ORDERS_HISTORY_DATA: //- Get and save only the pending orders history data SaveOrdersData(); //- Needed to generate the pending orders history data SavePendingOrdersData(); break; case GET_ALL_HISTORY_DATA: //- Get and save all the history data SaveDealsData(); SaveOrdersData(); SavePositionsData(); SavePendingOrdersData(); break; default: //-- Unknown entry Print("-----------------------------------------------------------------------------------------"); Print(__FUNCTION__, ": Can't fetch the historical data you need."); Print("*** Please specify the historical data you need in the (dataToGet) parameter."); break; }
すべてのコードセグメントが含まれた完全なGetHistoryData関数を次に示します。
bool GetHistoryData(datetime fromDateTime, datetime toDateTime, uint dataToGet) { //- Check if the provided period of dates are valid if(fromDateTime >= toDateTime) { //- Invalid time period selected Print("Invalid time period provided. Can't load history!"); return(false); } //- Reset last error and get the history ResetLastError(); if(HistorySelect(fromDateTime, toDateTime)) //- History selected ok { //- Get the history data switch(dataToGet) { case GET_DEALS_HISTORY_DATA: //- Get and save only the deals history data SaveDealsData(); break; case GET_ORDERS_HISTORY_DATA: //- Get and save only the orders history data SaveOrdersData(); break; case GET_POSITIONS_HISTORY_DATA: //- Get and save only the positions history data SaveDealsData(); //- Needed to generate the positions history data SaveOrdersData(); //- Needed to generate the positions history data SavePositionsData(); break; case GET_PENDING_ORDERS_HISTORY_DATA: //- Get and save only the pending orders history data SaveOrdersData(); //- Needed to generate the pending orders history data SavePendingOrdersData(); break; case GET_ALL_HISTORY_DATA: //- Get and save all the history data SaveDealsData(); SaveOrdersData(); SavePositionsData(); SavePendingOrdersData(); break; default: //-- Unknown entry Print("-----------------------------------------------------------------------------------------"); Print(__FUNCTION__, ": Can't fetch the historical data you need."); Print("*** Please specify the historical data you need in the (dataToGet) parameter."); break; } } else { Print(__FUNCTION__, ": Selecting the history failed. Error code = ", GetLastError()); return(false); } return(true); }
この段階でソースコードファイルを保存してコンパイルしようとすると、多数のコンパイルエラーと警告が発生します。これは、コード内で参照される関数の多くがまだ作成されていないためです。EX5ライブラリの開発はまだ初期段階であるため、不足している関数がすべて実装されると、EX5ライブラリファイルはエラーや警告なしでコンパイルされます。
SaveDealsData関数
SaveDealsData関数は、ライブラリ内のさまざまな関数によって要求されるさまざまな期間の取引履歴キャッシュで現在利用可能なすべての取引履歴を取得して保存する役割を担います。ライブラリ内で内部的に呼び出される(具体的にはGetHistoryData関数から呼び出される)ため、データは返されず、エクスポート可能として定義されません。この関数は、MQL5のHistoryDealGet...標準関数を利用して、さまざまな取引プロパティを取得し、それらをdealInfo動的データ構造配列に格納します。
まず、関数の定義またはシグネチャを作成します。
void SaveDealsData() { //-- Our function's code will go here }
SaveDealsDataはGetHistoryData関数内で呼び出されるため、取引履歴を処理する前にHistorySelectを再度呼び出す必要はありません。SaveDealsData関数の最初の手順は、処理する取引履歴があるかどうかを確認することです。これを実現するには、履歴キャッシュで利用可能な取引の合計数を返すHistoryDealsTotal関数を使用します。効率性を高めるために、合計履歴取引を格納するためにtotalDealsという整数を作成し、取引チケット識別子を格納するために、unsigned long型のdealTicketを作成します。
int totalDeals = HistoryDealsTotal(); ulong dealTicket;
利用可能な取引がない場合や見つからない場合(totalDeals<=0)、そのことを示すメッセージをログに記録し、不要な処理を避けるために関数を早期に終了します。
if(totalDeals > 0) { //-- Code to process deal goes here } else { Print(__FUNCTION__, ": No deals available to be processed, totalDeals = ", totalDeals); }
取引履歴が存在する場合、次の手順は取得したデータを格納する配列を準備することです。このタスクでは、dealInfo動的配列を使用し、ArrayResize関数を使用して、取引の合計数に合わせてサイズを変更することから始め、関連するすべての取引プロパティを格納するのに十分な容量があることを確認します。
ArrayResize(dealInfo, totalDeals);
次に、forループを使用して、最新の取引から逆順に取引を反復処理します。各取引について、HistoryDealGetTicket関数を使用して、取引に関連付けられた一意のチケットを取得します。チケットの取得が成功すると、さまざまな取引プロパティを取得して保存します。各プロパティを、現在のループ反復に対応するインデックスにあるdealInfo配列の対応するフィールドに格納します。
HistoryDealGetTicket関数が取引の有効なチケットを取得できなかった場合、デバッグの目的でエラーコードを含むエラーメッセージをログに記録します。これにより、プロパティ取得プロセス中に予期しない問題が発生した場合でも透明性が確保されます。
for(int x = totalDeals - 1; x >= 0; x--) { ResetLastError(); dealTicket = HistoryDealGetTicket(x); if(dealTicket > 0) { //- Deal ticket selected ok, we can now save the deals properties dealInfo[x].ticket = dealTicket; dealInfo[x].entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY); dealInfo[x].type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE); dealInfo[x].magic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC); dealInfo[x].positionId = HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); dealInfo[x].order = HistoryDealGetInteger(dealTicket, DEAL_ORDER); dealInfo[x].symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL); dealInfo[x].comment = HistoryDealGetString(dealTicket, DEAL_COMMENT); dealInfo[x].volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME); dealInfo[x].price = HistoryDealGetDouble(dealTicket, DEAL_PRICE); dealInfo[x].time = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME); dealInfo[x].tpPrice = HistoryDealGetDouble(dealTicket, DEAL_TP); dealInfo[x].slPrice = HistoryDealGetDouble(dealTicket, DEAL_SL); dealInfo[x].commission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); dealInfo[x].swap = HistoryDealGetDouble(dealTicket, DEAL_SWAP); dealInfo[x].reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(dealTicket, DEAL_REASON); dealInfo[x].profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT); } else { Print( __FUNCTION__, " HistoryDealGetTicket(", x, ") failed. (dealTicket = ", dealTicket, ") *** Error Code: ", GetLastError() ); } }
以下に、すべてのコードセグメントが完全に含まれている完全なSaveDealsData関数を示します。
void SaveDealsData() { //- Get the number of loaded history deals int totalDeals = HistoryDealsTotal(); ulong dealTicket; //- //- Check if we have any deals to be worked on if(totalDeals > 0) { //- Resize the dynamic array that stores the deals ArrayResize(dealInfo, totalDeals); //- Let us loop through the deals and save them one by one for(int x = totalDeals - 1; x >= 0; x--) { ResetLastError(); dealTicket = HistoryDealGetTicket(x); if(dealTicket > 0) { //- Deal ticket selected ok, we can now save the deals properties dealInfo[x].ticket = dealTicket; dealInfo[x].entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY); dealInfo[x].type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE); dealInfo[x].magic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC); dealInfo[x].positionId = HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); dealInfo[x].order = HistoryDealGetInteger(dealTicket, DEAL_ORDER); dealInfo[x].symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL); dealInfo[x].comment = HistoryDealGetString(dealTicket, DEAL_COMMENT); dealInfo[x].volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME); dealInfo[x].price = HistoryDealGetDouble(dealTicket, DEAL_PRICE); dealInfo[x].time = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME); dealInfo[x].tpPrice = HistoryDealGetDouble(dealTicket, DEAL_TP); dealInfo[x].slPrice = HistoryDealGetDouble(dealTicket, DEAL_SL); dealInfo[x].commission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); dealInfo[x].swap = HistoryDealGetDouble(dealTicket, DEAL_SWAP); dealInfo[x].reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(dealTicket, DEAL_REASON); dealInfo[x].profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT); } else { Print( __FUNCTION__, " HistoryDealGetTicket(", x, ") failed. (dealTicket = ", dealTicket, ") *** Error Code: ", GetLastError() ); } } } else { Print(__FUNCTION__, ": No deals available to be processed, totalDeals = ", totalDeals); } }
PrintDealsHistory関数
PrintDealsHistory関数は、指定された期間の取引履歴データを取得して表示するように設計されています。この関数は、特定の期間内の取引データの範囲を調べる必要がある場合に便利です。データは返されませんが、代わりに取引情報がMetaTrader5のログに出力され、確認されます。この関数は、GetHistoryData関数を使用して関連データを取得することにより、外部から呼び出してユーザーに過去の取引に関する洞察を提供することができます。
まず、PrintDealsHistory関数を定義します。この関数には、検索対象期間の開始時刻と終了時刻を表すfromDateTimeとtoDateTimeという2つのパラメータが必要です。この関数は、この時間枠内に実行された取引を取得します。この関数はexportとしてマークされていることに注意してください。つまり、この関数は他のプログラムやライブラリから呼び出すことができ、外部ですぐに使用できます。
void PrintDealsHistory(datetime fromDateTime, datetime toDateTime) export { //-- Our function's code will go here }
次に、GetHistoryData関数を呼び出し、fromDateTime、toDateTime、および追加の定数GET_DEALS_HISTORY_DATAを渡します。これは、指定された開始時刻と終了時刻の間の関連する取引データを取得するように関数に指示します。この関数呼び出しにより、目的の期間の取引情報が取得され、dealInfo配列に格納されます。
GetHistoryData(fromDateTime, toDateTime, GET_DEALS_HISTORY_DATA);
取引データを取得したら、利用可能なデータがあるかどうかを確認する必要があります。ArraySize関数を使用して、dealInfo配列に保存されている取引の合計数を取得します。取引が見つからない場合(totalDeals<=0)、ユーザーに通知するメッセージをログに記録し、関数を終了します。表示する取引がない場合、関数は早期に終了し、時間を節約し、不要な操作を防ぎます。
int totalDeals = ArraySize(dealInfo); if(totalDeals <= 0) { Print(""); Print(__FUNCTION__, ": No deals history found for the specified period."); return; //-- Exit the function }
取引データが見つかった場合は、詳細を出力します。最初の手順は、見つかった取引の合計数と、取引が実行された日付の範囲を示す概要メッセージを出力することです。
Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalDeals, " deals executed between (", fromDateTime, ") and (", toDateTime, ")." );
次に、forループを使用して、dealInfo配列内のすべての取引を反復処理します。各取引について、取引の銘柄、チケット番号、ポジションID、エントリータイプ、価格、ストップロス(SL)、テイクプロフィット(TP)レベル、スワップ、手数料、利益などの関連詳細を出力します。各取引の詳細は説明ラベルとともにきちんと出力されており、ユーザーは取引履歴を簡単に理解できます。
for(int r = 0; r < totalDeals; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Deal #", (r + 1)); Print("Symbol: ", dealInfo[r].symbol); Print("Time Executed: ", dealInfo[r].time); Print("Ticket: ", dealInfo[r].ticket); Print("Position ID: ", dealInfo[r].positionId); Print("Order Ticket: ", dealInfo[r].order); Print("Type: ", EnumToString(dealInfo[r].type)); Print("Entry: ", EnumToString(dealInfo[r].entry)); Print("Reason: ", EnumToString(dealInfo[r].reason)); Print("Volume: ", dealInfo[r].volume); Print("Price: ", dealInfo[r].price); Print("SL Price: ", dealInfo[r].slPrice); Print("TP Price: ", dealInfo[r].tpPrice); Print("Swap: ", dealInfo[r].swap, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Commission: ", dealInfo[r].commission, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Profit: ", dealInfo[r].profit, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Comment: ", dealInfo[r].comment); Print("Magic: ", dealInfo[r].magic); Print(""); }
すべてのコードセグメントが統合された完全なPrintDealsHistory関数を次に示します。
void PrintDealsHistory(datetime fromDateTime, datetime toDateTime) export { //- Get and save the deals history for the specified period GetHistoryData(fromDateTime, toDateTime, GET_DEALS_HISTORY_DATA); int totalDeals = ArraySize(dealInfo); if(totalDeals <= 0) { Print(""); Print(__FUNCTION__, ": No deals history found for the specified period."); return; //-- Exit the function } Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalDeals, " deals executed between (", fromDateTime, ") and (", toDateTime, ")." ); for(int r = 0; r < totalDeals; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Deal #", (r + 1)); Print("Symbol: ", dealInfo[r].symbol); Print("Time Executed: ", dealInfo[r].time); Print("Ticket: ", dealInfo[r].ticket); Print("Position ID: ", dealInfo[r].positionId); Print("Order Ticket: ", dealInfo[r].order); Print("Type: ", EnumToString(dealInfo[r].type)); Print("Entry: ", EnumToString(dealInfo[r].entry)); Print("Reason: ", EnumToString(dealInfo[r].reason)); Print("Volume: ", dealInfo[r].volume); Print("Price: ", dealInfo[r].price); Print("SL Price: ", dealInfo[r].slPrice); Print("TP Price: ", dealInfo[r].tpPrice); Print("Swap: ", dealInfo[r].swap, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Commission: ", dealInfo[r].commission, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Profit: ", dealInfo[r].profit, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Comment: ", dealInfo[r].comment); Print("Magic: ", dealInfo[r].magic); Print(""); } }
SaveOrdersData関数
SaveOrdersData関数は、取引履歴キャッシュで利用可能な履歴注文データを取得して保存する役割を担います。この関数は注文を1つずつ処理し、MQL5のHistoryOrderGet...関数を使用して注文の主要なプロパティを抽出し、orderInfoと呼ばれる動的配列に格納します。この配列は、必要に応じてライブラリの他の部分でデータを分析および操作するために使用されます。この関数はデータを返さず、ライブラリ内で内部的に使用されるためエクスポート可能として定義されず、エラーを適切に処理し、デバッグの目的で問題をログに記録します。
まず関数シグネチャを定義しましょう。
void SaveOrdersData() { //-- Our function's code will go here }
次に、利用可能な履歴注文の数を決定します。これは、キャッシュ内の履歴注文の合計数を返すHistoryOrdersTotal関数を使用して実現されます。結果はtotalOrdersHistoryという名前の変数に保存されます。さらに、各注文を処理するときにそのチケットを保持するために、unsigned long型のorderTicket変数を宣言します。
int totalOrdersHistory = HistoryOrdersTotal(); ulong orderTicket;
過去の注文がない場合(totalOrdersHistory<=0)、関数はこれを示すメッセージをログに記録し、不要な処理を避けるために早期に終了します。
if(totalOrdersHistory > 0) { //-- Code to process orders goes here } else { Print(__FUNCTION__, ": No order history available to be processed, totalOrdersHistory = ", totalOrdersHistory); return; }
過去の注文が利用可能な場合は、取得したデータを格納するためのorderInfo配列を準備します。これは、ArrayResize関数を使用して配列のサイズを変更し、履歴注文の合計数と一致させることによっておこなわれます。
ArrayResize(orderInfo, totalOrdersHistory);
forループを使用して、注文を逆順(最新のものから開始)でループします。注文ごとに、まず、HistoryOrderGetTicket関数を使用して注文チケットを取得します。チケットの取得が成功した場合、HistoryOrderGet...関数を使用して注文のさまざまなプロパティを抽出し、orderInfo配列の対応するフィールドに保存します。チケットの取得に失敗した場合、関数はデバッグのためにエラーコードとともにエラーメッセージをログに記録します。
for(int x = totalOrdersHistory - 1; x >= 0; x--) { ResetLastError(); orderTicket = HistoryOrderGetTicket(x); if(orderTicket > 0) { //- Order ticket selected ok, we can now save the order properties orderInfo[x].ticket = orderTicket; orderInfo[x].timeSetup = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_SETUP); orderInfo[x].timeDone = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_DONE); orderInfo[x].expirationTime = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_EXPIRATION); orderInfo[x].typeTime = (ENUM_ORDER_TYPE_TIME)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_TIME); orderInfo[x].magic = HistoryOrderGetInteger(orderTicket, ORDER_MAGIC); orderInfo[x].reason = (ENUM_ORDER_REASON)HistoryOrderGetInteger(orderTicket, ORDER_REASON); orderInfo[x].type = (ENUM_ORDER_TYPE)HistoryOrderGetInteger(orderTicket, ORDER_TYPE); orderInfo[x].state = (ENUM_ORDER_STATE)HistoryOrderGetInteger(orderTicket, ORDER_STATE); orderInfo[x].typeFilling = (ENUM_ORDER_TYPE_FILLING)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_FILLING); orderInfo[x].positionId = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_ID); orderInfo[x].positionById = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_BY_ID); orderInfo[x].symbol = HistoryOrderGetString(orderTicket, ORDER_SYMBOL); orderInfo[x].comment = HistoryOrderGetString(orderTicket, ORDER_COMMENT); orderInfo[x].volumeInitial = HistoryOrderGetDouble(orderTicket, ORDER_VOLUME_INITIAL); orderInfo[x].priceOpen = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_OPEN); orderInfo[x].priceStopLimit = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_STOPLIMIT); orderInfo[x].tpPrice = HistoryOrderGetDouble(orderTicket, ORDER_TP); orderInfo[x].slPrice = HistoryOrderGetDouble(orderTicket, ORDER_SL); } else { Print( __FUNCTION__, " HistoryOrderGetTicket(", x, ") failed. (orderTicket = ", orderTicket, ") *** Error Code: ", GetLastError() ); } }
すべての注文を処理した後、関数は正常に終了します。以下は、すべてのコードセグメントが含まれたSaveOrdersData関数の完全な実装です。
void SaveOrdersData() { //- Get the number of loaded history orders int totalOrdersHistory = HistoryOrdersTotal(); ulong orderTicket; //- //- Check if we have any orders in the history to be worked on if(totalOrdersHistory > 0) { //- Resize the dynamic array that stores the history orders ArrayResize(orderInfo, totalOrdersHistory); //- Let us loop through the order history and save them one by one for(int x = totalOrdersHistory - 1; x >= 0; x--) { ResetLastError(); orderTicket = HistoryOrderGetTicket(x); if(orderTicket > 0) { //- Order ticket selected ok, we can now save the order properties orderInfo[x].ticket = orderTicket; orderInfo[x].timeSetup = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_SETUP); orderInfo[x].timeDone = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_DONE); orderInfo[x].expirationTime = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_EXPIRATION); orderInfo[x].typeTime = (ENUM_ORDER_TYPE_TIME)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_TIME); orderInfo[x].magic = HistoryOrderGetInteger(orderTicket, ORDER_MAGIC); orderInfo[x].reason = (ENUM_ORDER_REASON)HistoryOrderGetInteger(orderTicket, ORDER_REASON); orderInfo[x].type = (ENUM_ORDER_TYPE)HistoryOrderGetInteger(orderTicket, ORDER_TYPE); orderInfo[x].state = (ENUM_ORDER_STATE)HistoryOrderGetInteger(orderTicket, ORDER_STATE); orderInfo[x].typeFilling = (ENUM_ORDER_TYPE_FILLING)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_FILLING); orderInfo[x].positionId = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_ID); orderInfo[x].positionById = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_BY_ID); orderInfo[x].symbol = HistoryOrderGetString(orderTicket, ORDER_SYMBOL); orderInfo[x].comment = HistoryOrderGetString(orderTicket, ORDER_COMMENT); orderInfo[x].volumeInitial = HistoryOrderGetDouble(orderTicket, ORDER_VOLUME_INITIAL); orderInfo[x].priceOpen = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_OPEN); orderInfo[x].priceStopLimit = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_STOPLIMIT); orderInfo[x].tpPrice = HistoryOrderGetDouble(orderTicket, ORDER_TP); orderInfo[x].slPrice = HistoryOrderGetDouble(orderTicket, ORDER_SL); } else { Print( __FUNCTION__, " HistoryOrderGetTicket(", x, ") failed. (orderTicket = ", orderTicket, ") *** Error Code: ", GetLastError() ); } } } else { Print(__FUNCTION__, ": No order history available to be processed, totalOrdersHistory = ", totalOrdersHistory); } }
PrintOrdersHistory関数
PrintOrdersHistory関数は、指定された期間内の注文履歴の詳細を表示するための重要な機能を提供します。以前に保存されたデータをorderInfo配列から照会し、注文の関連する詳細をすべて出力します。この関数は、このライブラリを使用する外部モジュールまたはMQL5アプリからアクセスできるようにするために、exportとして定義されています。これはPrintDealsHistory関数と同様のアプローチに従います。以下はPrintOrdersHistory関数の完全な実装です。コードの各部分の動作をよりよく理解できるように説明コメントが付いています。
void PrintOrdersHistory(datetime fromDateTime, datetime toDateTime) export { //- Get and save the orders history for the specified period GetHistoryData(fromDateTime, toDateTime, GET_ORDERS_HISTORY_DATA); int totalOrders = ArraySize(orderInfo); if(totalOrders <= 0) { Print(""); Print(__FUNCTION__, ": No orders history found for the specified period."); return; //-- Exit the function } Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalOrders, " orders filled or cancelled between (", fromDateTime, ") and (", toDateTime, ")." ); for(int r = 0; r < totalOrders; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Order #", (r + 1)); Print("Symbol: ", orderInfo[r].symbol); Print("Time Setup: ", orderInfo[r].timeSetup); Print("Type: ", EnumToString(orderInfo[r].type)); Print("Ticket: ", orderInfo[r].ticket); Print("Position ID: ", orderInfo[r].positionId); Print("State: ", EnumToString(orderInfo[r].state)); Print("Type Filling: ", EnumToString(orderInfo[r].typeFilling)); Print("Type Time: ", EnumToString(orderInfo[r].typeTime)); Print("Reason: ", EnumToString(orderInfo[r].reason)); Print("Volume Initial: ", orderInfo[r].volumeInitial); Print("Price Open: ", orderInfo[r].priceOpen); Print("Price Stop Limit: ", orderInfo[r].priceStopLimit); Print("SL Price: ", orderInfo[r].slPrice); Print("TP Price: ", orderInfo[r].tpPrice); Print("Time Done: ", orderInfo[r].timeDone); Print("Expiration Time: ", orderInfo[r].expirationTime); Print("Comment: ", orderInfo[r].comment); Print("Magic: ", orderInfo[r].magic); Print(""); } }
SavePositionsData関数
SavePositionsData関数は、取引と注文履歴を整理して各ポジションのライフサイクルを再構築し、利用可能なデータから情報を統合してポジション履歴を作成する上で中心的な役割を果たします。MQL5のドキュメントを見ると、履歴ポジションデータに直接アクセスするための標準関数(HistoryPositionSelectやHistoryPositionsTotalなど)がないことがわかります。したがって、ポジションIDを接続キーとして使用して、取引とその元の注文をリンクし、注文と取引データを結合するカスタム関数を作成する必要があります。
まず、取引を調べて、ポジションがクローズされたことを示すすべてのエグジット取引を特定します。そこから、対応するエントリー取引まで遡って、ポジションの開始に関する詳細を収集します。最後に、注文履歴を使用して、元の注文タイプやポジションが予約注文によって開始されたかどうかなどの追加のコンテキストでポジション履歴情報を充実させます。この段階的なプロセスにより、各ポジションのライフサイクル(開始から終了まで)が正確に再構築され、わかりやすい監査証跡が提供されます。
まず関数シグネチャを定義しましょう。この関数はEX5ライブラリコアモジュールによって内部的にのみ使用されるため、エクスポートすることはできません。
void SavePositionsData() { //-- Our function's code will go here }
次に、すべての取引データが含まれるdealInfo配列内の取引の合計数を計算します。その後、すべてのポジション履歴データを保存し、予想されるポジション数に対応できるように準備するために使用するpositionInfo配列のサイズを変更します。
int totalDealInfo = ArraySize(dealInfo); ArrayResize(positionInfo, totalDealInfo); int totalPositionsFound = 0, posIndex = 0;
dealInfo配列に利用可能な取引がない場合(totalDealInfo==0)、処理するデータがないため、関数を早期に終了します。
if(totalDealInfo == 0) { return; }
次に、取引を逆の順序でループし(最新の取引から開始)、エグジット取引を対応するエントリ取引にマッピングできるようにします。現在の取引がエグジット取引であるかどうかは、entryプロパティを評価することによって確認します(dealInfo[x].entry==DEAL_ENTRY_OUT)。 ポジションがクローズされ、アクティブではなくなったことを確認するために、エグジット取引の検索から始めることが重要です。アクティブなポジションではなく、過去のクローズ済みポジションのみを記録します。
for(int x = totalDealInfo - 1; x >= 0; x--) { if(dealInfo[x].entry == DEAL_ENTRY_OUT) { // Process exit deal } }
エグジット取引が見つかった場合は、POSITION_IDを照合して対応するエントリー取引を検索します。エントリー取引が見つかったら、その関連情報をpositionInfo配列に保存し始めます。
for(int k = ArraySize(dealInfo) - 1; k >= 0; k--) { if(dealInfo[k].positionId == positionId) { if(dealInfo[k].entry == DEAL_ENTRY_IN) { exitDealFound = true; totalPositionsFound++; posIndex = totalPositionsFound - 1; // Save the entry deal data positionInfo[posIndex].openingDealTicket = dealInfo[k].ticket; positionInfo[posIndex].openTime = dealInfo[k].time; positionInfo[posIndex].openPrice = dealInfo[k].price; positionInfo[posIndex].volume = dealInfo[k].volume; positionInfo[posIndex].magic = dealInfo[k].magic; positionInfo[posIndex].comment = dealInfo[k].comment; } } }
エグジット取引がエントリー取引と一致したら、終値、終値時間、利益、スワップ、手数料などのエグジット取引のプロパティを保存します。また、スワップと手数料を考慮して取引期間と純利益を計算します。
if(exitDealFound) { if(dealInfo[x].type == DEAL_TYPE_BUY) { positionInfo[posIndex].type = POSITION_TYPE_SELL; } else { positionInfo[posIndex].type = POSITION_TYPE_BUY; } positionInfo[posIndex].positionId = dealInfo[x].positionId; positionInfo[posIndex].symbol = dealInfo[x].symbol; positionInfo[posIndex].profit = dealInfo[x].profit; positionInfo[posIndex].closingDealTicket = dealInfo[x].ticket; positionInfo[posIndex].closePrice = dealInfo[x].price; positionInfo[posIndex].closeTime = dealInfo[x].time; positionInfo[posIndex].swap = dealInfo[x].swap; positionInfo[posIndex].commission = dealInfo[x].commission; positionInfo[posIndex].duration = MathAbs((long)positionInfo[posIndex].closeTime - (long)positionInfo[posIndex].openTime); positionInfo[posIndex].netProfit = positionInfo[posIndex].profit + positionInfo[posIndex].swap - positionInfo[posIndex].commission; }
各ポジションについて、そのポジションが買いか売りかに応じて、ストップロス(SL)レベルとテイクプロフィット(TP)レベルのピップ値を計算します。銘柄のポイント値を使用してピップ数を決定します。
if(positionInfo[posIndex].type == POSITION_TYPE_BUY) { // Calculate TP and SL pip values for buy position if(positionInfo[posIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].tpPips = int((positionInfo[posIndex].tpPrice - positionInfo[posIndex].openPrice) / symbolPoint); } if(positionInfo[posIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].slPips = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].slPrice) / symbolPoint); } // Calculate pip profit for buy position double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].pipProfit = int((positionInfo[posIndex].closePrice - positionInfo[posIndex].openPrice) / symbolPoint); } else { // Calculate TP and SL pip values for sell position if(positionInfo[posIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].tpPips = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].tpPrice) / symbolPoint); } if(positionInfo[posIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].slPips = int((positionInfo[posIndex].slPrice - positionInfo[posIndex].openPrice) / symbolPoint); } // Calculate pip profit for sell position double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].pipProfit = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].closePrice) / symbolPoint); }
最後に、orderInfo配列を調べて、ポジションを開始した注文を見つけます。POSITION_IDを照合し、注文がORDER_STATE_FILLED状態であることを確認します。見つかったら、開始注文のticketとtypeを保存します。これにより、ポジションが予約注文によって開始されたのか、直接成行エントリーによって開始されたのかを判断するのに役立ちます。
for(int k = 0; k < ArraySize(orderInfo); k++) { if( orderInfo[k].positionId == positionInfo[posIndex].positionId && orderInfo[k].state == ORDER_STATE_FILLED ) { positionInfo[posIndex].openingOrderTicket = orderInfo[k].ticket; positionInfo[posIndex].ticket = positionInfo[posIndex].openingOrderTicket; //- Determine if the position was initiated by a pending order or direct market entry switch(orderInfo[k].type) { case ORDER_TYPE_BUY_LIMIT: case ORDER_TYPE_BUY_STOP: case ORDER_TYPE_SELL_LIMIT: case ORDER_TYPE_SELL_STOP: case ORDER_TYPE_BUY_STOP_LIMIT: case ORDER_TYPE_SELL_STOP_LIMIT: positionInfo[posIndex].initiatedByPendingOrder = true; positionInfo[posIndex].initiatingOrderType = orderInfo[k].type; break; default: positionInfo[posIndex].initiatedByPendingOrder = false; positionInfo[posIndex].initiatingOrderType = orderInfo[k].type; break; } break; //- Exit the orderInfo loop once the required data is found } }
最後に、positionInfo配列をクリーンアップするために、すべてのポジションが処理された後に、空または未使用の要素を削除するように配列のサイズを変更します。
ArrayResize(positionInfo, totalPositionsFound);
以下は、すべてのコードセグメントが含まれたSavePositionsData関数の完全な実装です。
void SavePositionsData() { //- Since every transaction is recorded as a deal, we will begin by scanning the deals and link them //- to different orders and generate the positions data using the POSITION_ID as the primary and foreign key int totalDealInfo = ArraySize(dealInfo); ArrayResize(positionInfo, totalDealInfo); //- Resize the position array to match the deals array int totalPositionsFound = 0, posIndex = 0; if(totalDealInfo == 0) //- Check if we have any deal history available for processing { return; //- No deal data to process found, we can't go on. exit the function } //- Let us loop through the deals array for(int x = totalDealInfo - 1; x >= 0; x--) { //- First we check if it is an exit deal to close a position if(dealInfo[x].entry == DEAL_ENTRY_OUT) { //- We begin by saving the position id ulong positionId = dealInfo[x].positionId; bool exitDealFound = false; //- Now we check if we have an exit deal from this position and save it's properties for(int k = ArraySize(dealInfo) - 1; k >= 0; k--) { if(dealInfo[k].positionId == positionId) { if(dealInfo[k].entry == DEAL_ENTRY_IN) { exitDealFound = true; totalPositionsFound++; posIndex = totalPositionsFound - 1; positionInfo[posIndex].openingDealTicket = dealInfo[k].ticket; positionInfo[posIndex].openTime = dealInfo[k].time; positionInfo[posIndex].openPrice = dealInfo[k].price; positionInfo[posIndex].volume = dealInfo[k].volume; positionInfo[posIndex].magic = dealInfo[k].magic; positionInfo[posIndex].comment = dealInfo[k].comment; } } } if(exitDealFound) //- Continue saving the exit deal data { //- Save the position type if(dealInfo[x].type == DEAL_TYPE_BUY) { //- If the exit deal is a buy, then the position was a sell trade positionInfo[posIndex].type = POSITION_TYPE_SELL; } else { //- If the exit deal is a sell, then the position was a buy trade positionInfo[posIndex].type = POSITION_TYPE_BUY; } positionInfo[posIndex].positionId = dealInfo[x].positionId; positionInfo[posIndex].symbol = dealInfo[x].symbol; positionInfo[posIndex].profit = dealInfo[x].profit; positionInfo[posIndex].closingDealTicket = dealInfo[x].ticket; positionInfo[posIndex].closePrice = dealInfo[x].price; positionInfo[posIndex].closeTime = dealInfo[x].time; positionInfo[posIndex].swap = dealInfo[x].swap; positionInfo[posIndex].commission = dealInfo[x].commission; positionInfo[posIndex].tpPrice = dealInfo[x].tpPrice; positionInfo[posIndex].tpPips = 0; positionInfo[posIndex].slPrice = dealInfo[x].slPrice; positionInfo[posIndex].slPips = 0; //- Calculate the trade duration in seconds positionInfo[posIndex].duration = MathAbs((long)positionInfo[posIndex].closeTime - (long)positionInfo[posIndex].openTime); //- Calculate the net profit after swap and commission positionInfo[posIndex].netProfit = positionInfo[posIndex].profit + positionInfo[posIndex].swap - positionInfo[posIndex].commission; //- Get pip values for the position if(positionInfo[posIndex].type == POSITION_TYPE_BUY) //- Buy position { //- Get sl and tp pip values if(positionInfo[posIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].tpPips = int((positionInfo[posIndex].tpPrice - positionInfo[posIndex].openPrice) / symbolPoint); } if(positionInfo[posIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].slPips = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].slPrice) / symbolPoint); } //- Get the buy profit in pip value double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].pipProfit = int((positionInfo[posIndex].closePrice - positionInfo[posIndex].openPrice) / symbolPoint); } else //- Sell position { //- Get sl and tp pip values if(positionInfo[posIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].tpPips = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].tpPrice) / symbolPoint); } if(positionInfo[posIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].slPips = int((positionInfo[posIndex].slPrice - positionInfo[posIndex].openPrice) / symbolPoint); } //- Get the sell profit in pip value double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].pipProfit = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].closePrice) / symbolPoint); } //- Now we scan and get the opening order ticket in the orderInfo array for(int k = 0; k < ArraySize(orderInfo); k++) //- Search from the oldest to newest order { if( orderInfo[k].positionId == positionInfo[posIndex].positionId && orderInfo[k].state == ORDER_STATE_FILLED ) { //- Save the order ticket that intiated the position positionInfo[posIndex].openingOrderTicket = orderInfo[k].ticket; positionInfo[posIndex].ticket = positionInfo[posIndex].openingOrderTicket; //- Determine if the position was initiated by a pending order or direct market entry switch(orderInfo[k].type) { //- Pending order entry case ORDER_TYPE_BUY_LIMIT: case ORDER_TYPE_BUY_STOP: case ORDER_TYPE_SELL_LIMIT: case ORDER_TYPE_SELL_STOP: case ORDER_TYPE_BUY_STOP_LIMIT: case ORDER_TYPE_SELL_STOP_LIMIT: positionInfo[posIndex].initiatedByPendingOrder = true; positionInfo[posIndex].initiatingOrderType = orderInfo[k].type; break; //- Direct market entry default: positionInfo[posIndex].initiatedByPendingOrder = false; positionInfo[posIndex].initiatingOrderType = orderInfo[k].type; break; } break; //--- We have everything we need, exit the orderInfo loop } } } } else //--- Position id not found { continue;//- skip to the next iteration } } //- Resize the positionInfo array and delete all the indexes that have zero values ArrayResize(positionInfo, totalPositionsFound); }
PrintPositionsHistory関数
PrintPositionsHistory関数は、指定された時間枠内でクローズされたポジションの詳細な履歴を表示するように設計されています。これは、positionInfo配列から以前に保存されたデータにアクセスし、各ポジションの関連する詳細を出力します。この関数はエクスポート可能であり、このライブラリを利用する外部モジュールまたはMQL5アプリからアクセスできるようになります。その実装は、私たちが開発した他の出力関数と同様の構造に従います。以下に、わかりやすくするために詳細なコメントを付した完全な実装を示します。
void PrintPositionsHistory(datetime fromDateTime, datetime toDateTime) export { //- Get and save the deals, orders, positions history for the specified period GetHistoryData(fromDateTime, toDateTime, GET_POSITIONS_HISTORY_DATA); int totalPositionsClosed = ArraySize(positionInfo); if(totalPositionsClosed <= 0) { Print(""); Print(__FUNCTION__, ": No position history found for the specified period."); return; //- Exit the function } Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalPositionsClosed, " positions closed between (", fromDateTime, ") and (", toDateTime, ")." ); for(int r = 0; r < totalPositionsClosed; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Position #", (r + 1)); Print("Symbol: ", positionInfo[r].symbol); Print("Time Open: ", positionInfo[r].openTime); Print("Ticket: ", positionInfo[r].ticket); Print("Type: ", EnumToString(positionInfo[r].type)); Print("Volume: ", positionInfo[r].volume); Print("0pen Price: ", positionInfo[r].openPrice); Print("SL Price: ", positionInfo[r].slPrice, " (slPips: ", positionInfo[r].slPips, ")"); Print("TP Price: ", positionInfo[r].tpPrice, " (tpPips: ", positionInfo[r].tpPips, ")"); Print("Close Price: ", positionInfo[r].closePrice); Print("Close Time: ", positionInfo[r].closeTime); Print("Trade Duration: ", positionInfo[r].duration); Print("Swap: ", positionInfo[r].swap, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Commission: ", positionInfo[r].commission, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Profit: ", positionInfo[r].profit, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Net profit: ", DoubleToString(positionInfo[r].netProfit, 2), " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("pipProfit: ", positionInfo[r].pipProfit); Print("Initiating Order Type: ", EnumToString(positionInfo[r].initiatingOrderType)); Print("Initiated By Pending Order: ", positionInfo[r].initiatedByPendingOrder); Print("Comment: ", positionInfo[r].comment); Print("Magic: ", positionInfo[r].magic); Print(""); } }
SavePendingOrdersData関数
SavePendingOrdersData関数は、注文履歴のデータを処理し、予約注文履歴を生成して保存します。この関数は基本的に、注文履歴から予約注文をフィルタリングし、重要な詳細を保存し、テイクプロフィット(TP)レベルとストップロス(SL)レベルのピップ数などの特定の値を計算します。これは、予約注文のライフサイクルを追跡し、正確な注文履歴を生成するのに役立ち、各予約注文がどのように構成され実行されたかに関するデータをシステムに追加するのに重要な役割を果たします。
MQL5には現在、過去の予約注文データに直接アクセスするためのHistoryPendingOrderSelectやHistoryPendingOrdersTotalなどの標準関数がありません。そのため、注文履歴をスキャンし、特定の履歴期間内に完了またはキャンセルされたすべての予約注文を含むデータソースを構築するカスタム関数を作成する必要があります。
まず関数シグネチャを定義しましょう。この関数はEX5ライブラリコアモジュールによって内部的にのみ使用されるため、エクスポートすることはできません。
void SavePendingOrdersData() { //-- Function's code will go here }
次に、すべての注文の詳細を保持するorderInfo配列内の注文の合計数を計算します。最初に、注文の合計数に合わせてpendingOrderInfo配列のサイズを変更し、フィルタリングされた予約注文を保存するのに十分なスペースを確保します。
int totalOrderInfo = ArraySize(orderInfo); ArrayResize(pendingOrderInfo, totalOrderInfo); int totalPendingOrdersFound = 0, pendingIndex = 0;
処理する注文がない場合(totalOrderInfo==0)、処理する予約注文データがないため、すぐに関数を終了します。
if(totalOrderInfo == 0) { return; }
ここで、最新の注文が最初に処理されるように、注文を逆の順序でループします。ループ内では、現在の注文のタイプを評価して、それが予約注文であるかどうかを確認します。保存された注文履歴には、実行(約定)されてポジションに変換されたか、ポジションにならずにキャンセルされた予約注文(買い指値、売り指値など)が含まれます。
for(int x = totalOrderInfo - 1; x >= 0; x--) { if( orderInfo[x].type == ORDER_TYPE_BUY_LIMIT || orderInfo[x].type == ORDER_TYPE_BUY_STOP || orderInfo[x].type == ORDER_TYPE_SELL_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP || orderInfo[x].type == ORDER_TYPE_BUY_STOP_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP_LIMIT ) { totalPendingOrdersFound++; pendingIndex = totalPendingOrdersFound - 1; //-- Save the pending order properties into the pendingOrderInfo array }
注文が予約注文の場合、そのプロパティ(タイプ、状態、ポジションID、チケット、銘柄、時間など)をpendingOrderInfo配列に保存します。
pendingOrderInfo[pendingIndex].type = orderInfo[x].type; pendingOrderInfo[pendingIndex].state = orderInfo[x].state; pendingOrderInfo[pendingIndex].positionId = orderInfo[x].positionId; pendingOrderInfo[pendingIndex].ticket = orderInfo[x].ticket; pendingOrderInfo[pendingIndex].symbol = orderInfo[x].symbol; pendingOrderInfo[pendingIndex].timeSetup = orderInfo[x].timeSetup; pendingOrderInfo[pendingIndex].expirationTime = orderInfo[x].expirationTime; pendingOrderInfo[pendingIndex].timeDone = orderInfo[x].timeDone; pendingOrderInfo[pendingIndex].typeTime = orderInfo[x].typeTime; pendingOrderInfo[pendingIndex].priceOpen = orderInfo[x].priceOpen; pendingOrderInfo[pendingIndex].tpPrice = orderInfo[x].tpPrice; pendingOrderInfo[pendingIndex].slPrice = orderInfo[x].slPrice;
次に、指定されている場合はテイクプロフィット(TP)レベルとストップロス(SL)レベルの両方のピップ数を計算します。これをおこなうには、銘柄のポイント値を使用してピップの数を決定します。
if(pendingOrderInfo[pendingIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT); pendingOrderInfo[pendingIndex].tpPips = (int)MathAbs((pendingOrderInfo[pendingIndex].tpPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint); } if(pendingOrderInfo[pendingIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT); pendingOrderInfo[pendingIndex].slPips = (int)MathAbs((pendingOrderInfo[pendingIndex].slPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint); }
また、注文のマジックナンバー、理由、約定タイプ、コメント、初期数量、ストップリミット価格などの追加プロパティも保存します。
pendingOrderInfo[pendingIndex].magic = orderInfo[x].magic; pendingOrderInfo[pendingIndex].reason = orderInfo[x].reason; pendingOrderInfo[pendingIndex].typeFilling = orderInfo[x].typeFilling; pendingOrderInfo[pendingIndex].comment = orderInfo[x].comment; pendingOrderInfo[pendingIndex].volumeInitial = orderInfo[x].volumeInitial; pendingOrderInfo[pendingIndex].priceStopLimit = orderInfo[x].priceStopLimit;
すべての注文を処理したら、pendingOrderInfo配列のサイズを変更して、空または未使用の要素を削除し、配列に関連する予約注文データのみが含まれるようにします。
ArrayResize(pendingOrderInfo, totalPendingOrdersFound);
以下は、すべてのコードセグメントが含まれたSavePendingOrdersData関数の完全な実装です。
void SavePendingOrdersData() { //- Let us begin by scanning the orders and link them to different deals int totalOrderInfo = ArraySize(orderInfo); ArrayResize(pendingOrderInfo, totalOrderInfo); int totalPendingOrdersFound = 0, pendingIndex = 0; if(totalOrderInfo == 0) { return; //- No order data to process found, we can't go on. exit the function } for(int x = totalOrderInfo - 1; x >= 0; x--) { //- Check if it is a pending order and save its properties if( orderInfo[x].type == ORDER_TYPE_BUY_LIMIT || orderInfo[x].type == ORDER_TYPE_BUY_STOP || orderInfo[x].type == ORDER_TYPE_SELL_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP || orderInfo[x].type == ORDER_TYPE_BUY_STOP_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP_LIMIT ) { totalPendingOrdersFound++; pendingIndex = totalPendingOrdersFound - 1; pendingOrderInfo[pendingIndex].type = orderInfo[x].type; pendingOrderInfo[pendingIndex].state = orderInfo[x].state; pendingOrderInfo[pendingIndex].positionId = orderInfo[x].positionId; pendingOrderInfo[pendingIndex].ticket = orderInfo[x].ticket; pendingOrderInfo[pendingIndex].symbol = orderInfo[x].symbol; pendingOrderInfo[pendingIndex].timeSetup = orderInfo[x].timeSetup; pendingOrderInfo[pendingIndex].expirationTime = orderInfo[x].expirationTime; pendingOrderInfo[pendingIndex].timeDone = orderInfo[x].timeDone; pendingOrderInfo[pendingIndex].typeTime = orderInfo[x].typeTime; pendingOrderInfo[pendingIndex].priceOpen = orderInfo[x].priceOpen; pendingOrderInfo[pendingIndex].tpPrice = orderInfo[x].tpPrice; pendingOrderInfo[pendingIndex].slPrice = orderInfo[x].slPrice; if(pendingOrderInfo[pendingIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT); pendingOrderInfo[pendingIndex].tpPips = (int)MathAbs((pendingOrderInfo[pendingIndex].tpPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint); } if(pendingOrderInfo[pendingIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT); pendingOrderInfo[pendingIndex].slPips = (int)MathAbs((pendingOrderInfo[pendingIndex].slPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint); } pendingOrderInfo[pendingIndex].magic = orderInfo[x].magic; pendingOrderInfo[pendingIndex].reason = orderInfo[x].reason; pendingOrderInfo[pendingIndex].typeFilling = orderInfo[x].typeFilling; pendingOrderInfo[pendingIndex].comment = orderInfo[x].comment; pendingOrderInfo[pendingIndex].volumeInitial = orderInfo[x].volumeInitial; pendingOrderInfo[pendingIndex].priceStopLimit = orderInfo[x].priceStopLimit; } } //--Resize the pendingOrderInfo array and delete all the indexes that have zero values ArrayResize(pendingOrderInfo, totalPendingOrdersFound); }
PrintPendingOrdersHistory関数
PrintPendingOrdersHistory関数は、指定された期間内に実行またはキャンセルされた予約注文の詳細な履歴を表示するように設計されています。pendingOrderInfo配列から以前に保存されたデータにアクセスし、予約注文ごとに関連する詳細を出力します。この関数はエクスポート可能なので、このEX5ライブラリを利用する外部モジュールまたはMQL5アプリからアクセスできるようになります。その実装は、私たちが開発した他の出力関数と同様の構造に従います。わかりやすくするために詳細なコメントを付けた完全な実装を以下に示します。
void PrintPendingOrdersHistory(datetime fromDateTime, datetime toDateTime) export { //- Get and save the pending orders history for the specified period GetHistoryData(fromDateTime, toDateTime, GET_PENDING_ORDERS_HISTORY_DATA); int totalPendingOrders = ArraySize(pendingOrderInfo); if(totalPendingOrders <= 0) { Print(""); Print(__FUNCTION__, ": No pending orders history found for the specified period."); return; //- Exit the function } Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalPendingOrders, " pending orders filled or cancelled between (", fromDateTime, ") and (", toDateTime, ")." ); for(int r = 0; r < totalPendingOrders; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Pending Order #", (r + 1)); Print("Symbol: ", pendingOrderInfo[r].symbol); Print("Time Setup: ", pendingOrderInfo[r].timeSetup); Print("Type: ", EnumToString(pendingOrderInfo[r].type)); Print("Ticket: ", pendingOrderInfo[r].ticket); Print("State: ", EnumToString(pendingOrderInfo[r].state)); Print("Time Done: ", pendingOrderInfo[r].timeDone); Print("Volume Initial: ", pendingOrderInfo[r].volumeInitial); Print("Price Open: ", pendingOrderInfo[r].priceOpen); Print("SL Price: ", pendingOrderInfo[r].slPrice, " (slPips: ", pendingOrderInfo[r].slPips, ")"); Print("TP Price: ", pendingOrderInfo[r].tpPrice, " (slPips: ", pendingOrderInfo[r].slPips, ")"); Print("Expiration Time: ", pendingOrderInfo[r].expirationTime); Print("Position ID: ", pendingOrderInfo[r].positionId); Print("Price Stop Limit: ", pendingOrderInfo[r].priceStopLimit); Print("Type Filling: ", EnumToString(pendingOrderInfo[r].typeFilling)); Print("Type Time: ", EnumToString(pendingOrderInfo[r].typeTime)); Print("Reason: ", EnumToString(pendingOrderInfo[r].reason)); Print("Comment: ", pendingOrderInfo[r].comment); Print("Magic: ", pendingOrderInfo[r].magic); Print(""); } }
結論
この記事では、MQL5を使用して注文および取引に関する履歴データを取得する方法を解説しました。そしてこのデータを活用して、決済済みポジションや予約注文の履歴を生成し、それぞれのポジションのライフサイクルを追跡できる監査証跡の作成方法を学びました。これには、ポジションの発生源、決済方法、純利益、ピップ利益、ストップロスやテイクプロフィットのピップ値、取引期間など、さまざまな有用な情報が含まれます。
さらに、History Manager EX5ライブラリの中核となる関数群を開発し、さまざまな種類の履歴データをクエリ・保存・分類できるようにしました。これらの基本的な関数は、ライブラリ内部の処理を担うエンジンの一部を構成しています。ただし、まだ完成には至っておらず、本記事で作成した関数の多くは準備段階のものであり、今後よりユーザー志向のライブラリへと発展させていく土台となります。
次回の記事では、History Manager EX5ライブラリをさらに拡張し、一般ユーザーのニーズに基づいて履歴データを並べ替え・分析できるエクスポート可能な関数を導入していきます。たとえば、直近でクローズされたポジションのプロパティ取得、最後に約定またはキャンセルされた未決注文の分析、特定の銘柄に関する直近のクローズ済みポジションの確認、当日クローズした取引の利益計算、週単位のピップ利益の算出などが可能になります。
また、MetaTrader 5ストラテジーテスターが生成するレポートに匹敵する、詳細なトレードレポートを作成するための高度な並べ替え・分析モジュールも導入します。これらのレポートでは実際の取引履歴データを基に、EAや取引戦略のパフォーマンスを可視化・分析できるようになります。さらに、銘柄やマジックナンバーといったパラメータによって、データをプログラム的にフィルタリング・ソートすることも可能になります。
スムーズな実装を実現するために、History Manager EX5ライブラリには包括的なドキュメントと、実用的なユースケースの例を用意します。これにより、ライブラリをプロジェクトに組み込み、効果的な取引分析を行う方法をわかりやすく理解できます。加えて、取引戦略の最適化やライブラリの機能を最大限に活用するための、シンプルなEAの例やステップバイステップのデモンストレーションも提供します。
この記事の最後に、HistoryManager.mq5のソースコードファイルを添付しています。最後までお読みいただきありがとうございました。あなたの取引およびMQL5プログラミングの旅が素晴らしい成功につながることを心より願っています。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16528





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索