English Deutsch 日本語
preview
MQL5交易策略自动化(第二十一部分):借助自适应学习率提升神经网络交易效果

MQL5交易策略自动化(第二十一部分):借助自适应学习率提升神经网络交易效果

MetaTrader 5交易 |
89 0
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

前一篇文章(第二十部分)中,我们开发了基于商品通道指数(CCI)和动量震荡指标(AO)的多品种交易策略,在MetaQuotes Language 5(MQL5)中实现了针对多种货币对的趋势反转自动化交易,该策略具备强大的信号生成和风险管理功能。本系列第二十一部分中,我们将转向基于神经网络的交易策略,通过引入自适应学习率机制,优化市场走势预测的准确性。我们将涵盖以下主题:

  1. 理解自适应神经网络学习率策略
  2. 在MQL5中实现
  3. 测试与优化学习率调整
  4. 结论

阅读完本文后,您将掌握一套完整的MQL5交易系统,该系统利用神经网络实现动态学习率调整,为进一步优化做好准备——让我们开始深入探讨吧!


理解自适应神经网络学习率策略

第二十部分中,我们开发了一个多品种交易系统,该系统利用了商品通道指数和动量震荡指标,实现了在多个货币对上的自动化趋势反转交易。在本系列第二十一部分中,我们将深入探讨一种基于动态神经网络的交易策略,借助神经网络——一种模仿人脑神经元互联结构的计算模型——处理多种市场指标,并根据市场波动性调整学习过程,从而更精准地预测市场价格走势。我们的目标是构建一个灵活、高性能的交易系统,利用神经网络分析复杂的市场模式,并通过自适应学习率机制以优化精度执行交易。

神经网络由节点(神经元)构成的层级结构工作,这些层级包括用于捕获市场数据的输入层、用于揭示复杂模式特征的隐藏层,以及用于生成交易信号(如预测价格涨跌方向)的输出层。数据在这些层级间通过前向传播流动,神经元对输入数据施加权重和偏置,将其转化为预测结果。具体如下:

具有层级结构与权重参数的神经网络

激活函数是一个关键组件,它为这些变换引入了非线性特性,使网络能够对复杂关系进行建模;例如,sigmoid激活函数将数值映射到0到1的范围,使其非常适合用于诸如买入或卖出决策等二元分类任务。反向传播则通过从预测误差反向推导来优化这一过程,并调整权重和偏置,随着时间的推移提高预测准确性。具体如下:

传播

为使网络具备动态性,我们计划实施一种自适应学习率策略,能够根据反向传播过程中权重更新的速度进行调整——当预测结果与市场实际走势相符时加快学习速度,当误差激增时减缓学习速度以确保稳定性。

我们规划设计一个系统,将移动平均线和动量指标等市场指标输入神经网络的输入层,通过隐藏层处理这些指标以检测模式,并通过输出层生成可靠的交易信号。计划使用sigmoid激活函数对神经元输出进行转换,以确保为交易决策提供平滑、可解释的预测结果。选用该函数是为了能够得到两种输出选项。以下是其他可使用的函数示例:

激活函数

此外,我们将根据训练表现动态调整学习率,并依据市场波动性调整隐藏层神经元的数量,从而构建一个兼具复杂性与高效性的响应式系统。这一策略为稳健的系统实现和全面的测试奠定了基础。我们将采用两条移动平均线(MA)、相对强弱指数 (RSI)和平均真实波幅(ATR)作为输入指标,一旦生成交易信号,便执行开仓操作。以下是系统实现后的预期效果示意图:

规划图


在MQL5中的实现

要在MQL5中创建该程序,请打开MetaEditor,在导航器中找到“指标”文件夹,点击“新建”选项卡,并按照向导提示创建文件。完成代码编写环境的准备后,我们将首先声明一些输入变量、结构体和类,以采用面向对象编程(OOP)方法实现相关功能。

//+------------------------------------------------------------------+
//|                              Neural Networks Propagation EA.mq5  |
//|                          Copyright 2025, Allan Munene Mutiiria.  |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

#include <Trade/Trade.mqh>
CTrade tradeObject; //--- Instantiate trade object for executing trades

// Input parameters with clear, meaningful names
input double LotSize            = 0.1;      // Lot Size
input int    StopLossPoints     = 100;      // Stop Loss (points)
input int    TakeProfitPoints   = 100;      // Take Profit (points)
input int    MinHiddenNeurons   = 10;       // Minimum Hidden Neurons
input int    MaxHiddenNeurons   = 50;       // Maximum Hidden Neurons
input int    TrainingBarCount   = 1000;     // Training Bars
input double MinPredictionAccuracy = 0.7;   // Minimum Prediction Accuracy
input double MinLearningRate    = 0.01;     // Minimum Learning Rate
input double MaxLearningRate    = 0.5;      // Maximum Learning Rate
input string InputToHiddenWeights = "0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1"; // Input-to-Hidden Weights
input string HiddenToOutputWeights = "0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1"; // Hidden-to-Output Weights
input string HiddenBiases       = "0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1"; // Hidden Biases
input string OutputBiases       = "0.1,-0.1"; // Output Biases

// Neural Network Structure Constants
const int INPUT_NEURON_COUNT = 10; //--- Define number of input neurons
const int OUTPUT_NEURON_COUNT = 2; //--- Define number of output neurons
const int MAX_HISTORY_SIZE = 10;   //--- Define maximum history size for accuracy and error tracking

// Indicator handles
int ma20IndicatorHandle; //--- Handle for 20-period moving average
int ma50IndicatorHandle; //--- Handle for 50-period moving average
int rsiIndicatorHandle;  //--- Handle for RSI indicator
int atrIndicatorHandle;  //--- Handle for ATR indicator

// Training related structures
struct TrainingData {
    double inputValues[]; //--- Array to store input values for training
    double targetValues[]; //--- Array to store target values for training
};

在此阶段,我们通过初始化交易执行与数据处理的关键组件,为基于自适应学习率神经网络的交易策略奠定基础。我们引入了"Trade.mqh"库,并创建了"CTrade"类的"tradeObject"实例以管理交易操作,从而能够根据神经网络的预测结果执行买入和卖出订单。

我们定义了输入参数以配置该策略,设置"LotSize"以控制交易量,通过"StopLossPoints"和"TakeProfitPoints"进行风险管理,并设置"MinHiddenNeurons"和"MaxHiddenNeurons"以定义神经网络中隐藏层神经元的数量范围。此外,我们还指定"TrainingBarCount"用于确定训练所使用的历史K线数量,设置"MinPredictionAccuracy"作为准确率阈值,并设定"MinLearningRate"和"MaxLearningRate"以界定自适应学习率的范围。同时提供"InputToHiddenWeights"、"HiddenToOutputWeights"、"HiddenBiases"和"OutputBiases"作为字符串输入,用于初始化神经网络的权重和偏置,允许采用预设或默认配置。

接下来,我们为神经网络结构设定常量,将"INPUT_NEURON_COUNT"定义为10(用于市场数据输入),"OUTPUT_NEURON_COUNT"定义为2(用于生成买入/卖出信号输出),并将"MAX_HISTORY_SIZE"定义为10(用于跟踪训练准确率和误差)。创建指标句柄"ma20IndicatorHandle"、"ma50IndicatorHandle"、"rsiIndicatorHandle"和"atrIndicatorHandle",分别引用20周期和50周期MA、RSI以及ATR指标,以便将市场数据输入神经网络。最后,我们定义了"TrainingData"结构体,其中包含"inputValues"和"targetValues"数组,存储训练所需的输入特征和预期输出,确保神经网络学习过程中的数据管理井然有序。在填充数据时,我们将按如下方式存储数据:

基于输入的数据结构

接下来,我们需要定义一个类,用于封装大部分需要频繁使用的基本成员变量。

// Neural Network Class
class CNeuralNetwork {
private:
   int inputNeuronCount;          //--- Number of input neurons
   int hiddenNeuronCount;         //--- Number of hidden neurons
   int outputNeuronCount;         //--- Number of output neurons
   double inputLayer[];           //--- Array for input layer values
   double hiddenLayer[];          //--- Array for hidden layer values
   double outputLayer[];          //--- Array for output layer values
   double inputToHiddenWeights[]; //--- Weights between input and hidden layers
   double hiddenToOutputWeights[];//--- Weights between hidden and output layers
   double hiddenLayerBiases[];    //--- Biases for hidden layer
   double outputLayerBiases[];    //--- Biases for output layer
   double outputDeltas[];         //--- Delta values for output layer
   double hiddenDeltas[];         //--- Delta values for hidden layer
   double trainingError;          //--- Current training error
   double currentLearningRate;    //--- Current learning rate
   double accuracyHistory[];      //--- History of training accuracy
   double errorHistory[];         //--- History of training errors
   int historyRecordCount;        //--- Number of recorded history entries
};

在此阶段,我们通过创建“CNeuralNetwork”来实现神经网络的核心结构。我们定义了私有成员变量,管理网络架构和训练过程,首先设置“inputNeuronCount”、“hiddenNeuronCount”和“outputNeuronCount”,分别用于确定输入层、隐藏层和输出层的神经元数量,这与策略中处理市场数据并生成交易信号的设计相契合。

通过建立数组来存储各层数值,包括存储市场指标输入的“inputLayer”、处理中间模式的“hiddenLayer”,以及生成买入/卖出预测的“outputLayer”。为了处理神经网络计算,我们创建了“inputToHiddenWeights”和“hiddenToOutputWeights”数组,存储各层之间的权重连接,同时创建了“hiddenLayerBiases”和“outputLayerBiases”数组,对隐藏层和输出层进行偏置调整。对于反向传播过程,我们定义了“outputDeltas”和“hiddenDeltas”,存储误差梯度,从而在训练过程中实现权重和偏置的更新。

此外,我们还引入了“trainingError”以跟踪当前误差,使用“currentLearningRate”管理自适应学习率,采用“accuracyHistory”和“errorHistory”数组持续监控训练表现,并通过“historyRecordCount”统计记录条目数,确保网络能够根据性能趋势动态调整其学习过程。该类为后续函数中实现前向传播、反向传播以及自适应学习率调整奠定了基础。在私有访问修饰符内,我们可以定义一个方法,将字符串输入解析为数组以供使用。

// Parse comma-separated string to array
bool ParseStringToArray(string inputString, double &output[], int expectedSize) {
   //--- Check if input string is empty
   if(inputString == "") return false;
   string values[];
   //--- Initialize array for parsed values
   ArrayResize(values, 0);
   //--- Split input string by comma
   int count = StringSplit(inputString, 44, values);
   //--- Check if string splitting failed
   if(count <= 0) {
      Print("Error: StringSplit failed for input: ", inputString, ". Error code: ", GetLastError());
      return false;
   }
   //--- Verify correct number of values
   if(count != expectedSize) {
      Print("Error: Invalid number of values in input string. Expected: ", expectedSize, ", Got: ", count);
      return false;
   }
   //--- Resize output array to expected size
   ArrayResize(output, expectedSize);
   //--- Convert string values to doubles and normalize
   for(int i = 0; i < count; i++) {
      output[i] = StringToDouble(values[i]);
      //--- Clamp values between -1.0 and 1.0
      if(MathAbs(output[i]) > 1.0) output[i] = MathMax(-1.0, MathMin(1.0, output[i]));
   }
   return true;
}

我们在“CNeuralNetwork”类中实现了“ParseStringToArray”函数,处理神经网络权重和偏置的逗号分隔字符串。首先检查“inputString”是否为空,如果无效则返回“false”,并使用StringSplit以逗号为分隔符将其拆分为“values”数组。如果拆分失败或“values”数组的元素数量“count”与预期大小“expectedSize”不匹配,则使用Print函数记录错误信息,并返回“false”。我们通过ArrayResize调整“output”数组的大小,使用StringToDouble将“values”数组中的字符串转换为双精度浮点数,再利用MathMaxMathMin函数将数值标准化为-1.0至1.0的范围内,最后在成功解析后返回“true”。其余辅助函数可在公共访问修饰符部分按如下方式声明,后续再进行定义。 

public:
   CNeuralNetwork(int inputs, int hidden, int outputs); //--- Constructor
   void InitializeWeights();                            //--- Initialize network weights
   double Sigmoid(double x);                            //--- Apply sigmoid activation function
   void ForwardPropagate();                             //--- Perform forward propagation
   void Backpropagate(const double &targets[]);         //--- Perform backpropagation
   void SetInput(double &inputs[]);                     //--- Set input values
   void GetOutput(double &outputs[]);                   //--- Retrieve output values
   double TrainOnHistoricalData(TrainingData &data[]);  //--- Train on historical data
   void UpdateNetworkWithRecentData();                  //--- Update with recent data
   void InitializeTraining();                           //--- Initialize training arrays
   void ResizeNetwork(int newHiddenNeurons);            //--- Resize network
   void AdjustLearningRate();                           //--- Adjust learning rate dynamically
   double GetRecentAccuracy();                          //--- Get recent training accuracy
   double GetRecentError();                             //--- Get recent training error
   bool ShouldRetrain();                                //--- Check if retraining is needed
   double CalculateDynamicNeurons();                    //--- Calculate dynamic neuron count
   int GetHiddenNeurons() {
      return hiddenNeuronCount;                         //--- Get current hidden neuron count
   }

在此阶段,我们定义了“CNeuralNetwork”的公共接口。我们创建了“CNeuralNetwork”构造函数,该构造函数接受“inputs”、“hidden”和“outputs”)作为参数,用于初始化网络结构;同时定义了“InitializeWeights”方法,用于配置网络的权重和偏置。实现“Sigmoid”方法以激活函数计算,以及“ForwardPropagate”和“Backpropagate”方法,分别基于目标值“targets”进行前向传播处理和反向传播更新;还实现了“SetInput”和“GetOutput”方法,处理“inputs”和“outputs”。

我们开发了“TrainOnHistoricalData”方法,该方法利用“TrainingData”结构体对历史数据进行训练;设计了“UpdateNetworkWithRecentData”方法,用于使用最新数据进行网络更新;编写了“InitializeTraining”方法,用于初始化相关数组;实现了“ResizeNetwork”方法,以便根据“newHiddenNeurons”参数调整网络隐藏层神经元数量;开发了“AdjustLearningRate”方法,用于实现动态学习率调整;还实现了“GetRecentAccuracy”、“GetRecentError”、“ShouldRetrain”、“CalculateDynamicNeurons”和“GetHiddenNeurons”等方法,用于监控并根据需要调整隐藏层神经元数量“hiddenNeuronCount”。现在,我们开始按如下方式实现并定义这些函数:

// Constructor
CNeuralNetwork::CNeuralNetwork(int inputs, int hidden, int outputs) {
   //--- Set input neuron count
   inputNeuronCount = inputs;
   //--- Set output neuron count
   outputNeuronCount = outputs;
   //--- Initialize learning rate to minimum
   currentLearningRate = MinLearningRate;
   //--- Set hidden neuron count
   hiddenNeuronCount = hidden;
   //--- Ensure hidden neurons within bounds
   if(hiddenNeuronCount < MinHiddenNeurons) hiddenNeuronCount = MinHiddenNeurons;
   if(hiddenNeuronCount > MaxHiddenNeurons) hiddenNeuronCount = MaxHiddenNeurons;
   //--- Resize input layer array
   ArrayResize(inputLayer, inputs);
   //--- Resize hidden layer array
   ArrayResize(hiddenLayer, hiddenNeuronCount);
   //--- Resize output layer array
   ArrayResize(outputLayer, outputs);
   //--- Resize input-to-hidden weights array
   ArrayResize(inputToHiddenWeights, inputs * hiddenNeuronCount);
   //--- Resize hidden-to-output weights array
   ArrayResize(hiddenToOutputWeights, hiddenNeuronCount * outputs);
   //--- Resize hidden biases array
   ArrayResize(hiddenLayerBiases, hiddenNeuronCount);
   //--- Resize output biases array
   ArrayResize(outputLayerBiases, outputs);
   //--- Resize accuracy history array
   ArrayResize(accuracyHistory, MAX_HISTORY_SIZE);
   //--- Resize error history array
   ArrayResize(errorHistory, MAX_HISTORY_SIZE);
   //--- Initialize history record count
   historyRecordCount = 0;
   //--- Initialize training error
   trainingError = 0.0;
   //--- Initialize network weights
   InitializeWeights();
   //--- Initialize training arrays
   InitializeTraining();
}

// Initialize training arrays
void CNeuralNetwork::InitializeTraining() {
   //--- Resize output deltas array
   ArrayResize(outputDeltas, outputNeuronCount);
   //--- Resize hidden deltas array
   ArrayResize(hiddenDeltas, hiddenNeuronCount);
}

// Initialize weights
void CNeuralNetwork::InitializeWeights() {
   //--- Track if weights and biases are set
   bool isInputToHiddenWeightsSet = false;
   bool isHiddenToOutputWeightsSet = false;
   bool isHiddenBiasesSet = false;
   bool isOutputBiasesSet = false;
   double tempInputToHiddenWeights[];
   double tempHiddenToOutputWeights[];
   double tempHiddenBiases[];
   double tempOutputBiases[];

   //--- Parse and set input-to-hidden weights if provided
   if(InputToHiddenWeights != "" && ParseStringToArray(InputToHiddenWeights, tempInputToHiddenWeights, inputNeuronCount * hiddenNeuronCount)) {
      //--- Copy parsed weights to main array
      ArrayCopy(inputToHiddenWeights, tempInputToHiddenWeights);
      isInputToHiddenWeightsSet = true;
      //--- Log weight initialization
      Print("Initialized input-to-hidden weights from input: ", InputToHiddenWeights);
   }
   //--- Parse and set hidden-to-output weights if provided
   if(HiddenToOutputWeights != "" && ParseStringToArray(HiddenToOutputWeights, tempHiddenToOutputWeights, hiddenNeuronCount * outputNeuronCount)) {
      //--- Copy parsed weights to main array
      ArrayCopy(hiddenToOutputWeights, tempHiddenToOutputWeights);
      isHiddenToOutputWeightsSet = true;
      //--- Log weight initialization
      Print("Initialized hidden-to-output weights from input: ", HiddenToOutputWeights);
   }
   //--- Parse and set hidden biases if provided
   if(HiddenBiases != "" && ParseStringToArray(HiddenBiases, tempHiddenBiases, hiddenNeuronCount)) {
      //--- Copy parsed biases to main array
      ArrayCopy(hiddenLayerBiases, tempHiddenBiases);
      isHiddenBiasesSet = true;
      //--- Log bias initialization
      Print("Initialized hidden biases from input: ", HiddenBiases);
   }
   //--- Parse and set output biases if provided
   if(OutputBiases != "" && ParseStringToArray(OutputBiases, tempOutputBiases, outputNeuronCount)) {
      //--- Copy parsed biases to main array
      ArrayCopy(outputLayerBiases, tempOutputBiases);
      isOutputBiasesSet = true;
      //--- Log bias initialization
      Print("Initialized output biases from input: ", OutputBiases);
   }

   //--- Initialize input-to-hidden weights randomly if not set
   if(!isInputToHiddenWeightsSet) {
      for(int i = 0; i < ArraySize(inputToHiddenWeights); i++)
         inputToHiddenWeights[i] = (MathRand() / 32767.0) * 2 - 1;
   }
   //--- Initialize hidden-to-output weights randomly if not set
   if(!isHiddenToOutputWeightsSet) {
      for(int i = 0; i < ArraySize(hiddenToOutputWeights); i++)
         hiddenToOutputWeights[i] = (MathRand() / 32767.0) * 2 - 1;
   }
   //--- Initialize hidden biases randomly if not set
   if(!isHiddenBiasesSet) {
      for(int i = 0; i < ArraySize(hiddenLayerBiases); i++)
         hiddenLayerBiases[i] = (MathRand() / 32767.0) * 2 - 1;
   }
   //--- Initialize output biases randomly if not set
   if(!isOutputBiasesSet) {
      for(int i = 0; i < ArraySize(outputLayerBiases); i++)
         outputLayerBiases[i] = (MathRand() / 32767.0) * 2 - 1;
   }
}

// Sigmoid activation function
double CNeuralNetwork::Sigmoid(double x) {
   //--- Compute and return sigmoid value
   return 1.0 / (1.0 + MathExp(-x));
}

// Set input
void CNeuralNetwork::SetInput(double &inputs[]) {
   //--- Check for input array size mismatch
   if(ArraySize(inputs) != inputNeuronCount) {
      Print("Error: Input array size mismatch. Expected: ", inputNeuronCount, ", Got: ", ArraySize(inputs));
      return;
   }
   //--- Copy inputs to input layer
   ArrayCopy(inputLayer, inputs);
}

// Forward propagation
void CNeuralNetwork::ForwardPropagate() {
   //--- Compute hidden layer values
   for(int j = 0; j < hiddenNeuronCount; j++) {
      double sum = 0;
      //--- Calculate weighted sum for hidden neuron
      for(int i = 0; i < inputNeuronCount; i++)
         sum += inputLayer[i] * inputToHiddenWeights[i * hiddenNeuronCount + j];
      //--- Apply sigmoid activation
      hiddenLayer[j] = Sigmoid(sum + hiddenLayerBiases[j]);
   }
   //--- Compute output layer values
   for(int j = 0; j < outputNeuronCount; j++) {
      double sum = 0;
      //--- Calculate weighted sum for output neuron
      for(int i = 0; i < hiddenNeuronCount; i++)
         sum += hiddenLayer[i] * hiddenToOutputWeights[i * outputNeuronCount + j];
      //--- Apply sigmoid activation
      outputLayer[j] = Sigmoid(sum + outputLayerBiases[j]);
   }
}

// Get output
void CNeuralNetwork::GetOutput(double &outputs[]) {
   //--- Resize output array
   ArrayResize(outputs, outputNeuronCount);
   //--- Copy output layer to outputs
   ArrayCopy(outputs, outputLayer);
}

// Backpropagation
void CNeuralNetwork::Backpropagate(const double &targets[]) {
   //--- Calculate output layer deltas
   for(int i = 0; i < outputNeuronCount; i++) {
      double output = outputLayer[i];
      //--- Compute delta for output neuron
      outputDeltas[i] = output * (1 - output) * (targets[i] - output);
   }
   //--- Calculate hidden layer deltas
   for(int i = 0; i < hiddenNeuronCount; i++) {
      double error = 0;
      //--- Sum weighted errors from output layer
      for(int j = 0; j < outputNeuronCount; j++)
         error += outputDeltas[j] * hiddenToOutputWeights[i * outputNeuronCount + j];
      double output = hiddenLayer[i];
      //--- Compute delta for hidden neuron
      hiddenDeltas[i] = output * (1 - output) * error;
   }
   //--- Update hidden-to-output weights
   for(int i = 0; i < hiddenNeuronCount; i++) {
      for(int j = 0; j < outputNeuronCount; j++) {
         int idx = i * outputNeuronCount + j;
         //--- Adjust weight based on learning rate and delta
         hiddenToOutputWeights[idx] += currentLearningRate * outputDeltas[j] * hiddenLayer[i];
      }
   }
   //--- Update input-to-hidden weights
   for(int i = 0; i < inputNeuronCount; i++) {
      for(int j = 0; j < hiddenNeuronCount; j++) {
         int idx = i * hiddenNeuronCount + j;
         //--- Adjust weight based on learning rate and delta
         inputToHiddenWeights[idx] += currentLearningRate * hiddenDeltas[j] * inputLayer[i];
      }
   }
   //--- Update hidden biases
   for(int i = 0; i < hiddenNeuronCount; i++)
      //--- Adjust bias based on learning rate and delta
      hiddenLayerBiases[i] += currentLearningRate * hiddenDeltas[i];
   //--- Update output biases
   for(int i = 0; i < outputNeuronCount; i++)
      //--- Adjust bias based on learning rate and delta
      outputLayerBiases[i] += currentLearningRate * outputDeltas[i];
}

// Resize network (adjust hidden neurons)
void CNeuralNetwork::ResizeNetwork(int newHiddenNeurons) {
   //--- Clamp new neuron count within bounds
   newHiddenNeurons = MathMax(MinHiddenNeurons, MathMin(newHiddenNeurons, MaxHiddenNeurons));
   //--- Check if resizing is necessary
   if(newHiddenNeurons == hiddenNeuronCount)
      return;
   //--- Log resizing information
   Print("Resizing network. New hidden neurons: ", newHiddenNeurons, ", Previous: ", hiddenNeuronCount);
   //--- Update hidden neuron count
   hiddenNeuronCount = newHiddenNeurons;
   //--- Resize hidden layer array
   ArrayResize(hiddenLayer, hiddenNeuronCount);
   //--- Resize input-to-hidden weights array
   ArrayResize(inputToHiddenWeights, inputNeuronCount * hiddenNeuronCount);
   //--- Resize hidden-to-output weights array
   ArrayResize(hiddenToOutputWeights, hiddenNeuronCount * outputNeuronCount);
   //--- Resize hidden biases array
   ArrayResize(hiddenLayerBiases, hiddenNeuronCount);
   //--- Resize hidden deltas array
   ArrayResize(hiddenDeltas, hiddenNeuronCount);
   //--- Reinitialize weights
   InitializeWeights();
}

在此阶段,我们实现神经网络的关键组件。我们新建“CNeuralNetwork”构造函数,接收“inputs”、“hidden”和“outputs”作为参数,设置“inputNeuronCount”、“outputNeuronCount”以及“hiddenNeuronCount”(需被限制在“MinHiddenNeurons”和“MaxHiddenNeurons”的范围内)。由于我们在前一部分已经对类似的类原型做过详细说明,此处无需再过多解释。

接下来,我们将“currentLearningRate”初始化为“MinLearningRate”,使用 ArrayResize调整“inputLayer”、“hiddenLayer”、“outputLayer”、“inputToHiddenWeights”、“hiddenToOutputWeights”、“hiddenLayerBiases”、“outputLayerBiases”、“accuracyHistory”以及“errorHistory”等数组的大小,并调用“InitializeWeights”和“InitializeTraining”以配置神经网络。

我们开发了“InitializeTraining”函数,用于调整“outputDeltas”和“hiddenDeltas”数组的大小,以支持反向传播过程,确保能够正确存储误差梯度。“InitializeWeights”函数负责设置权重和偏置:如果提供了如“InputToHiddenWeights”等输入参数,则使用“ParseStringToArray”函数从这些参数中加载“inputToHiddenWeights”、“hiddenToOutputWeights”、“hiddenLayerBiases”和“outputLayerBiases”;如果未提供这些参数,则使用MathRand函数将权重和偏置随机初始化为-1到1之间的值,并使用“Print”函数记录相关操作。我们实现了“Sigmoid”函数,通过MathExp函数计算给定输入“x”的σ激活值。

“SetInput”函数会先通过ArraySize验证输入数据“inputs”的大小是否与预期匹配,如果不匹配则通过“Print”函数记录错误信息;当匹配成功后,将“inputs”的数据复制到“inputLayer”数组中。我们创建了“ForwardPropagate”函数,该函数通过计算加权和并应用“Sigmoid”激活函数(结合“hiddenLayerBiases”和“outputLayerBiases”偏置),来计算“hiddenLayer”和“outputLayer”的值。“GetOutput”函数会先通过“ArrayResize”调整“outputs”数组的大小,然后使用ArrayCopy将“outputLayer”数组的值复制到“outputs”中。

我们实现了“Backpropagate”函数,该函数根据目标值“targets”计算“outputDeltas”和“hiddenDeltas”,并使用“currentLearningRate”更新“hiddenToOutputWeights”、“inputToHiddenWeights”、“hiddenLayerBiases”和“outputLayerBiases”。最后,“ResizeNetwork”函数会在合理的范围内调整“hiddenNeuronCount”,调整“hiddenLayer”和“hiddenDeltas”等数组的大小,并通过“InitializeWeights”重新初始化权重,同时使用“Print”记录相关变更。接下来,需要根据误差趋势调整学习率,因此,让我们仔细深入地探讨这一点。

// Adjust learning rate based on error trend
void CNeuralNetwork::AdjustLearningRate() {
   //--- Check if enough history exists
   if(historyRecordCount < 2) return;
   //--- Get last and previous errors
   double lastError = errorHistory[historyRecordCount - 1];
   double prevError = errorHistory[historyRecordCount - 2];
   //--- Calculate error difference
   double errorDiff = lastError - prevError;
   //--- Increase learning rate if error decreased
   if(lastError < prevError)
      currentLearningRate = MathMin(currentLearningRate * 1.05, MaxLearningRate);
   //--- Decrease learning rate if error increased significantly
   else if(lastError > prevError * 1.2)
      currentLearningRate = MathMax(currentLearningRate * 0.9, MinLearningRate);
   //--- Slightly decrease learning rate otherwise
   else
      currentLearningRate = MathMax(currentLearningRate * 0.99, MinLearningRate);
   //--- Log learning rate adjustment
   Print("Adjusted learning rate to: ", currentLearningRate, ", Last Error: ", lastError, ", Prev Error: ", prevError, ", Error Diff: ", errorDiff);
}

我们在“CNeuralNetwork”类中通过实现“AdjustLearningRate”函数来构建自适应学习率机制。首先,我们会检查“historyRecordCount”是否至少为2,以确保“errorHistory”数组中有足够的数据,如果不足则直接退出函数以避免无效调整。接下来,我们从“errorHistory”数组中获取最新的误差值作为“lastError”,以及前一次的误差值作为“prevError”,然后计算它们的差值并存储于“errorDiff”中,以此评估训练表现。

如果“lastError”小于“prevError”,我们使用MathMin函数将“currentLearningRate”提高5%,同时确保其不超过“MaxLearningRate”;如果“lastError”比“prevError”高出20%以上,则使用MathMax函数将“currentLearningRate”降低10%,同时确保其不低于“MinLearningRate”。在其他情况下,我们使用“MathMax”函数将“currentLearningRate”略微降低1%。最后,我们使用“Print”函数记录调整后的“currentLearningRate”、“lastError”、“prevError”和“errorDiff”,以实现神经网络学习过程的动态优化。随后,我们利用ATR指标动态计算隐藏层神经元的数量,以便根据市场波动性进行调整。

// Calculate dynamic number of hidden neurons based on ATR
double CNeuralNetwork::CalculateDynamicNeurons() {
   double atrValues[];
   //--- Set ATR array as series
   ArraySetAsSeries(atrValues, true);
   //--- Copy ATR buffer
   if(CopyBuffer(atrIndicatorHandle, 0, 0, 10, atrValues) < 10) {
      Print("Error: Failed to copy ATR for dynamic neurons. Using default: ", hiddenNeuronCount);
      return hiddenNeuronCount;
   }
   //--- Calculate average ATR
   double avgATR = 0;
   for(int i = 0; i < 10; i++)
      avgATR += atrValues[i];
   avgATR /= 10;
   //--- Get current close price
   double closePrice = iClose(_Symbol, PERIOD_CURRENT, 0);
   //--- Check for valid close price
   if(MathAbs(closePrice) < 0.000001) {
      Print("Error: Invalid close price for ATR ratio. Using default: ", hiddenNeuronCount);
      return hiddenNeuronCount;
   }
   //--- Calculate ATR ratio
   double atrRatio = atrValues[0] / closePrice;
   //--- Compute new neuron count
   int newNeurons = MinHiddenNeurons + (int)((MaxHiddenNeurons - MinHiddenNeurons) * MathMin(atrRatio * 100, 1.0));
   //--- Return clamped neuron count
   return MathMax(MinHiddenNeurons, MathMin(newNeurons, MaxHiddenNeurons));
}

为了根据市场波动性动态调整隐藏层神经元的数量,我们声明一个名为“atrValues”的数组,并使用ArraySetAsSeries函数将其配置为时间序列数组,随后利用CopyBuffer函数从“atrIndicatorHandle”(中获取最近10根K线的ATR数据,存入“atrValues”数组。如果“CopyBuffer”函数获取到的数据少于10个值,我们使用“Print”函数记录错误信息,并返回默认的“hiddenNeuronCount”。将数组设置为时间序列至关重要,这样最新获取的数据会映射到数组的初始索引位置(即数组开头),从而确保我们能够优先使用最新数据进行计算。以下是相关示例说明:

K线数据时间序列

我们通过将“atrValues”数组中最近10根K线的ATR值求和并除以10,计算ATR均值,并将结果存储在“avgATR”变量中。接着,我们使用iClose函数获取当前品种_Symbol在当前周期“PERIOD_CURRENT”的收盘价,并将其存储在“closePrice”变量中。如果“closePrice”接近0,我们使用“Print”函数记录错误信息,并返回当前的“hiddenNeuronCount”。然后,我们计算ATR比率,即用最新的ATR值“atrValues[0]”除以“closePrice”,再通过缩放该比率来计算新的神经元数量“newNeurons”,缩放范围在“MinHiddenNeurons”和“MaxHiddenNeurons”之间,并使用MathMin函数确保结果不超过最大值。最后,我们结合MathMax和“MathMin”函数对计算出的神经元数量进行上下限约束,确保其处于合理范围内,并返回调整后的神经元数量,确保根据市场条件动态调整。现在,我们可以定义其余的训练和更新函数,具体如下:

// Train network on historical data
double CNeuralNetwork::TrainOnHistoricalData(TrainingData &data[]) {
   const int maxEpochs = 100; //--- Maximum training epochs
   const double targetError = 0.01; //--- Target error threshold
   double accuracy = 0; //--- Training accuracy
   //--- Reset learning rate
   currentLearningRate = MinLearningRate;
   //--- Iterate through epochs
   for(int epoch = 0; epoch < maxEpochs; epoch++) {
      double totalError = 0; //--- Total error for epoch
      int correctPredictions = 0; //--- Count of correct predictions
      //--- Process each training sample
      for(int i = 0; i < ArraySize(data); i++) {
         //--- Check target array size
         if(ArraySize(data[i].targetValues) != outputNeuronCount) {
            Print("Error: Mismatch in targets size for training data at index ", i);
            continue;
         }
         //--- Set input values
         SetInput(data[i].inputValues);
         //--- Perform forward propagation
         ForwardPropagate();
         double error = 0;
         //--- Calculate error
         for(int j = 0; j < outputNeuronCount; j++)
            error += MathPow(data[i].targetValues[j] - outputLayer[j], 2);
         totalError += error;
         //--- Check prediction correctness
         if((outputLayer[0] > outputLayer[1] && data[i].targetValues[0] > data[i].targetValues[1]) ||
               (outputLayer[0] < outputLayer[1] && data[i].targetValues[0] < data[i].targetValues[1]))
            correctPredictions++;
         //--- Perform backpropagation
         Backpropagate(data[i].targetValues);
      }
      //--- Calculate accuracy
      accuracy = (double)correctPredictions / ArraySize(data);
      //--- Update training error
      trainingError = totalError / ArraySize(data);
      //--- Update history
      if(historyRecordCount < MAX_HISTORY_SIZE) {
         accuracyHistory[historyRecordCount] = accuracy;
         errorHistory[historyRecordCount] = trainingError;
         historyRecordCount++;
      } else {
         //--- Shift history arrays
         for(int i = 1; i < MAX_HISTORY_SIZE; i++) {
            accuracyHistory[i - 1] = accuracyHistory[i];
            errorHistory[i - 1] = errorHistory[i];
         }
         //--- Add new values
         accuracyHistory[MAX_HISTORY_SIZE - 1] = accuracy;
         errorHistory[MAX_HISTORY_SIZE - 1] = trainingError;
      }
      //--- Log error history update
      Print("Error history updated: ", errorHistory[historyRecordCount - 1]);
      //--- Adjust learning rate
      AdjustLearningRate();
      //--- Log progress every 10 epochs
      if(epoch % 10 == 0)
         Print("Epoch ", epoch, ": Error = ", trainingError, ", Accuracy = ", accuracy);
      //--- Check for early stopping
      if(trainingError < targetError && accuracy >= MinPredictionAccuracy)
         break;
   }
   //--- Return final accuracy
   return accuracy;
}

// Update network with recent data
void CNeuralNetwork::UpdateNetworkWithRecentData() {
   const int recentBarCount = 10; //--- Number of recent bars to process
   TrainingData recentData[];
   //--- Collect recent training data
   if(!CollectTrainingData(recentData, recentBarCount))
      return;
   //--- Process each recent data sample
   for(int i = 0; i < ArraySize(recentData); i++) {
      //--- Set input values
      SetInput(recentData[i].inputValues);
      //--- Perform forward propagation
      ForwardPropagate();
      //--- Perform backpropagation
      Backpropagate(recentData[i].targetValues);
   }
}

// Get recent accuracy
double CNeuralNetwork::GetRecentAccuracy() {
   //--- Check if history exists
   if(historyRecordCount == 0) return 0.0;
   //--- Return most recent accuracy
   return accuracyHistory[historyRecordCount - 1];
}

// Get recent error
double CNeuralNetwork::GetRecentError() {
   //--- Check if history exists
   if(historyRecordCount == 0) return 0.0;
   //--- Return most recent error
   return errorHistory[historyRecordCount - 1];
}

// Check if retraining is needed
bool CNeuralNetwork::ShouldRetrain() {
   //--- Check if enough history exists
   if(historyRecordCount < 2) return false;
   //--- Get recent metrics
   double recentAccuracy = GetRecentAccuracy();
   double recentError = GetRecentError();
   double prevError = errorHistory[historyRecordCount - 2];
   //--- Determine if retraining is needed
   return (recentAccuracy < MinPredictionAccuracy || recentError > prevError * 1.5);
}

在此阶段,我们实现了神经网络关键训练与更新机制。我们创建了"TrainOnHistoricalData"函数,通过"TrainingData"结构体数组训练网络,设置最大训练轮次"maxEpochs"为100,目标误差"targetError"为0.01,并将"currentLearningRate"重置为"MinLearningRate"。在每轮训练中,我们遍历数据集"data",验证"data[i].targetValues"大小是否与输出层神经元数量匹配,并使用"SetInput"函数加载输入数据"data[i].inputValues",通过"ForwardPropagate"函数计算预测结果。我们使用MathPow计算输出层"outputLayer"与目标值"data[i].targetValues"的误差,统计正确预测数量"correctPredictions",并且利用"Backpropagate"函数进行反向传播。

我们更新准确率"accuracy"和训练误差"trainingError",使用"historyRecordCount"将历史数据存储到"accuracyHistory"和"errorHistory"中,当数组满时自动移位,通过"Print"函数记录更新日志,并且使用"AdjustLearningRate"函数优化学习率,如果"trainingError"和"accuracy"达到"targetError"和"MinPredictionAccuracy",则提前终止训练。

我们开发了"UpdateNetworkWithRecentData"函数,设置最近K线数量"recentBarCount"为10,使用"CollectTrainingData"函数填充"recentData",遍历每个样本执行"SetInput"、"ForwardPropagate"和"Backpropagate"操作。"GetRecentAccuracy"函数返回最新的"accuracyHistory"值(如果记录数为0则返回0.0),"GetRecentError"函数对"errorHistory"执行相同操作。

我们构建了"ShouldRetrain"函数,检查"historyRecordCount"是否至少为2,通过"GetRecentAccuracy"和"GetRecentError"获取最新准确率"recentAccuracy"和误差"recentError",将当前值与"MinPredictionAccuracy"和1.5倍的前次误差值"errorHistory"比较,返回是否需要重新训练的布尔值。现在我们可以创建实际使用的类实例:

// Global neural network instance
CNeuralNetwork *neuralNetwork; //--- Global neural network object

通过创建一个指向"CNeuralNetwork"类实例的"neuralNetwork"指针,我们建立了神经网络的全局作用域,该全局对象将实现神经网络功能的集中访问,包括训练、前向传播、反向传播和自适应学习率调整,确保与EA操作的无缝集成。

通过定义全局"neuralNetwork",以便在初始化、行情处理和反初始化函数中使用,从而管理策略的预测和交易能力。我们可以进一步定义模块化函数来初始化输入和收集训练数据:

// Prepare inputs from market data
void PrepareInputs(double &inputs[]) {
   //--- Resize inputs array if necessary
   if(ArraySize(inputs) != INPUT_NEURON_COUNT)
      ArrayResize(inputs, INPUT_NEURON_COUNT);
   double ma20Values[], ma50Values[], rsiValues[], atrValues[];
   //--- Set arrays as series
   ArraySetAsSeries(ma20Values, true);
   ArraySetAsSeries(ma50Values, true);
   ArraySetAsSeries(rsiValues, true);
   ArraySetAsSeries(atrValues, true);
   //--- Copy MA20 buffer
   if(CopyBuffer(ma20IndicatorHandle, 0, 0, 2, ma20Values) <= 0) {
      Print("Error: Failed to copy MA20 buffer. Error code: ", GetLastError());
      return;
   }
   //--- Copy MA50 buffer
   if(CopyBuffer(ma50IndicatorHandle, 0, 0, 2, ma50Values) <= 0) {
      Print("Error: Failed to copy MA50 buffer. Error code: ", GetLastError());
      return;
   }
   //--- Copy RSI buffer
   if(CopyBuffer(rsiIndicatorHandle, 0, 0, 2, rsiValues) <= 0) {
      Print("Error: Failed to copy RSI buffer. Error code: ", GetLastError());
      return;
   }
   //--- Copy ATR buffer
   if(CopyBuffer(atrIndicatorHandle, 0, 0, 2, atrValues) <= 0) {
      Print("Error: Failed to copy ATR buffer. Error code: ", GetLastError());
      return;
   }
   //--- Check array sizes
   if(ArraySize(ma20Values) < 2 || ArraySize(ma50Values) < 2 || ArraySize(rsiValues) < 2 || ArraySize(atrValues) < 2) {
      Print("Error: Insufficient data in indicator arrays");
      return;
   }
   //--- Get current market prices
   double closePrice = iClose(_Symbol, PERIOD_CURRENT, 0);
   double openPrice = iOpen(_Symbol, PERIOD_CURRENT, 0);
   double highPrice = iHigh(_Symbol, PERIOD_CURRENT, 0);
   double lowPrice = iLow(_Symbol, PERIOD_CURRENT, 0);
   //--- Calculate input features
   inputs[0] = (MathAbs(openPrice) > 0.000001) ? (closePrice - openPrice) / openPrice : 0;
   inputs[1] = (MathAbs(lowPrice) > 0.000001) ? (highPrice - lowPrice) / lowPrice : 0;
   inputs[2] = (MathAbs(ma20Values[0]) > 0.000001) ? (closePrice - ma20Values[0]) / ma20Values[0] : 0;
   inputs[3] = (MathAbs(ma50Values[0]) > 0.000001) ? (ma20Values[0] - ma50Values[0]) / ma50Values[0] : 0;
   inputs[4] = rsiValues[0] / 100.0;
   double highLowRange = highPrice - lowPrice;
   if(MathAbs(highLowRange) > 0.000001) {
      inputs[5] = (closePrice - lowPrice) / highLowRange;
      inputs[7] = MathAbs(closePrice - openPrice) / highLowRange;
      inputs[8] = (highPrice - closePrice) / highLowRange;
      inputs[9] = (closePrice - lowPrice) / highLowRange;
   } else {
      inputs[5] = 0;
      inputs[7] = 0;
      inputs[8] = 0;
      inputs[9] = 0;
   }
   inputs[6] = (MathAbs(closePrice) > 0.000001) ? atrValues[0] / closePrice : 0;
   //--- Log input preparation
   Print("Prepared inputs. Size: ", ArraySize(inputs));
}

// Collect training data
bool CollectTrainingData(TrainingData &data[], int barCount) {
   //--- Check output neuron count
   if(OUTPUT_NEURON_COUNT != 2) {
      Print("Error: OUTPUT_NEURON_COUNT must be 2 for binary classification.");
      return false;
   }
   //--- Resize data array
   ArrayResize(data, barCount);
   double ma20Values[], ma50Values[], rsiValues[], atrValues[];
   //--- Set arrays as series
   ArraySetAsSeries(ma20Values, true);
   ArraySetAsSeries(ma50Values, true);
   ArraySetAsSeries(rsiValues, true);
   ArraySetAsSeries(atrValues, true);
   //--- Copy MA20 buffer
   if(CopyBuffer(ma20IndicatorHandle, 0, 0, barCount + 1, ma20Values) < barCount + 1) {
      Print("Error: Failed to copy MA20 buffer for training. Error code: ", GetLastError());
      return false;
   }
   //--- Copy MA50 buffer
   if(CopyBuffer(ma50IndicatorHandle, 0, 0, barCount + 1, ma50Values) < barCount + 1) {
      Print("Error: Failed to copy MA50 buffer for training. Error code: ", GetLastError());
      return false;
   }
   //--- Copy RSI buffer
   if(CopyBuffer(rsiIndicatorHandle, 0, 0, barCount + 1, rsiValues) < barCount + 1) {
      Print("Error: Failed to copy RSI buffer for training. Error code: ", GetLastError());
      return false;
   }
   //--- Copy ATR buffer
   if(CopyBuffer(atrIndicatorHandle, 0, 0, barCount + 1, atrValues) < barCount + 1) {
      Print("Error: Failed to copy ATR buffer for training. Error code: ", GetLastError());
      return false;
   }
   MqlRates priceData[];
   //--- Set rates array as series
   ArraySetAsSeries(priceData, true);
   //--- Copy price data
   if(CopyRates(_Symbol, PERIOD_CURRENT, 0, barCount + 1, priceData) < barCount + 1) {
      Print("Error: Failed to copy rates for training. Error code: ", GetLastError());
      return false;
   }
   //--- Process each bar
   for(int i = 0; i < barCount; i++) {
      //--- Resize input and target arrays
      ArrayResize(data[i].inputValues, INPUT_NEURON_COUNT);
      ArrayResize(data[i].targetValues, OUTPUT_NEURON_COUNT);
      //--- Get price data
      double closePrice = priceData[i].close;
      double openPrice = priceData[i].open;
      double highPrice = priceData[i].high;
      double lowPrice = priceData[i].low;
      double highLowRange = highPrice - lowPrice;
      //--- Calculate input features
      data[i].inputValues[0] = (MathAbs(openPrice) > 0.000001) ? (closePrice - openPrice) / openPrice : 0;
      data[i].inputValues[1] = (MathAbs(lowPrice) > 0.000001) ? (highPrice - lowPrice) / lowPrice : 0;
      data[i].inputValues[2] = (MathAbs(ma20Values[i]) > 0.000001) ? (closePrice - ma20Values[i]) / ma20Values[i] : 0;
      data[i].inputValues[3] = (MathAbs(ma50Values[i]) > 0.000001) ? (ma20Values[i] - ma50Values[i]) / ma50Values[i] : 0;
      data[i].inputValues[4] = rsiValues[i] / 100.0;
      if(MathAbs(highLowRange) > 0.000001) {
         data[i].inputValues[5] = (closePrice - lowPrice) / highLowRange;
         data[i].inputValues[7] = MathAbs(closePrice - openPrice) / highLowRange;
         data[i].inputValues[8] = (highPrice - closePrice) / highLowRange;
         data[i].inputValues[9] = (closePrice - lowPrice) / highLowRange;
      }
      data[i].inputValues[6] = (MathAbs(closePrice) > 0.000001) ? atrValues[i] / closePrice : 0;
      //--- Set target values based on price movement
      if(i < barCount - 1) {
         double futureClose = priceData[i + 1].close;
         double priceChange = futureClose - closePrice;
         if(priceChange > 0) {
            data[i].targetValues[0] = 1;
            data[i].targetValues[1] = 0;
         } else {
            data[i].targetValues[0] = 0;
            data[i].targetValues[1] = 1;
         }
      } else {
         data[i].targetValues[0] = 0;
         data[i].targetValues[1] = 0;
      }
   }
   //--- Return success
   return true;
}

在此阶段,我们通过构建“PrepareInputs”函数来实现数据预处理,为神经网络生成输入特征。我们使用ArrayResize函数调整"inputs"数组大小,确保其与预定义的"INPUT_NEURON_COUNT"匹配,接着通过ArraySetAsSeries函数将声明的"ma20Values"、"ma50Values"、"rsiValues"和"atrValues"数组设置为时间序列格式。我们使用CopyBuffer函数从指标句柄"ma20IndicatorHandle"、"ma50IndicatorHandle"、"rsiIndicatorHandle"和"atrIndicatorHandle"中提取两根K线数据,如果失败则用"Print"函数记录错误并退出。

我们依据ArraySize验证数组大小,如果不足则通过"Print"记录错误,并使用iClose、"iOpen"、"iHigh"和"iLow"获取当前品种_SymbolPERIOD_CURRENT时间周期下的收盘价、开盘价、最高价和最低价,分别存入"closePrice"、"openPrice"、"highPrice"和"lowPrice"中。我们为"inputs"计算输入特征,其中包括:标准化的价格差值、MA偏差、缩放至0到1范围内的RSI值、相对于"closePrice"的ATR值(通过MathAbs检查避免除0错误)和基于"highLowRange"的区间特征。成功完成后,通过"Print"和"ArraySize"记录日志。

此外,我们创建"CollectTrainingData"函数,为"barCount"根K线准备"TrainingData"结构体数组的训练数据。我们验证"OUTPUT_NEURON_COUNT"是否为2,使用"ArrayResize"调整"data"数组大小,通过"CopyBuffer"从指标句柄获取数据,并通过"CopyRates"获取价格数据存入"priceData"中,失败时用"Print"记录错误。对于每根K线,使用"ArrayResize"设置"data[i].inputValues"和"data[i].targetValues"大小,采用与"PrepareInputs"类似的方式计算输入特征,并且根据"priceData"中的价格走势设置"data[i].targetValues"的目标值,成功时返回"true"。接下来,我们需要基于接收到的数据训练网络。

// Train the neural network
bool TrainNetwork() {
   //--- Log training start
   Print("Starting neural network training...");
   TrainingData trainingData[];
   //--- Collect training data
   if(!CollectTrainingData(trainingData, TrainingBarCount)) {
      Print("Failed to collect training data");
      return false;
   }
   //--- Train network
   double accuracy = neuralNetwork.TrainOnHistoricalData(trainingData);
   //--- Log training completion
   Print("Training completed. Final accuracy: ", accuracy);
   //--- Return training success
   return (accuracy >= MinPredictionAccuracy);
}

我们创建"TrainNetwork"函数,并通过"Print"函数记录训练开始日志,随后声明"TrainingData"结构体数组"trainingData",存储输入值和目标值。我们调用"CollectTrainingData"函数,利用"TrainingBarCount"根K线的数据填充"trainingData"。如果数据收集失败,则通过"Print"记录错误并返回"false"。

我们使用神经网络对象"neuralNetwork"的"TrainOnHistoricalData"函数训练网络,将结果存储于"accuracy"中,通过"Print"记录完成日志(包含"accuracy"值)。如果"accuracy"达到或超过"MinPredictionAccuracy",则返回"true",确保网络已充分训练以便用于交易。最后,我们可以按照如下方式创建函数来验证交易信号:

// Validate Stop Loss and Take Profit levels
bool CheckStopLossTakeprofit(ENUM_ORDER_TYPE orderType, double price, double stopLoss, double takeProfit) {
   //--- Get minimum stop level
   double stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
   //--- Validate buy order
   if(orderType == ORDER_TYPE_BUY) {
      //--- Check stop loss distance
      if(MathAbs(price - stopLoss) < stopLevel) {
         Print("Buy Stop Loss too close. Minimum distance: ", stopLevel);
         return false;
      }
      //--- Check take profit distance
      if(MathAbs(takeProfit - price) < stopLevel) {
         Print("Buy Take Profit too close. Minimum distance: ", stopLevel);
         return false;
      }
   }
   //--- Validate sell order
   else if(orderType == ORDER_TYPE_SELL) {
      //--- Check stop loss distance
      if(MathAbs(stopLoss - price) < stopLevel) {
         Print("Sell Stop Loss too close. Minimum distance: ", stopLevel);
         return false;
      }
      //--- Check take profit distance
      if(MathAbs(price - takeProfit) < stopLevel) {
         Print("Sell Take Profit too close. Minimum distance: ", stopLevel);
         return false;
      }
   }
   //--- Return validation success
   return true;
}

在此阶段,我们仅创建一个布尔型函数来验证交易点位,以避免因经纪商限制导致的潜在错误。当前,我们可以在OnInit事件处理器中初始化程序,具体包括初始化技术指标和神经网络类的实例,以完成核心计算任务,代码如下:

// Expert initialization function
int OnInit() {
   //--- Initialize 20-period MA indicator
   ma20IndicatorHandle = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_SMA, PRICE_CLOSE);
   //--- Initialize 50-period MA indicator
   ma50IndicatorHandle = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_SMA, PRICE_CLOSE);
   //--- Initialize RSI indicator
   rsiIndicatorHandle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
   //--- Initialize ATR indicator
   atrIndicatorHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
   //--- Check MA20 handle
   if(ma20IndicatorHandle == INVALID_HANDLE)
      Print("Error: Failed to initialize MA20 handle. Error code: ", GetLastError());
   //--- Check MA50 handle
   if(ma50IndicatorHandle == INVALID_HANDLE)
      Print("Error: Failed to initialize MA50 handle. Error code: ", GetLastError());
   //--- Check RSI handle
   if(rsiIndicatorHandle == INVALID_HANDLE)
      Print("Error: Failed to initialize RSI handle. Error code: ", GetLastError());
   //--- Check ATR handle
   if(atrIndicatorHandle == INVALID_HANDLE)
      Print("Error: Failed to initialize ATR handle. Error code: ", GetLastError());
   //--- Check for any invalid handles
   if(ma20IndicatorHandle == INVALID_HANDLE || ma50IndicatorHandle == INVALID_HANDLE ||
         rsiIndicatorHandle == INVALID_HANDLE || atrIndicatorHandle == INVALID_HANDLE) {
      Print("Error initializing indicators");
      return INIT_FAILED;
   }
   //--- Create neural network instance
   neuralNetwork = new CNeuralNetwork(INPUT_NEURON_COUNT, MinHiddenNeurons, OUTPUT_NEURON_COUNT);
   //--- Check neural network creation
   if(neuralNetwork == NULL) {
      Print("Failed to create neural network");
      return INIT_FAILED;
   }
   //--- Log initialization
   Print("Initializing neural network...");
   //--- Return success
   return(INIT_SUCCEEDED);
}

我们在OnInit函数中实现初始化逻辑。使用iMA函数初始化"ma20IndicatorHandle"和"ma50IndicatorHandle",分别计算当前品种"_Symbol"在PERIOD_CURRENT时间周期下的20周期和50周期简单移动平均线(价格类型为"PRICE_CLOSE"),与此同时,使用"iRSI"和iATR函数分别设置 "rsiIndicatorHandle" 和 "atrIndicatorHandle",用于计算14周期的RSI指标和ATR指标。对每个指标句柄进行"INVALID_HANDLE"检查,如果初始化失败,通过"Print"函数记录错误信息,并且调用"GetLastError"获取详细错误代码,一旦有指标初始化失败,立即返回INIT_FAILED

我们使用"INPUT_NEURON_COUNT"、"MinHiddenNeurons"和"OUTPUT_NEURON_COUNT"参数,为"neuralNetwork"创建"CNeuralNetwork"类的新实例以配置网络结构。如果"neuralNetwork"实例创建失败(为"NULL"),则通过"Print"函数记录错误日志,并返回"INIT_FAILED"。最后,使用"Print"函数记录初始化成功信息,并返回INIT_SUCCEEDED,确保EA已正确配置并可用于交易。程序运行后,输出如下:

初始化

由图可见,程序已成功完成初始化。既然我们已经创建了神经网络实例,还需要在程序卸载时将其删除以避免内存泄漏。以下是实现这一逻辑的方法:

// Expert deinitialization function
void OnDeinit(const int reason) {
   //--- Release MA20 indicator handle
   if(ma20IndicatorHandle != INVALID_HANDLE) IndicatorRelease(ma20IndicatorHandle);
   //--- Release MA50 indicator handle
   if(ma50IndicatorHandle != INVALID_HANDLE) IndicatorRelease(ma50IndicatorHandle);
   //--- Release RSI indicator handle
   if(rsiIndicatorHandle != INVALID_HANDLE) IndicatorRelease(rsiIndicatorHandle);
   //--- Release ATR indicator handle
   if(atrIndicatorHandle != INVALID_HANDLE) IndicatorRelease(atrIndicatorHandle);
   //--- Delete neural network instance
   if(neuralNetwork != NULL) delete neuralNetwork;
   //--- Log deinitialization
   Print("Expert Advisor deinitialized - ", EnumToString((ENUM_INIT_RETCODE)reason));
}

我们在OnDeinit函数中实现资源清理,如果"ma20IndicatorHandle"、"ma50IndicatorHandle"、"rsiIndicatorHandle"和"atrIndicatorHandle"指标句柄其中之一为"INVALID_HANDLE",则使用IndicatorRelease函数释放资源,确保正确释放MA、RSI和ATR指标。我们检查"neuralNetwork"是否为非"NULL",如果存在则使用"delete"操作符释放"CNeuralNetwork"类实例,完成神经网络内存清理。最后通过"Print"函数结合EnumToString记录卸载原因,确认EA所有资源已正确释放。未删除类实例将导致内存泄漏,示例如下:

内存泄漏

在解决内存泄漏问题后,我们可以在OnTick事件处理器中实现核心的数据采集、模型训练及实时交易逻辑。

// Expert tick function
void OnTick() {
   static datetime lastBarTime = 0; //--- Track last processed bar time
   //--- Get current bar time
   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
   //--- Skip if same bar
   if(lastBarTime == currentBarTime)
      return;
   //--- Update last bar time
   lastBarTime = currentBarTime;

   //--- Calculate dynamic neuron count
   int newNeuronCount = (int)neuralNetwork.CalculateDynamicNeurons();
   //--- Resize network if necessary
   if(newNeuronCount != neuralNetwork.GetHiddenNeurons())
      neuralNetwork.ResizeNetwork(newNeuronCount);

   //--- Check if retraining is needed
   if(TimeCurrent() - iTime(_Symbol, PERIOD_CURRENT, TrainingBarCount) >= 12 * 3600 || neuralNetwork.ShouldRetrain()) {
      //--- Log training start
      Print("Starting network training...");
      //--- Train network
      if(!TrainNetwork()) {
         Print("Training failed or insufficient accuracy");
         return;
      }
   }

   //--- Update network with recent data
   neuralNetwork.UpdateNetworkWithRecentData();

   //--- Check for open positions
   if(PositionsTotal() > 0) {
      //--- Iterate through positions
      for(int i = PositionsTotal() - 1; i >= 0; i--) {
         //--- Skip if position is for current symbol
         if(PositionGetSymbol(i) == _Symbol)
            return;
      }
   }

   //--- Prepare input data
   double currentInputs[];
   ArrayResize(currentInputs, INPUT_NEURON_COUNT);
   PrepareInputs(currentInputs);
   //--- Verify input array size
   if(ArraySize(currentInputs) != INPUT_NEURON_COUNT) {
      Print("Error: Inputs array not properly initialized. Size: ", ArraySize(currentInputs));
      return;
   }
   //--- Set network inputs
   neuralNetwork.SetInput(currentInputs);
   //--- Perform forward propagation
   neuralNetwork.ForwardPropagate();
   double outputValues[];
   //--- Resize output array
   ArrayResize(outputValues, OUTPUT_NEURON_COUNT);
   //--- Get network outputs
   neuralNetwork.GetOutput(outputValues);
   //--- Verify output array size
   if(ArraySize(outputValues) != OUTPUT_NEURON_COUNT) {
      Print("Error: Outputs array not properly initialized. Size: ", ArraySize(outputValues));
      return;
   }

   //--- Get market prices
   double askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   //--- Calculate stop loss and take profit levels
   double buyStopLoss = NormalizeDouble(askPrice - StopLossPoints * _Point, _Digits);
   double buyTakeProfit = NormalizeDouble(askPrice + TakeProfitPoints * _Point, _Digits);
   double sellStopLoss = NormalizeDouble(bidPrice + StopLossPoints * _Point, _Digits);
   double sellTakeProfit = NormalizeDouble(bidPrice - TakeProfitPoints * _Point, _Digits);

   //--- Validate stop loss and take profit
   if(!CheckStopLossTakeprofit(ORDER_TYPE_BUY, askPrice, buyStopLoss, buyTakeProfit) ||
         !CheckStopLossTakeprofit(ORDER_TYPE_SELL, bidPrice, sellStopLoss, sellTakeProfit)) {
      return;
   }

   // Trading logic
   const double CONFIDENCE_THRESHOLD = 0.8; //--- Confidence threshold for trading
   //--- Check for buy signal
   if(outputValues[0] > CONFIDENCE_THRESHOLD && outputValues[1] < (1 - CONFIDENCE_THRESHOLD)) {
      //--- Set trade magic number
      tradeObject.SetExpertMagicNumber(123456);
      //--- Place buy order
      if(tradeObject.Buy(LotSize, _Symbol, askPrice, buyStopLoss, buyTakeProfit, "Neural Buy")) {
         //--- Log successful buy order
         Print("Buy order placed - Signal Strength: ", outputValues[0]);
      } else {
         //--- Log buy order failure
         Print("Buy order failed. Error: ", GetLastError());
      }
   }
   //--- Check for sell signal
   else if(outputValues[0] < (1 - CONFIDENCE_THRESHOLD) && outputValues[1] > CONFIDENCE_THRESHOLD) {
      //--- Set trade magic number
      tradeObject.SetExpertMagicNumber(123456);
      //--- Place sell order
      if(tradeObject.Sell(LotSize, _Symbol, bidPrice, sellStopLoss, sellTakeProfit, "Neural Sell")) {
         //--- Log successful sell order
         Print("Sell order placed - Signal Strength: ", outputValues[1]);
      } else {
         //--- Log sell order failure
         Print("Sell order failed. Error: ", GetLastError());
      }
   }
}
//+------------------------------------------------------------------+

在此阶段,我们在OnTick函数中实现神经网络策略的核心交易逻辑。我们声明变量"lastBarTime"记录上次处理的K线时间,通过iTime函数获取"_Symbol"和PERIOD_CURRENT的当前K线时间"currentBarTime",如果时间无变化则直接退出,确保每根新K线仅处理一次。我们更新"lastBarTime"为当前K线时间,使用"CalculateDynamicNeurons"函数计算最优神经元数量"newNeuronCount",如果"newNeuronCount"与通过 "GetHiddenNeurons"函数获取的当前隐藏层神经元数不一致,则调用"ResizeNetwork"函数调整网络结构。

我们通过比较TimeCurrent减去"TrainingBarCount"根K线对应的时间(通过"iTime"获取)是否超过12小时,或使用"ShouldRetrain"函数来判断是否需要重新训练,然后调用"TrainNetwork"函数进行再训练,如果失败则退出。我们调用"UpdateNetworkWithRecentData"函数来优化网络。如果PositionsTotal显示有持仓,则通过"PositionGetSymbol"检查是否包含当前品种_Symbol,如果有则跳过交易。我们声明动态数组"currentInputs",通过"ArrayResize"调整大小为"INPUT_NEURON_COUNT",然后调用"PrepareInputs"函数填充数据,并通过ArraySize验证尺寸,失败时用"Print"记录错误信息。

我们使用"SetInput"函数加载"currentInputs",调用"ForwardPropagate"函数生成预测,并使用"GetOutput"函数获取"outputValues"(事先通过"ArrayResize"调整为"OUTPUT_NEURON_COUNT"大小),如果无效则用"Print"记录错误。我们通过SymbolInfoDouble获取"askPrice"和"bidPrice",使用NormalizeDouble配合"StopLossPoints"、"TakeProfitPoints"、"_Point"和"_Digits"计算"buyStopLoss"、"buyTakeProfit"、"sellStopLoss"和"sellTakeProfit",并利用"CheckStopLossTakeprofit"函数验证。

交易方面,我们将"CONFIDENCE_THRESHOLD"设置为0.8,如果"outputValues[0]"超过该阈值且"outputValues[1]"低于其补数,我们会在使用"SetExpertMagicNumber"设置magic数字后,调用"tradeObject.Buy"函数执行买入,并通过"Print"和"GetLastError"记录成功或失败信息。同理,对于卖出信号使用"tradeObject.Sell"函数,确保交易执行稳健可靠。编译后,我们得到以下输出。

最终输出结果

由图可见,我们训练神经网络以获取误差,在每个训练周期内传播误差,然后根据误差的精度调整学习率。接下来需完成的工作是程序回测,相关内容将在下一章节详细阐述。


测试与优化学习率调整

经过全面回测后,我们得到以下结果:

回测图:

图表

回测报告:

报告


结论

总结而言,我们已开发出一套MQL5程序,实现了基于神经网络的交易策略,并具备自适应学习率,利用"CNeuralNetwork"处理市场指标并执行交易,可动态调整学习速度和网络规模以达到最优性能。通过"TrainingData"结构体以及"AdjustLearningRate"和"TrainNetwork"等函数的模块化组件,该系统提供了灵活的框架。您可以通过微调参数或集成额外的市场指标来增强系统,以契合您的交易偏好。

免责声明:本文仅用于教学目的。交易存在重大财务风险,市场剧烈波动可能导致资金损失。在实盘操作前,务必进行充分的历史回测,并建立严格的风险控制机制。

通过利用所提出的概念和实现,您可以优化这个神经网络交易系统,或调整其架构以创建新策略,助力您在算法交易领域的探索之旅。祝您交易顺利!

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18660

附加的文件 |
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
MQL5 简介(第 19 部分):沃尔夫波浪自动检测 MQL5 简介(第 19 部分):沃尔夫波浪自动检测
本文展示了如何使用 MQL5 以编程方式识别看涨和看跌的沃尔夫波浪形态并进行交易。我们将探索如何通过编程方式识别沃尔夫波浪结构,并使用 MQL5 根据这些结构执行交易。这包括检测关键的波动点、验证形态规则,以及让 EA 根据它发现的信号采取行动。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
您应当知道的 MQL5 向导技术(第 62 部分):结合 ADX 与 CCI 形态的强化学习 TRPO 您应当知道的 MQL5 向导技术(第 62 部分):结合 ADX 与 CCI 形态的强化学习 TRPO
ADX 振荡器和 CCI 振荡器是趋势跟踪和动量指标,可在开发智能系统时配对。我们延续上一篇文章未竟的话题,实证如何得益于强化学习来实际运用训练、并更新我们已开发的模型。我们正在使用的算法尚未在本系列中涵盖,其称为可信区域政策优化。一如既往,由 MQL5 向导汇编的智能系统令我们能够更快地搭建测试模型,且可配合不同类型信号进行测试、并派发。