機械学習の限界を克服する(第4回):複数ホライズン予測による既約誤差の回避
機械学習は非常に広い分野であり、多様な視点から学んだり解釈したりすることができます。しかしその広さゆえに、すべてを習得することは難しい側面があります。本連載では、統計学的な視点や線形代数学的な視点から機械学習を扱ってきましたが、幾何学的解釈に焦点を当てることはあまりありませんでした。一般的には、機械学習モデルは入力から出力への写像を近似する関数として説明されますが、幾何学的な観点から見ると、この説明は十分とはいえません。
実際には、モデルはターゲットの像を入力空間へ埋め込み、将来は入力だけを使ってターゲットを表現しようとしています。その過程で、モデルは入力データによって新しい多様体を形作り、その多様体上で予測をおこないます。しかし、本来のターゲットは独自の多様体上に存在します。この2つの多様体のずれが微妙でありながら避けられない既約誤差を生みます。つまりモデルはターゲットそのものを指すのではなく、あくまで入力の組み合わせで表現可能な像にしか向かうことができないのです。
ひとつの思考実験を考えてみてください。2台の車の速度が記録されていて、どちらが速いか判断するよう求められたとします。簡単な問題に思えますが、一方が毎分あたりのマイル、もう一方が毎時あたりのキロメートルで測定されていると知った瞬間、判断は一気に怪しくなります。2つの測定が異なる単位系に属しているためです。同様に、モデルの予測は真のターゲットが存在する座標系とは異なる座標系の上に表現されます。言い換えれば、モデルはその場で独自の単位系を作り出してしまうのです。
こうした単位のずれが無害で済む場合もあります。ターゲットが入力の張る空間にうまく収まっている場合には、この不整合による誤差はほぼゼロになります。しかし、取引ではこの問題を無視すると大きな損失につながる可能性があります。機械学習モデルは内部で座標変換をおこない、私たちを本来のターゲットとは異なる座標系に置きます。金融市場は自然科学とは異なり、入力がターゲットを完全に説明してくれる保証がありません。そのため私たちは常に部分的に盲目の状態で作業していると言えます。
関連する自己最適化エキスパートアドバイザーに関する連載では、行列分解による線形回帰モデルの構築方法を紹介し、OpenBLASライブラリを取り上げ、特異値分解(SVD)について解説しました。これらの内容に触れていない読者の方は復習をおすすめします。本記事はその基礎の上に成り立っているためです。こちらがリンクです。
それ以外の読者の方には復習になりますが、SVDは行列をU、S、VTの3つの行列に分解します。それぞれには独自の幾何学的性質があります。UとVTは直交行列で、元データを回転または反転させます。これらはベクトルの長さを保ち、向きだけを変えます。中央のSは対角行列であり、データ値のスケーリングをおこないます。
これらを組み合わせて理解すると、SVDは回転、スケーリング、そして再び回転という操作を順に施していることになります。線形回帰モデルはこのプロセスによってターゲットの像を入力空間に埋め込んでいるのです。幾何学的な本質を抽出すると、線形回帰がおこなっているのは回転して、スケーリングして、また回転するという操作に尽きます。それ以上でもそれ以下でもありません。幾何学を学ぶことでこの視点が明確になると1つの挑発的な疑問が浮かびます。いったい学習とはどこでおこなわれているのでしょうか。
答えはやや不穏です。実務者が学習と呼んでいるものは、実際には座標系を整列し、軸のスケーリングを調整して、ターゲットを入力の張る空間に射影しやすくしているだけなのです。私たちはデータの隠れた真理を発見しているわけではありません。多様体同士が予測においてそれなりに一致するように、幾何学的変換を順番に適用しているにすぎません。
さらに言えば、SVDはこの新しい座標系を生成するプロセスそのものです。線形回帰では、入力データを直交軸へ射影し、拡大縮小し、回転させることで、ターゲットを可能な限り近づけられる変換空間を作り出します。モデルが学習しているように見える現象は、ターゲットをこの新しい座標系へ整列させる操作に他なりません。
この幾何学的な視点に立つと、従来は根拠が薄いと思われていた実務上の判断やベストプラクティスを正当化し得ます。ここで最も重要な点は、モデルの予測値とターゲットの実値を直接比較することをやめる必要があるということです。代わりに、異なるホライズン(予測期間)でのモデル自身の予測値同士を比較するべきです。
たとえば、モデルが1ステップ先の終値を$5、10ステップ先の終値を$15と予測したとします。この2つの予測の傾きが正であれば買い、負であれば売りです。予測が現実と完全に一致することを期待するのはやめます。多様体のずれによって、予測が現実と完全に一致する日は永遠に来ない可能性すらあります。その代わり、複数ホライズンの予測の相対的な傾きを取引判断に使います。この複数ステップ予測の概念自体はアルゴリズム取引では新しいものではありませんが、本記事では、機械学習モデルを用いるなら複数ステップ予測を事実上の標準とすべきだということを読者の方に伝えたいのです。
本記事は幾何学的な誤差を削減したり取り除いたりすることを主張しているわけではありません。むしろ、その誤差が支配的になる領域に踏み込まないようにすることで、誤差との関わりを最小限に抑える方法を示しています。
私たちの方法論では、9種類のターゲットを用いて機械学習モデルを訓練しました。これらは1日先、5日先、10日先の終値、高値、安値の移動平均です。従来型のコントロール設定では、クラシカルな手法に従い、1つ先のローソク足におけるターゲットの実値を予測し、現在の値と比較して取引する方法を採用しました。しかしご覧のとおり、私たちはターゲットとの直接比較をやめ、複数ホライズンの予測値同士を比較する方法に切り替えることで、従来型手法を繰り返し上回る結果を得ました。この考え方は非常にシンプルでありながら強力です。モデルの予測をターゲットの実値と比較するよりも、予測同士を比較したほうが収益性が高くなる可能性があるのです。
従来型のコントロール設定を2022年3月から2025年5月までの3年間の過去データでバックテストしました。その結果、この従来型の設定では純利益が$71でした。しかし、モデルの予測の解釈方法を変えるだけで、純利益が$180まで増加し、収益性が153%向上しました。シャープレシオは0.45から2.16へ改善し、利益が出た取引の割合は46%から65%へ上昇しました。これは取引精度が41%向上したことを意味します。
最も重要な点は、ここで示したすべての改善が、依存しているモデルを一切変更することなく実現できるということです。またこの方法論は、読者の方がすでに使用している他の機械学習モデルにも拡張して適用できます。
市場データの取得
まず、必要な過去の市場データを取得するMQL5スクリプトを作成します。MetaTrader 5ターミナルから過去の市場データを抽出するのがベストプラクティスです。なぜなら、これによりONNXモデルが最終的な本番環境と整合した過去データで学習されることが保証されるからです。作成するMQL5スクリプトは、主要な4つの価格レベルとそれぞれの移動平均に関する詳細な記録を取得します。また、これら各価格レベルが5ステップ前の値と比較してどの程度成長したかについても注目します。取得したすべてのデータはCSV形式で書き出され、ハードディスクに保存されます。
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- Define our moving average indicator #define MA_PERIOD 5 //--- Moving Average Period #define MA_TYPE MODE_SMA //--- Type of moving average we have #define HORIZON 5 //--- Forecast horizon //--- Our handlers for our indicators int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle; //--- Data structures to store the readings from our indicators double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[]; //--- File name string file_name = Symbol() + " Detailed Market Data As Series Moving Average.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { int fetch = size + (HORIZON * 2); //---Setup our technical indicators ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW); //---Set the values as series CopyBuffer(ma_handle,0,0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading); ArraySetAsSeries(ma_h_reading,true); CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading); ArraySetAsSeries(ma_l_reading,true); //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time", //--- OHLC "True Open", "True High", "True Low", "True Close", //--- MA OHLC "True MA C", "True MA O", "True MA H", "True MA L", //--- Growth in OHLC "Diff Open", "Diff High", "Diff Low", "Diff Close", //--- Growth in MA OHLC "Diff MA Close 2", "Diff MA Open 2", "Diff MA High 2", "Diff MA Low 2" ); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), //--- OHLC iClose(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), //--- MA OHLC ma_reading[i], ma_o_reading[i], ma_h_reading[i], ma_l_reading[i], //--- Growth in OHLC iOpen(_Symbol,PERIOD_CURRENT,i) - iOpen(_Symbol,PERIOD_CURRENT,(i + HORIZON)), iHigh(_Symbol,PERIOD_CURRENT,i) - iHigh(_Symbol,PERIOD_CURRENT,(i + HORIZON)), iLow(_Symbol,PERIOD_CURRENT,i) - iLow(_Symbol,PERIOD_CURRENT,(i + HORIZON)), iClose(_Symbol,PERIOD_CURRENT,i) - iClose(_Symbol,PERIOD_CURRENT,(i + HORIZON)), //--- Growth in MA OHLC ma_reading[i] - ma_reading[(i + HORIZON)], ma_o_reading[i] - ma_o_reading[(i + HORIZON)], ma_h_reading[i] - ma_h_reading[(i + HORIZON)], ma_l_reading[i] - ma_l_reading[(i + HORIZON)] ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef HORIZON #undef MA_PERIOD #undef MA_TYPE //+------------------------------------------------------------------+
市場データの分析
これで、過去の市場データを読み込む準備が整いました。まずは、データ操作用にいくつかのPythonライブラリを読み込みます。
import pandas as pd import numpy as np import matplotlib.pyplot as plt
ここで、予測したい3つの固有の時間範囲を定義します。
#Define our different forecast horizons H1 = 1 H2 = 5 H3 = 10
ここで、ターミナルからエクスポートした市場データを読み取り、さまざまな時間範囲での移動平均のターゲットを作成します。
#Read in the data data = pd.read_csv('../EURUSD Detailed Market Data As Series Moving Average.csv') #Label the data data['Target 1'] = data['True MA C'].shift(-H1) data['Target 2'] = data['True MA C'].shift(-H2) data['Target 3'] = data['True MA C'].shift(-H3) data['Target 4'] = data['True MA H'].shift(-H1) data['Target 5'] = data['True MA H'].shift(-H2) data['Target 6'] = data['True MA H'].shift(-H3) data['Target 7'] = data['True MA L'].shift(-H1) data['Target 8'] = data['True MA L'].shift(-H2) data['Target 9'] = data['True MA L'].shift(-H3) #Drop missing rows data = data.iloc[:-H3,:]
学習パーティションを作成します。
data = data.iloc[:-(365*3),:] data_test = data.iloc[-(365*3):,:]
入力とターゲットを分離します。
X = data.iloc[:,1:-9] y = data.iloc[:,-9:]
任意の機械学習モデルを読み込みます。本記事では、sklearnライブラリを使用し、線形モデルを使用して原理を説明します。
from sklearn.linear_model import LinearRegression
モデルを初期化します。
model = LinearRegression()
モデルを適合します。
model.fit(X,y)
テストセットに対するモデルの予測を取得します。ただし、テストセットでモデルを学習させてはいけません。モデルの予測はターゲットとよく一致しているように見えますが、これから説明するように、モデルはさらにより良いパフォーマンスを発揮することも可能です。
preds = pd.DataFrame(model.predict(data_test.iloc[:,1:-9])) plt.plot(data_test.iloc[:,-9].reset_index(drop=True),color='black') plt.plot(preds.iloc[:,0],color='red',linestyle=':') plt.grid() plt.title("Out Of Sample Forecasting") plt.ylabel('EUR/USD Exchange Rate') plt.xlabel('Time') plt.legend(['Actual Price','Forecasted Price'])

図1:モデルのアウトオブサンプル予測は実際のターゲットと一貫性があるように見えるが、このパフォーマンスはさらに改善できる
ONNXはOpen Neural Network Exchangeの略で、機械学習モデルを標準化されたライブラリで構築および展開できる仕組みを提供します。この標準は、ますます多くのプログラミング言語で採用されています。Pythonから機械学習モデルをONNXライブラリを使ってエクスポートし、その後MQL5にインポートします。ONNXを利用することで、機械学習モデルを迅速に開発し、展開することが可能になります。
import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
ONNXモデルの入力と出力の形状を定義する必要があります。前もって入力と出力を分けていたため、これは簡単におこなえます。各パーティションの列数を取得して保存するだけです。Pandasを使用すれば、shapeプロパティによってこの情報を簡単に取得できます。
initial_types = [("FLOAT INPUT",FloatTensorType([1,X.shape[1]]))] final_types = [("FLOAT OUTPUT",FloatTensorType([y.shape[1],1]))]
機械学習モデルのONNXプロトタイプを作成します。ここで、モデルに必要な入力と出力の数を指定します。
model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12) ONNXモデルをディスクに保存します。
onnx.save(model_proto,"EURUSD MFH LR D1.onnx")
ベースラインパフォーマンスの確立
これで、ベースラインとなるパフォーマンスレベルを確立する準備が整いました。まず、テストの一貫性を確保するために、戦略のパラメータを可能な限り固定します。//+------------------------------------------------------------------+ //| MFH.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ #define SYSTEM_INPUTS 16 #define SYSTEM_OUTPUTS 9 #define ATR_PERIOD 14 #define ATR_PADDING 1 #define TF_1 PERIOD_D1 #define TF_2 PERIOD_M15 #define MA_PERIOD 5 #define MA_TYPE MODE_SMA #define HORIZON 5
PythonからエクスポートしたONNXバッファを読み込みます。
//+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD MFH LR D1.onnx" as const uchar onnx_buffer[];
また、注文実行、ローソク足の生成、ONNXバッファの操作など、アルゴリズム取引における日常的なタスクを処理するために、いくつかのライブラリをインクルードします。
//+------------------------------------------------------------------+ //| System libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\ONNX\ONNXFloat.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> ONNXFloat *ONNXHandler; Time *TimeHandler; Time *LowerTimeHandler; TradeInfo *TradeHandler; CTrade Trade;
グローバル変数もいくつか必要です。これらは主にテクニカル指標の管理やONNXモデルの予測値の保存に使用されます。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle,fetch,atr_handler; double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],atr[]; double padding; vector model_prediction;
アプリケーションを最初に読み込んだ際には、グローバル変数やカスタムクラスの初期化をおこない、さらに作成したテクニカル指標のハンドルを保存します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- fetch = HORIZON * 2; ONNXHandler = new ONNXFloat(onnx_buffer); LowerTimeHandler = new Time(Symbol(),TF_2); TimeHandler = new Time(Symbol(),TF_1); TradeHandler = new TradeInfo(Symbol(),TF_1); ONNXHandler.DefineOnnxInputShape(0,1,SYSTEM_INPUTS); ONNXHandler.DefineOnnxOutputShape(0,1,SYSTEM_OUTPUTS); ma_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_LOW); atr_handler = iATR(Symbol(),TF_1,MA_PERIOD); model_prediction = vector::Zeros(SYSTEM_OUTPUTS); //--- return(INIT_SUCCEEDED); }
MQL5では、使用しなくなったリソースを解放することが、メモリ管理上の標準的なプラクティスとされています。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- delete ONNXHandler; IndicatorRelease(ma_h_handle); IndicatorRelease(ma_o_handle); IndicatorRelease(ma_l_handle); IndicatorRelease(ma_handle); IndicatorRelease(atr_handler); }
新しい価格レベルを受信したら、テクニカル指標のバッファとストップロスレベルを更新します。その後、ポジションがあるかどうかを確認します。ポジションがない場合は取引の機会を確認し、既にポジションがある場合はトレーリングストップを用いてポジションを管理します。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Has a new candle formed if(TimeHandler.NewCandle()) { //---Set the values as series CopyBuffer(ma_handle,0,0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading); ArraySetAsSeries(ma_h_reading,true); CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading); ArraySetAsSeries(ma_l_reading,true); CopyBuffer(atr_handler,0,0,fetch,atr); ArraySetAsSeries(atr,true); padding = (atr[0] * ATR_PADDING); //--- Obtain a prediction from our model if(PositionsTotal() == 0) { model_predict(); } //--- Manage open positions if(PositionsTotal() >0) { manage_setup(); } } } //+------------------------------------------------------------------+
トレーリングストップは、ATR (Average True Range)インジケーターによって定義されます。ATRは市場のボラティリティを測定し、リスクレベルを動的に調整するのに役立ちます。ストップロスをより利益の出る位置に安全に移動できる場合は更新し、そうでなければそのまま待機します。
//+------------------------------------------------------------------+ //| Manage our open positions | //+------------------------------------------------------------------+ void manage_setup(void) { //--- Select the position by its ticket number if(PositionSelectByTicket(PositionGetTicket(0))) { //--- Store the current tp and sl levels double current_tp,current_sl; current_tp = PositionGetDouble(POSITION_TP); current_sl = PositionGetDouble(POSITION_SL); //--- Before we calculate the new stop loss or take profit double new_sl,new_tp; //--- We first check the position type if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { new_sl = TradeHandler.GetBid()-padding; new_tp = TradeHandler.GetBid()+padding; //--- Check if the new stops are more profitable if(new_sl>current_sl) Trade.PositionModify(Symbol(),new_sl,new_tp); } if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { new_sl = TradeHandler.GetAsk()+padding; new_tp = TradeHandler.GetAsk()-padding; //--- Check if the new stops are more profitable if(new_sl<current_sl) Trade.PositionModify(Symbol(),new_sl,new_tp); } } }
まず、機械学習モデルを使用せずにアプリケーションをテストし、利益のベースラインレベルを確立します。機械学習モデルがこのベースラインを上回ることを目標とし、単純なブレイクアウト戦略を使用します。このベースラインを下回るモデルは許容されません。
//+------------------------------------------------------------------+ //| Obtain a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); } } //+------------------------------------------------------------------+
最後に、先ほど作成したすべてのシステム定数を未定義化します。
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_PERIOD #undef MA_TYPE #undef HORIZON #undef TF_1 #undef TF_2 #undef SYSTEM_INPUTS #undef SYSTEM_OUTPUTS #undef ATR_PADDING #undef ATR_PERIOD
以上をまとめると、システム構成は以下のようになります。
//+------------------------------------------------------------------+ //| MFH.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ #define SYSTEM_INPUTS 16 #define SYSTEM_OUTPUTS 9 #define ATR_PERIOD 14 #define ATR_PADDING 1 #define TF_1 PERIOD_D1 #define TF_2 PERIOD_M15 #define MA_PERIOD 5 #define MA_TYPE MODE_SMA #define HORIZON 5 //+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD MFH LR D1.onnx" as const uchar onnx_buffer[]; //+------------------------------------------------------------------+ //| System libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\ONNX\ONNXFloat.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> ONNXFloat *ONNXHandler; Time *TimeHandler; Time *LowerTimeHandler; TradeInfo *TradeHandler; CTrade Trade; //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle,fetch,atr_handler; double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],atr[]; double padding; vector model_prediction; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- fetch = HORIZON * 2; ONNXHandler = new ONNXFloat(onnx_buffer); LowerTimeHandler = new Time(Symbol(),TF_2); TimeHandler = new Time(Symbol(),TF_1); TradeHandler = new TradeInfo(Symbol(),TF_1); ONNXHandler.DefineOnnxInputShape(0,1,SYSTEM_INPUTS); ONNXHandler.DefineOnnxOutputShape(0,1,SYSTEM_OUTPUTS); ma_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_LOW); atr_handler = iATR(Symbol(),TF_1,MA_PERIOD); model_prediction = vector::Zeros(SYSTEM_OUTPUTS); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- delete ONNXHandler; IndicatorRelease(ma_h_handle); IndicatorRelease(ma_o_handle); IndicatorRelease(ma_l_handle); IndicatorRelease(ma_handle); IndicatorRelease(atr_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Has a new candle formed if(TimeHandler.NewCandle()) { //---Set the values as series CopyBuffer(ma_handle,0,0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading); ArraySetAsSeries(ma_h_reading,true); CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading); ArraySetAsSeries(ma_l_reading,true); CopyBuffer(atr_handler,0,0,fetch,atr); ArraySetAsSeries(atr,true); padding = (atr[0] * ATR_PADDING); } if(LowerTimeHandler.NewCandle()) { //--- Obtain a prediction from our model if(PositionsTotal() == 0) { model_predict(); } //--- Manage open positions if(PositionsTotal() >0) { manage_setup(); } } } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Manage our open positions | //+------------------------------------------------------------------+ void manage_setup(void) { //--- Select the position by its ticket number if(PositionSelectByTicket(PositionGetTicket(0))) { //--- Store the current tp and sl levels double current_tp,current_sl; current_tp = PositionGetDouble(POSITION_TP); current_sl = PositionGetDouble(POSITION_SL); //--- Before we calculate the new stop loss or take profit double new_sl,new_tp; //--- We first check the position type if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { new_sl = TradeHandler.GetBid()-padding; new_tp = TradeHandler.GetBid()+padding; //--- Check if the new stops are more profitable if(new_sl>current_sl) Trade.PositionModify(Symbol(),new_sl,new_tp); } if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { new_sl = TradeHandler.GetAsk()+padding; new_tp = TradeHandler.GetAsk()-padding; //--- Check if the new stops are more profitable if(new_sl<current_sl) Trade.PositionModify(Symbol(),new_sl,new_tp); } } } //+------------------------------------------------------------------+ //| Obtain a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); } } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_PERIOD #undef MA_TYPE #undef HORIZON #undef TF_1 #undef TF_2 #undef SYSTEM_INPUTS #undef SYSTEM_OUTPUTS #undef ATR_PADDING #undef ATR_PERIOD //+------------------------------------------------------------------+
まず、利益に対する合理的な期待値を設定することから始めます。このステップは非常に重要であり、機械学習モデルが実際にどれだけ改善に貢献しているかを公平に評価するための前提となります。

図2:コントロール設定のバックテスト期間を選択する
以下に、コントロール設定の詳細な結果を示します。見ての通り、戦略によっておこなわれた取引の大部分は利益を上げられませんでしたが、平均利益取引は平均損失取引より大きいことが分かります。この非対称的なリターン構造により、コントロール設定に対する一定の信頼を持つことができました。

図3:コントロール取引アルゴリズムの収益性分析
一方で、取引戦略によって生成されたエクイティカーブは非常に変動が大きく、今後この戦略を継続して追随することに自信を持つのは難しい状況です。そのため、次に機械学習モデルを活用して、現在の単純な取引戦略における変動の激しい波の平滑化を試みます。

図4:従来型の取引戦略は、開発者にほとんど自信を与えない
従来型アプローチによるコントロール超越の試み
ここでは、従来型の取引設定を用いてコントロール取引戦略を上回ることを試みます。通常、従来型の設定では、1本先のローソク足のターゲットを予測し、予測値と実際の価格を比較して取引シグナルを生成します。本記事では、この手法が必ずしも最良の実践方法ではない可能性があることを読者に示そうとしています。その理由を見ていきましょう。
アプリケーションの大部分のコードは意図的に変更しないため、アイデアをテストするために変更が必要なMQL5コードの部分にのみ集中できます。下図の通り、ONNXモデルによる予測に必要な16個の入力を取得し、計算をおこなう前にそれぞれをfloat型に変換する必要があります。その後、ONNXモデルから予測を取得し、ターゲットの実際の値と比較します。
//+------------------------------------------------------------------+ //| Obtain a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { vectorf model_inputs(SYSTEM_INPUTS); model_inputs[0] = (float) iClose(_Symbol,PERIOD_CURRENT,0); model_inputs[1] = (float) iOpen(_Symbol,PERIOD_CURRENT,0); model_inputs[2] = (float) iHigh(_Symbol,PERIOD_CURRENT,0); model_inputs[3] = (float) iLow(_Symbol,PERIOD_CURRENT,0); model_inputs[4] = (float) ma_reading[0]; model_inputs[5] = (float) ma_o_reading[0]; model_inputs[6] = (float) ma_h_reading[0]; model_inputs[7] = (float) ma_l_reading[0]; model_inputs[8] = (float)(iOpen(_Symbol,PERIOD_CURRENT,0) - iOpen(_Symbol,PERIOD_CURRENT,(0 + HORIZON))); model_inputs[9] = (float)(iHigh(_Symbol,PERIOD_CURRENT,0) - iHigh(_Symbol,PERIOD_CURRENT,(0 + HORIZON))); model_inputs[10] = (float)(iLow(_Symbol,PERIOD_CURRENT,0) - iLow(_Symbol,PERIOD_CURRENT,(0 + HORIZON))); model_inputs[11] = (float)(iClose(_Symbol,PERIOD_CURRENT,0) - iClose(_Symbol,PERIOD_CURRENT,(0 + HORIZON))); model_inputs[12] = (float)(ma_reading[0] - ma_reading[(0 + HORIZON)]); model_inputs[13] = (float)(ma_o_reading[0] - ma_o_reading[(0 + HORIZON)]); model_inputs[14] = (float)(ma_h_reading[0] - ma_h_reading[(0 + HORIZON)]); model_inputs[15] = (float)(ma_l_reading[0] - ma_l_reading[(0 + HORIZON)]); //--- Obtain the prediction ONNXHandler.Predict(model_inputs); for(int i=0;i<SYSTEM_OUTPUTS;i++) { model_prediction[i] = ONNXHandler.GetPrediction(i); } if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { if(model_prediction[0]>iClose(Symbol(),TF_2,0)) Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { if(model_prediction[0]<iClose(Symbol(),TF_2,0) Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); } } //+------------------------------------------------------------------+
コントロールの利益水準を確立するために使用したのと同じテスト期間で、従来型の機械学習取引アルゴリズムを実行します。

図5:コントロール設定を上回る最初の試み
取引アプリケーションの利益水準は惨憺たる結果となりました。戦略自体は高い精度を示し、全取引の63%が利益を上げたものの、これはほとんど印象的ではありません。なぜなら、コントロールアプリケーションで確立された$71の利益水準を上回ることに失敗したからです。

図6:コントロールアプリケーションを上回る最初の試みの詳細分析
私たちが作成した改良版アプリケーションは、従来型取引戦略の元のバージョンと同じ水準には達していません。しかし、公平に言えば、この改良版アプリケーションは元の戦略よりもはるかに変動が少なく、信頼性が高いように見える点も注目に値します。

図7:改良版取引アプリケーションによって生成されたエクイティカーブは、元の戦略よりも変動は少ないものの、同じ高水準には達していない
従来型アプローチの限界への挑戦
私たちはターゲットを1、5、10本先のローソク足で予測しています。ここで、10本先の予測が、最初におこなった単純な1ステップ予測よりも有益であるかどうかを確認します。したがって、以前と同様に、公平に比較するために変更が必要な取引アプリケーションの部分のみに焦点を当てます。
if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { if(model_prediction[ 2 ]>iClose(Symbol(),TF_2,0)) Trade.Buy(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { if(model_prediction[ 2 ]<iClose(Symbol(),TF_2,0)) Trade.Sell(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); }
従来の取引アプリケーションのコントロール設定と同様に、同じ3年間の期間を選択してアプリケーションをテストします。

図8:戦略が時間をより有効に活用しているかを確認するため、すべてのバックテストは同じ期間で実施する必要がある
多くの書籍では、機械学習を用いた金融市場予測において1ステップ先の予測が標準的な手法として教えられています。しかし、利益を上げる人間トレーダーは通常、1本ずつローソク足で取引することはほとんどありません。同様に、本記事で示すように、モデルも直近のローソク足を超えて予測できる場合の方が利益を上げやすいことが分かります。実際、これは本議論において初めてコントロール設定を上回る結果となった例です。

図9:10ステップ先の予測は、1ステップ先の予測よりも利益性が高いことが分かった
もちろん、これまで問題となっていたエクイティカーブの望ましくない変動性は改善されています。これは非常に励みになる結果ですが、読者がこれから目にするように、さらにパフォーマンスを向上させることも可能です。

図10:取引アプリケーションの第2版によるエクイティカーブはコントロール設定より優れているが、さらに高みに引き上げる方法を次に示す
新たな改善の余地
ここで、モデルの予測を複数の将来ステップにわたって比較する準備が整いました。具体的には、モデルが1ステップ先と10ステップ先の高値をどの位置に予測しているかを比較し、その価格レベルの期待成長を取引シグナルとして使用します。 if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { if(model_prediction[3]<model_prediction[5]) Trade.Buy(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { if(model_prediction[3]>model_prediction[5]) Trade.Sell(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); }
この方法により、単純にモデルの予測とターゲットの実際の値を比較する従来手法と比べて、複数タイムホライズンでの比較に価値があるかどうかを確認します。

図11:3年間のバックテスト期間で第3版取引アプリケーションを実行する
ご覧の通り、結果はほぼ自らを物語っています。アプリケーションはこれまでの開発サイクルのどの時点よりも利益性が高くなっています。ここで使用しているONNXモデルは以前にエクスポートしたものと同じです。取引条件の根幹は変わっていません。むしろ、モデルの予測を慎重に解釈することで、同じ取引戦略からより多くのアルファを引き出せているのです。

図12:第3版取引アプリケーションの詳細統計は、変更が有効であることを示しており、アプリケーションに自信を持たせる
エクイティカーブは一貫して新高値を更新しており、コントロール設定や従来の金融機械学習アプローチで確立された利益水準をはるかに上回っています。

図13:現在の取引アプリケーションによるエクイティカーブは、従来のすべてのバージョンで到達できなかった新高値を更新している
最終改善
筆者としては、価格そのものよりも予測しやすく、それでいて将来の価格レベルと同等に情報量のある市場構造を探すことを楽しんでいます。今回、高値と安値チャネルの移動平均を扱っていることから、これら2つの移動平均の中間点の成長率を予測する方が容易ではないかと直感しました。議論の結果、確かにその方法は有効でした。
if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { if(((model_prediction[3]+model_prediction[6])/2)<((model_prediction[5]+model_prediction[8])/2)) Trade.Buy(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { if(((model_prediction[3]+model_prediction[6])/2)>((model_prediction[5]+model_prediction[8])/2)) Trade.Sell(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); }
この最終版アプリケーションを、これまで使用してきた3年間のバックテスト期間に対して実行します。

図14:最終版取引アプリケーションの実行結果(これまでの全バージョンを上回る性能を目指す)
ご覧の通り、アプリケーションは一貫して新しいパフォーマンスレベルを達成しており、議論を開始した当初には到達できなかった領域まで成長しています。今回作成した最終版は、最初に手をかけた価値が十分にあったことを示しています。

図15:最終版アプリケーションの詳細パフォーマンスは、これまでに確立したすべての性能を上回る
エクイティカーブで観測されたボラティリティはほぼ完全に制御下にあり、新たな複雑性を加えずとも、取引戦略から得られる利益が著しく増加していることが分かります。

図16:最終版アプリケーションによるエクイティカーブの可視化により、これまでおこなったすべての変更が正しかったことを確認した
結論
アルゴリズム取引の世界において、複数ステップ先予測というアイデア自体は新しいものではありません。しかし本記事が強調したかった新しい視点は、複数ステップ先予測を単なる代替手法として扱うのではなく、むしろアルゴリズム取引そのもののゴールドスタンダード候補として位置づけるべきだという点です。
記事の前半で私は読者の皆さまに問いを投げかけました。線形回帰を幾何学的に捉えると、それは単なる回転とスケーリングの連続にすぎないことを示し、そのうえで「学習は本当にどこで起こっているのか」という疑問を提示しました。
この問いを覚えている読者の方々に向けて、ひとつの仮説を述べたいと思います。ただし最終的な答えは、ぜひ皆さま自身で独立して見つけていただきたいと強く願っています。
本記事を通して示してきた重要な原則は、数学的概念には常に幾何学的なアナロジーが存在するということです。どのような機械学習モデルであっても、その本質はデータが定義する多様体に対して、スケーリング、反射、射影、畳み込み、回転といった幾何学的変換を協調的に適用していく過程として再解釈できます。高度なニューラルネットワークでさえ、決して神秘的な存在ではなく、幾何学的変換を幾重にも重ね、折りたたみ、再配置する精緻な「振付」であると理解できます。
したがって、「学習はどこで起こっているのか」という問いに対する一つの答えは、学習とは情報が幾何学的パターンとして符号化されるときに起こる、ということかもしれません。回転やスケーリングのサイクルは、その中でも最も基本的で強力な幾何学的構造のひとつです。それは、三原色がすべての色の基礎となるのと同じ意味で、すべての変換の基礎となる「一次的変換」のひとつなのです。
画像分類の分野では、すでにこの現実が受け入れられています。その成功は、長く精密な前処理パイプラインに支えられていますが、その本質は幾何学的変換の緻密な調整です。表面的には単なる特徴量エンジニアリングに見えるかもしれませんが、実際にはこれらの深い幾何学的原理を静かに適用しており、その意義が十分に認識されていないことすらあります。
この視点から読者の方が得るべきもっとも重要な洞察は、取引における複数ステップ先予測が過小評価されている戦略であるという点です。それは、この手法が実際に担っている役割よりも、はるかに低く見積もられているからです。複数ステップ先予測は、モデル同士の比較を同じ座標系の中で実施することを保証してくれます。反対に、モデルの予測値をターゲットの実際の値と直接比較してしまうと、同じ単位で表現されている保証のない2つの量を比較することになり、誤った判断を導く可能性があるのです。
| ファイル名 | ファイルの説明 |
|---|---|
| Fetch_Data_MA_2.1.mq5 | MetaTrader 5ターミナルから履歴データを取得するために使用したMQL5スクリプト |
| MFH_Baseline.mq5 | 機械学習モデルを採用していない取引戦略のベースラインバージョン |
| MFH_1.1.mq5 | 従来の金融機械学習パラダイムのみに従って取引アプリケーションを構築しようとした最初の試み |
| MFH_1.2.mq5 | 従来の金融機械学習パラダイムを使用して取引アプリケーションを構築するための最後の試み |
| MFH_1.3.mq5 | 現在の価格と予測価格を直接比較することをやめようとした最初の試み |
| MFH_1.4.mq5 | この記事で紹介した、最高の収益性レベルを生み出した取引戦略の最終バージョン |
| Multiple_Forecast_Horizons.ipynb | ONNXモデルをエクスポートするために使用したJupyter Notebook |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19383
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL 標準ライブラリエクスプローラー(第1回):CTrade、CiMA、CiATRによる紹介
Market Sentimentインジケーターの自動化
サイクルベースの取引システム(DPO)の構築と最適化の方法
MQL5でのAI搭載取引システムの構築(第2回):ChatGPT統合型アプリケーションのUI開発
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索