在 MetaTrader 5 中交易的可视评估和调整·进阶篇
(2/3)· 当多个 EA 和手动单混跑同一账户,怎样一眼看穿止损挂得合不合理
账户跑了一堆 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 的成交,验证自己的仓位统计逻辑。外汇与贵金属杠杆高,历史统计仅反映过去,样本偏差可能导致实盘概率偏移。
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 具体数值——外汇和贵金属杠杆品种这两项是浮动成本,高风险下对净值的影响可能比肉眼估的要大。
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 都属于账户层面的资金变动,统计净值曲线时要单独剥离,否则会把非交易盈亏算进策略表现。 外汇与贵金属杠杆高,这类账户流水波动可能掩盖真实交易胜率,建议跑历史回测时先按类型分组再算指标。
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 脚本,接上你自己的历史成交遍历,就能在专家日志里直接看人话版记录,而不是一堆数字。
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_WRITE | FILE_BIN | FILE_COMMON 三标志拿到公共目录句柄;若返回 INVALID_HANDLE 就打印错误码并退出。读方向对称,FileOpenToRead 只把标志换成 FILE_READ,其余逻辑一致。 |
|---|
FileWriteDealsFromArray 负责批量落盘:空数组直接返 false;否则打开文件、FileSeek 到 SEEK_END 追加。注意数组 Size() 为 0 的检查,否则后续写入会静默丢数据。外汇与贵金属杠杆高,这类本地日志仅作风控留痕,不预示任何盈利可能。
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,高频重跑注意终端公共目录写入限速。
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