使用优化算法即时配置 EA 参数(基础篇)
让 EA 在跑盘时自己调参
传统 EA 把参数写死,行情换 regime 就失效。MetaTrader 5 的测试框架允许在 OnTester 里回写优化结果,使 EA 具备运行时自配置能力。
- 年 9 月 18 日发布的示例工程显示,这类带自优化结构的 EA 在公开测试中被查看 1848 次、评论 24 条,说明社区对「免手动重优化」需求明确。
核心思路分五块:概述、带自优化功能的 EA 结构、指标虚拟化、策略虚拟化、测试功能。前两块决定参数何时重算,后三块决定算出来怎么用。 开 MT5 建个 EA,把参数寻优逻辑挂到 OnTester,比每次手动跑优化器更贴近实盘节奏;外汇与贵金属杠杆高,自优化不代表免风险,信号失效仍可能快速回撤。
◍ EA 里的自我优化到底在调什么
很多交易者把 EA 优化想得太玄,其实在 MT5 里它本质是一个闭环:用历史数据和当下行情去反复拧参数,让策略表现更贴合当前市况。外汇和贵金属杠杆高、波动突发,这种自适应不是锦上添花,而是实盘活下来的基本功。 自我优化至少跑五步:先收数据做统计或机器学习式分析;再定目标,是冲收益还是压回撤;接着丢进优化算法找参数最优解;然后用近期行情回测验证;最后持续监控,行情一变就重调。少一步,优化就可能变成过拟合的自嗨。 落到具体用法,优化算法能干的远不止调均线条数。移动平均周期、止损止盈距离、信号触发间隔,都能用历史数据搜出概率上更优的值;进出场时机、组合里的贵金属与货币对配比、动态仓位和跟踪止损,也都能交给均值-方差或遗传编程去算。 对新手,懂这套原理等于多了个降低试错成本的工具;对老手,它打开的是多资产多周期协同优化的空间。但记住,所有‘更优’都只是历史样本里的概率倾向,实盘仍可能因流动性断裂或黑天鹅失效。
「让 EA 自己跑优化闭环」
一套能自我优化的 EA,最省事的架构是把『真实策略』和『虚拟策略』分开跑。图里的关键不是复杂数学,而是把决策时间点钉在 time now:EA 此刻把优化参数交给管理器,管理器再向优化算法要一组 set,丢进 EA Virt 里从 past 回测到 time now。 EA Virt 是真实 EA 的完整模拟,不碰实盘,只负责在历史区间虚拟成交。管理器按群体大小重复跑 EA Virt,每次回吐一个 ff result——也就是适应度函数值。这个值你随便定:余额、利润因子、数学期望,或者跨多个历史点的累积微分标准都行,它本质是你认定的策略质量尺。 管理器把 ff result 回传给优化算法,算法据此吐下一组 set,循环到停止条件触发,最佳 set 才交回真实 EA,从 time now 跑到 reoptimiz 点再重来。reoptimiz 可以是固定历史柱数,也能是指标跌破阈值的事件。 优化算法对外是黑盒子:它只管『要一组参数—收一份评估—给下一组』,不关心策略逻辑。你换任何交易系统,只要 EA Virt 能回 ff result,这套闭环就能转。外汇与贵金属波动剧烈,这种自优化仍可能过拟合历史,上 MT5 用策略测试器跑通闭环前别直接挂实盘。
把指标塞进 EA 里自己算
在历史数据上跑 EA 做策略验证时,如果依赖指标句柄,每换一组参数就要新建句柄。测试器里不允许删除句柄,参数集一旦有成百上千种组合,内存很可能很快被吃满。 绕开句柄的办法,是把指标计算直接虚拟化进 EA:把标准指标的 OnCalculate 改个名(比如 Calculate),用类封装,把主缓冲区公开、辅助缓冲区私有,再用 Init 传参数。随机振荡指标和 MACD 都能这么改,指数平滑和简单平滑的计算逻辑原样保留,只动缓冲区的分配与初始化。 下面这段随机振荡的虚拟化类声明,把 K/D/Slowing 三个参数收进 Init,ExtMainBuffer 和 ExtSignalBuffer 作为公开缓冲区供 EA 直接读。 别在测试器里死等句柄释放 测试器禁止 DeleteIndicator 类操作,靠句柄遍历参数会卡死在内存上限。类内算指标虽然要多写几十行,但 EURUSD 15 分钟上跑 200 组参数,内存占用可能只有句柄方式的零头。
class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_iStochastic { class="kw">public: class="type">void Init(const class="type">int InpKPeriod, class=class="str">"cmt">// K period const class="type">int InpDPeriod, class=class="str">"cmt">// D period const class="type">int InpSlowing) class=class="str">"cmt">// Slowing { inpKPeriod = InpKPeriod; inpDPeriod = InpDPeriod; inpSlowing = InpSlowing; } class="kw">public: class="type">int Calculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">double &high [], const class="type">class="kw">double &low [], const class="type">class="kw">double &close []); class=class="str">"cmt">//--- indicator buffers class="kw">public: class="type">class="kw">double ExtMainBuffer []; class="kw">public: class="type">class="kw">double ExtSignalBuffer []; class="kw">private: class="type">class="kw">double ExtHighesBuffer []; class="kw">private: class="type">class="kw">double ExtLowesBuffer []; class="kw">private: class="type">int inpKPeriod; class=class="str">"cmt">// K period class="kw">private: class="type">int inpDPeriod; class=class="str">"cmt">// D period class="kw">private: class="type">int inpSlowing; class=class="str">"cmt">// Slowing }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">int C_iStochastic::Calculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">double &high [], const class="type">class="kw">double &low [], const class="type">class="kw">double &close []) { if (rates_total <= inpKPeriod + inpDPeriod + inpSlowing) class="kw">return (class="num">0); ArrayResize(ExtHighesBuffer, rates_total); ArrayResize(ExtLowesBuffer, rates_total); ArrayResize(ExtMainBuffer, rates_total);
◍ 手写随机指标的缓冲与循环逻辑
这段实现把随机指标(%K 与 %D)的计算从头写进自定义缓冲区,没走内置 iStochastic。先给五个数组按 rates_total 扩容量并清零,其中 ExtSignalBuffer 用 ArrayResize 动态跟 K 线总数走,其余四个用 ArrayInitialize 填 0.0,避免首根柱子出现野值。 start 变量的处理是性能关键点:若 prev_calculated 已覆盖,则 start 回退到 prev_calculated-2,只对最后两根重算;否则把前 inpKPeriod-1 根强制置 0。这种写法在 MT5 每 tick 重算时,能把无效循环砍掉绝大多数,EURUSD M5 上历史 5000 根首次加载后,后续 tick 计算量通常只落在尾部 2 根。 %K 的内层循环在 i-inpKPeriod+1 到 i 之间捞出窗口内最低 low 与最高 high,分别写进 ExtLowesBuffer 和 ExtHighesBuffer。随后用 slowing 窗口累加 (close-LOW) 与 (HIGH-LOW),两者相除乘 100 得 %K;若分母为 0 直接给 100.0,对应极端锁板行情。 %D 只是把 %K 近 inpDPeriod 根做朴素移动平均写进 ExtSignalBuffer。整套返回 rates_total,缓冲区交由绘图层映射。外汇与贵金属波动跳空频繁,分母为零的分支在黄金跳空时可能触发,实战中需意识到信号会瞬间贴顶。
ArrayResize(ExtSignalBuffer, rates_total); ArrayInitialize(ExtHighesBuffer, class="num">0.0); ArrayInitialize(ExtLowesBuffer, class="num">0.0); ArrayInitialize(ExtMainBuffer, class="num">0.0); ArrayInitialize(ExtSignalBuffer, class="num">0.0); class="type">int i, k, start; start = inpKPeriod - class="num">1; if (start + class="num">1 < prev_calculated) { start = prev_calculated - class="num">2; Print("start ", start); } else { for (i = class="num">0; i < start; i++) { ExtLowesBuffer [i] = class="num">0.0; ExtHighesBuffer [i] = class="num">0.0; } } class=class="str">"cmt">//--- calculate HighesBuffer[] and ExtHighesBuffer[] for (i = start; i < rates_total && !IsStopped(); i++) { class="type">class="kw">double dmin = class="num">1000000.0; class="type">class="kw">double dmax = -class="num">1000000.0; for (k = i - inpKPeriod + class="num">1; k <= i; k++) { if (dmin > low [k]) dmin = low [k]; if (dmax < high [k]) dmax = high [k]; } ExtLowesBuffer [i] = dmin; ExtHighesBuffer [i] = dmax; } class=class="str">"cmt">//--- %K start = inpKPeriod - class="num">1 + inpSlowing - class="num">1; if (start + class="num">1 < prev_calculated) start = prev_calculated - class="num">2; else { for (i = class="num">0; i < start; i++) ExtMainBuffer [i] = class="num">0.0; } class=class="str">"cmt">//--- main cycle for (i = start; i < rates_total && !IsStopped(); i++) { class="type">class="kw">double sum_low = class="num">0.0; class="type">class="kw">double sum_high = class="num">0.0; for (k = (i - inpSlowing + class="num">1); k <= i; k++) { sum_low += (close [k] - ExtLowesBuffer [k]); sum_high += (ExtHighesBuffer [k] - ExtLowesBuffer [k]); } if (sum_high == class="num">0.0) ExtMainBuffer [i] = class="num">100.0; else ExtMainBuffer [i] = sum_low / sum_high * class="num">100; } class=class="str">"cmt">//--- signal start = inpDPeriod - class="num">1; if (start + class="num">1 < prev_calculated) start = prev_calculated - class="num">2; else { for (i = class="num">0; i < start; i++) ExtSignalBuffer [i] = class="num">0.0; } for (i = start; i < rates_total && !IsStopped(); i++) { class="type">class="kw">double sum = class="num">0.0; for (k = class="num">0; k < inpDPeriod; k++) sum += ExtMainBuffer [i - k]; ExtSignalBuffer [i] = sum / inpDPeriod; } class=class="str">"cmt">//--- OnCalculate done. Return new prev_calculated. class="kw">return (rates_total);
「把 MACD 类塞进自己的指标骨架」
上面这段是 MACD 封装类的头尾切片,重点在 Init 怎么接参数、Calculate 怎么防越界。外汇和贵金属波动快,周期设太短容易被噪音刷掉信号,接入前先想清楚快线慢线差几个档。 Init 公开方法接收三个整型入参:快 EMA 周期、慢 EMA 周期、信号 SMA 周期。内部先把它们存进私有变量,再跑一轮比较,把三者里最大的那个赋给 maxPeriod,后续计算总量不足这个值时直接不跑。 Calculate 一进来就判断 rates_total < maxPeriod 时 return(0),这是实打实的护栏:比如快 12、慢 26、信号 9,maxPeriod 是 26,前 25 根 K 线根本不给 MACD 值。下面四行 ArrayResize 把四个缓冲区拉到 rates_total 长度,ExtMacdBuffer 还顺手 ArrayInitialize 成 0.0,避免残留脏数据。 [CODE]public: void Init (const int InpFastEMA, // Fast EMA period const int InpSlowEMA, // Slow EMA period const int InpSignalSMA) // Signal SMA period { inpFastEMA = InpFastEMA; inpSlowEMA = InpSlowEMA; inpSignalSMA = InpSignalSMA; maxPeriod = InpFastEMA; if (maxPeriod < InpSlowEMA) maxPeriod = InpSlowEMA; if (maxPeriod < InpSignalSMA) maxPeriod = InpSignalSMA; } public: int Calculate (const int rates_total, const int prev_calculated, const double &close []); //--- indicator buffers public: double ExtMacdBuffer []; public: double ExtSignalBuffer []; private: double ExtFastMaBuffer []; private: double ExtSlowMaBuffer []; private: int ExponentialMAOnBuffer (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer []); private: int SimpleMAOnBuffer (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer []); private: int inpFastEMA; // Fast EMA period private: int inpSlowEMA; // Slow EMA period private: int inpSignalSMA; // Signal SMA period private: int maxPeriod; }; int C_iMACD::Calculate (const int rates_total, const int prev_calculated, const double &close []) { if (rates_total < maxPeriod) return (0); ArrayResize (ExtMacdBuffer, rates_total); ArrayResize (ExtSignalBuffer, rates_total); ArrayResize (ExtFastMaBuffer, rates_total); ArrayResize (ExtSlowMaBuffer, rates_total); ArrayInitialize (ExtMacdBuffer, 0.0);[/CODE] 逐行拆一下:Init 的 maxPeriod 比较逻辑决定类的最低数据门槛;Calculate 里 ArrayResize 四次调用每次都按当前 bars 数重分配,实盘切换周期不会漏内存也不会越界。你开 MT5 新建个类把这段贴进去,改改周期参数就能直接测 EURUSD 的 15M 信号延迟。
class="kw">public: class="type">void Init(const class="type">int InpFastEMA, class=class="str">"cmt">// Fast EMA period const class="type">int InpSlowEMA, class=class="str">"cmt">// Slow EMA period const class="type">int InpSignalSMA) class=class="str">"cmt">// Signal SMA period { inpFastEMA = InpFastEMA; inpSlowEMA = InpSlowEMA; inpSignalSMA = InpSignalSMA; maxPeriod = InpFastEMA; if (maxPeriod < InpSlowEMA) maxPeriod = InpSlowEMA; if (maxPeriod < InpSignalSMA) maxPeriod = InpSignalSMA; } class="kw">public: class="type">int Calculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">double &close []); class=class="str">"cmt">//--- indicator buffers class="kw">public: class="type">class="kw">double ExtMacdBuffer []; class="kw">public: class="type">class="kw">double ExtSignalBuffer []; class="kw">private: class="type">class="kw">double ExtFastMaBuffer []; class="kw">private: class="type">class="kw">double ExtSlowMaBuffer []; class="kw">private: class="type">int 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 []); class="kw">private: class="type">int 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="kw">private: class="type">int inpFastEMA; class=class="str">"cmt">// Fast EMA period class="kw">private: class="type">int inpSlowEMA; class=class="str">"cmt">// Slow EMA period class="kw">private: class="type">int inpSignalSMA; class=class="str">"cmt">// Signal SMA period class="kw">private: class="type">int maxPeriod; }; class="type">int C_iMACD::Calculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">double &close []) { if (rates_total < maxPeriod) class="kw">return (class="num">0); ArrayResize(ExtMacdBuffer, rates_total); ArrayResize(ExtSignalBuffer, rates_total); ArrayResize(ExtFastMaBuffer, rates_total); ArrayResize(ExtSlowMaBuffer, rates_total); ArrayInitialize(ExtMacdBuffer, class="num">0.0);