使用优化算法即时配置 EA 参数(基础篇)
📘

使用优化算法即时配置 EA 参数(基础篇)

第 1/3 篇

让 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 组参数,内存占用可能只有句柄方式的零头。

MQL5 / C++
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,缓冲区交由绘图层映射。外汇与贵金属波动跳空频繁,分母为零的分支在黄金跳空时可能触发,实战中需意识到信号会瞬间贴顶。

MQL5 / C++
  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 信号延迟。

MQL5 / C++
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);

常见问题

主要调的是入场阈值、止损止盈倍数和指标周期长度,不涉及改逻辑结构,只在预设区间内找更适配当前行情的数值。
自己算能把缓冲和循环逻辑写进EA内部,少一次跨指标调用,回测和实盘延迟更低,也方便做优化闭环。
小布可以读取品种页上的参数漂移和胜率变化,提示你某段行情里EA自调的参数是否已偏离安全区。
因为要缓存前N根K的极值,避免每tick重算全序列;用独立循环填缓冲,主逻辑只取最新值,省算力。
拿同周期MT终端自带的MACD数值逐根比对,误差在浮点精度内就说明骨架没问题,再接优化器跑。