通过指定的幻数计算总持仓量的最佳方法·进阶篇
(2/3)·同品种挂多个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 类,每对“品种-幻数”建一个实例,私有存状态、公有算量,避免全局变量污染和副本函数堆砌。
<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">// 保存持仓为 "零" 时的时间 { 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">// 仅选择需要的历史数据 } <span class="keyword">else </span> { <span class="functions">GlobalVariableSet</span>(Commom_gvp+sSymbol+<span class="class="type">class="kw">string">"_HistStTm"</span>,<span class="number">class="num">0</span>); } <span class="keyword">if</span>(!<span class="functions">HistorySelect</span>(sLoadHistoryFrom,<span class="functions">TimeCurrent</span>())) class=class="str">"cmt">// 加载必要的合约历史部分 { <span class="keyword">class="kw">return</span>(false); } <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) { <span class="keyword">if</span>(<span class="functions">PositionSelect</span>(aSymbol,<span class="number">class="num">1000</span>)) class=class="str">"cmt">// 仓位选择成功, 所以它存在 { <span class="keyword">class="kw">switch</span>(<span class="functions">PositionGetInteger</span>(<span class="keyword">POSITION_TYPE</span>)) class=class="str">"cmt">// 依据方向返回正或负 { <span class="keyword">case</span> <span class="keyword">POSITION_TYPE_BUY</span>: <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>)); <span class="keyword">break</span>; <span class="keyword">case</span> <span class="keyword">POSITION_TYPE_SELL</span>: <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>)); <span class="keyword">break</span>; } } <span class="keyword">else</span> { <span class="keyword">class="kw">return</span>(<span class="number">class="num">0</span>); } } <span class="keyword">class="type">class="kw">double</span> fSymbolLots(<span class="keyword">class="type">class="kw">string</span> aSymbol) { <span class="keyword">class="type">class="kw">double</span> TmpLots=<span class="number">class="num">0</span>; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>;i<<span class="functions">PositionsTotal</span>();i++) class=class="str">"cmt">// 遍历所有持仓 { <span class="keyword">if</span>(<span class="functions">PositionGetSymbol</span>(i)==aSymbol) class=class="str">"cmt">// 我们发现指定交易品种的持仓 { TmpLots=<span class="functions">PositionGetDouble</span>(<span class="keyword">POSITION_VOLUME</span>); <span class="keyword">if</span>(<span class="functions">PositionGetInteger</span>(<span class="keyword">POSITION_TYPE</span>)==<span class="keyword">POSITION_TYPE_SELL</span>) { TmpLots*=-<span class="number">class="num">1</span>; class=class="str">"cmt">// 标记依据持仓类型 } <span class="keyword">break</span>; }
「从成交历史反推当前净持仓」
这段逻辑干的事很直接:倒序扫一遍 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 与账户实际净仓是否一致。
} 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 占用可能瞬间翻倍。
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。 外汇与贵金属杠杆高,净持仓手数只是风险敞口的一面,实际浮亏还取决于点值与价格波动,这段代码本身不预示任何方向。
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 就能在日志里看到净持仓体积的变化。
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);