English
preview
MQL5で自己最適化エキスパートアドバイザーを構築する(第16回):教師あり学習を用いた線形システム同定

MQL5で自己最適化エキスパートアドバイザーを構築する(第16回):教師あり学習を用いた線形システム同定

MetaTrader 5 |
17 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

フィードバックコントローラーに関する前回の議論では、これらのシステムが、実際の挙動を観測することによって、取引戦略のパフォーマンスを安定化できることを学びました。前回の議論へのクイックリンクはこちらです。アプリケーション設計により、勝ちトレードと負けトレードの両方に共通して持続する主要な相関構造を捉えることができました。本質的には、フィードバックコントローラーは、現在の市場状況下で最適に振る舞う方法を取引アプリケーションに学習させる役割を果たしました。これは、将来を予測することよりも、現在に対して知的に反応することを重視する人間のトレーダーの姿勢と非常によく似ています。

ここで読者に注意していただきたいのは、これまでの議論では、単純なルールベース戦略を補正するフィードバックコントローラーに焦点を当ててきたという点です。この単純なアプローチにより、たとえ本テーマが初めての読者であっても、フィードバックコントローラーがもたらす影響を直感的に観察することができました。以下の図1では、本日おこなう変更を視覚的に理解していただくために、アプリケーション構成の概略図を示しています。 

図1:当初採用したフィードバックコントローラーの設計パターンの可視化

本記事では、さらに一歩踏み込み、より深い問いを投げかけます。市場の統計モデルによって定義された取引戦略そのものを、最適に制御することは可能なのでしょうか。 これは、アルゴリズム取引における機械学習の適用方法における転換点を意味します。単なる予測のためにモデルを用いるのではなく、統計モデル同士が互いを監督し補正するという、新たなクラスの機械学習タスクを探究します。 

図2:固定された取引戦略を市場データから推定された統計モデルに置き換える

本記事の目的は、より洗練されたデータ駆動型取引戦略を起点とすることで、フィードバックコントローラーが学習できる構造がより豊かになり、最終的により良い結果が得られるかどうかを検証することです。そのために、フィードバック制御と線形システム同定に関する過去の研究を再訪しました。そこでは、単純な移動平均戦略を構築し、フィードバックコントローラーを適用してベースラインを確立しました。その後、移動平均コンポーネントをEUR/USD市場の教師あり統計モデルに置き換え、同一のテスト条件下でパフォーマンスを評価しました。その結果は以下のとおりです。

  1. 純利益はベースラインシステムの56ドルから170ドルへと増加し、約200%の改善を達成しました。
  2. 総損失は333ドルから143ドルへと減少し、下振れリスクが57%低減しました。
  3. 精度は52.9%から72%へと向上し、37%の精度改善が見られました。
  4. 取引回数は51回から33回へと減少し、35%の効率向上を示しました。これは不要な取引が効果的に排除されたことを意味します。
  5. プロフィットファクターは1.17から2.18へと向上し、リスク単位あたりの収益性が86%改善しました。

これらの結果は、適切に選定された統計モデルとフィードバックコントローラーを組み合わせることで、効率性と安定性の両面において実質的な改善が得られることを示しています。閉ループ制御と教師あり学習の相乗効果により、一種の知的適応が実現されます。このシステムは強化学習アルゴリズムを想起させるかもしれませんが、そのアプローチはあくまで教師ありの視点に立ったものです。

最終的に本記事では、この改良されたシステムを形作った設計上の選択を整理し、フィードバック制御の原理を用いて統計モデルを支援することで、MetaTrader 5アプリケーションの性能を向上させるための体系的なアプローチを読者に提供します。


Pythonによる分析の開始

MetaTrader 5の市場データを用いたPythonベースの分析における最初のステップは、必要なライブラリを読み込むことです。 

#Import the standard python libraries
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import MetaTrader5 as mt5

依存関係の読み込みが完了したら、次にMetaTrader5ターミナルを初期化します。

#Check if we have started the terminal
if(mt5.initialize()):
    print("Failed To Startup")

else:
    print("Logged In")

Logged In

この段階で、分析対象となる銘柄を選択します。

if(mt5.symbol_select("EURUSD")):
    print("Found EURUSD Market")

else:
    print("Failed To Find EURUSD Market")

Found EURUSD Market

ここまで進めば、MetaTrader 5ターミナルから履歴の市場データを直接取得する準備が整いました。なお、MetaTrader 5は時刻データを秒単位で返すため、人が読みやすい形式に変換することを忘れないようにしてください。

#Read in the market data 
data = pd.DataFrame(mt5.copy_rates_from_pos("EURUSD",mt5.TIMEFRAME_D1,0,4000))
data['time'] = pd.to_datetime(data['time'],unit='s')
data

図3:MetaTrader 5ターミナルから取得した市場データ

ターミナルは複数の市場属性を含む詳細なデータセットを提供しますが、本記事では4つの主要な価格水準、すなわち始値、高値、安値、終値のみに注目します。そのため、それ以外の列はすべて削除します。

#Focus on the major price levels
data = data.iloc[:,:5]
data

図4:この演習では4つの基本的な価格水準のみに注目する

次に、これから実施するバックテスト期間と重複する観測値をすべて削除します。線形システム同定に関する前回の議論では、2023年1月1日から執筆時点の現在である2025年10月までをバックテスト期間として使用しました。一貫性を保つため、ここでも同じ期間設定を維持します。テスト期間の情報が学習データに漏れ込まないよう、該当データを除外することは良い実践です。

#Drop off the test period
data = data.iloc[:-(370*2),:]
data

図5:バックテストと重複する観測値を削除することは重要な前処理である

データセットが整理されたら、次に予測期間、すなわちモデルがどれだけ先の未来を予測するかを定義し、それに基づいて教師データを作成します。

#Define the new horizon
HORIZON = 10

その後、すべての欠損値を含む行を削除し、データの完全性を確保します。データセットが正しく整形された時点で、機械学習ライブラリを読み込み、モデル学習を開始する準備が整います。

#Label the data
data['Target 1'] = data['close'].shift(-HORIZON)
data['Target 2'] = data['high'].shift(-HORIZON)
data['Target 3'] = data['low'].shift(-HORIZON)

次に、欠落データがある行を削除します。 

#Drop missing rows
data.dropna(inplace=True)

ここから、機械学習モデルの適合を開始します。どのモデルが最適かは事前に分からないため、複数のモデルを同時に導入して比較検証します。

#Import cross validation tools
from sklearn.linear_model import Ridge,LinearRegression
from sklearn.model_selection import TimeSeriesSplit,cross_val_score
from sklearn.metrics import root_mean_squared_error
from sklearn.neural_network import MLPRegressor
from sklearn.ensemble import RandomForestRegressor,GradientBoostingRegressor
from sklearn.neighbors import KNeighborsRegressor,RadiusNeighborsRegressor
from sklearn.svm import LinearSVR

各モデルの新しいインスタンスを作成します。 

models = [LinearRegression(),
          Ridge(alpha=10e-3),
          RandomForestRegressor(random_state=0),
          GradientBoostingRegressor(random_state=0),
          KNeighborsRegressor(n_jobs=-1,n_neighbors=5),
          RadiusNeighborsRegressor(n_jobs=-1),
          LinearSVR(random_state=0),
          MLPRegressor(random_state=0,hidden_layer_sizes=(4,10,40,10),solver='lbfgs')]

次に、データを等分し、前半を学習用、後半をテスト用として分割します。 

#The big picture of what we want to test
train , test = data.iloc[:data.shape[0]//2,:] , data.iloc[data.shape[0]//2:,:]

データセットの準備が整ったら、機械学習モデルの入力変数と目的変数を定義します。 

#Define inputs and target
X = data.columns[1:-3]
y = data.columns[-3:]

続いて、呼び出すたびに新しいモデルインスタンスを返す専用の関数を定義します。

#Fetch a new copy of the model
def get_model():
    return(LinearRegression())

前回の市場記憶に関する議論で学んだように、すべての履歴データが現在の予測に有用であるとは限りません(こちらを参照)。どの程度の履歴データが実際に必要なのかを判断するため、今回はクロスバリデーション(交差検証)を用い、学習データ前半が後半をどれだけ正確に予測できるかを検証します。その結果、前半データのおよそ60%のみで残りを十分に予測できることが分かりました。これは、学習データを内部的に一貫性のある部分に絞り込めることを意味します。

#Store our performance
error = []

#Define the total number of iterations we wish to perform
ITERATIONS = 10

#Let us perform the line search
for i in np.arange(ITERATIONS):
    #Training fraction 
    fraction =((i+1)/10)

    #Partition the data to select the most recent information
    partition_index = train.shape[0] - int(train.shape[0]*fraction)

    train_X_partition = train.loc[partition_index:,X]
    train_y_partition = train.loc[partition_index:,y[0]]

    #Fit a model 
    model = get_model()

    #Fit the model
    model.fit(train_X_partition,train_y_partition)

    #Cross validate the model out of sample
    score = root_mean_squared_error(test.loc[:,y[0]],model.predict(test.loc[:,X]))

    #Append the error levels
    error.append(score)

#Plot the results
plt.title('Improvements Made By Historical Data')
plt.plot(error,color='black')
plt.grid()
plt.ylabel('Out of Sample RMSE')
plt.xlabel('Progressivley Fitting On All Historical Data')
plt.scatter(np.argmin(error),np.min(error),color='red')

図6:すべての履歴データが有効とは限らないことが確認できる

次に、最も一貫性の高いインデックスを特定します。

#Let us select the partition of interest
partition_index = train.shape[0] - int(train.shape[0]*(0.6))

そのインデックスを用いて学習データを再構成し、古く関連性の低い観測値を削除します。

train = train.loc[partition_index:,:]
train.reset_index(inplace=True,drop=True)
train

図7:現在の市場状況に最も整合的と考えられるデータのみを保持する

設計初期段階で定義した複数の候補モデルについて、ここから順にテストデータ上で性能を評価します。なお、テストデータは最終バックテスト専用であるため、モデルの学習には一切使用しません。

#Store each model's error levels
error = []

#Fit each model
for m in models:
    m.fit(train.loc[:,X],train.loc[:,y[0]])
    #Store our error levels
    error.append(root_mean_squared_error(test.loc[:,y[0]],m.predict(test.loc[:,X])))

次に、各モデルの性能を棒グラフで可視化します。結果から、リッジ回帰モデルが最も良好な性能を示しており、深層ニューラルネットワークがそれに続いています。このことから、DNNはパラメータ調整によってさらなる改善の余地があると考えられます。

sns.barplot(error,color='black')
plt.axhline(np.min(error),color='red',linestyle=':')
plt.scatter(np.argmin(error),np.min(error),color='red')
plt.ylabel('Out of Sample RMSE')
plt.title('Model Selection For EURUSD Market')
plt.xticks([0,1,2,3,4,5,6,7],['OLS','Ridge','RF','GBR','KNR','RNR','LSVR','DNN'])

図8:ベンチマークとして有効なモデルを特定した

DNNの最適なパラメータを探索するため、scikit-learnの時系列クロスバリデーションを用います。

from sklearn.model_selection import RandomizedSearchCV,TimeSeriesSplit

分割数と各フォールド間の時間的ギャップを定義し、探索対象となるパラメータ空間を設定します。また、再現性を確保するため、シャッフルを無効化し、乱数シードを固定し、早期終了を無効にした基本構成のニューラルネットワークを定義します。たとえば、時系列データでは順序の保持が不可欠であるためshuffle=Trueは無効化し、重みの初期化が実行ごとに変化しないようrandom_stateを0に固定します。また、早期終了は使用せず、最大反復回数は1000に設定します。

#Define the time series cross validation tool
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)

#Define the parameter values we want to search over
dist = dict(
    loss=['squared_error','poisson'],
    activation = ['identity','relu','tanh','logistic'],
    solver=['adam','lbfgs','sgd'],
    learning_rate=['constant','invscaling','adaptive'],
    learning_rate_init=[1,0,10e-1,10e-2,10e-3],
    hidden_layer_sizes=[(4,10,4),(4,4,4,4),(4,1,8,2),(4,2,6,3),(4,2,1,4),(4,2,8,16,2)],
    alpha=[1,0,10e-1,10e-2,10e-3]
)

#Define basic model parameters we want to keep fixed
model = MLPRegressor(shuffle=False,random_state=0,early_stopping=False,max_iter=1000)

#Define the randomized search object
rscv = RandomizedSearchCV(model,cv=tscv,param_distributions=dist,random_state=0,n_iter=50)

#Perform the search
rscv.fit(train.loc[:,X],train.loc[:,y[0]])

#Retreive the best parameters we found
rscv.best_params_

{'solver': 'lbfgs',

 'loss': 'squared_error',

 'learning_rate_init':0.1,

 'learning_rate': 'adaptive',

 'hidden_layer_sizes':(4, 2, 1, 4),

 'alpha':0.01,

 'activation': 'identity'}

グリッドサーチを実行した結果、モデルは最も性能の高いパラメータの組み合わせを返し、それを先の結果と比較します。興味深いことに、性能チャートの最右端に示されている最適化済みDNNは、それでもなおリッジ回帰のベンチマーク性能を上回ることはありませんでした。

sns.barplot(error,color='black')
plt.scatter(x=np.argmin(error),y=np.min(error),color='red')
plt.axhline(np.min(error),color='red',linestyle=':')
plt.xticks([0,1,2,3,4,5,6,7,8],['OLS','Ridge','RF','GBR','KNR','RNR','LSVR','DNN','ODNN'])
plt.ylabel('Out of Sample RMSE')
plt.title('Final Model Selection For EURUSD 2023-2025 Backtest')

図9:先に特定した制御レベルを上回ることに成功した



ONNXへのエクスポート

最適化が完了したら、次に最終モデルをOpen Neural Network Exchange (ONNX)形式にエクスポートします。ONNXはフレームワークに依存しないインターフェースを提供しており、訓練時の依存関係を引き継ぐことなく、学習済みモデルを複数のプログラミング環境で共有および展開できるようにします。

#Fit the baseline model
model = rscv.best_estimator_

エクスポートを開始するには、モデルを定義し、必要なONNXライブラリをインポートします。

#Prepare to export to ONNX
import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

入力の形状を 1×4(4つの主要な価格レベルに対応)として指定し、出力の形状を 1×1(予測値を表す)として指定します。

#Define ONNX model input and output dimensions
initial_types = [("FLOAT_INPUT",FloatTensorType([1,4]))]
final_types = [("FLOAT_OUTPUT",FloatTensorType([1,1]))]

次に、モデルの中間表現であるONNXプロトタイプを生成します。

#Convert the model to its ONNX prototype
onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12)

最後に、それをディスクにONNXバッファファイルとして保存し、後でMetaTrader 5アプリケーションにインポートできるようにします。

#Save the ONNX model
onnx.save(onnx_proto,"EURUSD Improved Baseline LR.onnx")


MQL5アプリケーションの構築

ONNXモデルが定義され準備できたら、次にMetaTrader 5アプリケーションの構築を開始します。最初のステップは システム定数 を定義することです。これはアプリケーション全体で戦略を導く固定パラメータを指します。具体的には、移動平均の期間、フィードバックコントローラーが作動するまでに必要な観測数、そしてONNXモデルの入力および出力変数の数などが含まれます。
//+------------------------------------------------------------------+
//|                                  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
#define TOTAL_MODEL_INPUTS 4
#define TOTAL_MODEL_OUTPUTS 1

これらの定数を定義したら、先ほど作成したONNXモデルを読み込みます。

//+------------------------------------------------------------------+
//| System resources we need                                         |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD Improved Baseline LR.onnx" as const uchar onnx_buffer[];

ポジションの開始、終了、変更などの一般的な取引操作を簡素化するためのサポートライブラリもいくつかインポートします。 

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

次に、関数間で共有状態を維持するためのグローバル変数を定義し、必要な場所で同じキー値にアクセスできるようにします。

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

初期化の際に、エクスポートしたバッファからONNXモデルをインスタンス化し、破損していないかの 整合性チェックをおこないます。チェックが成功した場合、モデルの入力および出力の形状を定義します。これらの形状はPython側で定義したものと一致している必要があります。その後、テクニカル指標を読み込み、グローバル変数をデフォルト値で初期化します。 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create the ONNX model from its buffer
   onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT);

//--- Check for errors
   if(onnx_model == INVALID_HANDLE)
     {
      //--- User feedback
      Print("An error occured loading the ONNX model:\n",GetLastError());
      //--- Abort
      return(INIT_FAILED);
     }

//--- Setup the ONNX handler input shape
   else
     {
      //--- Define the I/O shapes
      ulong input_shape[] = {1,4};
      ulong output_shape[] = {1,1};

      //--- Attempt to set input shape
      if(!OnnxSetInputShape(onnx_model,0,input_shape))
        {
         //--- User feedback
         Print("Failed to specify the correct ONNX model input shape:\n",GetLastError());
         //--- Abort
         return(INIT_FAILED);
        }

      //--- Attempt to set output shape
      if(!OnnxSetOutputShape(onnx_model,0,output_shape))
        {
         //--- User feedback
         Print("Failed to specify the correct ONNX model output shape:\n",GetLastError());
         //--- Abort
         return(INIT_FAILED);
        }
     }

//--- 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 ONNX model
   OnnxRelease(onnx_model);
//--- Release the indicator
   IndicatorRelease(ma_handler);
   IndicatorRelease(atr_handler);
  }

新しい価格データが届くたびに、システムは新しいローソク足の形成を確認します。新しいローソク足が形成されていれば、ローソク足のカウントと、フィードバックコントローラーが観測した総「シーン」(エピソード)数の両方を更新します。コントローラーが必要な観測数を集めると、これが アクティブ化され、それ以降は新しい取引を行う前にコントローラーの予測を参照します。

ポジションが存在しない場合、アプリケーションはテクニカル指標を更新し、予測を要求します。この予測は、コントローラーがアクティブであればフィードバックコントローラーから、そうでなければONNXモデル から取得されます。モデルには4つの主要な価格レベルが入力され、予測値が出力されます。その後、システムは価格レベル、口座残高、証拠金、指標の値などの主要変数のスナップショットを取得します。

//+------------------------------------------------------------------+
//| 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;

         //--- Prepare a prediction from our model
         onnx_inputs = vectorf::Zeros(TOTAL_MODEL_INPUTS);
         onnx_inputs[0] = (float) iOpen(Symbol(),SYSTEM_TIME_FRAME,0);
         onnx_inputs[1] = (float) iHigh(Symbol(),SYSTEM_TIME_FRAME,0);
         onnx_inputs[2] = (float) iLow(Symbol(),SYSTEM_TIME_FRAME,0);
         onnx_inputs[3] = (float) iClose(Symbol(),SYSTEM_TIME_FRAME,0);

         //--- Also prepare the outputs
         onnx_output = vectorf::Zeros(TOTAL_MODEL_OUTPUTS);

         //--- 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();
        }
     }
  }
//+------------------------------------------------------------------+

売買シグナルはポジションが存在しない場合のみ生成されます。ONNXモデルが価格の上昇を予測する場合、買いシグナルが登録されますが、これは価格がすでに移動平均を上回っている場合に限られます。逆に、価格が移動平均を下回っており、モデルが価格の下落を予測している場合のみ、売りシグナルが登録されます。

//+------------------------------------------------------------------+
//| Check for our trading signal                                     |
//+------------------------------------------------------------------+
void check_signal(void)
  {
   if(PositionsTotal() == 0)
     {
      //--- Fetch a prediction from our model
      if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_output))
        {
         if((close > ma[0]) && (onnx_output[0] > iClose(Symbol(),SYSTEM_TIME_FRAME,0)))
           {
            Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding);
           }

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

フィードバックコントローラーの予測メソッドは、まずこれまで記録されたすべての観測値をコピーし、現在の観測値を追加することから始まります。その後、現在の入力を表すパーティション、次ステップの目標値(将来の観測値)を表すパーティションの2つのシフトされたパーティションを構築します。ここでの目的変数は将来の口座残高です。

次に、特異値分解(SVD)を用いて、観測行列を3つのユニタリ行列に分解します。3つのうち2つは直交行列であるため、逆行列は転置を取るだけで求められ、残るのは対角行列Sの逆行列だけとなります。この手法により、計算負荷が大幅に削減されます。

最適係数が導出されると、コントローラーは現在の入力ベクトルに係数を掛けて将来の残高を予測します。予測残高が現在の残高を上回る場合は取引が許可され、そうでない場合は許可されません。まれに、係数の推定に失敗することがあります。これは通常、S行列にゼロが含まれる特異な場合に起こり、その場合コントローラーは予測プロセスを中止します。

//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+

次に、アプリケーションとテストの日付を選択します。  

図10:2年間のバックテストにエキスパートアドバイザー(EA)を選択する

評価期間において現実的な取引上の課題を設定することは、実際の市場を再現する上で非常に重要です。これには、MetaTrader 5のストラテジーテスターでランダムな遅延を有効化し、ライブ取引における不確実性をシミュレートすることが含まれます。

図11:実際の市場条件を模したバックテスト条件の選択

強化されたアプリケーションのパフォーマンス指標は明確に成果を示しています。総純利益は2倍以上に増加し、取引の正確性は72%に向上、80%台に近づきました。期待ペイオフ、シャープレシオ、リカバリーファクターなどの主要なパフォーマンス比率も、ベースラインモデルを上回っています。

図12:2年間のテスト期間における取引アプリケーションの詳細統計

改良版システムのエクイティカーブは、同じバックテスト期間においてより滑らかで一貫した上昇傾向を示しています。このバックテスト期間は学習データに含まれていなかったため、改善が情報漏洩によるものではなく、実際の成果であることを確認できます。

図13:取引アプリケーションによるエクイティカーブは、期待通りの強い上昇傾向を示す

最後に特に印象的だったのは、フィードバックコントローラーの予測精度です。バックテスト終了時、最終残高は270.28ドルと予測され、実際の結果はその予測から10セント以内に収まりました。前回の記事で議論した通り、このわずかな差異は、モデル予測の数学的多様体と現実の結果の間に本質的な違いがあることに起因しており、理論上、完全な一致は不可能です。それでも、この結果の近さは、フィードバック制御フレームワークが有意義な予測を提供できることを確認しています。

図14:フィードバックコントローラーも、戦略が口座残高に与える影響について合理的な予測を示している



結論

この記事を読むことで、読者は自己適応型の取引アプリケーションを構築する新しいフレームワークを学びます。このアプリケーションは、取引結果に応じて自身の動作を制御します。線形フィードバック制御アルゴリズムを用いることで、複雑な非線形システムであっても望ましくない挙動を効率的に特定することができます。アルゴリズム取引におけるこれらのアルゴリズムの有用性は非常に高く、従来の市場モデルの性能向上にも適しています。さらに、この記事では協調して学習する複数のインテリジェントシステムのアンサンブルを構築する方法も示されています。これにより、取引アプリケーションは市場での望ましい行動を学習しようとする仕組みを持つことができます。単体での時系列予測は、より大きな解決策の一部に過ぎないことも明らかになりました。

ファイル名ファイルの説明
Feedback_Control_Benchmark_3.mq5 教材で構築したMetaTrader 5取引アプリケーション。教師あり学習とシステム同定の組み合わせに基づく設計
Supervised Linear System Identification.ipynbPython統合ライブラリを使用してターミナルから取得したEURUSD市場データを解析するためのJupyter Notebook

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

取引におけるニューラルネットワーク:概念強化を備えたマルチエージェントシステム(最終回) 取引におけるニューラルネットワーク:概念強化を備えたマルチエージェントシステム(最終回)
FinConフレームワークの著者によって提案されたアプローチの実装を続けます。FinConは、大規模言語モデル(LLM)をベースとしたマルチエージェントシステムです。本日は、必要なモジュールを実装し、実際の過去データを用いたモデルの包括的なテストをおこないます。
取引におけるニューラルネットワーク:概念強化を備えたマルチエージェントシステム(FinCon) 取引におけるニューラルネットワーク:概念強化を備えたマルチエージェントシステム(FinCon)
FinConフレームワークは、大規模言語モデル(LLM)をベースにしたマルチエージェントシステムです。概念的言語強化を活用して意思決定とリスク管理を改善し、さまざまな金融タスクで効果的に機能するよう設計されています。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
初心者からエキスパートへ:パラメータ制御ユーティリティ 初心者からエキスパートへ:パラメータ制御ユーティリティ
従来のEAやインジケーターの入力プロパティを、リアルタイムで操作可能なオンチャートのコントロールインターフェースへと変換することを想像してみてください。本記事は、これまでに取り組んできたMarket Periods Synchronizerインジケーターでの基礎的な成果を土台とし、上位足(HTF)の市場構造を可視化し、管理する手法を大きく進化させるものです。ここでは、その概念を完全にインタラクティブなユーティリティへと昇華させ、動的な操作性と強化されたマルチタイムフレーム(MTF)のプライスアクションの可視化を、チャート上に直接統合したダッシュボードとして実装します。この革新的なアプローチが、トレーダーとツールの関わり方をどのように変えていくのか、一緒に見ていきましょう。