在MQL5中构建自适应的自动化交易系统(EA)·进阶篇
线性回归模型的内部状态字段
在 MT5 里用 MQL5 搭一个监督学习式的行情模型,第一步是把训练与验证阶段要用的状态量先声明清楚。下面这段类内成员定义,把学习率搜索、训练轮次、输入输出矩阵和权重偏置全部摊开,方便后续在 OnInit 或独立训练函数里直接读写。 mae_array 长度固定为 30,对应我们在验证集上测试 30 个不同学习率幂次时的平均绝对误差读数;learning_rate_power 与 lr_error_index 配合,记录当前正在评估的学习率 10 的负幂次位置。epochs_power 则决定总训练轮数取 10 的几次方,比如设为 3 即最多约 1000 轮。 x_train / y_train 与 x_validation / y_validation 是成对的向量,分别装训练与验证的样本输入和真实输出;y_hat_train / y_hat_validation 存模型预测值,用来算 mae_train 和 mae_validation 两个误差向量。权重 m 和偏置 b 都是 vector 类型,forecast 是 Double 标量,代表模型对下一根 K 线的点预测。 n 这个 ulong 存的是数据行数(即抓取样本量),在梯度下降的循环里控制边界。外汇与贵金属波动大、跳空频繁,这类线性外推模型在实盘只可作辅助参考,信号失效概率不低,务必先在策略测试器跑通再谈仓位。
class="type">class="kw">double mae_array[class="num">30]; class="type">bool trained; class="type">int epochs_power; vector mae_train,mae_validation; vector x_validation,x_train; vector y_validation,y_train; vector y_hat_validation,y_hat_train; vector m; vector b; class="type">class="kw">double forecast; class="type">class="kw">ulong learning_rate_power; class="type">int lr_error_index; class="type">class="kw">double learning_rate; class="type">class="kw">double epochs; class="type">class="kw">ulong n;
「回归模型类的私有字段与接口声明」
在 MT5 里自建线性回归器时,先得把时间和索引绑定清楚。下面这组私有成员用 output_start / output_end 与 input_start / input_end 四个 datetime 变量锁定输入输出样本区间,再用对应的 index_ 前缀整型记录柱线偏移位置,避免回测时取错 K 线。 first_reading 这个 double 承担数据缩放基准,allowed_to_evaluate 则是防止样本不足时硬跑拟合的开关。外汇与贵金属行情的高波动特性,意味着缩放因子若取在异常极值上,后续系数可能严重偏离,实盘前应在策略测试器里核对首根取值。 公开接口暴露了模型生命周期:LinearRegression() 构造函数、Init(_fetch,_look_ahead) 设定回看与前瞻窗、GetCurrentValidationData() 拉验证集。私有侧则给出 Fit() 算系数、Evaluate() 按指定 epoch 幂次评估、ScaleInputs() 做输入归一,以及 learning rate 与 epochs 的更新/重置系列方法。 把这段声明直接贴进 .mqh 头文件就能编译,下一步该填的是 Fit() 里的梯度下降循环——那是下一段要拆的。
class="type">class="kw">datetime output_end,output_start,input_end,input_start; 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="type">int _epochs_power); 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);
◍ 线性回归模型的初始化与训练准备
在 MT5 里用 CLinearRegression 类做价格预测时,构造函数只做一件事:用 Print 把当前交易品种打进日志,方便你确认上下文没错。 真正的准备逻辑都在 Init(int _fetch, int _look_ahead) 里。它会先 ObjectsDeleteAll(0) 清掉图表上所有对象,再把 allowed_to_evaluate 置 true 允许后续评估;epochs_power 写死为 4,epochs 算出来是 5 * 10^4 = 50000 次迭代,这是训练轮数上限。 trained 初始为 false,max_learning_rate_power 给到 30,意味着学习率幂次最多搜到 0.1^30。start 取当前周期第 1 根 K 线的时间戳 iTime(_Symbol, PERIOD_CURRENT, 1),作为验证数据末端。 fetch 和 look_ahead 在入参上各减/加 1 存入 this,梯度 m 和偏置 b 都用 vector::Zeros(1) 清零,forecast 先设 0。learning_rate_power 从 0 起跳,lr_error_index 为 0,mae_train 初始化成 10^100 的 1 维向量,mae_validation 则是 30 维、每维填 10^10000——这就是后续找最优学习率时的误差上界占位。 初始化收尾把 learning_rate 算成 0.1^0 = 1,n 等于 fetch 作为样本行数。若 GetCurrentValidationData() 返回真,就接着进数据缩放分支。外汇和贵金属波动剧烈,这种缩放和幂次搜索直接决定过拟合风险,建议你在策略测试器里改 epochs_power 看回测稳定性。
class=class="str">"cmt">//Function to determine if the model has been trained and is ready for use. class="type">bool Trained(class="type">void); class=class="str">"cmt">//A function to train the model class="kw">using the best learning rate and the most recent prices class="type">bool Train(class="type">void); class=class="str">"cmt">//A function to predict future price class="kw">using the current price. class="type">class="kw">double Predict(class="type">void); class=class="str">"cmt">//Destructor ~LinearRegression(); LinearRegression::LinearRegression() { Print("Current Symbol: ",_Symbol); } 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
回归预测与验证数据的落地写法
线性回归模型训练完后,真正产生交易信号的是 Predict() 这一环。它先取当前收盘价 _current_reading = iClose(_Symbol,PERIOD_CURRENT,0),再用已算出的斜率 m[0] 和截距 b[0] 做一步 y = m[0]*x + b[0] 的外推。 预测值大于现价时在终端提示 Buy,小于则提示 Sell,同时用 ObjectCreate 画一条竖线标时间、一条水平线标预测价位。注意这里只是方向倾向,外汇与贵金属杠杆高,信号失效可能很快。 验证数据切分靠 GetCurrentValidationData() 里的索引偏移:index_output_end 固定为 1,index_output_start = 1 + fetch,输入侧再叠加 look_ahead。也就是说模型看的是未来偏移 look_ahead 根 K 线后的收盘价做对照。 复制下面代码到 MT5 的 LinearRegression 类里,把 look_ahead 设成 5、fetch 设成 200,能在 EURUSD 的 M15 上直接看到预测水平线漂在现价上方或下方。
ScaleInputs(); class=class="str">"cmt">//Fit the model Fit(); } } 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); } 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
「验证集与训练集的切片对齐」
这段逻辑干的事很直白:先抓验证集的收盘价向量,再检查 x、y 两个向量长度是否一致,不一致直接返 false,避免后面矩阵维度炸掉。 若 CopyRates 拉不到数据,终端会打印错误码并退出;成功的话会在图表上画四条垂直参考线,标记 validation 与 training 各自的起止时间,肉眼就能核对窗口有没有错位。 训练集的时间戳不是直接传参,而是用索引推算:index_output_end = index_input_start + look_ahead*2,再借 iTime 反查真实时间。look_ahead 翻倍这段是滑动窗口的关键,调小它会让输入输出重叠度上升,过拟合概率可能变大。 最后分别用 y_train、x_train 的 CopyRates 按 output_end / input_end 拉取训练数据,这里若返回 false 仅打印提示而不中断,实盘跑之前建议自己补个 return 保护。
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; 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))
◍ 训练数据对齐与梯度下降拟合的实现细节
取数阶段先校验 x_train 与 y_train 的 Size() 是否一致,若维度不匹配直接 Print 报错退出,避免后续矩阵运算越界。这一步在 MT5 脚本里常被忽略,但实盘回测中样本错位会让回归系数完全失真。 数据就绪后在图表上用 OBJ_VLINE 标出 input_start / input_end 与 output_start / output_end,四条竖线把训练区间和标签区间可视化,方便肉眼核对特征与目标的偏移关系。 Fit() 里跑的是标准批量梯度下降:每个 epoch 先算 y_hat_train = m[0]*x_train + b[0],再用 (-2.0/n) 乘残差求和得到导数。learning_rate 乘以导数后更新 m[0] 与 b[0],循环 epochs 次逼近最优斜率与截距。 mae_train 用 MathAbs(y_train - y_hat_train).Mean() 记录平均绝对误差,外汇与贵金属市场波动剧烈、高风险,该值仅反映训练集拟合程度,对未知行情的解释力可能随品种与周期变化而下降。
{
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::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