MQL5 Cookbook: 减少过度配合的影响以及处理报价缺失·进阶篇
(2/3)· 当单一品种历史不够做前瞻验证,借多资产联合优化把未来失效概率压下来
多数交易者把优化结果锁死在单一品种的历史区间里,却忽略了一个事实:样本重复次数不够,参数再漂亮也只是回测里的幻觉。当某资产日图数据只够跑出几十笔交易,你凭什么相信它在下一季度还管用。
「正文」
<span class="keyword">int</span> limit_count =<span class="number">0</span>; <span class="comment">// 限制可能开启图表数目的计数器</span> <span class="keyword">int</span> strings_count =<span class="number">0</span>; <span class="comment">// 字符串计数器</span> <span class="keyword">int</span> sections_count =-<span class="number">1</span>; <span class="comment">// 区段计数器</span> <span class="comment">//--- Message 01</span> <span class="keyword">string</span> message_01=<span class="string">"<--- <- "</span>+file_name+<span class="string">" -> 文件还没有正确准备好!--->\n"</span> <span class="string">"<--- 第一个字符串没有包含区段编号标识符 ("</span>+delimeter+<span class="string">")!--->"</span>; <span class="comment">//--- Message 02</span> <span class="keyword">string</span> message_02=<span class="string">"<--- <- "</span>+file_name+<span class="string">" -> 文件还没有正确准备好!--->\n"</span> <span class="string">"<--- 最后的字符串没有换行符号, --->\n"</span>
◍ 文件游标与图表上限的临界处理
这段逻辑发生在遍历品种列表文件的过程中,核心是解决「文件指针走到行尾」与「MT5 图表总数封顶」两个临界状态。limit_count 每读一段就自增,一旦触到 CHARTS_MAX 宏定义的上限,就调用 PrepareArrayForOneSymbol 收尾并 FileClose 退出,返回已收集的行数。 当 FileIsLineEnding 判定当前位于行末时,先通过 FileTell 取偏移;若未到文件末尾则用 offset++ 跳过换行符,再 FileSeek 以 SEEK_SET 重定位到下一行起点。这里有个区段守卫:sections_count 不等于 SectionOfSymbolList 就直接 break,避免误读其他区块。 只有在区段匹配且 read_line 非空时,strings_count 才累加,并用 ArrayResize 动态扩 temporary_symbols 数组,把当前行写入末位索引。外汇与贵金属品种列表解析属于高频 IO 操作,MT5 终端在极端行情下可能限频,实盘前建议在策略测试器里用样本 txt 跑一遍确认行数对齐。
class="kw">return(strings_count); } class=class="str">"cmt">//--- 增加可能开启图表限制的计数器 limit_count++; class=class="str">"cmt">//--- 如果达到了限制 if(limit_count>=CHARTS_MAX) { PrepareArrayForOneSymbol(strings_count,message_02); class=class="str">"cmt">//--- 关闭文件 FileClose(file_handle); class=class="str">"cmt">//--- 返回文件的字符串数目 class="kw">return(strings_count); } class=class="str">"cmt">//--- 取得指针位置 offset=FileTell(file_handle); class=class="str">"cmt">//--- 如果这是字符串末尾 if(FileIsLineEnding(file_handle)) { class=class="str">"cmt">//--- 如果还没有到文件末尾,读取下一个字符串 class=class="str">"cmt">// 为了这个目的,增加文件指针的偏移 if(!FileIsEnding(file_handle)) offset++; class=class="str">"cmt">//--- 移动到下一个字符串 FileSeek(file_handle,offset,SEEK_SET); class=class="str">"cmt">//--- 如果我们不在文件的指定区段,退出循环 if(sections_count!=SectionOfSymbolList) break; class=class="str">"cmt">//--- 否则, else { class=class="str">"cmt">//--- 如果字符串不为空 if(read_line!="") { class=class="str">"cmt">//--- 增加字符串计数器 strings_count++; class=class="str">"cmt">//--- 增加字符串数组的大小, ArrayResize(temporary_symbols,strings_count); class=class="str">"cmt">//--- 把字符串写到当前索引处 temporary_symbols[strings_count-class="num">1]=read_line; } } class=class="str">"cmt">//--- 退出循环 break; } }
把回测结果导成可读的 CSV
读取完参数文件后,若句柄无效就直接调用 PrepareArrayForOneSymbol 给当前品种建一个只有 1 行的临时数组,并把 _Symbol 写进下标 0。这一步保证单品种模式下也不会因为数组越界崩在半路。 PrepareArrayForOneSymbol 里把 strings_count 硬性设为 1,再用 ArrayResize(temporary_symbols,1) 定型。实盘里若你改了逻辑想支持多品种,这里不跟着放大就会截断数据。
| 真正的落盘在 CreateSymbolBalanceReport:先尝试在通用目录建 LastTest.csv,标志位用 FILE_CSV | FILE_WRITE | FILE_ANSI | FILE_COMMON。文件句柄大于 0 才继续,否则静默 return,不会抛错到图表。 |
|---|
函数内预置了 digits、deals_total、ticket、drawdown_max、balance 等变量,其中 percent_drawdown 与 money_drawdown 用了 static 修饰,意味着两次调用之间回撤值会保留上次结果。外汇与贵金属杠杆高,回测最大回撤仅代表历史样本,换周期可能明显放大,请仅作概率参考。 写入前先拼 headers 字符串 "TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,",分隔符锁定为逗号。你在 MT5 里跑完策略测试,打开终端公共目录下的 LastTest.csv 就能直接看每笔成交的概览。
if(FileIsEnding(file_handle)) break; } FileClose(file_handle); } else PrepareArrayForOneSymbol(strings_count,message_03); class=class="str">"cmt">//--- 返回文件字符串数量 class="kw">return(strings_count); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 为一个交易品种准备一个数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void PrepareArrayForOneSymbol(class="type">int &strings_count,class="type">class="kw">string message) { Print(message); strings_count=class="num">1; ArrayResize(temporary_symbols,strings_count); temporary_symbols[class="num">0]=_Symbol; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建 .csv 格式的交易测试报告 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateSymbolBalanceReport() { class="type">int file_handle =INVALID_HANDLE; class=class="str">"cmt">// 文件句柄 class="type">class="kw">string path =""; class=class="str">"cmt">// 文件路径 if((path=CreateInputParametersFolder())=="") class="kw">return; file_handle=FileOpen(path+"\\LastTest.csv",FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON); if(file_handle>class="num">0) { class="type">int digits =class="num">0; class=class="str">"cmt">// 价格中的小数点位数 class="type">int deals_total =class="num">0; class=class="str">"cmt">// 指定历史的交易数量 class="type">ulong ticket =class="num">0; class=class="str">"cmt">// 交易订单号 class="type">class="kw">double drawdown_max =class="num">0.0; class=class="str">"cmt">// 回撤 class="type">class="kw">double balance =class="num">0.0; class=class="str">"cmt">// 余额 class="type">class="kw">string delimeter =","; class=class="str">"cmt">// 分隔符 class="type">class="kw">string string_to_write =""; class=class="str">"cmt">// 生成写入的字符串 class="kw">static class="type">class="kw">double percent_drawdown =class="num">0.0; class=class="str">"cmt">// 回撤表现为百分率 class="kw">static class="type">class="kw">double money_drawdown =class="num">0.0; class=class="str">"cmt">// 回撤钱数 class="type">class="kw">string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,"
「多品种回测报告的逐笔落盘」
做跨品种策略复盘时,最直接的坑是报告抬头写死成单品种字段。下面这段逻辑在 SYMBOLS_COUNT>1 时,用循环把 InputSymbols 数组里的每个品种名追加到 headers 字符串后面,保证 CSV 第一行就能区分多列数据源。 文件写入前必须先 HistorySelect(0,TimeCurrent()) 拉全历史,再用 HistoryDealsTotal() 拿到 deals_total。注意这里 ArrayResize(symbol_balance, SYMBOLS_COUNT) 和每个品种的 balance 子数组都按 deals_total resized,否则多品种资金曲线会越界报错。 逐笔循环里,balance+=deal.profit+deal.swap+deal.commission 是净值累加,而 TesterDrawdownMaximum(i,balance,percent_drawdown,money_drawdown) 实时算从局部峰值的回撤。最后 StringConcatenate 把时间、品种、类型、开平仓、手数、价格(按 SYMBOL_DIGITS 取小数位)、库存费、利润、两项回撤、余额拼成一行写盘——开 MT5 把这段塞进 EA 的 OnTester 或独立脚本,跑完能在文件目录拿到可透视的逐笔账单。外汇与贵金属杠杆高,回测最大回撤仅代表历史样本,实盘可能放大。
"PRICE,SWAP($),PROFIT($),DRAWDOWN(%),DRAWDOWN($),BALANCE"; class=class="str">"cmt">//--- 如果选了多个交易品种, 修改抬头字符串 if(SYMBOLS_COUNT>class="num">1) { for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++) StringAdd(headers,","+InputSymbols[s]); } class=class="str">"cmt">//--- 写报告抬头 FileWrite(file_handle,headers); class=class="str">"cmt">//--- 取得完整历史 HistorySelect(class="num">0,TimeCurrent()); class=class="str">"cmt">//--- 取得交易数量 deals_total=HistoryDealsTotal(); class=class="str">"cmt">//---根据交易品种数量修改余额数组大小 ArrayResize(symbol_balance,SYMBOLS_COUNT); class=class="str">"cmt">//--- 修改每个交易品种的交易数组大小 for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++) ArrayResize(symbol_balance[s].balance,deals_total); class=class="str">"cmt">//--- 在循环中迭代并写入数据 for(class="type">int i=class="num">0; i<deals_total; i++) { class=class="str">"cmt">//--- 取得交易订单号码 ticket=HistoryDealGetTicket(i); class=class="str">"cmt">//--- 取得所有的交易属性 GetHistoryDealProperties(ticket,D_ALL); class=class="str">"cmt">//--- 取得价格中的小数位数 digits=(class="type">int)SymbolInfoInteger(deal.symbol,SYMBOL_DIGITS); class=class="str">"cmt">//--- 计算总体余额 balance+=deal.profit+deal.swap+deal.commission; class=class="str">"cmt">//--- 计算从局部最大值的最大回撤 TesterDrawdownMaximum(i,balance,percent_drawdown,money_drawdown); class=class="str">"cmt">//--- 使用连接生成用于写入的字符串 StringConcatenate(string_to_write, deal.time,delimeter, DealSymbolToString(deal.symbol),delimeter, DealTypeToString(deal.type),delimeter, DealEntryToString(deal.entry),delimeter, DealVolumeToString(deal.volume),delimeter, DealPriceToString(deal.price,digits),delimeter, DealSwapToString(deal.swap),delimeter, DealProfitToString(deal.symbol,deal.profit),delimeter, DrawdownToString(percent_drawdown),delimeter, DrawdownToString(money_drawdown),delimeter, DoubleToString(balance,class="num">2));
◍ 多品种余额的逐笔累加逻辑
当监控列表里挂了不止一个交易品种(SYMBOLS_COUNT>1)时,就不能只写总账户曲线,得把每个品种的净余额分开记。下面这段就是按成交记录逐笔往对应品种余额上叠加减值的实现。 核心判断在于 deal.symbol 是否命中当前遍历的品种,且 deal.profit 不等于 0 才视为有效交易结果。此时把上一根索引的余额,加上该笔的 profit、swap、commission 三项,得到本索引的新余额,并用 DoubleToString 保留 2 位小数拼进写出字符串。 若这笔成交不属于当前品种,则走 else 分支:如果是 DEAL_TYPE_BALANCE 类型(通常是入金首笔),所有品种共用同一个 balance 值;否则直接复制 i-1 的前值,保证 CSV 里每个品种每一行都有连续数值,不会出现断档。 开 MT5 把这段嵌进你的成交回放脚本,跑一个跨品种实盘账户,重点看 swap 和 commission 是否被重复计入——这是多品种余额报表最容易算错的地方。外汇与贵金属杠杆交易风险较高,余额曲线仅作技术验证,不构成任何方向建议。
class=class="str">"cmt">//--- 如果选择了多个交易品种,写下它们的余额 if(SYMBOLS_COUNT>class="num">1) { class=class="str">"cmt">//--- 迭代所有交易品种 for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++) { class=class="str">"cmt">//--- 如果交易品种相同而交易结果不等于零 if(deal.symbol==InputSymbols[s] && deal.profit!=class="num">0) { class=class="str">"cmt">//--- 显示对应交易品种的交易余额 class=class="str">"cmt">// 考虑库存费和手续费 symbol_balance[s].balance[i]=symbol_balance[s].balance[i-class="num">1]+ deal.profit+ deal.swap+ deal.commission; class=class="str">"cmt">//--- 加到字符串上 StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],class="num">2)); } class=class="str">"cmt">//--- 否则写下前值 else { class=class="str">"cmt">//--- 如果交易类型为 "余额" (第一笔交易) if(deal.type==DEAL_TYPE_BALANCE) { class=class="str">"cmt">//--- 余额对所有交易品种都相同 symbol_balance[s].balance[i]=balance; StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],class="num">2)); } class=class="str">"cmt">//--- 否则在当前索引写下前值 else { symbol_balance[s].balance[i]=symbol_balance[s].balance[i-class="num">1]; StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],class="num">2)); } } } }