取引におけるニューラルネットワーク:Actor-Director-Critic(最終回)
はじめに
前回の記事では、Actor–Criticアーキテクチャを拡張したActor–Director–Criticフレームワークの理論的側面について説明しました。古典的なActor–Criticアーキテクチャは、多くの成功した強化学習(RL)アルゴリズムの基盤となっています。この構成では、エージェントは2つのコンポーネントに分割されます。Actorは行動を提案し、Criticは環境から受け取った報酬に基づいて、その行動を評価します。この組み合わせにより、行動は徐々に目的指向的なものとなり、評価も次第に正確になっていく戦略の形成が可能になります。しかし、その洗練された構造と有効性にもかかわらず、このアーキテクチャは実世界の応用、特に取引分野において、いくつかの重大な制約に直面します。
主な問題は、学習初期段階で発生します。この時点では、Actorはまだどの行動が有益であるかを学習しておらず、一方でCriticも学習を開始したばかりであるため、行動を適切に評価することができません。その結果、エージェントは一種の手探り状態に陥ります。つまり、多数のランダムで非効率的、場合によっては有害な行動を実行しながら、情報量の少ない、あるいは遅延したフィードバックを受け取ることになります。市場環境では、こうした誤りが直接的な損失につながる可能性があるため、このようなアプローチは過度にリスクが高く、不安定になります。
この根本的な問題に対処するため、Actor–Director–Criticフレームワークの著者らは、追加のコンポーネントであるDirectorを導入しました。Directorは、システムに新たな評価経路を追加します。環境から得られる報酬に基づいて行動を連続的に評価するCriticとは異なり、Directorは行動を二値的に分類します。つまり、その行動が与えられた戦略に適合するか、適合しないかを判断します。これにより、エージェントは意思決定空間内でより迅速かつ効果的に方向付けできるようになります。
ここで重要なのは、Directorはフィルターではなく、エージェントの行動自由度を制限するものではないという点です。むしろ、より明確な区分的フィードバックを提供することで、Criticの評価を補完します。Criticは、特に学習初期段階では、ある行動の良し悪しについて不確実な評価しかできない場合があります。一方、Directorは、学習した行動方針に基づいて、その行動が適切なものかどうかを即座に判断できます。これにより、Actorはすでに誤りであると判断された行動を繰り返すことを避けることができ、学習リソースを節約するとともに、より堅牢な戦略の形成を加速できます。
その結果、3つのコンポーネント間に相乗的な相互作用が生まれます。Actorは、どの行動を選択すべきかを学習します。Criticは、期待報酬という観点から行動を評価することを学習します。Directorは、そもそも避けるべき行動を明示的に識別します。これにより、連続的評価と二値的評価という2種類のフィードバックを備えた二重フィードバックシステムが形成されます。この仕組みにより、エージェントは有望でない方向性をより迅速に排除し、効果的な戦略へ集中できるようになります。
著者らによるActor-Director-Criticフレームワークの可視化を以下に示します。

前回の記事の実践的なセクションでは、学習可能モデルのアーキテクチャについて詳しく説明しました。ここで注意すべき点は、著者らによるオリジナルの実装と比較して、いくつかの大きな変更を導入していることです。
まず第一に、以前説明したHiSSDマルチエージェントフレームワークへ、提案された概念を適用しました。さらに、本実装ではDirectorとCriticの両方を、エージェントの共有スキルの潜在表現上で学習しています。これは、環境状態エンコーダの内部層から抽出された圧縮特徴であり、生の環境特徴そのものではありません。このアプローチにより、行動パターンの一般化された表現を形成することが可能になります。その結果、行動は特定の市場状況ではなく、より広い行動ロジックの文脈において評価されます。これにより、システムはより安定した、戦略的意味を持つフィードバックを受け取ることができます。これは、金融市場で頻繁に発生するような、情報が限られ、不確実性が高い状況において極めて重要です。
本記事では、焦点をモデルの学習プロセスへ移します。この学習過程についても、オリジナルの手法に対していくつかの変更を加えています。
主要な違いの一つは、学習を2段階に分割した点です。第1段階では、あらかじめ収集された学習データセットを用いて、システムのすべてのコンポーネントをオフライン学習します。この段階では、エージェントは初期経験を蓄積し、実運用中の取引に影響を与えることなく、基本的な行動戦略を形成できます。各モデルは、アーキテクチャ内での役割に合わせて設計された独自の目的関数を用いて学習されます。
第2段階では、オンラインファインチューニングをおこないます。この段階では、エージェントは環境と直接相互作用を開始します。Actorはリアルタイムで方策を改善し、CriticとDirectorも継続的に学習を続けます。これにより、更新されたデータに基づいて評価および分類能力が向上します。この段階によって、エージェントはオフライン学習で確立された戦略的方向性を維持しながら、現在の市場状況へ適応できるようになります。
この2段階アプローチにより、安定性と適応性のバランスが実現されます。エージェントはまず信頼性の高い初期方策を形成し、その後、実環境条件下でそれを改善していきます。その結果、変化する市場ダイナミクスへ適応しながらも、戦略的一貫性を失わない、頑健で効率的に学習可能なシステムを構築できることが期待されます。
オフライン学習
第1学習段階(オフライン学習)のアルゴリズムは、エキスパートアドバイザー(EA)「……\Experts\ADC\Study.mq5 」に実装されています。そのコードの大部分は以前のプロジェクトから継承されたものです。これは特に驚くことではありません。学習可能モデルのアーキテクチャ設計は、すでに説明したHiSSDフレームワーク内で開発された概念に大きく基づいているためです。
この継続性により、既存の実績ある手法を活用でき、車輪の再発明を避けられました。しかし、それでもいくつかの変更は避けられませんでした。
2つの新しいコンポーネント(CriticとDirector)を追加したことで、システムの機能は大幅に拡張されました。その統合には、モデル学習ロジックへの変更が必要でした。本記事では、モデル学習処理のほぼ全体を含む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; }
オフライン学習の初期段階では、経験リプレイバッファから1つの軌跡をサンプリングします。その後、この軌跡内から環境状態を1つランダムに選択し、それを新しい学習ミニバッチを構築する開始点として使用します。この方法により、学習データ間の多様性が確保されます。
新しく形成されたミニバッチを処理する前に、すべての学習可能モデルの内部一時データバッファを必ずリセットします。これは特にリカレントモジュールにおいて重要です。リカレントモジュールは周知のようにメモリを持ち、過去の状態から得られた文脈情報を保持し、以前の時間ステップから知識を蓄積することができます。しかし、関連性のない軌跡セグメント間を切り替える場合、このメモリは問題となる可能性があります。
以前のミニバッチで保存された隠れ状態の文脈は、もはや関連性を持ちません。それは新しいサンプルとは無関係であり、生成される信号を歪める可能性があります。したがって、新しいミニバッチを処理する前に内部状態をリセットすることは単なる予防策ではなく、現在の履歴データセグメントを正確かつ独立して分析するための必要条件です。
if( !cEncoder.Clear() || !cTask.Clear() || !cActor.Clear() || !cProbability.Clear() || !cDirector.Clear() || !cCritic.Clear() ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } result = vector<float>::Zeros(NActions);
ミニバッチの初期化後、次の段階へ進みます。ここでは、環境状態を順番に処理するネストされたループを構築します。これらの状態は、エージェントが取引環境との相互作用中に実際に観測した順序と完全に同じ、時系列順で処理されます。
履歴順序を維持することは単なる形式的な要件ではありません。これは、リカレントモデルコンポーネントを効果的に学習させるための根本的な条件です。独立した入力を処理する通常の全結合層とは異なり、リカレントニューラルネットワークは、過去の時間ステップから蓄積された情報に基づいて隠れ状態を維持します。その強みは、時系列データ内に存在する時間的依存関係、行動パターン、繰り返し現れるシグナルを識別できる能力にあります。
もしデータの時間構造が、たとえ部分的であっても破壊されれば、モデルは時間的ダイナミクスによってのみ現れる因果関係を認識する能力を失う可能性があります。その結果、こうした学習サンプルの価値は大きく低下します。
この理由から、ミニバッチ内の各時間ステップは、シャッフル、欠落、順序変更をおこなわず、厳密に連続して処理されます。この方法により、リカレントコンポーネントは内部的な文脈表現を段階的に構築し、市場環境の変化に関する情報を蓄積できます。
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(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cTask.feedForward((CBufferFloat*)GetPointer(bState), 1, false, GetPointer(cEncoder), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cActor.feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, GetPointer(cTask), -1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cProbability.feedForward(GetPointer(cEncoder), LatentLayer, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
ここで注意すべき点は、この段階ではエージェントの行動評価モデル、つまりCriticとDirectorの順伝播は実行しないということです。これは「準完全軌跡」アプローチを採用していることによる直接的な結果です。ただし、この点については後ほど改めて説明します。
学習可能モデルの順伝播が正常に完了すると、目標値の生成へ進みます。まず、経験リプレイバッファから、あらかじめ定められた計画ホライズンにわたる将来の環境状態系列を読み込みます。
//--- Look for target target = vector<float>::Zeros(NActions); bActions.AssignArray(target); if(!state.Assign(Buffer[tr].States[i + NForecast].state) || !state.Resize(NForecast * BarDescr) || MathAbs(state).Sum() == 0) { iter -= Batch + start - i; break; } if(!fstate.Resize(1, NForecast * BarDescr) || !fstate.Row(state, 0) || !fstate.Reshape(NForecast, BarDescr)) { iter -= Batch + start - i; break; } for(int j = 0; j < NForecast / 2; j++) { if(!fstate.SwapRows(j, NForecast - j - 1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } }
これらのデータは、環境状態エンコーダの目標値として使用されます。その結果、このモデルに対して逆伝播を実行し、パラメータを最適化するとともに、予測値と目標値との差を最小化できます。
//--- State Encoder Result.AssignArray(fstate); if(!cEncoder.backProp(Result, (CBufferFloat*)NULL, NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
将来の環境状態に関する情報は、「準完全」取引操作テンソルの構築にも利用されます。重要なのは、各新しい取引操作が直前の操作を考慮して生成される点です。これにより、単独の衝動的な行動ではなく、一貫した取引戦略をモデル化できます。その結果、文脈上一貫した取引系列が形成され、各後続行動は直前の行動の論理によって決定されます。これは、エージェントが環境からリアルタイムのフィードバックを受け取らず、記録済みデータからパターンを抽出しなければならないオフライン学習において特に重要です。
この方法により、実際の市場データに存在するランダムな変動として現れる市場ノイズの影響を軽減できます。「準完全軌跡」内では、エージェントの行動はより滑らかで、合理的、そして戦略的に一貫したものになります。これにより、モデル学習を加速すると同時に、後のオンライン運用時に適応可能な頑健な取引ロジックを形成できる可能性があります。
「準完全」取引操作の構築方法については前回の記事で詳細に説明しているため、本稿では繰り返しません。
生成された「準完全」取引操作テンソルは、高レベルActorモジュールの学習目標値として使用されます。
//--- Actor Policy if(!cActor.backProp(GetPointer(bActions), (CNet*)GetPointer(cTask), -1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
ここから、学習アーキテクチャにおける最も重要なコンポーネント、すなわちActorの行動評価モデルであるCriticとDirectorの学習へ移ります。これらのコンポーネントは、エージェントの戦略を導くフィードバック信号を生成し、有効な行動と効果の低い行動を区別する役割を担います。
しかし、ここで根本的な課題に直面します。任意の環境状態における可能な行動空間は非常に大きく、Actorが取り得るすべての意思決定を評価することは、現実的な計算資源の範囲内では不可能です。さらに、これらの行動の多くは実際にはエージェントによって実行されることがなく、そのため学習価値もほとんどありません。
この理由から、Criticの学習では通常、局所的な評価戦略を採用します。行動空間全体を探索するのではなく、現在時刻においてエージェントが実際に選択した行動周辺の近傍領域に焦点を当てます。この局所部分空間内で、目的関数を増加させ、結果として収益性を改善する可能性のある最適化方向を探索します。
ここで、「準完全軌跡」アプローチが大きな利点を提供します。将来状態への限定的な参照によって生成された基準行動を利用できるため、評価対象を「エージェントが実際におこなったこと」から「エージェントがおこなうべきだったこと」へ移すことができます。つまり、Criticは単に良い行動と悪い行動を区別するだけではなく、「準完全軌跡」によって表現された戦略的理想に近い行動へ評価を向けるよう学習されます。
このパラダイムにおいて、Criticを通して順伝播を実行し、「準完全」取引操作を評価します。
//--- Critic if(!cCritic.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
その後、Criticの評価と、実際の市場履歴データから計算された値との偏差を最小化するため、直ちに逆伝播を実行します。
float reward = float((result[0] - result[3]) * fstate[0, 0] / Point()); Result.Clear(); if(!Result.Add(reward) || !cCritic.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer) || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
Directorの学習プロセスは、概念的にはCriticと似ていますが、いくつか重要な違いがあります。分類器であるDirectorは、エージェントの「良い」行動と「悪い」行動の境界を学習するため、正例と負例のバランスの取れたデータセットを必要とします。
正例の構築は比較的単純です。それらは、「準完全軌跡」の枠組みで生成された取引操作です。これらの行動は、将来の環境状態を分析して導かれた戦略的に妥当な意思決定であり、肯定的な分類対象となります。
しかし、信頼性の高い分類器は、正例だけでは学習できません。目的に反する行動を示す代表的な負例も必要です。ここで方法論的な問題が発生します。学習データセットは、本質的に可能な行動空間全体をカバーしていません。そこに含まれるのは、過去にエージェントが実際に選択した行動だけです。
この問題に対処するため、本実装では単純ながら効果的なヒューリスティックを採用しました。負例として、エージェントの行動空間で許容される範囲内にランダム生成した値の集合を使用します。高い確率で、このようなランダム行動はモデルの戦略的目的とは一致せず、行動ノイズまたは誤りとして扱うことができます。
また、学習中の正例と負例の切り替えもランダム化されています。これにより、モデルがどちらか一方のカテゴリへ過適合することを防ぎ、より頑健な分類境界を形成できます。この手法により、Directorはより柔軟で汎化能力の高いモデルとなり、金融市場特有の高い不確実性の下でも、より明確で信頼性の高いシグナルを生成できます。
その結果、Directorは非生産的な行動を明確に排除し、エージェントをより有望な戦略的意思決定領域へ導く強力な二値ガイドとなります。
//--- Director Result.Clear(); if((MathRand() / 32767.0) > 0.5) Result.Add(1); else { target = vector<float>::Zeros(NActions); for(int i = 0; i < NActions; i++) target[i] = float(MathRand() / 32767.0); bActions.AssignArray(target); Result.Add(0); } if(!cDirector.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer) || !cDirector.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer) || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
次に、低レベルControllerおよび将来価格方向の確率を推定する予測モデルを学習します。これらのコードブロックは、詳細を説明した以前のプロジェクトから変更なしで移植されています。そのため、本記事では説明を省略し、読者自身の研究対象として残します。オフラインモデル学習用EAの完全なソースコードは添付資料で確認できます。
オンライン学習
次の段階では、オンライン環境における学習可能モデルのファインチューニングについて説明します。この段階では、オフライン学習とは異なる種類の課題や制約に直面します。オフライン学習では、履歴データと「準完全軌跡」手法を利用した深い汎化学習に重点を置いていました。一方、オンライン学習では、リアルタイム環境への適応のみを目的とします。
オンライン学習の大きな利点の一つは、環境からのフィードバックを直接利用できる点です。Actorが意思決定を行い、その行動が実行され、その結果が比較的短時間で明らかになります。これにより、エージェントの行動を迅速に調整することが可能となり、有効な戦略を強化し、効果の低い戦略を排除することができます。
しかし、このアプローチには重要な制限も存在します。その中でも最も大きな制約は、オフライン学習時に「準完全軌跡」を構築する際に利用できた未来状態への参照が、オンライン環境では不可能になる点です。オンライン環境では、エージェントは現在の状態と既存の方策のみに基づいて意思決定を行う必要があり、「未来の情報」を利用することはできません。この違いにより、学習条件は大きく変化し、より古典的な強化学習の枠組みへ移行する必要があります。
そのため、学習は試行錯誤によって進められ、意思決定の品質は事後的にのみ評価可能となります。この結果、評価モデルであるCriticとDirectorの重要性が大きく高まります。これらのコンポーネントはActorのための内部的な判断支援機構として機能し、その行動を方向付けます。ただし、オフライン学習段階とは異なり、オンライン環境では実際の取引活動を通じて継続的にファインチューニングされ、変化する市場環境へリアルタイムに適応します。
ここから実装内容を段階的に確認します。オンライン学習アルゴリズムは、EA ...\Experts\ADC\StudyOnline.mq5に実装されています。本稿では紙面の都合上、OnTickメソッドの詳細な解析にのみ焦点を当てます。このメソッドは受信したティックイベントを処理し、主要な学習アルゴリズムを実行する場所となります。
void OnTick() { //--- if(!IsNewBar()) return;
まず重要な点として、本モデルは確定済みローソク足の履歴データのみを解析し、すべてのティックごとに反応するようには設計されていません。そのため、新しいローソク足が確定するまで詳細な環境状態の解析を行う必要はありません。なぜなら、その結果は変化しないためです。不要な計算量を削減するため、メソッドの開始時には新しいバーが形成されたかどうかを確認します。新しいバーが存在しない場合、次のティックまで待機します。
新しいローソク足が形成されると、指定された期間分の履歴データをターミナルから取得し、入力データバッファを構築します。
int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, 1), HistoryBars, Rates); if(!ArraySetAsSeries(Rates, true)) return; //--- RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); Symb.Refresh(); Symb.RefreshRates(); //--- float atr = 0; for(int b = 0; b < (int)HistoryBars; b++) { float open = (float)Rates[b].open; float rsi = (float)RSI.Main(b); float cci = (float)CCI.Main(b); atr = (float)ATR.Main(b); float macd = (float)MACD.Main(b); float sign = (float)MACD.Signal(b); if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) continue; //--- int shift = b * BarDescr; sState.state[shift] = (float)(Rates[b].close - open); sState.state[shift + 1] = (float)(Rates[b].high - open); sState.state[shift + 2] = (float)(Rates[b].low - open); sState.state[shift + 3] = (float)(Rates[b].tick_volume / 1000.0f); sState.state[shift + 4] = rsi; sState.state[shift + 5] = cci; sState.state[shift + 6] = atr; sState.state[shift + 7] = macd; sState.state[shift + 8] = sign; } //---
次に、現在の口座状態および保有ポジションに関する情報を読み込みます。
sState.account[0] = (float)AccountInfoDouble(ACCOUNT_BALANCE); sState.account[1] = (float)AccountInfoDouble(ACCOUNT_EQUITY); //--- double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0; double position_discount = 0; double multiplyer = 1.0 / (60.0 * 60.0 * 10.0); int total = PositionsTotal(); datetime current = TimeCurrent(); for(int i = 0; i < total; i++) { if(PositionGetSymbol(i) != Symb.Name()) continue; double profit = PositionGetDouble(POSITION_PROFIT); switch((int)PositionGetInteger(POSITION_TYPE)) { case POSITION_TYPE_BUY: buy_value += PositionGetDouble(POSITION_VOLUME); buy_profit += profit; break; case POSITION_TYPE_SELL: sell_value += PositionGetDouble(POSITION_VOLUME); sell_profit += profit; break; } position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) * multiplyer * MathAbs(profit); } sState.account[2] = (float)buy_value; sState.account[3] = (float)sell_value; sState.account[4] = (float)buy_profit; sState.account[5] = (float)sell_profit; sState.account[6] = (float)position_discount; sState.account[7] = (float)Rates[0].time;
続いて、タイムスタンプのハーモニクス表現を生成します。
bTime.Clear(); double time = (double)Rates[0].time; 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(); //--- bAccount.Clear(); bAccount.Add((float)((sState.account[0] - PrevBalance) / PrevBalance)); bAccount.Add((float)(sState.account[1] / PrevBalance)); bAccount.Add((float)((sState.account[1] - PrevEquity) / PrevEquity)); bAccount.Add(sState.account[2]); bAccount.Add(sState.account[3]); bAccount.Add((float)(sState.account[4] / PrevBalance)); bAccount.Add((float)(sState.account[5] / PrevBalance)); bAccount.Add((float)(sState.account[6] / PrevBalance)); bAccount.AddArray(GetPointer(bTime)); //--- if(bAccount.GetIndex() >= 0) if(!bAccount.BufferWrite()) return; //--- bState.AssignArray(sState.state);
ここで重要なのは、生成された現在環境状態の表現が2つの異なる目的で使用される点です。当然ながら、この表現はエージェントに対するフォワードパスの入力として利用され、新しい取引アクションを生成します。しかし、そのアクションに対する環境からの報酬は、次のローソク足が形成された後で初めて取得できます。つまり、時間的な遅延が存在します。
一方で、この時点では、エージェントが前回の時間ステップで実行した行動の有効性を評価することが可能です。そのため、前回の環境状態を解析した結果を保持しているモデル状態を更新する前に、この評価を実行することが望ましいと考えられます。
そこでまず、新たに構築した環境状態表現をターゲットモデルへ入力し、エージェントが現在の方策に従って行動した場合の状態評価を取得します。
if(!bFirstRun) { //--- Target Nets if(!cEncoder[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !cTask[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(cEncoder[1]), LatentLayer) || !cActor[1].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, GetPointer(cTask[1]), -1) || !cCritic[2].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer) || !cCritic[3].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer) || !cCritic[4].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer) || !cCritic[5].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
ターゲットモデルを利用することは、市場ノイズの影響を抑えながら、一貫性と安定性を持った行動戦略を維持する上で重要な役割を果たします。これにより、エージェントは即時報酬だけでなく、将来的な期待リターンも考慮できるようになります。
次に、Criticの学習へ進みます。Actor —Director—Criticフレームワークの提案者は、2つのCriticを並列で使用し、それぞれに2つのターゲットモデルを対応させる方式を提案しました。まず、エージェントの直近の行動を評価するためのターゲット値を構築します。この際、最初のCriticについて現在得られた報酬を考慮し、その後モデルに対してフォワードパスとバックプロパゲーションを実行します。
//--- Critic 1 cCritic[2].getResults(Result); float reward = Result[0]; cCritic[4].getResults(Result); reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity); Result.Clear(); if(!Result.Add(reward) || !cCritic[0].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
ここで注意すべき点として、元のフレームワークでは実行された行動に対する最小評価値を基準として誤差勾配を伝播させます。しかし、本実装では若干異なる方式を採用しています。最小評価値ではなく、平均評価誤差が最も小さいモデルから誤差勾配を伝播します。つまり、最も保守的な評価ではなく、より正確な評価を利用する方式へ変更しています。
オンライン学習におけるもう一つの重要な要素は、Actor方策更新のタイミングです。最も単純な方法は固定された反復回数カウンタを使用することです。一定回数のステップ後にエージェントの戦略を更新する方式です。この方法は、各環境状態が一意であり再利用できない完全なオンライン学習環境では合理的です。
しかし、本実装ではMetaTrader 5ストラテジーテスターの強力なシミュレーション機能を利用します。これにより同じイベント系列を何度も再生でき、オンライン学習を模擬しながら複数回の学習を実行できます。
ここで問題が発生します。単純な固定更新スケジュールを使用すると、毎回の学習でActorの方策更新がまったく同じ環境状態で発生します。これは学習プロセスの多様性を低下させ、人工的なバイアスを導入し、エージェントがより一般的なパターンを学習することを妨げます。
この問題を回避するため、本実装ではポリシー更新タイミングを確率的に決定する仕組みを導入しました。固定カウンタを使用する代わりに乱数を生成し、その値があらかじめ設定した数値で割り切れる場合のみ方策を更新します。この方法により、必要な更新頻度を維持しながら、更新タイミングを予測困難にすることができます。その結果、特定のデータ区間への過学習リスクを低減できます。
if(cCritic[0].getRecentAverageError() <= cCritic[1].getRecentAverageError() && (MathRand() % ActorUpdate) == 0) if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
同じ処理を2つ目のCriticに対しても繰り返します。
//--- Critic 2 cCritic[3].getResults(Result); reward = Result[0]; cCritic[5].getResults(Result); reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity); Result.Clear(); if(!Result.Add(reward) || !cCritic[1].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; } if(cCritic[0].getRecentAverageError() > cCritic[1].getRecentAverageError() && (MathRand() % ActorUpdate) == 0) if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
Directorの学習プロセスは、Criticと比較するとかなり単純です。利益を生み出した取引操作は正例として扱い、それ以外の取引は負例として扱います。
//--- Director Result.Clear(); if((sState.account[1] - PrevEquity) > 0) Result.Add(1); else Result.Add(0); if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
ここでは、直近で確定したローソク足の方向を予測するモデルのパラメータ更新も行います。
//--- Probability vector<float> target = vector<float>::Zeros(NActions / 3); if(sState.state[0] > 0) target[0] = 1; else if(sState.state[0] < 0) target[1] = 1; if(!Result.AssignArray(target) || !cProbability.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cEncoder[0].backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; } }
モデル最適化の反復処理が完了すると、新しい取引操作の生成へ進みます。先ほど構築した現在環境状態の表現を、Actorの行動評価モデルを含む学習可能モデルへ入力します。
//--- New state if(!cEncoder[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !cTask[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cActor[0].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, (CNet*)GetPointer(cTask[0]), -1) || !cProbability.feedForward((CNet*)GetPointer(cEncoder[0]), LatentLayer, (CBufferFloat*)NULL) || !cDirector.feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cCritic[0].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cCritic[1].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
次のローソク足処理時に必要となる情報をグローバル変数へ保存します。
PrevBalance = sState.account[0]; PrevEquity = sState.account[1];
その後、取引処理を実行します。まずActorが生成した出力ベクトルを取得します。
vector<float> temp; cActor[0].getResults(temp); //--- if(temp.Size() < NActions) temp = vector<float>::Zeros(NActions);
生成されたテンソルから、互いに相殺されるポジション量を除去します。
double min_lot = Symb.LotsMin(); double step_lot = Symb.LotsStep(); double stops = (MathMax(Symb.StopsLevel(), 1) + Symb.Spread()) * Symb.Point(); if(temp[0] >= temp[3]) { temp[0] -= temp[3]; temp[3] = 0; } else { temp[3] -= temp[0]; temp[0] = 0; }
続いてActorの出力を実際の取引指示へ変換します。ロングポジション量が存在しない場合、既存のロングポジションはすべて決済されます。
//--- buy control if(temp[0] < min_lot || (temp[1] * MaxTP * Symb.Point()) <= 2 * stops || (temp[2] * MaxSL * Symb.Point()) <= stops) { if(buy_value > 0) CloseByDirection(POSITION_TYPE_BUY); }
ロングポジションを新規に保有、または維持する必要がある場合、生成された値を実際の取引数量および価格レベルへ変換します。
else { double buy_lot = min_lot + MathRound((double)(temp[0] - min_lot) / step_lot) * step_lot; double buy_tp = NormalizeDouble(Symb.Ask() + temp[1] * MaxTP * Symb.Point(), Symb.Digits()); double buy_sl = NormalizeDouble(Symb.Ask() - temp[2] * MaxSL * Symb.Point(), Symb.Digits());
既存ポジションが存在する場合、対応する取引レベルをトレーリング処理によって調整します。
if(buy_value > 0) TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);
その後、ポジション数量を調整します。必要に応じて部分決済するか、不足分を追加します。後者にはポジションの新規建ても含まれます。
if(buy_value != buy_lot) { if((buy_value - buy_lot) >= min_lot) ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot); else if((buy_lot - buy_value) >= min_lot) if(!Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp)) if(Trade.CheckResultRetcode() == 10019) { Result.Clear(); Result.Add(0); if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; } } } }
ここで重要なのは、ポジションの新規建てまたは既存ポジションの追加時に「資金不足」エラーが発生した場合、その結果を即座にDirectorへフィードバックする点です。つまり、その意思決定は負例として分類されるべきであることをDirectorに伝えます。
ショートポジションについても同様の処理を行います。
//--- sell control if(temp[3] < min_lot || (temp[4] * MaxTP * Symb.Point()) <= 2 * stops || (temp[5] * MaxSL * Symb.Point()) <= stops) { if(sell_value > 0) CloseByDirection(POSITION_TYPE_SELL); } else { double sell_lot = min_lot + MathRound((double)(temp[3] - min_lot) / step_lot) * step_lot;; double sell_tp = NormalizeDouble(Symb.Bid() - temp[4] * MaxTP * Symb.Point(), Symb.Digits()); double sell_sl = NormalizeDouble(Symb.Bid() + temp[5] * MaxSL * Symb.Point(), Symb.Digits()); if(sell_value > 0) TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp); if(sell_value != sell_lot) { if((sell_value - sell_lot) >= min_lot) ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot); else if((sell_lot - sell_value) >= min_lot) if(!Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp)) if(Trade.CheckResultRetcode() == 10019) { Result.Clear(); Result.Add(0); if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; } } } }
最後に、ターゲットモデルを更新するタイミングかどうかを確認します。必要に応じてソフトアップデート処理を実行し、学習モデルのパラメータを対応するターゲットネットワークへコピーします。
bFirstRun = false; //--- if((int(Rates[0].time / PeriodSeconds(TimeFrame)) % TragetUpdate) == 0) { if(MathRand() / 32767.0 > 0.5) cCritic[2].WeightsUpdate(GetPointer(cCritic[0]), tau); else cCritic[4].WeightsUpdate(GetPointer(cCritic[0]), tau); if(MathRand() / 32767.0 > 0.5) cCritic[3].WeightsUpdate(GetPointer(cCritic[1]), tau); else cCritic[5].WeightsUpdate(GetPointer(cCritic[1]), tau); cEncoder[1].WeightsUpdate(GetPointer(cEncoder[0]), tau); cTask[1].WeightsUpdate(GetPointer(cTask[0]), tau); cActor[1].WeightsUpdate(GetPointer(cActor[0]), tau); } if(PrevBalance < 50) ExpertRemove(); }
これでメソッドの処理は終了し、次のローソク足確定まで待機します。
オンライン学習用EAの完全なソースコードは、添付ファイルで提供されています。
テスト
Actor–Director–Criticフレームワークの中核となる概念をMQL5上で適応・実装し、その各コンポーネントを学習可能モデルのアーキテクチャへ統合するために、私たちは多くの労力を費やしてきました。Actor、Director、Critic間の相互作用ロジックは慎重に設計され、さらにエージェント学習に関するいくつかの独自手法も実装しました。そして現在、最終段階であり、おそらく最も緊張を伴う段階へ進みます。それは、実際の市場履歴データを用いて提案したソリューションの有効性を評価することです。
本フレームワークは、実際の取引環境に近い条件下で、市場履歴データを用いて評価します。これにより、選択したアーキテクチャおよびアルゴリズム的手法が、金融市場のダイナミクスや不確実性に対して実際に対応できるかを客観的に検証できます。さらに、このようなテストによって現在の実装における長所と短所の両方を明らかにでき、今後の改善や最適化の方向性を検討するための指針も得られます。
学習データセットは、MetaTrader 5ストラテジーテスター上でランダムなエージェント実行を行うことによって生成しました。これにより、幅広い行動パターンを含むデータを収集することが可能になりました。このデータセットには、2024年通年のEURUSD M1データが含まれています。
初期モデル学習は、予測誤差が安定するまで学習データセットを更新せず、オフライン環境で実行しました。その後、MetaTrader 5ストラテジーテスターに切り替え、安定した性能が得られるまでモデルパラメータのファインチューニングを継続しました。
学習された取引方策を客観的に評価するためには、学習データに含まれていないデータ上で学習済みモデルを検証する必要があります。性能評価には、2025年1月から3月までの履歴データを使用しました。この期間は学習には一切使用されていないため、過学習の影響を排除でき、得られた結果は実用的な意味を持ちます。
その他すべてのパラメータ、すなわち市場環境、時間足、約定シミュレーションモデル、ターミナル設定などは変更せず、そのまま維持しました。これにより、外部要因による影響を排除し、学習された戦略そのものを純粋に評価することが可能になります。
テスト結果を以下に示します。これらはエージェントが形成した行動モデルをわかりやすく示しています。

テスト期間中、モデルは684件の取引を実行しました。そのうち268件が利益決済となり、勝率は39%をわずかに上回りました。しかしながら、テスト期間全体では利益を生み出すことに成功しました。その理由は、平均利益取引のサイズが平均損失取引の約2倍に達していたということです。
結論
本記事では、Actor–Director–Criticフレームワークの理論的基盤を検討し、その提案された概念をMQL5を用いて独自に実装しました。このフレームワークは既存のマルチエージェントアーキテクチャへ完全に統合され、モジュール性、柔軟性、そして効率的な学習能力を備えたエージェントを構築することができました。このエージェントは、Criticを通じた局所的な行動評価だけでなく、Directorを通じて行動ロジック全体におけるより広範な戦略的文脈も考慮できます。このアプローチにより、Actorに対してより正確かつ安定したフィードバックを提供できるようになり、エージェントは効果の低い行動をより迅速に排除し、より有望な方策空間を効率的に探索できるようになります。
実施したテストでは、提案した手法の有効性が確認され、Actor–Director–Criticフレームワークが市場の不確実性の高い状況においても、よりバランスの取れた意思決定を行い、安定した行動を示す能力を持つことが確認されました。
ただし、本記事で紹介したプログラムは、あくまでフレームワークの能力を示すためのデモンストレーション用サンプルであることに注意する必要があります。提案したソリューションを実際のライブ取引環境で利用する前に、より代表性の高いデータセットによる追加学習を行い、十分な検証および評価プロセスを実施する必要があります。
参照文献
記事で使用されたプログラム
| # | 名前 | 種類 | 詳細 |
|---|---|---|---|
| 1 | Research.mq5 | EA | サンプル収集用EA |
| 2 | ResearchRealORL.mq5 | EA | Real-ORL法を用いたサンプル収集用EA |
| 3 | Study.mq5 | EA | モデルのオフライン学習用EA |
| 4 | StudyOnline.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/17819
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
取引におけるニューラルネットワーク:多変量時系列予測のためのLSTMの最適化(DA-CG-LSTM)
取引におけるニューラルネットワーク:Actor-Director-Critic
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
取引におけるニューラルネットワーク:適応型エージェント行動のための階層的スキル発見(最終章)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
https://nukadeti.ru/basni/krylov-kvartet
無知は無知を生む
https://nukadeti.ru/basni/krylov-kvartet
正しい書き方を教えていただけませんか?