使用经典机器学习方法预测汇率:逻辑回归(logit)模型和概率回归(probit)模型·进阶篇
「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 用历史数据试跑观察耗时。
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 下方,说明你那组特征在统计上倾向没信息量,别拿去喂信号。
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 变化。外汇与贵金属行情受杠杆与突发事件影响,模型参数标准误放大时,信号失效概率倾向上升,实盘前务必在策略测试器验证。
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,确认信号翻转频率你能承受再放开自动重优化。
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 验证一次。
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))