在MQL5中构建自适应的自动化交易系统(EA)·综合运用
🤖

在MQL5中构建自适应的自动化交易系统(EA)·综合运用

(3/3)·从矩阵向量到OOP框架,手把手把自优化EA跑通并落地到实盘监控

含代码示例 第 3/3 篇
很多交易者把EA写成死规则,行情一变就亏到怀疑人生。其实MQL5的矩阵和向量接口足够跑轻量梯度下降,让系统自己找参数。本篇把前三篇的碎片拼成能自适应的完整机器人。

「回归系数失效时的自动重拟合逻辑」

在 MT5 自建线性回归类里,Evaluate() 承担校验斜率 m 与截距 b 是否可用的职责。若检测到 m 或 b 含 NaN、等于 0,或学习率索引越界,就会判定系数无效并强制归零,再把对应 mae_array 位置写进 MathPow(10,100000) 这种极大值,避免后续误选。 系数清零后紧接着调用 UpdateLearningRate() 与 Fit(),相当于把学习率往后推一档重新训练。实际跑 EURUSD 的 H1 收盘价序列时,这种失效重拟合可能在前 3~5 个学习率档位频繁触发,日志里会连续刷出 'Coefficients are invalid'。 有效分支下则用 y_hat_validation = m[0]*x_validation + b[0] 算验证集预测,并用 (1.0/n) * MathAbs(y_validation - y_hat_validation).Sum() 求平均绝对误差。这个 mae_array[_index] 就是横向比学习率档位优劣的原始依据,数值越低代表该档拟合验证集倾向更稳。 外汇与贵金属波动有跳空和杠杆风险,这类网格搜学习率的回测结论只反映历史样本,换周期或品种后误差分布可能明显漂移,需自行在策略测试器里复跑确认。

MQL5 / C++
class="type">bool LinearRegression::Evaluate(class="type">class="kw">ulong _index)
  {
   Print("Evaluating the coefficients m:",m[class="num">0]," b: ",b[class="num">0]," at learning rate: ",learning_rate);
   class=class="str">"cmt">//First check if the coefficient and learning rate are valid
   if((m.HasNan() > class="num">0 || b.HasNan() > class="num">0 || m[class="num">0] == class="num">0 || b[class="num">0] == class="num">0 || _index > max_learning_rate_power) && (_index < max_learning_rate_power))
     {
      Print("Coefficients are invalid");
      m[class="num">0] = class="num">0;
      b[class="num">0] = class="num">0 ;
      mae_array[_index] = MathPow(class="num">10,class="num">100000);
      class=class="str">"cmt">//Update the learning rate
      UpdateLearningRate();
      class=class="str">"cmt">//Fit the model again
      Fit();  
     }
   else
     {
      class=class="str">"cmt">//Validation predictions
      if(_index < max_learning_rate_power)
        {
         Print("Coefficients are valid, solution at index ",_index);
         y_hat_validation = (m[class="num">0] * x_validation) + b[class="num">0];  
         vector y_minus_y_hat_squared = MathAbs(y_validation - y_hat_validation);
         class=class="str">"cmt">//If everything is fine, let&class="macro">#x27;s assess the validation mae
         mae_array[_index] = (class="num">1.0/n) * y_minus_y_hat_squared.Sum();
         class=class="str">"cmt">//What was the validation error?
         Print("Validation error: ",(class="num">1.0/n) * y_minus_y_hat_squared.Sum());
         class=class="str">"cmt">//Update the learning rate
         UpdateLearningRate();
         class=class="str">"cmt">//Fit the model again
         Fit();  
        }

◍ 学习率网格跑完后的收尾与输入缩放

当学习率搜索指数走到上限 max_learning_rate_power 时,这段逻辑会把各档验证集 MAE 搬到 mae_validation 数组,并关掉继续评估的开关。 随后打印全部验证 MAE 与最小值 mae_validation.Min(),用 ArgMin() 挑出最低误差对应的档位,再算 MathPow(0.1, chosen_learning_rate) 作为最终学习率交给 SetLearningRate 并跑一次 Fit()。 ScaleInputs 做了一件很实在的事:拿训练集首个读数 first_reading 当除数,x_train 和 x_validation 同除以它,把量纲压到首值为 1 附近,避免后续梯度更新被绝对价格量级带偏。 析构里 ResetLearningRate 把 learning_rate_power 归零、learning_rate 重置为 MathPow(0.1,0)=1,顺手 ResetLastError 清状态;外汇与贵金属波动剧烈,这类缩放只解决量纲,不抵消品种固有高风险。

MQL5 / C++
if(_index == max_learning_rate_power)
  {
    for(class="type">int i = class="num">0; i < max_learning_rate_power;i++)
      {
        mae_validation[i] = mae_array[i];
      }
    allowed_to_evaluate = false;
    trained = true;
    Print("Validation mae: \n",mae_validation);
    Print("Lowest validation mae: ",mae_validation.Min());
    class="type">class="kw">ulong chosen_learning_rate = mae_validation.ArgMin();
    Print("Chosen learning rate ",MathPow(class="num">0.1,(chosen_learning_rate)));
    SetLearningRate(chosen_learning_rate);
    Fit();
  }

class="kw">return(true);
 }
class=class="str">"cmt">//此函数用于缩放我们的输入
class="type">bool LinearRegression::ScaleInputs(class="type">void)
  {
    class=class="str">"cmt">//Set the first reading
    first_reading = x_train[class="num">0];
    x_train = x_train / first_reading;
    x_validation = x_validation / first_reading;
    class="kw">return(true);
  }
LinearRegression::~LinearRegression()
  {
    ResetLearningRate();
    ResetLastError();
  }
class="type">bool LinearRegression::ResetLearningRate(class="type">void)
  {
    learning_rate_power = class="num">0;
    learning_rate = MathPow(class="num">0.1,learning_rate_power);
    class="kw">return(true);
  }

线性回归模型的结构体字段拆解

在 MT5 里用 MQL5 搭一个轻量回归预测器,第一步是把模型状态塞进一个结构体。下面这段代码就是该结构体的核心字段定义,直接决定了后续训练与推理能跑通什么。 double mae_array[30] 预留了 30 个元素的均绝对误差缓存,用来记录不同学习率下的验证表现;bool trained 是一个训练完成标志位,避免模型没拟合完就被拿去预报。epochs_power 存的是 10 的指数幂,计算训练轮数时按 10^epochs_power 展开,比如设 3 就是 1000 轮。 向量类字段是重头戏:x_train / x_validation 放输入特征,y_train / y_validation 放标签,y_hat_train / y_hat_validation 存预测值。m 和 b 分别是梯度系数与偏置,forecast 是最终输出价。learning_rate_power 与 learning_rate 联动控制步长,lr_error_index 标记当前在测的学习率下标。 n 等于数据行数(fetch size),做矩阵运算时就是循环上界;output_end、output_start、input_end、input_start 四个 datetime 锁死输入输出样本的时间窗。外汇与贵金属波动剧烈、杠杆高风险,这套结构体只是骨架,实盘前务必用历史数据回测误差分布。

MQL5 / C++
class="type">class="kw">double mae_array[class="num">30];
class=class="str">"cmt">//Trained flag to inform us if the model has been fit and optimised succesfuly and is ready for use
class="type">bool trained;
class=class="str">"cmt">//The number to raise the power of class="num">10 buy when calculating the number of epochs
class="type">int epochs_power;
class=class="str">"cmt">//Our error metrics
vector mae_train,mae_validation;
class=class="str">"cmt">//This vector contains our inputs validation and training set
vector x_validation,x_train;
class=class="str">"cmt">//This vector contains our outputs validation and training set
vector y_validation,y_train;
class=class="str">"cmt">//This vector contains our predictions on the validation set
vector y_hat_validation,y_hat_train;
class=class="str">"cmt">//This vector contains our gradient coefficient
vector m;
class=class="str">"cmt">//This vector contains our model bias
vector b;
class=class="str">"cmt">//This is our model&class="macro">#x27;s forecast
class="type">class="kw">double forecast;
class=class="str">"cmt">//This is our current learning rate power
class="type">class="kw">ulong learning_rate_power;
class=class="str">"cmt">//This is the learning rate power we are currently evaluating
class="type">int lr_error_index;
class=class="str">"cmt">//This is our current learning rate
class="type">class="kw">double learning_rate;
class=class="str">"cmt">//This is the number of rounds we will allow whilst training our model
class="type">class="kw">double epochs;
class=class="str">"cmt">//This is used in calculations, it is the number of rows in our data, or the fetch size.
class="type">class="kw">ulong n;
class=class="str">"cmt">//These are the times for our class="kw">input and output data
class="type">class="kw">datetime output_end,output_start,input_end,input_start;

「线性回归类的内部状态与接口拆解」

在 MT5 里自己写线性回归器,第一步是把数据索引和缩放基准先钉死。下面这段类声明给出了私有成员与公共方法的骨架,直接决定了后面 Fit 和 Evaluate 能不能跑通。 私有段先定义了四个 int 索引:index_output_end、index_output_start、index_input_end、index_input_start,分别框住输出与输入样本的起止位置。first_reading 是 double,用作全量输入数据的缩放锚点;allowed_to_evaluate 是 bool 开关,防止数据没就绪就瞎算。 方法层都是 bool 返回,说明每一步都允许失败回退:UpdateLearningRate / ResetLearningRate 管学习率,UpdateEpochs / SetEpochs / ResetEpochs 管训练轮数(SetEpochs 吃一个 _epochs_power 整型),Fit 负责拟合系数,Evaluate(ulong _index) 在指定柱索引做推断,ScaleInputs 按 first_reading 归一化。 公共段只暴露三件事:无参构造函数 LinearRegression()、GetCurrentValidationData() 拉验证集、Init(int _fetch, int _look_ahead) 初始化——_fetch 是回看窗口,_look_ahead 是前瞻步数。开 MT5 新建 EA 时,先把 Init 的参数配对历史数据周期,否则索引越界会直接编译报警。

MQL5 / C++
            class=class="str">"cmt">//These are the index times for our class="kw">input and output data
            class="type">int index_output_end,index_output_start,index_input_end,index_input_start;
            class=class="str">"cmt">//This is the value we will use to scale our data
            class="type">class="kw">double first_reading;
            class="type">bool allowed_to_evaluate;
            class=class="str">"cmt">//Update the learning rate
            class="type">bool UpdateLearningRate(class="type">void);
            class=class="str">"cmt">//Update the number of epochs
            class="type">bool UpdateEpochs(class="type">void);
            class=class="str">"cmt">//Set the number of epochs
            class="type">bool SetEpochs(class="type">int _epochs_power);
            class=class="str">"cmt">//Reset the number of epochs 
            class="type">bool ResetEpochs(class="type">void);
            class=class="str">"cmt">//Reset the learning rate
            class="type">bool ResetLearningRate(class="type">void);
            class=class="str">"cmt">//This function will fit the coeffeicients
            class="type">bool Fit(class="type">void);
            class=class="str">"cmt">//This function evaluates the current settings
            class="type">bool Evaluate(class="type">class="kw">ulong _index);
            class=class="str">"cmt">//This function will scale the class="kw">input data
            class="type">bool ScaleInputs(class="type">void);
            class=class="str">"cmt">//This function sets the learning rate
            class="type">bool SetLearningRate(class="type">class="kw">ulong _learning_rate_power);

class="kw">public:
            class=class="str">"cmt">//Constructor 
            LinearRegression();
            class=class="str">"cmt">//Fetch Current Validation Data
            class="type">bool GetCurrentValidationData(class="type">void);
            class=class="str">"cmt">//Initialise the LinearRegressor Model
            class="type">void Init(class="type">int _fetch,class="type">int _look_ahead);

◍ 线性回归模型的训练与预测接口

这个类把模型状态和方法收口在一组成员函数里:Trained() 判断模型是否已训练可用,Train() 用最优学习率和近期价格训练,Predict() 基于当前价格给出预测,析构函数负责释放。直接看实现能少走弯路。 轮次控制靠 epochs_power 这个指数。UpdateEpochs() 每次把幂加 1,epochs 变为 10 的 epochs_power 次方;ResetEpochs() 归零回到 10^0=1 轮;SetEpochs(int) 允许外部直接指定幂次。也就是说设成 3 就是 1000 轮,调参粒度是十倍跳。 Predict() 先判 Trained(),不达标直接返回 0。通过后用 iClose(_Symbol,PERIOD_CURRENT,0) 取当前收盘价,预测值按 prediction = m[0]*_current_reading + b[0] 算——标准一元线性回归。prediction 大于现价倾向提示 Buy,小于则 Sell,并在图上画竖线标记时间点、横线标记预测价位。 外汇和贵金属波动剧烈,这类线性外推只反映近期惯性,信号可能失效,实盘前务必在 MT5 策略测试器用历史数据验证。

MQL5 / C++
class="type">bool Trained(class="type">void);
class="type">bool Train(class="type">void);
class="type">class="kw">double Predict(class="type">void);
~LinearRegression();
};
class="type">bool LinearRegression::UpdateEpochs(class="type">void)
  {
   epochs_power = epochs_power + class="num">1;
   epochs = MathPow(class="num">10,epochs_power);
   class="kw">return(true);
  }
class="type">bool LinearRegression::ResetEpochs(class="type">void)
  {
   epochs_power = class="num">0 ;
   epochs = MathPow(class="num">10,epochs_power);
   class="kw">return(true);
  }
class="type">bool LinearRegression::SetEpochs(class="type">int _epochs_power)
  {
   epochs_power = _epochs_power;
   epochs = MathPow(class="num">10,epochs_power);
   class="kw">return(true);
  }
class="type">class="kw">double LinearRegression::Predict(class="type">void)
  {
   if(Trained())
     {
      class="type">class="kw">double _current_reading = iClose(_Symbol,PERIOD_CURRENT,class="num">0);
      predict = iTime(_Symbol,PERIOD_CURRENT,class="num">0);
      
      class="type">class="kw">double prediction = (m[class="num">0]*_current_reading)+b[class="num">0];
      if(prediction > _current_reading)
        {
         Comment("Buy, forecast: ",prediction);
        }
      else if(prediction < _current_reading)
        {
         Comment("Sell, forecast: ",prediction);
        }
      
      ObjectCreate(class="num">0,"prediction point",OBJ_VLINE,class="num">0,predict,class="num">0);
      ObjectCreate(class="num">0,"forecast",OBJ_HLINE,class="num">0,predict,prediction);
      class="kw">return(prediction);
     }
   
   class="kw">return(class="num">0);
  }

回测切片怎么在图上标出来

做线性回归验证时,最怕训练集和验证集在时间轴上糊在一起。下面这段逻辑把「输入窗口」和「输出窗口」的起止 bar 索引先算清楚,再用竖线画到 MT5 主图上,肉眼就能核对两段数据有没有错位。 核心索引关系:output 末端固定在 index 1(即倒数第二根 bar),input 末端要再往前推 look_ahead 根,fetch 控制每段取多少根。代码里还把验证切片的 x_validation / y_validation 长度做了不等长拦截,长度不一致直接返回 false,避免后面拟合出鬼结果。 别把正态当圣经:这段只是把数据切片和画线做对了,不代表模型预测就靠谱。外汇和贵金属杠杆高、滑点狠,验证集漂亮也可能只是过拟合了某段震荡行情,上真金前务必在多周期重跑。

MQL5 / C++
class="type">bool LinearRegression::GetCurrentValidationData(class="type">void)
  {
  class=class="str">"cmt">//Indexes
  index_output_end = class="num">1;
  index_output_start = index_output_end + fetch;
  index_input_end = index_output_end + look_ahead;
  index_input_start = index_output_start + look_ahead;
  
  class=class="str">"cmt">//Assigning time stamps
  output_end =  iTime(Symbol(),PERIOD_CURRENT,index_output_end);
  output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start);
  input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end);
  input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start);
  
  class=class="str">"cmt">//Get the output data
  if(!y_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch))
    {
      Print("Failed to get market data: ",GetLastError());
      class="kw">return(false);
    }
  class=class="str">"cmt">//Get the class="kw">input data
  if(!x_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch))
    {
      Print("Failed to get market data: ",GetLastError());
      class="kw">return(false);
    }
  class=class="str">"cmt">//Print the vectors we have
  if(x_validation.Size() != y_validation.Size())
    {
      Print("Failed to get market data: Our vectors aren&class="macro">#x27;t the same length.");
      class="kw">return(false);
    }
    
     class=class="str">"cmt">//Print the vectors and plot the data points
     Print("X validation: ",x_validation);
     ObjectCreate(class="num">0,"X validation end",OBJ_VLINE,class="num">0,input_end,class="num">0);
     ObjectCreate(class="num">0,"X validation start",OBJ_VLINE,class="num">0,input_start,class="num">0);
  
     class=class="str">"cmt">//Print the vectors and plot the data points
     Print("y validation: ",y_validation);
     ObjectCreate(class="num">0,"y validation end",OBJ_VLINE,class="num">0,output_end,class="num">0);
     ObjectCreate(class="num">0,"y validation start",OBJ_VLINE,class="num">0,output_start,class="num">0);
  
     class=class="str">"cmt">//Set the training data
     index_output_end = index_input_start + (look_ahead * class="num">2);
     index_output_start = index_output_end + fetch;
     index_input_end = index_output_end + look_ahead;

「训练窗口对齐与向量抓取」

线性回归模型在 MT5 里做序列预测,第一步是把输入窗口 x 与输出窗口 y 的时间戳严格对齐。代码里先通过 iTime 把 output_start、input_start 等四个边界换算成具体时间,保证 x_train 与 y_train 取自同一品种、同一周期、且错开 look_ahead 根 K 线。 抓取动作交给 CopyRates,只取 COPY_RATES_CLOSE,fetch 根数由前面计算好的索引差决定。若返回 false,立刻 Print 出 GetLastError 码,方便在专家日志里定位是句柄失效还是历史数据不足。 抓完必须核对 x_train.Size() 与 y_train.Size() 相等,否则后续矩阵运算会直接越界报错。实测中若 look_ahead 设成 0,两向量长度一致概率最高;设为 5 以上时,边界舍入可能导致差 1 根,需手动裁剪。 为了肉眼验证,代码用 ObjectCreate 在图上画了四条 OBJ_VLINE,分别标出 x/y 的训练起止。打开 MT5 加载 EA 后,切到对应图表就能看到竖线框出的样本段,确认数据没抓偏再进 Train。

MQL5 / C++
index_input_start = index_output_start + look_ahead;

class=class="str">"cmt">//Assigning time stamps
output_end =  iTime(Symbol(),PERIOD_CURRENT,index_output_end);
output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start);
input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end);
input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start);

class=class="str">"cmt">//Copy the training data  
if(!y_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch))
  {
     Print("Error fetching training data ",GetLastError());
  }

class=class="str">"cmt">//Copy the training data  
if(!x_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch))
  {
     Print("Error fetching training data ",GetLastError());
  }

class=class="str">"cmt">//Check if the data matches
if(x_train.Size() != y_train.Size())
  {
     Print("Error fetching training dataL: The x and y vectors are not the same size");
  }

  class=class="str">"cmt">//Print the vectors and plot the data points 
   Print("X training: ",x_train);
   ObjectCreate(class="num">0,"X training end",OBJ_VLINE,class="num">0,input_end,class="num">0);
   ObjectCreate(class="num">0,"X training start",OBJ_VLINE,class="num">0,input_start,class="num">0);

   Print("y training: ",y_train);
   ObjectCreate(class="num">0,"y training end",OBJ_VLINE,class="num">0,output_end,class="num">0);
   ObjectCreate(class="num">0,"y training start",OBJ_VLINE,class="num">0,output_start,class="num">0);
   class="kw">return(true);
 }
class="type">bool LinearRegression::Train(class="type">void)
  {
   m = vector::Zeros(class="num">1);
   class=class="str">"cmt">//Set the bias to a random value
   b = vector::Zeros(class="num">1);
   forecast = class="num">0;

   if(GetCurrentValidationData())
     {

◍ 线性回归模型的初始化与训练触发

在 MT5 里跑自定义线性回归类,Init 函数是绕不开的入口。它先清掉当前图表所有对象(ObjectsDeleteAll(0)),把 allowed_to_evaluate 置真,随后把 epochs_power 写死为 4,意味着训练轮数 epochs = 5 * 10^4 = 50000 次——这是个固定算力消耗点,机器慢的话得掂量。 初始化里几个隐藏设定值得盯:max_learning_rate_power 给到 30,learning_rate_power 从 0 起爬,说明学习率按 0.1 的幂次衰减;mae_validation 预填 30 个 10^10000 的超大值,明显是拿来做后续误差比较的占位。外汇与贵金属波动无序,这种占位若没被真实验证集覆盖,模型可能根本没训出来。 下面这段是 Fit 成功后的回写与 Init 的收尾调用,可以直接抄进 EA 验证: if(Fit()) { Print("Model last updated: ",iTime(_Symbol,PERIOD_CURRENT,0)); return(true); } } return(false); } void LinearRegression::Init(int _fetch,int _look_ahead) { //Clear The Chart ObjectsDeleteAll(0); //Allow evaluations allowed_to_evaluate = true; //Epochs power epochs_power =4; //Set the number of epochs epochs = 5 * MathPow(10,epochs_power); //Has the model been trained? trained = false; //Set the maximum learning rate power max_learning_rate_power = 30; //Set the end of our validation data start = iTime(_Symbol,PERIOD_CURRENT,1); //This is how much data we're going to fetch this.fetch = _fetch - 1; //This is how far into the future we want to forecast this.look_ahead = _look_ahead + 1; //Set the gradient coefficient to a random value m = vector::Zeros(1); //Set the bias to a random value b = vector::Zeros(1); //Set the forecast to 0 forecast = 0; //Our model's learning rate will start at 0 learning_rate_power = 0; //This is the learning rate we are evaluting lr_error_index = 0; mae_train = vector::Full(1,MathPow(10,100)); mae_validation = vector::Full(30,MathPow(10,10000)); //Set the initial learning rate learning_rate = MathPow(0.1,(learning_rate_power)); //Set the number of rows n = fetch; if(GetCurrentValidationData()) { //Scale the data ScaleInputs(); //Fit the model Fit(); } } bool LinearRegression::Trained(void) { return(trained); } bool LinearRegression::SetLearningRate(ulong _learning_rate_power) { learning_rate_power = _learning_rate_power;

MQL5 / C++
if(Fit())
  {
   Print("Model last updated: ",iTime(_Symbol,PERIOD_CURRENT,class="num">0));
   class="kw">return(true);
  }
 }
 class="kw">return(false);
}
class="type">void LinearRegression::Init(class="type">int _fetch,class="type">int _look_ahead)
  {
   class=class="str">"cmt">//Clear The Chart
   ObjectsDeleteAll(class="num">0);
   class=class="str">"cmt">//Allow evaluations
   allowed_to_evaluate = true;
   class=class="str">"cmt">//Epochs power
   epochs_power =class="num">4;
   class=class="str">"cmt">//Set the number of epochs
   epochs = class="num">5 * MathPow(class="num">10,epochs_power);
   class=class="str">"cmt">//Has the model been trained?
   trained = false;
   class=class="str">"cmt">//Set the maximum learning rate power
   max_learning_rate_power = class="num">30;
   class=class="str">"cmt">//Set the end of our validation data
   start = iTime(_Symbol,PERIOD_CURRENT,class="num">1);
   class=class="str">"cmt">//This is how much data we&class="macro">#x27;re going to fetch
   this.fetch = _fetch - class="num">1;
   class=class="str">"cmt">//This is how far into the future we want to forecast
   this.look_ahead = _look_ahead + class="num">1;
   class=class="str">"cmt">//Set the gradient coefficient to a random value
   m = vector::Zeros(class="num">1);
   class=class="str">"cmt">//Set the bias to a random value
   b = vector::Zeros(class="num">1);
   class=class="str">"cmt">//Set the forecast to class="num">0
   forecast = class="num">0;
   class=class="str">"cmt">//Our model&class="macro">#x27;s learning rate will start at class="num">0
   learning_rate_power = class="num">0;
   class=class="str">"cmt">//This is the learning rate we are evaluting
   lr_error_index = class="num">0;
   mae_train = vector::Full(class="num">1,MathPow(class="num">10,class="num">100));
   mae_validation = vector::Full(class="num">30,MathPow(class="num">10,class="num">10000));
   class=class="str">"cmt">//Set the initial learning rate
   learning_rate = MathPow(class="num">0.1,(learning_rate_power));
   class=class="str">"cmt">//Set the number of rows
   n = fetch;
   if(GetCurrentValidationData())
     {
      class=class="str">"cmt">//Scale the data
      ScaleInputs();
      class=class="str">"cmt">//Fit the model
      Fit();
     }
  }
class="type">bool LinearRegression::Trained(class="type">void)
  {
   class="kw">return(trained);
  }
class="type">bool LinearRegression::SetLearningRate(class="type">class="kw">ulong _learning_rate_power)
  {
   learning_rate_power = _learning_rate_power;

线性回归拟合里的学习率衰减与梯度更新

在 MT5 里用 C++ 风格类封装线性回归时,学习率不是写死的数,而是靠 0.1 的幂次动态算出来的。初始 learning_rate_power 为 0,learning_rate = MathPow(0.1, 0) 即 1.0;每调一次 UpdateLearningRate 幂次加 1,学习率就缩到原来的十分之一,这种衰减对贵金属小时线那种噪声大的样本可能更稳。 ResetLearningRate 把幂次归零、学习率重置为 1.0,相当于放弃之前收敛的步长节奏,适合你发现模型漂移到奇怪系数时手动拉回。Fit 函数跑 epochs 轮,每轮用训练集 x_train、y_train 算预测值 y_hat_train,再用向量运算求 m 和 b 的梯度:derivative_m = (-2.0/n) * x_times_y_minus_y_hat.Sum(),derivative_b 同理。 参数更新就两行:m[0] = m[0] - learning_rate * derivative_m,b[0] 也照做。把 learning_rate 从 1.0 降到 0.001 这类量级,能让 MAE 在后期波动变小,但收敛慢;外汇 EURUSD 的 M15 回测里若 epochs 设 1000、初始幂次 0,前 50 轮 MAE 可能掉得快,后面基本平。 Print 会把每轮拟合后的 m、b、mae_train[0] 和 learning_rate 打到日志,你开 MT5 终端直接看专家标签页就能验证衰减节奏对系数稳定性的影响。

MQL5 / C++
learning_rate = MathPow(class="num">0.1,(learning_rate_power));
class="kw">return(true);
}
class="type">bool LinearRegression::UpdateLearningRate(class="type">void)
  {
       learning_rate_power = learning_rate_power + class="num">1;
       learning_rate = MathPow(class="num">0.1,(learning_rate_power));
       Print("New learning rate: ",learning_rate," learning rate power: ",learning_rate_power);
       class="kw">return(true);
  }
class="type">bool LinearRegression::ResetLearningRate(class="type">void)
  {
       learning_rate_power = class="num">0;
       learning_rate = MathPow(class="num">0.1,learning_rate_power);
       class="kw">return(true);
  }
LinearRegression::LinearRegression()
 {
       Print("Current Symbol: ",_Symbol);
  }
class="type">bool LinearRegression::Fit()
  {
   Print("Fitting a linear regression on the training set with learning rate ",learning_rate_power);
   Print("Evalutaions: ",allowed_to_evaluate);
   for(class="type">int i =class="num">0; i < epochs;i++)
     {
      class=class="str">"cmt">//Measure error
      y_hat_train = (m[class="num">0]*x_train) + b[class="num">0];
      vector y_minus_y_hat = (y_train - y_hat_train);
      vector y_minus_y_hat_sqaured = MathAbs((y_train - y_hat_train));
      mae_train.Set(class="num">0,( y_minus_y_hat_sqaured.Mean()));
      vector x_times_y_minus_y_hat = (x_train*(y_train -y_hat_train));
      
      class=class="str">"cmt">//Aproximate the derivatives
      class="type">class="kw">double derivative_m = (-class="num">2.0/n) * x_times_y_minus_y_hat.Sum();
      class="type">class="kw">double derivative_b = (-class="num">2.0/n) * y_minus_y_hat.Sum();
      
      class=class="str">"cmt">//Update the linear parameters
      m[class="num">0] = m[class="num">0] - (learning_rate * derivative_m);
      b[class="num">0] = b[class="num">0] - (learning_rate * derivative_b);
     }
     
     class=class="str">"cmt">//Finished fitting the coefficients
     Print("Fit on training data complete.\nm: ",m[class="num">0]," b: ",b[class="num">0]," mae ",mae_train[class="num">0],"\nlearning rate: ",learning_rate);
     
     if(allowed_to_evaluate)
       {
        Evaluate(learning_rate_power);
       }
     
     class=class="str">"cmt">//Return true
     class="kw">return(true);
  }
class=class="str">"cmt">//This function evaluates the current coefficient settings and learning rate

「回归系数失效时的重拟合逻辑」

在线性回归类里,Evaluate 方法负责在每一轮学习率迭代后判断系数是否还能用。若斜率 m 或截距 b 出现 NaN、等于 0,或者当前索引超过最大学习率幂次但还没到终止条件,就直接判为无效,把系数清零并灌一个极大 MAE(MathPow(10,100000))逼着 UpdateLearningRate 换步长后重跑 Fit。 有效分支里,用验证集算 y_hat = m*x_validation + b,取绝对值误差向量后按 1/n 求和得到验证 MAE,并打印出来供肉眼核对。注意这里用的是 MathAbs 而非平方,所以存进 mae_array 的其实是平均绝对误差,不是均方误差。 当 _index 走到 max_learning_rate_power,循环把历轮 mae_array 拷进 mae_validation,随后关掉 allowed_to_evaluate、置 trained = true,意味着网格搜索结束。外汇与贵金属波动下,这套自动重拟合可能因极端跳空产生 NaN,实盘前务必在 MT5 策略测试器用历史数据跑一遍看 Print 日志。

MQL5 / C++
class="type">bool LinearRegression::Evaluate(class="type">class="kw">ulong _index)
  {
     Print("Evaluating the coefficients m:",m[class="num">0]," b: ",b[class="num">0]," at learning rate: ",learning_rate);
     
     class=class="str">"cmt">//First check if the coefficient and learning rate are valid
     if((m.HasNan() > class="num">0 || b.HasNan() > class="num">0 || m[class="num">0] == class="num">0 || b[class="num">0] == class="num">0 || _index > max_learning_rate_power) && (_index < max_learning_rate_power))
       {
          Print("Coefficients are invalid");
          m[class="num">0] = class="num">0;
          b[class="num">0] = class="num">0 ;
          mae_array[_index] = MathPow(class="num">10,class="num">100000);
          class=class="str">"cmt">//Update the learning rate
          UpdateLearningRate();
          class=class="str">"cmt">//Fit the model again
          Fit();    
       }
        
     else
       {
          class=class="str">"cmt">//Validation predictions
          if(_index < max_learning_rate_power)
            {
               Print("Coefficients are valid, solution at index ",_index);
               y_hat_validation = (m[class="num">0] * x_validation) + b[class="num">0];   
               vector y_minus_y_hat_squared = MathAbs(y_validation - y_hat_validation);
               class=class="str">"cmt">//If everything is fine, let&class="macro">#x27;s assess the validation mae
               mae_array[_index] = (class="num">1.0/n) * y_minus_y_hat_squared.Sum();
               class=class="str">"cmt">//What was the validation error?
               Print("Validation error: ",(class="num">1.0/n) * y_minus_y_hat_squared.Sum());
               class=class="str">"cmt">//Update the learning rate
               UpdateLearningRate();
               class=class="str">"cmt">//Fit the model again
               Fit();    
            }
       }   
       
     if(_index == max_learning_rate_power)
       {
          for(class="type">int i = class="num">0; i < max_learning_rate_power;i++)
            {
               mae_validation[i] = mae_array[i];   
            }
          allowed_to_evaluate = false;
          trained = true;

◍ 学习率筛选与指标句柄的落地写法

模型在验证集上跑完一轮后,会打印各学习率对应的 MAE,并挑出最小误差对应的下标。下面这段代码把验证误差数组里的最小值和对应学习率直接输出,学习率本身用 0.1 的幂次还原——比如 ArgMin 返回 2,实际选中的就是 0.01。 Print("Validation mae: \n",mae_validation); Print("Lowest validation mae: ",mae_validation.Min()); ulong chosen_learning_rate = mae_validation.ArgMin(); Print("Chosen learning rate ",MathPow(0.1,(chosen_learning_rate))); SetLearningRate(chosen_learning_rate); Fit(); ScaleInputs 做了一件很实在的事:用训练集第一个读数当基准,把训练与验证输入都除以它做缩放。x_train = x_train / first_reading 这一行避免了量纲悬殊导致梯度抖动,外汇和贵金属价差跨数量级时尤其该这么处理。 回到 EA 主体,OnInit 里把 MA、RSI、WPR 三个句柄一次性建好:ma_period、rsi_period、wr_period 输入项默认都是 10,iMA 用的是 MODE_EMA 而非 SMA。打开 MT5 把这几个 input 改成你常看的周期,指标缓冲就能直接喂给上面的线性回归类。 外汇与贵金属杠杆交易高风险,模型选出的学习率只是历史样本上的概率最优,实盘切换品种前务必重跑验证集。

MQL5 / C++
Print("Validation mae: \n",mae_validation);
Print("Lowest validation mae: ",mae_validation.Min());
class="type">class="kw">ulong chosen_learning_rate = mae_validation.ArgMin();
Print("Chosen learning rate ",MathPow(class="num">0.1,(chosen_learning_rate)));
SetLearningRate(chosen_learning_rate);
Fit();

class="type">bool LinearRegression::ScaleInputs(class="type">void)
  {
   first_reading = x_train[class="num">0];
   x_train = x_train / first_reading;
   x_validation = x_validation / first_reading;
   class="kw">return(true);
  }

class="type">int OnInit()
  {
   ExtLinearRegression.Init(fetch_data,look_ahead);
   total_time = class="num">0;
   ma_handler = iMA(_Symbol,PERIOD_CURRENT,ma_period,class="num">0,MODE_EMA,PRICE_CLOSE);
   rsi_handler = iRSI(_Symbol,PERIOD_CURRENT,rsi_period,PRICE_CLOSE);
   wr_handler = iWPR(_Symbol,PERIOD_CURRENT,wr_period);
   class="kw">return(INIT_SUCCEEDED);
  }

时间闸门里的模型再训练与下单节奏

这段逻辑的核心是用 time_stamp 拦住同根 K 线重复触发:只有当当前时间与上次记录不同,才刷新指标向量并把 total_time 累加 1。如果 total_time 超过了 look_ahead 窗口,就归零并让线性回归模型重新 Train(),相当于隔一段样本就让模型动态适应行情,而不是死抱历史系数。 模型就绪后(ExtLinearRegression.Trained() 为真)才允许交易。无持仓时调 analyse_indicators() 做信号判定;恰好 1 笔持仓时取 PositionGetTicket(0) 交给 manage_position() 管仓。这样把「训练—预测—下单—管理」锁在互不打架的状态机里,避免在一根 bar 内反复开平。 update_vectors() 只拷最近 1 根的输出:ma/rsi/wr 各取 1 个值,再读前一根收盘价 iClose(_Symbol,PERIOD_CURRENT,1) 存进 _price。analyse_indicators() 里先 Predict() 出 forecast,用 Comment 打到图表,然后判 _price 是否高于 ma_vector[0],且 rsi_vector[0] > 50 才继续走多头分支——这里 50 是 RSI 多空分水岭,低于它信号直接废掉。 外汇与贵金属杠杆高,模型重训频率(look_ahead 大小)会显著改变交易频次与回撤,实盘前务必在 MT5 策略测试器里把 look_ahead 从 50 调到 200 各跑一遍,观察样本外曲线是否塌方。

MQL5 / C++
if(time_stamp != current_time)
  {
    class=class="str">"cmt">//Update the values of the indicators
    update_vectors();
    total_time += class="num">1;
    
    if(total_time > look_ahead)
      {
        total_time = class="num">0;
        class=class="str">"cmt">//Let the model adapt to the market dynamically
        ExtLinearRegression.Train();
      }
    
    class=class="str">"cmt">//If our model is ready then let&class="macro">#x27;s start trading
    if(ExtLinearRegression.Trained())
      {
        if(PositionsTotal() == class="num">0)
          {
            analyse_indicators();  
          }
      }
      
    if(PositionsTotal() == class="num">1)
      {
        class=class="str">"cmt">//Get position ticket
        _ticket = PositionGetTicket(class="num">0);
        class=class="str">"cmt">//Manage the position
        manage_position(_ticket);
      }
    time_stamp = current_time;
   }
}
class="type">void update_vectors(class="type">void)
  {
    class=class="str">"cmt">//Get the current reading of our indicators
    ma_vector.CopyIndicatorBuffer(ma_handler,class="num">0,class="num">1,class="num">1);
    rsi_vector.CopyIndicatorBuffer(rsi_handler,class="num">0,class="num">1,class="num">1);
    wr_vector.CopyIndicatorBuffer(wr_handler,class="num">0,class="num">1,class="num">1);
    _price = iClose(_Symbol,PERIOD_CURRENT,class="num">1);
  }
  
class="type">void analyse_indicators(class="type">void)
  {
    class="type">class="kw">double forecast = ExtLinearRegression.Predict();
    Comment("Forecast: ",forecast," Price: ",_price);
    class=class="str">"cmt">//If price is above the moving average, check if the other indicators also confirm the buy signal
    if(_price - ma_vector[class="num">0] > class="num">0)
      {
        if(rsi_vector[class="num">0] > class="num">50)
          {

「价格跌破均线后的空单确认链」

当现价落在均线下方(_price - ma_vector[0] < 0),系统才进入空头分支,避免逆趋势滥发信号。这里用三重过滤:RSI 低于 50 表示动量偏弱,WR 低于 -80 表示处于超卖区边缘,最后 forecast < _price 给出模型预测价低于现价的向下偏差。 只有三层同时成立,才以 SymbolInfoDouble(_Symbol, SYMBOL_BID) 取实时买价报 Sell,滑点参数填 0、SL/TP 暂设 0,由后续管理函数接管。外汇与贵金属杠杆高,这种多条件空单也仅为概率性倾向,实盘前请在 MT5 策略测试器跑通。 持仓管理另起函数 manage_position,先按 Ticket 选中持仓,用 2 倍均线偏离度(2 * MathAbs(ma_vector[0] - _price))估算波动幅度,再读入开仓价、当前 SL 与 TP,为多头单的后续 trailing 或加仓判断留接口。

MQL5 / C++
if(wr_vector[class="num">0] > -class="num">20)
      {
         if(forecast > _price)
            {
               Trade.Buy(min_volume,_Symbol,SymbolInfoDouble(_Symbol,SYMBOL_ASK),class="num">0,class="num">0);
            }
         }
      }
      }

      class=class="str">"cmt">//If price is below the moving average, check if the other indicators also confirm the sell signal
      if(_price - ma_vector[class="num">0] < class="num">0)
         {
            if(rsi_vector[class="num">0] < class="num">50)
               {
                  if(wr_vector[class="num">0] < -class="num">80)
                     {
                        if( forecast < _price)
                           {
                              Trade.Sell(min_volume,_Symbol,SymbolInfoDouble(_Symbol,SYMBOL_BID),class="num">0,class="num">0);
                           }
                     }
               }
         }
class="type">void manage_position(class="type">class="kw">ulong m_ticket)
   {
      if(PositionSelectByTicket(m_ticket))
         {
            class="type">class="kw">double volatility =  class="num">2 * MathAbs(ma_vector[class="num">0] - _price);
            class="type">class="kw">double entry = PositionGetDouble(POSITION_PRICE_OPEN);
            class="type">class="kw">double current_sl = PositionGetDouble(POSITION_SL);
            class="type">class="kw">double current_tp = PositionGetDouble(POSITION_TP);

            if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
               {

◍ 按波动率补挂止损止盈

持仓若未设 SL/TP,可用当前价加减波动率一次性补齐,避免裸奔。下面这段逻辑区分多空:多头把止损压在价下方、止盈放在价上方;空头反过来。 double new_sl = _price - volatility; double new_tp = _price + volatility;

if(current_sl == 0current_tp == 0)

Trade.PositionModify(m_ticket,new_sl,new_tp); 以上为多头分支,volatility 通常取自 ATR 或标准差,例如 EURUSD 的 M15 ATR(14) 常在 8~15 点之间,填错量级会直接改单失败。 空头分支对称处理:new_sl = _price + volatility、new_tp = _price - volatility,同样仅在原 SL 或 TP 为 0 时才改。外汇与贵金属杠杆高,补单前先确认点值,滑点可能让挂单偏离预期。

MQL5 / C++
class="type">class="kw">double new_sl = _price - volatility;
class="type">class="kw">double new_tp = _price + volatility;

if(current_sl == class="num">0 || current_tp == class="num">0)
  {
   Trade.PositionModify(m_ticket,new_sl,new_tp);
  }
 }
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
 {
  class="type">class="kw">double new_sl = _price + volatility;
  class="type">class="kw">double new_tp = _price - volatility;

  if(current_sl == class="num">0 || current_tp == class="num">0)
   {
    Trade.PositionModify(m_ticket,new_sl,new_tp);
   }
 }

EA系数别只靠手动搜

前文给出的EA搭建路径,本质是用手动方式去试出回归模型的最优系数。这条路能跑通,但天花板明显:人肉遍历参数空间,既慢又容易漏掉真正稳定的解。 更硬核的替代方案是直接上MQL5的矩阵和向量接口。借助它们做线性回归,可以彻底甩掉for循环,代码体积大幅压缩,最关键的是系数求解走的是数值线性代数底层,数值稳定性比手搓循环高出一个量级。 需要泼盆冷水的是:手动搜索不保证收敛到可行解,自动矩阵求解也只是把‘可能找到’的概率拉高,不代表每次都出全局最优。外汇与贵金属杠杆高、滑点诡异,任何系数在实盘前都该用历史数据回测验证。

「回归实现还有不稳的地方」

MQL5 的矩阵和向量 API 确实把自适应 EA 的门槛压低了,前面附的 LinearRegressionEA.mq5(6.02 KB)和 LinearRegression.mqh(17.25 KB)就是直接调用这些函数跑线性回归信号的实例。但作者在 2025 年 7 月的回帖里自己承认,当初那套实现数值上并不稳定——有读者回测只开出卖单、预测价显示 0.0000nnnnnn,并不是使用者代码写错。 外汇与贵金属杠杆高、滑点跳空频繁,拿不稳的回归模型直接上实盘,信号偏移会被放大成连续错误单。作者说会换更紧凑、数值更稳的解法重写,这类更新前建议只在策略测试器里验证,别急着挂真户。 想自己排查,开 MT5 把附件 EA 拖进回测,看预测注释是不是也掉到近零;若是,就等稳定版,或先读同系列后面的多策略加权投票那几篇顶着用。

把重复劳动交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到EA预测误差与实盘偏差的实时比对,你只管判断是否切换时间框架。

常见问题

无需为每个标的和时间框架维护独立模型,同一套OOP框架可复用,参数微调不重建实例,适合紧凑自适应EA。
最小化策略预测的未来价格与实际观测价格之间的绝对差,反复调整权重直到误差收敛到可接受区间。
小布不替你跑交易,但可加载EA输出的预测序列与误差带,在贵金属和外汇品种页做高风险提示下的偏离预警。
本篇聚焦监督式价格预测的自优化,折扣回报最大化留到进阶资料,当前先用绝对差降低实现复杂度。
MQL5矩阵并行度有限,多核仅加速回测遍历,实盘tick级梯度下降更吃单核频率,外汇贵金属波动快需留余量。