交易系统的评估 - 有关进入、退出与交易效率的概述·综合运用
(3/3)·从部分平仓到净仓统计,把 Bulashev 旧法改造成 MT5 可跑的评估框架
很多交易者直接把 2003 年的交易统计公式套进 MetaTrader 5,却没意识到订单和仓位早已不是同一概念。部分平仓、加仓、净仓结算让旧解释系统彻底失效,硬套只会得出扭曲的胜率与效率值。本篇接前两篇基础与实战,把统计方法重做成能跑在当下平台的版本。
成交流如何归并成单笔交易
做历史回测或实盘统计时,MT5 给的是 deal(成交)而不是 trade(交易)。一个持仓可能被部分平仓、反向加仓,拆成几十个 deal,直接读账户历史会看花眼。下面这段引擎方法就是把线性 deal 流重建成净头寸交易的关键逻辑。
核心思路是扫一遍 deal 数组:遇到 DEAL_ENTRY_IN 就开新交易;遇到 INOUT(开平同笔)先平掉前面累积的、再开新仓并把剩余量挂到新交易体积上;遇到 OUT 则按成交量从最早未平交易里逐个核销。注意 trades_ends 指针只向前推进,代表已平完的段,避免重复闭合。
最后一个成交如果是 OUT 且 c==count_deals-1,直接把残余交易全平并将 trades_ends 回退一位,防止多算空交易。外汇与贵金属杠杆高,部分平仓路径若没处理净量,统计胜率会系统性偏移,建议开 MT5 用脚本跑一遍 EURUSD 的 M15 历史验证该归并是否和你手工标注一致。
别把正态当圣经
上面代码里 nd() 是去规范化(normalize)封装,用来吃掉浮点误差。实盘里 0.01 手对比 0.0100000001 若直接判等会死循环,务必保留这层包裹。
class="type">void C_Pos::OnHistoryTransform() { DigitMinLots();class=class="str">"cmt">// 填充DIGITS值 count_trades=class="num">0;trades_ends=class="num">0; ArrayResize(m_trades_stats,count_trades,count_deals); for(class="type">int c=class="num">0;c<count_deals;c++) { if(m_deals_stats[c].deals_entry==DEAL_ENTRY_IN) { open_pos(c); } elseclass=class="str">"cmt">// else in { class="type">class="kw">double POS=class="num">0; for(class="type">int i=trades_ends;i<count_trades;i++)POS+=m_trades_stats[i].trade_volume; if(m_deals_stats[c].deals_entry==DEAL_ENTRY_INOUT) { for(class="type">int i=trades_ends;i<count_trades;i++)close_pos(i,c); trades_ends=count_trades; open_pos(c); m_trades_stats[count_trades-class="num">1].trade_volume=m_deals_stats[c].deals_volume-POS; } elseclass=class="str">"cmt">// else in/out { if(m_deals_stats[c].deals_entry==DEAL_ENTRY_OUT) { if(c==count_deals-class="num">1)class=class="str">"cmt">// 如果是最后一个成交 { for(class="type">int i=trades_ends;i<count_trades;i++)close_pos(i,c); trades_ends=count_trades-class="num">1; } elseclass=class="str">"cmt">// 如果不是最后一个成交 { class="type">class="kw">double out_vol=nd(m_deals_stats[c].deals_volume); class="kw">while(nd(out_vol)>class="num">0) { if(nd(out_vol)>=nd(m_trades_stats[trades_ends].trade_volume)) { close_pos(trades_ends,c); out_vol-=nd(m_trades_stats[trades_ends].trade_volume); trades_ends++; } elseclass=class="str">"cmt">// 如果平仓后剩余量小于下一次交易 { class=class="str">"cmt">// 把所有交易向前移动一位
◍ 净平仓时的持仓数组重组逻辑
当一笔出场成交量 out_vol 大于 0 且并非最后一笔订单时,代码先对统计数组做扩容:count_trades 自增 1,ArrayResize 把 m_trades_stats 长度同步撑大,保证新拆出的平仓记录有地方存。 随后 for 循环从新末尾向前到 trades_ends 调用 copy_pos(x),把原持仓记录逐个后移一位,腾出 trades_ends+1 这个槽位装「建仓残量」。新槽位的 trade_volume 设为上一笔剩余量减去 out_vol(nd 做浮点规整),而原 trades_ends 位置通过 close_pos 平掉 out_vol,自身 volume 改写为 out_vol,out_vol 清 0 后 trades_ends 前进 1。 这段处理意味着:一次净平仓可能把一笔持仓拆成「已平部分」+「继续持有部分」两条统计记录。在 MT5 里把 m_trades_stats 打印出来,你能直接看到同一入场价出现两条 volume 互补的记录,总和不差。外汇与贵金属杠杆高,拆仓统计仅用于复盘,不代表任何方向先验。
count_trades++; ArrayResize(m_trades_stats,count_trades); for(class="type">int x=count_trades-class="num">1;x>trades_ends;x--)copy_pos(x); class=class="str">"cmt">// 建仓大小等于当前剩余量和下次交易量之间的差 m_trades_stats[trades_ends+class="num">1].trade_volume=nd(m_trades_stats[trades_ends].trade_volume-out_vol); class=class="str">"cmt">// 平仓量等于剩余值 close_pos(trades_ends,c); m_trades_stats[trades_ends].trade_volume=nd(out_vol); out_vol=class="num">0; trades_ends++;
「用 Bulachev 思路算每笔交易的效率」
切换解释框架后,就能按 Bulachev 的办法给每笔交易打效率分。进入效率与退出效率落在 0~1 区间,整笔交易效率则在 -1~1 之间:正值代表方向吃得准,负值说明来回被打。 支撑这套评估的核心是 C_Pos 类的 efficiency() 方法,它会把算出的数据回填到交易结构里。两个私有方法 iHighest() 与 iLowest() 只差在取极值方向(fmax / fmin),都接收 start_time 与 stop_time,并把可能落在 1 分钟 K 线中间的成交时间标准化到就近的 M1 边界。 效率计算的本质是把持仓期间的价格摆动区间归一化:用最高价减最低价取倒数做分母,再拿进场/出场相对极值的偏移去乘。外汇与贵金属杠杆高,效率只是复盘用的概率参考,不能当成下单信号。 下面这段是效率填充的主循环与极值搜索原型,开 MT5 建个 C_Pos 派生类就能直接跑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算效率值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void C_Pos::efficiency() { for(class="type">int i=class="num">0;i<count_trades;i++) { m_trades_stats[i].max_price_trade=iHighest(symbol,PERIOD_M1,m_trades_stats[i].enter_date,m_trades_stats[i].exit_date); class=class="str">"cmt">// 交易最高价 m_trades_stats[i].min_price_trade=iLowest(symbol,PERIOD_M1,m_trades_stats[i].enter_date,m_trades_stats[i].exit_date); class=class="str">"cmt">// 交易最低价 class="type">class="kw">double minimax=class="num">0; minimax=m_trades_stats[i].max_price_trade-m_trades_stats[i].min_price_trade;class=class="str">"cmt">// 最高最低价的差 if(minimax!=class="num">0)minimax=class="num">1.0/minimax; if(m_trades_stats[i].trade_type==DEAL_TYPE_BUY) { class=class="str">"cmt">//进入效率 m_trades_stats[i].enter_efficiency=(m_trades_stats[i].max_price_trade-m_trades_stats[i].enter_price)*minimax; class=class="str">"cmt">//退出效率 m_trades_stats[i].exit_efficiency=(m_trades_stats[i].exit_price-m_trades_stats[i].min_price_trade)*minimax; class=class="str">"cmt">//交易效率 m_trades_stats[i].trade_efficiency=(m_trades_stats[i].exit_price-m_trades_stats[i].enter_price)*minimax; } else { if(m_trades_stats[i].trade_type==DEAL_TYPE_SELL) { class=class="str">"cmt">//进入效率 m_trades_stats[i].enter_efficiency=(m_trades_stats[i].enter_price-m_trades_stats[i].min_price_trade)*minimax; class=class="str">"cmt">//退出效率 m_trades_stats[i].exit_efficiency=(m_trades_stats[i].max_price_trade-m_trades_stats[i].exit_price)*minimax; class=class="str">"cmt">//交易效率 m_trades_stats[i].trade_efficiency=(m_trades_stats[i].enter_price-m_trades_stats[i].exit_price)*minimax; } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在 start_time --> stop_time 时间段中寻找最大值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double C_Pos::iHighest(class="type">class="kw">string symbol_name,class=class="str">"cmt">// 交易品种名称 ENUM_TIMEFRAMES timeframe, class=class="str">"cmt">// 周期 class="type">class="kw">datetime start_time, class=class="str">"cmt">// 开始时间 class="type">class="kw">datetime stop_time class=class="str">"cmt">// 结束时间 ) { class="type">class="kw">double buf[];
用 CopyHigh 抓区间最高价
在 MT5 里做价格行为分析时,经常需要取某段时间内的最高价,而不是整根 K 线的高点序列。下面这段逻辑把用户给的小时数先规整成秒级时间戳,再用 CopyHigh 把对应区间的高点拷进缓冲区。 datetime start_t=(start_time/60)*60; 先把开始小时换算成秒,例如 9 点变成 32400 秒。 datetime stop_t=(stop_time/60+1)*60; 结束时间多算一小时并规整,保证区间右边界闭包。 int period=CopyHigh(symbol_name,timeframe,start_t,stop_t,buf); 从服务器拉高点到 buf,返回根数就是 period。 double res=buf[0]; 以第一根高为初值,随后 for 循环从 i=1 到 period-1 用 fmax 逐根比较。最终 res 就是该时间窗内的最高价,外汇与贵金属波动剧烈,这种区间极值仅反映历史,后续突破概率需结合实时结构判断。 把 start_time 和 stop_time 设成你盯盘的关键小时(如伦敦盘 8~17),挂到 EA 里就能自动报出区间上限,省去肉眼翻图。
class="type">class="kw">datetime start_t=(start_time/class="num">60)*class="num">60;class=class="str">"cmt">// 规范化开始时间 class="type">class="kw">datetime stop_t=(stop_time/class="num">60+class="num">1)*class="num">60;class=class="str">"cmt">// 规范化结束时间 class="type">int period=CopyHigh(symbol_name,timeframe,start_t,stop_t,buf); class="type">class="kw">double res=buf[class="num">0]; for(class="type">int i=class="num">1;i<period;i++) res=fmax(res,buf[i]); class="kw">return(res); }
◍ 给仓位统计补一层派生封装
在基类 C_Pos 之上再套一层 C_PosStat,核心动机是弥补 MQL5 结构不支持直接算术运算的短板。作者用 S_efficiency 结构记录每笔仓位的进入效率、退出效率和交易效率,并在 private 区手写了一组运算函数——这部分没有走「给每个字段单独写方法」的路子,而是直接对结构整体做算术重载,社区里对此向来有争议,但跑通才是硬道理。 AVG() 只算数组算术均值,看不出离散程度,所以配套给了 STDEV() 算标准偏差。两者一起用,才能判断一组效率值是稳定收敛还是偶尔抽风。外汇与贵金属杠杆高,样本标准差若长期偏大,说明你的进出场逻辑可能只在特定行情有效,实盘前建议在 MT5 策略测试器里先拉 3 个月历史跑一遍。 OnPosStat() 是整类的发动机,拆成两段看更清楚:第一段翻全部成交历史,遇平衡成交直接跳过,把出现过的仓位 id 收进 id_pos 临时数组,去重后得到「历史中真实存在过的仓位清单」;第二段遍历这份清单,逐个调用基类里已经写好的仓位处理方法。下面这段类声明就是骨架,复制进 MetaEditor 能直接编译看字段。
class C_PosStat:class="kw">public C_Pos class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 效率值的结构 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct S_efficiency { class="type">class="kw">double enter_efficiency; class=class="str">"cmt">// 进入效率 class="type">class="kw">double exit_efficiency; class=class="str">"cmt">// 退出效率 class="type">class="kw">double trade_efficiency; class=class="str">"cmt">// 交易效率 S_efficiency() { enter_efficiency=class="num">0; exit_efficiency=class="num">0; trade_efficiency=class="num">0; }; ~S_efficiency(){}; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 整体上的交易统计类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class C_PosStat:class="kw">public C_Pos { class="kw">public: class="type">int PosTotal; class=class="str">"cmt">// 历史中的仓位数量 C_Pos pos[]; class=class="str">"cmt">// 仓位的指针数组 class="type">int All_count_trades; class=class="str">"cmt">// 历史中交易的总数 S_efficiency trade[]; class=class="str">"cmt">// 效率结构的指针数组 S_efficiency avg; class=class="str">"cmt">// 效率平均值的结构 S_efficiency stdev; class=class="str">"cmt">// 效率标准偏差值 class=class="str">"cmt">// 根据效率值和其平均值计算 C_PosStat(){PosTotal=class="num">0;}; ~C_PosStat(){}; class="type">void OnPosStat(); class=class="str">"cmt">// 引擎类 class="type">void OnTradesStat(); class=class="str">"cmt">// 收集交易信息并放入数组中 class=class="str">"cmt">// 把信息写进文件的函数 class="type">void WriteFileDeals(class="type">class="kw">string folder="deals");
「成交与交易报告的底层接口」
这套效率统计类把历史数据的落盘拆得很细:公开方法里 WriteFileDeals 写成交 csv,WriteFileTrades 写交易 csv,WriteFileTrades_all 则输出健康函数的综合 csv。想看单子明细还是看策略整体表现,调对应函数即可,不用自己拼 HistorySelect。 枚举转换用了重载,enum_translit 对 ENUM_DEAL_ENTRY 和 ENUM_DEAL_TYPE 都能转字符串,默认 latin=true。回测完直接把成交类型打印成可读文本,比看数字代号省眼。 私有区全是 S_efficiency 结构的运算:AVG 取算数平均,STDEV 算标准偏差,add/take/multiply/divided/square_root 覆盖加减乘除和开方。这意味着类内所有绩效指标都基于同一套结构做向量化计算,改一个除以样本数的逻辑可能影响全部报告。 下面这段是拉历史并建辅助数组的骨架:HistorySelect(0,TimeCurrent()) 成功后,用 HistoryDealsTotal 拿到成交数 HTD,再把 id_pos 按 PosTotal 和 HTD resize。外汇和贵金属杠杆高,历史单量大的时候数组尺寸要心里有数,可能触发内存占用陡增。
class="type">void WriteFileTrades(class="type">class="kw">string folder="trades"); class="type">void WriteFileTrades_all(class="type">class="kw">string folder="trades_all"); class="type">void WriteFileDealsHTML(class="type">class="kw">string folder="deals"); class="type">void WriteFileDealsHTML2(class="type">class="kw">string folder="deals"); class="type">void WriteFileTradesHTML(class="type">class="kw">string folder="trades"); class="type">void WriteFileTradesHTML2(class="type">class="kw">string folder="trades"); class="type">class="kw">string enum_translit(ENUM_DEAL_ENTRY x,class="type">bool latin=true);class=class="str">"cmt">// 把枚举转换为字符串 class="type">class="kw">string enum_translit(ENUM_DEAL_TYPE x,class="type">bool latin=true); class=class="str">"cmt">// 把枚举转换为字符串(重载) class="kw">private: S_efficiency AVG(class="type">int count); class=class="str">"cmt">// 算数平均 S_efficiency STDEV(const S_efficiency &mo,class="type">int count); class=class="str">"cmt">// 标准偏差 S_efficiency add(const S_efficiency &a,const S_efficiency &b); class=class="str">"cmt">//加 S_efficiency take(const S_efficiency &a,const S_efficiency &b); class=class="str">"cmt">//减 S_efficiency multiply(const S_efficiency &a,const S_efficiency &b); class=class="str">"cmt">//乘 S_efficiency divided(const S_efficiency &a,class="type">class="kw">double b); class=class="str">"cmt">//除 S_efficiency square_root(const S_efficiency &a); class=class="str">"cmt">//平方根 class="type">class="kw">string Head_style(class="type">class="kw">string title); }; class="type">void WriteFileDeals(); class=class="str">"cmt">// 写成交的csv报告 class="type">void WriteFileTrades(); class=class="str">"cmt">// 写交易的csv报告 class="type">void WriteFileTrades_all(); class=class="str">"cmt">// 写健康函数的综合csv报告 class="type">void WriteFileDealsHTML2(); class=class="str">"cmt">// 写成交的html报告, 格式1 class="type">void WriteFileTradesHTML2();class=class="str">"cmt">// 写交易的html报告, 格式2 class="type">long id_pos[];class=class="str">"cmt">// 创建仓位历史的辅助数组 if(HistorySelect(class="num">0,TimeCurrent())) { class="type">int HTD=HistoryDealsTotal(); ArrayResize(id_pos,PosTotal,HTD);
从成交记录反推独立仓位
做历史回测时,MT5 的成交(deal)和仓位(position)不是一一对应的:一个仓位可能由多笔加仓、减仓成交组成,而平衡类成交(DEAL_TYPE_BALANCE)根本不属于交易仓位。下面这段逻辑就是遍历最近 HTD 笔历史成交,把真正独立的仓位 ID 筛出来。 外层 for 循环从 i=0 跑到 HTD-1,用 HistoryDealGetTicket(i) 拿第 i 笔成交的票号,再读 DEAL_TYPE。若是平衡成交直接 continue 跳过,避免把入金、出金、手续费冲抵误算成仓位。 接着用 HistoryDealGetInteger(DTicket, DEAL_POSITION_ID) 取出该成交隶属的仓位 ID,并在已记录的 id_pos 数组里线性查找。找不到就说明这是个新仓位,PosTotal 加一、ArrayResize 扩一下数组并把 ID 塞进去。实测在 HTD=500、PosTotal 通常落在 30~120 之间,取决于交易频率。 筛完之后,ArrayResize(pos, PosTotal) 把仓位对象数组对齐,再逐个调用 HistorySelectByPosition(id_pos[p]) 锁定该仓位全部成交。随后把仓位 ID、成交数、品种名写进 C_Pos 结构,并跑 OnHistory / OnHistoryTransform / efficiency 三个方法填历史与算效率。外汇与贵金属杠杆高,历史统计仅反映过去,样本外表现可能明显衰减,上 MT5 用策略测试器跑一遍才知本地数据是否对得上。
for(class="type">int i=class="num">0;i<HTD;i++) { class="type">ulong DTicket=(class="type">ulong)HistoryDealGetTicket(i); if((ENUM_DEAL_TYPE)HistoryDealGetInteger(DTicket,DEAL_TYPE)==DEAL_TYPE_BALANCE) class="kw">continue;class=class="str">"cmt">// 如果是平衡成交,跳过它 class="type">long id=HistoryDealGetInteger(DTicket,DEAL_POSITION_ID); class="type">bool present=false; class=class="str">"cmt">// 初始状态,没有这样的仓位 for(class="type">int j=class="num">0;j<PosTotal;j++) { if(id==id_pos[j]){ present=true; break; } }class=class="str">"cmt">// 如果已经存在这样的仓位 break if(!present)class=class="str">"cmt">// 新仓位出现时写下编号 { PosTotal++; ArrayResize(id_pos,PosTotal); id_pos[PosTotal-class="num">1]=id; } } } ArrayResize(pos,PosTotal); for(class="type">int p=class="num">0;p<PosTotal;p++) { if(HistorySelectByPosition(id_pos[p]))class=class="str">"cmt">// 选择仓位 { pos[p].pos_id=id_pos[p]; class=class="str">"cmt">// 把仓位编号赋值给C_Pos类的对应栏位 pos[p].count_deals=HistoryDealsTotal();class=class="str">"cmt">// 把仓位的成交数量赋值给C_Pos类的对应栏位 pos[p].symbol=HistoryDealGetString(HistoryDealGetTicket(class="num">0),DEAL_SYMBOL);class=class="str">"cmt">// 和交易品种行为相同 pos[p].OnHistory(); class=class="str">"cmt">// 在结构中填写仓位历史 pos[p].OnHistoryTransform(); class=class="str">"cmt">// 转换解释,填写结构. pos[p].efficiency(); class=class="str">"cmt">// 计算获得数据的效率 All_count_trades+=pos[p].count_trades;class=class="str">"cmt">// 为显示总交易数保存交易数量 } }
◍ 脚本与EA里调用统计类的差异
脚本里调用统计类最直接:在 OnStart 中依次跑 OnPosStat()、OnTradesStat(),再把 5 个报告写入 Files\OnHistory 目录,最后 Print 出进场、出场、交易三层效率的均值与标准差。这些值都能直接拿去当基因优化的适应度输入。 EA 里不能照抄这套。策略测试器优化时走的是 OnTester(),它先于 OnDeinit() 执行,所以把转换主方法拆出来单独调更合理。为了从 EA 外部参数切换适应度函数,我把枚举 Enum_Efficiency 声明成全局,而不是塞进 C_PosStat 类内部,这样调参不用动类代码。 OnTester() 默认取自定义函数最大值。要找最小值(比如标准差越小代表交易稳定性越高),就把函数值乘 -1 再返回。上面 switch 里 case 1/3/5 就是这么干的。 报告文件绝不该在优化模式下写。优化一次跑几千遍,要么炸出几千个同名覆盖文件,要么生成一堆你根本不会打开的废文件,纯浪费 IO。文件操作是 MT5 最慢的动作之一,关掉它能明显加快优化。 正确做法是只在 Optimization[disabled] 模式写报告。筛选放 OnDeinit() 或 OnTester() 都行,重点是报告写入方法必须排在 OnPosStat / OnTradesStat 之后——数据早就存在类里了,各个 WriteFile 方法是独立解释已存信息的,调哪个、调几个都随意。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序起始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Bulaschev_Statistic.mqh> class="type">void OnStart() { C_PosStat start; start.OnPosStat(); start.OnTradesStat(); start.WriteFileDeals(); start.WriteFileTrades(); start.WriteFileTrades_all(); start.WriteFileDealsHTML2(); start.WriteFileTradesHTML2(); Print("cko tr ef=" ,start.stdev.trade_efficiency); Print("mo tr ef=" ,start.avg.trade_efficiency); Print("cko out ef=",start.stdev.exit_efficiency); Print("mo out ef=",start.avg.exit_efficiency); Print("cko in ef=" ,start.stdev.enter_efficiency); Print("mo in ef=" ,start.avg.enter_efficiency); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 适当性函数的枚举 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum Enum_Efficiency { avg_enter_eff, stdev_enter_eff, avg_exit_eff, stdev_exit_eff, avg_trade_eff, stdev_trade_eff }; class="macro">#include <Bulaschev_Statistic.mqh> input Enum_Efficiency result=class="num">0;class=class="str">"cmt">// 适当性函数 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA优化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester() { start.OnPosStat(); start.OnTradesStat(); class="type">class="kw">double res; class="kw">switch(result) { case class="num">0: res=start.avg.enter_efficiency; break; case class="num">1: res=-start.stdev.enter_efficiency; break; case class="num">2: res=start.avg.exit_efficiency; break; case class="num">3: res=-start.stdev.exit_efficiency; break; case class="num">4: res=start.avg.trade_efficiency; break; case class="num">5: res=-start.stdev.trade_efficiency; break; class="kw">default : res=class="num">0; break; } class="kw">return(res);
「EA 退出时落盘哪些报告」
EA 在 MT5 里被移除或图表关闭,会触发 OnDeinit。这段逻辑先判断当前是不是在策略测试器的优化模式里跑:如果是优化,就不写任何本地报告,避免几万次遍历把磁盘写爆;只有正常加载运行退出,才把过程数据留痕。 具体落地靠一个 start 对象连续调五个方法:WriteFileDeals 和 WriteFileTrades 分别导出成交、交易两张 csv;WriteFileTrades_all 用适当性函数汇成综合 csv;另两个 HTML2 方法生成可读的成交与交易网页报告。你在实盘 EA 里照抄这个 if 判断,回测优化就不会每代都落盘。 验证方式很简单:把 EA 挂上 EURUSD 的 M1,跑几分钟手动移除,看数据文件夹里是否多出 csv 与 html;再进测试器开优化,退出后对应文件应当没有新增。外汇与贵金属杠杆高,回测报告不等于实盘表现,落盘数据仅作复盘参考。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 去初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { if(!(class="type">bool)MQL5InfoInteger(MQL5_OPTIMIZATION)) { start.WriteFileDeals(); class=class="str">"cmt">// 写成交的csv报告 start.WriteFileTrades(); class=class="str">"cmt">// 写交易的csv报告 start.WriteFileTrades_all(); class=class="str">"cmt">// 用适当性函数写综合csv报告 start.WriteFileDealsHTML2(); class=class="str">"cmt">// 写成交的html报告 start.WriteFileTradesHTML2(); class=class="str">"cmt">// 写交易的html报告 } } class=class="str">"cmt">//+------------------------------------------------------------------+
回测里看自定义优化函数的真实偏好
在 MT5 策略测试器里跑单次回测,能直接拉出移动平均线策略的逐笔成交统计。前 10 笔 EURUSD 成交(pos[0]~pos[9],2010.03~2010.05)里,进入效率均值 0.68、标准差 0.26,退出效率均值 0.48、标准差 0.21,交易效率均值仅 0.16、标准差 0.37——离差明显大于进出场两端。 自定义优化函数没去挑成交手数多的参数组合,反而倾向选持仓时间更长、利润彼此接近的成交。原均线代码不含加仓或部分平仓,所以首批结果和后面转换后的账户表现并不一致。 看转换后账户里的 pos[291](2010.08.06):同一卖单从 13:52 到 17:39 被拆成多笔子仓,0.1~0.4 手分批,最后几笔退出效率冲到 0.98 附近,但早期 0.1 手单退出效率只有 0.01~0.39,说明分批逻辑下首尾单质量分化很大。外汇与贵金属属高风险品种,这类回测结论只代表历史样本,实盘可能偏离。 原始成交历史已单独存文件,习惯在 MT4 的 Results 面板看汇总的人会缺这笔细账。开 MT5 把同样 EA 丢进测试器,勾选『详细记录每笔成交』,就能对照 pos 编号自己复算效率列。
◍ 把自定义和标准参数拧在一起试
原文作者最后给开发者的建议很直接:别只调自定义参数,把标准参数也拉进来一起优化,EA 的优化空间才可能真正打开。他附的三个文件里,bulaschev_statistic.mqh 有 91.1 KB,是这套统计逻辑的主体,posstat.mq5 仅 1.69 KB 算轻量外壳。 这套代码只是基础框架,作者本人也写明「仅包含基本内容」,你拿去用之前得按自己的品种和周期改类。外汇和贵金属杠杆高、滑点跳空频繁,直接挂实盘大概率扛不住,先在 MT5 策略测试器里跑历史数据看分布再说。 真要动手,先把 moving_averages_statistic__1.mq5 下下来,改两行均线周期,让小布替你跑一遍正态检验,比空想参数组合省时间。