通过指定的幻数计算总持仓量的最佳方法·进阶篇
🧮

通过指定的幻数计算总持仓量的最佳方法·进阶篇

(2/3)·同品种挂多个EA时幻数混淆会让仓位判断失真,本文给出净仓归零时间点的定位法

实战向进阶 第 2/3 篇
把几个EA挂在同一品种却共用默认幻数,回测和实盘仓位统计会互相污染。很多人直接拉全量历史算净仓,EA卡到没法用。先定位净仓归零的时刻,才是多EA并存时的正路。

零仓位起点与净持仓量抓取

EA 启动时先查全局变量里是否存过“零仓位”时刻——也就是账户对某交易品种无任何敞口的时间点。用 GlobalVariableCheck() 探一下,有记录就从那个时间加载历史,没有就 GlobalVariableSet() 写 0 并拉全量历史,避免每次重启都重扫整段成交。 净持仓量建议直接用 fSymbolLots() 拿。PositionSelect() 返回 false 即当前零仓;返回 true 再用 PositionGetDouble(POSITION_VOLUME) 取量,PositionGetInteger(POSITION_TYPE) 判方向,多头回正、空头回负,归一保留两位小数。 另一种写法是遍历 PositionsTotal() 个持仓,PositionGetSymbol(i) 筛品种,再分别取量和方向累加。两种方式在 MT5 里实测等价,但前者少一层循环,品种持仓多时更轻。 抓到当前净量后,从 HistoryDealsTotal() 尾端往回扫,用 HistoryDealGetTicket() 取单证,DEAL_TYPE 与 DEAL_VOLUME 提类型和量,直到累计量等于当前净量。那个分界点之前的总仓为 0,时间写进全局变量,下次函数调用直接续扫。 静态变量 sLoadHistoryFrom、sLastTicket、sVolume 跨调用保留状态;但 MQL5 更干净的做法是包成 PositionVolume 类,每对“品种-幻数”建一个实例,私有存状态、公有算量,避免全局变量污染和副本函数堆砌。

MQL5 / C++
<span class="keyword">if</span>(<span class="functions">GlobalVariableCheck</span>(Commom_gvp+sSymbol+<span class="class="type">class="kw">string">"_HistStTm"</span>)) class=class="str">"cmt">// 保存持仓为 "零" 时的时间
&nbsp; {
&nbsp;&nbsp; pLoadHistoryFrom=(<span class="keyword">class="type">class="kw">datetime</span>)<span class="functions">GlobalVariableGet</span>(Commom_gvp+pSymbol+<span class="class="type">class="kw">string">"_HistStTm"</span>); class=class="str">"cmt">// 初始化时间设置
class=class="str">"cmt">// 仅选择需要的历史数据
&nbsp; }
<span class="keyword">else
</span> {
&nbsp;&nbsp; <span class="functions">GlobalVariableSet</span>(Commom_gvp+sSymbol+<span class="class="type">class="kw">string">"_HistStTm"</span>,<span class="number">class="num">0</span>);
&nbsp;}
<span class="keyword">if</span>(!<span class="functions">HistorySelect</span>(sLoadHistoryFrom,<span class="functions">TimeCurrent</span>())) class=class="str">"cmt">// 加载必要的合约历史部分
&nbsp; {
&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(false);
&nbsp;&nbsp;}
<span class="keyword">class="type">class="kw">double</span> CurrentVolume=fSymbolLots(pSymbol);
<span class="keyword">class="type">class="kw">double</span> fSymbolLots(<span class="keyword">class="type">class="kw">string</span> aSymbol)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">if</span>(<span class="functions">PositionSelect</span>(aSymbol,<span class="number">class="num">1000</span>)) class=class="str">"cmt">// 仓位选择成功, 所以它存在
&nbsp; &nbsp;&nbsp; {
&nbsp; &nbsp; &nbsp; <span class="keyword">class="kw">switch</span>(<span class="functions">PositionGetInteger</span>(<span class="keyword">POSITION_TYPE</span>)) class=class="str">"cmt">// 依据方向返回正或负
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; <span class="keyword">case</span> <span class="keyword">POSITION_TYPE_BUY</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="functions">NormalizeDouble</span>(<span class="functions">PositionGetDouble</span>(<span class="keyword">POSITION_VOLUME</span>),<span class="number">class="num">2</span>));
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">case</span> <span class="keyword">POSITION_TYPE_SELL</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="functions">NormalizeDouble</span>(-<span class="functions">PositionGetDouble</span>(<span class="keyword">POSITION_VOLUME</span>),<span class="number">class="num">2</span>));
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;}
<span class="keyword">class="type">class="kw">double</span> fSymbolLots(<span class="keyword">class="type">class="kw">string</span> aSymbol)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> TmpLots=<span class="number">class="num">0</span>;
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>;i&lt;<span class="functions">PositionsTotal</span>();i++) class=class="str">"cmt">// 遍历所有持仓
&nbsp; &nbsp;&nbsp; {
&nbsp; &nbsp; &nbsp; <span class="keyword">if</span>(<span class="functions">PositionGetSymbol</span>(i)==aSymbol) class=class="str">"cmt">// 我们发现指定交易品种的持仓
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; TmpLots=<span class="functions">PositionGetDouble</span>(<span class="keyword">POSITION_VOLUME</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(<span class="functions">PositionGetInteger</span>(<span class="keyword">POSITION_TYPE</span>)==<span class="keyword">POSITION_TYPE_SELL</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TmpLots*=-<span class="number">class="num">1</span>; class=class="str">"cmt">// 标记依据持仓类型&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; <span class="keyword">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}

「从成交历史反推当前净持仓」

这段逻辑干的事很直接:倒序扫一遍 HistoryDealsTotal() 返回的成交记录,用买加卖减的方式把账户在某品种上的净持仓算出来。代码里 Sum 每步都过 NormalizeDouble(...,2),说明手数精度被强制压到两位小数,MT5 实盘里多数品种的最小交易量精度也就是 0.01,这里留两位不会丢信息。 第一段 for 循环从后往前跑,遇到 DEAL_TYPE_BUY 就 Sum+=成交量、DEAL_TYPE_SELL 就 Sum-=成交量。当 Sum 回零的那一刻,意味着上一轮持仓已经完全平仓,此时把该时刻 DEAL_TIME 写进全局变量 Commom_gvp+aSymbol+"_HistStTm",并记录索引 FromI,相当于给历史切了一刀——只统计这之后的净头寸。 第二段从 FromI 正序扫到结尾,额外加了品种过滤(DEAL_SYMBOL==aSymbol)和 Magic 过滤(PosMagic==aMagic 或 aMagic==-1 表示不挑标识)。同样买加卖减累到静态变量 sVolume,并把最后一次成交单号存进 sLastTicket。 外汇与贵金属杠杆高、滑点随机,这套统计只反映历史成交层面的净仓,不替你预判方向;跑之前建议在策略测试器里用 EURUSD 近三个月 tick 数据验证 sVolume 与账户实际净仓是否一致。

MQL5 / C++
  }
  TmpLots=NormalizeDouble(TmpLots,class="num">2);
  class="kw">return(TmpLots);
  }
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=class="str">"cmt">// 从后至前遍历所有合约
  {
   class="type">class="kw">ulong ticket=HistoryDealGetTicket(i); class=class="str">"cmt">// 得到合约的单号
  if(ticket!=class="num">0)
    {
      class="kw">switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) class=class="str">"cmt">// 依据合约方向增减持仓量
        {
         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) class=class="str">"cmt">// 所有合约已扫描
        {
         sLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME); class=class="str">"cmt">// 保存持仓为 "零" 时的时间
         GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",sLoadHistoryFrom);
         FromI=i; class=class="str">"cmt">// 保存索引
         break;
        }
    }
  }
class="kw">static class="type">class="kw">double sVolume=class="num">0;
class="kw">static class="type">class="kw">ulong sLastTicket=class="num">0;
for(class="type">int i=FromI;i<HistoryDealsTotal();i++) class=class="str">"cmt">// 从第一个合约至结尾
  {
   class="type">class="kw">ulong ticket=HistoryDealGetTicket(i);   class=class="str">"cmt">// 取合约的单号
   if(ticket!=class="num">0)
    {
      if(HistoryDealGetString(ticket,DEAL_SYMBOL)==aSymbol) class=class="str">"cmt">// 指定交易品种
       {
         class="type">long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
         if(PosMagic==aMagic || aMagic==-class="num">1) class=class="str">"cmt">// 指定标识号
          {
           class="kw">switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) class=class="str">"cmt">// 增减持仓量
class=class="str">"cmt">// 依据合约类型
            {
             case DEAL_TYPE_BUY:
                sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                sLastTicket=ticket;
                break;
             case DEAL_TYPE_SELL:
                sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                sLastTicket=ticket;
                break;
            }
          }
       }
    }
  }

◍ 用历史成交回推实时净持仓

EA 启动阶段若不做历史成交回放,实时算出的净持仓量往往会漏掉本次 MT5 会话之前已平掉的仓位,导致加仓逻辑误判。下面这段做法是在首次运行时从全局变量读取历史起点,再用 HistorySelect 拉取区间成交,从后往前遍历 DEAL_TYPE 累加或扣减体积。 核心思路是:先 fSymbolLots 拿当前持仓总量,再反向扫 HistoryDealsTotal 里的每笔 deal,用 DEAL_TYPE_BUY 加、DEAL_TYPE_SELL 减,直到某次累计 Sum 回到 0,那个点之前的历史就可以不再管。这样 sVolume 静态变量里留下的就是「本会话有效」的净仓。 注意 HistorySelect 失败直接 return(false),下一 tick 会重试,不会因为一次历史读取超时就把 EA 卡死。外汇与贵金属杠杆高,这类净仓回推若用于自动加仓,务必先在策略测试器用真实点差回测,实盘可能放大滑点风险。 别把全局变量当永生存储。Commom_gvp+aSymbol+"_HistStTm" 这个键值如果在终端重启后被手动清掉,sLoadHistoryFrom 会回到 0,等于重头拉全部历史,CPU 占用可能瞬间翻倍。

MQL5 / C++
if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) class=class="str">"cmt">// 请求至当前时间的合约历史
  {
   class="kw">return(false);
  }
for(class="type">int i=HistoryDealsTotal()-class="num">1;i>=class="num">0;i--) class=class="str">"cmt">// 从末至前循环
  {
   class="type">class="kw">ulong ticket=HistoryDealGetTicket(i); class=class="str">"cmt">// 取单号
   if(ticket!=class="num">0)
     {
      if(ticket==sLastTicket) class=class="str">"cmt">// 我们发现已计算的合约, 保存单号并终止循环
        {
         sLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-class="num">1);
         break;
        }
      class="kw">switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) class=class="str">"cmt">// 依据合约类型增减持仓量
        {
         case DEAL_TYPE_BUY:
            sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            break;
         case DEAL_TYPE_SELL:
            sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            break;
        }
     }
  }
class="type">bool fGetPositionVolume(class="type">class="kw">string aSymbol,class="type">int aMagic,class="type">class="kw">double aVolume)
  {
   class="kw">static class="type">bool FirstStart=false;
   class="kw">static class="type">class="kw">double sVolume=class="num">0;
   class="kw">static class="type">class="kw">ulong sLastTicket=class="num">0;
   class="kw">static class="type">class="kw">datetime sLoadHistoryFrom=class="num">0;
   class=class="str">"cmt">// 当 EA 开始时,函数首次执行
   if(!FirstStart)
     {
      if(GlobalVariableCheck(Commom_gvp+aSymbol+"_HistStTm"))
        {
         sLoadHistoryFrom=(class="type">class="kw">datetime)GlobalVariableGet(Commom_gvp+aSymbol+"_HistStTm");
        }
      else
        {
         GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",class="num">0);
        }
      if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) class=class="str">"cmt">// 如果不成功返回, 我们将在下一个即时价时重复
        {
         class="kw">return(false);
        }
      class="type">class="kw">double CurrentVolume=fSymbolLots(aSymbol); class=class="str">"cmt">// 总计持仓量
      class="type">class="kw">double Sum=class="num">0;
      class="type">int FromI=class="num">0;
      class="type">int FromTicket=class="num">0;
      class=class="str">"cmt">// 搜索最后的持仓为零的时间
      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);

按成交历史反推净持仓量

这段逻辑干的事很直接:从已缓存的起始索引 FromI 开始,遍历 HistoryDealsTotal() 返回的每一张成交单,只挑出符号等于 aSymbol 且魔术码匹配 aMagic(或 aMagic==-1 表示不区分)的纪录。 买成交把 DEAL_VOLUME 累加到 sVolume,卖成交则减去对应手数,同时把 ticket 记进 sLastTicket——也就是说,循环跑完,sVolume 就是该品种在该魔术码下的净持仓方向手数,sLastTicket 指向最近一次改变净仓的单子。 前面那段 switch 其实是在倒推「当前持仓量 CurrentVolume 对应的最早历史边界」:遇到 DEAL_TYPE_BUY 就 Sum+=成交量,DEAL_TYPE_SELL 就 Sum-=成交量,每次 NormalizeDouble(Sum,2) 保留两位小数;一旦 Sum 回补到等于 CurrentVolume,就把该单的 DEAL_TIME 写进全局变量 Commom_gvp+aSymbol+"_HistStTm",并 break 定下 FromI。 外汇与贵金属杠杆高,净持仓手数只是风险敞口的一面,实际浮亏还取决于点值与价格波动,这段代码本身不预示任何方向。

MQL5 / C++
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)
         {
          sLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME);
          GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",sLoadHistoryFrom);
          FromI=i;
          break;
         }
      }
   }
class=class="str">"cmt">// 计算指定的识别号及交易品种的持仓量
   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)==aSymbol)
            {
             class="type">long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
             if(PosMagic==aMagic || aMagic==-class="num">1)
               {
                class="kw">switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
                  {
                   case DEAL_TYPE_BUY:
                      sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                      sLastTicket=ticket;
                      break;
                   case DEAL_TYPE_SELL:
                      sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                      sLastTicket=ticket;
                      break;
                  }
               }
            }
         }
      }
   FirstStart=true;
class=class="str">"cmt">// 重计算合约的持仓量 (指定的交易品种及识别号)
class=class="str">"cmt">// , 零持仓时间之后
   if(!HistorySelect(sLoadHistoryFrom,TimeCurrent()))
      {

「用历史成交回推当前净持仓」

这段逻辑干一件事:从 MT5 历史成交里倒推某个符号的净持仓体积。循环从 HistoryDealsTotal()-1 往下扫,遇到上一次记录的 ticket 就停,说明已经算到上次位置,避免重复累加。 买成交 DEAL_TYPE_BUY 把体积加进 sVolume,卖成交 DEAL_TYPE_SELL 减去对应体积,最终 NormalizeDouble(sVolume,2) 保留两位小数写回 aVolume。外汇与贵金属杠杆高,历史回推只反映已平仓与未平净敞口,不预示后续波动方向。 类 PositionVolume 把符号、Magic、上次 ticket 封成私有成员,Init 里把 pFirstStart 置 false、pVolume 清 0;首次跑会读全局变量 Commom_gvp+Symbol+"_HistStTm" 决定从哪里开始选历史,没有就建一个写 0。 GetVolume 先判 pFirstStart,未启动就 HistorySelect 拉区间成交,失败直接 return(false)。你开 MT5 把下面代码贴进 EA,改 pMagic 和 pSymbol 就能在日志里看到净持仓体积的变化。

MQL5 / C++
  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==sLastTicket)
        {
         sLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-class="num">1);
         break;
        }
      class="kw">switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
        {
         case DEAL_TYPE_BUY:
           sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
           break;
         case DEAL_TYPE_SELL:
           sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
           break;
        }
     }
  }
  aVolume=NormalizeDouble(sVolume,class="num">2);;
  class="kw">return(true);
 }
class PositionVolume
  {
class="kw">private:
   class="type">class="kw">string            pSymbol;
   class="type">int               pMagic;
   class="type">bool              pFirstStart;
   class="type">class="kw">ulong             pLastTicket;
   class="type">class="kw">double            pVolume;
   class="type">class="kw">datetime          pLoadHistoryFrom;
   class="type">class="kw">double            SymbolLots();
class="kw">public:
   class="type">void Init(class="type">class="kw">string aSymbol,class="type">int aMagic)
    {
     pSymbol=aSymbol;
     pMagic=aMagic;
     pFirstStart=false;
     pLastTicket=class="num">0;
     pVolume=class="num">0;
    }
   class="type">bool              GetVolume(class="type">class="kw">double &aVolume);
   };
class="type">bool PositionVolume::GetVolume(class="type">class="kw">double &aVolume)
  {
   if(!pFirstStart)
    {
     if(GlobalVariableCheck(Commom_gvp+pSymbol+"_HistStTm"))
       {
        pLoadHistoryFrom=(class="type">class="kw">datetime)GlobalVariableGet(Commom_gvp+pSymbol+"_HistStTm");
       }
     else
       {
        GlobalVariableSet(Commom_gvp+pSymbol+"_HistStTm",class="num">0);
       }
     if(!HistorySelect(pLoadHistoryFrom,TimeCurrent()))
       {
        class="kw">return(false);
       }
     class="type">class="kw">double CurrentVolume=fSymbolLots(pSymbol);
让小布替你跑这套净仓扫描
把历史遍历和全局变量维护的重复劳动交给小布盯盘,你专注决策;对应品种的幻数分组净仓概览已内置在品种页,打开就能看。

常见问题

账户历史可能极长,每次全量遍历会拖慢EA尤其在测试优化阶段;应先找净仓为零的最后时刻从该点往后算。
可以,小布盯盘的品种页内置了按幻数拆分的持仓诊断,不需要自己写遍历逻辑就能看清各组EA的敞口。
EA被分离时内部变量销毁,下次启动又要重算;用客户端全局变量跨EA共享才能避免重复开销。
给每个EA设独立幻数并仅统计自身幻数下的买卖量,才能让其建仓逻辑与回测一致、各自风控独立。