使用优化算法即时配置 EA 参数·进阶篇
(2/3)· 还在手动重跑优化器?这篇把自我优化EA的骨架与虚拟化拆给你看
◍ EMA与信号线的缓冲区算法拆解
MACD 自定义类里,快、慢 EMA 与信号线都先走 ArrayInitialize 把缓冲区清零,避免历史残留值污染首根 K 线的计算。ExtFastMaBuffer、ExtSlowMaBuffer、ExtSignalBuffer 三个数组在 OnCalculate 开头被置为 0.0,这是 MT5 指标重载时容易漏掉的一步。 ExponentialMAOnBuffer 的核心平滑系数是 2.0/(1.0+period)。当 prev_calculated==0 时,前 begin 根强制填 0.0,从 begin 到 period+begin 之间用递推式 buffer[i]=price[i]*smooth_factor+buffer[i-1]*(1.0-smooth_factor) 预热,start_position 定为 period+begin,意味着首根有效 EMA 出现在第 period+begin 根。 信号线走的是 SimpleMAOnBuffer,把 MACD 主线 ExtMacdBuffer 再套一层简单移动平均,周期由 inpSignalSMA 控制。主循环里 i 从 prev_calculated-1 起步,仅重算新增及上一根,这是 MT5 增量计算的标准写法,能省掉重复遍历。 开 MT5 把这段贴进自定义指标类,改 inpFastEMA / inpSlowEMA / inpSignalSMA 三个输入,能在 EURUSD 15 分钟图上直接比对与系统 iMACD 的偏移量;外汇与贵金属杠杆品种波动剧烈,指标仅描述概率倾向,实盘须自担高风险。
ArrayInitialize(ExtSignalBuffer, class="num">0.0); ArrayInitialize(ExtFastMaBuffer, class="num">0.0); ArrayInitialize(ExtSlowMaBuffer, class="num">0.0); ExponentialMAOnBuffer(rates_total, prev_calculated, class="num">0, inpFastEMA, close, ExtFastMaBuffer); ExponentialMAOnBuffer(rates_total, prev_calculated, class="num">0, inpSlowEMA, close, ExtSlowMaBuffer); class="type">int start; if (prev_calculated == class="num">0) start = class="num">0; else start = prev_calculated - class="num">1; class=class="str">"cmt">//--- calculate MACD for (class="type">int i = start; i < rates_total && !IsStopped(); i++) ExtMacdBuffer [i] = ExtFastMaBuffer [i] - ExtSlowMaBuffer [i]; class=class="str">"cmt">//--- calculate Signal SimpleMAOnBuffer(rates_total, prev_calculated, class="num">0, inpSignalSMA, ExtMacdBuffer, ExtSignalBuffer); class="kw">return (rates_total); } class="type">int C_iMACD::ExponentialMAOnBuffer(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">int period, const class="type">class="kw">double& price [],class="type">class="kw">double& buffer []) { if (period <= class="num">1 || period > (rates_total - begin)) class="kw">return (class="num">0); class="type">bool as_series_price = ArrayGetAsSeries(price); class="type">bool as_series_buffer = ArrayGetAsSeries(buffer); ArraySetAsSeries(price, false); ArraySetAsSeries(buffer, false); class="type">int start_position; class="type">class="kw">double smooth_factor = class="num">2.0 / (class="num">1.0 + period); if (prev_calculated == class="num">0) { for (class="type">int i = class="num">0; i < begin; i++) buffer [i] = class="num">0.0; start_position = period + begin; buffer [begin] = price [begin]; for (class="type">int i = begin + class="num">1; i < start_position; i++) buffer [i] = price [i] * smooth_factor + buffer [i - class="num">1] * (class="num">1.0 - smooth_factor); } else start_position = prev_calculated - class="num">1; for (class="type">int i = start_position; i < rates_total; i++) buffer [i] = price [i] * smooth_factor + buffer [i - class="num">1] * (class="num">1.0 - smooth_factor); ArraySetAsSeries(price, as_series_price); ArraySetAsSeries(buffer, as_series_buffer); class="kw">return (rates_total); } class="type">int C_iMACD::SimpleMAOnBuffer(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">int period, const class="type">class="kw">double& price [],class="type">class="kw">double& buffer []) { class=class="str">"cmt">//--- check period
用递推算均线比循环累加更省
这段自定义指标的计算核心,走的是递推思路而不是每根 K 线都从头累加。首次加载时(prev_calculated==0),它把前 period+begin 根之前的缓冲区填 0,再对 begin 到 start_position 的收盘价做一次求和取平均,作为第一个有效值。 后续每根新柱只做一步:buffer[i] = buffer[i-1] + (price[i] - price[i-period]) / period。也就是说,只用一次加减除就更新了均线,时间复杂度从 O(n·period) 降到 O(n),在 period 设到 200 以上、品种刷新频繁时,CPU 占用差异可能肉眼可感。 数组方向是个暗坑。代码先 ArrayGetAsSeries 存下 price 和 buffer 原本的序列方向,统一设成 false(即下标 0 是最旧数据),算完再用 ArraySetAsSeries 还原。若你抄这段代码却漏掉还原,调用方按时间序列取数就会整体错位。
| 边界判断 if(period<=1 | period>(rates_total-begin)) return(0) 直接拦掉非法周期和样本不足,避免在少于 begin+period 根柱时写出越界。开 MT5 把这段塞进自定义指标 OnCalculate,设 period=50、begin=0,加载到 XAUUSD 的 M5 上即可验证递推值与内置 SMA 是否重合。外汇与贵金属杠杆高,指标仅作参考,实际信号失效概率不低。 |
|---|
if (period <= class="num">1 || period > (rates_total - begin)) class="kw">return (class="num">0); class=class="str">"cmt">//--- save as_series flags class="type">bool as_series_price = ArrayGetAsSeries(price); class="type">bool as_series_buffer = ArrayGetAsSeries(buffer); ArraySetAsSeries(price, false); ArraySetAsSeries(buffer, false); class=class="str">"cmt">//--- calculate start position class="type">int start_position; if (prev_calculated == class="num">0) class=class="str">"cmt">// first calculation or number of bars was changed { class=class="str">"cmt">//--- set empty value for first bars start_position = period + begin; for (class="type">int i = class="num">0; i < start_position - class="num">1; i++) buffer [i] = class="num">0.0; class=class="str">"cmt">//--- calculate first visible value class="type">class="kw">double first_value = class="num">0; for (class="type">int i = begin; i < start_position; i++) first_value += price [i]; buffer [start_position - class="num">1] = first_value / period; } else start_position = prev_calculated - class="num">1; class=class="str">"cmt">//--- main loop for (class="type">int i = start_position; i < rates_total; i++) buffer [i] = buffer [i - class="num">1] + (price [i] - price [i - period]) / period; class=class="str">"cmt">//--- restore as_series flags ArraySetAsSeries(price, as_series_price); ArraySetAsSeries(buffer, as_series_buffer); class=class="str">"cmt">//--- class="kw">return (rates_total); }
「把随机EA改造成自优化虚拟策略」
| 在 EA 里做策略虚拟化,核心是把实盘逻辑复制一份到历史回测函数里,让优化算法能离线算适应度。读者提供的随机振荡 EA 示例里,关键优化参数是 InpKPeriod_P 和 InpUpperLevel_P,它们以字符串声明复合值:默认 18、起始 9、步长 3、终止 24(上限水平则是 96 | 88 | 2 | 98)。这种写法让 OnInit 时能把字符串拆进 Set、Range_Min、Range_Step、Range_Max 四个数组。 |
|---|
OnTick 中插入的 Optimize 函数充当调度器,流程和标准群体优化算法测试脚本一致:初始化算法、准备群体、取参数组、算适应度、更新最优、取最佳解。VirtualStrategy 则在历史数据上跑策略,载入 rates 数组后算 Stochastic,若指标失败返回 -DBL_MAX,无成交也返回 -DBL_MAX,否则返回最终余额作为适应度。 注意虚拟策略要和主 EA 执行节奏对齐:示例以柱线开盘价交易,对应主代码的开盘控制;若原逻辑跑在每 tick,就要下载 tick 历史并改写 VirtualStrategy。接 ESG 算法时,只需在 Optimize 里声明对象并配边界,搜索算法本身是裸逻辑、不含去重加速,迭代耗时会偏多。 外汇与贵金属自带高杠杆风险,自优化回测收益不代表实盘概率,参数过拟合可能让 live 表现明显衰减。
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="preprocessor">class="macro">#class="kw">import </span><span class="class="type">class="kw">string">"\Market\AO Core.ex5"</span> <span class="keyword">class="type">bool</span> Init(<span class="keyword">class="type">int</span> colonySize, <span class="keyword">class="type">class="kw">double</span> &range_min [], <span class="keyword">class="type">class="kw">double</span> &range_max [], <span class="keyword">class="type">class="kw">double</span> &range_step []); <span class="comment">class=class="str">"cmt">//------------------------------------------------------------------------------</span> <span class="keyword">class="type">void</span> Preparation (); <span class="keyword">class="type">void</span> GetVariantCalc(<span class="keyword">class="type">class="kw">double</span> &variant [], <span class="keyword">class="type">int</span> pos); <span class="keyword">class="type">void</span> SetFitness (<span class="keyword">class="type">class="kw">double</span> value, <span class="keyword">class="type">int</span> pos); <span class="keyword">class="type">void</span> Revision (); <span class="comment">class=class="str">"cmt">//------------------------------------------------------------------------------</span> <span class="keyword">class="type">void</span> GetVariant (<span class="keyword">class="type">class="kw">double</span> &variant [], <span class="keyword">class="type">int</span> pos); <span class="keyword">class="type">class="kw">double</span> GetFitness (<span class="keyword">class="type">int</span> pos); <span class="preprocessor">class="macro">#class="kw">import </span><span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="preprocessor">class="macro">#include </span><Trade\Trade.mqh> <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"cStochastic.mqh"</span> <span class="keyword">input</span> group <span class="class="type">class="kw">string">"==== GENERAL ===="</span>; <span class="keyword">sinput</span> <span class="keyword">class="type">long</span> InpMagicNumber = <span class="number">class="num">132516</span>; <span class="comment">class=class="str">"cmt">//Magic Number</span> <span class="keyword">sinput</span> <span class="keyword">class="type">class="kw">double</span> InpLotSize = <span class="number">class="num">0.01</span>; <span class="comment">class=class="str">"cmt">//Lots</span> <span class="keyword">input</span> group <span class="class="type">class="kw">string">"==== Trading ===="</span>; <span class="keyword">input</span> <span class="keyword">class="type">int</span> InpStopLoss = <span class="number">class="num">1450</span>; <span class="comment">class=class="str">"cmt">//Stoploss</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> InpTakeProfit = <span class="number">class="num">1200</span>; <span class="comment">class=class="str">"cmt">//Takeprofit</span> <span class="keyword">input</span> group <span class="class="type">class="kw">string">"==== Stochastic ==|value|start|step|end|=="</span>; <span class="keyword">input</span> <span class="keyword">class="type">class="kw">string</span> InpKPeriod_P = <span class="class="type">class="kw">string">"<span style="background-class="type">class="kw">color:rgb(class="num">119, class="num">183, class="num">83);">class="num">18</span>|<span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">235, class="num">85);">class="num">9</span>|<span style="background-class="type">class="kw">color:rgb(class="num">193, class="num">115, class="num">176);">class="num">3</span>|<span style="background-class="type">class="kw">color:rgb(class="num">164, class="num">192, class="num">228);">class="num">24</span>"</span>; <span class="comment">class=class="str">"cmt">//STO K period : it is necessary to optimize</span>
◍ 把参数塞进数组留给自优化跑
这段初始化逻辑干的事很直接:把 Stochastic 的上轨阈值和 K 周期从输入字符串拆成四个数值(基准值、最小边界、步长、最大边界),分别填进 Set、Range_Min、Range_Step、Range_Max 这四个长度为 2 的 double 数组。索引 0 对应 K 周期,索引 1 对应上轨水平,拆不出来正好 4 段就直接 INIT_FAILED 退出。
| 注意 InpUpperLevel_P 的默认值 "96 | 88 | 2 | 98" 意味着上轨基准 96、搜索下限 88、步长 2、上限 98,这组区间在 EURUSD 这类品种上通常要重优化,原文也标注了 it is necessary to optimize。 |
|---|
自优化开关由 SelfOptimization 控制,开启后 EA 会用 18000 根历史 bar 做初始优化,每走过 1440 根 bar 触发一次重优化;种群规模 50、历史跑 10000 次、点差按 10 点固定计入。外汇与贵金属杠杆高,这类固定点差假设和重优化频率在实盘滑点下可能偏离回测。 OnInit 里先取了 SYMBOL_TRADE_TICK_SIZE 存进 TickSize,后面下单计算止损距离会用到;数组用 ArrayResize 显式开长度 2,避免越界。
input class="type">class="kw">string InpUpperLevel_P = "class="num">96|class="num">88|class="num">2|class="num">98"; class=class="str">"cmt">//STO upper level: it is necessary to optimize input group "====Self-optimization===="; sinput class="type">bool SelfOptimization = true; sinput class="type">int InpBarsOptimize = class="num">18000; class=class="str">"cmt">//Number of bars in the history for optimization sinput class="type">int InpBarsReOptimize = class="num">1440; class=class="str">"cmt">//After how many bars, EA will reoptimize sinput class="type">int InpPopSize = class="num">50; class=class="str">"cmt">//Population size sinput class="type">int NumberFFlaunches = class="num">10000; class=class="str">"cmt">//Number of runs in the history during optimization sinput class="type">int Spread = class="num">10; class=class="str">"cmt">//Spread class="type">MqlTick Tick; CTrade Trade; C_iStochastic IStoch; class="type">class="kw">double Set []; class="type">class="kw">double Range_Min []; class="type">class="kw">double Range_Step []; class="type">class="kw">double Range_Max []; class="type">class="kw">double TickSize = class="num">0.0; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">int OnInit() { TickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); ArrayResize(Set, class="num">2); ArrayResize(Range_Min, class="num">2); ArrayResize(Range_Step, class="num">2); ArrayResize(Range_Max, class="num">2); class="type">class="kw">string result []; if (StringSplit(InpKPeriod_P, StringGetCharacter("|", class="num">0), result) != class="num">4) class="kw">return INIT_FAILED; Set [class="num">0] = (class="type">class="kw">double)StringToInteger(result [class="num">0]); Range_Min [class="num">0] = (class="type">class="kw">double)StringToInteger(result [class="num">1]); Range_Step [class="num">0] = (class="type">class="kw">double)StringToInteger(result [class="num">2]); Range_Max [class="num">0] = (class="type">class="kw">double)StringToInteger(result [class="num">3]); if (StringSplit(InpUpperLevel_P, StringGetCharacter("|", class="num">0), result) != class="num">4) class="kw">return INIT_FAILED; Set [class="num">1] = (class="type">class="kw">double)StringToInteger(result [class="num">0]); Range_Min [class="num">1] = (class="type">class="kw">double)StringToInteger(result [class="num">1]); Range_Step [class="num">1] = (class="type">class="kw">double)StringToInteger(result [class="num">2]);
把自优化塞进 OnTick 的节奏
这段逻辑把「自适应重优化」直接挂在新 K 线判定之后。IsNewBar() 为假就直接 return,保证一根bar只跑一次主逻辑,避免 tick 级重复计算拖慢 MT5 实盘。 SelfOptimization 开关打开时,用静态变量 LastOptimizeTime 记录上次优化时间,并与 iTime(_Symbol, PERIOD_CURRENT, InpBarsReOptimize) 对比。当 LastOptimizeTime 小于等于回看 N 根 bar 的时间戳,就触发 Optimize(),把旧参数组 Set 和边界数组 Range_Min/Step/Max 丢进去,顺带把点差按 SYMBOL_TRADE_TICK_SIZE 折算成绝对价格传入。 优化完成后会 ArrayPrint 新旧 Set,并重新 IStoch.Init((int)Set[0],1,3) 让随机指标用新周期参数。注意 Set[0] 同时是随机指标 K 周期,也是 CopyRates 索要的 bar 数基准:代码里取 Set[0]+1+3+1 根,确保算 Stochastic 不越界。 外汇与贵金属波动剧烈,这种自优化若在过小 InpBarsReOptimize 上跑,可能频繁重写参数导致过拟合,开 MT5 后建议先把重优化间隔调到 200 根以上观察稳定性。
Range_Max [class="num">1] = (class="type">class="kw">double)StringToInteger(result [class="num">3]); IStoch.Init((class="type">int)Set [class="num">0], class="num">1, class="num">3); class=class="str">"cmt">// set magicnumber to trade object Trade.SetExpertMagicNumber(InpMagicNumber); class=class="str">"cmt">//--- class="kw">return (INIT_SUCCEEDED); } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void OnTick() { class=class="str">"cmt">//---------------------------------------------------------------------------- if (!IsNewBar()) { class="kw">return; } class=class="str">"cmt">//---------------------------------------------------------------------------- if (SelfOptimization) { class=class="str">"cmt">//-------------------------------------------------------------------------- class="kw">static class="type">class="kw">datetime LastOptimizeTime = class="num">0; class="type">class="kw">datetime timeNow = iTime(_Symbol, PERIOD_CURRENT, class="num">0); class="type">class="kw">datetime timeReop = iTime(_Symbol, PERIOD_CURRENT, InpBarsReOptimize); if (LastOptimizeTime <= timeReop) { LastOptimizeTime = timeNow; Print("-------------------Start of optimization----------------------"); Print("Old set:"); ArrayPrint(Set); Optimize(Set, Range_Min, Range_Step, Range_Max, InpBarsOptimize, InpPopSize, NumberFFlaunches, Spread * SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE)); Print("New set:"); ArrayPrint(Set); IStoch.Init((class="type">int)Set [class="num">0], class="num">1, class="num">3); } } class=class="str">"cmt">//---------------------------------------------------------------------------- if (!SymbolInfoTick(_Symbol, Tick)) { Print("Failed to get current symbol tick"); class="kw">return; } class=class="str">"cmt">//data preparation------------------------------------------------------------ class="type">MqlRates rates []; class="type">int dataCount = CopyRates(_Symbol, PERIOD_CURRENT, class="num">0, (class="type">int)Set [class="num">0] + class="num">1 + class="num">3 + class="num">1, rates); if (dataCount == -class="num">1) { Print("Data get error"); class="kw">return; } class="type">class="kw">double hi []; class="type">class="kw">double lo []; class="type">class="kw">double cl []; ArrayResize(hi, dataCount); ArrayResize(lo, dataCount); ArrayResize(cl, dataCount); for (class="type">int i = class="num">0; i < dataCount; i++) {
「把随机指标拐点接进开仓逻辑」
这段把前面算好的 Stochastic 主缓冲接进了实盘判定:取倒数第二根与第三根柱的 ExtMainBuffer 值,分别存为 buff0、buff1,用来识别指标从超卖/超买区穿出的最新两根 K 线拐点。 若当前无多单(cntBuy==0),且 buff1 低于 100 减去 Set[1] 阈值、buff0 向上突破该阈值,则先平掉空单(ClosePositions(2)),以 Tick.bid 减止损点数、加止盈点数算出 sl/tp,再市价开多。反向同理:无空单且指标从超买区下穿 Set[1] 时平多开空,外汇与贵金属杠杆品种此处滑点与扩散成本可能显著侵蚀回测收益。 末尾的 Optimize 函数是遗传/火焰优化入口:用传入的 range_min、range_step、range_max 做参数边界,按 inpPopSize 与 numberFFlaunches 推算 epochCount,再调 Init 初始化种群。你在 MT5 策略测试器里把 inpPopSize 设成 50、numberFFlaunches 设成 500,相当于跑 10 代,可直观看 Set[1] 阈值寻优后的夏普变化。
hi [i] = rates [i].high; lo [i] = rates [i].low; cl [i] = rates [i].close; } class="type">int calc = IStoch.Calculate(dataCount, class="num">0, hi, lo, cl); if (calc <= class="num">0) class="kw">return; class="type">class="kw">double buff0 = IStoch.ExtMainBuffer [ArraySize(IStoch.ExtMainBuffer) - class="num">2]; class="type">class="kw">double buff1 = IStoch.ExtMainBuffer [ArraySize(IStoch.ExtMainBuffer) - class="num">3]; class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// count open positions class="type">int cntBuy, cntSell; if (!CountOpenPositions(cntBuy, cntSell)) { Print("Failed to count open positions"); class="kw">return; } class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// check for buy if (cntBuy == class="num">0 && buff1 <= (class="num">100 - (class="type">int)Set [class="num">1]) && buff0 > (class="num">100 - (class="type">int)Set [class="num">1])) { ClosePositions(class="num">2); class="type">class="kw">double sl = NP(Tick.bid - InpStopLoss * TickSize); class="type">class="kw">double tp = NP(Tick.bid + InpTakeProfit * TickSize); Trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, InpLotSize, Tick.ask, sl, tp, "Stochastic EA"); } class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// check for sell if (cntSell == class="num">0 && buff1 >= (class="type">int)Set [class="num">1] && buff0 < (class="type">int)Set [class="num">1]) { ClosePositions(class="num">1); class="type">class="kw">double sl = NP(Tick.ask + InpStopLoss * TickSize); class="type">class="kw">double tp = NP(Tick.ask - InpTakeProfit * TickSize); Trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, InpLotSize, Tick.bid, sl, tp, "Stochastic EA"); } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void Optimize(class="type">class="kw">double &set [], class="type">class="kw">double &range_min [], class="type">class="kw">double &range_step [], class="type">class="kw">double &range_max [], const class="type">int inpBarsOptimize, const class="type">int inpPopSize, const class="type">int numberFFlaunches, const class="type">class="kw">double spread) { class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">class="kw">double parametersSet []; ArrayResize(parametersSet, ArraySize(set)); class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">int epochCount = numberFFlaunches / inpPopSize; Init(inpPopSize, range_min, range_max, range_step); class=class="str">"cmt">// Optimization-------------------------------------------------------------