根据品种和 EA 的 ORDER_MAGIC 分析余额/净值图形·进阶篇
「从一张成交回执看掉期与盈利构成」
下面这条 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 手记录,能直接验证上述数值是否与你的券商点差环境一致。
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 把这段塞进你自己的回测脚本,跑长历史就能直接感觉到差距。
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 在周三凌晨误判。
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 里照此分支,先画全品种全魔法的净值,再可缩到单品种单魔法,历史交易抓取和绘图都跟着这两个哨兵值走。
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"、选「所有品种」时等于 "",就能验证整条链路通了。
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);