模式搜索的暴力方法(第六部分):循环优化·进阶篇
🔁

模式搜索的暴力方法(第六部分):循环优化·进阶篇

(2/3)·还在手动改参数跑回测?循环优化让终端替你完成持续重优化与筛选

偏理论 第 2/3 篇
不少人把优化当成一次性动作,EA上线后就再不碰参数,直到曲线走平才慌。另一些人频繁手动重优化,把大量时间耗在终端点按钮上。这两种极端都忽略了循环优化能把重复劳动压缩到接近零。

用曲线族因子替平直度挑优化结果

MT5 自带优化器不会直接给你一条“余额线像不像直线”的评分,但这类线性因子才是挑出稳健参数的硬指标。核心思路是把回测余额点阵当成统计样本:以一条从零连到终值的直线作模型,利润曲线是真实数据流,线性系数越高,说明整套交易规则在样本内越可靠,外推到未来行情盈利的概率倾向更高。外汇与贵金属杠杆高、跳空频繁,这种可靠性只代表历史样本内表现,不等于实盘必过。 单一直线标准太理想化,实战会砍掉大量有效场景、交易数骤降。改进办法是引入一族凹形曲线作可接受模型,分别计算每条与实盘曲线的拟合偏差,取最小偏差对应的“曲线族因子”。约束条件很具体:曲线无拐点且严格凹、凹度可用百分比调、模型尽量简单。作者用弹性杆偏转来建模,极值横坐标按交易数 N 奇偶分别取 (N+1)/2 或 N/2。 代码里 Lines 宏设 11 条曲线,凹度从 0%(直线)渐增至 MaxPercent=10.0。InitLines 吃进交易段数 SegmentsInput 和终值 BalanceInput,算出基础斜率 K = BalanceInput / Segments。之后 BuildBalances 按族生成曲线,CalculateMinDeviation 挑最小偏差。你可以直接把这段抄进 EA 的自定义测试器,跑两次回测(第一次拿终值,第二次算各点)验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Number of lines in the balance model                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define Lines class="num">11
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initializing variables                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double MaxPercent = class="num">10.0;
class="type">class="kw">double BalanceMidK[,Lines];
class="type">class="kw">double Deviations[Lines];
class="type">int Segments;
class="type">class="kw">double K;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Method for initializing required variables and arrays             |
class=class="str">"cmt">//| Parameters: number of segments and initial balance               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void InitLines(class="type">int SegmentsInput, class="type">class="kw">double BalanceInput)
{
  Segments = SegmentsInput;
  K = BalanceInput / Segments;

「曲线簇的归零与重建逻辑」

在指标初始化或参数变动时,先给平衡中线数组扩容量:ArrayResize(BalanceMidK, Segments+1),随后依次调用三个子函数把旧状态清掉再重算。若跳过 ZeroStartBalances 直接 BuildBalances,数组里残留的上一次 Segments 更大的数据会污染新曲线,这在MT5里表现为图形右侧拖出一条偏离的尾线。 ZeroStartBalances 用双层循环把 BalanceMidK[j,i] 全部置 0.0,覆盖 i 从 0 到 Lines-1、j 从 0 到 Segments。ZeroDeviations 则只跑一层循环,把每一条线的 Deviations[i] 设成 -1.0,这个 -1.0 后续会被真实标准差覆盖,留 -1.0 即代表该线尚未计算偏离。 BuildBalances 是核心。先算 N0:若 Segments 为偶数取 Segments/2,奇数则向下取整,这决定抛物线拐点位置。第 0 条线为直线,BalanceMidK[j,0]=K*j;其余线按 ThisP = i*(MaxPercent/10.0) 取曲率百分比,再用 KDelta、Psi0、KDelta1 三个辅助系数分前半段(j<=N0)和后半段拼出弯曲平衡线。把 MaxPercent 从 10 调到 30,同等 Segments 下曲线弧度肉眼可见变陡,外汇与贵金属波动剧烈,参数误调可能引发图形失真,实盘前务必在MT5策略测试器用历史数据核对。

MQL5 / C++
  ArrayResize(BalanceMidK,Segments+class="num">1);
  ZeroStartBalances();
  ZeroDeviations();
  BuildBalances();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Resetting variables for incrementing balances                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ZeroStartBalances()
{
  for (class="type">int i = class="num">0; i < Lines; i++ )
  {
    for (class="type">int j = class="num">0; j <= Segments; j++)
    {
      BalanceMidK[j,i] = class="num">0.0;
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Reset deviations                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ZeroDeviations()
{
  for (class="type">int i = class="num">0; i < Lines; i++)
  {
    Deviations[i] = -class="num">1.0;
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructing all balances                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void BuildBalances()
{
  class="type">int N0 = MathFloor(Segments / class="num">2.0) - Segments / class="num">2.0 == class="num">0 ? Segments / class="num">2 : (class="type">int)MathFloor(Segments / class="num">2.0);
  for (class="type">int i = class="num">0; i < Lines; i++)
  {
    if (i==class="num">0)
    {
      for (class="type">int j = class="num">0; j <= Segments; j++)
      {
        BalanceMidK[j,i] = K*j;
      }
    }
    else
    {
      class="type">class="kw">double ThisP = i * (MaxPercent / class="num">10.0);
      class="type">class="kw">double KDelta = ( (ThisP /class="num">100.0) * K * Segments) / (MathPow(N0,class="num">2)/class="num">2.0 );
      class="type">class="kw">double Psi0 = -KDelta * N0;
      class="type">class="kw">double KDelta1 = ((ThisP / class="num">100.0) * K * Segments) / (MathPow(Segments-N0, class="num">2) / class="num">2.0);
      for (class="type">int j = class="num">0; j <= N0; j++)
      {
        BalanceMidK[j,i] = (K + Psi0 + (KDelta * j) / class="num">2.0) * j;
      }

◍ 曲线拟合里的最小偏离怎么算

上一段把资金曲线后半段按 K 与 KDelta1 递推补完了,接下来要量化哪条参考线最贴近真实账户权益。核心思路是:对每条线、每个分段点算相对偏离,再挑出全局最小的那一个值。 CalculateMinDeviation 接收原始余额数组的引用,先双重循环遍历 Lines 条线与 Segments 个分段。CurrentDeviation 用 MathAbs(OriginalBalance[j] - BalanceMidK[j,i]) 除以期末余额做归一;若期末余额为 0 则赋 -1.0 表示无效,避免除零崩脚本。 偏离上限存进 Deviations[i],只增不减。第二轮单循环从 Deviations 里捞最小值:首个有效值(非 -1.0)初始化 MinDeviation,之后遇更小才更新,最后 return 出去供上层判断拟合优度。 实盘里把 Segments 设成 20、Lines 设成 8 跑一遍,若 MinDeviation 持续高于 0.15,说明账户曲线和预设增长模型脱节,外汇与贵金属杠杆品种尤其容易因滑点跳变触发这种偏离,需警惕模型失效。

MQL5 / C++
for (class="type">int j = N0; j <= Segments; j++)class=class="str">"cmt">//construct the second half of the curve 
  {
    BalanceMidK[j,i] = BalanceMidK[i, N0] + (K + (KDelta1 * (j-N0)) / class="num">2.0) * (j-N0);
  }
   }
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculation of the minimum deviation from all lines              |
class=class="str">"cmt">//| Parameters: initial balance passed via link                      |
class=class="str">"cmt">//| Return: minimum deviation                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CalculateMinDeviation(class="type">class="kw">double &OriginalBalance[])
{
  class=class="str">"cmt">//define maximum relative deviation for each curve 
  for (class="type">int i = class="num">0; i < Lines; i++)
  {
    for (class="type">int j = class="num">0; j <= Segments; j++)
    {
      class="type">class="kw">double CurrentDeviation = OriginalBalance[Segments] ? MathAbs(OriginalBalance[j] - BalanceMidK[j, i]) / OriginalBalance[Segments] : -class="num">1.0;
      if (CurrentDeviation > Deviations[i])
      {
        Deviations[i] = CurrentDeviation;
      }             
    }
  }
   
  class=class="str">"cmt">//determine curve with minimum deviation and deviation itself 
  class="type">class="kw">double MinDeviation=class="num">0.0;
  for (class="type">int i = class="num">0; i < Lines; i++)
  {
    if ( Deviations[i] != -class="num">1.0 && MinDeviation == class="num">0.0)
    {
      MinDeviation = Deviations[i];
    }
    else if (Deviations[i] != -class="num">1.0 && Deviations[i] < MinDeviation)
    {
      MinDeviation = Deviations[i];
    }
  }
  class="kw">return MinDeviation;
}

用随机图表喂报价的循环优化器

整套自动搜配置系统的核心,是终端和一个外部程序之间的交互层,本质是一个带高级优化准则的循环优化器。它先要一个报价源,这个源就是 MT5 终端里某个被我们改造过的 EA:它不交易,只充当分时处理器或计时器,从任意已开图表里随机抽一个,把历史报价按自定义格式写进文件。 写文件靠的是 WriteDataIfPresent 这套逻辑。它先用随机数挑图表,再用 CopyRates 拉数据,并以「理想 bar 数」为基准算覆盖率。只有当实际复制到的 bar 数 ≥ 理想值的 95% 时才落盘,否则弹「数据不足」——这一步直接卡掉了残缺历史,避免后续优化跑偏。 支撑它的几个结构得先定义好:ChartData 存每个图表的文件名、品种、周期;Randomindex(start,end) 出随机数;SelectAnyChart() 遍历所有可用图表(排除当前图)随机选一个。这样生成的报价被外部程序抓走后,才会进入自动搜盈利配置的阶段。 算法本身只有三种状态:不活动、等报价、活动。只要 EA 还没产出文件或文件夹被清空,它就暂停并轮询等待。优化准则我按 MQL5 风格写了高级版,用曲线族因子代替标准算法的线性因子,暴力搜参空间明显更宽;更细的改进放不下,下一篇会讲基于多货币模板把多个 EA 粘到一张图上跑的思路。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Function to write data if present&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;                |</span>
<span class="comment">class=class="str">"cmt">//| Write quotes to file&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;           |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> WriteDataIfPresent()
{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Declare array to store quotes </span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="predefines">class="type">MqlRates</span> rates[];
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArraySetAsSeries</span>(rates, <span class="macro">class="kw">false</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Select a random chart from those we added to the workspace </span>
&nbsp;&nbsp;&nbsp;&nbsp;ChartData Chart = SelectAnyChart();
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// If the file name class="type">class="kw">string is not empty </span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (Chart.FileNameString != <span class="class="type">class="kw">string">""</span>)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Copy quotes and calculate the real number of bars</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> copied = <span class="functions">CopyRates</span>(Chart.SymbolX, Chart.PeriodX, <span class="number">class="num">1</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span>((YearsE*(<span class="number">class="num">365.0</span>*(<span class="number">class="num">5.0</span>/<span class="number">class="num">7.0</span>)*<span class="number">class="num">24</span>*<span class="number">class="num">60</span>*<span class="number">class="num">60</span>)) / <span class="keyword">class="type">class="kw">double</span>(<span class="functions">PeriodSeconds</span>(Chart.PeriodX))),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rates);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Calculate ideal number of bars</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> ideal = <span class="keyword">class="type">int</span>((YearsE*(<span class="number">class="num">365.0</span>*(<span class="number">class="num">5.0</span>/<span class="number">class="num">7.0</span>)*<span class="number">class="num">24</span>*<span class="number">class="num">60</span>*<span class="number">class="num">60</span>)) / <span class="keyword">class="type">class="kw">double</span>(<span class="functions">PeriodSeconds</span>(Chart.PeriodX)));
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Calculate percentage of received data </span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> Percent = <span class="number">class="num">100.0</span> * copied / ideal;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// If the received data is not very different from the desired data, </span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// then we accept them and write them to a file</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (Percent &gt;= <span class="number">class="num">95.0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Open file(create it if it does not exist, </span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// otherwise, erase all the data it contained)</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;OpenAndWriteStart(rates, Chart, CommonE);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;WriteAllBars(rates); <span class="comment">class=class="str">"cmt">// Write all data to file</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;WriteEnd(rates); <span class="comment">class=class="str">"cmt">// Add to end</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CloseFile(); <span class="comment">class=class="str">"cmt">// Close and save data file</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// If there are much fewer quotes than required for calculation </span>
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到EA是否需要重优化的提示,你只管决策要不要跑新一轮循环。

常见问题

单次优化只跑一遍参数空间取最优,循环优化在选定范式下反复迭代,结合自定义标准持续筛选,倾向提升远期效率而非拟合历史。
小布目前提供品种页的EA状态诊断与重优化提醒,具体参数迭代仍在终端执行,但重复监控可交给小布,你专注决策。
消除分时主要减少非必要计算节点,可能加快速度,但是否可靠取决于自定义标准是否合理,外汇贵金属属高风险,结论带概率性。
合并机器人让多个EA组件按正确架构交互,是循环优化上层体系的一部分,目的在降低人工维护强度,并非保证收益。