模式搜索的暴力方法(第六部分):循环优化·进阶篇
(2/3)·还在手动改参数跑回测?循环优化让终端替你完成持续重优化与筛选
用曲线族因子替平直度挑优化结果
MT5 自带优化器不会直接给你一条“余额线像不像直线”的评分,但这类线性因子才是挑出稳健参数的硬指标。核心思路是把回测余额点阵当成统计样本:以一条从零连到终值的直线作模型,利润曲线是真实数据流,线性系数越高,说明整套交易规则在样本内越可靠,外推到未来行情盈利的概率倾向更高。外汇与贵金属杠杆高、跳空频繁,这种可靠性只代表历史样本内表现,不等于实盘必过。 单一直线标准太理想化,实战会砍掉大量有效场景、交易数骤降。改进办法是引入一族凹形曲线作可接受模型,分别计算每条与实盘曲线的拟合偏差,取最小偏差对应的“曲线族因子”。约束条件很具体:曲线无拐点且严格凹、凹度可用百分比调、模型尽量简单。作者用弹性杆偏转来建模,极值横坐标按交易数 N 奇偶分别取 (N+1)/2 或 N/2。 代码里 Lines 宏设 11 条曲线,凹度从 0%(直线)渐增至 MaxPercent=10.0。InitLines 吃进交易段数 SegmentsInput 和终值 BalanceInput,算出基础斜率 K = BalanceInput / Segments。之后 BuildBalances 按族生成曲线,CalculateMinDeviation 挑最小偏差。你可以直接把这段抄进 EA 的自定义测试器,跑两次回测(第一次拿终值,第二次算各点)验证。
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策略测试器用历史数据核对。
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,说明账户曲线和预设增长模型脱节,外汇与贵金属杠杆品种尤其容易因滑点跳变触发这种偏离,需警惕模型失效。
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 粘到一张图上跑的思路。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Function to write data if present |</span> <span class="comment">class=class="str">"cmt">//| Write quotes to file |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> WriteDataIfPresent() { <span class="comment">class=class="str">"cmt">// Declare array to store quotes </span> <span class="predefines">class="type">MqlRates</span> rates[]; <span class="functions">ArraySetAsSeries</span>(rates, <span class="macro">class="kw">false</span>); <span class="comment">class=class="str">"cmt">// Select a random chart from those we added to the workspace </span> ChartData Chart = SelectAnyChart(); <span class="comment">class=class="str">"cmt">// If the file name class="type">class="kw">string is not empty </span> <span class="keyword">if</span> (Chart.FileNameString != <span class="class="type">class="kw">string">""</span>) { <span class="comment">class=class="str">"cmt">// Copy quotes and calculate the real number of bars</span> <span class="keyword">class="type">int</span> copied = <span class="functions">CopyRates</span>(Chart.SymbolX, Chart.PeriodX, <span class="number">class="num">1</span>, <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))), rates); <span class="comment">class=class="str">"cmt">// Calculate ideal number of bars</span> <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))); <span class="comment">class=class="str">"cmt">// Calculate percentage of received data </span> <span class="keyword">class="type">class="kw">double</span> Percent = <span class="number">class="num">100.0</span> * copied / ideal; <span class="comment">class=class="str">"cmt">// If the received data is not very different from the desired data, </span> <span class="comment">class=class="str">"cmt">// then we accept them and write them to a file</span> <span class="keyword">if</span> (Percent >= <span class="number">class="num">95.0</span>) { <span class="comment">class=class="str">"cmt">// Open file(create it if it does not exist, </span> <span class="comment">class=class="str">"cmt">// otherwise, erase all the data it contained)</span> OpenAndWriteStart(rates, Chart, CommonE); WriteAllBars(rates); <span class="comment">class=class="str">"cmt">// Write all data to file</span> WriteEnd(rates); <span class="comment">class=class="str">"cmt">// Add to end</span> CloseFile(); <span class="comment">class=class="str">"cmt">// Close and save data file</span> } <span class="keyword">else</span> { <span class="comment">class=class="str">"cmt">// If there are much fewer quotes than required for calculation </span>