English Русский Español Português
preview
取引におけるニューラルネットワーク:高精度分類のための効果的な特徴量抽出(最終章)

取引におけるニューラルネットワーク:高精度分類のための効果的な特徴量抽出(最終章)

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

はじめに

Mantisフレームワークの紹介も、いよいよ最終段階に入りました。これまで、まずその理論的基盤を詳しく検討しました。モデルがどのようにマルチチャネル時系列を処理するのか、なぜ差分シグナルが必要なのか、そしてどのようにローカルトークンが形成されるのかを見てきました。続いて、アーキテクチャをさらに詳しく掘り下げ、パッチがどのように構成され、モデルが市場ノイズの中から安定したパターンをどのように抽出することを学習するのかを確認しました。ここで、これらすべてを一つにまとめる段階に入ります。

Mantisフレームワークは、独立していながら論理的に関連付けられた複数のモジュールからなる一連の処理として構成されています。それぞれが、入力データの初期処理からトークン化、情報の集約まで、特定の役割を担います。その根底にある原則は、バイアスを最小限に抑えながら、データそのものから可能な限り多くのパターンを抽出することです。

すべては、生の市場特徴量がモデルに入力されるところから始まります。モデルは、それらを1つの行列として扱うのではなく、データの種類ごとに独立したチャネルへ分離します。さらに、それぞれのデータに対して1階差分のチャネルを追加します。これにより、モデルは短期的なダイナミクスを明示的に捉えられるようになります。このような差分チャネルによって、価格変動の方向の変化に対するモデルの感度が高まります。たとえば、差分チャネルにおける急激な加速は、各種インジケータがそれを捉えるよりも前に、インパルス的な値動きの始まりを示している可能性があります。

これらの各チャネルは、畳み込みブロックに入力されます。ここから初期分析が始まります。畳み込みによって局所的な特徴量が抽出され、安定したマイクロパターンを識別することを学習します。各チャネルから、その局所的な挙動を表現する256個の特徴量が生成されます。

畳み込みの後、次の段階に進みます。データは32個の重複しないパッチに分割されます。それぞれが時系列の特定の区間をカバーします。各パッチに対して、チャネルごとの平均化、すなわち平均プーリングをおこないます。これにより、256個の特徴量を持つ32個のトークンが得られます。ここは重要なポイントです。パッチは市場の挙動を局所的に切り出した区間であり、いわば市場の動きを凝縮したミニチャートです。インパルス的な値動きやレンジ相場、押し戻し、ダイバージェンスなど、さまざまな局面を捉えます。このような分割によって、モデルの挙動はより安定し、適応性も高まります。モデルは市場を単なる数値の連続としてではなく、認識可能な一連のシナリオとして見るようになります。

この段階では、各入力データチャネルについて、元のストリームと差分ストリームからそれぞれ1つのトークンが取得されます。これらのトークンは共通の範囲にスケーリングされ、正規化された後、結合されます。その後、線形射影層によって要約トークンが生成され、指定された区間におけるそのチャネルの挙動が表現されます。このようなトークンには、状態、変化率、強度が含まれます。つまり、トレーダーが市場センチメントを直感的に判断するために必要となる情報が含まれているわけです。

次に、トークンの系列全体がアテンションブロックに入力されます。ここが最も重要な段階です。ここでパッチ同士が情報をやり取りし始めます。モデルは、過去のどの区間が現在の状態を判断するうえで重要なのかを評価します。たとえば、20本前のバーで発生したシグナルが、現在になって反転を判断するうえで重要になっている可能性があります。このように、離れた区間同士の関連性を捉えられることが、複雑な市場構造を分析するうえでMantisを特に有用なものにしています。

そのすべての背後にある考え方は、シンプルでありながら強力です。それは、局所的な精度を維持しながら、全体的なコンテキストを見失わないことです。これは取引において非常に重要です。たとえば、相場が小動きしているときに連続して現れる小さなローソク足には、ほとんど情報がないかもしれません。しかし、それらが出来高の急増、特に重要な価格水準での急増の直後に現れた場合、状況はまったく異なります。Mantisは、このようなコンテキストを自動的に考慮します。

Mantisフレームワークは、単なるアーキテクチャ以上のものです。さまざまな種類の取引戦略に適応できる柔軟なツールなのです。

著者が作成したMantisフレームワークの可視化を以下に示します。


モデルアーキテクチャ

これまでの記事では、Mantisフレームワークの主要なコンポーネントをすでに構築してきました。そして現在、重要な段階に到達しています。それは、リアルタイムで取引判断をおこなえる学習可能なモデルのアーキテクチャを設計することです。価格の動き、出来高、ローソク足パターン、そして市場全体のセンチメントを観察し、それらをコンテキストとして考慮したうえで取引へのエントリーを判断する経験豊富なトレーダーと同じように、私たちのモデルも市場のダイナミクスを単なる数値の流れとしてではなく、相互に関連した一連のプロセスとして認識できるように学習する必要があります。私たちが目指しているのは、市場パターンを識別し、市場局面の変化を感じ取り、新しいシナリオに適応できる行動モデルを再現することです。Mantisフレームワーク全体のロジックは、まさにこの考え方に基づいて構築されています。

ここで強調しておきたいのは、Mantisフレームワークがもともと時系列分類器として開発されたということです。そのアーキテクチャは、複雑な系列を分割し、その中に潜むパターンを識別するように設計されています。このアプローチは、価格変動がしばしば市場ノイズの層の下に隠れている金融分野において、特に有効であることが示されています。畳み込み、局所的なパッチの形成、チャネルの集約などを含む多段階の処理によって、モデルはランダムな変動ではなく、市場データの構造を捉えることを学習します。Mantisによって、単にシグナルを捉えるだけでなく、市場レジームを識別することも可能になります。

しかし、今回の目的においては、Mantisはもはや独立した分類器として機能するのではありません。むしろ、取引エージェントの知覚層の基盤として機能します。私たちはMantisアーキテクチャを利用して、市場の状況をコンパクトにまとめながらも豊富な情報を含む表現、すなわち埋め込みを構築します。そして、それをモデルの制御コンポーネントへ入力します。このようにして、分類器はエージェントの目となり、市場の振る舞いを把握する力を与えます。

この埋め込みは、アーキテクチャの次の部分へと渡されます。そこでは、Actor–Director–Criticの原則に基づく構成を採用します。このアプローチによって、各モジュールの責任を分担し、エージェントの挙動を安定させることができます。Actorは埋め込みを受け取り、取引判断を形成します。Directorは構造的なフィルターおよび行動を補正する役割を担います。Actorが提案した行動を適切なものか誤ったものかに分類し、強いフィードバック信号を提供します。その目的は、特に市場が不安定な領域において、明らかに不合理または不安定な判断を排除することです。Criticは、現在の状態と過去の相互作用履歴に基づいて行動の戦略的な妥当性を評価することで、このサイクルを完成させます。いわば内部アドバイザーとして機能します。つまり、「その行動を取ることは可能だとしても、現在の状況において、そのリスクを負うだけの価値が本当にあるのか?」を評価する役割です。

すべてのモデルコンポーネントのアーキテクチャは、CreateDescriptionsメソッドを使用して定義されます。このメソッド内で、各モジュールの層が順番に設定されます。柔軟なパラメータシステムにより、アーキテクチャを拡張したり、さまざまな金融商品の特性に適応させたり、コアロジックを書き直すことなく、実験的な要素を追加したり無効にしたりすることが可能になります。

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

メソッドのパラメータを通じて、4つの動的配列へのポインタを受け取ります。それぞれ、対応するモデルコンポーネントであるエンコーダ、Actor、Director、Criticのアーキテクチャ記述を格納するためのものです。これらの配列は、ニューラルネットワークの各ブロックの構造を記述するオブジェクトを1つずつ追加していくためのコンテナとして機能します。

メソッド本体では、まず受け取ったポインタの有効性を検証します。いずれかが初期化されていない、または無効なメモリアドレスを指している場合には、対応するオブジェクトの新しいインスタンスが自動的に生成されます。この方法により、あらかじめ配列を手動で用意しておく必要がなくなり、コードのモジュール性が高まるとともに、スケーラブルなソリューションにも適した構成になります。

次に、モデルのアーキテクチャを直接定義していきます。この流れでは、まずエンコーダから説明するのが自然でしょう。エンコーダは、モデルが市場を認識し始めるための中心的なモジュールです。ここから、生の時系列を市場状態を意味のある形で表現したデータへと変換する処理が始まります。

最初の段階では、全結合層を使用します。ただし、この場合、この層は計算処理をおこなうためのものではなく、純粋にインターフェースとしての役割を担います。その役割は、入力データをモデルへ読み込むための扱いやすいバッファインターフェースを提供することです。

//--- 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 = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

学習時に穏やかなデータ拡張をおこなうため、ここでは制御されたノイズを加えるバッチ正規化層を使用します。これにより、モデルが市場ノイズに対してより頑健になり、重要ではない小さな変動に過度に適合することを防ぎます。このアプローチによって、適応性と安定性のバランスを維持しながら、システムが市場情報をより柔軟に解釈できるようになります。

次に重要なステップは、1階差分チャネルを作成することです。この層では差分特徴量、つまり隣接する時点間における値の変化を生成します。これにより、モデルは市場の値動きのダイナミクスと方向性を捉えやすくなります。取引においては、このような変化がトレンドの反転や継続を判断するための重要な手掛かりになることがよくあります。ある時点における価格の上昇や下落は、場合によっては価格の絶対値そのものよりも重要だからです。差分チャネルを作成することで、元のデータに含まれる情報量が大幅に増加し、モデルは局所的なトレンドや急激な変動に対してより高い感度を持つようになります。

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

1階差分チャネルを形成した後、拡張されたデータセットに時間エンコーディングの調波成分を追加します。この処理は、モデルが時間的依存関係のコンテキストや市場のプロセスに見られる周期性を理解できるようにするうえで、非常に重要です。調波成分は、いわば時間的な道標のような役割を果たします。これによってモデルは、単なる特定の時点だけでなく、繰り返し現れるパターン、季節変動、さまざまな時間スケールも認識できるようになります。このような調波成分による時間エンコーディングによって、モデルは時系列データの構造をより深く理解できるようになります。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defMamba4CastEmbeding;
   prev_count = descr.count = HistoryBars;
   descr.window = 2 * BarDescr;
   int prev_out = descr.window_out = NSkills;
     {
      int temp[] = {PeriodSeconds(PERIOD_H1), PeriodSeconds(PERIOD_D1)};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

データにおけるチャネル間の関係性と相互依存関係を効果的に捉えるため、異なるウィンドウサイズを持つ畳み込みブロックを使用します。このマルチウィンドウ方式により、モデルは複数のチャネルにまたがるダイナミクスを同時に分析し、局所的な短期パターンだけでなく、より長期的なトレンドも捉えることができます。

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

データ準備の最終段階は、パッチ化層です。この層では、処理済みのマルチチャネルテンソルを、互いに重ならない個別のパッチに分割します。これは基本的に時系列の断片であり、それぞれのパッチには、局所的な市場状況に関する情報がまとめられています。

この分割によって、モデルは個々のデータ区間に集中できるようになり、計算量を削減するとともに、局所的なパターンを識別しやすくなります。さらに、各パッチ内で追加の集約処理をおこなうことで、コンパクトでありながら情報量の豊富な表現を構築します。

その結果、各パッチは独立したトークンとなり、限られた時間区間における市場状況を、圧縮かつ構造化された形で表現します。これにより、モデルは局所的なパターンを捉える能力が大幅に向上し、その後の処理段階や取引判断を支える強固な基盤が形成されます。

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisPatching;
   descr.count = prev_count;
   descr.layers = prev_out;
   descr.window = EmbeddingSize;
   descr.window_out = NSkills;
   descr.step = Segments;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count = descr.step;
   prev_out = descr.layers;

エンコーダ内のアテンションブロックは、CNeuronMantisAttentionUnitという単一のオブジェクトとして実装されています。このコンポーネントは、パッチとチャネルの間に存在する重要な関係性を捉えるうえで中心的な役割を果たします。オブジェクトのパラメータでは、内部に配置するクロスアテンションモジュールの数を指定します。これにより、情報を分析する際の深さと範囲を柔軟に調整できます。

各クロスアテンションモジュールは、データストリームの中から特に重要な要素を識別し、それらを学習可能なClass Tokenと関連付けます。このアーキテクチャにより、ノイズを効果的にフィルタリングするとともに、モデルが取引判断に実際に影響を与えるシグナルに集中できるようになります。

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisAttentionUnit;
   descr.step = 4;
   descr.count = prev_count;
     {
      int temp[] = {EmbeddingSize, EmbeddingSize};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = 3;
   descr.window_out = NSkills;
   descr.window = prev_out;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

クロスアテンションモジュールの出力では、トークンが生成されます。これは、分析対象となった環境の状態をコンパクトかつ情報量豊富に表現したものです。今回の目的においては、状態を正確に分類することが最優先ではありません。私たちにとってより重要なのは、Actorが十分に検討された適切な取引判断を下せるように、明確で識別性の高い潜在表現を得ることです。

だからこそ、エンコーダのアーキテクチャを意図的に複雑化しすぎないようにしています。このアプローチによって、モデルの知覚能力の深さと計算効率のバランスを維持し、判断の品質を損なうことなく、実際の市場環境に柔軟に適応できるようになります。

次に、Actorのアーキテクチャについて説明します。エンコーダの場合と同様に、入力段階では全結合層とバッチ正規化ブロックを組み合わせて使用します。この2つの層は、入力データを受け取り、初期的な標準化をおこなうためのインターフェースとして機能します。ただし、この場合、主要な情報チャネルが扱う情報は異なります。ここでは、取引口座の現在の状態に関するデータを入力します。具体的には、残高、保有中のポジション、ドローダウンの水準、その他、取引エージェントの内部的な運用状況を表す各種指標が含まれます。

//---
   CLayerDescription *latent = encoder.At(7);
//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   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 = AccountDescr;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

並行して、補助チャネルを通じて、エンコーダによって形成された市場環境の潜在表現がモデルに入力されます。この2つのデータストリーム、すなわち内部状態と外部コンテキストを組み合わせることで、Actorは、すでに戦略的思考とリスク管理の要素を取り入れた判断を下せるようになります。モデルは、現在の市場状況だけでなく、自身が利用できる資金、許容できるリスク、そして現在のポジションの変化も考慮して判断するように学習されます。このアプローチにより、市場が大きく変動している状況でも、エージェントは意図的かつ頑健な行動を取れるようになります。

市場のコンテキストと取引口座の状態という2つの情報ストリームは、結合層で統合されます。このオブジェクトが担う役割はシンプルですが重要です。エンコーダから受け取った埋め込みベクトルと、取引口座の内部的な特性を1つの表現に結合します。これにより、モデルは市場で何が起きているのかだけでなく、自身が置かれている状況も含めた全体像を把握できるようになります。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = AccountDescr;        // Inputs window
   descr.step = latent.windows[0];     // Cross window
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

データの結合後、深層特徴処理が始まります。この処理は、3つの全結合層を順番に通すことで実装されています。第1層では、重要な特徴を抽出し、それらを一般化します。第2層では、パラメータ間の重要な依存関係を強化し、潜在的なパターンを明らかにします。第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 = SoftPlus;
   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;
     }

DirectorモデルとCriticモデルのアーキテクチャは、ほぼ完全にActorモデルの構造を踏襲しています。主な違いは、入力データの次元数にあります。これらのモデルには、メインの入力チャネルを通じてエージェントのアクションテンソルが入力されます。そして出力では、そのアクションに対する評価が生成されます。Directorは分類的な評価をおこない、Criticは戦略的な評価をおこないます。これらのモデルの内部ロジックは互いに類似しているため、ここで同じ説明を繰り返して記事を冗長にすることは避けます。より詳細な内容については、この記事に付属するコードを参照してください。そこでは、システムを構成するすべての学習可能なコンポーネントのアーキテクチャが完全に記述されています。


対照学習

モデルのアーキテクチャを構築した後、次の重要な段階である学習へ進みます。理論のセクションで詳しく説明したように、Mantisフレームワークの主要な特徴の1つが、自己教師あり対照学習です。この仕組みによって、モデルは環境の状態をコンパクトでありながら表現力豊かな形で記述する、情報量が多く明確に区別可能なトークンを生成できるようになります。経験豊富なトレーダーが、チャートを一目見ただけでレンジ相場とインパルス的な値動きを区別できるのと同じように、私たちのモデルも特徴的な市場状況を識別し、それらを後続のシステムコンポーネントが利用できるよう、凝縮された表現として構築できるようになる必要があります。

この場合、対照学習は、モデルに市場に対する直感を身につけさせるための基盤として機能します。厳密な教師信号を与えることなく、状態のペア間に存在する違いを明確に捉えることで、エンコーダは、互いに似ているものの本質的には異なる市場シナリオを、できる限り効果的に区別できる表現を構築するように学習します。

対応するアルゴリズムの実装は、エキスパートアドバイザー(EA)「…\MQL5\Experts\Mantis\StudyContrast.mq5」に含まれています。このEAは、正例と負例のペアを形成する処理、学習イテレーションの実行、統計情報の収集、そして学習済みパラメータの保存を管理します。

ここで、私たちの対照学習の実装と従来のアプローチとの間にある、重要な、そして誇張ではなく概念的な違いについて触れておく必要があります。私たちは、この学習段階において、時間のかかる学習データセットの作成処理を意図的に省略しました。その代わりに、MetaTrader 5プラットフォームが持つ機能と柔軟性を最大限に活用し、学習中に学習データを動的に生成する仕組みを実装しました。

ユーザーがおこなう必要があるのは、EAの設定で学習期間の開始日と終了日を指定することだけです。必要な市場データはすべて、ターミナルから自動的に要求され、リアルタイムで取得されます。このアプローチによって、学習手順が簡素化されるだけでなく、モデルの学習において、より幅広い可能性を活用できるようになります。

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input datetime             Start          = D'2020.01.01';
input datetime             End            = D'2025.01.01';
input int                  Iterations     = 100000;
input int                  Batch          = 50;
input group                "---- Indicators ----"
input ENUM_TIMEFRAMES      TimeFrame   =  PERIOD_M1;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod =  9;            //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price

次に、この処理がTrainメソッドでどのように実装されているのかを詳しく見ていきます。このメソッドが対照学習の一連のサイクルを開始し、データの準備、バッファの管理、ネットワークのフォワードパスとバックプロパゲーションに関するロジックを担います。

処理はまず、過去データの範囲を定義するところから始まります。iBarShift関数を使用して、現在のバーから学習期間の開始時点および終了時点までのオフセットを求めます。

void Train(void)
  {
   int start = iBarShift(Symb.Name(), TimeFrame, Start);
   int end = iBarShift(Symb.Name(), TimeFrame, End);
   int bars = CopyRates(Symb.Name(), TimeFrame, 0, start, Rates);

次に、読み込む履歴データの長さに合わせて、すべてのインジケータ用のバッファを確保します。

if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) ||
   !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

次の段階はデータの読み込みです。これをおこなうため、すべてのインジケータのRefreshメソッドを順番に呼び出し、計算済みの値の数を確認するループを実装します。このループは、データの読み込みが正常に完了した時点、または100回の試行を終えた時点で終了します。

int count = -1;
bool load = false;
do
  {
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   count++;
   load = (RSI.BarsCalculated() >= bars &&
           CCI.BarsCalculated() >= bars &&
           ATR.BarsCalculated() >= bars &&
           MACD.BarsCalculated() >= bars
          );
   Sleep(100);
   count++;
  }
while(!load && count < 100);
if(!load)
  {
   PrintFormat("%s -> %d The training data has not been loaded",
                                        __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

データ準備の最後に、価格配列のインデックス方向を必要な形式に設定するため、ArraySetAsSeriesを使用します。その後、必要なローカル変数を宣言します。

   if(!ArraySetAsSeries(Rates, true))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   bars -= end + HistoryBars;
   if(bars < 0)
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
//---
   vector<float> result, target, neg_target;
   bool Stop = false;

ここから、メインの学習ループが始まります。各反復処理にでは、全体の範囲から、基準となる状態を形成する位置、つまりバーインデックスをランダムに選択します。

   uint ticks = GetTickCount();
//---
   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch)
     {
      int posit = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      if(!CreateBuffers(posit + end, GetPointer(bState), GetPointer(bTime)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         return;
        }

この状態に対して入力データ用のバッファを作成し、エンコーダのフォワードパスを実行します。

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

フォワードパスの結果を基準値として保存します。これは、モデルが一致させることを目指す市場状態のサンプルに相当します。次に重要なのが、まったく同じ入力データを使って、もう一度エンコーダのフォワードパスを実行することです。ただし、ここには微妙な違いがあります。私たちのアーキテクチャに組み込まれている、ノイズを追加したバッチ正規化層が、穏やかなデータ拡張として機能します。そのため、2回目のフォワードパスでは、モデルは単純に前回と同じ結果を再現するのではなく、わずかに変化したノイズを含む出力を生成します。

私たちは、まさにこの性質を利用して正例ペアを形成します。バックプロパゲーションでは、先ほど保存した基準値を目標値として使用します。これにより、モデルはランダムなノイズを無視し、意味のある安定したコンテキスト特徴に集中するように学習します。簡単に言えば、モデルはノイズの中から本質を見抜き、市場の主要な特徴を安定して捉えることを学習するのです。

//--- Positive
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                 (CBufferFloat*)GetPointer(bTime)) ||
   !cEncoder.backProp(Result, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

次に、おそらく最も興味深い部分である負例ペアの形成に進みます。負例ペアの数は、EAのパラメータであるBatch変数によって指定されます。必要な数の負例ペアを生成するため、まず基準トークンをベクトルに移したうえで、入れ子になったループを作成します。

//--- Negative
if(!Result.GetData(target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
for(int b = 0; b < Batch; b++)
  {
   int negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
   int count = 0;
   while(negot == posit)
     {
      negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      count++;
      if(count > 100)
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }
     }
   if(Stop)
            break;

入れ子になったループの本体では、学習範囲から別の状態をランダムに選択します。ただし、先ほど使用した基準状態とは異なる状態を選ぶ必要があります。ここで重要な点を強調しておきます。基準状態と一致しない限り、どの状態でも選択できます。理論上は、選択した状態の範囲が基準状態の範囲と重複していても問題ありません。

まさにこの点が、学習プロセスをより動的で現実的なものにしています。モデルは、非常によく似た状態の違いを識別することを学習します。範囲が重複している場合、モデルは2つの状態を区別するための微妙な違いを正確に捉える必要があります。実際の市場環境では、このような微妙な違いこそが、取引判断の成否を左右することが少なくありません。

このように、私たちの対照学習は、エンコーダに単に状態を認識させるだけではなく、重要な特徴を抽出させることで、その後のActorによる処理に必要となる、情報量が豊富で識別性の高いトークンを生成できるようにします。

選択した状態について、入力データ用のバッファを生成し、エンコーダのフォワードパスを実行します。

if(!CreateBuffers(negot + end, GetPointer(bState), GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }
//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                   (CBufferFloat*)GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

次に、負例に対する学習目標を形成する段階に進みます。そのために、現在のステップで得られたトークンと、先ほど保存した基準表現との差を計算します。ここが重要なポイントです。現在のトークンを基準表現から遠ざけることで、負例に対する目標値を生成します。

この効果を強めるため、基準状態と現在の状態との距離を2倍にします。これによって、トークンを互いに遠ざける力が強まります。その結果、基準状態に近い状態はわずかに遠ざけられる一方、大きく異なる状態はより強く遠ざけられます。これにより、基準表現の周囲に余裕のある空間を広げ、トークン同士をより明確に区別できるようになるとともに、ノイズに対する頑健性も高まります。

この仕組みによって、学習プロセスに動的な要素が導入され、潜在空間が明確に分離されるようになります。モデルは単に状態を認識することを学習するのではありません。基準状態の周囲に安全領域を形成し、異なる状態を一定の距離に保つことを学習します。これにより、その後の取引判断の品質と信頼性が大幅に向上します。

cEncoder.getResults(result);
neg_target = result * 2 - target;
neg_target = neg_target - neg_target.Max();
if(!neg_target.Activation(neg_target, AF_SOFTMAX) ||
   !Result.AssignArray(neg_target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

最後に、もう1つ重要なステップが残っています。それは、生成した負例の目標値を使用してエンコーダのバックプロパゲーションを実行することです。この段階によってモデルのパラメータが最適化され、潜在空間において異なる状態同士を効果的に引き離すようにモデルが調整されます。

if(!cEncoder.backProp(Result, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

このステップにおけるすべての学習処理が正常に完了すると、システムはユーザーに現在の状況を知らせるため、処理ステータスを更新します。進捗率とモデルの誤差指標を表示することで、現在の進行状況を確認できるようにします。このように処理状況を可視化することで、アルゴリズムの動作に対する信頼性を高めるだけでなく、学習の有効性を迅速に確認することもできます。

その後、学習ループの次のイテレーションへ進み、ここまで説明した一連の処理を新しいデータに対して再び実行します。このサイクルを繰り返すことで、モデルは継続的かつ着実に改善され、十分な情報量と精度を備えた表現を徐々に構築していきます。これらの表現が、より適切な取引判断を下すための基盤となります。

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

すべての学習ループのイテレーションが正常に完了した後、学習結果をログに記録します。その後、プログラムの終了処理が開始されます。

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

これは、セッションを適切に終了させるための、明確かつ制御された終了処理です。これにより、重要なデータがすべて保持され、使用したリソースも適切に解放されます。

エンコーダの対照学習をおこなうプログラムの完全なソースコードは、添付ファイルに収録されています。同じアーカイブには、ActorDirectorCriticモデルのオフライン学習およびオンライン学習用プログラムも含まれています。これらのプログラムは、過去に開発したものを最小限の変更で移植したものです。特に、エンコーダの学習処理は削除されています。現在では、この処理を独立したプログラムとして実装しているためです。このアプローチによって、学習処理を整理し、フレームワーク全体の柔軟性と保守性を高めることができます。


テスト

学習プロセスは、モデル全体に対して体系的かつ信頼性の高いワークフローを確立するため、3つの段階に分けて順番に実施しました。

第1段階は、エンコーダの対照学習です。EURUSDM1について、過去5年間の履歴データを使用して実施します。このデータ量と詳細度によって、エンコーダは市場状態を高品質かつ情報量豊富な潜在表現として構築できるようになり、これがシステム全体のその後の処理を支える基盤となります。

次が第2段階である、システムの主要コンポーネントであるActorDirectorCriticのオフライン学習です。この学習には2024年のデータから収集した学習データセットを使用し、それまでに設定したすべてのパラメータを維持します。この処理では、ほぼ完全な軌跡という概念を利用し、最も信頼性の高いアクションと評価の事例からモデルを学習させます。この段階は、基本的な戦略と取引判断の基準を強化するうえで重要です。

第3段階は、モデルのオンラインファインチューニングです。これは同じ履歴データの期間を使用し、ストラテジーテスター上で直接実施します。これにより、変化する市場環境にモデルを適応させるとともに、パラメータを可能な限り正確に調整できます。

すべての学習段階が完了した後、2025年1月から3月までのデータを使用してモデルをテストします。この際、学習段階で使用したすべてのパラメータは変更しません。この方法によって、これまで使用していない新しいデータセットに対して、モデルの性能を公平かつ客観的に評価できます。以下にそのテスト結果を示します。

テスト期間中、モデルは881回の取引を実行し、そのうち447回が利益確定となり、勝率は50.74%でした。この結果は、利益取引と損失取引がほぼ均衡していることを示しています。プロフィットファクターは1.25です。

全体として、この戦略は中程度のリスクでプラスの結果を示しており、テスト期間の前半では資産曲線も安定して上昇しています。しかし、2月中旬以降は収益性が低下し、明確な横ばい・レンジ相場となり、資産曲線も低下しています。

したがって、この戦略には実用性があり、テスト期間全体ではプラスのパフォーマンスを示していますが、いくつかのパラメータについてはさらなる改善が必要です。


結論

今回の開発では、Mantisフレームワークをカスタマイズしたアルゴリズム取引モデルに完全に統合し、高頻度時系列データから有益な特徴を抽出し、それをリアルタイムで意味のある取引判断へ変換できるシステムを構築しました。

特に注力したのが、StudyContrast.mq5 EAに実装したエンコーダの自己教師ありコントラスト学習です。私たちは静的なデータセットを使用する方式から脱却し、ターミナルから市場データを動的に読み込む仕組みを実装しました。さらに、EURUSDのM1について過去5年間のライブの事例データを学習中に動的に利用できるようにしました。ソフトなデータ拡張と制御されたノイズによって生成された正例ペアと負例ペアにより、モデルは市場ノイズを透かして本質的な特徴を捉えることができるようになりました。

2025年1月から3月までの期間を対象とした最終テストでは、今回実装した手法が収益性を示す結果が得られました。この戦略には実用性があり、テスト期間全体でプラスのパフォーマンスを示しています。ただし、さらなる改善が必要なパラメータも残されています。


参照文献


記事で使用されているプログラム

# 名前 種類 詳細
1 Research.mq5 EA 事例収集用EA
2 ResearchRealORL.mq5
EA
事例収集(Real-ORL)用EA
3 StudyContrast.mq5 EA エンコーダ対照学習用EA
4 Study.mq5 EA オフラインモデル学習用EA
5 StudyOnline.mq5
EA
オンラインモデル学習用EA
6 Test.mq5 EA モデルテスト用EA
7 Trajectory.mqh クラスライブラリ システム状態とモデルアーキテクチャ記述構造
8 NeuroNet.mqh クラスライブラリ ニューラルネットワーク構築用のクラスライブラリ
9 NeuroNet.cl ライブラリ OpenCLプログラムコードライブラリ


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

添付されたファイル |
MQL5.zip (2794.71 KB)
最後のコメント | ディスカッションに移動 (1)
削除済み | 30 5月 2025 において 17:49
不思議ですね、なぜ片方向だけで取引しているのでしょうか。取引を決済するシグナルは、どのように形成されるのでしょうか?
機械学習におけるガウス過程(第1回):MQL5における分類モデル 機械学習におけるガウス過程(第1回):MQL5における分類モデル
ガウス過程の分類モデルを扱います。まず、その理論的原理について学び、続いてMQL5におけるGPライブラリの実装へと進みます。
最強のML搭載取引プラットフォームが生まれるまで:アーカイブ、フォーラム、リリースからたどるMQLとMetaTraderの進化 最強のML搭載取引プラットフォームが生まれるまで:アーカイブ、フォーラム、リリースからたどるMQLとMetaTraderの進化
MQLの技術史を振り返り、機能が限定されていた初期のMQLおよびMQL II言語から、手続き型のMQL4、そしてオブジェクト指向のMQL5へと至る進化をたどります。MQL5では、ネイティブコンパイル、豊富なAPI、本格的な開発環境が実現しました。本記事では、MQLの主要な機能に加え、Python、OpenCL、ONNX、OpenBLAS、データベース、DirectX、エージェント型AIアシスタント(Agentic AI Assistant)、そしてModel Context Protocol (MCP)との統合について紹介します。MCPは、AIシステムとターミナル、MetaEditor、市場データ、取引操作、開発ツールを接続します。さらに、MetaQuotesとMetaTraderの起源に関するアーカイブ資料、MQL4.COMおよびMQL5.COMの開設、各種選手権、Algo Forge、そしてこれらがエコシステムに与えた影響について考察します。
科学者コミュニティ最適化 (CoSO):理論編 科学者コミュニティ最適化 (CoSO):理論編
メタヒューリスティック手法における取引戦略の効果的な最適化の秘訣。Community of Scientists Optimization (CoSO)は、科学者コミュニティの仕組みに着想を得た、集団ベースの新しい最適化アルゴリズムです。従来の自然界を模倣したメタファーとは異なり、CoSOでは、人間による科学活動の特徴的な側面、すなわち、学術誌への研究成果の発表、研究助成金をめぐる競争、研究チームの形成などをモデル化します。
初級から中級まで:MetaTrader 5のサンドボックスにおけるファイル操作 初級から中級まで:MetaTrader 5のサンドボックスにおけるファイル操作
サンドボックスとは何かご存じでしょうか。また、サンドボックスをどのように扱うのかご存じでしょうか。もし、これらの質問のいずれかに「いいえ」と答えるのであれば、サンドボックスの基本的な動作原理を理解するために、この記事を読んでください。また、MetaTrader 5が内部データの整合性を保護するためにサンドボックスを使用している理由についても理解できるでしょう。ここで紹介する内容は、純粋に教育目的です。いかなる場合においても、本稿で扱うアプリケーションを、ここで説明する概念を学習すること以外の目的を持つ完成品として扱わないでください。