在 MetaTrader 5 中交易的可视评估和调整·进阶篇
📊

在 MetaTrader 5 中交易的可视评估和调整·进阶篇

(2/3)· 当多个 EA 和手动单混跑同一账户,怎样一眼看穿止损挂得合不合理

进阶 第 2/3 篇

账户跑了一堆 EA 还夹着手动单,想回头看某笔止损是不是挂歪了,终端自带报告只能静态翻。很多人就这么忍了,从没试过把成交导出来在测试器里动态重放。

「把成交历史捞进数组的底层写法」

在 MT5 里做回测或实盘统计,第一步往往是把账户成交历史完整搬进自定义结构体数组。下面这段枚举和全局变量定义,决定了后面排序与过滤的维度:按止盈价、品种、注释、外部系统 ID,乃至测试器内的成交票号与持仓 ID 都能作为比较键。 输入参数给了三个钩子:InpTestedSymbol 默认空串代表不限定品种;InpTestedMagic 默认 -1 表示不过滤魔术码;InpShowDataInLog 设为 false 时静默运行,避免日志被刷屏。全局变量 ExtArrayDeals[] 就是承载所有成交的容器。 SaveDealsToArray 函数先用 HistorySelect(0, TimeCurrent()) 拉取从账户开立到当下的全部历史,若返回 false 直接打印错误码并退出。HistoryDealsTotal() 拿到总数后逐条遍历,用 HistoryDealGetTicket(i) 取票号,过滤掉非买卖与非余额的成交类型,只保留 DEAL_TYPE_BUY、DEAL_TYPE_SELL 和 DEAL_TYPE_BALANCE。 保留下来的成交,其 ticket、type、order、entry 等字段被写进 SDeal 结构。你在 MT5 里按此框架改 InpTestedMagic 为一个具体数值,就能只抽取某套 EA 的成交,验证自己的仓位统计逻辑。外汇与贵金属杠杆高,历史统计仅反映过去,样本偏差可能导致实盘概率偏移。

MQL5 / C++
   SORT_MODE_DEAL_TP,                class=class="str">"cmt">// Mode of comparing/sorting by Take Profit level
   SORT_MODE_DEAL_SYMBOL,             class=class="str">"cmt">// Mode of comparing/sorting by a name of a traded symbol
   SORT_MODE_DEAL_COMMENT,            class=class="str">"cmt">// Mode of comparing/sorting by a deal comment
   SORT_MODE_DEAL_EXTERNAL_ID,        class=class="str">"cmt">// Mode of comparing/sorting by a deal ID in an external trading system
   SORT_MODE_DEAL_TICKET_TESTER,      class=class="str">"cmt">// Mode of comparing/sorting by a deal ticket in the tester
   SORT_MODE_DEAL_POS_ID_TESTER,      class=class="str">"cmt">// Mode of comparing/sorting by a position ID in the tester
   };
class=class="str">"cmt">//--- input parameters
input   class="type">class="kw">string   InpTestedSymbol   = "";      class=class="str">"cmt">/* The symbol being tested in the tester        */
input   class="type">long     InpTestedMagic     = -class="num">1;      class=class="str">"cmt">/* The magic number being tested in the tester  */
sinput  class="type">bool     InpShowDataInLog   = false;   class=class="str">"cmt">/* Show collected data in the log               */
class=class="str">"cmt">//--- global variables
SDeal            ExtArrayDeals[]={};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save deals from history into the array                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int SaveDealsToArray(SDeal &array[], class="type">bool logs=false)
  {
class=class="str">"cmt">//--- deal structure
   SDeal deal={};

class=class="str">"cmt">//--- request the deal history in the interval from the very beginning to the current moment 
   if(!HistorySelect(class="num">0, TimeCurrent()))
     {
       Print("HistorySelect() failed. Error ", GetLastError());
       class="kw">return class="num">0;
     }

class=class="str">"cmt">//--- total number of deals in the list 
   class="type">int total=HistoryDealsTotal();
class=class="str">"cmt">//--- handle each deal 
   for(class="type">int i=class="num">0; i<total; i++)
     {
       class=class="str">"cmt">//--- get the ticket of the next deal(the deal is automatically selected to get its properties)
       class="type">ulong ticket=HistoryDealGetTicket(i);
       if(ticket==class="num">0)
         class="kw">continue;

       class=class="str">"cmt">//--- save only balance and trading deals
       ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket, DEAL_TYPE);
       if(deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL && deal_type!=DEAL_TYPE_BALANCE)
         class="kw">continue;

       class=class="str">"cmt">//--- save the deal properties in the structure
       deal.ticket=ticket;
       deal.type=deal_type;
       deal.order=HistoryDealGetInteger(ticket, DEAL_ORDER);
       deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket, DEAL_ENTRY);

把历史成交塞进结构体数组的实操细节

在 MT5 里用 HistoryDealGetInteger / HistoryDealGetDouble / HistoryDealGetString 三类函数,可以把某一笔成交的字段逐个读进自定义结构体 SDeal。上面这段把 ticket 对应的 reason、time_msc、pos_id、volume、price、profit、commission、swap、fee、sl、tp、magic 以及品种、注释、外部 ID 全部抓取,最后用 SymbolInfoInteger 补一个 digits 字段,方便后续按品种精度格式化输出。 数组扩容这一步容易踩坑:先取当前 Size(),调 ArrayResize(array, size+1, total) 时第三个参数 total 是预估总量,若返回大小不等于 size+1 就说明内存分配失败,代码里用 ResetLastError + GetLastError 打印错误并 continue 跳过该笔,避免半截数据写进数组。 存完之后如果 logs 开关为真,就调 DealPrint(deal, i) 把单笔描述丢到专家日志;循环结束直接 return array.Size() 作为实际存入的成交数。另一个 DealsArrayPrint 函数则负责把整个数组再遍历打印一遍,空数组会报 'Empty deals array passed' 并直接 return,不会崩。 开 MT5 随便跑一个 EA 把这段接在 HistoryDealSelect 后面,就能在日志里看到每笔成交的 commission 与 swap 具体数值——外汇和贵金属杠杆品种这两项是浮动成本,高风险下对净值的影响可能比肉眼估的要大。

MQL5 / C++
deal.reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(ticket, DEAL_REASON);
deal.time=(class="type">class="kw">datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
deal.time_msc=HistoryDealGetInteger(ticket, DEAL_TIME_MSC);
deal.pos_id=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
deal.volume=HistoryDealGetDouble(ticket, DEAL_VOLUME);
deal.price=HistoryDealGetDouble(ticket, DEAL_PRICE);
deal.profit=HistoryDealGetDouble(ticket, DEAL_PROFIT);
deal.commission=HistoryDealGetDouble(ticket, DEAL_COMMISSION);
deal.swap=HistoryDealGetDouble(ticket, DEAL_SWAP);
deal.fee=HistoryDealGetDouble(ticket, DEAL_FEE);
deal.sl=HistoryDealGetDouble(ticket, DEAL_SL);
deal.tp=HistoryDealGetDouble(ticket, DEAL_TP);
deal.magic=HistoryDealGetInteger(ticket, DEAL_MAGIC);
deal.SetSymbol(HistoryDealGetString(ticket, DEAL_SYMBOL));
deal.SetComment(HistoryDealGetString(ticket, DEAL_COMMENT));
deal.SetExternalID(HistoryDealGetString(ticket, DEAL_EXTERNAL_ID));
deal.digits=(class="type">int)SymbolInfoInteger(deal.Symbol(), SYMBOL_DIGITS);

class=class="str">"cmt">//--- increase the array and
class="type">int size=(class="type">int)array.Size();
ResetLastError();
if(ArrayResize(array, size+class="num">1, total)!=size+class="num">1)
  {
   Print("ArrayResize() failed. Error ", GetLastError());
   class="kw">continue;
  }
class=class="str">"cmt">//--- save the deal in the array
array[size]=deal;
class=class="str">"cmt">//--- if allowed, display the description of the saved deal to the journal
if(logs)
   DealPrint(deal, i);
class=class="str">"cmt">//--- class="kw">return the number of deals stored in the array
 class="kw">return (class="type">int)array.Size();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display deals from the array to the journal                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DealsArrayPrint(SDeal &array[])
  {
  class="type">int total=(class="type">int)array.Size();
class=class="str">"cmt">//--- if an empty array is passed, report this and class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(total==class="num">0)
    {
     PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
     class="kw">return;
    }
class=class="str">"cmt">//--- In a loop through the deal array, print out a description of each deal
  for(class="type">int i=class="num">0; i<total; i++)
    {
     DealPrint(array[i], i);
    }
  }

◍ 把成交类型翻译成人话

在 MT5 的成交历史里,每一笔记录都带一个 ENUM_DEAL_TYPE 枚举值。直接打印数字没意义,写个映射函数转成可读字符串,复盘时一眼就能区分是真实买卖还是手续费、利息、分红这类非交易流水。 下面这段函数覆盖了 MT5 当前全部 20 种成交类型。注意 DEAL_TYPE_BUY / DEAL_TYPE_SELL 才是真实开平仓,其余如 DEAL_TYPE_COMMISSION_DAILY、DEAL_TYPE_INTEREST、DEAL_DIVIDEND 都属于账户层面的资金变动,统计净值曲线时要单独剥离,否则会把非交易盈亏算进策略表现。 外汇与贵金属杠杆高,这类账户流水波动可能掩盖真实交易胜率,建议跑历史回测时先按类型分组再算指标。

MQL5 / C++
class="type">class="kw">string DealTypeDescription(class="kw">const ENUM_DEAL_TYPE type)
  {
   class="kw">switch(type)
     {
      case DEAL_TYPE_BUY                   :  class="kw">return "Buy";
      case DEAL_TYPE_SELL                  :  class="kw">return "Sell";
      case DEAL_TYPE_BALANCE               :  class="kw">return "Balance";
      case DEAL_TYPE_CREDIT                :  class="kw">return "Credit";
      case DEAL_TYPE_CHARGE                :  class="kw">return "Additional charge";
      case DEAL_TYPE_CORRECTION            :  class="kw">return "Correction";
      case DEAL_TYPE_BONUS                 :  class="kw">return "Bonus";
      case DEAL_TYPE_COMMISSION            :  class="kw">return "Additional commission";
      case DEAL_TYPE_COMMISSION_DAILY      :  class="kw">return "Daily commission";
      case DEAL_TYPE_COMMISSION_MONTHLY    :  class="kw">return "Monthly commission";
      case DEAL_TYPE_COMMISSION_AGENT_DAILY :  class="kw">return "Daily agent commission";
      case DEAL_TYPE_COMMISSION_AGENT_MONTHLY:  class="kw">return "Monthly agent commission";
      case DEAL_TYPE_INTEREST              :  class="kw">return "Interest rate";
      case DEAL_TYPE_BUY_CANCELED          :  class="kw">return "Canceled buy deal";
      case DEAL_TYPE_SELL_CANCELED         :  class="kw">return "Canceled sell deal";
      case DEAL_DIVIDEND                   :  class="kw">return "Dividend operations";
      case DEAL_DIVIDEND_FRANKED           :  class="kw">return "Franked(non-taxable) dividend operations";
      case DEAL_TAX                        :  class="kw">return "Tax charges";

「把成交记录拼成可读字符串」

成交事件在 MT5 里本质是结构体字段,直接打印 ticket 或 price 对人类不友好。下面这组函数把 ENUM_DEAL_ENTRY 和成交类型映射成文字,再统一拼成一行描述。 DealEntryDescription 用 switch 把四种入场方式转成短语:DEAL_ENTRY_IN 对应 Entry In,DEAL_ENTRY_OUT 对应 Entry Out,DEAL_ENTRY_INOUT 对应 Entry InOut,DEAL_ENTRY_OUT_BY 对应 Entry OutBy;default 分支返回 Unknown entry 加数值,方便排查未定义枚举。 DealDescription 接收 SDeal 引用和序号,先用 StringFormat("% 5d", index) 把序号补成 5 位右对齐。若 type 不是 DEAL_TYPE_BALANCE,就拼出含品种、magic、挂单价、sl、tp 的完整行;若是余额类成交,只输出金额和账户币种,例如实测一行:deal #190715988 Entry In, type Balance 3000.00 USD at 2024.09.13 21:48。外汇与贵金属杠杆高,这类日志仅用于复盘,不代表任何收益预期。 把下面代码丢进 MT5 脚本,接上你自己的历史成交遍历,就能在专家日志里直接看人话版记录,而不是一堆数字。

MQL5 / C++
class="type">class="kw">string DealEntryDescription(class="kw">const ENUM_DEAL_ENTRY entry)
  {
   class="kw">switch(entry)
     {
      case DEAL_ENTRY_IN     :  class="kw">return "Entry In";
      case DEAL_ENTRY_OUT    :  class="kw">return "Entry Out";
      case DEAL_ENTRY_INOUT  :  class="kw">return "Entry InOut";
      case DEAL_ENTRY_OUT_BY :  class="kw">return "Entry OutBy";
      class="kw">default                :  class="kw">return "Unknown entry: "+(class="type">class="kw">string)entry;
     }
  }
class="type">class="kw">string DealDescription(SDeal &deal, class="kw">const class="type">int index)
  {
   class="type">class="kw">string indexs=StringFormat("% 5d", index);
   if(deal.type!=DEAL_TYPE_BALANCE)
     class="kw">return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s(magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f",
                         indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type),
                         deal.pos_id, deal.Symbol(), deal.magic, deal.digits, deal.price,
                         TimeToString(deal.time, TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.digits, deal.sl, deal.digits, deal.tp));
   else
     class="kw">return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s",
                         indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type),
                         deal.profit, AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.time)));
  }

把成交记录落盘到公共文件

下面这条日志是真实成交落库前的一行样例:deal #190724678 为 USDCHF 多单入场,magic=600,价格 0.84940,时间 2024.09.13 23:49:03,sl 0.84811、tp 0.84983。把这类记录写进文件,才能在 MT5 重启后复盘每笔仓位。

写文件先走 FileOpenToWrite,用 FILE_WRITEFILE_BINFILE_COMMON 三标志拿到公共目录句柄;若返回 INVALID_HANDLE 就打印错误码并退出。读方向对称,FileOpenToRead 只把标志换成 FILE_READ,其余逻辑一致。

FileWriteDealsFromArray 负责批量落盘:空数组直接返 false;否则打开文件、FileSeek 到 SEEK_END 追加。注意数组 Size() 为 0 的检查,否则后续写入会静默丢数据。外汇与贵金属杠杆高,这类本地日志仅作风控留痕,不预示任何盈利可能。

MQL5 / C++
class="type">void DealPrint(SDeal &deal, class="kw">const class="type">int index)
  {
   Print(DealDescription(deal, index));
  }
class="type">bool FileOpenToWrite(class="type">int &handle)
  {
   ResetLastError();
   handle=FileOpen(PATH, FILE_WRITE|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
     {
       PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError());
       class="kw">return false;
     }
   class="kw">return true;
  }
class="type">bool FileOpenToRead(class="type">int &handle)
  {
   ResetLastError();
   handle=FileOpen(PATH, FILE_READ|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
     {
       PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError());
       class="kw">return false;
     }
   class="kw">return true;
  }
class="type">bool FileWriteDealsFromArray(SDeal &array[], class="type">ulong &file_size)
  {
   if(array.Size()==class="num">0)
     {
       PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
       class="kw">return false;
     }
   class="type">int handle=INVALID_HANDLE;
   if(!FileOpenToWrite(handle))
      class="kw">return false;
   class="type">bool res=true;
   ResetLastError();
   res&=FileSeek(handle, class="num">0, SEEK_END);
   if(!res)
      PrintFormat("%s: FileSeek(SEEK_END) failed. Error %d",__FUNCTION__, GetLastError());
  }

◍ 把成交记录落盘再读回来做闭环校验

写入端先把 file_size 清零,再用 FileWriteArray 把结构体数组整体刷进文件,返回值与数组 Size 比对才算成功。失败时直接打印函数名和 GetLastError 编码,成功才用 FileSize 取真实字节数,最后务必 FileClose 释放句柄。 读回路径反过来:FileOpenToRead 拿到句柄后,FileReadArray 的返回值大于 0 才认为有数据进数组,否则报的虽是 FileWriteArray 的错字(源码笔误),本质仍是读通道异常。同样以 FileSize 回填 file_size,关文件后返回布尔。 PreparesDealsHistoryFile 是真正的串联点:SaveDealsToArray 先抓全账户成交,数量为 0 直接退出;写盘后立刻 ArrayResize(deals_array, 0, total) 清空但保留缓存容量,再从文件读回做往返验证。日志会给出类似「N deals were saved... written to a file of X bytes」和「X bytes were read... A total of N deals were received」两行,两边 N 与 X 一致即证明序列化无损。 开 MT5 把这段挂到 EA 的 OnInit 前半段,跑完看专家日志两次打印的成交数与字节数是否完全对等;若读回 N 少于写盘 N,优先查 FileReadArray 那行被误写的报错函数名以及文件共用路径权限。外汇与贵金属品种 tick 密集,成交量大时文件字节数可能到数十 KB,高频重跑注意终端公共目录写入限速。

MQL5 / C++
  file_size=class="num">0;
  res&=(FileWriteArray(handle, array)==array.Size());
  if(!res)
     PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
  else
     file_size=FileSize(handle);
class=class="str">"cmt">//--- close the file 
  FileClose(handle);
  class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load the deal data from the file into the array                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool FileReadDealsToArray(SDeal &array[], class="type">ulong &file_size)
  {
class=class="str">"cmt">//--- open the file for reading, get its handle
  class="type">int handle=INVALID_HANDLE;
  if(!FileOpenToRead(handle))
     class="kw">return false;
  
class=class="str">"cmt">//--- move the file pointer to the end of the file 
  class="type">bool res=true;
  ResetLastError();
  
class=class="str">"cmt">//--- read data from the file into the array
  file_size=class="num">0;
  res=(FileReadArray(handle, array)>class="num">0);
  if(!res)
     PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
  else
     file_size=FileSize(handle);
class=class="str">"cmt">//--- close the file 
  FileClose(handle);
  class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Prepare a file with history deals                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool PreparesDealsHistoryFile(SDeal &deals_array[])
  {
class=class="str">"cmt">//--- save all the account deals in the deal array
  class="type">int total=SaveDealsToArray(deals_array);
  if(total==class="num">0)
     class="kw">return false;
     
class=class="str">"cmt">//--- write the deal array data to the file
  class="type">ulong file_size=class="num">0;
  if(!FileWriteDealsFromArray(deals_array, file_size))
     class="kw">return false;
     
class=class="str">"cmt">//--- print in the journal how many deals were read and saved to the file, the path to the file and its size
  PrintFormat("%u deals were saved in an array and written to a \"%s\" file of %I64u bytes in size",
               deals_array.Size(), "TERMINAL_COMMONDATA_PATH\\Files\\"+ PATH, file_size);

class=class="str">"cmt">//--- now, to perform a check, we will read the data from the file into the array
  ArrayResize(deals_array, class="num">0, total);
  if(!FileReadDealsToArray(deals_array, file_size))
     class="kw">return false;
     
class=class="str">"cmt">//--- print in the journal how many bytes were read from the file and the number of deals received in the array
  PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, deals_array.Size());
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- If the EA is not running in the tester
把重放诊断交给小布盯盘
这些历史成交重演与止损调整的可视诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到重放轨迹与异常标注,你只管判断要不要改算法。

常见问题

图表模式负责把账户全部已完结成交落盘成文件,测试器模式读文件重演开平仓,两阶段拆开才能先备数据再改逻辑试错。
可以,小布盯盘内置了历史成交读取与可视重放,省去自己写文件导出 EA 的步骤,品种页里就能看动态持仓演化。
在 EA 输入参数里限定测试品种与 magic number,文件写入和读取阶段都按该过滤条件走,避免不同策略成交互相污染。
不会自动变,需在测试器版 EA 里显式追加新离场条件代码,原文件只记录已发生成交,改写部分完全由你加的算法决定。
手动成交通常 magic 为 0,导出时把 0 也列为可接受值即可,但分析时要单独标记,防止和 EA 单混淆导致误判。