重构经典策略(第十一部分)移动平均线的交叉(二)(基础篇)
用 EURGBP 日线给老策略做基准回测
移动平均线交叉是市面上最容易被滥用的老策略之一,但把它当基准框架反而最省事:社区里多数人捂着自己的规则不肯细说,拿人人都懂的均线交叉做参照,才能横向比出改进空间。 我们在 MT5 里给 EURGBP 日线搭了一套纯经典规则的版本:两条收盘价上的 EMA,周期 20 和 60;随机振荡器用默认 5,3,3、指数平滑、CLOSE_CLOSE 模式;ATR(14) 只用来算止损止盈。买入条件是快线向上穿慢线且随机读数 >80,卖出反过来——快线在下且随机 <20。 回测窗口掐在 2022-01-01 到 2024-06 初。后面会换 AI 模型替掉这些死规则,但训练集只取 2011–2021,和回测彻底隔离,避免偷看未来数据。 先跑通这套传统参数,关键看夏普、胜率、最大利润。等对照组的 AI 版跑完,经典组就是那把尺——新方案把盈利交易占比顶到 55% 以上、夏普也抬了上去,你开 MT5 照这套参数复刻一遍,就能亲手量出「老狗新把戏」的差距。外汇与贵金属高杠杆,回测优不等于实盘稳,参数请自测。
「从均线交叉回测到虚拟编码的思路切换」
先在 MetaEditor 里搭一个最简框架:导入交易类库、定义全局变量、给均线/随机/ATR 建指标句柄,再把交易量、止损这些设为手动输入参数。启动时用专用函数初始化指标并缓存市场数据,退出时释放资源;无持仓就扫信号,信号出现则按 ATR 设止损止盈。 拿这套均线交叉在 EURGBP 日线跑回测,区间 2022-01 至 2024-06,用真实 tick 建模、Forward=false。结果很直白:账户整体亏损,夏普比率 -5.0,交易亏损比例 69.57%,平均亏幅大于平均盈利。对外汇和贵金属这类高杠杆品种来说,延迟信号直接实盘大概率会快速吞金。 均线交叉加随机振荡器已被用烂,人类很难从中抠出优势,但模型不一定。这里引入「虚拟编码」:无序分类变量给每个取值单列,命中置 1 其余 0。比如 MT5 主题红/粉/蓝就建三列,选哪个哪列是 1。若编码成 1/2/3,模型可能瞎学出「2.5 最优」这种虚假区间关系。 把快均线高于慢均线记为状态一、否则状态二;随机指标 >80 状态一、<20 状态二、中间状态三。任一时刻仅一个状态为 1。模型被迫学习「状态切换时目标的平均变化」,接近职业交易者的模糊概率判断,而非工程级精度。 第一步是抓生产环境同款数据,避免不同库算指标有偏差。下面这段 MQL5 就是建框架并定常量的起点,慢均线 60、快均线 20、ATR 14。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EURGBP Stochastic AI.mq5 | class=class="str">"cmt">//| Gamuchirai Zororo Ndawana | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Gamuchirai Zororo Ndawana" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Libraries | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Trade\Trade.mqh> CTrade Trade; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double vol,bid,ask; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Technical indicator handlers | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int slow_ma_handler,fast_ma_handler,stochastic_handler,atr_handler; class="type">class="kw">double slow_ma[],fast_ma[],stochastic[],atr[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constants | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">const class="type">int slow_period = class="num">60; class="kw">const class="type">int fast_period = class="num">20; class="kw">const class="type">int atr_period = class="num">14;
◍ EA 骨架里的参数与生命周期钩子
这段 MT5 代码给出的是一个多指标 EA 的基础框架,重点在输入参数分组与三个系统回调的分工。输入区把仓位和风控拆成两个 group:lot_multiple 默认 5,代表最小合约单位的倍数;atr_multiple 默认 5,用作止损宽度的 ATR 倍数,外汇与贵金属波动大,实盘前务必按品种重设这两个值。 OnInit 只做一件事——调用 setup() 初始化指标并抓数据,返回 INIT_SUCCEEDED 表示加载成功;OnDeinit 则在卸载时释放四个指标句柄(快慢 MA、Stochastic、ATR),不释放可能拖慢终端。 OnTick 的逻辑很直接:先 update() 刷新报价,再用 PositionsTotal()==0 判断无持仓时才跑 find_setup()。也就是说,这个 EA 默认同一时间只盯一个 EURGBP 日线机会。 setup() 里写死了品种字符串 "EURGBP" 和周期 PERIOD_D1,并挂了 5/3/3 的随机指标与 ATR。vol 按 lot_multiple 乘 SYMBOL_VOLUME_MIN 算出,想换品种得连字符串和参数一起改。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| User inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ input group "Money Management" input class="type">int lot_multiple = class="num">5; class=class="str">"cmt">//Lot size input group "Risk Management" input class="type">int atr_multiple = class="num">5; class=class="str">"cmt">//Stop Loss Width class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Setup our technical indicators and fetch market data setup(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- IndicatorRelease(fast_ma_handler); IndicatorRelease(slow_ma_handler); IndicatorRelease(atr_handler); IndicatorRelease(stochastic_handler); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- Fetch updated quotes update(); class=class="str">"cmt">//--- If we have no open positions, check for a setup if(PositionsTotal() == class="num">0) { find_setup(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Setup technical market data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void setup(class="type">void) { class=class="str">"cmt">//--- Setup our indicators slow_ma_handler = iMA("EURGBP",PERIOD_D1,slow_period,class="num">0,MODE_EMA,PRICE_CLOSE); fast_ma_handler = iMA("EURGBP",PERIOD_D1,fast_period,class="num">0,MODE_EMA,PRICE_CLOSE); stochastic_handler = iStochastic("EURGBP",PERIOD_D1,class="num">5,class="num">3,class="num">3,MODE_EMA,STO_CLOSECLOSE); atr_handler = iATR("EURGBP",PERIOD_D1,atr_period); class=class="str">"cmt">//--- Fetch market data vol = lot_multiple * SymbolInfoDouble("EURGBP",SYMBOL_VOLUME_MIN); } class=class="str">"cmt">//+------------------------------------------------------------------+
拉数据和下单信号的底层函数
这段逻辑把 EURGBP 的实时买卖价和四个指标缓冲区一次性刷进内存,避免每次判断都重复调用终端接口。update() 里用 SymbolInfoDouble 抓 bid/ask,再用 CopyBuffer 把 ATR、快慢 MA、Stochastic 的当前值(索引 0)各取 1 根柱,开销极低。 find_setup() 的入场规则很直白:快线在慢线上方且 Stochastic 突破 80,市价买;快线在慢线下方且 Stochastic 跌破 20,市价卖。止损止盈都用 ATR 倍数展开,例如买单止损放在 ask - atr[0]*atr_multiple,盈亏对称。 外汇与贵金属属高杠杆品种,这类反转思路在震荡市可能频繁触发、在趋势市可能连续踏空,实盘前务必在 MT5 策略测试器用 2020 年以来的 EURGBP 小时线跑一遍。把 size 输入设为 100000 仅代表脚本预取数据量上限,不代表任何样本外表现。
class="type">void update(class="type">void) { class=class="str">"cmt">//--- Update our market prices bid = SymbolInfoDouble("EURGBP",SYMBOL_BID); ask = SymbolInfoDouble("EURGBP",SYMBOL_ASK); class=class="str">"cmt">//--- Copy indicator buffers CopyBuffer(atr_handler,class="num">0,class="num">0,class="num">1,atr); CopyBuffer(slow_ma_handler,class="num">0,class="num">0,class="num">1,slow_ma); CopyBuffer(fast_ma_handler,class="num">0,class="num">0,class="num">1,fast_ma); CopyBuffer(stochastic_handler,class="num">0,class="num">0,class="num">1,stochastic); } class="type">void find_setup(class="type">void) { class=class="str">"cmt">//--- Can we buy? if((fast_ma[class="num">0] > slow_ma[class="num">0]) && (stochastic[class="num">0] > class="num">80)) { Trade.Buy(vol,"EURGBP",ask,(ask - (atr[class="num">0] * atr_multiple)),(ask + (atr[class="num">0] * atr_multiple)),"EURGBP"); } class=class="str">"cmt">//--- Can we sell? if((fast_ma[class="num">0] < slow_ma[class="num">0]) && (stochastic[class="num">0] < class="num">20)) { Trade.Sell(vol,"EURGBP",bid,(bid + (atr[class="num">0] * atr_multiple)),(bid - (atr[class="num">0] * atr_multiple)),"EURGBP"); } } input class="type">int size = class="num">100000; class=class="str">"cmt">//How much data should we fetch?
「把多周期指标一次性倒进 CSV」
做价格行为复盘时,最费劲的是把均线、 stochastic 和 ATR 分头抓出来对齐时间轴。下面这段 MT5 脚本在 OnStart 里一次性挂了四个句柄:20/60 周期 EMA 看快慢线交叉,5-3-3 stochastic 抓超买超卖,日线 14 周期 ATR 用来量波动尺度。 句柄拿到后立刻用 CopyBuffer 把数值拉进数组,并全部 ArraySetAsSeries(true) 倒序排列,这样下标 0 就是当前 bar。注意 atr_handler 用的是 PERIOD_D1,意味着即便你在 M15 图表跑脚本,ATR 列填的也是日线级数值,跨周期错位要心里有数。 文件命名直接拼了品种名和指标标签,FileOpen 用 FILE_CSV 加逗号分隔,循环从 size 写到 0,首行先落字段名。跑完去 MQL5/Files 目录下找 'Market Data XXX MA Stoch ATR As Series.csv',Excel 打开就能喂给小布做特征工程。外汇和贵金属杠杆高,跨周期信号仅作概率参考,实盘前务必在策略测试器回测。
class="type">int ma_fast_handler,ma_slow_handler,stoch_handler,atr_handler; class="type">class="kw">double ma_fast[],ma_slow[],stoch[],atr[]; class="type">void OnStart() { class=class="str">"cmt">//--- Load indicator ma_fast_handler = iMA(Symbol(),PERIOD_CURRENT,class="num">20,class="num">0,MODE_EMA,PRICE_CLOSE); ma_slow_handler = iMA(Symbol(),PERIOD_CURRENT,class="num">60,class="num">0,MODE_EMA,PRICE_CLOSE); stoch_handler = iStochastic(Symbol(),PERIOD_CURRENT,class="num">5,class="num">3,class="num">3,MODE_EMA,STO_CLOSECLOSE); atr_handler = iATR(Symbol(),PERIOD_D1,class="num">14); class=class="str">"cmt">//--- Load the indicator values CopyBuffer(ma_fast_handler,class="num">0,class="num">0,size,ma_fast); CopyBuffer(ma_slow_handler,class="num">0,class="num">0,size,ma_slow); CopyBuffer(stoch_handler,class="num">0,class="num">0,size,stoch); CopyBuffer(atr_handler,class="num">0,class="num">0,size,atr); ArraySetAsSeries(ma_fast,true); ArraySetAsSeries(ma_slow,true); ArraySetAsSeries(stoch,true); ArraySetAsSeries(atr,true); class=class="str">"cmt">//--- File name class="type">class="kw">string file_name = "Market Data " + Symbol() +" MA Stoch ATR " + " As Series.csv"; class=class="str">"cmt">//--- Write to file class="type">int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(class="type">int i= size;i>=class="num">0;i--) { if(i == size) { FileWrite(file_handle,"Time","Open","High","Low","Close","MA Fast","MA Slow","Stoch Main","ATR"); } else { FileWrite(file_handle,iTime(Symbol(),PERIOD_CURRENT,i), iOpen(Symbol(),PERIOD_CURRENT,i), iHigh(Symbol(),PERIOD_CURRENT,i), iLow(Symbol(),PERIOD_CURRENT,i),
◍ 把多指标快照落盘到文件
上面这段是数据导出函数的收尾部分,负责把每个 Bar 上取到的收盘价、快慢均线、随机指标与 ATR 一次性写进 CSV 行,随后关闭文件句柄。 iClose(Symbol(),PERIOD_CURRENT,i) 取当前品种当前周期的第 i 根 K 线收盘价,ma_fast[i] 与 ma_slow[i] 是预先算好的快慢均线数组,stoch[i] 和 atr[i] 同理。五个字段按顺序逗号分隔写入,和前面 FileWrite 的表头一一对应。 FileClose(file_handle) 必须执行,否则 MT5 终端里文件会处于占用状态,下次开图可能写不进新数据。外汇与贵金属波动受杠杆和新闻影响大,导出的历史快照仅用于回测参考,实盘信号概率仍会随流动性变化而漂移。
iClose(Symbol(),PERIOD_CURRENT,i), ma_fast[i], ma_slow[i], stoch[i], atr[i] ); } } class=class="str">"cmt">//--- Close the file FileClose(file_handle); } class=class="str">"cmt">//+------------------------------------------------------------------+