通过指定的幻数计算总持仓量的最佳方法·综合运用
🧮

通过指定的幻数计算总持仓量的最佳方法·综合运用

(3/3)·多EA同品种并行时幻数隔离失效,全量历史回算拖垮测试速度,这篇收口

含代码示例偏理论 第 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 处——这意味着你回放的起点可能不是直觉里的「第一笔开仓」,而是净量刚凑齐的那一笔,外汇与贵金属杠杆高,这种切分偏差会直接改变胜率统计。

MQL5 / C++
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 位;外汇与贵金属杠杆品种成交频繁,这类回算出错会直接误导仓位判断,属高风险操作环节。

MQL5 / C++
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 是否互不串号。外汇与贵金属杠杆高,净手数算错会直接放大敞口风险。

MQL5 / C++
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 亲手验一遍。

MQL5 / C++
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);
    }
  }
让小布替你跑这套隔离统计
多EA同品种下的幻数净持仓诊断,小布盯盘已内置对应品种页的AIGC视图,打开即可看各幻数分组仓位,不必自己写HistorySelect遍历。

常见问题

共用幻数会让各EA把彼此仓位算进自己的建仓条件,策略互相干扰,回测与实盘一致性被破坏,概率上更倾向于信号失真。
账户交易多时历史记录体量很大,每次初始化都全量遍历,EA在测试和优化中速度显著下降,实战中也可能延迟触发。
可以,小布盯盘对应品种页的AIGC视图支持按幻数分组呈现净持仓,省去自己维护全局变量和截断时间点的代码。
EA局部变量在分离时被销毁,存全局变量能与同品种其他EA共享截断点,重启后只需载入最少必需历史,外汇贵金属高风险下也减少重复计算。
基础篇铺垫幻数与持仓概念,实战篇给单EA统计代码,本篇综合解决多EA并行与历史截断,关于单EA写法见《通过指定的幻数计算总持仓量的最佳方法·实战篇》。