用激活函数给优化器加自定义约束
◍ 用激活函数给优化器加自定义约束
MQL5 的遗传优化器默认只认净利润、夏普这类内建目标,但实战里我们常想塞进「最大回撤不超过 15%」「连亏次数≤3」这种硬约束。新框架允许在 OnTester 里直接返回经激活函数映射后的自定义分数,把不满足条件的个体直接压到接近零,逼着遗传算法绕开坏解。 核心思路是写一个 sigmoid 类的门限函数:当指标落在可接受区间,输出逼近 1;一旦越界,输出指数衰减到 0。下面这段把「回撤 > 20% 直接判死」落成了可编译代码,开 MT5 新建脚本即可验证。 外汇与贵金属品种波动剧烈,遗传优化结果高度依赖样本区间,实盘前务必用 out-of-sample 数据复跑,回测漂亮不代表 live 账户概率占优。
class="type">class="kw">double OnTester() { class="type">class="kw">double ret = class="num">0; class="type">class="kw">double dd = TesterStatistics(STAT_EQUITY_DDREL_PERCENT); class=class="str">"cmt">// 取相对回撤百分比 if(dd <= class="num">20.0) class=class="str">"cmt">// 回撤不超 class="num">20% ret = class="num">1.0 / (class="num">1.0 + MathExp(-(class="num">20.0 - dd))); class=class="str">"cmt">// sigmoid 门限,区间内趋近 class="num">1 else ret = MathExp(-(dd - class="num">20.0)); class=class="str">"cmt">// 越界指数衰减到 class="num">0 class="kw">return ret; }
「自定义优化准则里的隐性陷阱」
MT5 的自定义优化准则允许开发者用代码干预遗传优化器的排序逻辑,理论上能把 Excel、Python 或 R 里的人工筛选工作量压下来,直接让优化器吐出稳定参数组合。但论坛里大量已发布的自定义准则里,仍频繁出现 return(0) 的写法——这意味着该组参数被直接判死,不会进入后续评估。
这种写法在实际或潜在层面都有危险:一是可能把勉强不达标但泛化能力不错的参数直接丢弃;二是遗传算法依赖种群梯度,盲目 return(0) 会让搜索偏离有效路径,尤其当自营交易公司把边界条件卡得比散户严数倍时,错杀概率更高。
作者为找更合理的曲线方程,参考了神经网络激活函数文献并改写出适用于 MT5 的版本,后续章节会给出修改、缩放、加权及实操案例。开 MT5 把下面这段代码跑一遍,能看到标准激活函数在 OnTester 里的返回结构,先建立直觉再谈改写。
class="type">class="kw">double OnTester() { class=class="str">"cmt">// 标准激活函数示例:线性修正 class="type">class="kw">double result = class="num">0.0; class="type">class="kw">double profit = TesterStatistics(STAT_PROFIT); if(profit > class="num">0) result = profit; class="kw">return(result); }
复合准则给遗传优化挖的坑
论坛里两位常驻贡献者点破过一个隐患:在自定义准则里写 return(0) 或者依赖复合准则返回值时,结果会反直觉。版主 Alain Verleyen 贴过一组对照——某参数组合亏了 6000,得分却高于另一组赚 1736、且交易次数更多、盈利因子/风险回报比/夏普/回撤全面占优的组合。他原话是“神秘的复合准则给出了奇怪结果”,这已经足以让人怀疑它背后的加权逻辑。 Muhammad Fahad 在同一帖补了一刀:负余额在优化里不算毒药,基因交叉时它帮着判断后代优劣。但问题在于,复合准则给出的高正向分,很可能把遗传优化器带偏,让它追着错误目标跑。 MQL5 表面上看,除了用 return(0) 硬拒不合格结果,没法像老 MT4 那样直接给统计指标设约束。return(0) 的副作用前面说过,会切断遗传算法的交叉路径。一个能绕开的思路是:当比率或指标触碰你设的上下限、或逼近目标值时,返回一个极接近 1 的值,再把多个指标返回值相乘、乘 100——这样好组合得高分,坏组合自然沉底,外汇和贵金属品种波动大、滑点狠,跑之前务必用历史数据先验证这套乘算权重会不会被极端行情洗掉。
◍ 遗传优化里的神经网络影子
遗传优化和神经网络在逻辑上高度同构:先试一批参数组合,按选定准则打分,留下可能盈利的继续迭代,丢掉其余,直到分数不再抬升。这套循环和反向传播前的搜索过程几乎是一个模子。 遗传算法也会撞上机器学习的老坑——非凸误差曲面下的局部最小值或鞍点。若准则里写了误导性的 return(0),算法可能直接把“有认知价值”的组合弃掉。早期我拿除法、乘法甚至指数因子堆复合准则,分数飙到离谱,这就是梯度爆炸;后来发现系统把分数压在 0–100,背后大概率是 Sigmoid 或逻辑函数做了归一。 自定义准则的各组成部分必须上权重,才能排优先级;同时对输入输出做标准化,调敏感度、方便跨模型比。加权与归一的细节留到后面展开。 人在表格里手筛优化结果时,按指标排序、染色、凭直觉挑参数集,本质也是在复刻上述流程——所谓自动优化,只是把交易者下意识的思维过程显式化。外汇与贵金属市场高波动,这类准则仍可能过拟合,需 MT5 实盘前用历史数据交叉验证。
「用激活函数给恢复因子打分」
把恢复因子(RF)当输入喂给激活函数,本质是想把优化器的目标从『越大越好』变成『过阈值才有意义』。原文以 RF=5 为门槛,用 MT5 的 STAT_RECOVERY_FACTOR 取值,再减去 MinRF 得到偏差值。 ReLU 写法最直白:x<=0 返回 0,x>0 返回 x。但实践中 RF 正好等于 5 时偏差为 0,只有大于 5 才非零,而且强负输入会让神经元『死亡』、正输出无上限,这两点在自定义准则里都算硬伤。 Softplus 用 ln(1+e^x) 平滑了死区,却仍无上界;Sigmoid 把输出压在 0~1,公式 1/(1+e^-x),平移后能让阈值点附近输出逼近 1。代码里给 DeviationRF 再减一个 SigCorrection(实测取 >=5)做左移校正。 Sigmoid 导数乘 4 可把中心峰值从 0.25 拉回 1,降低梯度消失概率;Tanh 及其导数经 (tanh+1)/2 重缩也落进 0~1。外汇与贵金属回测属高风险,这些曲线只解决『打分形态』,不保证样本外盈利。 下面这段 MQL5 是原文各函数的拼装,开 MT5 新建脚本即可逐行验证:ReLU 取偏差、Softplus 对数、Sigmoid 带校正、以及从测试报告抓 RF 与 Trades 的统计调用。
<span class="keyword">class="kw">return</span>(<span class="number">class="num">0</span>); <span class="keyword">class="type">class="kw">double</span> ReLU(<span class="keyword">class="type">class="kw">double</span> x) { <span class="keyword">class="kw">return</span>(<span class="functions">MathMax</span>(<span class="number">class="num">0</span>, x)); } <span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> MinRF = <span class="number">class="num">5.0</span>; <span class="keyword">class="type">class="kw">double</span> RF = <span class="functions">TesterStatistics</span>(<span class="macro">STAT_RECOVERY_FACTOR</span>); <span class="keyword">class="type">class="kw">double</span> DeviationRF = RF - MinRF; <span class="keyword">class="type">class="kw">double</span> ReLU(<span class="keyword">class="type">class="kw">double</span> x) { <span class="keyword">class="kw">return</span>(<span class="functions">MathMax</span>(<span class="number">class="num">0</span>, x)); } <span class="keyword">class="type">class="kw">double</span> ScoreRF = ReLU(DeviationRF); <span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> MinRF = <span class="number">class="num">5.0</span>; <span class="keyword">class="type">class="kw">double</span> RF = <span class="functions">TesterStatistics</span>(<span class="macro">STAT_RECOVERY_FACTOR</span>); <span class="keyword">class="type">class="kw">double</span> ScoreRF = (RF - MinRF); <span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> MinRF = <span class="number">class="num">5.0</span>; <span class="keyword">class="type">class="kw">double</span> RF = <span class="functions">TesterStatistics</span>(<span class="macro">STAT_RECOVERY_FACTOR</span>); <span class="keyword">class="type">class="kw">double</span> ScoreRF = (RF - MinRF); <span class="keyword">class="type">class="kw">double</span> Softplus(<span class="keyword">class="type">class="kw">double</span> x) { <span class="keyword">class="kw">return</span>(<span class="functions">MathLog</span>(<span class="number">class="num">1</span> + <span class="functions">MathExp</span>(x))); } <span class="keyword">class="type">class="kw">double</span> Sigmoid(<span class="keyword">class="type">class="kw">double</span> x) { <span class="keyword">class="kw">return</span>(<span class="number">class="num">1</span> / (<span class="number">class="num">1</span> + <span class="functions">MathExp</span>(-x))); } <span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> MinRF = <span class="number">class="num">5.0</span>; <span class="keyword">sinput</span> <span class="keyword">class="type">class="kw">double</span> SigCorrection = <span class="number">class="num">5</span>; <span class="keyword">class="type">class="kw">double</span> RF = <span class="functions">TesterStatistics</span>(<span class="macro">STAT_RECOVERY_FACTOR</span>); <span class="keyword">class="type">class="kw">double</span> DeviationRF = RF - MinRF; <span class="keyword">class="type">class="kw">double</span> CorrectedSigmoid(<span class="keyword">class="type">class="kw">double</span> x) { <span class="keyword">class="kw">return</span>(<span class="number">class="num">1</span> / (<span class="number">class="num">1</span> + <span class="functions">MathExp</span>(-(x - SigCorrection)))); } <span class="keyword">class="type">class="kw">double</span> ScoreRF = CorrectedSigmoid(DeviationRF); <span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> TargetTrades = <span class="number">class="num">100</span>; <span class="keyword">class="type">class="kw">double</span> Trades = <span class="functions">TesterStatistics</span>(<span class="macro">STAT_TRADES</span>);
用 sigmoid 与 tanh 给回测评分
在 MT5 策略测试器里做参数寻优时,光看盈利曲线容易过拟合。把成交次数和恢复因子(Recovery Factor)各自归一化成 0~1 的分数,能逼着优化器兼顾稳定性与样本量。 先算成交偏离:DeviationTrades = 实际成交数 Trades 减去目标 TargetTrades(默认 100)。Sigmoid 函数把任意实数压到 (0,1),其 4 倍导数 Deriv4Sigmoid 在 0 附近最陡、两端趋零,意味着成交数贴近目标时分数最高,偏离越多惩罚越重。 恢复因子一侧设了输入参数 MinRF = 5.0 作为及格线。DeviationRF = 测试器 STAT_RECOVERY_FACTOR 减去 MinRF,再喂进 CorrectedRSTanh:用 MathTanh(x + TanhCorrection) 做平移(TanhCorrection 默认 2.5,2~3 都合理),加 1 除 2 把输出从 (-1,1) 重标定到 (0,1)。这样 RF 刚过 5 时分数爬升快,后续边际增益放缓。 另一版 DerivTanh 直接用 1 - x^2 当成交分数,是 tanh 导数的形状,但不做 rescale,分数可能为负。两种都可在 MT5 里复制验证,调 TargetTrades 和 MinRF 看优化曲面怎么变。外汇与贵金属杠杆高,回测分数好不等于实盘能复现,参数需谨慎。
class="type">class="kw">double DeviationTrades = Trades - TargetTrades; class="type">class="kw">double Sigmoid(class="type">class="kw">double x) { class="kw">return(class="num">1 / (class="num">1 + MathExp(-x))); } class="type">class="kw">double Deriv4Sigmoid(class="type">class="kw">double x) { class="kw">return(class="num">4 * (Sigmoid(x) * (class="num">1 - Sigmoid(x)))); } class="type">class="kw">double ScoreTrades = Deriv4Sigmoid(DeviationTrades); class="kw">input class="type">class="kw">double MinRF = class="num">5.0; sinput class="type">class="kw">double TanhCorrection = class="num">2.5; class=class="str">"cmt">// Anything between class="num">2 and class="num">3 would be reasonable(see last figure) class="type">class="kw">double RF = TesterStatistics(STAT_RECOVERY_FACTOR); class="type">class="kw">double DeviationRF = RF - MinRF; class="type">class="kw">double CorrectedRSTanh(class="type">class="kw">double x) { class="kw">return((MathTanh(x + TanhCorrection) + class="num">1) / class="num">2); class=class="str">"cmt">// rescaled and corrected } class="type">class="kw">double ScoreRF = CorrectedRSTanh(DeviationRF); class="kw">input class="type">class="kw">double TargetTrades = class="num">100; class="type">class="kw">double Trades = TesterStatistics(STAT_TRADES); class="type">class="kw">double DeviationTrades = Trades - TargetTrades; class="type">class="kw">double DerivTanh(class="type">class="kw">double x) { class="kw">return(class="num">1 - MathPow(x, class="num">2)); }class="type">class="kw">double ScoreTrades = DerivTanh(DeviationTrades);
◍ 把这条线请下神坛
这一路把机器学习优化和遗传、全局寻优的关系拆开看,核心落在激活函数怎么改写自定义准则。ReLU 与 Softplus 在梯度消失、爆炸上的硬伤,把我们逼向类 Sigmoid 曲线——它能在 x>t 单边界或 t1<x<t2 双边界里给分,同时压住梯度爆炸、缓住消失。 校正偏移量与目标偏移量那一步,本质是把评分焦点钉在你想观测的属性区间,而不是让优化器满世界乱跑。这套东西没有圣杯属性,外汇与贵金属杠杆市场里照搬易放大回撤,高风险的帽子摘不掉。 下一篇会动刀改这些标准函数,加缩放与加权,再给几个自定义准则的实跑例子。线已铺好,剩下是你在 MT5 里把 BaseFunctions.mqh 拖进去改两行偏移量验证的事。