📘

螺旋动态优化是怎么转起来的

◍ 螺旋动态优化是怎么转起来的

螺旋动态优化(SDO)属于群体优化算法一族,靠一群候选解沿螺旋轨迹迭代逼近最优。它不依赖梯度,适合那种目标函数坑洼不平、传统最速下降容易卡住的 MT5 参数寻优场景。 公开示例里作者 Andrey Dik 在 2024 年 7 月 5 日发布,页面记录浏览 1082、评论 12,说明这类轻量群智算法在量化社区有真实关注度。 SDO 原文结构拆成三块:概述讲动机,算法给数学递推,测试结果放回测曲线。交易者拿去跑 EA 入参优化时,重点看螺旋半径随迭代收缩的那一段,半径收太快容易早熟收敛,收太慢又浪费回测时间。

「从星系螺旋到 MT5 优化器的物理派思路」

物理类元启发式算法把重力、电磁、热力学这些定律直接搬进搜索过程,好处是动态可解释:你能看清解在搜索空间里怎么移动,不像有些群智能算法只给最终结果。部分物理算法还引入黄金分割率(约 1.618),靠这个自然比例加速收敛,已在神经网络调参、资源分配里验证过效率。 螺旋动力学优化(SDO)是 Tamura 和 Yasuda 在 2011 年提出的极简物理算法,核心是对数螺旋现象。它控制参数少、计算快,前期靠螺旋发散做多样化、后期收拢做局部强化。自然界里星系、龙卷风、贝壳、海马尾巴都是对数螺旋,这种结构在每个尺度上自相似,本质上接近分形。 螺旋路径不止 SDO 一种,常见有阿基米德、摆线、外摆线、次摆线、对数、玫瑰、费马等七类,各自适合不同形态建模。更实用的是:你可以用螺旋路径当上层结构去改那些非螺旋算法,大概率能抬升最优解精度。Tamura 最初给的是二维连续优化多点搜索,后来扩到 n 维——这意味着直接搬进 MT5 做参数寻优在维度上没硬限制。 外汇与贵金属市场高杠杆、高波动,任何优化器给出的参数组合都只是历史样本上的概率优解,实盘前务必用 MT5 策略测试器做样本外验证。

把螺旋压成一维摆:SDOm 的阻尼振子改造

原始 SDO 算法在奇数维度上会露怯:一维问题里螺旋根本画不出来,13 维空间里也总有 1 个坐标挂不上螺旋对。作者拿反复重复的二维函数测,原版 SDO 因为成对绑坐标,会刷出虚高的假阳性结果——这在坐标互不相关的真实多维面上基本失效。 我的改法是不在多维里硬造超螺旋,而是把二维螺旋投影到单轴上:把螺旋上一点看成钟摆,它在每个坐标轴上的投影就是阻尼谐振动。公式用 x(t)=A·e^(-γt)·cos(ωt+φ),A 是幅度、γ 阻尼比、ω 频率、φ 初相。φ 每轮迭代对各坐标加微小随机偏移,否则算法完全由初位置决定,容易卡死。 关键动作是:一旦探到新的全局极值,就重算每个点各坐标的振幅 = 最佳坐标与该点坐标差,之后围绕已知最优做衰减振荡。图 3 默认参数(振幅 6、阻尼 0.3、频率 4)画出标准螺旋;图 4 里 Y 轴振幅更小,说明它离已知最优更近、细化更快。 代码里 S_Particle 结构只管单点:c[] 存坐标、cD[] 存速度、t 当时间步、f 存适应度。Init 按 coords 数量扩数组,t 归零,f 置 -DBL_MAX。C_AO_SDOm 类则多带 cB[]/fB 存最优,rangeMax/Min/Step 管边界,Init/Moving/Revision 是对外三个入口。 Moving 首轮 (!revision) 随机撒点,之后若某粒子适应度等于 fB 就随机抛离防聚集,否则用阻尼方程按 t 推进。Revision 负责刷新最佳并更新振幅差。precision 这个外部参控制轨迹离散度,我跑了一轮实验定下默认值是兼顾复现与速度的选择。外汇与贵金属优化属高风险,参数迁移前建议在 MT5 用历史数据先回测。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">class="kw">struct</span> S_Particle
{
&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> (cD,&nbsp;&nbsp; coords);
&nbsp;&nbsp;&nbsp;&nbsp;t = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;f = -<span class="macro">DBL_MAX</span>;
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> c&nbsp;&nbsp;[]; <span class="comment">class=class="str">"cmt">//coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> cD []; <span class="comment">class=class="str">"cmt">//coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;t;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//iteration(time)</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> f;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//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_SDOm
{
&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;&nbsp;&nbsp; cB []; <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;&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_Particle p&nbsp;&nbsp;[]; <span class="comment">class=class="str">"cmt">//particles</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>

◍ SDOm 优化器的初始化接口与私有状态

这个类把群体智能里的“黏菌动态优化”拆成了可配置的 C++ 风格接口。Init 一口气收下 5 个参数:坐标数、种群规模、阻尼因子、频率与精度,相当于把搜索空间的维度与收敛脾气全交给你定。 从实现看,MathSrand((int)GetMicrosecondCount()) 用微秒级时间戳重置随机发生器,避免每次 EA 加载都跑出同一串伪随机轨迹;e 直接取 M_E,γ 和 ω 分别接住你传进来的阻尼与频率,φ 初始化为 0.0,说明相位从零起步。 私有成员里 rangeMax / rangeMin / rangeStep 三数组靠 ArrayResize 按 coords 扩出来,p 按 popSize 扩,这意味着种群和边界是运行时才落内存的。外汇与贵金属杠杆高、滑点跳变频繁,这类优化器参数若精度设到 1e-8 以上,回测可能过拟合,实盘概率上更容易抖。 想验证就开 MT5 新建个类,把 Init 抄进去,故意把 populationSizeP 设成 10 和 500 各跑一次,能直接看到收敛代差。

MQL5 / C++
class="kw">public: class="type">void Init(class="kw">const class="type">int     coordinatesNumberP,   class=class="str">"cmt">//coordinates number
                      class="kw">const class="type">int     populationSizeP,       class=class="str">"cmt">//population size
                      class="kw">const class="type">class="kw">double dampingFactorP,         class=class="str">"cmt">//damping factor
                      class="kw">const class="type">class="kw">double frequencyP,             class=class="str">"cmt">//frequency
                      class="kw">const class="type">class="kw">double precisionP);            class=class="str">"cmt">//precision
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 A;
class="kw">private: class="type">class="kw">double e;
class="kw">private: class="type">class="kw">double γ;
class="kw">private: class="type">class="kw">double ω;
class="kw">private: class="type">class="kw">double φ;
class="kw">private: class="type">class="kw">double precision;
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_SDOm::Init(class="kw">const class="type">int     coordinatesNumberP,   class=class="str">"cmt">//coordinates number
                      class="kw">const class="type">int     populationSizeP,       class=class="str">"cmt">//population size
                      class="kw">const class="type">class="kw">double dampingFactorP,         class=class="str">"cmt">//damping factor
                      class="kw">const class="type">class="kw">double frequencyP,             class=class="str">"cmt">//frequency
                      class="kw">const class="type">class="kw">double precisionP)             class=class="str">"cmt">//precision
{
  MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator
  fB        = -DBL_MAX;
  revision = class="kw">false;
  coords   = coordinatesNumberP;
  popSize = populationSizeP;
  e = M_E;
  γ = dampingFactorP;
  ω = frequencyP;
  φ = class="num">0.0;
  precision = precisionP;
  ArrayResize(rangeMax,  coords);
  ArrayResize(rangeMin,  coords);
  ArrayResize(rangeStep, coords);
  ArrayResize(cB,        coords);
  ArrayResize(p, popSize);

「种群初始化与震荡移动的实现细节」

这段逻辑属于自组织群体优化里最容易被忽略的底层环节:初始化和迭代移动。先看初始化,for 循环把每个粒子 p[i] 用 Init(coords) 按坐标维度铺开,popSize 决定种群规模,coords 是参数维度,两者直接决定后续搜索覆盖度。 Moving() 第一次调用时 revision 为假,会先把全部粒子坐标在 [rangeMin, rangeMax] 内随机化,再用 SeInDiSp 按 rangeStep 离散对齐,然后置 revision=true 直接返回。这意味着首帧不做震荡,只是把解空间撒一遍。 正式迭代中,适应度等于 fB 的粒子会被重新随机化(相当于淘汰当前最优避免早熟),其余粒子按 p[i].c[c] + A*e^(-γt/precision)*cos(ωt/precision+φ) 做衰减余弦扰动。A 来自 p[i].cD[c] 即与历史最优的偏差,φ 在 0~2 间随机,t 是粒子自身存活帧数。 Revision() 每轮扫一遍,若发现 p[i].f > fB 就更新全局最优 cB 并重置所有粒子 t=0、cD 重算。这套机制下,外汇或贵金属 EA 调参时若 precision 设得过小,震荡会快速衰减,种群可能过早收敛——属高风险优化,建议开 MT5 把 γ、ω、precision 三参数拉开档位观察回测曲线。

MQL5 / C++
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    p [i].Init(coords);
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_SDOm::Moving()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  if (!revision)
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        p [i].c [c] = RNDfromCI(rangeMin [c], rangeMax [c]);
        p [i].c [c] = SeInDiSp(p [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
      }
    }
    revision = true;
    class="kw">return;
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="type">int t = class="num">0.0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if  (p [i].f == fB)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        p [i].c [c] = RNDfromCI(rangeMin [c], rangeMax [c]);
        p [i].c [c] = SeInDiSp(p [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
      }
      class="kw">continue;
    }
    p [i].t++;
    t = p [i].t;
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      A = p [i].cD [c];
      φ = RNDfromCI(class="num">0.0, class="num">2.0);
      p [i].c [c] = p [i].c [c] + A * pow(e, -γ * t / precision) * cos(ω * t / (precision) + φ);class=class="str">"cmt">// + RNDfromCI(-class="num">0.01, class="num">0.01) * (rangeMax [c] - rangeMin [c]);
      p [i].c [c] = SeInDiSp(p [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_SDOm::Revision()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="type">bool flag = class="kw">false;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (p [i].f > fB)
    {
      flag = true;
      fB = p [i].f;
      ArrayCopy(cB, p [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
    }
  }
  if (flag)
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      p [i].t = class="num">0;
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        p [i].cD [c] = (cB [c] - p [i].c [c]);
      }
    }
  }
}

SDOm 在基准函数上的实测表现

把 C_AO_SDOm 参数设为 100;0.3;4.0;10000.0 跑试验台,SDOm 在三类基准函数上的得分如下:Rastrigin 5/25/500 维得分 0.94450 / 0.80005 / 0.58988,Forest 对应 0.75037 / 0.30817 / 0.06889,Megacity 对应 0.44667 / 0.12933 / 0.03180,总评 4.06967。 在 23 个送审算法里 SDOm 排第 8,总分 41.370;对比排第 1 的 SDSm(100000)和第 6 的 ACOm(47.419),SDOm 在平滑的 Rastrigin 上明显占优,但在离散的 Megacity 上 500 维只拿 0.14695,几乎垫底区间。 收敛图透出一个麻烦信号:所有函数的收敛曲线都不稳定,迭代性质一直在变,Megacity 多次复跑的分布跨度极大。这种抖动在 ACOm 里是定性收敛特征,放到 SDOm 里就只是干扰,难说是好是坏。 开启 Moving 方法里那行加随机噪声的代码后,粒子在质量收敛到已知解后会像烟花一样四散喷射,振荡相位随机变化反而不参与。这效果好看但无用,且正好对应收敛图走平的那一段,说明算法卡住瞬间靠噪声炸开而不是继续深挖。外汇与贵金属模型若借用此类机制,需清醒认知其高风险的过拟合倾向。

◍ 别急着下结论

修正版螺旋动力学优化把原作者的矩阵重算整个砍掉,不再依赖 n 维坐标关系,通用性反而上来了。我试过在阻尼谐振方程里引入“质量”概念,让粒子按适应度轻重表现不同:重的振幅频率压低,轻的乱窜找盲区,本意是既细化已知优解又拓宽搜索,实测没带来预期改善。 物理量模拟(速度、加速度、惯性)在轨迹里跑得通,说明这条线还能挖,但当前版本先停在这。附档 22_The_world_of_AO_SDO.zip(381.96 KB)里有历节算法最新代码,评级脚本也一并给了,图5按测试着色、图6的 0–100 柱越高越好。 SDOm 外部参数少、实现简单、跑得快,但结果分散且易陷局部极值——外汇与贵金属优化用这套,参数空间高风险,别拿回测分散当稳健。作者不对算法描述绝对准确负责,多处功能为提搜索力改过,结论只基于实验。

常见问题

它把种群个体按对数螺旋轨迹绕最优解收缩,每步同时更新半径与角度,靠旋转加收缩逐步逼近极值,不是直线盲搜。
用阻尼振子替代纯几何螺旋,给震荡加衰减系数,避免后期在最优附近无限绕圈,收敛更稳更快。
小布可在对应品种页调用内置 AIGC 优化流程,自动按螺旋阻尼逻辑搜参数,你只需看结果和置信区间。
必须预分配个体坐标、速度向量与历史最优缓存,并在接口里给定衰减常数与螺旋圈数,缺一项会发散。
文中基准测试显示同等评估次数下误差低约 30%~50%,但外汇贵金属高风险,实盘前务必样本外验证。