English Deutsch
preview
MQL5での取引戦略の自動化(第21回):適応学習率によるニューラルネットワーク取引の強化

MQL5での取引戦略の自動化(第21回):適応学習率によるニューラルネットワーク取引の強化

MetaTrader 5トレーディング |
139 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第20回)では、商品チャンネル指数(CCI: Commodity Channel Index)とオーサムオシレータ(AO)を用いた多銘柄戦略を開発し、MetaQuotes Language 5 (MQL5)で複数通貨ペアにおけるトレンド反転トレードを自動化しました。堅牢なシグナル生成とリスク管理も実装済みです。第21回では、ニューラルネットワーク(英語Wiki)を活用した取引戦略に進み、適応型学習率(英語Wiki)メカニズムによって市場動向の予測精度を最適化します。本記事では以下のトピックを扱います。

  1. 適応型ニューラルネットワーク学習率戦略の理解
  2. MetaQuotes言語5(MQL5)による実装
  3. 学習率調整のテストと最適化
  4. 結論

本記事を読み終える頃には、学習率の動的調整を活用したニューラルネットワーク取引システムをMQL5で構築できるようになり、さらに改良を加える準備が整うでしょう。それでは、始めましょう。


適応型ニューラルネットワーク学習率戦略の理解

第20回では、商品チャンネル指数(CCI: Commodity Channel Index)とオーサムオシレータ(AO: Awesome Oscillator)を活用し、複数の通貨ペアにわたるトレンド反転取引を自動化できる多通貨取引システムを開発しました。第21回ではさらに進めて、ニューラルネットワークを活用した動的な取引戦略に取り組みます。ニューラルネットワークは、人間の脳内における相互接続されたニューロンの働きを模倣した計算モデルであり、多様な市場指標を処理し、市場のボラティリティに適応して学習プロセスを調整することで、価格変動の予測精度を高めることが可能です。私たちの目標は、柔軟かつ高性能なトレーディングシステムを構築し、ニューラルネットワークを用いて複雑な市場パターンを分析し、適応的学習率(英語Wiki)メカニズムによって精度を最適化した取引実行を実現することです。

ニューラルネットワークは、入力層・隠れ層・出力層というノード(ニューロン)の層構造で動作します。入力層では市場データを取り込み、隠れ層では複雑なパターンを抽出し、出力層では価格の上昇または下降といった売買シグナルを生成します。順伝播では、入力に対して重みやバイアスが適用され、層を通して変換されながら予測が得られます。以下を参照してください。

層と重みを持つニューラルネットワーク

この過程で重要な要素となるのが活性化関数です。活性化関数は変換に非線形性を導入することで、ネットワークが複雑な関係性をモデル化できるようにします。例えばシグモイド関数は出力を0から1の範囲にマッピングするため、売買のような二値分類タスクに適しています。さらにバックプロパゲーション(誤差逆伝播法)によって予測誤差から逆方向に重みやバイアスを調整し、時間をかけて精度を高めていきます。以下を参照してください。

伝播

ネットワークを動的にするために、私たちは適応型学習率戦略を実装する計画です。これは、誤差逆伝播において重みの更新速度を調整するものであり、予測が市場の結果と一致したときには学習を加速させ、誤差が急増したときには学習を減速させることで、安定性を確保します。

私たちは、移動平均やモメンタム指標といった市場インジケーターをニューラルネットワークの入力層に与え、それを隠れ層で処理してパターンを検出し、出力層で信頼性の高い売買シグナルを生成するシステムを設計する計画です。出力層のニューロンの変換にはシグモイド活性化関数を使用し、滑らかで解釈しやすい予測を得ることを目指しています。これを選んでいる理由は、出力を2つの選択肢にできるからです。また、利用可能な他の関数の例についても検討しています。

活性化関数

さらに、学習性能に基づいて学習率を動的に調整し、市場のボラティリティに合わせて隠れ層のニューロン数を適応させることで、複雑さと効率性のバランスを取った柔軟なシステムを構築します。この戦略は、堅牢な実装と徹底したテストの基盤となります。入力としては、2種類の移動平均、相対力指数(RSI: Relative Strength Index)、Average True Range (ATR)を利用し、シグナルが得られた段階でトレードを開始します。以下は、実装後に目指しているイメージの可視化です。

プランイメージ


MQL5での実装

MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲータに移動して、Indicatorsフォルダを見つけ、[新規作成]タブをクリックして、表示される手順に従ってファイルを作成します。作成が完了したら、コーディング環境では、まず使用する入力変数や構造体、クラスを宣言します。本戦略では、オブジェクト指向プログラミング(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は学習に使用する過去バーの数を指定し、MinPredictionAccuracyは精度の閾値として設定します。MinLearningRateとMaxLearningRateは適応学習率の範囲を制限するために用います。加えて、InputToHiddenWeights、HiddenToOutputWeights、HiddenBiases、OutputBiasesといった文字列形式の入力を用いて、ニューラルネットワークの重みやバイアスを初期化でき、事前設定またはデフォルト設定を利用可能にします。

次に、ニューラルネットワークの構造に関する定数を定義します。INPUT_NEURON_COUNTは市場データ入力用に10と設定し、OUTPUT_NEURON_COUNTは売買シグナルの出力として2とします。MAX_HISTORY_SIZEは学習精度や誤差を追跡するために10と設定します。また、20期間および50期間の移動平均線、RSIATRを参照するために、インジケーターハンドル「ma20IndicatorHandle」「ma50IndicatorHandle」「rsiIndicatorHandle」「atrIndicatorHandle」を作成し、これらをニューラルネットワークへの市場データ入力として利用します。最後に、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クラスを作成して、ニューラルネットワークのコア構造を実装します。ネットワークのアーキテクチャおよび学習プロセスを管理するために、privateメンバー変数を定義します。まず、入力層、隠れ層、および出力層のニューロン数を設定するためにinputNeuronCount、hiddenNeuronCount、outputNeuronCountを定義します。これにより、市場データを処理し、売買シグナルを生成する戦略の設計に沿った構成が可能になります。

層の値を格納するための配列も用意します。市場指標の入力を保持するinputLayer、中間パターンを処理するhiddenLayer、および売買予測を生成するoutputLayerです。ニューラルネットワークの計算をおこなうために、層間の重み接続を保持するinputToHiddenWeightsおよびhiddenToOutputWeights配列、隠れ層および出力層のバイアス調整用にhiddenLayerBiasesおよびoutputLayerBiasesを作成します。バックプロパゲーションを実装するために、誤差勾配を格納するoutputDeltasおよびhiddenDeltasを定義し、学習中の重みおよびバイアスの更新を可能にします。

さらに、現在の誤差を追跡するためのtrainingError、適応学習率を管理するcurrentLearningRate、学習パフォーマンスを時間経過で監視するaccuracyHistoryおよびerrorHistory配列、記録されたエントリ数を保持するhistoryRecordCountを含めています。これにより、ネットワークはパフォーマンスの傾向に応じて学習プロセスを動的に調整できます。このクラスは、順伝播、逆伝播、および適応学習率調整を実装するための基盤を形成します。また、privateアクセス修飾子内で、文字列入力を配列に変換して利用するためのメソッドを定義することも可能です。

// 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に分割します。分割に失敗した場合やcountがexpectedSizeと一致しない場合は、Print関数でエラーを記録し、falseを返します。outputをArrayResizeでリサイズし、valuesをStringToDoubleで数値に変換し、MathMaxおよびMathMinを用いて-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クラスのpublicインターフェースを定義します。CNeuralNetworkコンストラクタを作成し、inputs、hidden、outputsを用いてネットワークを構築します。また、InitializeWeightsで重みおよびバイアスを設定します。活性化関数としてSigmoidを実装し、順伝播処理にはForwardPropagate、目標値targetsに基づく更新にはBackpropagateを使用します。入力および出力を扱うために、SetInputおよびGetOutputを用意します。

さらに、TrainOnHistoricalDataで過去データを用いた学習をおこない、UpdateNetworkWithRecentDataで最新データに対応します。配列初期化用にInitializeTrainingを作成し、隠れ層のニューロン数変更にはResizeNetwork、動的学習率調整には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を設定します。hiddenNeuronCountはMinHiddenNeuronsとMaxHiddenNeuronsの範囲内に制限されます。クラスプロトタイプの詳細な説明は前段でおこなっているため、ここでは簡潔に扱います。

次に、currentLearningRateをMinLearningRateに初期化し、inputLayer、hiddenLayer、outputLayer、inputToHiddenWeights、hiddenToOutputWeights、hiddenLayerBiases、outputLayerBiases、accuracyHistory、errorHistoryなどの配列をArrayResizeでリサイズし、InitializeWeightsおよびInitializeTrainingを呼び出してネットワークを設定します。

InitializeTraining関数では、バックプロパゲーション用にoutputDeltasおよびhiddenDeltas配列をリサイズし、誤差勾配の適切な格納を確保します。InitializeWeights関数では、ParseStringToArrayを用いてInputToHiddenWeightsやHiddenToOutputWeights、HiddenLayerBiases、OutputLayerBiasesから重みとバイアスを読み込み、入力がない場合はMathRandで-1から1の範囲でランダムに初期化し、処理内容をPrint関数で記録します。Sigmoid関数は、与えられたxに対するシグモイド活性化値をMathExpを使って計算します。

SetInput関数では、inputsのサイズをArraySizeで確認し、inputLayerにコピーします。サイズが一致しない場合はPrint関数でエラーを記録します。ForwardPropagate関数は、重み付き和とSigmoid活性化関数、hiddenLayerBiasesおよびoutputLayerBiasesを加えてhiddenLayerとoutputLayerの値を計算します。GetOutput関数では、ArrayResizeでoutputsをリサイズし、ArrayCopyでoutputLayerの値をコピーします。

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配列に十分なデータがない場合は無効な調整を避けるために処理を終了します。最新の誤差をlastError、前回の誤差をprevErrorとしてerrorHistoryから取得し、その差分をerrorDiffとして計算し、学習性能を評価します。

学習率の調整には、MathMin関数を用いて、lastErrorがprevErrorより小さい場合はcurrentLearningRateを5%増加させ、上限をMaxLearningRateに制限します。一方、lastErrorがprevErrorを20%以上上回る場合はMathMax関数で10%減少させ、下限をMinLearningRate以上に維持します。それ以外の場合は、MathMaxを用いてcurrentLearningRateをわずかに1%減少させます。最後に、Print関数で調整後のcurrentLearningRate、lastError、prevError、およびerrorDiffを記録し、ニューラルネットワークの学習プロセスを動的に最適化します。さらに、ATRインジケーターを用いて隠れ層のニューロン数を動的に計算し、市場のボラティリティに応じてhiddenNeuronCountを調整できるようにします。

// 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本分のATRデータをatrValuesに取得します。CopyBufferで10本分の値が取得できなかった場合は、Print関数でエラーを記録し、デフォルト値としてhiddenNeuronCountを返します。ここで配列を時系列として設定することで、最新データが配列の先頭インデックスに配置され、最初に使用できるようになります。以下にその表現を示します。

バーデータの時系列

次に、atrValuesの直近10本分を合計して10で割り、平均ATRをavgATRとして算出します。iClose関数を使用して、_SymbolのPERIOD_CURRENTにおける最新の終値をclosePriceに取得します。closePriceがほぼゼロの場合は、Print関数でエラーを記録し、hiddenNeuronCountを返します。ATR比率をatrValues[0]をclosePriceで割ることで算出し、MinHiddenNeuronsとMaxHiddenNeuronsの範囲に基づいてスケーリングし、MathMinで上限を調整してnewNeuronsを計算します。最後に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のサイズがoutputNeuronCountと一致するかを確認します。その後、SetInputでdata[i].inputValuesを入力し、ForwardPropagateで予測値を計算します。誤差はoutputLayerとdata[i].targetValuesの差をMathPowで算出し、correctPredictionsを記録したうえで、Backpropagateを呼び出して重み更新をおこないます。

次に、accuracyとtrainingErrorを更新し、historyRecordCountを用いてaccuracyHistoryおよびerrorHistoryに格納します。配列が一杯になった場合はシフト処理をおこないます。更新内容はPrint関数で記録し、AdjustLearningRateを呼び出して学習を最適化します。trainingErrorがtargetError以下、かつaccuracyがMinPredictionAccuracyを満たした場合は、早期終了します。

「UpdateNetworkWithRecentData」関数では、recentBarCountを10に設定し、CollectTrainingData関数を使ってrecentDataを収集します。その後、各サンプルについてSetInput、ForwardPropagate、Backpropagateを順に実行し、ネットワークを最新データに基づいて調整します。GetRecentAccuracy関数は、historyRecordCountがゼロでない場合にaccuracyHistoryの最新値を返し、ゼロであれば0.0を返します。同様にGetRecentError関数は、errorHistoryの最新値を返すか、履歴が存在しない場合は0.0を返します。

ShouldRetrain関数では、historyRecordCountが少なくとも2以上であることを確認し、GetRecentAccuracyとGetRecentErrorを使って、recentAccuracyおよびrecentErrorをそれぞれMinPredictionAccuracyおよび直前のerrorHistory値の1.5倍と比較します。これらの条件を満たした場合にtrueを返し、再学習が必要であると判断します。これで、実際の実装に使用するクラスインスタンスを作成できる準備が整いました。

// 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に合わせてリサイズします。次に、ma20Values、ma50Values、rsiValues、atrValues配列を宣言し、ArraySetAsSeriesで時系列配列として設定します。その後、CopyBufferを用いて、ma20IndicatorHandle、ma50IndicatorHandle、rsiIndicatorHandle、およびatrIndicatorHandleから直近2本分のデータを取得し、取得に失敗した場合はPrint関数でエラーログを出力して処理を終了します。

配列サイズをArraySizeで確認し、不足があればPrint関数でエラーを記録します。さらに、iClose、iOpen、iHigh、iLowを用いて、_Symbol とPERIOD_CURRENTから現在の価格をclosePrice、openPrice、highPrice、lowPriceに取得します。これらを基に入力特徴量を計算します。具体的には、正規化した価格差、移動平均からの乖離、0~1にスケーリングしたRSI、closePriceに対するATR比率を算出し、ゼロ除算を避けるためにMathAbsでチェックをおこないます。また、highLowRangeを用いたレンジベースの特徴量も算出します。最後に、Print関数で成功をログに残し、ArraySizeで入力の確保状況を出力します。

さらに、CollectTrainingData関数を作成し、指定したbarCount分のデータをTrainingData構造体配列に準備します。まず、OUTPUT_NEURON_COUNTが2であることを確認し、ArrayResizeでdataをリサイズします。その後、各インジケーターハンドルからのデータをCopyBufferで取得し、価格データはCopyRatesでpriceDataに格納します。取得に失敗した場合はPrint関数でエラーログを出力します。各バーごとに、data[i].inputValuesとdata[i].targetValuesをArrayResizeで確保し、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本分のバーから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関数を用いて、_SymbolとPERIOD_CURRENTに対して20期間および50期間の単純移動平均を算出するためのma20IndicatorHandleおよびma50IndicatorHandleを初期化します。さらに、iRSI関数およびiATR関数を用いて、14期間のRSIおよびATRを算出するrsiIndicatorHandleとatrIndicatorHandleを初期化します。各ハンドルについてINVALID_HANDLEでないことを確認し、失敗した場合はPrint関数とGetLastErrorでエラーを記録し、INIT_FAILEDを返して初期化を終了します。

次に、ニューラルネットワークの構造を設定するために「CNeuralNetwork」クラスの新しいインスタンスを作成し、INPUT_NEURON_COUNT、MinHiddenNeurons、OUTPUT_NEURON_COUNTを渡してneuralNetworkを初期化します。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関数を呼び出し、移動平均、RSI、ATRインジケーターのリソースを適切に解放します。次に、neuralNetworkがNULLでない場合はdelete演算子を用いてCNeuralNetworkクラスのインスタンスを明示的に破棄し、ニューラルネットワークのリソースをクリーンアップします。最後に、Print関数とEnumToStringを組み合わせて、OnDeinitが呼ばれた理由をログ出力し、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を宣言し、iTime関数を使用し「_Symbol」とPERIOD_CURRENTのcurrentBarTimeを取得します。バーが更新されていない場合は終了し、新しいバーのみを処理します。その後、lastBarTimeを更新し、CalculateDynamicNeurons関数を用いてnewNeuronCountを計算します。newNeuronCountがGetHiddenNeurons関数の結果と異なる場合は、ResizeNetwork関数を呼び出してネットワークを調整します。

次に、再学習が必要かどうかを確認します。具体的には、TimeCurrentからTrainingBarCountの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を取得し、StopLossPoints、TakeProfitPoints、_Point、_Digitsを用いてbuyStopLoss、buyTakeProfit、sellStopLoss、sellTakeProfitをNormalizeDoubleで計算します。さらにCheckStopLossTakeprofit関数で妥当性を確認します。

取引ロジックについては、CONFIDENCE_THRESHOLDを0.8に設定します。outputValues[0]が閾値を上回り、outputValues[1]がその補数を下回る場合、tradeObject.Buy関数を呼び出して買い注文を実行します。事前にSetExpertMagicNumberでマジックナンバーを設定し、成功・失敗はPrintとGetLastErrorでログに記録します。同様に、売りシグナルの場合はtradeObject.Sell関数を使用します。これにより、堅牢な取引実行を確保しています。コンパイルすると以下の出力が得られます。

最終出力

画像から分かるように、ニューラルネットワークをトレーニングし、誤差を算出してエポック内で誤差を伝播させ、その誤差の精度に基づいて学習率を調整します。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


学習率調整のテストと最適化

徹底的なバックテストの結果、次の結果が得られました。

バックテストグラフ

グラフ

バックテストレポート

レポート


結論

本記事では、ニューラルネットワークを活用した取引戦略を実装するMQL5プログラムを開発しました。本プログラムは、CNeuralNetworkクラスを用いて市場指標を処理し、誤差に応じた学習率の動的調整やネットワークサイズの最適化を通じて取引を実行します。TrainingData構造体やAdjustLearningRate、TrainNetworkといった関数など、モジュール化されたコンポーネントを活用することで、パラメータの微調整や追加の市場指標の統合など、柔軟に機能を拡張可能なフレームワークを提供します。

免責条項:本記事は教育目的のみを意図したものです。取引には重大な財務リスクが伴い、市場の変動によって損失が生じる可能性があります。本プログラムを実際の市場で運用する前に、十分なバックテストと慎重なリスク管理が不可欠です。

本稿で紹介した概念と実装を活用することで、このニューラルネットワーク取引システムをさらに改善したり、独自の戦略に応用したりすることが可能です。アルゴリズム取引の学習や実践に役立ててください。取引をお楽しみください。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18660

添付されたファイル |
知っておくべきMQL5ウィザードのテクニック(第72回):教師あり学習でMACDとOBVのパターンを活用する 知っておくべきMQL5ウィザードのテクニック(第72回):教師あり学習でMACDとOBVのパターンを活用する
前回の記事で紹介したMACDとOBVのインジケーターペアをフォローアップし、今回はこのペアを機械学習でどのように強化できるかを見ていきます。MACDとOBVは、それぞれトレンド系と出来高系という補完的なペアです。私たちの機械学習アプローチでは、畳み込みニューラルネットワーク(CNN)を使い、カーネルとチャンネルのサイズを調整する際に指数カーネルを利用して、このインジケーターペアの予測をファインチューニングします。今回もこれまでと同様に、MQL5ウィザードでエキスパートアドバイザー(EA)を組み立てられるようにしたカスタムシグナルクラスファイル内で実装しています。
データサイエンスとML(第45回):FacebookのPROPHETモデルを用いた外国為替時系列予測 データサイエンスとML(第45回):FacebookのPROPHETモデルを用いた外国為替時系列予測
Prophetモデルは、Meta(旧Facebook)によって開発された強力な時系列予測ツールであり、トレンドや季節性、イベント効果(holiday effects)を最小限の手作業で捉えることができます。このモデルは、需要予測やビジネスプランニングにおいて広く活用されてきました。本記事では、ProphetモデルをFXのボラティリティ予測に応用する効果について探り、従来のビジネス用途を超えた利用例を紹介します。
プライスアクション分析ツールキットの開発(第30回):コモディティチャンネル指数(CCI)、Zero Line EA プライスアクション分析ツールキットの開発(第30回):コモディティチャンネル指数(CCI)、Zero Line EA
プライスアクション分析の自動化は、今後の方向性を示す重要なステップです。本記事では、デュアルCCIインジケーター、ゼロラインクロスオーバー戦略、EMA、そしてプライスアクションを組み合わせ、ATRを用いて売買シグナルを生成し、ストップロス(SL)およびテイクプロフィット(TP)を設定するツールを開発します。CCI Zero Line EAの開発手法について学ぶために、ぜひお読みください。
初心者からエキスパートへ:MQL5を使ったアニメーションニュース見出し(III)-インジケーターインサイト 初心者からエキスパートへ:MQL5を使ったアニメーションニュース見出し(III)-インジケーターインサイト
本記事では、News Headline EAをさらに進化させるために、専用の「インジケーターインサイトレーン」を導入します。これは、RSI、MACD、ストキャスティクス、CCIなどの主要インジケーターから生成されるテクニカルシグナルを、チャート上にコンパクトにまとめて表示する仕組みです。この方法により、MetaTrader 5ターミナルで複数のインジケーターウィンドウを開く必要がなくなり、作業スペースをすっきりと保つことができます。さらに、MQL5のAPIを活用してインジケーターデータをバックグラウンドで取得することで、カスタムロジックを使ったリアルタイムの市場分析や可視化が可能になります。本記事では、MQL5でインジケーターデータを操作し、チャート上の単一水平レーンに、知的で省スペースなスクロール式インサイトシステムを作成する方法を詳しく解説します。