MQL5 交易工具包(第 4 部分):开发历史管理 EX5 库
概述
在这个引人入胜的文章系列中,我们开发了两个全面的 EX5 库:PositionsManager.ex5 ,用于处理和管理仓位,以及 PendingOrdersManager.ex5 ,用于处理挂单。除此之外,我们还创建了实际示例,包括一些具有图形用户界面的示例,以有效地演示这些库的实现。
在本文中,我们将介绍另一个重要的 EX5 库,旨在检索和处理已完成订单、交易和持仓交易的历史记录。此外,我们将开发分析模块来生成交易报告,根据不同的灵活标准评估交易系统、EA 交易或特定交易品种的性能。
本文为那些在处理仓位、订单和交易历史方面遇到困难的 MQL5 初级开发人员提供了实用指南。对于任何寻求库来简化和提高处理交易历史效率的 MQL5 程序员来说,这也将是一个宝贵的资源。
首先,我们将解决许多 MQL5 程序员,特别是那些刚开始在 MetaTrader 5 中处理交易历史的程序员,经常发现难以理解的几个关键问题。
MQL5 中交易事务的生命周期是什么?
在 MQL5 中,交易的生命周期始于订单的执行。订单主要分为两种类型:直接市价单或挂单。
直接市价入场订单
直接市价单是以当前市场价格(卖价或买价)买入或卖出资产的实时请求。我们之前在第一篇和第二篇文章中介绍了如何在开发 Positions Manager 库时处理这些订单。

直接市价订单会立即执行,使其成为手动和自动交易策略的理想选择。一旦执行,订单将转换为有效的未平仓头寸,并被分配一个唯一的编号和单独的头寸标识符( POSITION_ID ) ,这对于在整个生命周期中跟踪和管理头寸的各个阶段更加可靠。
挂单入场
相比之下,挂单( BUY STOP 、 BUY LIMIT 、 SELL STOP 、 SELL LIMIT 、 BUY STOP LIMIT 和 SELL STOP LIMIT )是在达到指定价格水平时触发的延迟订单。本系列的第三篇文章介绍了处理这些类型订单的深入指南,我们在其中开发了挂单管理器库。

在市场价格与预先定义的挂单触发价格一致之前,挂单将保持无效状态。一旦触发,它就会转换为市价单并执行,接收类似于直接市价单的唯一单号和头寸标识符( POSITION_ID ) 。
头寸状态在其生命周期内如何变化?
在一个头寸的整个生命周期中,其状态可能由于各种因素而发生变化:
- 部分关闭:如果部分头寸被关闭,则交易历史记录中会记录相应的退出交易。
- 仓位反转:仓位反转,例如 “平仓” 交易,也被记录为退出交易。
- 完全关闭:当整个仓位被关闭时,无论是手动关闭还是由于追加保证金而通过止盈、止损或强平事件自动关闭,最终退出交易都会记录在交易历史记录中。
了解 MQL5 中交易操作的生命周期至关重要。每笔交易都是从发送到交易服务器的订单开始的,无论是开立挂单的请求、执行直接买入或卖出的市场订单,还是部分关闭现有头寸。无论何种类型,所有交易操作都首先被记录为订单。
如果订单成功执行,它将转换到下一阶段,并作为交易保存在历史数据库中。使用订单和交易可用的各种属性和函数,您可以将每笔交易追溯到其原始订单并将其链接到相应的头寸。这为交易的生命周期创建了一个清晰而系统的轨迹。
这种“面包屑”方法允许您在 MQL5 环境中跟踪每笔交易或交易的起源、进展和结果。它提供了详细的审计跟踪,包括启动交易的订单、执行的确切时间、过程中所做的任何修改以及交易(头寸)的最终结果。这种跟踪不仅提高了透明度,还将使您作为 MQL5 程序员能够开发用于分析交易策略、确定改进领域和优化性能的算法。
在 MQL5 中,头寸代表您当前在市场中持有的活跃且持续进行的交易(仓位)。它处于开放状态,反映特定交易品种的买入或卖出仓位。另一方面,交易是指交易完成 —— 即仓位已完全平仓。 活跃的未平仓头寸和挂单显示在 MetaTrader 5 的工具箱窗口的 “交易” 选项卡下。

已平仓头寸(交易)以及订单和成交均显示在工具箱窗口的“历史”选项卡中。

要访问完整的持仓历史记录,您可以使用平台的菜单选项并选择“持仓”菜单项。还可以使用相同的菜单选项访问订单和成交历史记录。

对于初级 MQL5 程序员来说,仓位和交易之间的区别可能会令人困惑,尤其是在使用平台的标准历史函数时。本文以及我们即将创建的库中的详细代码,将让您清楚地了解 MQL5 中头寸和交易是如何分类和跟踪的。如果你时间紧迫,需要一个现成的历史库,你可以简单地按照下一篇文章中关于如何将其直接实现到项目中的综合文档进行操作。
创建历史管理器库源代码文件 (.mq5)
首先,打开您的 MetaEditor IDE ,然后从菜单中选择“新建”来访问 MQL 向导。在向导中,选择创建一个新的库源文件,我们将其命名为 HistoryManager.mq5 。该文件将成为我们核心函数的基础,致力于管理和分析账户的交易历史。创建新的 HistoryManager.mq5 时,将其保存在我们在第一篇文章中建立的 Libraries\Toolkit 文件夹中。通过将这个新文件存储在与 Positions Manager 和 Pending Orders Manager EX5 库相同的目录中,我们可以为我们的项目保持清晰一致的组织结构。随着工具包的扩展,这种方法将使定位和管理每个组件变得更加容易。

这是我们新创建的 HistoryManager.mq5 源文件的样子。首先删除位于属性指令下方的 “My function” 注释。您的文件中的版权和链接属性指令可能有所不同,但这不会影响代码的行为或性能。您可以使用您喜欢的任何信息自定义版权和链接指令,但确保库属性指令保持不变。
//+------------------------------------------------------------------+ //| 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 库源文件中,我们将首先定义以下组件:
- 预处理器指令:这些将有助于对各种类型的交易历史进行排序和查询。
- 数据结构:这些将存储订单、成交、头寸和挂单的历史数据。
- 全局动态结构数组:这些将保存库中的所有相关历史数据。
在全局范围内定义这些组件可以确保它们在整个库中都是可访问的,并且可以被库中的所有不同模块或函数使用。
预处理器指令
由于我们的历史管理库将处理各种类型的请求,因此以仅检索每个请求所需的特定历史数据的方式进行设计至关重要。这种模块化和有针对性的方法将提高我们库的性能,同时保持各种用例的灵活性。
为了实现这一点,我们将定义整数常量,作为特定类型历史数据的标识符。这些常量将允许库仅针对所需的数据,确保最小的资源消耗和更快的处理速度。
我们将历史数据整理为五大类:
- 订单历史。
- 交易历史。
- 头寸历史。
- 挂单历史。
- 所有历史数据。
通过使用这些常量,库中的函数可以指定它们想要处理的历史记录类型。主要历史记录获取函数将仅查询并返回所请求的数据,从而节省时间和计算资源。让我们首先定义这些整数常量,将它们直接放在代码中最后的 #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[];
获取历史数据函数
GetHistoryDataFunction() 将作为我们 EX5 库的核心,构成其功能的支柱。库中的大多数其他函数将依赖于它来检索基于指定时期和历史类型的交易历史。由于此函数仅供内部使用,因此不会被定义为可导出。
此函数用于获取给定时间段和历史类型所请求的历史数据。它是一个布尔类型的函数,意味着如果成功检索历史记录,它将返回 true ,如果操作失败,它将返回 false 。
GetHistoryDataFunction() 接受三个输入参数:
- 两个 datetime 变量, fromDateTime 和 toDateTime ,指定所需时间段的开始和结束。
- 无符号整数 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:00 UTC 以来经过的秒数),我们可以直接比较这些值以确保正确性。另外,请注意,在 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; }
这是包含所有代码段的完整的 GetHistoryDataFunction() 。
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()函数将负责检索和保存交易历史缓存中当前可用的所有成交历史记录,这些历史记录适用于库中不同函数所请求的不同时期。它不会返回任何数据,并且未定义为可导出,因为它是在库内部调用的,特别是从 GetHistoryData() 函数调用的。此函数将利用 MQL5 的 HistoryDealGet... 标准函数来获取各种成交属性并将它们存储在 dealInfo 动态数据结构数组中。
首先,让我们从创建函数定义或签名开始。
void SaveDealsData() { //-- Our function's code will go here }
由于 SaveDealsData() 是在 GetHistoryData() 函数中调用的,因此在处理交易历史记录之前无需再次调用 HistorySelect() 。SaveDealsData() 函数的第一步是检查是否有任何成交历史记录需要处理。我们将使用 HistoryDealsTotal() 函数实现这一点,该函数返回历史缓存中可用的成交总数。为了提高效率,我们将创建一个整数并将其命名为 totalDeals 来存储总历史成交,并创建一个无符号长整型数,命名为 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() 函数用于检索和显示指定时间段内的历史成交数据。当您需要检查给定时间范围内的一系列成交数据时,此功能将非常有用。它不返回任何数据,而是将成交信息输出到 MetaTrader 5 的日志中以供审查。可以从外部调用此函数,通过利用 GetHistoryData() 函数获取相关数据,为用户提供对过去交易的洞察。
我们首先定义 PrintDealsHistory() 函数。该函数需要两个参数, fromDateTime 和 toDateTime ,它们表示我们要搜索的时间段的开始时间和结束时间。该函数将获取在此时间范围内执行的成交。请注意,该函数被标记为 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() 函数将负责检索和存储交易历史缓存中可用的历史订单数据。此函数逐个处理订单,使用 MQL5 的 HistoryOrderGet... 函数提取其关键属性,并将它们存储在名为 orderInfo 的动态数组中。然后,库的其他部分将使用此数组根据需要分析和操作数据。此函数不会返回任何数据,也不会被定义为可导出,因为它在库内部使用,将优雅地处理错误,并记录任何问题以进行调试。
让我们首先定义函数签名。
void SaveOrdersData() { //-- Our function's code will go here }
接下来,我们确定有多少历史订单可用。这是通过使用 HistoryOrdersTotal() 函数实现的,该函数返回缓存中历史订单的总数。结果存储在名为 totalOrdersHistory 的变量中。此外,我们声明一个无符号长整型变量 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() 函数提供了显示指定时间段内的订单历史详细信息的基本功能。它从 orderInfo 数组中查询先前保存的数据并打印订单的所有相关详细信息。该函数被定义为 export,因为它旨在供使用该库的外部模块或 MQL5 应用程序访问。它遵循与 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() 函数组织交易和订单历史记录,以重建每个头寸的生命周期,通过综合可用数据中的信息,在创建头寸历史记录中发挥核心作用。在 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; }
接下来,我们以相反的顺序循环遍历交易(从最近的交易开始),以确保我们可以将退出交易映射到其对应的入场交易。我们通过评估其入场属性来检查当前交易是否为退出交易。( 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 状态。一旦找到,我们会存储开仓订单的单号和类型,这将有助于确定该仓位是由挂单还是市价单发起的。
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() 函数用于显示指定时间范围内平仓的详细历史记录。它从 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() 函数处理来自订单历史记录的数据以生成并保存挂单历史记录。该函数本质上是从订单历史记录中过滤挂单,存储关键细节,并计算特定值,例如止盈(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; }
现在,我们以相反的顺序循环遍历订单,以确保我们首先处理最近的订单。在循环内部,我们通过评估其类型来检查当前订单是否为挂单。保存的订单历史记录将包括已执行(filled)并转换为仓位或未成为仓位而取消的挂单(如买入限价、卖出止损等)。
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() 函数用于显示指定时间范围内已完成或取消的挂单的详细历史记录。它从 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 检索订单和成交的交易历史数据。您学习了如何利用这些数据来生成平仓和挂单的历史记录,并附带跟踪每个已平仓头寸生命周期的审计线索。这包括其来源、如何平仓以及其他有价值的细节,如净利润、点利润、止损和止盈的点值、交易持续时间等。
我们还开发了历史管理器 EX5 库的核心函数,使我们能够查询、保存和分类不同类型的历史数据。这些基础函数构成了处理其内部工作的库引擎的一部分。然而,我们还有很多工作要做。我们在本文中创建的大多数函数都是准备性的,为更加面向用户的库奠定了基础。
在下一篇文章中,我们将通过引入可导出的函数来扩展历史管理器 EX5库,这些函数旨在根据常见用户需求对历史数据进行排序和分析。例如,您将能够检索最近平仓的属性、分析最后完成或取消的挂单、检查特定交易品种的最后平仓、计算当天的平仓利润、确定每周的点利润等功能。
此外,我们还将包括高级排序和分析模块,以生成类似于 MetaTrader 5 策略测试器生成的详细交易报告。这些报告将分析真实的交易历史数据,提供对 EA 交易或交易策略表现的洞察。您还可以通过编程方式根据交易品种或幻数等参数对这些数据进行过滤和排序。
为了使实施无缝衔接,我们将提供历史管理器 EX5库的全面文档以及实际用例示例。这些示例将演示如何将库集成到您的项目中并执行有效的交易分析。此外,我们将提供简单的 EA 交易示例和分步演示,以帮助您优化交易策略并充分利用该库的功能。
您可以在本文末尾找到附加的 HistoryManager.mq5 源代码文件。感谢您的关注,并祝您在交易和 MQL5 编程之旅中取得巨大成功!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16528
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
交易中的神经网络:搭配预测编码的混合交易框架(终篇)
价格行为分析工具包开发(第11部分):基于Heikin Ashi(平均K线)信号的智能交易系统(EA)
开发回放系统(第 78 部分):新 Chart Trade(五)
迁移至 MQL5 Algo Forge(第 4 部分):使用版本和发布