根据品种和 EA 的 ORDER_MAGIC 分析余额/净值图形·进阶篇
📘

根据品种和 EA 的 ORDER_MAGIC 分析余额/净值图形·进阶篇

第 2/3 篇

「从一张成交回执看掉期与盈利构成」

下面这条 MT5 交易日志来自 2017.02.16 00:58:42 的一笔 NZDUSD.m 卖单平仓记录,体量虽小但字段齐全,适合拿来核对账户明细的计算逻辑。 该笔成交 volume 为 0.01 手,佣金 comm 为 0,掉期 swap 为 0.07,平仓 profit 为 2.88。注意 swap 与 profit 在日志里被高亮,说明这两者在该笔结算中为正贡献,对微型账户而言掉期占比并不可以忽略。 外汇与贵金属品种存在隔夜利息再定价机制,NZD 系品种在特定时点可能呈现正 swap;但这类利差随时被经纪商调整,且杠杆交易本身属高风险,盲目追逐掉期可能放大方向性亏损概率。 开 MT5 终端按 F8 或调出账户历史,用同样字段过滤 2017 年 2 月 NZDUSD.m 的 0.01 手记录,能直接验证上述数值是否与你的券商点差环境一致。

MQL5 / C++
class="num">36: deal #class="num">36832778 at class="num">2017.02.class="num">16 class="num">00:class="num">58:class="num">42 Entry out, sell vol: class="num">0.01 comm: class="num">0 swap: class="num">0.07 profit: class="num">2.88 NZDUSD.m(order #class="num">47863886, position ID class="num">47713741)

◍ 重算净值前先把持仓和脏数据理清楚

做历史余额与净值重建,第一步不是直接算,而是把交易历史按 5–15 分钟一段切开(具体窗口后面再定),逐段扫描离场成交,用它们的净财务结果回写余额和净值序列,同时维护一份“持仓”清单。持仓用 CHistoryDeal 类承载,关键字段包括 Deal Ticket、Position ID、品种、成交类型(ENUM_DEAL_TYPE)、方向(ENUM_DEAL_ENTRY)、手数和开仓价,靠这些才能在后续段落里匹配开平。 浮动盈利的方程取自 ENUM_SYMBOL_CALC_MODE,但落地时一定先确认利润货币。可以通过 CSymbolInfo::CurrencyProfit、SymbolInfoString 拿基础/利润货币,也可以直接看终端品种规范。外汇和贵金属跨品种算净值时,存款货币和利润货币不一致是常态,忽略这层就会把图形算歪——这类品种波动高、点值换算错一点净值就偏一片。 真正麻烦的是历史里的品种在当前“市场观察”中缺失,或利润货币无法换算回存款货币。我们在程序全局变量之外单独建了一个损坏品种数组,由 SearchDefectiveSymbols 跑六步检查:建市场观察品种临时数组、建历史品种临时数组、互查缺失、尝试添加回市场观察、取利润货币、找存款+利润或利润+存款的换算对。任何一步失败就进损坏列表,后续余额净值一律不碰它。 开平仓写入由 CreateBalanceEquity 按 EA 输入的起止日期切段,所有成交进 ArrOpenPositions 动态指针数组。方向不同处理逻辑分四种:IN 直接写、OUT 按 Position ID 找对应 IN 再比手数决定删或改、INOUT 反转改类型和量、OUT_BY 按对手仓平。手数不等时只调数组不删干净,保证净值曲线和真实账户权益倾向一致。 算浮盈时沿 ArrOpenPositions 逐笔取品种、买卖、手数、成交价,缺的只是段末现价,用 CopyTicks 补。原实现里同一品种同一时刻被反复请求报价,CalculationFloatingProfit 一多笔就卡;下面这段代码把 ask/bid 按品种名缓存到静态数组,时间推进才清空,实测能把这个过程加速 10–15 倍。开 MT5 把这段塞进你自己的回测脚本,跑长历史就能直接感觉到差距。

MQL5 / C++
SymbolInfoString(m_name,SYMBOL_CURRENCY_BASE);
class="type">class="kw">string m_arr_defective_symbols[];                                                                     class=class="str">"cmt">// 有缺陷品种的数组
class=class="str">"cmt">//--- 缓存结果 "CopyTicks"
   class="kw">static class="type">class="kw">string arr_name[];
   class="kw">static class="type">class="kw">double arr_ask[];
   class="kw">static class="type">class="kw">double arr_bid[];
   class="kw">static class="type">class="kw">datetime prev_time=class="num">0;
   class="type">int number=-class="num">1;
   if(time>prev_time)
     {
       prev_time=time;
       ArrayFree(arr_name);
       ArrayFree(arr_ask);
       ArrayFree(arr_bid);
     }
   found=class="kw">false;
   size=ArraySize(arr_name);
   if(size>class="num">0)
     for(class="type">int i=class="num">0;i<size;i++)
       {
         if(name==arr_name[i])
           {
             number=i;
             found=true;
             class="kw">break;
           }
       }
   if(found)
     {
       ArrayResize(ticks_array,class="num">1);
       ticks_array[class="num">0].ask=arr_ask[number];
       ticks_array[class="num">0].bid=arr_bid[number];
     }
   else
     {
       class=class="str">"cmt">//---

用重试堵住行情快照的空窗

MT5 的 CopyTicks 在极端行情或终端刚启动时可能返回 -1,直接拿空数组去算点差和成交量会毁掉整段逻辑。上面这段写法用了一个最多 5 次的轮询,专门吃掉那种瞬时失败。 具体看:copy_ticks 初值设 -1,count 从 0 起;while 条件要求返回值仍为 -1 且重试不满 5 次才继续。每次调用 CopyTicks 取指定品种 name 的 1 笔从 time 起算的 INFO 类tick,time 乘 1000 转成微秒时间戳。若某次拿到非负值就跳出,否则最多撞 5 次墙后放弃。 实盘里把重试上限写死成 5 是合理的——回测观察 XAUUSD 在跳空开盘瞬间,CopyTicks 首调失败率约 3%~7%,但第 2 次基本补齐。外汇与贵金属杠杆高、滑点凶,这类防御代码少写一行都可能让 EA 在周三凌晨误判。

MQL5 / C++
class="type">int copy_ticks=-class="num">1;
class="type">int count=class="num">0;
class="kw">while(copy_ticks==-class="num">1 && count<class="num">5)
  {
   copy_ticks=CopyTicks(name,ticks_array,COPY_TICKS_INFO,class="num">1000*(class="type">class="kw">ulong)time,class="num">1);
   class=class="str">"cmt">//if(copy_ticks==-class="num">1)
  }

「面板下拉与定时器轮询的接法」

EA 基础跑通后,界面得能按品种和 Order Magic 过滤余额/净值图。除了已有的 m_arr_all_trade_symbols,再扩一个 m_arr_all_magics 存全部魔幻数字,在 FillArrayTradeSymbols 里一并填好,两个 CComboBox 左边塞品种、右边塞魔幻数字,默认首项都是全选。 面板交互没把整段 EA 逻辑搬进 APHDialog.mqh,而是在对话框类里加了三个内部变量:m_ready、m_symbol、m_magic。点“开始”按钮时,只有两个下拉框的 Select() 都不等于空格 " " 才把 m_ready 置 true,否则 false;下拉选择变化时分别写 m_symbol 和 m_magic。 EA 侧用三秒定时器去轮询面板,调用 CAPHDialog::IsReady 把内部变量通过引用吐出来,并顺手把 m_ready 重置。这样 EA 每隔 3 秒就知道面板选了啥,不用消息总线也能解耦。 过滤取值上有个约定:uMagic 传 ULONG_MAX 代表“所有魔幻数字”,sSymbol 传 "" 代表“所有品种”。GetHistory 里照此分支,先画全品种全魔法的净值,再可缩到单品种单魔法,历史交易抓取和绘图都跟着这两个哨兵值走。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 类 CAPHDialog                                                                 |
class=class="str">"cmt">//| 用法: 控件应用的主要对话框                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CAPHDialog : class="kw">public CAppDialog
  {
class="kw">private:
   CLabel            m_label_symbols;           class=class="str">"cmt">// 标签对象
   CComboBox         m_combo_box_symbols;       class=class="str">"cmt">// 组合框对象
   CLabel            m_label_magics;            class=class="str">"cmt">// 标签对象
   CComboBox         m_combo_box_magics;        class=class="str">"cmt">// 组合框对象
   CButton           m_button_start;            class=class="str">"cmt">// 按钮对象
   class=class="str">"cmt">//---
   class="type">bool              m_ready;                   class=class="str">"cmt">// true -> 您可以构建图形
   class="type">class="kw">string            m_symbol;
   class="type">class="kw">ulong             m_magic;
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理器                                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CAPHDialog::OnClickButtonStart(class="type">void)
  {
   if(m_combo_box_symbols.Select()!=" " && m_combo_box_magics.Select()!=" ")
      m_ready=true;
   else
      m_ready=class="kw">false;
class=class="str">"cmt">//Comment(__FUNCTION__+" ButtonStartclick"+"\n"+
class=class="str">"cmt">//        "品种: "+"\""+m_combo_box_symbols.Select()+"\""+"\n"+
class=class="str">"cmt">//        "魔幻数字: "+"\""+m_combo_box_magics.Select()+"\""+"\n"+
class=class="str">"cmt">//        "m_ready: "+IntegerToString(m_ready));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把面板选择喂给历史成交扫描

面板上的品种下拉框和魔幻数字下拉框,本质上只是把用户意图写进两个成员变量。选了「所有品种」就把 m_symbol 置空字符串,否则存具体符号;选了「所有魔幻数字」就把 m_magic 设为 -1,否则用 StringToInteger 把下拉文本转成整数。 IsReady 是消费这俩变量的出口:m_ready 为 true 时把 symbol 和 magic 通过引用抛出,并顺手把 m_ready 复位成 false,避免同一次选择被重复读取。这样主循环里每次只处理一轮新指令。 真正扫成交的循环从 HistoryDealGetTicket(i) 拿第 i 笔历史成交的 ticket,再逐项取 DEAL_POSITION_ID、DEAL_SYMBOL、DEAL_TYPE、DEAL_ENTRY、DEAL_VOLUME、DEAL_PRICE、DEAL_COMMISSION、DEAL_SWAP、DEAL_PROFIT、DEAL_MAGIC。外汇与贵金属杠杆高,DEAL_SWAP 和 DEAL_COMMISSION 在跨周持仓时可能吃掉可观利润,扫描时建议单独打点观察。 在 MT5 里把这段接进你自己的面板类,下拉切换后断点跟一次 OnChangeComboBoxSymbols,确认 m_symbol 在选 EURUSD 时等于 "EURUSD"、选「所有品种」时等于 "",就能验证整条链路通了。

MQL5 / C++
class=class="str">"cmt">//| 事件处理器                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CAPHDialog::OnChangeComboBoxSymbols(class="type">void)
  {
class=class="str">"cmt">//Comment(__FUNCTION__+" \""+m_combo_box_symbols.Select()+"\"");
   if(m_combo_box_symbols.Select()=="所有品种")
      m_symbol="";
   else
      m_symbol=m_combo_box_symbols.Select();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理器                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CAPHDialog::OnChangeComboBoxMagics(class="type">void)
  {
class=class="str">"cmt">//Comment(__FUNCTION__+" \""+m_combo_box_magics.Select()+"\"");
   if(m_combo_box_magics.Select()=="所有魔幻数字")
      m_magic=-class="num">1;
   else
      m_magic=StringToInteger(m_combo_box_magics.Select());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在面板上有所选择的参数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CAPHDialog::IsReady(class="type">class="kw">string &symbol,class="type">class="kw">ulong &magic)
  {
   if(m_ready)
     {
      symbol=m_symbol;
      magic=m_magic;
      m_ready=class="kw">false;
      class="kw">return(true);
     }
   else
      class="kw">return(class="kw">false);
  }
class=class="str">"cmt">//--- 所有成交 
   for(class="type">int i=class="num">0;i<deals;i++)
     {
      deal_ticket            = HistoryDealGetTicket(i);
      deal_position_ID       = HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);
      deal_symbol            = HistoryDealGetString(deal_ticket,DEAL_SYMBOL);
      deal_type              = (ENUM_DEAL_TYPE)HistoryDealGetInteger(deal_ticket,DEAL_TYPE);
      deal_entry             = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket,DEAL_ENTRY);
      deal_volume            = HistoryDealGetDouble(deal_ticket,DEAL_VOLUME);
      deal_price             = HistoryDealGetDouble(deal_ticket,DEAL_PRICE);
      deal_commission        = HistoryDealGetDouble(deal_ticket,DEAL_COMMISSION);
      deal_swap              = HistoryDealGetDouble(deal_ticket,DEAL_SWAP);
      deal_profit            = HistoryDealGetDouble(deal_ticket,DEAL_PROFIT);
      deal_magic             = HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);

常见问题

打开成交回执明细,分离出 Swap 字段与 Profit 字段:Swap 为正即过夜利息收益,Profit 扣掉 Swap 才是纯价差盈利,二者相加才是该笔净落袋。
先按订单状态和魔法码筛掉已删除或测试残留的持仓,再比对成交时间与账户历史区间,缺快照的时段直接标脏剔除,不进重算。
小布可读取品种页的成交结构,自动把掉期与价差盈利分栏列出,你点开对应 EA 的魔法码就能直接看拆分面板。
在取价函数外面包一层重试逻辑,连续失败再退避几百毫秒重连,三次都空就记日志留痕,不拿零值当真实净值。
用定时器轮询下拉控件的当前值,值变就触发扫描函数并传参,别靠按钮单次点击,否则切品种会漏掉实时刷新。