用退火温度换出全局最优解
◍ 用退火温度换出全局最优解
模拟退火(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 看字段布局。
<span class="comment">class=class="str">"cmt">//————————————————————————————————————————</span> <span class="keyword">class="kw">struct</span> S_Agent { <span class="keyword">class="type">void</span> Init(<span class="keyword">class="type">int</span> coords) { <span class="functions">ArrayResize</span> (c, coords); <span class="functions">ArrayResize</span> (cPrev, coords); f = -<span class="macro">DBL_MAX</span>; fPrev = -<span class="macro">DBL_MAX</span>; } <span class="keyword">class="type">class="kw">double</span> c []; <span class="comment">class=class="str">"cmt">//coordinates</span> <span class="keyword">class="type">class="kw">double</span> cPrev []; <span class="comment">class=class="str">"cmt">//previous coordinates</span> <span class="keyword">class="type">class="kw">double</span> f; <span class="comment">class=class="str">"cmt">//fitness</span> <span class="keyword">class="type">class="kw">double</span> fPrev; <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 { <span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> cB []; <span class="comment">class=class="str">"cmt">//best coordinates</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> fB; <span class="comment">class=class="str">"cmt">//FF of the best coordinates</span> <span class="keyword">class="kw">public</span>: S_Agent a []; <span class="comment">class=class="str">"cmt">//agent</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMax []; <span class="comment">class=class="str">"cmt">//maximum search range</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMin []; <span class="comment">class=class="str">"cmt">//manimum search range</span> <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 估一遍典型振幅。
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 把这两个系数接进自己的优化框架,跑一组对照回测就能看见差异。
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 跑个对比帧就知。外汇和贵金属优化高风险,任何算法结论都只是概率倾向,实盘前必须自己验证。