English
preview
MQL5で自己最適化エキスパートアドバイザーを構築する(第15回):線形系同定

MQL5で自己最適化エキスパートアドバイザーを構築する(第15回):線形系同定

MetaTrader 5トレーディングシステム |
28 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

取引システムは、混沌として動的な環境の中で動作することを求められる、極めて複雑なアプリケーションです。これは、最も経験豊富な開発者にとっても大きな挑戦となります。市場の結果は事実上無限に存在するため、取引アプリケーションが取るべき正しい行動をすべて定義することは不可能です。このような不確実性の中で安定性を保ち、継続的な収益性を確保することは、アルゴリズム取引における最大の課題の一つです。

単純な戦略は、穏やかな市場環境では信頼できるように見えるかもしれません。しかし、単純なシステムであれ複雑なシステムであれ、変動性が高まる局面では破綻することが少なくありません。それにもかかわらず、制御理論や信号処理の知見は、この問題に対して十分に活用されているとは言えません。動的かつ不確実な系の安定性を維持することを目的とする制御理論は、アルゴリズム取引が日々直面している課題と極めて高い親和性を持っています。

古典的な制御理論では、入力と出力の関係を明示的な数式で表現できる、第一原理に基づいた系の理解が前提とされます。しかし、現代の金融市場は、そのような明確な数理構造に従いません。このため、制御理論と機械学習を統合し、明示的な方程式に頼ることなく、データから直接その関係を近似しようとする試みへの関心が高まっています。

この考え方は非常に強力です。たとえ正確な制御方程式を知らなくても、データから系の挙動を学習し、制御することが可能になります。制御理論とアルゴリズム取引は、不確実性を管理しながら安定性を維持するという共通の目的を持っています。帰還制御器は価格を予測するものではありません。ノイズに対する過剰反応を抑制し、系の応答を調整することで、安定した性能を維持する役割を果たします。

また、帰還制御は、資本が有効に使用されているかどうかを学習し、不要な取引を抑制することで、資本効率の向上にも寄与します。これを機械学習と組み合わせることで、制御系は自律的に適応する能力を獲得し、精度、制御性、信頼性をさらに高めることができます。これほど明確な共通点があるにもかかわらず、制御理論とアルゴリズム取引の間には、依然として大きな研究上の空白が存在しています。そして、その空白には大きな可能性が秘められています。

本記事では、制御理論がいかにして、最も基本的な取引システムさえも再生させ得るかを示します。具体例として、価格が移動平均を上回れば買い、下回れば売るという単純な移動平均戦略を用います。この戦略はしばしば時代遅れだと見なされますが、帰還制御を導入することで、その安定性と収益性を回復できることを示します。「よく知られすぎているから機能しない」といった主張には、実証的な裏付けが欠けています。本記事のアプローチでは、帰還制御を用いて、この戦略がいつ、なぜ機能し、いつ、なぜ失敗するのかを特定します。

実験では、古典的な移動平均戦略を実装し、すべてのパラメータを固定しました。2年分の履歴データ(2023年1月から2025年5月)を使用し、前半期間で移動平均の期間を最適化し、後半期間で性能を検証することで、比較用の基準を設定しました。この基準を確立した後、帰還制御器はバックテスト中の系の挙動のみから学習をおこない、追加のパラメータ調整は一切おこなっていません。

初期段階では、制御ありと制御なしの両システムは同一の性能を示しました。これは、制御器が観測に専念していたためです。しかし、制御が有効化されると、帰還制御を導入したシステムは顕著な改善を示しました。

  • 総損失は–575ドルから–333ドルへと減少し、資本の非効率な使用が42%削減された
  • 純利益は-49ドルから+57ドルへと増加した
  • 取引数は78件から51件に減少した(効率が34%向上) 
  • 勝率は44%から53%に向上した
  • 利益率は0.91から1.17に上昇し、収益性が28%増加した

これらの結果は、同一の市場条件および同一のシステム制約下で得られたものです。これは、帰還制御が持つ安定化能力を明確に示しています。人間の直感や従来のモデル化手法が限界に達する領域において、制御理論は原理に基づいた前進の道を示してくれます。そして、長らく使い尽くされたと考えられてきた戦略の中に、より深い関係性と未開拓の可能性が存在することを明らかにします。


MQL5で始める

アプリケーション開発を始めるにあたり、まずすべての演習で固定される主要なシステム定数を定義します。将来的なバージョンでは定数の数は増加しますが、各バージョン間でこれらの定数は引き継いで使用する予定です。
//+------------------------------------------------------------------+
//|                                  Feedback Control Benchmark .mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL Symbol()
#define MA_SHIFT 0
#define MA_MODE MODE_EMA
#define MA_APPLIED_PRICE PRICE_CLOSE
#define SYSTEM_TIME_FRAME PERIOD_D1
#define MIN_VOLUME SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN)

このベンチマーク版の目的が、テクニカル指標の初期期間を適切に設定することにある点に注意してください。そのため、チューニング用のパラメータを入力として設定しており、後ほど遺伝的アルゴリズムを用いて最適化する予定です。

//+------------------------------------------------------------------+
//| Tuning parameters                                                |
//+------------------------------------------------------------------+
input group "Technical Indicators"
input int  MA_PERIOD = 10;//Moving average period

次に、この演習で必要となるライブラリを読み込みます。今回はTradeライブラリのみで十分です。 

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

さらに、移動平均(MA)やATRのバッファなど、重要なグローバル変数も定義します。ATRはストップロスやリスク管理の基準となり、すべての演習で共通して使用します。また、市場価格(始値、高値、安値、終値)やテクニカル指標ハンドルもグローバル変数として保持します。

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double ma[],atr[];
double ask,bid,open,high,low,close,padding;
int    ma_handler,atr_handler;

アプリケーションを初めて読み込む際には、移動平均用とATR用のハンドルを初期化します。ATRは市場の変動性を測定し、ストップロスやテイクプロフィットのレベル設定に用います。 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the indicator
   ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE);
   atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14);
   return(INIT_SUCCEEDED);
  }

終了時には、MQL5のベストプラクティスに従い、インジケーターを解放してリソースを返却します。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the indicator
   IndicatorRelease(ma_handler);
   IndicatorRelease(atr_handler);
  }

新しい価格がターミナルに届くたびに、取引ロジックを実行します。まず、新しい日足が形成されたかを確認し、日次で一回だけ取引をおこないます。

取引戦略は、価格が移動平均を上回っているか下回っているかで売買を判断します。ただし、MetaTrader5でバックテストをおこなう場合、初日の日足は0時に形成されることが多く、価格データがフラットで信頼できないことがあります。これを避けるため、このアルゴリズムは前日のローソク足とインジケーターの値を参照します。本質的には、前日の出来事に基づいて当日の売買判断をおこないます。

終値が移動平均より上か下かを確認し、買いか売りかを判断して売買ロジックを実行します。 

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new candle has formed
   datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0);
   static datetime time_stamp;

   if(current_time != time_stamp)
     {
      //--- Update the time
      time_stamp = current_time;

      //--- If we have no open positions
      if(PositionsTotal()==0)
        {
         //--- Update indicator buffers
         CopyBuffer(ma_handler,0,1,1,ma);
         CopyBuffer(atr_handler,0,0,1,atr);
         padding = atr[0] * 2;

         //--- Fetch current market prices
         ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK);
         bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID);
         close = iClose(SYMBOL,SYSTEM_TIME_FRAME,0);

         //--- Check trading signal
         if(close > ma[0])
            Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding);

         if(close < ma[0])
            Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding);
        }
     }
  }
//+------------------------------------------------------------------+

すべての処理が完了した後、以前に定義したシステム定数を#undefで解除します。

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef SYMBOL
#undef SYSTEM_TIME_FRAME
#undef MA_APPLIED_PRICE
#undef MA_MODE
#undef MA_SHIFT
#undef MIN_VOLUME
//+------------------------------------------------------------------+

これらを組み合わせることで、アプリケーションのベンチマーク版が完成します。

//+------------------------------------------------------------------+
//|                                  Feedback Control Benchmark .mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL Symbol()
#define MA_SHIFT 0
#define MA_MODE MODE_EMA
#define MA_APPLIED_PRICE PRICE_CLOSE
#define SYSTEM_TIME_FRAME PERIOD_D1
#define MIN_VOLUME SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN)

//+------------------------------------------------------------------+
//| Tuning parameters                                                |
//+------------------------------------------------------------------+
input group "Technical Indicators"
input int  MA_PERIOD = 10;//Moving average period

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double ma[],atr[];
double ask,bid,open,high,low,close,padding;
int    ma_handler,atr_handler;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the indicator
   ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE);
   atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the indicator
   IndicatorRelease(ma_handler);
   IndicatorRelease(atr_handler);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new candle has formed
   datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0);
   static datetime time_stamp;

   if(current_time != time_stamp)
     {
      //--- Update the time
      time_stamp = current_time;

      //--- If we have no open positions
      if(PositionsTotal()==0)
        {
         //--- Update indicator buffers
         CopyBuffer(ma_handler,0,1,1,ma);
         CopyBuffer(atr_handler,0,0,1,atr);
         padding = atr[0] * 2;

         //--- Fetch current market prices
         ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK);
         bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID);
         close = iClose(SYMBOL,SYSTEM_TIME_FRAME,0);

         //--- Check trading signal
         if(close > ma[0])
            Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding);

         if(close < ma[0])
            Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding);
        }
     }

  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef SYMBOL
#undef SYSTEM_TIME_FRAME
#undef MA_APPLIED_PRICE
#undef MA_MODE
#undef MA_SHIFT
#undef MIN_VOLUME
//+------------------------------------------------------------------+



良好な初期条件の特定

次に、ベンチマーク取引戦略を選択し、バックテスト用の履歴期間を定義します。この演習では、2023年から2025年のデータを使用し、フォワードテストもおこないます。フォワードテストとは、バックテスト期間をいくつかのセグメントに分割して評価する手法で、セグメントの大きさは必ずしも均等である必要はありません。ここではデータセットを半分に分割し、フォワードパラメータを½に設定しました。これにより、遺伝的オプティマイザはデータの前半でパラメータを調整し、後半でその性能を検証できます。後半部分はモデルから隠され、学習終了後にオプティマイザのみが評価用データとして利用します。

図1:最適化手順でのバックテスト期間の選定

フォワードテストの重要性を理解したうえで、戦略を評価するモデル条件を定義します。 ネットワーク制限のため、「実ティックに基づく全ティック」ではなく「全ティック」モードを使用しましたが、後者はより現実的な結果を提供するため、安定した接続環境がある場合は推奨されます。最適化には高速な遺伝的アルゴリズムを使用しましたが、より徹底的に探索する場合は、より遅い完全版を使用することも可能です。入力パラメータは、最小値、最大値、ステップサイズを設定し、オプティマイザの探索範囲を制御しています。

図2:初期指標期間の特定に役立つ高速遺伝的アルゴリズムの選択

また、遅延パラメータをランダム遅延に設定し、市場のリアルなラグをシミュレートしました。 

図3:取引アプリケーションの単純明快なチューニングパラメータ

バックテスト結果は芳しくなく、どの構成も利益を出せませんでした。 

図4:バックテスト結果は不安定で改善が必要

散布図でも、すべての試行で一貫して損失が出ていることが確認されました。

図5:最適化手順の前半では、どの構成も利益を出せなかったことがわかる

驚くべきことに、フォワードテストでは戦略が利益を出す場面があり、特に期間を42に設定した場合に有効でした。最良の結果を選ぶと過学習のリスクがありますが、長期間の評価をおこなうことでその可能性は低くなります。

図6:フォワードテストの結果は、バックテスト結果よりも利益が大きかった

フォワードテストでは、多くの収益性の高い構成が確認され、戦略は特定の条件下で十分に機能する可能性があることが示唆されました。ただし、現状では安定性に欠けます。信頼できる戦略は、時間を通じて一貫した性能を示す必要がありますが、本戦略はそれを満たしていませんでした。

図7:遺伝的オプティマイザーが観測していないデータ期間における戦略のアウトオブサンプル性能の可視化


ベンチマークの確立

ベンチマークとなる収益性を確立するため、まずIDEでコンパイル済みのアプリケーションを選択し、バックテスト期間を指定します。この期間は、以前使用したものと同じで、後ほどフォワードテストにも適用します。

図8:確認済みの最適期間を使用して、取引アプリケーションの完全な履歴バックテストを実行する

42期間の設定で行ったフルバックテストでは、戦略は78回の取引で合計損失559ドルを記録し、損益係数は0.97でした。これは、長期的には資本が減少しており、増加していないことを示しています。

図9:履歴データにおける制御ベンチマークの詳細統計パフォーマンスを確認する

このバージョンの戦略による資産曲線は非常に変動が大きく、全体としてパフォーマンスは低調です。遺伝的オプティマイザが見つけた最良の結果を示すための初期バックテストであっても、2年間の取引を経て、戦略はほとんどトントンでしかありませんでした。

図10:取引アプリケーションの制御設定による資産曲線は非常に不安定である


初期結果の改善

これで、ベンチマークの収益性を改善する準備が整いました。まず、先に定義した定数を拡張する形で、追加のシステム定数を定義します。これらの新しい定数は、モデルが必要とするパラメータを決定します。たとえば、線形帰還系が戦略の挙動を調整する前に収集すべき観測数などです。 
//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL            Symbol()
#define MA_PERIOD         42
#define MA_SHIFT          0
#define MA_MODE           MODE_EMA
#define MA_APPLIED_PRICE  PRICE_CLOSE
#define SYSTEM_TIME_FRAME PERIOD_D1
#define MIN_VOLUME        SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN)
#define OBSERVATIONS      90
#define FEATURES          7
#define MODEL_INPUTS      8  

また、線形系からの予測結果や、その入力とターゲット、過去の観測値を格納するためのグローバル変数も定義します。

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double ma[],atr[];
double ask,bid,open,high,low,close,padding;
int    ma_handler,atr_handler,scenes;
bool   forecast;
matrix snapshots,b,X,y,U,S,VT,current_forecast;
vector s;

EAの初期化関数もわずかに変更し、これらの変数を準備します。 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the indicator
   ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE);
   atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14);

//--- Prepare global variables
   forecast = false;
   snapshots = matrix::Zeros(FEATURES,OBSERVATIONS);
   scenes = -1;
   return(INIT_SUCCEEDED);
  }

価格レベルが更新されるたびに、取引ロジックは線形帰還系による予測が必要かどうかを確認します。システムがまだ観測を収集中の場合は予測をスキップし、以前のロジックに基づいて取引を実行します。十分な観測が収集されると、予測が有効化されます。ポジションの有無にかかわらず、システムは新しいローソク足ごとに定期的にスナップショットを記録し、線形帰還系の状態を時間足上で保存します。

ポジションが開いている場合は、線形系の予測メソッドを呼び出して次ステップの口座残高を推定します。この予測は、将来的な決済タイミングの判断に活用される可能性があります。現時点での目的は、単純に線形帰還系が戦略の挙動を制御できるかを観察することです。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new candle has formed
   datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0);
   static datetime time_stamp;

   if(current_time != time_stamp)
     {
      //--- Update the time
      time_stamp = current_time;
      scenes = scenes+1;

      //--- Check how many scenes have elapsed
      if(scenes == (OBSERVATIONS-1))
        {
         forecast   = true;
        }

      //--- If we have no open positions
      if(PositionsTotal()==0)
        {
         //--- Update indicator buffers
         CopyBuffer(ma_handler,0,1,1,ma);
                     CopyBuffer(atr_handler,0,0,1,atr);
            padding = atr[0] * 2;

         //--- Fetch current market prices
         ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK);
         bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID);
         close = iClose(SYMBOL,SYSTEM_TIME_FRAME,1);

         //--- Do we need to forecast?
         if(!forecast)
           {
            //--- Check trading signal
            check_signal();
           }

         //--- We need a forecast
         else
            if(forecast)
              {
               model_forecast();
              }
        }

      //--- Take a snapshot
      if(!forecast)
         take_snapshot();

      //--- Otherwise, we have positions open
      else
        {
         //--- Let the model decide if we should close or hold our position
         if(forecast)
            model_forecast();

         //--- Otherwise record all observations on the performance of the application
         else
            if(!forecast)
               take_snapshot();
        }
     }
  }
//+------------------------------------------------------------------+

取引ロジックはCheckSignalメソッドに分離されました。ルール自体は以前と同様で、ポジションが開いていない場合は、終値が移動平均より上なら買い、下なら売りを実行します。

//+------------------------------------------------------------------+
//| Check for our trading signal                                     |
//+------------------------------------------------------------------+
void check_signal(void)
  {
   if(PositionsTotal() == 0)
     {
      if(close > ma[0])
        {
         Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding);
        }

      if(close < ma[0])
        {
         Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding);
        }
     }
  }

予測を取得する際は、スナップショットを準備して更新します。既存のスナップショットをコピーした後、最新の値を追加し、線形系の入力(X)とターゲット(y)を設定します。Xの1行目は切片用の1ベクトル、残りの行には観測値が格納されます。ターゲットはスナップショットの1ステップ先の口座残高です。

次に、特異値分解(SVD)をおこないます。SVDは教師なし手法の一つで、行列を一連のランク1成分に分解し、データ中の支配的な相関構造を明らかにします。アルゴリズムは1つのベクトルと2つの行列を返し、これを用いて線形系を再構築します。返された特異値ベクトルsは、Diag()メソッドを用いて対角行列Sに変換しそのランクを確認します。ランクが0でなければ、擬似逆行列を計算してシステムの係数を推定し、その値をbに格納します。

次に、現在の市場入力を取得し、それに係数bを行列積として適用することで、線形系からの予測値を算出します。予測された口座残高が現在の残高を上回る場合は、システムは取引を実行します。そうでない場合は、より良い市場条件が整うまで待機します。最後に、行列の逆行列計算でエラーが発生する可能性があるケースをチェックします。

//+------------------------------------------------------------------+
//| Obtain a forecast from our model                                 |
//+------------------------------------------------------------------+
void model_forecast(void)
  {

   Print(scenes);
   Print(snapshots);

//--- Create a copy of the current snapshots
   matrix temp;
   temp.Copy(snapshots);
   snapshots = matrix::Zeros(FEATURES,scenes+1);

   for(int i=0;i<FEATURES;i++)
     {
      snapshots.Row(temp.Row(i),i);
     }

//--- Attach the latest readings to the end
   take_snapshot();

//--- Obtain a forecast for our trading signal
//--- Define the model inputs and outputs

//--- Implement the inputs and outputs
   X = matrix::Zeros(FEATURES+1,scenes);
   y = matrix::Zeros(1,scenes);

//--- The first row is the intercept.
   X.Row(vector::Ones(scenes),0);

//--- Filling in the remaining rows
   for(int i =0; i<scenes;i++)
     {
      //--- Filling in the inputs
      X[1,i] = snapshots[0,i]; //Open
      X[2,i] = snapshots[1,i]; //High
      X[3,i] = snapshots[2,i]; //Low
      X[4,i] = snapshots[3,i]; //Close
      X[5,i] = snapshots[4,i]; //Moving average
      X[6,i] = snapshots[5,i]; //Account equity
      X[7,i] = snapshots[6,i]; //Account balance

      //--- Filling in the target
      y[0,i] = snapshots[6,i+1];//Future account balance
     }

   Print("Finished implementing the inputs and target: ");
   Print("Snapshots:\n",snapshots);
   Print("X:\n",X);
   Print("y:\n",y);

//--- Singular value decomposition
   X.SingularValueDecompositionDC(SVDZ_S,s,U,VT);

//--- Transform s to S, that is the vector to a diagonal matrix
   S = matrix::Zeros(s.Size(),s.Size());
   S.Diag(s,0);

//--- Done
   Print("U");
   Print(U);
   Print("S");
   Print(s);
   Print(S);
   Print("VT");
   Print(VT);

//--- Learn the system's coefficients

//--- Check if S is invertible
   if(S.Rank() != 0)
     {
      //--- Invert S
      matrix S_Inv = S.Inv();
      Print("S Inverse: ",S_Inv);

      //--- Obtain psuedo inverse solution
      b = VT.Transpose().MatMul(S_Inv);
      b = b.MatMul(U.Transpose());
      b = y.MatMul(b);

      //--- Prepare the current inputs
      matrix inputs = matrix::Ones(MODEL_INPUTS,1);
      for(int i=1;i<MODEL_INPUTS;i++)
        {
         inputs[i,0] = snapshots[i-1,scenes];
        }

      //--- Done
      Print("Coefficients:\n",b);
      Print("Inputs:\n",inputs);
      current_forecast = b.MatMul(inputs);
      Print("Forecast:\n",current_forecast[0,0]);

      //--- The next trade may be expected to be profitable
      if(current_forecast[0,0] > AccountInfoDouble(ACCOUNT_BALANCE))
        {
         //--- Feedback
         Print("Next trade expected to be profitable. Checking for trading singals.");
         //--- Check for our trading signal
         check_signal();
        }
        
        //--- Next trade may be expected to be unprofitable
        else
         {
            Print("Next trade expected to be unprofitable. Waiting for better market conditions");
         }
     }

//--- S is not invertible!
   else
     {
      //--- Error
      Print("[Critical Error] Singular values are not invertible.");
     }
  }

また、システムのスナップショットを記録するメソッドも定義します。各スナップショットは関心のある値を行列に格納します(MQL5では、行列は行→列の順で参照される点に注意してください)。

//+------------------------------------------------------------------+
//| Take a snapshot of the market                                    |
//+------------------------------------------------------------------+
void take_snapshot(void)
  {
//--- Record system state
   snapshots[0,scenes]=iOpen(SYMBOL,SYSTEM_TIME_FRAME,1); //Open
   snapshots[1,scenes]=iHigh(SYMBOL,SYSTEM_TIME_FRAME,1); //High
   snapshots[2,scenes]=iLow(SYMBOL,SYSTEM_TIME_FRAME,1);  //Low
   snapshots[3,scenes]=iClose(SYMBOL,SYSTEM_TIME_FRAME,1);//Close
   snapshots[4,scenes]=ma[0];                             //Moving average
   snapshots[5,scenes]=AccountInfoDouble(ACCOUNT_EQUITY); //Equity
   snapshots[6,scenes]=AccountInfoDouble(ACCOUNT_BALANCE);//Balance

   Print("Scene: ",scenes);
   Print(snapshots);
  }

アプリケーションの実行が完了すると、すべてのシステム定数は未定義になります。

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef SYMBOL
#undef SYSTEM_TIME_FRAME
#undef MA_APPLIED_PRICE
#undef MA_MODE
#undef MA_SHIFT
#undef MIN_VOLUME
#undef MODEL_INPUTS
#undef FEATURES
#undef OBSERVATIONS
//+------------------------------------------------------------------+
これらを組み合わせることで、線形帰還系バージョンの取引戦略が完成します。
//+------------------------------------------------------------------+
//|                                  Feedback Control Benchmark .mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL Symbol()
#define MA_PERIOD 42
#define MA_SHIFT 0
#define MA_MODE MODE_EMA
#define MA_APPLIED_PRICE PRICE_CLOSE
#define SYSTEM_TIME_FRAME PERIOD_D1
#define MIN_VOLUME SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN)
#define OBSERVATIONS 90
#define FEATURES     7
#define MODEL_INPUTS 8  

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double ma[],atr[];
double ask,bid,open,high,low,close,padding;
int    ma_handler,atr_handler,scenes;
bool   forecast;
matrix snapshots,b,X,y,U,S,VT,current_forecast;
vector s;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the indicator
   ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE);
   atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14);

//--- Prepare global variables
   forecast = false;
   snapshots = matrix::Zeros(FEATURES,OBSERVATIONS);
   scenes = -1;
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the indicator
   IndicatorRelease(ma_handler);
   IndicatorRelease(atr_handler);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new candle has formed
   datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0);
   static datetime time_stamp;

   if(current_time != time_stamp)
     {
      //--- Update the time
      time_stamp = current_time;
      scenes = scenes+1;

      //--- Check how many scenes have elapsed
      if(scenes == (OBSERVATIONS-1))
        {
         forecast   = true;
        }

      //--- If we have no open positions
      if(PositionsTotal()==0)
        {
         //--- Update indicator buffers
         CopyBuffer(ma_handler,0,1,1,ma);
                     CopyBuffer(atr_handler,0,0,1,atr);
            padding = atr[0] * 2;

         //--- Fetch current market prices
         ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK);
         bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID);
         close = iClose(SYMBOL,SYSTEM_TIME_FRAME,1);

         //--- Do we need to forecast?
         if(!forecast)
           {
            //--- Check trading signal
            check_signal();
           }

         //--- We need a forecast
         else
            if(forecast)
              {
               model_forecast();
              }
        }

      //--- Take a snapshot
      if(!forecast)
         take_snapshot();

      //--- Otherwise, we have positions open
      else
        {
         //--- Let the model decide if we should close or hold our position
         if(forecast)
            model_forecast();

         //--- Otherwise record all observations on the performance of the application
         else
            if(!forecast)
               take_snapshot();
        }
     }
  }
//+------------------------------------------------------------------+


//+------------------------------------------------------------------+
//| Check for our trading signal                                     |
//+------------------------------------------------------------------+
void check_signal(void)
  {
   if(PositionsTotal() == 0)
     {
      if(close > ma[0])
        {
         Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding);
        }

      if(close < ma[0])
        {
         Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding);
        }
     }
  }

//+------------------------------------------------------------------+
//| Obtain a forecast from our model                                 |
//+------------------------------------------------------------------+
void model_forecast(void)
  {

   Print(scenes);
   Print(snapshots);

//--- Create a copy of the current snapshots
   matrix temp;
   temp.Copy(snapshots);
   snapshots = matrix::Zeros(FEATURES,scenes+1);

   for(int i=0;i<FEATURES;i++)
     {
      snapshots.Row(temp.Row(i),i);
     }

//--- Attach the latest readings to the end
   take_snapshot();

//--- Obtain a forecast for our trading signal
//--- Define the model inputs and outputs

//--- Implement the inputs and outputs
   X = matrix::Zeros(FEATURES+1,scenes);
   y = matrix::Zeros(1,scenes);

//--- The first row is the intercept.
   X.Row(vector::Ones(scenes),0);

//--- Filling in the remaining rows
   for(int i =0; i<scenes;i++)
     {
      //--- Filling in the inputs
      X[1,i] = snapshots[0,i]; //Open
      X[2,i] = snapshots[1,i]; //High
      X[3,i] = snapshots[2,i]; //Low
      X[4,i] = snapshots[3,i]; //Close
      X[5,i] = snapshots[4,i]; //Moving average
      X[6,i] = snapshots[5,i]; //Account equity
      X[7,i] = snapshots[6,i]; //Account balance

      //--- Filling in the target
      y[0,i] = snapshots[6,i+1];//Future account balance
     }

   Print("Finished implementing the inputs and target: ");
   Print("Snapshots:\n",snapshots);
   Print("X:\n",X);
   Print("y:\n",y);

//--- Singular value decomposition
   X.SingularValueDecompositionDC(SVDZ_S,s,U,VT);

//--- Transform s to S, that is the vector to a diagonal matrix
   S = matrix::Zeros(s.Size(),s.Size());
   S.Diag(s,0);

//--- Done
   Print("U");
   Print(U);
   Print("S");
   Print(s);
   Print(S);
   Print("VT");
   Print(VT);

//--- Learn the system's coefficients

//--- Check if S is invertible
   if(S.Rank() != 0)
     {
      //--- Invert S
      matrix S_Inv = S.Inv();
      Print("S Inverse: ",S_Inv);

      //--- Obtain psuedo inverse solution
      b = VT.Transpose().MatMul(S_Inv);
      b = b.MatMul(U.Transpose());
      b = y.MatMul(b);

      //--- Prepare the current inputs
      matrix inputs = matrix::Ones(MODEL_INPUTS,1);
      for(int i=1;i<MODEL_INPUTS;i++)
        {
         inputs[i,0] = snapshots[i-1,scenes];
        }

      //--- Done
      Print("Coefficients:\n",b);
      Print("Inputs:\n",inputs);
      current_forecast = b.MatMul(inputs);
      Print("Forecast:\n",current_forecast[0,0]);

      //--- The next trade may be expected to be profitable
      if(current_forecast[0,0] > AccountInfoDouble(ACCOUNT_BALANCE))
        {
         //--- Feedback
         Print("Next trade expected to be profitable. Checking for trading singals.");
         //--- Check for our trading signal
         check_signal();
        }
        
        //--- Next trade may be expected to be unprofitable
        else
         {
            Print("Next trade expected to be unprofitable. Waiting for better market conditions");
         }
     }

//--- S is not invertible!
   else
     {
      //--- Error
      Print("[Critical Error] Singular values are not invertible.");
     }
  }

//+------------------------------------------------------------------+
//| Take a snapshot of the market                                    |
//+------------------------------------------------------------------+
void take_snapshot(void)
  {
//--- Record system state
   snapshots[0,scenes]=iOpen(SYMBOL,SYSTEM_TIME_FRAME,1); //Open
   snapshots[1,scenes]=iHigh(SYMBOL,SYSTEM_TIME_FRAME,1); //High
   snapshots[2,scenes]=iLow(SYMBOL,SYSTEM_TIME_FRAME,1);  //Low
   snapshots[3,scenes]=iClose(SYMBOL,SYSTEM_TIME_FRAME,1);//Close
   snapshots[4,scenes]=ma[0];                             //Moving average
   snapshots[5,scenes]=AccountInfoDouble(ACCOUNT_EQUITY); //Equity
   snapshots[6,scenes]=AccountInfoDouble(ACCOUNT_BALANCE);//Balance

   Print("Scene: ",scenes);
   Print(snapshots);
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef SYMBOL
#undef SYSTEM_TIME_FRAME
#undef MA_APPLIED_PRICE
#undef MA_MODE
#undef MA_SHIFT
#undef MIN_VOLUME
#undef MODEL_INPUTS
#undef FEATURES
#undef OBSERVATIONS
//+------------------------------------------------------------------+

同じバックテスト期間で実行すると、収益性に大きな改善が見られました。 

図11:線形帰還系を用いた改善ベンチマークの確立

システムは一貫した損失状態から、持続的な収益性へと変化しました。勝率は40%台半ばから50%以上に上昇し、取引回数は減少して効率が向上しています。シャープレシオやリカバリーファクターも大幅に改善しました。これらの改善は、アプリケーションに具体的な動作指示を与えることなく達成されています。

図12:観測データから特定した線形帰還系による改善の詳細分析

新しい資産曲線は以前の不安定さを置き換え、初期のドローダウンも後続期間では再発しません。損失が続く期間であっても、以前のように深く落ち込むことはなくなっています。

図13:改良版取引アプリケーションによる資産曲線の可視化



結論

本日の解説はこれで終了です。本記事では、線形帰還系に機械学習を組み合わせることで、MQL5 APIを用いて不確実性を管理し、資本効率を向上させ、ボラティリティの高いまたは変動する市場において取引システムを安定化させる方法を示しました。従来の戦略がなぜ失敗しやすいのかを理解するとともに、データ駆動型手法によって戦略を体系的に改善する方法や、実務的に線形帰還系を実装し、パフォーマンスを最適化し、リスクを管理する方法を学ぶことができたと思います。最終的に、本記事で示した概念は、不安定または低パフォーマンスの戦略を制御可能で収益性の高いシステムへと変革する力を提供します。

ファイル名 ファイルの説明
Feedback Control Benchmark 1.mq5 市場との関係を観察することで上回ることを目指した、戦略の従来版 
Feedback Control Benchmark 2.mq5 戦略と現在の市場状況との関係を学習するために実装した線形帰還系

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

MQL標準ライブラリエクスプローラー(第2回):ライブラリコンポーネントの接続 MQL標準ライブラリエクスプローラー(第2回):ライブラリコンポーネントの接続
本記事では、MQL5標準ライブラリを用いてエキスパートアドバイザー(EA)を効率的に構築するために、クラス構造をどのように読み解くべきかを整理します。標準ライブラリは高い拡張性と機能性を備えていますが、その全体像が見えにくく、体系的な指針がないまま複雑なツールキットを渡されたように感じることも少なくありません。そこで本記事では、実際の開発現場でクラスを確実に連携させるための、簡潔かつ再現性の高い統合手順を紹介します。
知っておくべきMQL5ウィザードのテクニック(第84回):ストキャスティクスとFrAMAのパターンの使用 - 結論 知っておくべきMQL5ウィザードのテクニック(第84回):ストキャスティクスとFrAMAのパターンの使用 - 結論
ストキャスティクスとフラクタル適応型移動平均(FrAMA: Fractal Adaptive Moving Average)は、互いに補完し合う特性を持っており、MQL5のエキスパートアドバイザー(EA)で使えるインジケーターペアの1つです。この組合せについては前回の記事で紹介しましたが、今回はその締めくくりとして、残る5つのシグナルパターンを検討していきます。これらの検証にあたっては、これまでと同様にMQL5ウィザードを用いて構築およびテストをおこないます。
プライスアクション分析ツールキットの開発(第46回):MQL5におけるスマートな可視化を備えたインタラクティブフィボナッチリトレースメントEAの設計 プライスアクション分析ツールキットの開発(第46回):MQL5におけるスマートな可視化を備えたインタラクティブフィボナッチリトレースメントEAの設計
フィボナッチツールは、テクニカル分析で最も人気のあるツールのひとつです。本記事では、価格の動きに応じて動的に反応するリトレースメントおよびエクステンションレベルを描画し、リアルタイムアラート、スタイリッシュなライン、ニュース風のスクロールヘッドラインを提供するインタラクティブフィボナッチEAの作成方法をご紹介します。このEAのもうひとつの大きな利点は柔軟性です。チャート上で高値(A)と安値(B)のスイング値を直接入力できるため、分析したい価格範囲を正確にコントロールできます。
MQL5入門(第24回):チャートオブジェクトで取引するEAの構築 MQL5入門(第24回):チャートオブジェクトで取引するEAの構築
本記事では、チャート上に描かれたサポートラインやレジスタンスラインを検出し、それに基づいて自動で取引を実行するエキスパートアドバイザー(EA)の作成方法を解説します。