
MQL5 Cookbook: 交易历史和取得仓位信息的函数库
简介
现在是时候简单总结一下之前关于仓位属性文章的内容了,在本文中,我们会额外创建几个函数来取得只能通过访问交易历史才能获得的属性,我们也会对数据结构更加熟悉,这使我们可以用更加方便的方法访问仓位和交易品种属性。
交易系统中如果仓位在它们的存续期间都保持相同的交易量,则不需要本文中将要提供的函数,但是如果您计划实现一个资金管理系统,晚些时候在您的交易策略中控制仓位的手数,这些函数是不可缺少的。
在我们开始之前,我想建议第一次访问这个网站或者才开始学习MQL5语言,链接到这篇文章的读者们从"MQL Cookbook"系列早些的文章开始。
EA 交易开发
为了能够看到修改过的EA交易中新函数的操作(EA交易来自前文,叫做"MQL5 Cookbook: 怎样在设置/修改交易参数时避免错误"), 在已有持仓,而建仓信号再度出现的时候,我们将增加继续增加仓位交易量的功能,
仓位历史中可能有几个交易,而且如果在交易过程中有仓位交易量的改变,当前仓位价格必然也有所改变。为了找出第一次的进场价格,我们需要访问指定仓位的交易历史,下图演示了仓位仅有一个交易(进场点)的实例:
图 1. 仓位的第一个交易
下面的图显示了随第二个交易改变的仓位价格:
图 2. 仓位的第二个交易
如前文中演示的,您使用标准标识符只能取得当前的仓位价格(POSITION_PRICE_OPEN)和持仓的交易品种的当前价格(POSITION_PRICE_CURRENT)。
然而,在一些交易系统中我们需要知道当前价格和第一个进场点价格,以及最后一笔交易价格之间的距离,所有这些信息存在于账户的交易/订单历史中。以下是和上图关联的交易列表:
图 3. 账户中的交易历史
我相信,现在状况非常清楚了,所有目标都已设定,让我们继续修改前文中EA交易。首先,我们会在仓位属性枚举中增加新的数值为0,6,9,12和16的标识符:
//--- 仓位属性的枚举 enum ENUM_POSITION_PROPERTIES { P_TOTAL_DEALS = 0, P_SYMBOL = 1, P_MAGIC = 2, P_COMMENT = 3, P_SWAP = 4, P_COMMISSION = 5, P_PRICE_FIRST_DEAL= 6, P_PRICE_OPEN = 7, P_PRICE_CURRENT = 8, P_PRICE_LAST_DEAL = 9, P_PROFIT = 10, P_VOLUME = 11, P_INITIAL_VOLUME = 12, P_SL = 13, P_TP = 14, P_TIME = 15, P_DURATION = 16, P_ID = 17, P_TYPE = 18, P_ALL = 19 };
关于每个属性的注释我们会在稍下面的一个结构中看到,
让我们增加外部参数的数量,现在,我们可以设置:
- MagicNumber - EA交易的唯一编号 (幻数);
- Deviation - 滑点;
- VolumeIncrease - 仓位交易量增加值;
- InfoPanel - 一个让您启用/禁用信息面板显示的参数
这是如何实现:
//--- EA交易的外部参数 sinput long MagicNumber=777; // 幻数 sinput int Deviation=10; // 滑点 input int NumberOfBars=2; // 用于买/卖的牛市/熊市柱数 input double Lot=0.1; // 手数 input double VolumeIncrease=0.1; // 仓位交易量增加量 input double StopLoss=50; // 止损 input double TakeProfit=100; // 获利 input double TrailingStop=10; // 跟踪止损 input bool Reverse=true; // 反向仓位 sinput bool ShowInfoPanel=true; // 是否显示信息面板
请注意使用sinput修饰符的变量,这个修饰符允许您在策略测试器中禁用其优化。事实上,当开发您自己使用的程序时,您会很清楚哪些参数会影响最终结果,所以您从优化中排除它们,但是当有很多参数时,这个方法使您直观地把他们从其他参数中分开,因为它们是灰色标识的:
图 4. 禁用优化的参数时灰色标识的.
我们现在用数据结构(struct)来替代用于保存仓位和交易品种属性值的全局变量:
//--- 仓位属性 struct position_properties { uint total_deals; // 交易数量 bool exists; // 有没有开启仓位的标志 string symbol; // 交易品种 long magic; // 幻数 string comment; // 注释 double swap; // 库存费 double commission; // 手续费 double first_deal_price; // 仓位中第一笔交易的价格 double price; // 仓位当前价格 double current_price; // 仓位交易品种的当前价格 double last_deal_price; // 仓位最后一笔交易的价格 double profit; // 仓位的利润/亏损 double volume; // 当前仓位交易量 double initial_volume; // 初始仓位交易量 double sl; // 仓位的止损价位 double tp; // 仓位的获利价位 datetime time; // 仓位建仓时间 ulong duration; // 持仓以秒数计算的时间 long id; // 仓位编号 ENUM_POSITION_TYPE type; // 仓位类型 };
//--- 交易品种属性 struct symbol_properties { int digits; // 价格的小数点位数 int spread; // 点差点数 int stops_level; // 止损价位 double point; // 点值 double ask; // 买价 double bid; // 卖价 double volume_min; // 交易的最小交易量 double volume_max; // 交易的最大交易量 double volume_limit; // 仓位以及一个方向上订单允许的最大交易量 double volume_step; // 交易量改变的最小步长 double offset; // 一个事务中最大价格偏移量 double up_level; // 止损价格上限 double down_level; // 止损价格下限 }
现在,为了访问结构中的某个元素,我们需要建立此结构类型的一个变量,这个过程和创建一个交易类的对象类似,就像前一篇叫做"MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性"的文章中提到的那样。
//--- 仓位和交易品种属性的变量
position_properties pos;
symbol_properties symb;
您可以像使用类方法同样的方式访问其中的元素,换句话说,只要在结构变量的后面加一个点,然后此结构包含的元素列表就显示出来了,这非常方便,如果为结构中的字段提供单行注释(像我们例子中那样),他们会在列表右边的提示信息中显示。
图 5. 结构栏位列表.
另外一个重点,在修改EA交易的过程中,我们已经把在很多函数中用到的全局变量修改了,所以现在我们需要用对应交易品种和仓位属性结构的字段来替代它们,比如,用于保存是否有开启仓位标志的pos_open全局变量已经被position_properties结构类型中的exists字段替代,这样的话,哪里使用了pos_open变量,哪里都要用pos.exists来替代。
如果您人工做的话会很慢而且很累,所以我们最好使用MetaQuotes语言编辑器的特性来自动化这个任务:在编辑菜单下的查找和替换 -> 替换或者Ctrl+H组合键:
图 6. 查找和替换文字.
我们需要查找和替换所有用于仓位和交易品种属性的全局变量,然后编译文件再运行一个测试。如果没有发现错误,说明我们已经做好了所有事情。我在这里就不提供代码了,以免把文章变得太长而没有必要。另外,准备好的代码已经附在文章末尾以便下载。
现在我们已经把变量的问题解决了,让我们继续修改已有的函数以及创建新函数。
在外部参数中,您现在可以设置幻数和滑点点数,我们则需要在EA交易代码中做相关修改。我们会创建一个用户定义的辅助函数OpenPosition(), 这些参数将使用CTrade类的函数在发送订单或者开启仓位之前设置。
//+------------------------------------------------------------------+ //| 开启一个仓位 | //+------------------------------------------------------------------+ void OpenPosition(double lot, ENUM_ORDER_TYPE order_type, double price, double sl, double tp, string comment) { trade.SetExpertMagicNumber(MagicNumber); // 设置交易结构中的幻数 trade.SetDeviationInPoints(CorrectValueBySymbolDigits(Deviation)); // 设置滑点点数 //--- 如果开启仓位失败,打印相关信息 if(!trade.PositionOpen(_Symbol,order_type,lot,price,sl,tp,comment)) { Print("开启仓位错误: ",GetLastError()," - ",ErrorDescription(GetLastError())); } }
在EA交易的主要交易函数 - TradingBlock() 的代码中,我们只需要做些很小的修改. 以下是改动过的部分程序代码:
//--- 如果没有持仓 if(!pos.exists) { //--- 调整交易量 lot=CalculateLot(Lot); //--- 开启仓位 OpenPosition(lot,order_type,position_open_price,sl,tp,comment); } //--- 如果有持仓 else { //--- 读取仓位类型 GetPositionProperties(P_TYPE); //--- 如果仓位和信号方向相反并且反向仓位被启用 if(pos.type==opposite_position_type && Reverse) { //--- 取得仓位交易量 GetPositionProperties(P_VOLUME); //--- 调整交易量 lot=pos.volume+CalculateLot(Lot); //--- 反转仓位 OpenPosition(lot,order_type,position_open_price,sl,tp,comment); return; } //--- 如果信号与仓位方向相同并且启用了增加交易量,则增加仓位交易量 if(!(pos.type==opposite_position_type) && VolumeIncrease>0) { //--- 取得当前仓位的止损 GetPositionProperties(P_SL); //--- 取得当前仓位的获利 GetPositionProperties(P_TP); //--- 调整交易量 lot=CalculateLot(Increase); //--- 增加仓位交易量 OpenPosition(lot,order_type,position_open_price,pos.sl,pos.tp,comment); return; }
以上代码在检查当前仓位方向和信号方向模块部分已经增强,如果它们的方向一致,并且已经通过外部变量启用了增加仓位交易量(VolumeIncrease 参数大于零), 我们检查/调整指定的手数然后发送相关的订单。现在,如果您需要发送一个订单,用于开启或反转一个仓位或者增加仓位交易量,只需要写下一行代码。
让我们创建函数来从交易历史中获取仓位属性,我们从CurrentPositionTotalDeals() 函数开始,它返回当前仓位交易的数量:
//+------------------------------------------------------------------+ //| 返回当前仓位的交易数量 | //+------------------------------------------------------------------+ uint CurrentPositionTotalDeals() { int total =0; // 选择的历史列表中总的交易数量 int count =0; // 根据仓位交易品种的交易计数器 string deal_symbol =""; // 交易的交易品种 //--- 如果获得了仓位历史 if(HistorySelect(pos.time,TimeCurrent())) { //--- 取得获得列表中的交易数量 total=HistoryDealsTotal(); //--- 遍历获得列表中所有的交易 for(int i=0; i<total; i++) { //--- 取得交易的交易品种 deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL); //--- 如果交易的交易品种和当前交易品种相同,增加计数器 if(deal_symbol==_Symbol) count++; } } //--- return(count); }
以上代码已经提供了比较详细的注释,但是我们还是应该说一下历史是怎样选择的。在我们的实例中,我们使用HistorySelect()函数以当前仓位开启时间为起点,获得直到当前时间点的历史列表,在历史选择好以后,我们可以使用HistoryDealsTotal()函数来获得列表中的交易数量。其他部分看注释就应该很清楚了。
特定仓位的历史也可以根据其编号使用HistorySelectByPosition()函数进行选择,这里,您需要考虑到,仓位反转的时候仓位编号还是相同的,就像在我们的EA交易中出现的那样,然而,仓位开启时间并不会因为反转而改变,所以这种方法更容易实现。但是如果您还必须处理当前开启仓位以外的交易历史,您需要使用仓位编号。我们会在将来的文章中再回头讨论交易历史。
让我们继续创建一个CurrentPositionFirstDealPrice()函数用来返回仓位第一笔交易的价格,也就是仓位开启时的交易价格。
//+------------------------------------------------------------------+ //| 返回当前仓位第一笔交易的价格 | //+------------------------------------------------------------------+ double CurrentPositionFirstDealPrice() { int total =0; // 所选历史列表中的交易总数 string deal_symbol =""; // 交易的交易品种 double deal_price =0.0; // 交易的价格 datetime deal_time =NULL; // 交易的时间 //--- 如果获得了仓位历史 if(HistorySelect(pos.time,TimeCurrent())) { //--- 取得获得列表中的交易数量 total=HistoryDealsTotal(); //--- 遍历获得列表中所有的交易 for(int i=0; i<total; i++) { //--- 取得交易的价格 deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE); //--- 取得交易的交易品种 deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL); //--- 取得交易的时间 deal_time=(datetime)HistoryDealGetInteger(HistoryDealGetTicket(i),DEAL_TIME); //--- 如果交易和仓位开启时间相同, // 并且交易的交易品种和当前交易品种相同,退出循环 if(deal_time==pos.time && deal_symbol==_Symbol) break; } } //--- return(deal_price); }
这里的原则和前一函数相同,我们从仓位开启时开始读取历史,然后在每个迭代中检查仓位开启的时间和交易的时间,我们读取交易品种名称,交易的时间,以及交易的价格,当交易的时间和仓位开启时间一致时,即是第一笔交易,因为价格已经赋值给相关的变量,我们只需要返回其数值。
让我们继续,有时,您可能需要取得当前仓位的最后一笔交易的价格,为了这个目的,我们将要创建一个CurrentPositionLastDealPrice() 函数:
//+------------------------------------------------------------------+ //| 返回当前仓位的最后一笔交易的价格 | //+------------------------------------------------------------------+ double CurrentPositionLastDealPrice() { int total =0; // 所选历史列表的交易总数 string deal_symbol =""; // 交易的交易品种 double deal_price =0.0; // 价格 //--- 如果获得了仓位历史 if(HistorySelect(pos.time,TimeCurrent())) { //--- 取得获得列表中的交易数量 total=HistoryDealsTotal(); //--- 在获得的列表中从第一个交易开始遍历所有交易 for(int i=total-1; i>=0; i--) { //--- 取得交易的价格 deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE); //--- 取得交易的交易品种 deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL); //--- 如果交易的交易品种和当前交易品种相同,退出循环 if(deal_symbol==_Symbol) break; } } //--- return(deal_price); }
这一次循环从列表的最后一笔交易开始,通常情况所需的交易在循环的第一个迭代就找到了,但是如果您同时交易几个交易品种,循环将继续,直到交易的交易品种和当前交易品种相匹配。
当前仓位的交易量可以使用POSITION_VOLUME标准标识符获得. 为了找到初始仓位交易量(第一笔交易的交易量), 我们将要创建一个CurrentPositionInitialVolume() 函数:
//+------------------------------------------------------------------+ //| 返回当前仓位的初始交易量 | //+------------------------------------------------------------------+ double CurrentPositionInitialVolume() { int total =0; // 所选历史列表的交易总数 ulong ticket =0; // 交易的订单编号 ENUM_DEAL_ENTRY deal_entry =WRONG_VALUE; // 仓位修改方法 bool inout =false; // 仓位反转标志 double sum_volume =0.0; // 所有交易累计交易量的计数器,第一笔交易除外 double deal_volume =0.0; // 交易的交易量 string deal_symbol =""; // 交易的交易品种 datetime deal_time =NULL; // 交易执行时间 //--- 如果获得了仓位历史 if(HistorySelect(pos.time,TimeCurrent())) { //--- 取得获得列表中的交易数量 total=HistoryDealsTotal(); //--- 在获得的列表中从最后一笔交易到第一笔交易迭代所有交易 for(int i=total-1; i>=0; i--) { //--- 如果根据仓位得到了订单编号,那么... if((ticket=HistoryDealGetTicket(i))>0) { //--- 取得交易的交易量 deal_volume=HistoryDealGetDouble(ticket,DEAL_VOLUME); //--- 取得仓位修改方法 deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY); //--- 取得交易执行时间 deal_time=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME); //--- 取得交易的交易品种 deal_symbol=HistoryDealGetString(ticket,DEAL_SYMBOL); //--- 当仓位执行时间小于或者等于仓位开启时间,退出循环 if(deal_time<=pos.time) break; //--- 否则根据仓位交易品种计算交易的累积交易量,第一笔交易除外 if(deal_symbol==_Symbol) sum_volume+=deal_volume; } } } //--- 如果仓位修改方法是反转 if(deal_entry==DEAL_ENTRY_INOUT) { //--- 如果仓位交易量曾经被增加/减少 // 比如交易数量大于1 if(fabs(sum_volume)>0) { //--- 当前仓位交易量减去除第一笔外的所有交易的交易量 double result=pos.volume-sum_volume; //--- 如果结果大于0,返回该结果,否则返回当前仓位交易量 deal_volume=result>0 ? result : pos.volume; } //--- 如果除了进场交易外没有更多交易, if(sum_volume==0) deal_volume=pos.volume; // 返回当前仓位交易量 } //--- 返回初始仓位交易量 return(NormalizeDouble(deal_volume,2)); }
这个函数比前面的要复杂一些,我已经尝试考虑到所有可能得出错误结果的状况,通过仔细测试也没有发现任何问题。代码中提供的详细注释应该帮助您了解到这一点。
一个返回仓位持仓时间的函数也是很有用的,我们会安排它并且允许用户选择返回值的适当格式:秒,分钟,小时或者天。为了这个目标,让我们创建另外一个枚举:
//--- 仓位持仓时间 enum ENUM_POSITION_DURATION { DAYS = 0, // 天 HOURS = 1, // 小时 MINUTES = 2, // 分钟 SECONDS = 3 // 秒 };
以下是CurrentPositionDuration()函数所有相关计算的代码:
//+------------------------------------------------------------------+ //| 返回当前仓位的持仓时间 | //+------------------------------------------------------------------+ ulong CurrentPositionDuration(ENUM_POSITION_DURATION mode) { ulong result=0; // 最终结果 ulong seconds=0; // 秒数 //--- 计算持仓时间的秒数 seconds=TimeCurrent()-pos.time; //--- switch(mode) { case DAYS : result=seconds/(60*60*24); break; // 计算天数 case HOURS : result=seconds/(60*60); break; // 计算小时数 case MINUTES : result=seconds/60; break; // 计算分钟数 case SECONDS : result=seconds; break; // 无计算 (秒数) //--- default : Print(__FUNCTION__,"(): 未知传入持仓时间模式!"); return(0); } //--- 返回结果 return(result); }
让我们为显示仓位属性的信息面板创建一个CurrentPositionDurationToString()函数. 这个函数将把持仓时间的描述转换为易于被用户理解的格式,秒数会传给函数,而此函数会返回包含仓位持仓时间的天数,小时数,分钟数和秒数的字符串:
//+------------------------------------------------------------------+ //| 把仓位持仓时间转换为字符串 | //+------------------------------------------------------------------+ string CurrentPositionDurationToString(ulong time) { //--- 如果没有仓位则返回横线 string result="-"; //--- 如果有持仓 if(pos.exists) { //--- 用于计算结果的变量 ulong days=0; ulong hours=0; ulong minutes=0; ulong seconds=0; //--- seconds=time%60; time/=60; //--- minutes=time%60; time/=60; //--- hours=time%24; time/=24; //--- days=time; //--- 以指定的格式 DD:HH:MM:SS 生成字符串 result=StringFormat("%02u d: %02u h : %02u m : %02u s",days,hours,minutes,seconds); } //--- 返回结果 return(result); }
现在每件事情都完成了,我就不再提供GetPositionProperties() 和GetPropertyValue() 函数为适应以上改变所修改的代码了,如果您阅读了本系列前面的文章,您可以没有任何困难地自己去做,不管怎样,源代码文件在本文附件中。
最终结果,信息面板显示如下:
图 7. 在信息面板上演示所有仓位属性.
这样,我们现在有了获得仓位属性的函数库,在将来的文章中如果我们需要还会继续做工作。
优化参数和测试EA交易
作为试验,让我们尝试优化EA交易的参数,尽管我们现在拥有的还不能叫做全功能交易系统,我们所获得的结果将可以让我们大开眼界,并且增加我们作为交易系统开发者的经验。
我们使策略测试器如下显示:
图 8. 策略测试器参数优化设置
EA交易外部参数设置应该如下:
图 9. EA交易用于优化的参数设置
在优化之后,我们根据最大采收率将获得的结果排序:
图 10. 根据最大采收率排序的结果.
让我们现在测试最上面的一组参数,其采收率等于4.07. 即使我们只是在EURUSD上做了优化, 我们在其他许多交易品种上也能得到正面结果:
EURUSD的结果:
图 11. EURUSD的结果.
AUDUSD的结果:
图 12. AUDUSD的结果.
NZDUSD的结果:
图13. Results for NZDUSD.
结论
理论上任何想法都可以开发和提高,每一个交易系统在认为有缺陷而摒弃之前都应该仔细测试,在未来的文章中,我们会看一下在自定义和适应几乎任何交易系统时起到重要作用的各种不同的机制和规划。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/644
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


我发现在默认脚本设置下,会出现类似 "无法返回HistoryOrderGetDouble() 函数"的记录。
也就是说,布尔版本的函数无法获取属性值。
我正在成功运行所有程序。更详细地查看所有内容(日志输出),找出原因。订单数、票据数、错误数等。
我明白了。
一定是中介出了问题。我得到的错误编号是 4755(未找到交易)。我会写信给服务台。
。
哦,我明白了。
一定是中介出了问题。我得到的错误信息是 4755(找不到交易)。我会写信给服务台。
。