在MQL5中构建自适应的自动化交易系统(EA)·综合运用
(3/3)·从矩阵向量到OOP框架,手把手把自优化EA跑通并落地到实盘监控
「回归系数失效时的自动重拟合逻辑」
在 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] 就是横向比学习率档位优劣的原始依据,数值越低代表该档拟合验证集倾向更稳。 外汇与贵金属波动有跳空和杠杆风险,这类网格搜学习率的回测结论只反映历史样本,换周期或品种后误差分布可能明显漂移,需自行在策略测试器里复跑确认。
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 清状态;外汇与贵金属波动剧烈,这类缩放只解决量纲,不抵消品种固有高风险。
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 锁死输入输出样本的时间窗。外汇与贵金属波动剧烈、杠杆高风险,这套结构体只是骨架,实盘前务必用历史数据回测误差分布。
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 的参数配对历史数据周期,否则索引越界会直接编译报警。
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 策略测试器用历史数据验证。
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,避免后面拟合出鬼结果。 别把正态当圣经:这段只是把数据切片和画线做对了,不代表模型预测就靠谱。外汇和贵金属杠杆高、滑点狠,验证集漂亮也可能只是过拟合了某段震荡行情,上真金前务必在多周期重跑。
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。
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;
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 终端直接看专家标签页就能验证衰减节奏对系数稳定性的影响。
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 日志。
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 改成你常看的周期,指标缓冲就能直接喂给上面的线性回归类。 外汇与贵金属杠杆交易高风险,模型选出的学习率只是历史样本上的概率最优,实盘切换品种前务必重跑验证集。
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 各跑一遍,观察样本外曲线是否塌方。
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 或加仓判断留接口。
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 == 0 | current_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 时才改。外汇与贵金属杠杆高,补单前先确认点值,滑点可能让挂单偏离预期。
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 拖进回测,看预测注释是不是也掉到近零;若是,就等稳定版,或先读同系列后面的多策略加权投票那几篇顶着用。