MQL5 Cookbook: 交易历史和取得仓位信息的函数库·进阶篇
「把仓位和品种属性塞进结构体」
写 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。
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 仍会计入历史成交,可能影响你的加仓次数判断。外汇与贵金属杠杆高,这类自动加仓逻辑在跳空行情中可能连续触发,请先在模拟盘验证。
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 新建脚本即可粘贴验证。
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">//+------------------------------------------------------------------+
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 会漏算反手前的对冲部分,复制代码后先把这一支单独打日志看一眼。
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