使用经典机器学习方法预测汇率:逻辑回归(logit)模型和概率回归(probit)模型·进阶篇
📘

使用经典机器学习方法预测汇率:逻辑回归(logit)模型和概率回归(probit)模型·进阶篇

第 2/3 篇

「Logit与Probit的损失函数及拟合入口」

在回归模型里,损失函数的写法决定了优化方向。当 L2_ 开关打开时,正则项取参数向量自点积的一半:L2_reg = 0.5 * params.Dot(params);关闭时直接记 0,这一步只是给后续目标函数加惩罚,不影响梯度外的逻辑。 若走 probit 分支,逐行计算 LLF[i] = target_y_gl[i]*MathLog(probitact[i]) + (1-target_y_gl[i])*MathLog(1-probitact[i]);logit 分支公式完全一致只是换成 logitact[i]。每一行都过 MathIsValidNumber 校验,一旦算出非数立即 break,避免污染整体求和。 最终目标 func = -MathSum(LLF) + L2_reg/(Rows_*C_),也就是负对数似然加缩放后的 L2 惩罚。注意 C_ 默认 1.0,若你调小 C_ 正则权重会上升,参数会更保守。 FitLogitRegression 负责把输入矩阵拼成设计矩阵:先建 Rows×(Cols+1) 的全 1 矩阵 One,再把原 input_X 各列塞进 i+1 列,第 0 列留作截距。参数 w 初始化全 0、s 全 1,停止条件 epsg/epsf/epsx 都设为 0.000001,意味着目标或步长变动低于该量级就退出。外汇与贵金属行情噪声大,这种小容差可能让优化跑很多轮,实盘前建议在 MT5 用历史数据试跑观察耗时。

MQL5 / C++
  logitact[i]= class="num">0.001;
   }
  }
class=class="str">"cmt">//-------------------------------------------------------------------------------------------------
   class="type">class="kw">double L2_reg;
   if(L2_)
      L2_reg = class="num">0.5 * params.Dot(params); class=class="str">"cmt">//  L2_regularization
   else
      L2_reg =class="num">0;
class=class="str">"cmt">//------------------ calculate loss function-------------------------------------------------------------
   if(probit_)
    {
     for(class="type">int i = class="num">0;i<Rows_; i++)
      {
       LLF[i]=target_y_gl[i]*MathLog(probitact[i]) + (class="num">1-target_y_gl[i])*MathLog(class="num">1-probitact[i]) ;
       if(!MathIsValidNumber(LLF[i]))
        {
         break;
        }
      }
    }
   else
    {
     for(class="type">int i = class="num">0;i<Rows_; i++)
      {
       LLF[i]=target_y_gl[i]*MathLog(logitact[i]) + (class="num">1-target_y_gl[i])*MathLog(class="num">1-logitact[i]);
       if(!MathIsValidNumber(LLF[i]))
        {
         break;
        }
      }
    }
   func = -MathSum(LLF) + L2_reg/(Rows_*C_); class=class="str">"cmt">// Negative Loglikelihood + L2_regularization
class=class="str">"cmt">//------------------------------------------------------------------------------------------------------
   func_ = func;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Finding the optimal parameters for the Logit or Probit model       |
class=class="str">"cmt">//+------------------------------------------------------------------+
vector FitLogitRegression(matrix &input_X, vector &target_y,class="type">bool L2 = class="kw">false, class="type">class="kw">double C=class="num">1.0,class="type">bool probit = class="kw">false,class="type">class="kw">double alpha = class="num">0.05)
  {
   L2_=L2;
   probit_ = probit;
   C_ = C;
   class="type">class="kw">double        w[],s[];
   CObject       obj;
   CNDimensional_Logit ffunc;
   CNDimensional_Rep  frep;
   class="type">class="kw">ulong Rows = input_X.Rows();
   class="type">class="kw">ulong Cols = input_X.Cols();
   matrix One=matrix::Ones(class="type">int(Rows),class="type">int(Cols+class="num">1));
   for(class="type">int i=class="num">0;i<class="type">int(Cols); i++)
    {
     One.Col(input_X.Col(i),i+class="num">1);  class=class="str">"cmt">// design matrix
    }
   input_X = One;
   Cols = input_X.Cols();
   Rows_ = class="type">int(Rows);
   Cols_ = class="type">int(Cols);
   Input_X_gl = input_X;
   target_y_gl = target_y;
   ArrayResize(w,class="type">int(Cols));
   ArrayResize(s,class="type">int(Cols));
class=class="str">"cmt">//--- initialization
   ArrayInitialize(w,class="num">0.0);
   ArrayInitialize(s,class="num">1.0);
class=class="str">"cmt">//--- optimization stop conditions
   class="type">class="kw">double epsg=class="num">0.000001;
   class="type">class="kw">double epsf=class="num">0.000001;
   class="type">class="kw">double epsx=class="num">0.000001;

◍ Probit 拟合后的显著性与协方差估计

用 LBFGS 跑完权重优化后,先别急着拿参数去下单,得先确认模型整体是否显著。代码里用似然比检验(LR)做这件事:先算零模型的对数似然 L0,再用 2*(-func_ - L0) 得到 LR 统计量,和自由度为 Cols-1 的卡方分位数 MathQuantileChiSquare(1-alpha, Cols-1, err) 比大小,LR 超过 Chi2 才把 ModelSignificant 置真。 参数协方差矩阵得单独估。对 Probit 模型,先把 input_X 和 parameters 做矩阵乘得到 logit,过一遍 sigmoid 激活;循环里用正态密度平方除以「累积概率乘其补数」得到每样本的 c[i],再聚合成 CovProbit。外汇与贵金属波动具有高风险,这套显著性判断只说明样本内模型可能优于零模型,样本外仍可能失效。 直接把下面这段拷进 MT5 脚本,把 alpha 设成 0.05,跑完看 Print 出来的 LR 和 Chi2 两个数字:若 LR 常年贴着 Chi2 下方,说明你那组特征在统计上倾向没信息量,别拿去喂信号。

MQL5 / C++
  class="type">class="kw">double diffstep=class="num">0.000001;
  class="type">int maxits=class="num">0;
class=class="str">"cmt">//------------------------------
  CMinLBFGSStateShell state;
  CMinLBFGSReportShell rep;
  CAlglib::MinLBFGSCreateF(class="num">1,w,diffstep,state);
  CAlglib::MinLBFGSSetCond(state,epsg,epsf,epsx,maxits);
  CAlglib::MinLBFGSSetScale(state,s);
  CAlglib::MinLBFGSOptimize(state,ffunc,frep,class="num">0,obj);
  CAlglib::MinLBFGSResults(state,w,rep);
  Print("TerminationType ="," ",rep.GetTerminationType());
  Print("IterationsCount ="," ",rep.GetIterationsCount());
  vector parameters=vector::Zeros(Cols);
  for(class="type">int i = class="num">0; i<class="type">int(Cols); i++)
    {
      parameters[i]= w[i];
    }
  Print("Parameters = "," ",parameters);
class=class="str">"cmt">//-------Likelihood Ratio Test LR-----------------------------------------
  class="type">class="kw">double S = target_y.Sum();   class=class="str">"cmt">// number of "success"
  class="type">class="kw">ulong All = target_y.Size(); class=class="str">"cmt">// all data
  class="type">class="kw">double L0 = S*MathLog(S/All) + (All-S)*MathLog((All-S)/All); class=class="str">"cmt">// Log-likelihood for the trivial model
class=class="str">"cmt">//  Print("L0 = ",L0);
class=class="str">"cmt">//  Print("LLF = ",func_);
  class="type">class="kw">double LR;
  LR = class="num">2*(-func_ - L0); class=class="str">"cmt">// Likelihood Ratio Test LR
  class="type">int err;
  class="type">class="kw">double Chi2 = MathQuantileChiSquare(class="num">1-alpha,Cols-class="num">1,err); class=class="str">"cmt">// If H0 true ---> Chi2Distribution(alpha,v)
  Print("LR ",LR," ","Chi2 = ",Chi2);
class=class="str">"cmt">//--------------------------------------------------------------------------------
class=class="str">"cmt">//-------------- calculate if model significant or not
  if(LR > Chi2)
      ModelSignificant = true;
  else
      ModelSignificant = class="kw">false;
class=class="str">"cmt">//----------------------------------------------------
class=class="str">"cmt">//-------------Estimation of the covariance matrix of parameters for the Probit model------------
  vector logit = input_X.MatMul(parameters);  class=class="str">"cmt">//
  vector activation;
  logit.Activation(activation,AF_SIGMOID); class=class="str">"cmt">// Logit activation
  class="type">class="kw">double probit_SE[],probitact[];
  ArrayResize(probit_SE,Rows_);
  for(class="type">int i=class="num">0; i <Rows_; i++)
    {
      probit_SE[i] = logit[i];
    }
  if(probit_)
    {
      class="type">class="kw">ulong size_parameters = parameters.Size();
      matrix CovProbit=matrix::Zeros(class="type">int(size_parameters),class="type">int(size_parameters));
      class="type">int err;
      vector a_=vector::Zeros(Rows_);
      vector b=vector::Zeros(Rows_);
      vector c=vector::Zeros(Rows_);
      vector xt=vector::Zeros(class="type">int(size_parameters));
      for(class="type">int i = class="num">0; i<Rows_; i++)
        {
         a_[i] = MathPow((MathProbabilityDensityNormal(probit_SE[i],class="num">0,class="num">1,err)),class="num">2);
         b[i] = MathCumulativeDistributionNormal(probit_SE[i],class="num">0,class="num">1,err);
         c[i] = a_[i]/(b[i]*(class="num">1-b[i]));

Probit 与 Logit 的参数标准误怎么算

在二元选择模型拟合完后,光看参数本身不够,还得知道每个系数的波动范围。MQL5 里用信息矩阵求逆拿到协方差矩阵,再取对角线开根号,就是标准误(SE)。 Probit 部分直接对累计的 Outer 乘积矩阵 CovProbit 求逆,diag(0) 抽出对角线元素,MathSqrt 之后 Print 出 "Probit_SE"。Logit 部分则先算激活值向量 v = activation*(1-activation),用 R.Diag(v) 做成对角阵,再走 X^T·R·X 得到海森近似,求逆后同样取对角线开方。 两个模型的标准误都通过 Print 输出到 MT5 专家日志,你可以改 input_X 的行数或 c[i] 权重,重跑后比对 SE 变化。外汇与贵金属行情受杠杆与突发事件影响,模型参数标准误放大时,信号失效概率倾向上升,实盘前务必在策略测试器验证。

MQL5 / C++
 xt = input_X.Row(i);
 CovProbit = CovProbit + c[i]*xt.Outer(xt);
 }
 CovProbit = CovProbit.Inv();
 vector SE;
 SE = CovProbit.Diag(class="num">0);
 SE = MathSqrt(SE); class=class="str">"cmt">// standard errors of parameters
 Print("Probit_SE = ", SE);
 }
 else
 {
 class=class="str">"cmt">//-------------Estimation of the covariance matrix of parameters for the Logit model------------
 vector v = vector::Zeros(Rows_);
 for(class="type">int i = class="num">0;i<Rows_; i++)
 {
 v[i] = activation[i]*(class="num">1-activation[i]);
 }
 matrix R,Hesse,X,a,CovLogit;
 R.Diag(v,class="num">0);
 X = input_X.Transpose();
 a = X.MatMul(R);
 Hesse = a.MatMul(input_X);
 CovLogit = Hesse.Inv();
 vector SE;
 SE = CovLogit.Diag(class="num">0);
 SE = MathSqrt(SE); class=class="str">"cmt">// standard errors of parameters
 Print("Logit_SE = ", SE);
 class=class="str">"cmt">//-----------------------------------------------
 }
 class="kw">return parameters;
}

「信号怎么变成真仓位」

预测类别标签的函数 Trade_PredictedTarget 只干一件事:吃进已优化的回归参数,吐出 0/1 信号。1 代表模型倾向开多,0 代表倾向反手空。EA 拿到信号后不做任何加权过滤,直接映射成下单动作。 规则写得很死:signal=1 就开多;本来持多就拿着不动;signal=0 则平掉多头并反手开空。外汇与贵金属杠杆高,这种无过滤反手在跳空时可能瞬时扩大浮亏,实盘前务必在 MT5 策略测试器跑历史。 下面这段是 EA 头部声明,几个 input 直接决定模型形态:InpCount=20 是回看深度,_lag_=4 是特征数量,reoptimize_step=2 表示每 2 根柱重优化一次。把 _probit_ 从 false 改成 true 就切到 probit 而非 logit 链接函数。 别把重优化当免死金牌 每 2 根柱重跑一次优化,样本只有 20 根历史,参数过拟合概率偏高。建议先固定参数手测 EURUSD 的 M15,确认信号翻转频率你能承受再放开自动重优化。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 LogitExpert.mq5 |
class=class="str">"cmt">//|                                                                 Eugene |
class=class="str">"cmt">//|                                                  [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Eugene"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <LogitReg.mqh>
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <Trade\PositionInfo.mqh>
CTrade m_trade;
CPositionInfo m_position;
class="kw">input class="type">class="kw">string  symbol_X       = "EURUSD";      class=class="str">"cmt">// Input symbol
class="kw">input class="type">class="kw">string  symbol_y       = "EURUSD";      class=class="str">"cmt">// Target symbol
class="kw">input class="type">bool    _probit_       = class="kw">false;         class=class="str">"cmt">// Probit model
class="kw">input  class="type">int    InpCount       = class="num">20;             class=class="str">"cmt">// Depth of history
class="kw">input  class="type">int    _lag_          = class="num">4;              class=class="str">"cmt">// Number of features
class="kw">input class="type">bool    _L2_           = class="kw">false;          class=class="str">"cmt">// L2_regularization
class="kw">input class="type">class="kw">double  _C_            = class="num">1;              class=class="str">"cmt">// C(class="num">0,class="num">1) inverse of regularization strength
class="kw">input class="type">class="kw">double  alpha_         = class="num">0.05;           class=class="str">"cmt">// Significance level Alpha(class="num">0,class="num">1)
class="kw">input class="type">int     reoptimize_step = class="num">2;             class=class="str">"cmt">// Reoptimize step
class="macro">#define MAGIC_NUMBER class="num">23092024
class="type">int prev_bars = class="num">0;
class="type">MqlTick ticks;
class="type">class="kw">double min_lot;
vector params_;
matrix _Input_X;
vector _Target_y;
class="kw">static class="type">int count_ = class="num">0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetTypeFillingBySymbol(Symbol());
   m_trade.SetMarginMode();

◍ 信号落地与订单开关的实盘衔接

这段逻辑跑在 OnTick 里,只在 isnewBar 判定出新 K 线时才往下走,避免同一根 bar 内反复重算模型。reoptimize_step 控制重训练节奏,step==0 时调用 GetDataset 拉训练集并 FitLogitRegression 拟合参数,其余 tick 直接复用 params_。 拿到 signal 后先 Comment 打出信号值与 ModelSignificant 标志,方便在 MT5 图表左上角肉眼核对。真正下单前用 PosExists 查同向仓,且必须 ModelSignificant 为真才允许开仓,否则只做反向平仓。 买卖都以 min_lot 手数推进,买价取 ticks.ask、卖价取 ticks.bid,由 m_trade 执行。外汇与贵金属杠杆高、滑点跳空频繁,min_lot 虽小但连续反向仍可能快速放大回撤,建议先在策略测试器用 2020—2023 年 XAUUSD M15 跑一遍观察胜率分布。 isnewBar 靠 prev_bars 与当前 Bars 数不等来识别新 bar,首次调用 prev_bars 为 0 会先赋值。这个写法在切换周期或历史回放时可能漏判,手动改周期后最好重启 EA 验证一次。

MQL5 / C++
  min_lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
  Print(__FUNCTION__," Deinitialization reason code = ",reason);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
  if(!isnewBar(PERIOD_CURRENT))
    class="kw">return;
  class="type">class="kw">double step;
  step = count_ % reoptimize_step;
class=class="str">"cmt">//------------------------------------Train Dataset-------------------------------------------------
  class="type">int start = class="num">0;
  if(step == class="num">0)
    {
      GetDataset(InpCount,_lag_,start,_Input_X,_Target_y,symbol_X,symbol_y);
      params_ = FitLogitRegression(_Input_X,_Target_y,_L2_,_C_,_probit_,alpha_);
    }
  count_ = count_+class="num">1;
class=class="str">"cmt">//--------------------------------------------------------------------------------------------------
class=class="str">"cmt">//--- Get trade signal
  class="type">int signal = Trade_PredictedTarget(params_,start,_lag_,InpCount,symbol_X);
  Comment("Trade signal: ",signal,"  ","ModelSignificant: ",ModelSignificant);  
class=class="str">"cmt">//---------------------------------------------
class=class="str">"cmt">//--- Open trades based on Signals
  SymbolInfoTick(Symbol(), ticks);
  if(signal==class="num">1)
    {
      if(!PosExists(POSITION_TYPE_BUY) && ModelSignificant)
        {
         m_trade.Buy(min_lot,Symbol(), ticks.ask);
         PosClose(POSITION_TYPE_SELL);
        }
      else
        {
         PosClose(POSITION_TYPE_SELL);
        }
    }
  else
    {
      if(!PosExists(POSITION_TYPE_SELL) && ModelSignificant)
        {
         m_trade.Sell(min_lot,Symbol(), ticks.bid);
         PosClose(POSITION_TYPE_BUY);
        }
      else
        {
         PosClose(POSITION_TYPE_BUY);
        }
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   Function tracks the occurrence of a new bar event              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool isnewBar(ENUM_TIMEFRAMES TF)
  {
  if(prev_bars == class="num">0)
    prev_bars = Bars(Symbol(), TF);
  if(prev_bars != Bars(Symbol(), TF))

常见问题

Logit用对数似然基于logistic分布,Probit基于正态累积分布;拟合入口都走极大似然,但Probit对尾部极端汇率跳动更敏感,实盘前先用历史样本比一下AIC。
看各系数对应z值或p值,p小于0.05倾向显著;协方差估计矩阵对角线开方是标准误,非对角反映变量间共线性,共线性高时信号会发飘。
小布盯盘的AIGC分析可内置这类诊断,打开对应品种页即可看到拟合显著性与协方差估计,不用自己手算。
标准误差异来自分布假设,样本够大时两者趋近;拿滚动窗口回测,标准误骤增的模型那段信号建议降权。
按概率分层下仓,0.6~0.7轻仓、0.8以上才正常仓;订单开关加阈值缓冲,避免噪声触发频繁交易,外汇贵金属高风险需严控杠杆。