MQL5 Cookbook: 交易历史和取得仓位信息的函数库·进阶篇
📘

MQL5 Cookbook: 交易历史和取得仓位信息的函数库·进阶篇

第 2/3 篇

「把仓位和品种属性塞进结构体」

写 EA 时最忌讳把止损、点差、交易量限制散落在全局变量里,后期调参会乱套。用结构体把单仓属性和品种属性分开装,是后续批量管理持仓的前提。 下面这段定义里,position_properties 存的是单笔仓位的运行时数据:sl 是止损价、tp 是获利价、time 是建仓时间、duration 以秒计持仓时长、id 是仓位编号、type 标记多空方向。symbol_properties 则装品种规则:digits 是报价小数位、spread 是点差点数、stops_level 是止损最小距离、point 是点值、ask/bid 是当前买卖价,volume_min/max/step 约束手数,up_level/down_level 框住止损价上下限。 实际下单前,先声明 pos 和 symb 两个实例,再调 OpenPosition 函数。函数入口接了手数、订单类型、挂单价、sl、tp 和注释,内部第一件事是 trade.SetExpertMagicNumber 绑定幻数,第二件事用 CorrectValueBySymbolDigits 把 Deviation 折算成对应品种小数位的滑点点数再塞给 trade.SetDeviationInPoints。若开仓返回失败,代码会打印诊断信息方便你回 MT5 终端查日志。 外汇和贵金属杠杆高,stops_level 和 up_level/down_level 若没按品种真实值填,订单会被服务器直接拒,开仓前务必用 SymbolInfoInteger 把这几个字段刷进 symb。

MQL5 / C++
class="type">class="kw">double sl; class=class="str">"cmt">// 仓位的止损价位
class="type">class="kw">double tp; class=class="str">"cmt">// 仓位的获利价位
class="type">class="kw">datetime time; class=class="str">"cmt">// 仓位建仓时间
class="type">ulong duration; class=class="str">"cmt">// 持仓以秒数计算的时间
class="type">long id; class=class="str">"cmt">// 仓位编号
class="type">ENUM_POSITION_TYPE type; class=class="str">"cmt">// 仓位类型
};
class=class="str">"cmt">//--- 交易品种属性
class="kw">struct symbol_properties
  {
   class="type">int digits; class=class="str">"cmt">// 价格的小数点位数
   class="type">int spread; class=class="str">"cmt">// 点差点数
   class="type">int stops_level; class=class="str">"cmt">// 止损价位
   class="type">class="kw">double point; class=class="str">"cmt">// 点值
   class="type">class="kw">double ask; class=class="str">"cmt">// 买价
   class="type">class="kw">double bid; class=class="str">"cmt">// 卖价
   class="type">class="kw">double volume_min; class=class="str">"cmt">// 交易的最小交易量
   class="type">class="kw">double volume_max; class=class="str">"cmt">// 交易的最大交易量
   class="type">class="kw">double volume_limit; class=class="str">"cmt">// 仓位以及一个方向上订单允许的最大交易量
   class="type">class="kw">double volume_step; class=class="str">"cmt">// 交易量改变的最小步长
   class="type">class="kw">double offset; class=class="str">"cmt">// 一个事务中最大价格偏移量
   class="type">class="kw">double up_level; class=class="str">"cmt">// 止损价格上限
   class="type">class="kw">double down_level; class=class="str">"cmt">// 止损价格下限
  }
class=class="str">"cmt">//--- 仓位和交易品种属性的变量
position_properties pos;
symbol_properties symb;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 开启一个仓位 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OpenPosition(class="type">class="kw">double lot,
                  ENUM_ORDER_TYPE order_type,
                  class="type">class="kw">double price,
                  class="type">class="kw">double sl,
                  class="type">class="kw">double tp,
                  class="type">class="kw">string comment)
  {
   trade.SetExpertMagicNumber(MagicNumber); class=class="str">"cmt">// 设置交易结构中的幻数
   trade.SetDeviationInPoints(CorrectValueBySymbolDigits(Deviation)); class=class="str">"cmt">// 设置滑点点数
class=class="str">"cmt">//--- 如果开启仓位失败,打印相关信息

◍ 持仓状态下的加仓与反手逻辑

当持仓不存在时,脚本先按 CalculateLot(Lot) 算出手数,再调用 OpenPosition 开仓;这一步和裸信号触发等价,适合在 MT5 策略测试器里单步看 lot 取值。 若已有持仓,先读仓位类型。若信号方向与持仓相反且 Reverse 开关开启,则取原仓 volume,用 pos.volume+CalculateLot(Lot) 合成新手数反手开仓,随后直接 return 避免同根 K 线重复操作。 同向情形由 VolumeIncrease 控制:当它大于 0,会取原仓 sl/tp,再算 CalculateLot(Increase) 的手数追加同方向仓位。注意这里新单的止损止盈沿用老仓数值,不是重新计算,回测时若老仓 sl 已漂移,加仓单会带着旧止损进场。 CurrentPositionTotalDeals 这个函数用来数当前品种在持仓生命周期内的成交笔数:用 HistorySelect(pos.time, TimeCurrent()) 拉历史,再遍历 HistoryDealsTotal 比对 DEAL_SYMBOL。实盘里若同一品种手动平过部分仓,count 仍会计入历史成交,可能影响你的加仓次数判断。外汇与贵金属杠杆高,这类自动加仓逻辑在跳空行情中可能连续触发,请先在模拟盘验证。

MQL5 / C++
if(!trade.PositionOpen(_Symbol,order_type,lot,price,sl,tp,comment))
   { Print("开启仓位错误: ",GetLastError()," - ",ErrorDescription(GetLastError())); }
}
class=class="str">"cmt">//--- 如果没有持仓
   if(!pos.exists)
   {
   class=class="str">"cmt">//--- 调整交易量
   lot=CalculateLot(Lot);
   class=class="str">"cmt">//--- 开启仓位
   OpenPosition(lot,order_type,position_open_price,sl,tp,comment);
   }
class=class="str">"cmt">//--- 如果有持仓
   else
   {
   class=class="str">"cmt">//--- 读取仓位类型
   GetPositionProperties(P_TYPE);
   class=class="str">"cmt">//--- 如果仓位和信号方向相反并且反向仓位被启用
   if(pos.type==opposite_position_type && Reverse)
     {
     class=class="str">"cmt">//--- 取得仓位交易量
     GetPositionProperties(P_VOLUME);
     class=class="str">"cmt">//--- 调整交易量
     lot=pos.volume+CalculateLot(Lot);
     class=class="str">"cmt">//--- 反转仓位
     OpenPosition(lot,order_type,position_open_price,sl,tp,comment);
     class="kw">return;
     }
   class=class="str">"cmt">//--- 如果信号与仓位方向相同并且启用了增加交易量,则增加仓位交易量
   if(!(pos.type==opposite_position_type) && VolumeIncrease>class="num">0)
     {
     class=class="str">"cmt">//--- 取得当前仓位的止损
     GetPositionProperties(P_SL);
     class=class="str">"cmt">//--- 取得当前仓位的获利
     GetPositionProperties(P_TP);
     class=class="str">"cmt">//--- 调整交易量
     lot=CalculateLot(Increase);
     class=class="str">"cmt">//--- 增加仓位交易量
     OpenPosition(lot,order_type,position_open_price,pos.sl,pos.tp,comment);
     class="kw">return;
     }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回当前仓位的交易数量                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">uint CurrentPositionTotalDeals()
  {
   class="type">int     total       =class="num">0;   class=class="str">"cmt">// 选择的历史列表中总的交易数量
   class="type">int     count       =class="num">0;   class=class="str">"cmt">// 根据仓位交易品种的交易计数器
   class="type">class="kw">string  deal_symbol =""; class=class="str">"cmt">// 交易的交易品种
class=class="str">"cmt">//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
   {
   class=class="str">"cmt">//--- 取得获得列表中的交易数量
   total=HistoryDealsTotal();
   class=class="str">"cmt">//--- 遍历获得列表中所有的交易
   for(class="type">int i=class="num">0; i<total; i++)
     {
     class=class="str">"cmt">//--- 取得交易的交易品种
     deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
     class=class="str">"cmt">//--- 如果交易的交易品种和当前交易品种相同,增加计数器
     if(deal_symbol==_Symbol)
       count++;
     }
   }
class=class="str">"cmt">//---
   class="kw">return(count);
   }

抓取持仓首单与末单的成交价

做加仓逻辑或浮盈回测时,往往需要知道当前持仓的第一笔和最后一笔成交价。下面两个函数直接基于 MT5 历史交易池提取,不依赖外部缓存,开 MT5 新建脚本即可粘贴验证。

MQL5 / C++
class="type">class="kw">double CurrentPositionFirstDealPrice()
  {
   class="type">int       total       =class="num">0;      class=class="str">"cmt">// 所选历史列表中的交易总数
   class="type">class="kw">string    deal_symbol ="";     class=class="str">"cmt">// 交易的交易品种
   class="type">class="kw">double    deal_price  =class="num">0.0;    class=class="str">"cmt">// 交易的价格
   class="type">class="kw">datetime  deal_time   =NULL;   class=class="str">"cmt">// 交易的时间
class=class="str">"cmt">//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
     {
      class=class="str">"cmt">//--- 取得获得列表中的交易数量
      total=HistoryDealsTotal();
      class=class="str">"cmt">//--- 遍历获得列表中所有的交易
      for(class="type">int i=class="num">0; i<total; i++)
        {
         class=class="str">"cmt">//--- 取得交易的价格
         deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE);
         class=class="str">"cmt">//--- 取得交易的交易品种
         deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
         class=class="str">"cmt">//--- 取得交易的时间
         deal_time=(class="type">class="kw">datetime)HistoryDealGetInteger(HistoryDealGetTicket(i),DEAL_TIME);
         class=class="str">"cmt">//--- 如果交易和仓位开启时间相同,
         class=class="str">"cmt">//    并且交易的交易品种和当前交易品种相同,退出循环
         if(deal_time==pos.time && deal_symbol==_Symbol)
            break;
        }
     }
class=class="str">"cmt">//---
   class="kw">return(deal_price);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回当前仓位的最后一笔交易的价格                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CurrentPositionLastDealPrice()
  {
   class="type">int      total       =class="num">0;     class=class="str">"cmt">// 所选历史列表的交易总数
   class="type">class="kw">string   deal_symbol ="";    class=class="str">"cmt">// 交易的交易品种 
   class="type">class="kw">double   deal_price  =class="num">0.0;   class=class="str">"cmt">// 价格
class=class="str">"cmt">//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
     {
      class=class="str">"cmt">//--- 取得获得列表中的交易数量
      total=HistoryDealsTotal();
      class=class="str">"cmt">//--- 在获得的列表中从第一个交易开始遍历所有交易
      for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
        {
         class=class="str">"cmt">//--- 取得交易的价格
         deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE);
         class=class="str">"cmt">//--- 取得交易的交易品种
         deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
         class=class="str">"cmt">//--- 如果交易的交易品种和当前交易品种相同,退出循环
         if(deal_symbol==_Symbol)
            break;
        }
     }
class=class="str">"cmt">//---
   class="kw">return(deal_price);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
首单函数从 HistorySelect 返回的列表头部正向遍历,用 deal_time==pos.time 且 symbol 匹配来锁定建仓那一笔,精准拿到开盘价。末单函数则倒序从 total-1 往下扫,只要 symbol 对上就 break,因此返回的是该品种最近一次成交价,可能就是加仓单或减仓单的价格。 两个函数都依赖全局变量 pos.time(持仓开启时间),实盘里若 pos 未被正确赋值,first 函数会返回 0.0。外汇与贵金属杠杆高、滑点随机,用末单价算平均成本时务必自行加容错,避免把 0.0 当真实成交价喂给风控模块。

MQL5 / C++
class="type">class="kw">double CurrentPositionFirstDealPrice()
  {
   class="type">int       total       =class="num">0;      class=class="str">"cmt">// 所选历史列表中的交易总数
   class="type">class="kw">string    deal_symbol ="";     class=class="str">"cmt">// 交易的交易品种
   class="type">class="kw">double    deal_price  =class="num">0.0;    class=class="str">"cmt">// 交易的价格
   class="type">class="kw">datetime  deal_time   =NULL;   class=class="str">"cmt">// 交易的时间
class=class="str">"cmt">//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
     {
      class=class="str">"cmt">//--- 取得获得列表中的交易数量
      total=HistoryDealsTotal();
      class=class="str">"cmt">//--- 遍历获得列表中所有的交易
      for(class="type">int i=class="num">0; i<total; i++)
        {
         class=class="str">"cmt">//--- 取得交易的价格
         deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE);
         class=class="str">"cmt">//--- 取得交易的交易品种
         deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
         class=class="str">"cmt">//--- 取得交易的时间
         deal_time=(class="type">class="kw">datetime)HistoryDealGetInteger(HistoryDealGetTicket(i),DEAL_TIME);
         class=class="str">"cmt">//--- 如果交易和仓位开启时间相同,
         class=class="str">"cmt">//    并且交易的交易品种和当前交易品种相同,退出循环
         if(deal_time==pos.time && deal_symbol==_Symbol)
            break;
        }
     }
class=class="str">"cmt">//---
   class="kw">return(deal_price);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回当前仓位的最后一笔交易的价格                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CurrentPositionLastDealPrice()
  {
   class="type">int      total       =class="num">0;     class=class="str">"cmt">// 所选历史列表的交易总数
   class="type">class="kw">string   deal_symbol ="";    class=class="str">"cmt">// 交易的交易品种 
   class="type">class="kw">double   deal_price  =class="num">0.0;   class=class="str">"cmt">// 价格
class=class="str">"cmt">//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
     {
      class=class="str">"cmt">//--- 取得获得列表中的交易数量
      total=HistoryDealsTotal();
      class=class="str">"cmt">//--- 在获得的列表中从第一个交易开始遍历所有交易
      for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
        {
         class=class="str">"cmt">//--- 取得交易的价格
         deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE);
         class=class="str">"cmt">//--- 取得交易的交易品种
         deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
         class=class="str">"cmt">//--- 如果交易的交易品种和当前交易品种相同,退出循环
         if(deal_symbol==_Symbol)
            break;
        }
     }
class=class="str">"cmt">//---
   class="kw">return(deal_price);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「从成交历史反推当前仓位的初始手数」

在 MT5 的 EA 逻辑里,净仓位的初始开仓量往往不能直接读持仓对象拿到,尤其经历过加仓、减仓或反手之后。下面这个函数演示了如何回溯 HistoryDeals 列表,把当前符号上、早于仓位开启时间之后的成交量累加,从而逼近“最初那一笔”的规模。 函数先用 HistorySelect 按仓位开启时间 pos.time 到当前时间拉取成交历史,再用 HistoryDealsTotal 拿到总数,从最后一笔倒序遍历到第一笔。每一笔都取出交易量、入场方式、执行时间和品种;一旦 deal_time 小于等于 pos.time 就 break,保证只统计该仓位生效以来的后续改动。 关键判断在 deal_entry==DEAL_ENTRY_INOUT:这是平台标记“仓位内反转/反手”的枚举值。若命中,说明仓位曾被反向操作,此时 sum_volume 里累计的加减仓量需要结合第一笔除外的逻辑重新看待——实际初始手数可能不是净仓面板上显示的值。外汇与贵金属杠杆高,这类反手仓的回测偏差可能直接放大实盘风险,建议开 MT5 用策略测试器跑一段 EURUSD 历史验证累加结果。 别把反转标记当普通加仓 DEAL_ENTRY_INOUT 和普通 DEAL_ENTRY_IN 的累计规则不同,直接 sum_volume 会漏算反手前的对冲部分,复制代码后先把这一支单独打日志看一眼。

MQL5 / C++
class="type">class="kw">double CurrentPositionInitialVolume()
  {
   class="type">int         total      =class="num">0;          class=class="str">"cmt">// 所选历史列表的交易总数
   class="type">ulong       ticket     =class="num">0;          class=class="str">"cmt">// 交易的订单编号
   ENUM_DEAL_ENTRY deal_entry =WRONG_VALUE; class=class="str">"cmt">// 仓位修改方法
   class="type">bool        inout      =false;      class=class="str">"cmt">// 仓位反转标志
   class="type">class="kw">double      sum_volume =class="num">0.0;        class=class="str">"cmt">// 所有交易累计交易量的计数器,第一笔交易除外
   class="type">class="kw">double      deal_volume =class="num">0.0;       class=class="str">"cmt">// 交易的交易量
   class="type">class="kw">string      deal_symbol ="";        class=class="str">"cmt">// 交易的交易品种 
   class="type">class="kw">datetime    deal_time   =NULL;      class=class="str">"cmt">// 交易执行时间
class=class="str">"cmt">//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
     {
      class=class="str">"cmt">//--- 取得获得列表中的交易数量
      total=HistoryDealsTotal();
      class=class="str">"cmt">//--- 在获得的列表中从最后一笔交易到第一笔交易迭代所有交易
      for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
        {
         class=class="str">"cmt">//--- 如果根据仓位得到了订单编号,那么...
         if((ticket=HistoryDealGetTicket(i))>class="num">0)
           {
            class=class="str">"cmt">//--- 取得交易的交易量
            deal_volume=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            class=class="str">"cmt">//--- 取得仓位修改方法
            deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY);
            class=class="str">"cmt">//--- 取得交易执行时间
            deal_time=(class="type">class="kw">datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
            class=class="str">"cmt">//--- 取得交易的交易品种
            deal_symbol=HistoryDealGetString(ticket,DEAL_SYMBOL);
            class=class="str">"cmt">//--- 当仓位执行时间小于或者等于仓位开启时间,退出循环
            if(deal_time<=pos.time)
               break;
            class=class="str">"cmt">//--- 否则根据仓位交易品种计算交易的累积交易量,第一笔交易除外
            if(deal_symbol==_Symbol)
               sum_volume+=deal_volume;
           }
        }
     }
class=class="str">"cmt">//--- 如果仓位修改方法是反转
   if(deal_entry==DEAL_ENTRY_INOUT)
     {
      class=class="str">"cmt">//--- 如果仓位交易量曾经被增加/减少
      class=class="str">"cmt">//    比如交易数量大于1

常见问题

定义结构体把品种属性(点值、合约大小、止损水平等)和仓位字段打包,初始化时调用属性获取函数填充,后续直接读结构体成员即可,不用反复调系统接口。
先确认持仓方向和无新订单在途,再比对当前浮盈或价格距离是否过阈值,满足条件才用原方向发送新单;反手则先平掉现有仓再开反向单,避免锁仓混乱。
小布可在品种页直接标出你当前仓位的首单与末单成交价及价差,不用自己写循环抓历史,打开页面就能看。
按魔术号和时间筛选同一波成交,把开仓类成交手数累加,再扣掉中途平仓部分,剩下的就是当前仓位初始手数,用结构体存首末单号最稳。
多是没按开仓时间排序或把平仓单也算进去了;先过滤出同向未平成交,取时间最小那笔的成交价,别直接用持仓返回的第一个元素。