通过指定的幻数计算总持仓量的最佳方法·综合运用
(3/3)·多EA同品种并行时幻数隔离失效,全量历史回算拖垮测试速度,这篇收口
「用净持仓反推历史成交的切分点」
想在 MT5 里只回放某段持仓周期内的成交,常见做法是按时间窗口硬切,但更稳的思路是先算净持仓量,再倒着找历史成交里第一次凑齐该净量的位置。下面这段代码就是干这个的:从最新成交往旧成交遍历,BUY 加体积、SELL 减体积,Sum 用 NormalizeDouble 保留两位小数,直到 Sum 等于当前净持仓 CurrentVolume,就把那笔 ticket 的时间写进全局变量,并记录下标 FromI。 代码里 DEAL_TYPE_BUY 分支把成交量累加进 Sum,DEAL_TYPE_SELL 分支做减法,每步都 NormalizeDouble(Sum,2) 避免浮点尾巴。命中 CurrentVolume==Sum 后,用 HistoryDealGetInteger(ticket,DEAL_TIME) 取出成交时间,GlobalVariableSet 存成带品种后缀的变量,FromI 作为后续遍历起点。 第二段循环从 FromI 正向扫到 HistoryDealsTotal(),用 DEAL_SYMBOL 过滤品种、DEAL_MAGIC 过滤魔术码(pMagic==-1 表示不挑),再按 DEAL_TYPE_BUY 把体积叠进 pVolume。实盘里若 CurrentVolume 是 0.50,而某段历史恰好由 0.30 BUY + 0.20 BUY 构成,倒推会停在第二笔 0.20 BUY 处——这意味着你回放的起点可能不是直觉里的「第一笔开仓」,而是净量刚凑齐的那一笔,外汇与贵金属杠杆高,这种切分偏差会直接改变胜率统计。
class="type">class="kw">double Sum=class="num">0; class="type">int FromI=class="num">0; class="type">int FromTicket=class="num">0; for(class="type">int i=HistoryDealsTotal()-class="num">1;i>=class="num">0;i--) { class="type">class="kw">ulong ticket=HistoryDealGetTicket(i); if(ticket!=class="num">0) { class="kw">switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) { case DEAL_TYPE_BUY: Sum+=HistoryDealGetDouble(ticket,DEAL_VOLUME); Sum=NormalizeDouble(Sum,class="num">2); break; case DEAL_TYPE_SELL: Sum-=HistoryDealGetDouble(ticket,DEAL_VOLUME); Sum=NormalizeDouble(Sum,class="num">2); break; } if(CurrentVolume==Sum) { pLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME); GlobalVariableSet(Commom_gvp+pSymbol+"_HistStTm",pLoadHistoryFrom); FromI=i; break; } } } for(class="type">int i=FromI;i<HistoryDealsTotal();i++) { class="type">class="kw">ulong ticket=HistoryDealGetTicket(i); if(ticket!=class="num">0) { if(HistoryDealGetString(ticket,DEAL_SYMBOL)==pSymbol) { class="type">long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC); if(PosMagic==pMagic || pMagic==-class="num">1) { class="kw">switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) { case DEAL_TYPE_BUY: pVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
◍ 净持仓量的历史回算逻辑
这段实现的核心是:从已平仓历史里反推某个符号、某个 magic 下的当前净持仓体积。先调用 HistorySelect 把从 pLoadHistoryFrom 到 TimeCurrent 的成交记录拉进内存,若返回 false 直接退出,说明这次加载失败。 循环从 HistoryDealsTotal()-1 往下走到 0,用 HistoryDealGetTicket 取每笔 ticket。遇到 ticket==pLastTicket 就 break,这是避免重复计算上一次已经扫过的成交。 对符号匹配且 magic 相符(或 pMagic==-1 表示不限)的成交,按 DEAL_TYPE_BUY 加体积、DEAL_TYPE_SELL 减体积,累计进 pVolume。最后用 NormalizeDouble(pVolume,2) 把结果保留两位小数,赋给 aVolume 返回。 注意 DEAL_VOLUME 是 double 类型,直接加减会有浮点误差,所以末尾必须 NormalizeDouble 到 2 位;外汇与贵金属杠杆品种成交频繁,这类回算出错会直接误导仓位判断,属高风险操作环节。
pLastTicket=ticket;
break;
case DEAL_TYPE_SELL:
pVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
pLastTicket=ticket;
break;
}
}
}
}
pFirstStart=true;
}
if(!HistorySelect(pLoadHistoryFrom,TimeCurrent()))
{
class="kw">return(false);
}
for(class="type">int i=HistoryDealsTotal()-class="num">1;i>=class="num">0;i--)
{
class="type">class="kw">ulong ticket=HistoryDealGetTicket(i);
if(ticket!=class="num">0)
{
if(ticket==pLastTicket)
{
break;
}
if(HistoryDealGetString(ticket,DEAL_SYMBOL)==pSymbol)
{
class="type">long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
if(PosMagic==pMagic || pMagic==-class="num">1)
{
class="kw">switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
{
case DEAL_TYPE_BUY:
pVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
break;
case DEAL_TYPE_SELL:
pVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
break;
}
}
}
}
}
if(HistoryDealsTotal()>class="num">0)
{
pLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-class="num">1);
}
pVolume=NormalizeDouble(pVolume,class="num">2);
aVolume=pVolume;
class="kw">return(true);
}
class="type">class="kw">double PositionVolume::SymbolLots()
{
class="type">class="kw">double TmpLots=class="num">0;按品种与魔术码拆出净持仓手数
多品种多策略并行时,同一个 Symbol 下可能挂着不同 Magic 号的单子,直接读总持仓容易算重。上面这段把持仓遍历封装进 PositionVolume 类,用 Symbol+Magic 双键定位,能把某一组策略的净手数单独抠出来。 循环里先以 PositionsTotal() 为上限扫一遍,PositionGetSymbol(i) 命中目标品种后才取 POSITION_VOLUME。若是 POSITION_TYPE_SELL,手数乘 -1 转成负值,buy 为正、sell 为负,break 退出避免重复计数。 最后 NormalizeDouble(TmpLots,2) 把结果压到两位小数——MT5 多数品种手数精度就是 2 位,返回前不归一化可能在面板比对时差 0.01。实盘里你可以把 Symbol_1 / Magic_1 换成自己 EA 的参数,开 MT5 策略测试器挂两组魔术码验证 Vol11 与 Vol12 是否互不串号。外汇与贵金属杠杆高,净手数算错会直接放大敞口风险。
for(class="type">int i=class="num">0;i<PositionsTotal();i++) { if(PositionGetSymbol(i)==pSymbol) { TmpLots=PositionGetDouble(POSITION_VOLUME); if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) { TmpLots*=-class="num">1; } break; } } TmpLots=NormalizeDouble(TmpLots,class="num">2); class="kw">return(TmpLots); } PositionVolume PosVol11; PositionVolume PosVol12; PositionVolume PosVol21; PositionVolume PosVol22; PosVol11.Init(Symbol_1,Magic_1); PosVol12.Init(Symbol_1,Magic_2); PosVol21.Init(Symbol_2,Magic_1); PosVol22.Init(Symbol_2,Magic_2); class="type">class="kw">double Vol11; class="type">class="kw">double Vol12; class="type">class="kw">double Vol21; class="type">class="kw">double Vol22; PosVol11.GetVolume(Vol11); PosVol12.GetVolume(Vol12); PosVol21.GetVolume(Vol21); PosVol22.GetVolume(Vol22);
「四仓位同跑验证持仓量统计」
想确认前面那套持仓量统计函数是否靠谱,最直接的办法是让一个 EA 同时管四个仓位跑一遍。 我们用了 EURUSD 和 GBPUSD 两个品种,各自挂周期 14 与 21 的 RSI 指标,魔法码分别设 1 和 2;魔法码 1 的仓位下 0.1 手,魔法码 2 的仓位下 0.2 手。 EA 在每笔交易前后都调用统计函数算一次各魔法码对应的持仓量,并把结果写进变量。一旦计算返回异常,函数会直接弹消息报错,这样肉眼就能抓到统计逻辑在哪一步失准。 测试代码在 ePosVolTest.mq5 里,MT5 加载后切到这两个货币对,跑几天就能复现上面的四通道分仓统计。外汇与贵金属杠杆高,验证时先用模拟账户,别拿实盘赌函数边界。
◍ 把持仓量函数写进自己的EA里
持仓量计算类在实盘里的价值,不在于算得准,而在于算得省。启动时只加载最少交易历史,运行时只用最后几笔成交重算当前持仓,这种写法对 MT5 回测和实盘的资源占用都更友好,倾向让 EA 在长周期跑起来不卡。 附件里的 eposvoltest.mq5 体积 34.34 KB,但 2025 年 9 月多名用户反馈在新版编译器下出现 long 转 int 丢数据、OrderSend 返回值未检查、MQL5_TESTING 弃用等告警。代码最早写于 2010 年,直接编译可能有损,建议把 MQL5_TESTING 改成 MQL_TESTER 再上机。 下面这段卖单封装函数,展示了一次成型的下单+回报写法,复制进你的 EA 就能直接调: bool fOpSell(string aSymbol,double aVolume=0.1,int aSlippage=0,int aMagic=0,string aComment="",string aMessage="",bool aSound=false) { request.symbol=aSymbol; request.action=TRADE_ACTION_DEAL; request.type=ORDER_TYPE_SELL; request.volume=aVolume; request.price=SymbolInfoDouble(aSymbol,SYMBOL_BID); request.sl=0; request.tp=0; request.deviation=aSlippage; request.type_filling=ORDER_FILLING_FOK; request.comment=aComment; request.magic=aMagic; if(aMessage!="")Print(aMessage); if(aSound)PlaySound("expert"); OrderSend(request,result); if(result.retcode==TRADE_RETCODE_DONE) { Print("......幸运(#"+IntegerToString(result.order)+")"); if(aSound)PlaySound("ok"); return(1); } else { Print("......错误"+IntegerToString(result.retcode)+" - "+fTradeRetCode(result.retcode)); if(aSound)PlaySound("timeout"); return(-1); } } 逐行看:函数入参给了品种、手数默认0.1、滑点、魔术码、注释和声音开关;request 结构先填品种和 ACTION_DEAL,类型定为 SELL,价格抓当前 BID;sl/tp 暂置 0,填仓方式用 FOK;发单后看 retcode,成功打印订单号并返回 1,失败打印错误码并返回 -1。外汇和贵金属波动剧烈,这类封装只是执行层,仓位风险仍由你自己的逻辑控。 最后一句大实话:EA 里所有通用函数,衡量标准就两条——你调起来顺手,机器跑起来不浪费;这篇的持仓量算法和卖单封装,值得你今晚开 MT5 新建一个 mq5 亲手验一遍。
class="type">bool fOpSell(class="type">class="kw">string aSymbol,class="type">class="kw">double aVolume=class="num">0.1,class="type">int aSlippage=class="num">0,class="type">int aMagic=class="num">0,class="type">class="kw">string aComment="",class="type">class="kw">string aMessage="",class="type">bool aSound=false) { request.symbol=aSymbol; request.action=TRADE_ACTION_DEAL; request.type=ORDER_TYPE_SELL; request.volume=aVolume; request.price=SymbolInfoDouble(aSymbol,SYMBOL_BID); request.sl=class="num">0; request.tp=class="num">0; request.deviation=aSlippage; request.type_filling=ORDER_FILLING_FOK; request.comment=aComment; request.magic=aMagic; if(aMessage!="")Print(aMessage); if(aSound)PlaySound("expert"); OrderSend(request,result); if(result.retcode==TRADE_RETCODE_DONE) { Print("......幸运(#"+IntegerToString(result.order)+")"); if(aSound)PlaySound("ok"); class="kw">return(class="num">1); } else { Print("......错误"+IntegerToString(result.retcode)+" - "+fTradeRetCode(result.retcode)); if(aSound)PlaySound("timeout"); class="kw">return(-class="num">1); } }