MQL5中使用坐标下降法的弹性网络回归·进阶篇
📉

MQL5中使用坐标下降法的弹性网络回归·进阶篇

(2/3)·从正则化原理到纯MQL5代码实现,用坐标下降把过拟合策略拉回正轨

新手友好 第 2/3 篇
很多人把十几个指标一股脑塞进策略,以为参数越多越能抓行情,结果只是把噪声当成了规律。弹性网络回归能在保留有用因子的同时压住冗余变量,本篇用坐标下降在MQL5里把它跑通。

「坐标下降类的接口与矩阵预填实测」

在 MT5 里做正则化回归,先得把坐标下降求解器的对外接口钉死。下面这组声明给出了三个核心方法:GetLambdaThreshold 按显著性水平 alpha 返回 lambda 阈值;SetData 灌入起始索引、样本数、自变量矩阵 xx_matrix 与因变量 yy;Train 与 TrainLambda 分别跑固定 lambda 训练与lambda 路径搜索。 //Set model parameters and raw input data double GetLambdaThreshold(const double alpha) ; bool SetData(const int begin, const int num_observations, double &xx_matrix[], double &yy[]) ; //Training routines void Train(const double alpha, const double lambda, const int maxits, const double convergence_criterion, const bool fast_test, const bool warm_start) ; void TrainLambda(const double alpha, const int maxits, const double convergence_criterion, const bool fast_test, const double maxlambda, const bool print_steps) ; 验证数组布局不用绕弯。直接开 4 行 3 列的扁平矩阵,用双重循环把每列填成 1、2、3,ArrayPrint 后终端会原样吐出 12 个数:1.0 2.0 3.0 重复 4 次。外汇与贵金属行情的高波动会让这类特征缩放敏感,实盘前务必标准化。 int rows=4, columns=3; double our_matrix[]; ArrayResize(our_matrix,rows*columns); /* Creating matrix with columns of 1s,2s,3s */ for(int i = 0; i<rows; i++) for(int j=0; j<columns; j++) our_matrix[i*columns+j]=j+1; ArrayPrint(our_matrix); 构造函数负责把维度与开关落进成员变量。num_predictors、num_observations、num_lambdas_to_trial 任一为负就直接置 m_initialized=false 并报警,避免后续训练在脏状态下跑出无意义系数。 CCoordinateDescent::CCoordinateDescent( const int num_predictors, const int num_observations, const bool use_covariance_updates, const int num_lambdas_to_trial ) { m_nvars = num_predictors ; m_observs = num_observations ; m_covarupdates = use_covariance_updates ; m_nlambda = num_lambdas_to_trial ; m_initialized=true; m_ymean=m_yscale=m_explained=0;

if(m_nvars<0m_observs<0m_nlambda<0)

{ m_initialized=false; Print("Invalid parameter value, neither num_predictors ,num_observations, nor num_lambdas_to_trial can be negative"); } }

MQL5 / C++
class="type">class="kw">double GetLambdaThreshold(const class="type">class="kw">double alpha) ;
class=class="str">"cmt">//Set model parameters and raw input data
class="type">bool SetData(const class="type">int begin, const class="type">int num_observations, class="type">class="kw">double &xx_matrix[], class="type">class="kw">double &yy[]) ;
class=class="str">"cmt">//Training routines
class="type">void Train(const class="type">class="kw">double alpha, const class="type">class="kw">double lambda, const class="type">int maxits, const class="type">class="kw">double convergence_criterion, const class="type">bool fast_test, const class="type">bool warm_start) ;
class="type">void TrainLambda(const class="type">class="kw">double alpha, const class="type">int maxits, const class="type">class="kw">double convergence_criterion, const class="type">bool fast_test, const class="type">class="kw">double maxlambda, const class="type">bool print_steps) ;
class="type">int rows=class="num">4,
    columns=class="num">3;
class="type">class="kw">double our_matrix[];
ArrayResize(our_matrix,rows*columns);
class=class="str">"cmt">/*
   Creating matrix with columns of 1s,2s,3s
*/
for(class="type">int i = class="num">0; i<rows; i++)
  for(class="type">int j=class="num">0; j<columns; j++)
    our_matrix[i*columns+j]=j+class="num">1;
ArrayPrint(our_matrix);
CCoordinateDescent::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
)
{
  m_nvars = num_predictors ;
  m_observs = num_observations ;
  m_covarupdates = use_covariance_updates ;
  m_nlambda = num_lambdas_to_trial ;
  m_initialized=true;
  m_ymean=m_yscale=m_explained=class="num">0;
  if(m_nvars<class="num">0 || m_observs<class="num">0 || m_nlambda<class="num">0)
    {
      m_initialized=false;
      Print("Invalid parameter value, neither num_predictors ,num_observations, nor num_lambdas_to_trial can be negative");
    }

内存分配失败如何拖垮回归训练

坐标下降回归类在初始化阶段会一次性为多个矩阵申请内存:观测数乘变量数的 X 矩阵、Y 向量、均值与缩放向量、系数 Beta、残差等。只要任意一个 ArrayResize 返回值小于预期长度,m_initialized 就被置为 false,后续所有计算直接跳过。 如果开启了协方差增量更新(m_covarupdates),还会额外分配 m_nvars*m_nvars 的内积矩阵与长度为 m_nvars 的 Y 内积向量;若 λ 路径点数 m_nlambda 大于 0,则需再分配 m_nlambda*m_nvars 的 λ 系数矩阵与长度 m_nlambda 的 λ 序列。任何一块不足都会让初始化失败并打印 GetLastError 代码。 SetData 是真正灌入行情数据的入口。它先校验 m_initialized,再检查 begin 与 num_observations 不能为负;接着要求外部传入的 xx_matrix 尺寸至少达到 m_observs*m_nvars、yy 长度至少 m_observs,否则报“数据不足”并返回 false。 在 MT5 里复现这套逻辑时,建议先打印 m_observs 与 m_nvars 的实际值,再对照 ArrayResize 的返回值。外汇与贵金属市场高杠杆、跳空频繁,样本窗口设得过长容易因内存或边界回绕出错,调小观测数往往能立刻暴露分配隐患。

MQL5 / C++
   class="kw">return;
   }   
   
   if(ArrayResize(m_x_matrix,m_observs*m_nvars)<m_observs*m_nvars   ||
       ArrayResize(m_y,m_observs)<m_observs                ||
       ArrayResize(m_xmeans,m_nvars)<m_nvars               ||
       ArrayResize(m_xscales,m_nvars)<m_nvars              ||
       ArrayResize(m_beta,m_nvars)<m_nvars                ||
       ArrayResize(m_resid,m_observs)<m_observs)
      m_initialized=false;
class=class="str">"cmt">//---conditional allocation
   if(m_covarupdates)
     {
       if(ArrayResize(m_xinner_matrix,m_nvars*m_nvars)<m_nvars*m_nvars||
           ArrayResize(m_yinner,m_nvars)<m_nvars)
           m_initialized=false;
     }
class=class="str">"cmt">//---
   if(m_nlambda>class="num">0)
     {
       if(ArrayResize(m_lambdabeta_matrix,m_nlambda*m_nvars)<m_nlambda*m_nvars ||
           ArrayResize(m_lambdas,m_nlambda)<m_nlambda)
           m_initialized=false;
     }
class=class="str">"cmt">//---class="kw">return immediately if any error
   if(!m_initialized)
      Print("Memory allocation error ", GetLastError());
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Get and standardize the data                                      |
class=class="str">"cmt">//|   Also compute inner products if covar_update                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCoordinateDescent::SetData(
   const class="type">int begin,      class=class="str">"cmt">// Starting index in full database for getting m_observs of training set
   const class="type">int num_observations,class=class="str">"cmt">// Number of cases in full database(we wrap back to the start if needed)
   class="type">class="kw">double  &xx_matrix[], class=class="str">"cmt">// Full database(num_observations rows, m_nvars columns)
   class="type">class="kw">double  &yy[]        class=class="str">"cmt">// Predicted variable vector, num_observations class="type">long
)
{
   if(!m_initialized)
      class="kw">return false;
   class=class="str">"cmt">// parameter checks  
   if(begin<class="num">0 || num_observations<class="num">0)
     {
       Print("Invalid parameter value: neither begin nor num_observations can be negative");
       class="kw">return false;
     }
   class=class="str">"cmt">//--- invalid a  
   if(ArraySize(xx_matrix)<(m_observs*m_nvars) || ArraySize(yy)<m_observs)
     {
       Print("Insufficient data supplied relative to object specification");
       class="kw">return false;
     }
   class=class="str">"cmt">//---  
   class="type">int icase, ivar, jvar, k,xptr;
   class="type">class="kw">double sum, xm, xs, diff;
   class=class="str">"cmt">/*
      Standardize X
   */
   for(ivar=class="num">0 ; ivar<m_nvars ; ivar++)
     {

◍ 标准化与协方差矩阵的预处理细节

做多元回归前,先把自变量和因变量都标准化,否则量纲不同的品种(比如黄金点数和美元指数)会互相压制权重。下面这段逻辑对第 ivar 个自变量先求均值 xm,再除以样本数 m_observs 得到 m_xmeans[ivar],随后用 1.e-60 兜底防止后续除以零——外汇与贵金属序列里出现常数段并不罕见,这行不能省。 方差 xs 从 1.e-60 起累加 (x-xm)^2,开平方后存为 m_xscales[ivar],再用 (x-xm)/xs 把原矩阵写回 m_x_matrix。因变量 Y 走完全一样的流程,m_yscale 同样以 1.e-60 垫底,标准化后 m_y 的均值理论为 0、标准差理论为 1。 若 m_covarupdates 为真,代码会预存内积:m_yinner[ivar] 是 Xi 与 Y 的协方差估计(除以 m_observs),而 m_xinner_matrix 存 XiXj。对角线直接置 1.0,因为 X 已标准化;下三角 jvar<ivar 时利用对称性直接复制,避免重复计算。 在 MT5 里跑这套,重点看 m_xscales 有没有卡在 1.e-60——若多个变量都触底,说明该窗口内这些序列近乎常数,回归系数会失真,贵金属窄幅震荡时段尤其要警惕这种高风险情形。

MQL5 / C++
xm = class="num">0.0 ;
for(icase=class="num">0 ; icase<m_observs ; icase++)
  {
   k = (icase + begin) % num_observations ;
   xm += xx_matrix[k*m_nvars+ivar] ;
   }
xm /= m_observs ;
m_xmeans[ivar] = xm ;
xs = class="num">1.e-60 ;  class=class="str">"cmt">// Prevent division by zero later
for(icase=class="num">0 ; icase<m_observs ; icase++)
  {
   k = (icase + begin) % num_observations ;
   diff = xx_matrix[k*m_nvars+ivar] - xm ;
   xs += diff * diff ;
   }
xs = sqrt(xs / m_observs) ;
m_xscales[ivar] = xs ;
for(icase=class="num">0 ; icase<m_observs ; icase++)
  {
   k = (icase + begin) % num_observations ;
   m_x_matrix[icase*m_nvars+ivar] = (xx_matrix[k*m_nvars+ivar] - xm) / xs ;
   }
  }
 class=class="str">"cmt">/*
   Standardize Y
 */
 m_ymean = class="num">0.0 ;
 for(icase=class="num">0 ; icase<m_observs ; icase++)
   {
   k = (icase + begin) % num_observations ;
   m_ymean += yy[k] ;
   }
 m_ymean /= m_observs ;
 m_yscale = class="num">1.e-60 ;  class=class="str">"cmt">// Prevent division by zero later
 for(icase=class="num">0 ; icase<m_observs ; icase++)
   {
   k = (icase + begin) % num_observations ;
   diff = yy[k] - m_ymean ;
   m_yscale += diff * diff ;
   }
 m_yscale = sqrt(m_yscale / m_observs) ;
 for(icase=class="num">0 ; icase<m_observs ; icase++)
   {
   k = (icase + begin) % num_observations ;
   m_y[icase] = (yy[k] - m_ymean) / m_yscale ;
   }

 class=class="str">"cmt">/*
    If user requests covariance updates, compute required inner products
    We store the full m_xinner_matrix matrix for faster addressing later,
    even though it is symmetric.
    We handle both unweighted and weighted cases here.
 */
 if(m_covarupdates)
   {
   for(ivar=class="num">0 ; ivar<m_nvars ; ivar++)
     {
     xptr = ivar ;
     class=class="str">"cmt">// Do XiY
     sum = class="num">0.0 ;

       for(icase=class="num">0 ; icase<m_observs ; icase++)
         sum += m_x_matrix[xptr+icase*m_nvars] * m_y[icase] ;
     m_yinner[ivar] = sum / m_observs ;
     class=class="str">"cmt">// Do XiXj

       for(jvar=class="num">0 ; jvar<m_nvars ; jvar++)
         {
         if(jvar == ivar)
           m_xinner_matrix[ivar*m_nvars+jvar] = class="num">1.0 ;       class=class="str">"cmt">// Recall that X is standardized
         else
         if(jvar < ivar)              class=class="str">"cmt">// Matrix is symmetric, so just copy

「坐标下降训练里的矩阵对称与阈值初始化」

这段逻辑先处理内积矩阵的对称性:当变量下标 ivar 与 jvar 相等或已算过转置位置时,直接复制已有值,避免重复计算。 若未对称填充,则对 m_observs 条样本做点积累加,再除以样本数得到均值内积,写回 m_xinner_matrix[ivar*m_nvars+jvar]。这一步把原始特征矩阵压缩成协方差类结构,供后续回归使用。 Train() 入口先做参数护栏:alpha 必须落在 [0,1],lambda 与 maxits 必须为正数,否则 Print 报错直接返回。实际调参时 alpha 取 0 会让 lambda 递减路径出问题,建议至少留 1e-3 以上。 初始化段把软阈值算子门槛设为 S_threshold = alpha * lambda,并令 prior_crit = 1.0e60 作为首轮收敛判据的占位极大值。开 MT5 把这段抄进 EA,改 alpha 从 0.1 试到 0.9,能直接观察门槛对系数稀疏化的影响。外汇与贵金属杠杆高,回测结论仅代表历史样本倾向,实盘可能失效。

MQL5 / C++
m_xinner_matrix[ivar*m_nvars+jvar] = m_xinner_matrix[jvar*m_nvars+ivar] ;
   else
      {
       sum = class="num">0.0 ;
       for(icase=class="num">0 ; icase<m_observs ; icase++)
          sum += m_x_matrix[xptr+icase*m_nvars] * m_x_matrix[icase*m_nvars+jvar] ;
       m_xinner_matrix[ivar*m_nvars+jvar] = sum / m_observs ;
      }
     }
   } class=class="str">"cmt">// For ivar
  }
class=class="str">"cmt">//---
   class="kw">return true;  
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Core training routine                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCoordinateDescent::Train(
   const class="type">class="kw">double alpha,      class=class="str">"cmt">// User-specified alpha, (class="num">0,class="num">1) (class="num">0 problematic for descending lambda)
   const class="type">class="kw">double lambda,     class=class="str">"cmt">// Can be user-specified, but usually from TrainLambda()
   const class="type">int maxits,        class=class="str">"cmt">// Maximum iterations, for safety only
   const class="type">class="kw">double convergence_criterion,      class=class="str">"cmt">// Convergence criterion, typically class="num">1.e-5 or so
   const class="type">bool fast_test,    class=class="str">"cmt">// Base convergence on max m_beta change vs m_explained variance?
   const class="type">bool warm_start    class=class="str">"cmt">// Start from existing m_beta, rather than zero?
)
{
   if(!m_initialized)
      class="kw">return;

   if(alpha<class="num">0 || alpha>class="num">1)
      {
       Print("Invalid parameter value: Legal values for alpha are between class="num">0 and class="num">1 inclusive");
       class="kw">return;
      }

   if(lambda<class="num">0)
      {
       Print("Invalid parameter value: lambda accepts only positive values");
       class="kw">return;
      }

   if(maxits<=class="num">0)
      {
       Print("Invalid parameter value: maxist accepts only non zero positive values");
       class="kw">return;
      }

   class="type">int i, iter, icase, ivar, kvar, do_active_only, active_set_changed, converged,xptr ;
   class="type">class="kw">double residual_sum, S_threshold, argument, new_beta, correction, update_factor ;
   class="type">class="kw">double sum, explained_variance, crit, prior_crit, penalty, max_change, Xss, YmeanSquare ;
   class=class="str">"cmt">/*
      Initialize
   */
   S_threshold = alpha * lambda ;      class=class="str">"cmt">// Threshold for the soft-thresholding class="kw">operator S()
   do_active_only = class="num">0 ;                class=class="str">"cmt">// Begin with a complete pass
   prior_crit = class="num">1.0e60 ;               class=class="str">"cmt">// For convergence test

弹性网回归的预热与迭代初始化

在 MT5 里跑弹性网(Elastic Net)类回归模型时,warm_start 开关决定了 beta 系数的起点。若设为 true 且未启用协方差更新(m_covarupdates 为 false),代码会按观测数 m_observs 重算残差:用当前 beta 与自变量矩阵 m_x_matrix 做内积,再用 m_y 减掉得到 m_resid,这一步在样本量小于变量数时尤其不能省。 若 warm_start 为 false,所有 m_beta 直接清零,初始残差就是因变量 m_y 本身——相当于从零开始下降。YmeanSquare 被固定为 1.0,仅用于后续给用户展示解释方差,不参与训练更新。 外层 for 循环以 maxits 为上限反复迭代,每次先清空 active_set_changed 与 max_change 标记。变量遍历时若 do_active_only 且 beta 已为 0.0 则跳过;update_factor 由标准化常量 Xss(此处为 1)加 lambda*(1.0-alpha) 构成。启用协方差更新时用 m_xinner_matrix 算 residual_sum,未启用则走逐观测的慢速定义式,适合 m_observs < m_nvars 的场景。 外汇与贵金属行情具有高杠杆、高波动风险,上述初始化逻辑仅影响模型拟合起点,不预示任何价格方向;开 MT5 把这段接进你的 EA 回测,对比 warm_start 开关节省的时间差异即可验证。

MQL5 / C++
if(warm_start)                 class=class="str">"cmt">// Pick up with current betas?
    {
    if(! m_covarupdates)                class=class="str">"cmt">// If not class="kw">using covariance updates, must recompute residuals
      {
      for(icase=class="num">0 ; icase<m_observs ; icase++)
        {
        xptr = icase * m_nvars ;
        sum = class="num">0.0 ;
        for(ivar=class="num">0 ; ivar<m_nvars ; ivar++)
          sum += m_beta[ivar] * m_x_matrix[xptr+ivar] ;
        m_resid[icase] = m_y[icase] - sum ;
        }
      }
    }
  else                            class=class="str">"cmt">// Not warm start, so initial betas are all zero
    {
    for(i=class="num">0 ; i<m_nvars ; i++)
      m_beta[i] = class="num">0.0 ;
    for(i=class="num">0 ; i<m_observs ; i++)      class=class="str">"cmt">// Initial residuals are just the Y variable
      m_resid[i] = m_y[i] ;
    }
class=class="str">"cmt">// YmeanSquare will remain fixed throughout training.
class=class="str">"cmt">// Its only use is for computing m_explained variance for the user&class="macro">#x27;s edification.
  YmeanSquare = class="num">1.0 ;
  class=class="str">"cmt">/*
      Outmost loop iterates until converged or user&class="macro">#x27;s maxits limit hit
   */
  for(iter=class="num">0 ; iter<maxits ; iter++)
    {
    class=class="str">"cmt">/*
        Pass through variables
     */
    active_set_changed = class="num">0 ;   class=class="str">"cmt">// Did any betas go to/from class="num">0.0?
    max_change = class="num">0.0 ;         class=class="str">"cmt">// For fast convergence test
    for(ivar=class="num">0 ; ivar<m_nvars ; ivar++)     class=class="str">"cmt">// Descend on this m_beta
      {
      if(do_active_only  &&  m_beta[ivar] == class="num">0.0)
        class="kw">continue ;
      Xss = class="num">1 ;                class=class="str">"cmt">// X was standardized
      update_factor = Xss + lambda * (class="num">1.0 - alpha) ;
      if(m_covarupdates)       class=class="str">"cmt">// Any sensible user will specify this unless m_observs < m_nvars
        {
        sum = class="num">0.0 ;
        for(kvar=class="num">0 ; kvar<m_nvars ; kvar++)
          sum += m_xinner_matrix[ivar*m_nvars+kvar] * m_beta[kvar] ;
        residual_sum = m_yinner[ivar] - sum ;
        argument = residual_sum + Xss * m_beta[ivar] ;   class=class="str">"cmt">// Argument to S() [MY FORMULA]
        }
      else
      class=class="str">"cmt">// Use slow definitional formula(okay if m_observs < m_nvars)
        {
        residual_sum = class="num">0.0 ;
        xptr = ivar ;     class=class="str">"cmt">// Point to column of this variable

◍ 正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(icase=<span class="number">0</span> ; icase&lt;m_observs ; icase++) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;residual_sum += m_x_matrix[xptr+icase*m_nvars] * m_resid[icase] ;&nbsp;&nbsp;<span class="comment">// X_ij * RESID_i</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; residual_sum /= m_observs ; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; argument = residual_sum + m_beta[ivar] ;&nbsp;&nbsp;<span class="comment">// Argument to S() ;&nbsp;&nbsp;&nbsp;&nbsp;(Eq 8)</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">// Apply the soft-thresholding operator S()</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(argument &gt; <span class="number">0.0</span>&nbsp;&nbsp;&amp;&amp;&nbsp;&nbsp;S_threshold &lt; argument) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new_beta = (argument - S_threshold) / update_factor ; &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;<span class="keyword">if</span>(argument &lt; <span class="number">0.0</span

把择参重复劳动交给小布
小布盯盘已内置正则化诊断视图,打开对应品种页就能看到因子权重衰减曲线,你只需判断哪些指标该留、哪些该砍。

常见问题

alpha为0时惩罚退化为L2岭回归,保留全部预测因子但压缩系数;alpha为1时退化为L1拉索,会把无用因子系数直接归零实现筛选。
目前小布提供因子显著性与过拟合风险的可视化,具体回归训练建议在MQL5端按本篇代码执行,再把结果贴回小布做跟踪。
它将多维优化拆成一维子问题逐维最小化,在固定其他维度时单变量更新很高效,尤其适配带L1/L2混合罚项的回归。
历史样本外的行情结构可能突变,模型倾向降低过拟合但不保证盈利,实盘前需用多周期验证并严控仓位。