MQL5中使用坐标下降法的弹性网络回归(基础篇)
📘

MQL5中使用坐标下降法的弹性网络回归(基础篇)

第 1/3 篇

◍ 在MT5里跑弹性网络回归

MQL5 里可以直接用坐标下降法做弹性网络回归,这套思路适合处理外汇与贵金属多因子建模时的共线性问题,但杠杆品种波动剧烈,回测结论只代表历史概率,实盘仍属高风险。 实现上不依赖外部库,核心是在每次迭代里对单个系数做软阈值更新,L1 与 L2 惩罚项按比例 α 混合,α 靠近 1 偏 Lasso、靠近 0 偏 Ridge。 原文示例发布于 2024 年 3 月 5 日,在 MetaTrader 5 社区展示后获得 941 次浏览与 4 条讨论,说明这类纯 MQL5 数值计算方案在量化交易者中有真实需求。 开 MT5 新建脚本把下面这段坐标下降骨架抄进去,先拿 EURUSD 的 H1 收盘价与两三个动量因子试跑,调 α 看系数稀疏度变化。

「为什么交易策略总在过拟合上栽跟头」

弹性网络回归把岭回归的 L2 惩罚和拉索回归的 L1 惩罚揉进同一个线性模型里,本质是用双重约束换掉普通最小二乘的放任。对做外汇和贵金属的人来讲,这套东西不是学院派玩具——过拟合正是策略上线即失效的头号原因,把训练期的随机噪声当成可重复模式,回测漂亮实盘崩。 本文要在纯 MQL5 里用优化坐标下降实现弹性网络,不依赖外部库。后面会拿一个简单的移动平均预测策略做演示,你能直接把代码丢进 MT5 跑一遍,看惩罚项怎么把拟合曲线从“记住噪声”拉回“捕捉倾向”。 外汇与贵金属属高杠杆品种,任何模型输出的都只是概率倾向,实盘前请在策略测试器用不同品种周期交叉验证。

用弹性网络给策略因子瘦身

预测模型上线前最怕的不是拟合不够,而是把训练样本里的噪声当成了规律。变量一多,冗余因子就会拉高训练偏差,实盘立刻露馅。正则化就是压住这种过拟合的手段。 拉索(Lasso)靠 L1 罚项把不重要因子的系数直接压到 0,相当于自动做因子筛选;岭回归(Ridge)用 L2 罚项只缩小系数、不归零,所有因子都留着但整体更平滑。弹性网络(Elastic Net)把两者揉在一起,罚项是绝对值和与平方和的组合,由 alpha 和 lambda 两个超参加权。 alpha 决定偏向哪边:取 0 时退化为纯岭(L2),取 1 时退化为纯拉索(L1),中间值则线性混合。lambda 控制惩罚力度,越大模型越简单。 放到 MT5 策略开发里,这东西很实用——我们常堆十几个指标碰运气,弹性网络能在不关心因子相关性的前提下,把没用的指标系数压没,把有预测力的留下来,过度拟合的概率会明显下降。外汇和贵金属波动受事件驱动,高杠杆下模型失效很快,任何因子筛选都只是降低风险而非消除。

◍ 用坐标下降拆掉多维优化的维度墙

坐标下降把多维优化拆成一串一维子问题:每次只沿一个维度去最小化目标函数,其余维度暂时冻结。对做策略的人而言,它最实用的落点在于弹性网络回归的两步调用。 第一步用用户给定的固定 alpha 去搜最优 lambda;lambda 定了以后,再跑一遍同一套优化逻辑,把回归方程的 beta 系数算出来。这两步共用一个下降循环,只是目标变量换了。 想验证就开 MT5 自建脚本:先锁 alpha 跑 lambda 扫描,再固定 lambda 解 beta,对比 sklearn 的 ElasticNet 路径看偏差。外汇与贵金属杠杆高,回测结论只代表历史样本,实盘仍可能失效。

「兼容 MT4 的矩阵压平与坐标下降类骨架」

做跨平台库时,这套实现刻意不碰 MQL5 的矩阵/向量新类型,全部用一维平面数组模拟矩阵,目的是保住和 MQL4 的兼容。比如要建 4 行 3 列的矩阵,就开一个长度为 4×3=12 的普通数组,成员按行依次排:先第一行 3 个,再第二行,以此类推。遍历时用「行索引×列数 + 列索引」定位,所有这类数组在变量或参数名后都挂 _matrix 后缀,调用时另用参数传维数。 CCoordinateDescent 的构造函数吃 4 个参数:num_predictors 是预测器(指标)列数;num_observations 是每列的行数;use_covariance_updates 是布尔量,仅在观测数大于预测器数(num_observations > num_predictors)时建议开 true,执行时间会明显缩短;num_lambdas_to_trial 限定训练里试 lambda 的最大个数。构造只铺内部数据结构,不碰具体数据。 灌数据走 SetData:首参起始索引方便后面交叉验证;num_observations 这里若小于构造时的值,索引越界会回卷到数组头,不需要就设成和构造一致,但绝不能 ≤0。xx_matrix 按前面说的压平矩阵塞原始指标,大小必须是 num_observations*num_predictors;yy 是对应目标值。方法内部先标准化再拷进缓冲,返回 true 后才准调 Train() 或 TrainLambda()。 Train() 里定正则类型 alpha、强度 lambda、收敛测试 fast_test 与精度 convergence_criterion;maxits 防死循环,设 1000 往上较稳;warm_start 控制 beta 是否从零初始化。慢测试(fast_test=false)看目标方差解释率逐轮变化小于标准即停,快测试看所有 beta 最大调整量小于标准即停。TrainLambda() 用 GetLambdaThreshold() 给的 lambda(全 beta 为零处)做起点,循环调 Train() 最多存 m_nlambda 组 beta。外汇/贵金属样本噪声大,lambda 搜过头容易过拟合,调参时建议先用 covariance 更新提速、小步试 lambda。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Coordinate Descent optimization class&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</span> CCoordinateDescent
&nbsp;&nbsp;{
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_initialized;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Was everything legal and allocs successful?</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_beta[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Beta coefs(m_nvars of them)</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_explained;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Fraction of variance m_explained by model; computed by Train()</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_xmeans[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Mean of each X predictor</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_xscales[];&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// And standard deviation</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_ymean;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Intercept(mean of Y)</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_yscale;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Standard deviation of Y</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_nvars ;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Number of variables</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_observs ;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Number of cases</span>
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_covarupdates ; <span class="comment">class=class="str">"cmt">// Does user want(often faster) covariance update method?</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_nlambda ;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Reserve space for this many m_beta sets for saving by TrainLambda() (may be zero)</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_lambdabeta_matrix[];&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Saved m_beta coefs(m_nlambda sets of m_nvars of them)</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_lambdas[];&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Lambdas tested by TrainLambda()</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_x_matrix[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Normalized(mean=class="num">0, std=class="num">1) X; m_observs by m_nvars</span>

坐标下降类的存取接口与内部状态

CCoordinateDescent 把回归过程的中间量都塞进了私有数组:m_y[] 是均值0、标准差1的标准化Y,m_resid[] 存残差,m_xinner_matrix[] 与 m_yinner[] 仅在开启协方差增量更新(m_covarupdates)时保留 Nvars 阶内积矩阵和向量,用来避免每步重算全样本。 公开侧只暴露只读 accessor:GetYmean / GetYscale 拿Y的归一化参数,GetExplainedVariance 回传已解释方差比例,GetBetaAt 与 GetLambdaAt 按索引取系数和惩罚项——索引越界时统一返回0而非抛错,调用方得自己保证下标合法。 构造签名 CCoordinateDescent(num_predictors, num_observations, use_covariance_updates, num_lambdas_to_trial) 表明:预测器个数、样本数、是否走协方差更新、要试几条 lambda 路径,全在实例化时定死。想在 MT5 里复用这个类,先确认你的 num_observations 与历史 Bar 数对齐,否则 GetXmeansAt 返回的均值会偏。

MQL5 / C++
  class="type">class="kw">double                m_y[];                 class=class="str">"cmt">// Normalized(mean=class="num">0, std=class="num">1) Y
  class="type">class="kw">double                m_resid[];              class=class="str">"cmt">// Residual
  class="type">class="kw">double                m_xinner_matrix[];      class=class="str">"cmt">// Nvars square inner product matrix if m_covarupdates
  class="type">class="kw">double                m_yinner[];             class=class="str">"cmt">// Nvars XY inner product vector if m_covarupdates
class="kw">public:
                      class=class="str">"cmt">//constructor
                      CCoordinateDescent(const class="type">int num_predictors, const class="type">int num_observations, const class="type">bool use_covariance_updates, const class="type">int num_lambdas_to_trial) ;
                      class=class="str">"cmt">//desctructor
                      ~CCoordinateDescent(class="type">void) ;
                      
                      class=class="str">"cmt">//Accessor methods for class="kw">private properties
  class="type">class="kw">double                GetYmean(class="type">void)                       { class="kw">return m_ymean; }
  class="type">class="kw">double                GetYscale(class="type">void)                      { class="kw">return m_yscale;}
  class="type">class="kw">double                GetExplainedVariance(class="type">void)           { class="kw">return m_explained;}
  class="type">class="kw">double                GetXmeansAt(const class="type">int index)         { if(index>=class="num">0 && index<ArraySize(m_xmeans)) class="kw">return m_xmeans[index]; else class="kw">return class="num">0;}
  class="type">class="kw">double                GetXscalesAt(const class="type">int index)        { if(index>=class="num">0 && index<ArraySize(m_xscales)) class="kw">return m_xscales[index]; else class="kw">return class="num">0;}
  class="type">class="kw">double                GetBetaAt(const class="type">int index)           { if(index>=class="num">0 && index<ArraySize(m_beta)) class="kw">return m_beta[index]; else class="kw">return class="num">0;}
  class="type">class="kw">double                GetLambdaAt(const class="type">int index)         { if(index>=class="num">0 && index<ArraySize(m_lambdas)) class="kw">return m_lambdas[index]; else class="kw">return class="num">0;}
  class="type">class="kw">double                GetLambdaBetaAt(const class="type">int index)     { if(index>=class="num">0 && index<ArraySize(m_lambdabeta_matrix)) class="kw">return m_lambdabeta_matrix[index]; else class="kw">return class="num">0;}

常见问题

看样本外表现是否明显低于样本内,或参数微调后收益断崖式下跌。建议用不同年份数据交叉验证,贵金属和外汇波动大,高风险下更需谨慎。
它同时带L1和L2惩罚,能把不重要因子权重压到0实现瘦身,又缓解共线因子抖动。复制文中坐标下降代码即可在本地给策略因子降维。
小布盯盘的AIGC可内置因子共线和过拟合倾向扫描,打开对应品种页就能看到诊断,不用自己写坐标下降代码。
用坐标下降法每次只调一个维度,拆掉维度墙。文中MT4兼容的矩阵压平骨架可直接套,迭代次数设50内通常够用。
内部权重向量和迭代计数要一起存取,漏了计数会从头跑。按文中存取接口封装,避免手改数组长度导致越界。