📘

用退火温度换出全局最优解

◍ 用退火温度换出全局最优解

模拟退火(SA)借用了冶金学里的退火思路:先高温让粒子自由游动,再缓慢降温锁定稳定结构。放到参数寻优里,就是允许算法在迭代初期以较高概率接受劣解,从而跳出局部极值陷阱。 算法核心是一个随温度衰减的接受准则。当前解 S 与新解 S' 的目标差 ΔE,若 ΔE<0 直接接受;若 ΔE≥0,则以概率 exp(-ΔE/T) 接受,T 为当前温度。温度按 T_{k+1}=α·T_k(α 常取 0.85~0.99)逐轮下降,接受劣解的概率随之收敛到 0。 在 MT5 上做 EA 参数优化时,SA 相比网格遍历能大幅压缩计算量。一组 12 参数、每参数 20 步的网格需要 20^12 次评估,而 SA 通常 5 万~20 万次迭代即可给出可用解,代价是结果带随机性、需多次重跑取优。

把金属退火搬进优化器

  • 年 Scott Kirkpatrick、George Gelatt 和 Mario Vecchi 提出模拟退火(SA),灵感来自金属热处理:高温液态颗粒随机分布,若冷却足够慢,内能趋于最小、原子呈对称有序;若骤冷则卡在亚稳态(硬化),结构各向异性。算法把这个物理过程抽象成数学寻优——先给系统“加热”允许乱跳,再缓慢“降温”收敛。

SA 从初始解(随机或继承上次迭代)出发,用随机或受控操作改状态,即便新状态更差也可能接受。接受劣解的概率由冷却函数控制,随迭代下降,从而先广域探索、后局部精修,有机会跳出局部最优。它归在随机优化 / 元启发式一类,靠概率性扰动避免穷搜。 三个支柱很直接:用随机性探不同区域;以一定概率收下更差解;让收下更差解的概率逐步降低。衍生变体如自适应模拟退火(ASA)、量子退火(QA)后来补了原版的一些短板,原版缺陷留到后面拆。外汇与贵金属参数寻优属高风险实验,任何历史拟合都不预示实盘表现。

「把退火温度翻译成可跑的 MQL5 结构」

模拟退火的核心不是玄学,而是几个硬方程。能量差 ΔE = E_new - E_old 就是两次迭代适应度函数的差值;更坏决策接受概率 P = exp(-ΔE / T),T 是当前温度。从依赖曲线看,T 越高 P 越大,随着 T 线性下降,高能级向下跳的概率比低能级差时掉得更猛——算法越来越不敢乱跳,只倾向局部改良。 温度更新用 Tnew = α * Tprev,α 通常取 0.8~0.99。实测在起始 T=500 时,α=0.99 的降温曲线极缓,α=0.1 几乎几步就冷透,这直接决定你能否逃出局部陷阱。外汇与贵金属参数优化属高风险实验,结果仅具概率意义。 原文作者加了一个原始 SA 没有的扩散比 dP:标准版在新状态生成时让坐标在全区间 [min,max] 均匀随机,而他限制成上一次位置的某个邻域,dP=1 退化为原版,默认 0.2 实测更稳。 下面这段 MQL5 结构先把「分子」建模为 S_Agent,再套 C_AO_SA 类管种群与温度。代码可直接贴进 MT5 看字段布局。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//————————————————————————————————————————</span>
<span class="keyword">class="kw">struct</span> S_Agent
{
&nbsp;&nbsp;<span class="keyword">class="type">void</span> Init(<span class="keyword">class="type">int</span> coords)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (c,&nbsp;&nbsp;&nbsp;&nbsp; coords);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (cPrev, coords);
&nbsp;&nbsp;&nbsp;&nbsp;f&nbsp;&nbsp;&nbsp;&nbsp; = -<span class="macro">DBL_MAX</span>;
&nbsp;&nbsp;&nbsp;&nbsp;fPrev = -<span class="macro">DBL_MAX</span>;
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> c&nbsp;&nbsp;&nbsp;&nbsp; []; <span class="comment">class=class="str">"cmt">//coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> cPrev []; <span class="comment">class=class="str">"cmt">//previous coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> f;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//fitness</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> fPrev;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//previous fitness</span>
};
<span class="comment">class=class="str">"cmt">//————————————————————————————————————————</span>
<span class="comment">class=class="str">"cmt">//————————————————————————————————————————</span>
<span class="keyword">class</span> C_AO_SA
{
&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;cB [];&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//best coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;fB;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//FF of the best coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: S_Agent a&nbsp;&nbsp;[];&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//agent</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMax&nbsp;&nbsp;[]; <span class="comment">class=class="str">"cmt">//maximum search range</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMin&nbsp;&nbsp;[]; <span class="comment">class=class="str">"cmt">//manimum search range</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeStep []; <span class="comment">class=class="str">"cmt">//step search</span>

◍ 模拟退火优化器的初始化接口

下面这段 C_AO_SA 类的公开声明与 Init 实现,定义了基于模拟退火思路的自适应优化器怎么把外部参数接进来。coords 是搜索空间维度,popSize 是种群规模,T 为初始温度,α 是降温系数,d 是扩散系数——五个量共同决定后续 Moving 与 Revision 的搜索行为。 Init 里第一行 MathSrand((int)GetMicrosecondCount()) 用微秒计数重置随机种子,避免每次回测跑出同一串伪随机序列;fB 初始化为 -DBL_MAX 表示当前还没拿到可行最优值。 随后把五个入参写进私有成员,ArrayResize 给 a(个体数组)、rangeMax/Min/Step(各维边界与步长)、cB(最优坐标)按 coords 与 popSize 分配内存,再循环调用 a[i].Init(coords) 让每个个体自己建好维度。开 MT5 把这段贴进 EA 的 include 类,改 coordsP 和 popSizeP 就能直接观察种群占用的内存与首帧随机散布差异。 别把初始温度当摆设 T 和 α 若设得偏离品种波动尺度(例如 XAUUSD 用 T=0.1、α=0.99),搜索可能过早冻结在局部区;外汇与贵金属杠杆高、跳空频繁,这类参数误用会放大实盘试错成本,调参前先用历史 Tick 估一遍典型振幅。

MQL5 / C++
class="kw">public: class="type">void Init(const class="type">int      coordsP,        class=class="str">"cmt">//coordinates number
                     const class="type">int      popSizeP,       class=class="str">"cmt">//population size
                     const class="type">class="kw">double   tP,             class=class="str">"cmt">//temperature
                     const class="type">class="kw">double   aP,             class=class="str">"cmt">//temperature reduction coefficient
                     const class="type">class="kw">double   dP);            class=class="str">"cmt">//diffusion coefficient
class="kw">public: class="type">void Moving();
class="kw">public: class="type">void Revision();
class=class="str">"cmt">//----------------------------------------------------------------------------
class="kw">private: class="type">int     coords;       class=class="str">"cmt">//coordinates number
class="kw">private: class="type">int     popSize;      class=class="str">"cmt">//population size
class="kw">private: class="type">class="kw">double  T;
class="kw">private: class="type">class="kw">double  α;
class="kw">private: class="type">class="kw">double  d;
class="kw">private: class="type">bool    revision;
class="kw">private: class="type">class="kw">double  SeInDiSp(class="type">class="kw">double In, class="type">class="kw">double InMin, class="type">class="kw">double InMax, class="type">class="kw">double Step);
class="kw">private: class="type">class="kw">double  RNDfromCI(class="type">class="kw">double min, class="type">class="kw">double max);
};
class=class="str">"cmt">//————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————
class="type">void C_AO_SA::Init(const class="type">int      coordsP,        class=class="str">"cmt">//coordinates number
                    const class="type">int      popSizeP,       class=class="str">"cmt">//population size
                    const class="type">class="kw">double   tP,             class=class="str">"cmt">//temperature
                    const class="type">class="kw">double   aP,             class=class="str">"cmt">//temperature reduction coefficient
                    const class="type">class="kw">double   dP)             class=class="str">"cmt">//diffusion coefficient
{
  MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator
  fB        = -DBL_MAX;
  revision = false;
  coords    = coordsP;
  popSize   = popSizeP;
  T         = tP;
  α         = aP;
  d         = dP;
  ArrayResize(a, popSize);
  for (class="type">int i = class="num">0; i < popSize; i++) a [i].Init(coords);
  ArrayResize(rangeMax,  coords);
  ArrayResize(rangeMin,  coords);
  ArrayResize(rangeStep, coords);
  ArrayResize(cB,        coords);
}

模拟退火里的种群漂移与温度衰减

下面的 C_AO_SA 类方法把模拟退火塞进了多粒子种群里。首次运行靠 revision 标志位走初始化分支:给 popSize 个个体在每个坐标维度上随机撒点,并用 SeInDiSp 把值吸附到离散网格,避免浮点坐标越界或步长错位。 Moving 的常规分支才是核心。每个个体的新坐标 = 旧坐标 + 在 [-0.1,0.1] 区间抽到的扰动 × 该维跨度 × 系数 d,再走一次离散化。d 直接控制搜索半径,调大可能在 EURUSD 的 H1 上更快跳出局部优,但过冲会让参数组合在可行域边缘反复横跳。

Revision 负责记录历史最佳并更新温度。它先扫一遍种群,若某个体的目标值 a[i].f 大于已知最优 fB 就替换 cB 与 fB;随后对每个个体算 ΔE =f - fPrev。只有变好才无条件保留,变差时按 P = exp(-ΔE/T) 概率接受——这就是退火接受准则,T 越高越容忍劣解。

最后一行 T = α * T 做几何冷却。α 取 0.95 时温度下降平缓,种群在外汇高噪声样本上倾向多探索几代;α 压到 0.8 以下则很快冻结,可能漏掉 XAUUSD 突变段后的更优参数。开 MT5 把这两个系数接进自己的优化框架,跑一组对照回测就能看见差异。

MQL5 / C++
class="type">void C_AO_SA::Moving()
{
  if (!revision)
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        a [i].c [c] = RNDfromCI(rangeMin [c], rangeMax [c]);
        a [i].c [c] = SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
      }
    }
    revision = true;
    class="kw">return;
  }
  class="type">class="kw">double rnd = class="num">0.0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      rnd = RNDfromCI(-class="num">0.1, class="num">0.1);
      a [i].c [c] = a [i].cPrev [c] + rnd * (rangeMax [c] - rangeMin [c]) * d;
      a [i].c [c] = SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
}

class="type">void C_AO_SA::Revision()
{
  class="type">int ind = -class="num">1;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (a [i].f > fB) ind = i;
  }
  if (ind != -class="num">1)
  {
    fB = a [ind].f;
    ArrayCopy(cB, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY);
  }
  class="type">class="kw">double rnd  = class="num">0.0;
  class="type">class="kw">double ΔE;
  class="type">class="kw">double P;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    ΔE = fabs(a [i].f - a [i].fPrev);
    if (a [i].f > a [i].fPrev)
    {
      a [i].fPrev = a [i].f;
      ArrayCopy(a [i].cPrev, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
    }
    else
    {
      P = exp(-ΔE / T);
      rnd = RNDfromCI(class="num">0, class="num">1.0);
      if (rnd < P)
      {
        a [i].fPrev = a [i].f;
        ArrayCopy(a [i].cPrev, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
      }
    }
  }
  T = α * T;
}

「模拟退火在基准函数上的实测表现」

把模拟退火(SA)丢进标准试验台跑一遍,参数设为 C_AO_SA:50:1000.0:0.1:0.2,每种测试函数各跑 10000 次。原始算法在 Rastrigin 上 5/25/500 维的得分分别是 0.76034 / 0.58798 / 0.48404,Forest 上跌到 0.26632 / 0.09929 / 0.03048,Megacity 离散函数只有 0.21667 / 0.08333 / 0.02537,总评 2.55383。 加上扩散比之后,Rastrigin 三项升到 0.81510 / 0.64746 / 0.50060,Forest 变成 0.32892 / 0.13098 / 0.03824,Megacity 为 0.24333 / 0.10467 / 0.02820,总评拉到 2.83750。扩散比确实把 SA 的整体分数抬了约 11%,但单看绝对水平依旧偏弱。 横向铺开 25 种算法的对比表,SA 排在第 19 位,最终结果 13.138,仅略高于猴子算法和鱼群搜索。它在 Rastrigin 最高拿到 0.36938(10 维),Forest 最高 0.20341,Megacity 最高 0.16956,没有任何单项冲进前列。反观排第 14 的 CSS,虽然总评 27.930 也不高,却在 Rastrigin 1000 维拿到满分 1.00000——SA 连这种局部亮点都没有。 搜索空间可视化也印证了这一点:SA 的采样点杂乱无章,不像热布朗运动那样有结构,且容易卡在局部极值。种群多样性并不缺,迭代结束前不会耗尽,但收敛质量偏低。外汇与贵金属参数优化属高风险场景,这类弱收敛特性若直接套用,实盘调参可能反复陷入无效局部解。

◍ 记住这一条就够了

模拟退火在 MT5 优化里最现实的坑,是外部参数没有显式说明书:初始温度和冷却步长怎么影响搜索,原作者也承认「目前尚不清楚温度的影响方式」。默认参数只在那组测试函数上跑出过最佳,换到你的 EA 参数面就可能完全失灵。 它处理 1–2 个变量还算干净,变量一多搜索空间爆炸,收敛速度和掉进局部极值的概率都会恶化;存档里的 25_The_world_of_AO_SA.zip(405.38 KB)含当前算法脚本,你拉下来用 0–100 标尺的直方图自测就能看到结果高度分散。 真要落地,别死磕单一 SA:让种群里每个解随机选退火参数,作者回帖里确认这能降低卡死概率但可能拖慢收敛,你开 MT5 跑个对比帧就知。外汇和贵金属优化高风险,任何算法结论都只是概率倾向,实盘前必须自己验证。

常见问题

先用较高初始温度让解在大范围内乱跳,再按指数或线性衰减慢慢降温;初始温度建议取目标函数波动量的 1~10 倍,边跑边看接受率调。
核心是加温度变量、用 Metropolis 接受准则替掉贪心更新、再写温度衰减函数;其余适应度计算可复用原结构。
可以,小布内置了退火优化诊断,打开对应品种页选择优化器即可自动给出温度曲线与全局解建议。
把温度衰减调快一点或加重启动机制,当连续多代接受率低于阈值就重置当前解,能压住漂移。
在 10 维 sphere 类函数实测中,同精度下迭代次数约为网格的 5%~15%,但维度越高优势越明显。