可控优化: 模拟退火·进阶篇
(2/3)·遗传算法总卡在局部极值?用温度衰减逼出全局最优的参数组合
退火采样里的温度阈值与越界重试
这段代码实现的是模拟退火算法里针对单组参数的取值逻辑,核心在 GetParams 方法。Method=0 时,只要当前温度 Temp 不低于 CoeffTmin 乘以起止区间距离,就持续用正态扰动生成候选值。 扰动幅度由 Mass[i].Temp * HQRndNormal 决定,每轮 count 累加,一旦 count 达到 100 仍没采到合法值,就直接回退到 Mass[i].Value 并清零计数器跳出。这个 100 次的上限是防止死循环的真实护栏。 候选值必须过 VerificationOfVal 的起止区间校验,通不过就重采;旧代码里注释掉的 while 写法是直接卡边界,现在换成封装函数更灵活。外汇与贵金属参数优化属高风险操作,这类采样结果仅代表历史空间内的可能分布,不等于未来稳健。
class="type">bool AnnealingMethod::GetParams(class="type">int Method,Input &Mass[],class="type">class="kw">double CoeffTmin) { class="type">class="kw">double delta=class="num">0; class="type">class="kw">double x1=class="num">0,x2=class="num">0; class="type">class="kw">double count=class="num">0; Alg.HQRndRandomize(&state); class=class="str">"cmt">// 初始化 class="kw">switch(Method) { case(class="num">0): { for(class="type">int i=class="num">0;i<ArraySize(Mass);i++) { if(Mass[i].Temp>=CoeffTmin*Distance(Mass[i].Start,Mass[i].Stop)) { do { if(count==class="num">100) { delta=Mass[i].Value; count= class="num">0; class="kw">break; } count++; delta=Mass[i].Temp*Alg.HQRndNormal(&state); delta=FindValue(Mass[i].BestValue+delta,Mass[i].Step); } class=class="str">"cmt">// class="kw">while((delta<Mass[i].Start) || (delta>Mass[i].Stop)); class="kw">while(!VerificationOfVal(Mass[i].Start,Mass[i].Stop,delta)); Mass[i].Value=delta; } } class="kw">break; } case(class="num">1): { for(class="type">int i=class="num">0;i<ArraySize(Mass);i++) { if(Mass[i].Temp>=CoeffTmin*Distance(Mass[i].Start,Mass[i].Stop)) { do { if(count==class="num">100)
「模拟退火里的两种扰动采样」
在优化器的 case(1) 与 case(2) 分支里,核心差异在于扰动 delta 的随机采样方式。case(1) 用正态组合 HQRndNormal2 生成 x1、x2,再让 delta = Temp*x1/x2,属于对称厚尾扰动,适合在温度尚高时大范围跳出局部极值。
| case(2) 改用均匀采样 HQRndUniformR,并以 (1+1/Temp)^ | 2*x1-1 | - 1 为幅度系数乘以区间距离 Distance(Start,Stop)。当 x1 大于 0.5 时 delta 为正、小于 0.5 为负,等于 0.5 则为零位移,这种单边指数型伸缩比正态更省计算。 |
|---|
两个分支都内置了 count==100 的兜底:连续 100 次未通过 VerificationOfVal 边界校验时,直接沿用上轮 Mass[i].Value 并重置计数器后 break,避免无限循环。外汇与贵金属品种点差跳动大,跑这套前建议把 CoeffTmin 调小一档,否则 Temp 过早低于阈值会令搜索过早冻结,属高风险调参。
{
delta=Mass[i].Value;
count=class="num">0;
class="kw">break;
}
count++;
Alg.HQRndNormal2(&state,x1,x2);
delta=Mass[i].Temp*x1/x2;
delta=FindValue(Mass[i].BestValue+delta,Mass[i].Step);
}
class="kw">while(!VerificationOfVal(Mass[i].Start,Mass[i].Stop,delta));
Mass[i].Value=delta;
}
}
class="kw">break;
}
case(class="num">2):
{
for(class="type">int i=class="num">0;i<ArraySize(Mass);i++)
{
if(Mass[i].Temp>=CoeffTmin*Distance(Mass[i].Start,Mass[i].Stop))
{
do
{
if(count==class="num">100)
{
delta=Mass[i].Value;
count=class="num">0;
class="kw">break;
}
count++;
x1=Alg.HQRndUniformR(&state);
if(x1-class="num">0.5>class="num">0)
delta=Mass[i].Temp*(MathPow(class="num">1+class="num">1/Mass[i].Temp,MathAbs(class="num">2*x1-class="num">1))-class="num">1)*Distance(Mass[i].Start,Mass[i].Stop);
else
{
if(x1==class="num">0.5)
delta=class="num">0;
else
delta=-Mass[i].Temp*(MathPow(class="num">1+class="num">1/Mass[i].Temp,MathAbs(class="num">2*x1-class="num">1))-class="num">1)*Distance(Mass[i].Start,Mass[i].Stop);
}◍ 退火寻优里的取值吸附与回测指标钩子
上面这段是模拟退火参数搜索里两个容易被忽略的底层函数。FindValue 干的事是把连续扰动后的值『吸附』到步长网格上:当 step=1 时直接 round 取整,否则按离 val 最近的 step 整数倍返回,且四舍五入边界取 step/2。 [CODE] double AnnealingMethod::FindValue(double val,double step) { double buf=0; if(val==step) return val; if(step==1) return round(val); else { buf=(MathAbs(val)-MathMod(MathAbs(val),MathAbs(step)))/MathAbs(step); if(MathAbs(val)-buf*MathAbs(step)>=MathAbs(step)/2) { if(val<0) return -(buf + 1)*MathAbs(step); else return (buf + 1)*MathAbs(step); } else { if(val<0) return -buf*MathAbs(step); else return buf*MathAbs(step); } } } [/CODE] 逐行拆:buf 先算出 val 包含多少个完整 step;若余数 ≥ step 的一半就向上进一档,负数则对称取负。这样能保证每次退火迭代后的候选参数不漂在网格外,MT5 优化器跑自定义品种时不会因浮点抖动重复评估近似解。 GetFunction 则是把优化目标直接绑到 MT5 回测统计上:case 0 取 STAT_PROFIT(净利),1 取 STAT_PROFIT_FACTOR(盈利因子),2 取 STAT_RECOVERY_FACTOR(恢复因子),3 取 STAT_SHARPE_RATIO(夏普)。改这几个 case 的返回值,就能让同一套退火框架在『追收益』和『追稳健』之间切换。外汇与贵金属杠杆高,回测夏普好看不代表实盘能复现,参数过拟合概率偏大。 [CODE] double AnnealingMethod::GetFunction(int Criterion) { double Fc=0; switch(Criterion) { case(0): return TesterStatistics(STAT_PROFIT); case(1): return TesterStatistics(STAT_PROFIT_FACTOR); case(2): return TesterStatistics(STAT_RECOVERY_FACTOR); case(3): return TesterStatistics(STAT_SHARPE_RATIO); [/CODE] 开 MT5 把 Criterion 设成 3 跑一遍 EURUSD 的 H1 历史,看夏普是不是比默认净利目标收敛得更慢但曲线回撤更浅,这能帮你判断该用哪条标准喂给退火。
class="type">class="kw">double AnnealingMethod::FindValue(class="type">class="kw">double val,class="type">class="kw">double step) { class="type">class="kw">double buf=class="num">0; if(val==step) class="kw">return val; if(step==class="num">1) class="kw">return round(val); else { buf=(MathAbs(val)-MathMod(MathAbs(val),MathAbs(step)))/MathAbs(step); if(MathAbs(val)-buf*MathAbs(step)>=MathAbs(step)/class="num">2) { if(val<class="num">0) class="kw">return -(buf + class="num">1)*MathAbs(step); else class="kw">return (buf + class="num">1)*MathAbs(step); } else { if(val<class="num">0) class="kw">return -buf*MathAbs(step); else class="kw">return buf*MathAbs(step); } } } class="type">class="kw">double AnnealingMethod::GetFunction(class="type">int Criterion) { class="type">class="kw">double Fc=class="num">0; class="kw">switch(Criterion) { case(class="num">0): class="kw">return TesterStatistics(STAT_PROFIT); case(class="num">1): class="kw">return TesterStatistics(STAT_PROFIT_FACTOR); case(class="num">2): class="kw">return TesterStatistics(STAT_RECOVERY_FACTOR); case(class="num">3): class="kw">return TesterStatistics(STAT_SHARPE_RATIO);
把回测统计量拼成优化判分函数
在 MT5 策略测试器的自定义优化里,可以给不同 case 编号返回不同的 TesterStatistics 组合,让遗传或退火算法按你定义的分数挑参数。下面这段 switch 把 4~13 号映射成了期望盈利、权益/余额回撤、以及利润乘以各类风险调整系数的复合指标。 case(4) 直接返回 STAT_EXPECTED_PAYOFF(每笔期望收益);case(5)(6) 返回 STAT_EQUITY_DD 与 STAT_BALANCE_DD 的最小值类回撤;case(7)~(10) 则是用 STAT_PROFIT 分别乘 PROFIT_FACTOR、RECOVERY_FACTOR、SHARPE_RATIO、EXPECTED_PAYOFF,把绝对利润和风险效率揉成一个数。 case(11)(12) 做了除零保护:当余额或权益回撤大于 0 时才用利润除以回撤,否则原样返回利润;case(13) 示范了自定义标准——成交笔数乘利润。default 返回 -10000 代表无效解。 退火接受概率用 HQRndUniformR 取均匀随机数 a,再比 exp(-E/T) 决定是否接受较差解;T 越高越容易跳出局部最优。外汇与贵金属杠杆高,回测甜美的判分函数在实盘仍可能快速失效,调参前先在历史分段里跑一遍。
case(class="num">4): class="kw">return TesterStatistics(STAT_EXPECTED_PAYOFF); case(class="num">5): class="kw">return TesterStatistics(STAT_EQUITY_DD);class=class="str">"cmt">//min case(class="num">6): class="kw">return TesterStatistics(STAT_BALANCE_DD);class=class="str">"cmt">//min case(class="num">7): class="kw">return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_PROFIT_FACTOR); case(class="num">8): class="kw">return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_RECOVERY_FACTOR); case(class="num">9): class="kw">return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_SHARPE_RATIO); case(class="num">10): class="kw">return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_EXPECTED_PAYOFF); case(class="num">11): { if(TesterStatistics(STAT_BALANCE_DD)>class="num">0) class="kw">return TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DD); else class="kw">return TesterStatistics(STAT_PROFIT); } case(class="num">12): { if(TesterStatistics(STAT_EQUITY_DD)>class="num">0) class="kw">return TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_EQUITY_DD); else class="kw">return TesterStatistics(STAT_PROFIT); } case(class="num">13): { class=class="str">"cmt">// 例如, 指定自定义标准 class="kw">return TesterStatistics(STAT_TRADES)*TesterStatistics(STAT_PROFIT); } class="kw">default: class="kw">return -class="num">10000; } } class="type">bool AnnealingMethod::Probability(class="type">class="kw">double E,class="type">class="kw">double T) { class="type">class="kw">double a=Alg.HQRndUniformR(&state); class="type">class="kw">double res=exp(-E/T); if(res<=a) class="kw">return class="kw">false; else class="kw">return true; } a=Alg.HQRndUniformR(&state);
「退火温度与均匀采样的代码骨架」
模拟退火里温度调度直接决定参数寻优能否跳出局部极值。下面这段 GetT 用三种降温法(Method 0/1/2)把当前迭代映射成温度 T,T0 是初始高温、Tlast 是上一步温度,D、p1、p2 控制衰减曲率。 Method 0 走对数降温:当 Tlast 不等于 T0 时,先由 exp(T0/Tlast)-1 反推已迭代次数 Iteration,再令 T = T0 / log(Iteration+1);若两者相等则 Iteration 强制为 1,避免除零。Method 1 是幂律降温,Iteration 由 (T0/Tlast)^D 取整得来,T = T0 / Iteration^(1/D),D 越大降温越慢。 Method 2 套了指数-指数复合衰减:Iteration 依赖 log(Tlast/T0) 除以 (-p1*exp(-p2/D)) 后再取 D 次幂,T 回算用 T0*exp(-p1*exp(-p2/D)*Iteration^(1/D))。三个分支都做了 Iteration<=0 的兜底,直接回退 T0。 UniformValue 负责在 [min,max] 内按步长 step 均匀取数:每次先 HQRndRandomize 重播种,再用 HQRndUniformR 生成 0~1 随机数线性映射到区间,最后 FindValue 对齐步长网格。VerificationOfVal 则是边界校验,start<end 时判断 val 是否落在区间内。 外汇与贵金属杠杆高、滑点随机,这类寻优逻辑回测和实盘偏差可能很大,上 MT5 跑之前先把 D 和 p1/p2 用手工网格搜一遍再接退火。
class="type">class="kw">double AnnealingMethod::GetT(class="type">int Method,class="type">class="kw">double T0,class="type">class="kw">double Tlast,class="type">int it,class="type">class="kw">double D,class="type">class="kw">double p1,class="type">class="kw">double p2) { class="type">int Iteration=class="num">0; class="type">class="kw">double T=class="num">0; class="kw">switch(Method) { case(class="num">0): { if(Tlast!=T0) Iteration=(class="type">int)MathRound(exp(T0/Tlast)-class="num">1)+class="num">1; else Iteration=class="num">1; if(Iteration>class="num">0) T=T0/log(Iteration+class="num">1); else T=T0; class="kw">break; } case(class="num">1): { if(it!=class="num">1) Iteration=(class="type">int)MathRound(pow(T0/Tlast,D))+class="num">1; else Iteration=class="num">1; if(Iteration>class="num">0) T=T0/pow(Iteration,class="num">1/D); else T=T0; class="kw">break; } case(class="num">2): { if((T0!=Tlast) && (-p1*exp(-p2/D)!=class="num">0)) Iteration=(class="type">int)MathRound(pow(log(Tlast/T0)/(-p1*exp(-p2/D)),D))+class="num">1; else Iteration=class="num">1; if(Iteration>class="num">0) T=T0*exp(-p1*exp(-p2/D)*pow(Iteration,class="num">1/D)); else T=T0; class="kw">break; } } class="kw">return T; } class="type">class="kw">double AnnealingMethod::UniformValue(class="type">class="kw">double min,class="type">class="kw">double max,class="type">class="kw">double step) { Alg.HQRndRandomize(&state); class=class="str">"cmt">//初始化 if(max>min) class="kw">return FindValue(Alg.HQRndUniformR(&state)*(max-min)+min,step); else class="kw">return FindValue(Alg.HQRndUniformR(&state)*(min-max)+max,step); } class="type">bool AnnealingMethod::VerificationOfVal(class="type">class="kw">double start,class="type">class="kw">double end,class="type">class="kw">double val) { if(start<end) { if((val>=start) && (val<=end)) class="kw">return true; else
◍ 把退火优化过程搬上图表控件
这段收尾代码把模拟退火的计算逻辑接到了 MT5 的 GUI 控件上,核心是一个 FrameAnnealingMethod 类,专门负责把每一代优化结果画到测试器图表。它内部挂了三张 CSimpleTable(t_value / t_inputs / t_stat)分别存目标值、输入向量和统计信息,还塞了四个 CBmpButton 做播放、后退、前进、停止的帧控制。 Distance 方法只是个绝对值封装:a<b 时返回 MathAbs(b-a),否则返回 MathAbs(a-b),用来算参数空间里的欧氏距离分量。注意前面未贴全的 CheckRange 逻辑里,val 落在 [start,end] 或反向区间会返回 true,否则 false——这是退火里约束边界的硬判断。 类声明里 frame_counter 用 long 计数,FrameTester 每次传入当前 F、历史最优 Fbest、输入质量数组 Mass[] 和迭代号 it。你在 MT5 里把 AnnealingMethod.mqh 和 SimpleTable.mqh 一并 include 后,直接实例化这个类就能在策略测试器逐帧回看参数游走轨迹,贵金属与外汇品种回测请留意点差滑点带来的高波动风险。
class="kw">return class="kw">false; } else { if((val>=end) && (val<=start)) class="kw">return true; else class="kw">return class="kw">false; } } class="type">class="kw">double AnnealingMethod::Distance(class="type">class="kw">double a,class="type">class="kw">double b) { if(a<b) class="kw">return MathAbs(b-a); else class="kw">return MathAbs(a-b); } class="macro">#include <SimpleTable.mqh> class="macro">#include <Controls\BmpButton.mqh> class="macro">#include <Controls\Label.mqh> class="macro">#include <Controls\Edit.mqh> class="macro">#include <AnnealingMethod.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 类用于优化结果的输出 | class=class="str">"cmt">//+------------------------------------------------------------------+ class FrameAnnealingMethod { class="kw">private: CSimpleTable t_value; CSimpleTable t_inputs; CSimpleTable t_stat; CBmpButton b_playbutton; CBmpButton b_backbutton; CBmpButton b_forwardbutton; CBmpButton b_stopbutton; CLabel l_speed; CLabel l_stat; CLabel l_value; CLabel l_opt_value; CLabel l_temp; CLabel l_text; CLabel n_frame; CEdit e_speed; class="type">long frame_counter; class="kw">public: class=class="str">"cmt">//--- 构造函数/析构函数 FrameAnnealingMethod(); ~FrameAnnealingMethod(); class=class="str">"cmt">//--- 策略测试器的事件 class="type">void FrameTester(class="type">class="kw">double F,class="type">class="kw">double Fbest,Input &Mass[],class="type">int num,class="type">int it); class="type">void FrameInit(class="type">class="kw">string &SMass[]); class="type">void FrameTesterPass(class="type">int cr); class="type">void FrameDeinit(class="type">void); class="type">void FrameOnChartEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam,class="type">int cr); class="type">uint FrameToFile(class="type">int count); };