English Русский Español Português
preview
取引におけるニューラルネットワーク:市場異常の適応型検出(最終回)

取引におけるニューラルネットワーク:市場異常の適応型検出(最終回)

MetaTrader 5トレーディングシステム |
27 0
Dmitriy Gizlyk
Dmitriy Gizlyk

はじめに

前回の記事では、ディープラーニング技術を用いて時系列データから異常を検出するよう設計されたDADA(Adaptive Bottlenecks and Dual Adversarial Decoders)フレームワークの理論的基盤について説明しました。このツールは、特に高いボラティリティ環境において重要となる異常な市場状態の検出とデータ分析を可能にします。適応型の情報処理手法を用いることで、モデルは変化する市場ダイナミクスに柔軟に対応でき、幅広い時系列データの分析に適した汎用的なソリューションとなっています。

DADAフレームワークのアーキテクチャは、3つの主要コンポーネントをで構築されており、それぞれが異なる役割を担っています。1つ目の要素はAdaptive Bottlenecksモジュールであり、入力データに適用される圧縮レベルを動的に調整できます。この手法により、分析精度を低下させる可能性のある情報損失を最小限に抑えつつ、市場データの重要な特徴を保持することができます。固定された圧縮パラメータを持つ従来モデルとは異なり、このシステムは現在の市場状況にリアルタイムで適応します。

2つ目の主要コンポーネントは、敵対的な2つのデコーダです。1つ目のデコーダは正常な市場状態を再構成し、典型的な行動パターンをより正確に捉えることを可能にします。2つ目のデコーダは異常データに焦点を当て、正常と異常の明確な分離を実現します。この二重デコーダ構造により、偽陽性を減らし、モデル全体のロバスト性が向上します。

3つ目の重要な要素はパッチングおよびマスキング機構であり、時系列データの処理において重要な役割を果たします。これはモデルが重要なセグメントを動的に強調し、ノイズを抑制し、データ表現の品質を向上させることを可能にします。パッチングはデータを小さなセグメントに分割し、ローカルパターンをより効果的に分析できるようにします。ランダムマスキングは学習を強化し、隠された部分を再構築することを強制することで、データ内の潜在的な依存関係を学習させます。これにより、複雑な構造やパターンの検出能力が向上します。これらの技術を組み合わせることで、異常検知の精度が向上し、市場変動に対する耐性も強化されます。またマスキングは、特定の市場データセグメントへの過学習を防ぎ、汎化性能の向上にも寄与します。

DADAの主要な利点の一つはその適応性です。市場環境の変化に応じて再学習が必要となる従来アルゴリズムとは異なり、DADAはパラメータを自動的に調整することができます。これは、瞬時の意思決定が求められる高頻度取引において特に重要です。この動的適応能力により、モデルは安定したトレンドから急激なボラティリティのスパイクまで、幅広い市場シナリオにおいて効果的に動作することができます。

DADAフレームワークの模式図を以下に示します。

前回の記事の実装部分では、マルチウィンドウ畳み込み層オブジェクトであるCNeuronMultiWindowsConvOCLを実装しました。なお、このコンポーネントは、DADAアーキテクチャの元の説明から直接導かれるものではありません。しかしながら、我々の実装ではAdaptive Bottlenecksモジュール内で重要な役割を果たしており、特にデータ圧縮レベルの動的調整を可能にしています。


Adaptive Bottlenecksモジュール

次の重要なステップは、Adaptive Bottlenecksモジュールの実際の構築です。このモジュールは入力データを動的に処理するための強力なツールであり、複雑な時系列の効率的な分析と、その挙動における異常の検出を可能にします。

前述の通り、このモジュールは概念的にはMixture of Experts (MoE)モジュールに類似しており、これは以前にCNeuronMoEオブジェクトとして実装されています。どちらのアプローチも、複数の小さなモデルを並列に動作させて入力データを分析するという考え方に基づいています。システムはコンテキストに基づいて、各セグメントの処理に最も関連性の高い上位k個のミニモデルを動的に選択します。これにより、最も重要なパターンに計算資源を集中させることで、適応性と精度の両方が向上します。

Adaptive Bottlenecksの特徴的な点は、これらのミニモデルとしてオートエンコーダの集合を使用することにあります。それぞれのオートエンコーダは潜在空間において異なる圧縮レベルを持っています。これにより、モデルは変化する条件に柔軟に適応し、時系列の特性に応じてデータ表現の詳細度を調整することができます。その結果、Adaptive Bottlenecksモジュールは冗長性を効果的に削減し、ノイズを抑制し、異常パターンの検出を向上させます。

私たちのAdaptive Bottlenecksの実装は、CNeuronAdaBNオブジェクトとして構築されています。想定通り、このクラスはCNeuronMoEを継承しています。このアプローチにより、Mixture of Expertsの主要な仕組みである動的な負荷分散や適応的なエキスパートアドバイザー(EA)選択といった機能を再利用できます。これらはいずれもAdaptive Bottlenecksの概念と自然に一致します。このことは新しいオブジェクトの構造にも反映されています。

class CNeuronAdaBN   :  public CNeuronMoE
  {
public:
                     CNeuronAdaBN(void) {};
                    ~CNeuronAdaBN(void) {};
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_out, uint units_count,
                          uint &bottlenecks[], uint top_k, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronAdaBN; }
  };

ご覧のとおり、初期化メソッドのみをオーバーライドしており、新たな内部オブジェクトは追加していません。

親クラスであるCNeuronMoEには、上位k個のエキスパートを選択するゲーティング機構(cGates)と、内部モデルオブジェクトへのポインタを格納する動的配列(cExperts)が含まれています。一見すると、並列エキスパートという考え方は従来の逐次モデルと相反するように思えるかもしれません。しかし本実装では、独立したシーケンスを並列に処理可能な畳み込み層のスタックを用いることで、この課題を解決しています。これにより、それぞれが独自の学習可能パラメータを持つ小規模なMLPを複数構築し、同時に動作させられるようになります。各エキスパートは特定のサブタスクに特化して学習されます。

このアーキテクチャにより適応性が大きく向上し、各エキスパートが時系列データに含まれる複雑なパターンをより効果的に捉えられるようになります。さらに、動的な選択メカニズムによって、その時々で最も関連性の高いエキスパートのみが使用されるため、予測精度の向上にも寄与します。

class CNeuronMoE  :  public CNeuronBaseOCL
  {
protected:
   CNeuronTopKGates     cGates;
   CLayer               cExperts;
   //---
   ..........
   ..........
   ..........
  };

上位k個のエキスパートを選択する継承された仕組みは、CNeuronAdaBNモジュールの要件を完全に満たしています。そのため、この部分は変更せずにそのまま再利用します。必要なのは、継承された動的配列に対して、本タスクに適した新しいオブジェクト列を構築して格納することだけです。順伝播および逆伝播についても親クラス側で処理されるため、追加の実装は不要です。

内部オブジェクトのシーケンスは、オーバーライドするInitメソッド内で定義します。

bool CNeuronAdaBN::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                        uint window, uint window_out, uint units_count,
                        uint &bottlenecks[], uint top_k, uint variables,
                        ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * variables,
                                                                 optimization_type, batch))
      return false;

通常どおり、メソッドのパラメータにはクラスのアーキテクチャを定義する各種定数が含まれます。その中でも重要なパラメータがbottlenecks配列であり、これは生成されるオートエンコーダの潜在次元を指定します。

メソッド内部では、まずベースとなる全結合ニューラルレイヤーの初期化メソッドを呼び出します。このクラスは、ライブラリ内のすべてのニューラルレイヤーオブジェクトに共通する基底クラスとして機能します。なお、直接の親クラスについては初期化を意図的にスキップします。これは、そのエキスパートプールを初期化したくないためです。ただし、この判断により、継承されたコンポーネントはすべて手動で初期化する必要があります。

ベースインターフェースの初期化が完了した後、親クラスから継承した各コンポーネントの初期化を進めます。まず、上位k個のエキスパートを選択するモジュールを初期化します。エキスパートの総数はbottlenecks配列のサイズによって決定され、その他のパラメータは外部プログラムから渡されます。

   int index = 0;
   if(!cGates.Init(0, index, OpenCL, window, units_count * variables, bottlenecks.Size(), top_k, optimization, iBatch))
      return false;

次に、Adaptive Bottlenecksモジュールの内部オブジェクトへのポインタを一時的に格納するための動的配列とローカル変数を準備します。

   cExperts.Clear();
   cExperts.SetOpenCL(OpenCL);
   CNeuronConvOCL *conv = NULL;
   CNeuronMultiWindowsConvOCL *mwconv = NULL;
   CNeuronTransposeRCDOCL *transp = NULL;

ループを実行して、オートエンコーダ全体にわたるすべての潜在状態の合計サイズを計算します。

   uint bn_size = 0;
   for(uint i = 0; i < bottlenecks.Size(); i++)
      bn_size += bottlenecks[i];

準備が整ったところで、オートエンコーダ構成要素のシーケンス構築に進みます。

興味深いことに、最初に作成するレイヤーは標準的な畳み込み層です。Adaptive Bottlenecksのアーキテクチャの複雑さを考えると一見不自然に思えるかもしれませんが、このレイヤーは処理パイプラインにおいて重要な役割を果たします。

ポイントは、この畳み込み層のフィルタ数を、すべてのオートエンコーダの潜在次元の合計と等しく設定している点です。各オートエンコーダは同一の入力データを処理するため、圧縮の度合いは潜在次元のサイズに依存します。各フィルタは独立して動作し、出力バッファ内に1つの要素を生成します。概念的には、多数のミニモデルが入力をそれぞれ単一のスカラー値へと圧縮していると捉えることができます。これらの出力は、その後、各オートエンコーダの潜在空間に対応する任意のサイズのセグメントへとグループ化できます。唯一の制約は、要素数の総和が一定であることです。

   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, window, window, bn_size, units_count, variables, optimization, iBatch) ||
      !cExperts.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(SoftPlus);

この特性を学習プロセスで活用します。続いて、マルチウィンドウ畳み込み層を導入します。各畳み込みウィンドウは、それぞれに対応する要素グループを処理し、特定のオートエンコーダの潜在表現を形成します。その後、所望のサイズへとデータを再構成します。

   index++;
   mwconv = new CNeuronMultiWindowsConvOCL();
   if(!mwconv ||
      !mwconv.Init(0, index, OpenCL, bottlenecks, window_out, units_count, variables, optimization, iBatch) ||
      !cExperts.Add(mwconv))
     {
      delete conv;
      return false;
     }
   mwconv.SetActivationFunction(SoftPlus);

続いて、オートエンコーダのデコーダ部分にもう一つレイヤーを追加します。各オートエンコーダは、それぞれ固有の学習可能パラメータを持つ専用レイヤーを備える必要があります。一方で、マルチウィンドウ畳み込み層の出力は、[Variable, Units, Autoencoder, Dimension]という4次元テンソルとして捉えることができます。標準的な畳み込み層では、各オートエンコーダごとに異なるパラメータを割り当てることができません。そこで、この制約を回避するために、オートエンコーダ次元を先頭に移動させます。そのため、次にデータ転置レイヤーを定義します。

   transp = new CNeuronTransposeRCDOCL();
   index++;
   if(!transp ||
      !transp.Init(0, index, OpenCL, units_count * variables, bottlenecks.Size(), window_out, optimization, iBatch) ||
      !cExperts.Add(transp))
     {
      delete transp;
      return false;
     }
   transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());

その後、畳み込み層を追加し、変数数をオートエンコーダの数と同じに設定します。これにより、このレイヤーは各オートエンコーダごとに独立した重み行列を初期化するようになります。

   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, window_out, window_out, window, units_count * variables, bottlenecks.Size(),
                                                                                       optimization, iBatch) ||
      !cExperts.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(None);

最後に、元のデータレイアウトを復元するために、もう一つ転置レイヤーを追加します。

   transp = new CNeuronTransposeRCDOCL();
   index++;
   if(!transp ||
      !transp.Init(0, index, OpenCL, bottlenecks.Size(), units_count * variables, window, optimization, iBatch) ||
      !cExperts.Add(transp))
     {
      delete transp;
      return false;
     }
   transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());
//---
   return true;
  }

これによりメソッドは完成です。終了前に、呼び出し元プログラムへ成功実行を示すブール値の結果を返します。

これで、Adaptive Bottlenecksモジュールの実装アプローチに関する説明は完了です。前述の通り、順伝播および逆伝播は親クラスの継承機能によって処理されます。このオブジェクトおよびそのすべてのメソッドの完全なソースコードは、添付資料に含まれています。


モデルアーキテクチャ

今回のイテレーションでは、 DADAフレームワーク全体を単一のモノリシックなオブジェクトとしてまとめることはしませんでした。その代わりに、ライブラリ内の実績あるコンポーネントを活用し、先に開発したAdaptive Bottlenecksモジュールを統合し、学習可能なモデルとして柔軟な線形構造に組み立てています。

このアプローチにより、システムの自由度は大きく向上します。第一に、モジュール性が高まり、個々のコンポーネントをコード全体を書き換えることなく置換・最適化できます。第二に、エンコーダおよびデコーダ設計に対する制約が解消され、固定的な構造に縛られることなく、タスクに応じた最適化や実験的な設計が可能になります。

このモジュール設計により、モデル構成の記述はやや複雑になりますが、柔軟性と適応性を得るための合理的なトレードオフです。

本実験では、3つのモデルを同時に学習させます。これらは全体の分析および意思決定システムにおいて重要な役割を担います。この構成により、データ分析、変化する市場ダイナミクスへの適応、価格変動の予測、そして総合的なデータ分析に基づく戦略改善をおこなう多層的な知能システムが形成されます。

最初のモデルは環境状態エンコーダです。これはDADAフレームワークのアーキテクチャを基盤とし、通常状態デコーダを備えています。このモデルは古典的なオートエンコーダとして学習され、主な目的は潜在空間から入力データを最小誤差で再構成することです。これにより、データの最も情報量の多い圧縮表現を獲得しつつ、環境の本質的な特徴を保持できます。単なる次元削減にとどまらず、データ内に潜む依存関係を抽出し、高精度な解析モデル構築に不可欠な情報を明らかにします。

第二の主要コンポーネントはActorです。これは元のDADAアーキテクチャにおける異常デコーダを置き換えるモジュールです。本モジュールは市場環境との能動的な相互作用を担い、主に安定したトレンドの特定、反転ポイントの検出、そして取引戦略を最適化する意思決定を目的としています。

Actorは多次元入力データを処理し、繰り返しパターンや変化する市場ダイナミクスを認識し、それらの洞察に基づいて売買シグナルを生成します。これによりシステムは単なる分析ツールから脱却し、データを理解するだけでなく変化に適応し、最適なエントリーおよびエグジットポイントを特定できる適応型システムへと進化します。

しかし、優れた分析およびシグナル生成機構を備えていても、予測精度は依然として重要です。そこで第三のモデルが登場します。このモデルの主な役割は、将来の価格変動方向を最も高い確率で推定することです。このコンポーネントはActorを補完し、取引判断に対する追加のフィルタとして機能します。

3つのモデルすべてのアーキテクチャはCreateDescriptionsメソッド内で定義されます。このメソッドは3つの動的データバッファへのポインタを受け取り、それらの中にモデルのアーキテクチャ記述を構築します。

bool CreateDescriptions(CArrayObj *&encoder, CArrayObj *&actor, CArrayObj *&probability)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!probability)
     {
      probability = new CArrayObj();
      if(!probability)
         return false;
     }

メソッドの冒頭では、ポインタを検証し、必要に応じて新しいインスタンスを生成します。

まず、環境状態エンコーダのアーキテクチャを定義します。予想される通り、このモデルは現在の環境状態を表すテンソルを入力として受け取ります。生の入力データを処理するために、まず十分なサイズを持つ全結合層を導入します。

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

入力は、ターミナルから取得される生データで構成されています。当然ながら、この多変量データ表現に含まれる各変数は異なる分布に従っています。そのため、解析はより困難になります。この問題に対処するため、データスケールを統一する目的でバッチ正規化レイヤーを適用します。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

続いて、DADAフレームワークに従い、パッチ化およびマスキングを適用します。データの20%をランダムにマスクするために、Dropoutレイヤーを使用します。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDropoutOCL;
   descr.count = prev_count;
   descr.probability = 0.2f;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

元のDADAアプローチでは、パッチ化はユニットシーケンスに対して適用されます。本実装では、そのようなシーケンスを最適に処理できる条件を整えるために、データを転置します。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.window = BarDescr;
   prev_count = descr.count = HistoryBars;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

その後、一定長のセグメントを独立して処理するために畳み込み層を使用します。具体的には、2層の畳み込み層を連続して適用し、いずれも重なりのないセグメントを並列にエンコードすることで、環境モデルのエンコーダとして機能します。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.window = HistoryBars / Segments;
   prev_count = descr.count = (HistoryBars + descr.window - 1) / descr.window;
   descr.step = descr.window;
   descr.layers = BarDescr;
   descr.activation = SoftPlus;
   int prev_wout = descr.window_out = EmbeddingSize;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = EmbeddingSize;
   descr.layers = BarDescr;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

その後にAdaptive Bottlenecksモジュールを配置し、潜在次元が8の倍数となる15個の小規模オートエンコーダを構築します。各セグメントに対しては、エンコーディングに最も適した上位3つのオートエンコーダを選択します。

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronAdaBN;
   descr.window = prev_wout;
   descr.count = prev_count;
   descr.window_out = 256;
   descr.step = 3; // Top K
   descr.layers = BarDescr; // Variables
     {
      int temp[15];
      for(uint i = 0; i < temp.Size(); i++)
         temp[i] = int(i + 1) * 8;
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

セグメントの再構成には、エンコーダ構造を反転させた形で実装されたデコーダを使用します。具体的には、2層の畳み込み層を連続して適用し、エンコーダと対称的な構成で復元処理をおこないます。

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count * BarDescr;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = EmbeddingSize / 2;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count * BarDescr;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = HistoryBars / Segments;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

なお、デコーダの出力には双曲線正接(tanh)を使用します。これは意図的な設計です。バッチ正規化によりデータは平均0付近に中心化され、分散は1に近い状態になります。3シグマルールに従うと、正規分布に従う値の約68%は平均から標準偏差1の範囲内に収まります。標準偏差が1の場合、この範囲は[-1, 1]となり、これはちょうどtanh関数の出力範囲と一致します。これにより、デコーダの出力は最も確率の高い値域内に維持され、同時に外れ値が抑制されます。

再構成品質を評価するため、デコーダの出力と元データを比較します。なお、処理のために事前にデータを転置しているため、まず逆転置を適用し、

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = BarDescr;
   descr.window = HistoryBars;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

元のデータ形式へと復元します。

//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = HistoryBars * BarDescr;
   descr.layers = 1;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

残りの2つのモデルはAdaptive Bottlenecksモジュールによって生成された潜在表現を基に動作します。そのため、このモジュールのパラメータをローカル変数に格納します。

//--- Latent
   CLayerDescription *latent = encoder.At(LatentLayer);
   if(!latent)
      return false;

次に、Actorのアーキテクチャを定義します。Actorは、現在の口座残高および保有ポジションのコンテキストにおいて環境を分析し、最適な取引操作を決定します。これを実現するため、本モデルは口座状態を表すベクトルを入力として受け取ります。まず、この入力は全結合層によって処理されます。

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

データが正規化されます。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

その後、結合層を適用し、2つのデータソースを統合します。すなわち、口座状態と、エンコーダの潜在空間から得られる圧縮された環境表現です。これにより、モデルは意思決定を行う際に、金融指標と抽象化された環境特徴の両方を同時に考慮できるようになります。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = latent.count * latent.window * latent.layers;
   descr.batch = 1e4;
   descr.activation = SoftPlus;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

統合されたデータは、3層の全結合層から構成される意思決定モジュールへと入力されます。このモジュールは重要なパターンを抽出し、最終的なアクション出力を生成します。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = TANH;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = SIGMOID;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

将来の価格変動の確率を予測する第三のモデルは、最も単純なアーキテクチャを持ちます。このモデルは、環境状態の圧縮表現のみに基づいて動作します。まず、潜在入力を処理するために全結合層を適用します。

   probability.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = latent.count * latent.window * latent.layers;
   descr.activation = latent.activation;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
     {
      delete descr;
      return false;
     }

その後、3層の全結合層から構成される意思決定モジュールが続きます。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = TANH;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = TANH;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions / 3;
   descr.activation = SoftPlus;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
     {
      delete descr;
      return false;
     }

最後に、出力はSoftMax関数によって確率へと変換されます。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   prev_count = descr.count = prev_count;
   descr.step = 1;
   descr.activation = None;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

モデルの完全なアーキテクチャ記述は、添付資料に提供されています。


モデル学習

モデルアーキテクチャを定義した後、次の段階である学習に移ります。学習アルゴリズムはEA「...\DADA\Study.mq5」に実装されています。今回は3つのモデルを並列で学習させる必要があるため、EAのロジックにはいくつかの調整が必要となりました。本記事ではコード全体の詳細には踏み込まず、中心となる学習メソッドTrainに焦点を当てます。

メソッドの冒頭では、いくつかのローカル変数を宣言し、基本的な設定をおこないます。

void Train(void)
  {
//---
   vector<float> probability = vector<float>::Full(Buffer.Size(), 1.0f / Buffer.Size());
//---
   vector<float> result, target, state;
   matrix<float> fstate = matrix<float>::Zeros(1, NForecast * BarDescr);
   bool Stop = false;
//---
   uint ticks = GetTickCount();

実際の学習は、ネストされたループ構造の中で行われます。外側のループは学習バッチを反復処理します。各バッチに対して、経験リプレイバッファから系列データがランダムにサンプリングされ、さらにその軌道内における学習開始位置も併せて選択されます。

   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch)
     {
      int tr = SampleTrajectory(probability);
      int start = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast - Batch));
      if(start <= 0)
        {
         iter -= Batch;
         continue;
        }
      if(!Encoder.Clear() ||
         !Actor.Clear())
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }
      result = vector<float>::Zeros(NActions);

内側のループでは、単一バッチ内の連続した状態を用いてモデルの学習がおこなわれます。

ここで重要なのは、本記事で構築しているモデル群には再帰構造(リカレントブロック)が含まれていないという点です。一般的には、このようなモデルは経験リプレイバッファからランダムに抽出された状態を用いて学習されます。しかし本ケースでは、より「理想的に近い」軌道を用いて学習をおこないます。これらの軌道は、環境の次状態に関する利用可能な情報に基づき、本EAのアルゴリズムによって直接生成されたアクションによって構成されています。リアルタイムでのモデル学習とは異なり、リプレイバッファを用いる場合、最後のレコードを除くすべてのデータについて、環境の次状態に関する情報を保持しています。これにより、学習プロセスをより正確に誘導できるようになります。

しかし一方で、この手法には別の側面もあります。この場合、保有ポジションに関する情報は存在しません。そのためモデルには、単にポジションを開くことだけでなく、最適なエグジットポイントを探索しながらポジションを管理する能力も学習させる必要があります。そこで学習プロセス中に、小規模なトレーニングバッチを構成すると同時に、「最適な」ポジションも形成していきます。

      for(int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++)
        {
         if(!state.Assign(Buffer[tr].States[i].state) ||
            MathAbs(state).Sum() == 0 ||
            !bState.AssignArray(state))
           {
            iter -= Batch + start - i;
            break;
           }
         //---
         bTime.Clear();
         double time = (double)Buffer[tr].States[i].account[7];
         double x = time / (double)(D'2024.01.01' - D'2023.01.01');
         bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_MN1);
         bTime.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_W1);
         bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_D1);
         bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         if(bTime.GetIndex() >= 0)
            bTime.BufferWrite();
         //--- Account
         float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
         float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
         float profit = float(bState[0] / _Point * (result[0] - result[3]));
         bAccount.Clear();
         bAccount.Add(1);
         bAccount.Add((PrevEquity + profit) / PrevEquity);
         bAccount.Add(profit / PrevEquity);
         bAccount.Add(MathMax(result[0] - result[3], 0));
         bAccount.Add(MathMax(result[3] - result[0], 0));
         bAccount.Add((bAccount[3] > 0 ? profit / PrevEquity : 0));
         bAccount.Add((bAccount[4] > 0 ? profit / PrevEquity : 0));
         bAccount.Add(0);
         bAccount.AddArray(GetPointer(bTime));
         if(bAccount.GetIndex() >= 0)
            bAccount.BufferWrite();

ループ本体では、まずデータリプレイバッファから情報を抽出し、学習対象となるモデルの入力データオブジェクトを構築します。その後、環境状態エンコーダの順伝播メソッドを呼び出します。

         //--- Feed Forward
         if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

その後、他の2つのモデルについても同様のメソッドを呼び出し、環境状態エンコーダのオブジェクトへのポインタを渡して入力として与えます。

         if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, GetPointer(Encoder), LatentLayer))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }
         if(!Probability.feedForward(GetPointer(Encoder), LatentLayer, (CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

その後、「最適な取引操作」の生成処理が続きます。このロジックはマルチタスク学習に関する以前の記事からそのまま引き継がれているため、ここでは繰り返し説明しません。完全な解説については、こちらを参照してください。

ターゲット値の準備が完了した後、それらとの乖離を最小化するようにモデルパラメータを最適化します。学習は環境状態エンコーダから開始されます。ターゲットテンソルとしては、順伝播で使用したものと同じく、解析対象の環境状態を表すベクトルを渡します。

         //--- State Encoder
         if(!Encoder.backProp(GetPointer(bState), (CBufferFloat*)NULL, NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

次にActorの学習をおこない、「最適な取引操作」からの乖離を最小化するように最適化します。

         //--- Actor Policy
         if(!Actor.backProp(GetPointer(bActions), (CNet*)GetPointer(Encoder), LatentLayer)
            || !Encoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true)
            )
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

3つ目のモデルについては、次のバーの色に基づいて将来の価格変動方向を決定します。

         target = vector<float>::Zeros(NActions / 3);
         if(fstate[0, 0] > 0)
            target[0] = 1;
         else
            if(fstate[0, 0] < 0)
               target[1] = 1;
         if(!Result.AssignArray(target) ||
            !Probability.backProp(Result, (CBufferFloat*)NULL)
            || !Encoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer)
           )
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

最後に、学習の進行状況をログに記録し、ループの次のイテレーションへ進みます。

         //---
         if(GetTickCount() - ticks > 500)
           {
            double percent = double(iter + i - start) * 100.0 / (Iterations);
            string str = StringFormat("%-13s %6.2f%% -> Error %15.8f\n", "Encoder",
                                         percent, Encoder.getRecentAverageError());
            str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Actor", percent,
                                                     Actor.getRecentAverageError());
            str += StringFormat("%-13s %6.2f%% -> Error %15.8f\n", "Probability", 
                                      percent, Probability.getRecentAverageError());
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

すべての学習イテレーションが完了した後、学習結果をログへ出力し、EAのシャットダウン処理を開始します。

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Probability", Probability.getRecentAverageError());
   ExpertRemove();
//---
  }

EAの完全なソースコードは付録に含まれており、独立した学習・解析のために利用可能です。また添付ファイルには、環境とのインタラクションおよび学習済みモデルのテストをおこなうためのプログラムも含まれています。


テスト

MQL5DADAフレームワークの概念を実装し、それを学習可能なモデルへ統合した後の次の重要なステップは、実際の過去データ上での性能評価です。これにより、実運用における取引条件下での有効性を検証することができます。

学習用データセットは、2024年のEURUSDM1時間足)に対してMetaTrader 5のストラテジーテスターを用い、ランダムな実行結果を生成することで作成しました。データは標準的なインジケータ設定を使用して収集し、外的要因の影響を極力排除し、クリーンな実験環境を構築しています。

学習済みモデルは、2025年1月〜2月の過去データに対してテストを実施しました。すべての実験パラメータは学習時と同一に保たれており、Actorの学習結果を客観的に評価できるようにしています。学習に使用していないデータでのテストは、実環境に近い条件下での性能を評価するための重要な検証ステップです。

テスト結果を以下に示します。

テスト期間中、モデルは57回の取引をおこない、そのうち35%以上が利益で終了しました。勝率は比較的控えめであるものの、勝ちトレードの平均利益は負けトレードの平均損失の約3倍となっており、結果として全体としては利益を確保し、プロフィットファクターは1.53となりました。

ただし、利益の大部分は1月前半に集中しており、それ以降の期間ではエクイティカーブは狭いレンジ内で変動しています。このことから、モデルにはさらなる最適化が必要であると考えられます。

また実装過程においてDADAアーキテクチャに対するいくつかの変更が加えられています。そのため、結果はこの特定バージョンのDADAフレームワークに依存したものです。


結論

本研究では、適応型ボトルネックとデュアル並列デコーダを組み合わせることで、時系列データ解析の精度向上を目指す革新的なアプローチであるDADAフレームワークを検討しました。この手法の大きな利点は、事前調整を必要とせず、異なるデータ構造に対して動的に適応できる点にあります。

提案手法をMQL5上で独自に実装し、それを学習可能なモデルへ統合しました。これらのモデルは実際の過去データ上で学習され、テストにおいて収益性を示しました。しかしながら、エクイティカーブは安定した右肩上がりにはならず、戦略のさらなる改善および最適化が必要であることが示されています。


関連リンク


記事で使用されたプログラム

# 名前 種類 詳細
1 Research.mq5 EA データセット収集用EA
2 ResearchRealORL.mq5
EA
Real-ORL法を用いたデータセット収集用EA
3 Study.mq5 EA モデル学習用EA
4 Test.mq5 EA モデルテスト用EA
5 Trajectory.mqh クラスライブラリ システム状態とモデルアーキテクチャ記述構造
6 NeuroNet.mqh クラスライブラリ ニューラルネットワークを作成するためのクラスのライブラリ
7 NeuroNet.cl ライブラリ OpenCLプログラムコード

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/17577

添付されたファイル |
MQL5.zip (2565.75 KB)
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
リスク管理(第4回):主要クラスメソッドの完了 リスク管理(第4回):主要クラスメソッドの完了
MQL5におけるリスク管理に関する連載の第4回です。本連載では、取引戦略を保護しつつ最適化するための高度な手法を段階的に解説しています。前回までの内容で重要な基礎はすでに整っており、本記事では第3回で後回しにしていた残りの実装をすべて完結させます。具体的には、設定された利益および損失の上限に到達したかどうかを判定するための各種関数を完成させます。さらに、より高精度かつ柔軟なリスク制御を実現するための新しいイベント機構についても導入します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
市場シミュレーション(第13回):ソケット(VII) 市場シミュレーション(第13回):ソケット(VII)
xlwingsなど、Excelへの直接的な読み書きを可能にするパッケージを用いて何かを開発する場合には、すべてのプログラム、関数、または手続きは実行され、その処理を完了すると同時に終了するという点に注意する必要があります。どれだけ工夫をしても、それらを継続的なループ処理として動作させ続けることはできません。