基于通用 MLP 逼近器的EA·进阶篇
多层感知机结构在MT5里的落地骨架
想在 MT5 里自己搭一个前馈神经网络,先得把网络结构用结构体钉死。下面这段定义把每一层神经元值塞进动态数组,再用一个总数组管理所有层,权重总数、层数、遍历游标都单独记——这是后续前向计算不越界的基础。 初始化函数 C_MLP::Init 干的第一件事是防御性校验:层数少于 2(没有输入和输出层)直接返回 0 并在日志抛 "Layers less than 2";任意一层神经元数 ≤0 也会返回 0 并报出具体第几层出错。 通过校验后,代码用 ArrayCopy 把配置存进 layerConf,按 layersCNT 扩出层数组,再逐层给 neurons 分配内存。权重总数 weightsCNT 初始为 0,随后对相邻层循环累加,这部分循环体虽被截断,但逻辑就是 (前层神经元数 × 后层神经元数 + 后层偏置数) 的连加。 外汇与贵金属行情用这类模型做信号推断时,过拟合和样本外失效风险极高,任何权重初始化后的网络都只是未训练空壳,实盘前必须自行喂数据回测。
class="kw">struct S_Layer { class="type">class="kw">double l []; class=class="str">"cmt">// Neuron values }; S_Layer layers []; class=class="str">"cmt">// Array of all network layers class="type">int weightsCNT; class=class="str">"cmt">// Total number of weights in the network(including biases) class="type">int layersCNT; class=class="str">"cmt">// Total number of layers(including input and output ones) class="type">int cnt_W; class=class="str">"cmt">// Current index in the weights array when traversing the network class="type">class="kw">double temp; class=class="str">"cmt">// Temporary variable to store the sum of the weighted inputs class=class="str">"cmt">// Calculate values of one layer of the network class="type">void LayerCalc(class="type">class="kw">double &inLayer [], class=class="str">"cmt">// values of neurons of the previous layer class="type">class="kw">double &weights [], class=class="str">"cmt">// array of weights and biases of the entire network class="type">class="kw">double &outLayer [], class=class="str">"cmt">// array for writing values of the current layer class="kw">const class="type">int inSize, class=class="str">"cmt">// number of neurons in the input layer class="kw">const class="type">int outSize); class=class="str">"cmt">// outSize - number of neurons in the output layer }; class=class="str">"cmt">//+----------------------------------------------------------------------------+ class=class="str">"cmt">//| Initialize the network | class=class="str">"cmt">//| layerConfig - array with the number of neurons in each layer | class=class="str">"cmt">//| Returns the total number of weights needed, or class="num">0 in case of an error | class=class="str">"cmt">//+----------------------------------------------------------------------------+ class="type">int C_MLP::Init(class="type">int &layerConfig []) { class=class="str">"cmt">// Check that the network has at least class="num">2 layers(input and output) layersCNT = ArraySize(layerConfig); if (layersCNT < class="num">2) { Print("Error Net config! Layers less than class="num">2!"); class="kw">return class="num">0; } class=class="str">"cmt">// Check that each layer has at least class="num">1 neuron for (class="type">int i = class="num">0; i < layersCNT; i++) { if (layerConfig [i] <= class="num">0) { Print("Error Net config! Layer No." + class="type">class="kw">string (i + class="num">1) + " contains class="num">0 neurons!"); class="kw">return class="num">0; } } class=class="str">"cmt">// Save network configuration ArrayCopy(layerConf, layerConfig, class="num">0, class="num">0, WHOLE_ARRAY); class=class="str">"cmt">// Create an array of layers ArrayResize(layers, layersCNT); class=class="str">"cmt">// Allocate memory for neurons of each layer for (class="type">int i = class="num">0; i < layersCNT; i++) { ArrayResize(layers [i].l, layerConfig [i]); } class=class="str">"cmt">// Calculate the total number of weights in the network weightsCNT = class="num">0; for (class="type">int i = class="num">0; i < layersCNT - class="num">1; i++) {
◍ 单层前向传播与权重布局的实算
构建一个多层感知机时,权重总数不是简单相乘。代码里用 weightsCNT += layerConf[i] * layerConf[i+1] + layerConf[i+1] 累加:每一层向下一层全连接,需要「本层神经元数 × 下层神经元数」条连接权重,再加下层每个神经元一个偏置。比如三层配置 [10, 5, 1],权重总量 = 10*5+5 + 5*1+1 = 61。 单层计算在 LayerCalc 里完成,核心就是 y = tanh(bias + Σ w*u)。循环先取当前神经元的偏置 weights[cnt_W],再遍历输入层每个神经元做加权累加,最后套双曲正切。 激活函数写成 2.0/(1.0+exp(-temp))-1.0,输出被压进 [-1, 1] 区间。对外汇或贵金属信号做归一化时,这个有界输出比无界线性层更不容易在极端波动下炸数值,但模型本身不预测方向,只是拟合历史样本,实盘仍属高风险。 ANN 方法负责串起整网:cnt_W 归零后,ArrayCopy 把行情特征塞进第 0 层,再逐层调 LayerCalc 直到输出层。开 MT5 把这段直接挂进 EA,改 layerConf 数组就能验证不同网络宽度对回测拟合的影响。
class=class="str">"cmt">// For each neuron of the next layer we need: class=class="str">"cmt">// - one bias value class=class="str">"cmt">// - weights for connections with all neurons of the current layer weightsCNT += layerConf [i] * layerConf [i + class="num">1] + layerConf [i + class="num">1]; } class="kw">return weightsCNT; } class=class="str">"cmt">//+----------------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate values of one layer of the network | class=class="str">"cmt">//| Implement the equation: y = tanh(bias + w1*x1 + w2*x2 + ... + wn*xn) | class=class="str">"cmt">//+----------------------------------------------------------------------------+ class="type">void C_MLP::LayerCalc(class="type">class="kw">double& inLayer [], class="type">class="kw">double& weights [], class="type">class="kw">double& outLayer [], class="kw">const class="type">int inSize, class="kw">const class="type">int outSize) { class=class="str">"cmt">// Calculate the value for each neuron in the output layer for (class="type">int i = class="num">0; i < outSize; i++) { class=class="str">"cmt">// Start with the bias value for the current neuron temp = weights [cnt_W]; cnt_W++; class=class="str">"cmt">// Add weighted inputs from each neuron in the previous layer for (class="type">int u = class="num">0; u < inSize; u++) { temp += inLayer [u] * weights [cnt_W]; cnt_W++; } class=class="str">"cmt">// Apply the "hyperbolic tangent" activation function class=class="str">"cmt">// f(x) = class="num">2/(class="num">1 + e^(-x)) - class="num">1 class=class="str">"cmt">// Range of values f(x): [-class="num">1, class="num">1] outLayer [i] = class="num">2.0 / (class="num">1.0 + exp(-temp)) - class="num">1.0; } } class=class="str">"cmt">//+----------------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate the values of all layers sequentially from input to output | class=class="str">"cmt">//+----------------------------------------------------------------------------+ class="type">void C_MLP::ANN(class="type">class="kw">double &inLayer [], class=class="str">"cmt">// input values class="type">class="kw">double &weights [], class=class="str">"cmt">// network weights(including biases) class="type">class="kw">double &outLayer []) class=class="str">"cmt">// output layer values { class=class="str">"cmt">// Reset the weight counter before starting the pass cnt_W = class="num">0; class=class="str">"cmt">// Copy the input data to the first layer of the network ArrayCopy(layers [class="num">0].l, inLayer, class="num">0, class="num">0, WHOLE_ARRAY); class=class="str">"cmt">// Calculate the values of each layer sequentially for (class="type">int i = class="num">0; i < layersCNT - class="num">1; i++) { LayerCalc(layers [i].l, class=class="str">"cmt">// output of the previous layer weights, class=class="str">"cmt">// network weights(including bias) layers [i + class="num">1].l, class=class="str">"cmt">// next layer layerConf [i], class=class="str">"cmt">// size of current layer
「EA 输入参数与指标句柄的初始化落点」
这段 MT5 代码把多层感知机(MLP)交易 EA 的外部参数和内部句柄一次性铺开,重点在 input 分组与 OnInit 里的指标挂载。交易参数块里 Lot_P 默认 0.01 手,交易时段被锁在 StartTradeH_P=3 到 EndTradeH_P=12(服务器小时),避开了流动性最稀薄的亚盘尾段。 训练侧给的是典型离线配置:NumbTestFuncRuns_P=5000 次函数评估、MLPstructure_P="1|1" 表示单隐藏层结构、DepthHistoryBars_P=10000 根历史棒做训练样本、RetrainingPeriod_P=12 小时强制重训一次,SigThr_P=0.5 是网络输出越过即触发的信号阈值。外汇与贵金属杠杆高,这类默认仓位和阈值直接实盘可能放大回撤,建议先开 MT5 策略测试器用 EURUSD 15M 跑一遍再调。 OnInit 中只挂了两个输入特征源:iStochastic(_Symbol,PERIOD_CURRENT,5,3,3,MODE_EMA,STO_LOWHIGH) 与 iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_TYPICAL)。随机指标用 5 周期快线、3 平滑,RSI 取典型价的 14 周期——这两个句柄后续会喂给 BarsAnalysis_P=3 根棒拼成的输入向量。
| 别把默认结构当最优解。MLPstructure_P 写成 "1 | 1" 只是最小验证用例,真要做贵金属波段,hidden 层加到 "4 | 6 | 2" 才可能吃到非线性边界,但 5000 次评估在 10000 根历史上大概率欠拟合。 |
|---|
class="macro">#include "class="macro">#Symbol.mqh" class="macro">#include <Math\AOs\Utilities.mqh> class="macro">#include <Math\AOs\NeuroNets\MLP.mqh> class="macro">#include <Math\AOs\PopulationAO\class="macro">#C_AO_enum.mqh> class=class="str">"cmt">//------------------------------------------------------------------------------ input group "---Trade parameters-------------------"; input class="type">class="kw">double Lot_P = class="num">0.01; class=class="str">"cmt">// Position volume input class="type">int StartTradeH_P = class="num">3; class=class="str">"cmt">// Trading start time input class="type">int EndTradeH_P = class="num">12; class=class="str">"cmt">// Trading end time input group "---Training parameters----------------"; input E_AO OptimizerSelect_P = AO_CLA; class=class="str">"cmt">// Select optimizer input class="type">int NumbTestFuncRuns_P = class="num">5000; class=class="str">"cmt">// Total number of function runs input class="type">class="kw">string MLPstructure_P = "class="num">1|class="num">1"; class=class="str">"cmt">// Hidden layers, <class="num">4|class="num">6|class="num">2> - three hidden layers input class="type">int BarsAnalysis_P = class="num">3; class=class="str">"cmt">// Number of bars to analyze input class="type">int DepthHistoryBars_P = class="num">10000; class=class="str">"cmt">// History depth for training in bars input class="type">int RetrainingPeriod_P = class="num">12; class=class="str">"cmt">// Duration in hours of the model&class="macro">#x27;s relevance input class="type">class="kw">double SigThr_P = class="num">0.5; class=class="str">"cmt">// Signal threshold class=class="str">"cmt">//------------------------------------------------------------------------------ C_AO_Utilities U; C_MLP NN; class="type">int InpSigNumber; class="type">int WeightsNumber; class="type">class="kw">double Inputs []; class="type">class="kw">double Weights []; class="type">class="kw">double Outs [class="num">1]; class="type">class="kw">datetime LastTrainingTime = class="num">0; C_Symbol S; C_NewBar B; class="type">int HandleS; class="type">int HandleR; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">int OnInit() { class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// Initializing indicators: Stochastic and RSI HandleS = iStochastic(_Symbol, PERIOD_CURRENT, class="num">5, class="num">3, class="num">3, MODE_EMA, STO_LOWHIGH); HandleR = iRSI(_Symbol, PERIOD_CURRENT, class="num">14, PRICE_TYPICAL);
MLP 结构初始化与权重随机 seeding
把分析窗口换算成网络输入维度时,代码用 BarsAnalysis_P * 2 + BarsAnalysis_P * 4 直接算出 InpSigNumber。若 BarsAnalysis_P 取 30,输入节点就是 180,Print 会吐出 "Number of network logins : 180",开 MT5 跑这段能立刻核对自己的窗口参数有没有算错。 MLP 隐层靠 '|' 分隔的字符串解析,StringSplit 拿到 layersNumb 后若小于 1 直接 INIT_FAILED,说明配置里至少得写一层隐层。代码随后把数组扩成 layersNumb+2,下标 0 塞输入数、末位塞 1(单输出),中间循环把字符串转成每隐层神经元数,任一层 ≤0 同样报错退初始化。 NN.Init 返回权重总数,权重数组按 WeightsNumber 扩容,并用 rand()/32767.0 做 [-1,1] 均匀随机初始化——这只是调试用的 naive seeding,真实训练前会被覆盖。最后 Print 的 "Number of network parameters" 就是后续要优化的自由参数量,例如 180 输入+隐层 50+输出 1 的全连接结构,权重数会明显大于 230,外汇与贵金属品种用此类模型属高风险,过拟合概率偏高。 交易与 K 线对象以 _Symbol 和 PERIOD_CURRENT 绑定后返回 INIT_SUCCEEDED,EA 才算真正挂上当前图表。改 BarsAnalysis_P 或 MLPstructure_P 任意一个,都要重看 Print 出来的层数与参数总量是否匹配预期。
class=class="str">"cmt">// Calculate the number of inputs to the neural network based on the number of bars to analyze InpSigNumber = BarsAnalysis_P * class="num">2 + BarsAnalysis_P * class="num">4; class=class="str">"cmt">// Display information about the number of inputs Print("Number of network logins : ", InpSigNumber); class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// Initialize the structure of the multilayer MLP class="type">class="kw">string sepResult []; class="type">int layersNumb = StringSplit(MLPstructure_P, StringGetCharacter("|", class="num">0), sepResult); class=class="str">"cmt">// Check if the number of hidden layers is greater than class="num">0 if (layersNumb < class="num">1) { Print("Network configuration error, hidden layers < class="num">1..."); class="kw">return INIT_FAILED; class=class="str">"cmt">// Return initialization error } class=class="str">"cmt">// Increase the number of layers by class="num">2 (input and output) layersNumb += class="num">2; class=class="str">"cmt">// Initialize array for neural network configuration class="type">int nnConf []; ArrayResize(nnConf, layersNumb); class=class="str">"cmt">// Set the number of inputs and outputs in the network configuration nnConf [class="num">0] = InpSigNumber; class=class="str">"cmt">// Input layer nnConf [layersNumb - class="num">1] = class="num">1; class=class="str">"cmt">// Output layer class=class="str">"cmt">// Filling the hidden layers configuration for (class="type">int i = class="num">1; i < layersNumb - class="num">1; i++) { nnConf [i] = (class="type">int)StringToInteger(sepResult [i - class="num">1]); class=class="str">"cmt">// Convert a class="type">class="kw">string value to an integer class=class="str">"cmt">// Check that the number of neurons in a layer is greater than class="num">0 if (nnConf [i] < class="num">1) { Print("Network configuration error, in layer ", i, " <= class="num">0 neurons..."); class="kw">return INIT_FAILED; class=class="str">"cmt">// Return initialization error } } class=class="str">"cmt">// Initialize the neural network and get the number of weights WeightsNumber = NN.Init(nnConf); if (WeightsNumber <= class="num">0) { Print("Error initializing MLP network..."); class="kw">return INIT_FAILED; class=class="str">"cmt">// Return initialization error } class=class="str">"cmt">// Resize the input array and weights ArrayResize(Inputs, InpSigNumber); ArrayResize(Weights, WeightsNumber); class=class="str">"cmt">// Initialize weights with random values in the range [-class="num">1, class="num">1] (for debugging) for (class="type">int i = class="num">0; i < WeightsNumber; i++) Weights [i] = class="num">2 * (rand() / class="num">32767.0) - class="num">1; class=class="str">"cmt">// Output network configuration information Print("Number of all layers : ", layersNumb); Print("Number of network parameters: ", WeightsNumber); class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// Initialize the trade and bar classes S.Init(_Symbol); B.Init(_Symbol, PERIOD_CURRENT); class="kw">return (INIT_SUCCEEDED); class=class="str">"cmt">// Return successful initialization result }
◍ 定时重训与每根新K线的特征归一化
EA 用 RetrainingPeriod_P 控制重训节奏:当当前时间距离上次训练已超过该参数乘以 3600 秒(即小时数转秒),就触发 Training() 并重写 LastTrainingTime;训练失败仅打印错误并直接 return,不往下走。这一步若设得太短,CPU 占用会明显抬升,外汇与贵金属的高波动时段跑模型要留意算力成本。 非重训分支里,先靠 B.IsNewBar() 拦截,只在每根 K 线开盘后处理一次,避免 tick 级重复计算。随后用 CopyRates 取 BarsAnalysis_P 根历史 K 线,再用 CopyBuffer 分别抓 Stochastic 主线与 RSI 各 BarsAnalysis_P 个值,任一带回数量不符就退出。 归一化是这套 NN 输入的关键:先扫一遍 high/low 找全样本 max/min,再把每根 K 线的 open/high/low/close 以及 sto、rsi 都通过 U.Scale 压到 [-1,1] 区间,其中 sto 与 rsi 的固定上下界是 0 和 100。若你改了指标周期,这一步的边界常量要同步核对。 喂完 Inputs 后调 NN.ANN 得到 Outs,当 Outs[0] 大于 SigThr_P 时 signal 置 1,作为做多倾向的触发条件。SigThr_P 越靠近 1,信号越保守,实盘前建议在 MT5 策略测试器里用不同阈值跑一遍命中率。
if (TimeCurrent() - LastTrainingTime >= RetrainingPeriod_P * class="num">3600) { class=class="str">"cmt">// Start the neural network training if (Training()) LastTrainingTime = TimeCurrent(); class=class="str">"cmt">// Update last training time else Print("Training error..."); class=class="str">"cmt">// Display an error message class="kw">return; class=class="str">"cmt">// Complete function execution } class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// Check if the current tick is the start of a new bar if (!B.IsNewBar()) class="kw">return; class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// Declare arrays to store price and indicator data class="type">MqlRates rates []; class="type">class="kw">double rsi []; class="type">class="kw">double sto []; class=class="str">"cmt">// Get price data if (CopyRates(_Symbol, PERIOD_CURRENT, class="num">1, BarsAnalysis_P, rates) != BarsAnalysis_P) class="kw">return; class=class="str">"cmt">// Get Stochastic values if (CopyBuffer(HandleS, class="num">0, class="num">1, BarsAnalysis_P, sto) != BarsAnalysis_P) class="kw">return; class=class="str">"cmt">// Get RSI values if (CopyBuffer(HandleR, class="num">0, class="num">1, BarsAnalysis_P, rsi) != BarsAnalysis_P) class="kw">return; class=class="str">"cmt">// Initialize variables to normalize data class="type">int wCNT = class="num">0; class="type">class="kw">double max = -DBL_MAX; class=class="str">"cmt">// Initial value for maximum class="type">class="kw">double min = DBL_MAX; class=class="str">"cmt">// Initial value for minimum class=class="str">"cmt">// Find the maximum and minimum among high and low for (class="type">int b = class="num">0; b < BarsAnalysis_P; b++) { if (rates [b].high > max) max = rates [b].high; class=class="str">"cmt">// Update the maximum if (rates [b].low < min) min = rates [b].low; class=class="str">"cmt">// Update the minimum } class=class="str">"cmt">// Normalization of input data for neural network for (class="type">int b = class="num">0; b < BarsAnalysis_P; b++) { Inputs [wCNT] = U.Scale(rates [b].high, min, max, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing high Inputs [wCNT] = U.Scale(rates [b].low, min, max, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing low Inputs [wCNT] = U.Scale(rates [b].open, min, max, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing open Inputs [wCNT] = U.Scale(rates [b].close, min, max, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing close Inputs [wCNT] = U.Scale(sto [b], class="num">0, class="num">100, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing Stochastic Inputs [wCNT] = U.Scale(rsi [b], class="num">0, class="num">100, -class="num">1, class="num">1); wCNT++; class=class="str">"cmt">// Normalizing RSI } class=class="str">"cmt">// Convert data from Inputs to Outs NN.ANN(Inputs, Weights, Outs); class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// Generate a trading signal based on the output of a neural network class="type">int signal = class="num">0; if (Outs [class="num">0] > SigThr_P) signal = class="num">1; class=class="str">"cmt">// Buy signal
「用历史K线喂给优化器找权重」
信号判定之后,真正决定策略泛化能力的是参数权重。Training() 函数先把当前品种、当前周期的历史数据拉进来:CopyRates 从偏移 1 根开始取 DepthHistoryBars_P 根,假设设成 5000,终端日志会打印 'Training on history of 5000 bars',这就是可验证的数据点。 RSI 与随机指标各自用 CopyBuffer 取主缓冲区,若返回根数不等于 bars 直接 return false,说明历史深度对齐失败,常见于品种休市缺口。 时间过滤不是只在实盘做,回测同样按 StartTradeH_P 到 EndTradeH_P 逐根打标签写进 truTradeTime[],非交易时段不参与适应度计算。 优化骨架用固定种群:popSize = 50,epochCount = NumbTestFuncRuns_P / 50。若总运行次数设 20000,则迭代 400 代。WeightsNumber 个权重各自分配 min/max/step 边界数组,循环里逐个初始化,下一步就是丢进进化算法搜最优组合。外汇与贵金属杠杆高,历史权重优异不代表实盘能复现,开 MT5 把 DepthHistoryBars_P 调小先跑通再放大。
class="type">bool Training() { class="type">MqlRates rates []; class="type">class="kw">double rsi []; class="type">class="kw">double sto []; class="type">int bars = CopyRates(_Symbol, PERIOD_CURRENT, class="num">1, DepthHistoryBars_P, rates); Print("Training on history of ", bars, " bars"); if (CopyBuffer(HandleS, class="num">0, class="num">1, DepthHistoryBars_P, sto) != bars) class="kw">return class="kw">false; if (CopyBuffer(HandleR, class="num">0, class="num">1, DepthHistoryBars_P, rsi) != bars) class="kw">return class="kw">false; class="type">MqlDateTime time; class="type">bool truTradeTime []; ArrayResize(truTradeTime, bars); ArrayInitialize(truTradeTime, class="kw">false); for (class="type">int i = class="num">0; i < bars; i++) { TimeToStruct(rates [i].time, time); if (time.hour >= StartTradeH_P && time.hour < EndTradeH_P) truTradeTime [i] = true; } class="type">int popSize = class="num">50; class=class="str">"cmt">// Population size for optimization algorithm class="type">int epochCount = NumbTestFuncRuns_P / popSize; class=class="str">"cmt">// Total number of epochs(iterations) for optimization class="type">class="kw">double rangeMin [], rangeMax [], rangeStep []; class=class="str">"cmt">// Arrays for storing the parameters&class="macro">#x27; boundaries and steps ArrayResize(rangeMin, WeightsNumber); class=class="str">"cmt">// Resize &class="macro">#x27;min&class="macro">#x27; borders array ArrayResize(rangeMax, WeightsNumber); class=class="str">"cmt">// Resize &class="macro">#x27;max&class="macro">#x27; borders array ArrayResize(rangeStep, WeightsNumber); class=class="str">"cmt">// Resize the steps array for (class="type">int i = class="num">0; i < WeightsNumber; i++) {