English Deutsch
preview
古典的な戦略を再構築する(第21回):ボリンジャーバンドとRSIのアンサンブル戦略の発見

古典的な戦略を再構築する(第21回):ボリンジャーバンドとRSIのアンサンブル戦略の発見

MetaTrader 5 |
49 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

ボリンジャーバンドは、あらゆる経験レベルのトレーダーによって広く使用されている代表的なテクニカル指標です。主にサポートとレジスタンスの特定、または平均回帰型の取引戦略の構築に利用されます。その基本的な前提には、価格は一定の均衡水準へ回帰する傾向があるという考え方があります。この指標は移動平均線を中心とし、その上下にそれぞれ標準偏差に基づいたバンドを配置することで構成されます。この標準偏差の幅は重要なチューニングパラメータとなります。

古典的な設定では、価格が上側バンドを上抜けた場合、価格は中央の移動平均へ回帰すると期待されます。逆に下側バンドを下抜けた場合には、上方向への回帰が想定されます。しかし実際の市場では、このような明確な境界内で常に推移するわけではありません。市場が平均回帰的な挙動を示す局面では、この古典的なボリンジャーバンド戦略は有効に機能します。一方で、強いトレンドが発生している局面では、このルールに依存した取引は、継続的な損失につながる可能性があります。このため、ボリンジャーバンドをどのようにすれば市場環境の変化に適応させつつ収益化できるのか、という課題が残ります。

その一つの解決策として、ボリンジャーバンドと別のテクニカル指標を組み合わせる方法が考えられます。その有力な候補がRSI(相対力指数)です。この2つを組み合わせることで、ロングエントリーは価格が下側バンドを下抜けし、同時にRSIが売られ過ぎ領域に入った場合にのみ検討されます。これにより、価格が均衡水準へ回帰する可能性に対する追加の確認が得られます。同様に、価格が上側バンドを上抜けし、RSIが買われ過ぎ領域に入った場合にのみショートエントリーを検討します。これにより、平均回帰の確度が高まることが期待されます。

本記事では、この組み合わせ手法の実現可能性を検証し、戦略を段階的に改善するプロセスを示します。本記事での観察では、ボリンジャーバンドとRSIは高確度のシグナルを生成するものの、その発生頻度はシステマティックな取引目的としては十分ではないという課題が確認されました。シグナル抽出量を増加させつつノイズを最小化することを目的とした、5つの戦略バリエーションを評価しました。このプロセスは容易ではありませんでしたが、統計的モデリング手法を用いることで、手動ルールでは直接的に観測できない売買シグナルを特定することが可能となりました。本記事は、従来の取引概念が現代的なアルゴリズム手法によってどのように拡張および発展可能であるかを示すものです。


重要な注意事項

冒頭で述べた通り、本記事ではこの取引戦略の5つのバージョンを段階的に実装していきます。読み進めながら同様に検証したい場合は、図1に示す構成に従ってアプリケーションを整理することを推奨します。

図1:本記事を通して使用するファイル構成

同じ説明の繰り返しを避けるため、ここでは5つのすべてのバージョンで共通して固定するバックテスト条件の重要なポイントを整理します。まず最初に固定する設定は、バックテストの期間です。本記事では2023年1月から2026年1月までの3年間を対象とします。また、すべてのバックテストはEURUSDの日足で実行します。

図2:4つのバージョンすべてで共通して使用するバックテスト期間

さらに、実運用における不確実性を再現するため、ランダムディレイ設定を使用します。これにより、実際のネットワークを通じて注文を送信する際に発生する遅延を現実的に模擬できます。加えて、モデリングには実ティックデータを使用します。これにより、過去データに基づきながらも、可能な限り実際の取引環境に近い結果を得ることを目的としています。

図3:上記のバックテスト条件はすべてのテストで共通して適用される


ベースラインの確立

まず最初に、アプリケーションのベースラインバージョンを構築し、以降のすべての実装で上回るべき収益性の基準を設定します。まず、アプリケーションに必要な取引関連ライブラリを読み込みます。ポジションを管理するためにTradeライブラリを読み込みます。加えて、現在のビッド価格およびアスク価格の取得や、市場で許可されている最小取引量の取得などを補助するために、独自に作成したTradeInfoライブラリも読み込みます。
//+------------------------------------------------------------------+
//|                                                    Version 1.mq5 |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>
#include <VolatilityDoctor/Trade/TradeInfo.mqh>
CTrade Trade;
TradeInfo *TradeHelper;

これらの準備が整ったら、次に重要となるのは、アプリケーションのすべてのバージョンで一貫して維持する必要のあるシステム定数を定義することです。これらの定義は取引戦略における重要なチューニングパラメータであり、変更すると戦略のパフォーマンスに大きな影響を与えます。そのため、今後観察される改善が取引ロジックそのものの改良によるものであることを確実にするためには、これらの重要なパラメータを固定しておくことが不可欠です。

//+------------------------------------------------------------------+
//| System definitions                                               |
//+------------------------------------------------------------------+
#define ATR_PERIOD    14
#define ATR_MULTIPLE  2
#define BB_PERIOD     30
#define BB_SD         2
#define BB_PRICE      PRICE_CLOSE
#define RSI_PERIOD    15
#define RSI_PRICE     PRICE_CLOSE
#define RSI_LEVEL_MAX 70
#define RSI_LEVEL_MIN 30
#define SYMBOL        "EURUSD"
#define TF_MAIN       PERIOD_D1
#define TF_TRADING    PERIOD_H4
#define SHIFT         0

チューニングパラメータの設定が完了したら、次にアプリケーション内のさまざまなスコープで使用されるグローバル変数を定義します。これらのグローバル変数は、テクニカル指標のハンドラおよびバッファを構成するものです。

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
int      bb_handler,rsi_handler,atr_handler;
double   bb_upper[],bb_mid[],bb_lower[],rsi[],atr[];

取引アプリケーションが初めて初期化される際には、まずテクニカル指標のハンドラを定義します。必要な指標を読み込んだ後、次におこなうべき重要な処理は、それぞれの指標が正しく読み込まれているか、無効な状態になっていないかを確認することです。これには、各ハンドラがMQL5 APIで定義されているINVALID_HANDLEマクロと等しいかどうかをチェックします。いずれかの指標が正常に読み込まれていない場合は、ユーザーにフィードバックを返したうえで初期化処理を終了します。一方、すべて正常に読み込まれている場合は、独自に定義したクラスを読み込み、初期化成功として処理を完了します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Setup the technical indicators
   bb_handler  = iBands(SYMBOL,TF_MAIN,BB_PERIOD,SHIFT,BB_SD,BB_PRICE);
   rsi_handler = iRSI(SYMBOL,TF_MAIN,RSI_PERIOD,RSI_PRICE);
   atr_handler = iATR(SYMBOL,TF_MAIN,ATR_PERIOD);

//--- Validate the indicators were setup correctly
   if(bb_handler == INVALID_HANDLE)
     {
      //--- Failed to sertup the Bollinger Bands
      Comment("Failed to setup the Bollinger Bands Indicator: ",GetLastError());
      return(INIT_FAILED);
     }

   else
      if(rsi_handler == INVALID_HANDLE)
        {
         //--- Failed to setup the RSI indicator
         Comment("Failed to setup the RSI Indicator: ",GetLastError());
         return(INIT_FAILED);
        }

      else
         if(atr_handler == INVALID_HANDLE)
           {
            //--- Failed to setup the ATR indicator
            Comment("Failed to setup the ATR Indicator: ",GetLastError());
            return(INIT_FAILED);
           }

         else
           {
            //--- User defined types
            TradeHelper = new TradeInfo(SYMBOL,TF_MAIN);

            //--- Good news: no errors
            return(INIT_SUCCEEDED);

           }
  }

アプリケーションの使用が終了した際には、ターミナルが指標および読み込んだライブラリのために確保したメモリリソースを解放します。これはMQL5における基本的な実装上の作法であり、リソースを適切にクリーンアップするために重要です。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the indicators
   IndicatorRelease(bb_handler);
   IndicatorRelease(rsi_handler);
   IndicatorRelease(atr_handler);
   delete TradeHelper;
  }

ブローカーから新しい価格レベルが受信されるたびに、まず時間の管理を始めます。MQL5では、新しいローソク足の形成をアルゴリズム的に容易に検出できます。新しいローソク足が形成されている場合は、直近のタイムスタンプを更新し、指標バッファの更新処理へ進みます。さらに、取引ルールを評価する前に、終値も追跡します。

本アプリケーションでは、一度に保有できるポジションを1つに制限しています。そのため、まず現在のポジション数がゼロであるかを確認します。これが満たされている場合にのみ、記事冒頭で定義したルールに従って取引判断をおこないます。具体的には、終値がボリンジャーバンドの上側極限を上回り、かつRSIが買われ過ぎ領域にある場合には売ります。ロングポジションの場合はその逆であり、価格がボリンジャーバンドの下側極限を下抜けし、RSIが売られ過ぎ領域に入った場合に買います。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Keep track of the time
   static datetime time_stamp;
   datetime time_current = iTime(SYMBOL,TF_TRADING,0);

//--- Check if a new candle has formed
   if(time_stamp != time_current)
     {
      //--- Update the time
      time_stamp = time_current;

      //--- Update our indicator readings
      CopyBuffer(bb_handler,0,0,1,bb_mid);
      CopyBuffer(bb_handler,1,0,1,bb_upper);
      CopyBuffer(bb_handler,2,0,1,bb_lower);
      CopyBuffer(rsi_handler,0,0,1,rsi);
      CopyBuffer(atr_handler,0,0,1,atr);

      //--- Update current price levels
      double close = iClose(SYMBOL,TF_MAIN,SHIFT);

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX))
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN))
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }
     }
  }
//+------------------------------------------------------------------+

これでアプリケーションは完成です。最後のステップとして、アプリケーションのヘッダーで定義したすべてのシステム定義を解除します。これもMQL5における良い実装上の習慣の一つです。

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef ATR_PERIOD
#undef ATR_MULTIPLE
#undef BB_PERIOD
#undef BB_SD
#undef BB_PRICE
#undef RSI_PERIOD
#undef RSI_PRICE
#undef SYMBOL
#undef TF_MAIN
#undef SHIFT
#undef TF_TRADING
//+------------------------------------------------------------------+

この取引ルールによって生成されたエクイティカーブを図4に示します。見てわかる通り、エクイティカーブは上昇トレンドを示しています。ドローダウン期間や一部許容できないレベルのボラティリティは存在するものの、全体としては正の傾向を維持しています。

図4:ボリンジャーバンドとRSIを組み合わせた初期アプローチによって生成されたエクイティカーブ

次に、定義した取引ルールの詳細統計を分析すると、良い結果と悪い結果が混在していることが分かります。まず取引戦略の良い側面として、全取引のうち64パーセントが利益を上げていることが確認できます。これは高品質な取引シグナルを示しており、期待ペイオフは4.37です。これらの数値は非常に良好です。

しかし一方で、総取引回数はわずか14回にとどまっています。バックテストは3年間にわたって実施されているため、平均すると年間5回未満の取引しか発生していないことになります。これは極めて少なく、実用的な取引戦略としては不十分です。そのため、シグナルの品質を損なうことなく、より多くの取引機会を発見する必要があります。これは非常に繊細なバランスであり、適切なルール設計は直感的に導出することが困難です。

図5:取引アプリケーション第1バージョンの詳細統計


ベースラインの改善

初期パフォーマンスの改善を目的として、シグナル対ノイズ比と取引頻度の両方を向上させるために、複数の手動ルールを試しました。市場に強いモメンタムが発生している局面に着目することで、高品質な取引機会を抽出できると考えたためです。多くのトレーダーは、市場センチメントを分析するためにローソク足パターンを利用します。そこで本記事でも、強い値動きを示唆するローソク足パターンを探索しました。
//--- Update current price levels
double close = iClose(SYMBOL,TF_MAIN,SHIFT);
      
double open_current       = iOpen(SYMBOL,TF_MAIN,SHIFT);
double open_previous      = iOpen(SYMBOL,TF_MAIN,1);
      
double low_current       = iLow(SYMBOL,TF_MAIN,SHIFT);
double low_previous      = iLow(SYMBOL,TF_MAIN,1);

double high_current      = iHigh(SYMBOL,TF_MAIN,SHIFT);
double high_previous     = iHigh(SYMBOL,TF_MAIN,1);

トレーダーの間で広く用いられている代表的なルールの一つに、高値更新と安値更新の判定があります。現在の高値が前日の高値を上回っている場合、これは強い上昇トレンドを示していると解釈されます。逆に、現在の安値が前日の安値を下回っている場合は、強い下落モメンタムを示していると考えられます。そこで、これらのルールと既存戦略を組み合わせることで、エントリー精度を向上させ、より信頼性の高い取引機会を得られると考えました。

//--- If we have no open positions
if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if(((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) || ((low_current<low_previous) && (high_current>high_previous) && (open_current<open_previous) && (close < bb_mid[0])))
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if(((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) || ((high_current>high_previous) && (low_current<low_previous) && (open_current>open_previous) && (close > bb_mid[0])))
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }

しかし、エクイティカーブの結果から分かるように、この戦略は利益を上げたものの、私たちの期待には応えられませんでした。前回の結果と比較すると、今回の設定ではパフォーマンスの悪化が確認されました。初期設定では資産は84ドルまでの下落にとどまっていたのに対し、今回の設定では71ドルまで下落しており、これは望ましくありません。

さらに、初期戦略では最終残高が150ドルを上回っていたのに対し、新しい戦略では140ドルを下回る結果となりました。その結果、エクイティカーブの観点でも、新たに導入したルールを継続的に使用する明確な根拠は確認できません。

図6:EAバージョン2によるエクイティカーブは期待を満たさなかった

新しい取引戦略の詳細統計を分析すると、総取引回数は14回から29回へと増加しており、これは約2倍に増加しています。この結果は、追加のシグナル抽出には成功したことを示しています。しかし、総純利益は61ドルから35ドルへと減少しており、戦略にノイズが増加した可能性が示唆されます。戦略自体は依然として利益を上げているものの、ベンチマークと比較すると成功とは言えません。

図7:バージョン2の詳細統計は取引ロジックに重大な問題があることを示している


過去市場データの取得

本記事では2つのバージョンのみを紹介していますが、実際にはさらに多くのバリエーションを検証しています。手動によるルールベースの手法を試し尽くした結果、直感だけでは発見できない取引ルールを見つけるためには、統計モデルの活用が有効であると判断しました。このため、まずターミナルから過去データをCSVファイルとして抽出するスクリプトを作成しました。従来と同じシステム定義を用い、過去の始値、高値、安値、終値、およびテクニカル指標の値をファイルに書き出しています。
//+------------------------------------------------------------------+
//|                          Fetch Data Bollinger Bands RSI Strategy |
//|                                      Copyright 2026, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//+------------------------------------------------------------------+
//| System definitions                                               |
//+------------------------------------------------------------------+
#define BB_PERIOD     30
#define BB_SD         2
#define BB_PRICE      PRICE_CLOSE
#define RSI_PERIOD    15
#define RSI_PRICE     PRICE_CLOSE
#define RSI_LEVEL_MAX 70
#define RSI_LEVEL_MIN 30
#define TF_MAIN       PERIOD_D1
#define SHIFT         0

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
double   bb_upper[],bb_mid[],bb_lower[],rsi[];

//--- Setup the technical indicators
int bb_handler  = iBands(Symbol(),TF_MAIN,BB_PERIOD,SHIFT,BB_SD,BB_PRICE);
int rsi_handler = iRSI(Symbol(),TF_MAIN,RSI_PERIOD,RSI_PRICE);

//--- File name
string file_name = Symbol() + " Bollinger Band RSI Data.csv";

//--- Amount of data requested
input int size = 365;

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");
   
   CopyBuffer(bb_handler,0,0,size,bb_mid);
   ArraySetAsSeries(bb_mid,true);
   CopyBuffer(bb_handler,1,0,size,bb_upper);
   ArraySetAsSeries(bb_upper,true);
   CopyBuffer(bb_handler,2,0,size,bb_lower);
   ArraySetAsSeries(bb_lower,true);
   CopyBuffer(rsi_handler,0,0,size,rsi);
   ArraySetAsSeries(rsi,true);
   
   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
        
         FileWrite(file_handle,
                  //--- Time
                  "Time",
                   //--- OHLC
                   "Open",
                   "High",
                   "Low",
                   "Close",
                   //--- Technical Indicators
                   "BB Upper",
                   "BB Mid",
                   "BB Lower",
                   "RSI"
                  );
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i),
                   //--- OHLC
                   iOpen(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i),
                   //--- Technical Indicators
                   bb_upper[i],
                   bb_mid[i],
                   bb_lower[i],
                   rsi[i]
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+


新たなパフォーマンスレベルへの到達

データの保存後、Pythonの統計解析ライブラリを用いて分析しました。まず必要な分析ライブラリを読み込み、その後MQL5スクリプトで生成したCSVファイルを読み込みます。

#Load the analytical libraries
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

データリークを防ぐため、バックテスト期間のデータが学習に使用されないようにしました。そのため、3年間のバックテスト期間に該当するデータは学習用データセットから除外しています。 

#Read in the data
data = pd.read_csv("/ENTER/YOUR/PATH/HERE/EURUSD Bollinger Band RSI Data.csv")

生成された学習データの一部を確認用に示します。

#Drop the dates that overlap with our backtest
train = data.iloc[:((-365 * 2) - 90),:]
test  = data.iloc[((-365 * 2) - 90):,:]

#Check the dates left
train

図8:バックテスト期間と重複するデータが除外されていることを確認するベストプラクティスとして、必ず同じことをおこなってください。

エクスポートされたデータの正確性を検証するため、価格とボリンジャーバンドをプロットしました。図の通り、ボリンジャーバンドが価格を適切に包み込んでおり、データが正しく取得されていることが確認できます。

plt.plot(data['Close'],color='green')
plt.plot(data['BB Upper'],color='red')
plt.plot(data['BB Lower'],color='blue')
plt.grid()
plt.title('Visualizing Historical EURUSD Exchange Rates')
plt.ylabel('Exchange Rate')
plt.xlabel('Historical Time')
plt.legend(['EURUSD Close','BB Upper','BB Lower'])

図9:MQL5スクリプトによって取得されたEURUSDデータの視覚的検証

どの統計モデルが最適かは事前には判断できないため、複数の候補モデルをクロスバリデーションによって評価しました。 

#Load our machine learning training libraries
from sklearn.linear_model import LinearRegression,Ridge,Lasso,ARDRegression
from sklearn.neighbors    import KNeighborsRegressor,RadiusNeighborsRegressor
from sklearn.svm          import LinearSVR
from sklearn.ensemble     import RandomForestRegressor,BaggingRegressor,AdaBoostRegressor
from sklearn.model_selection import TimeSeriesSplit,cross_val_score

予測期間を定義します。

#Define our forecast horizon
HORIZON = 5

次に、将来の終値を用いて教師データをラベル付けします。

#Label the data
train['Target'] = train['Close'].shift(-HORIZON)

複数の候補モデルをまとめた辞書を作成しました。

#List all the models we wish to evaluate
models = [LinearRegression(),Ridge(),Lasso(),ARDRegression(),KNeighborsRegressor(),RadiusNeighborsRegressor(),LinearSVR(),RandomForestRegressor(),BaggingRegressor(),AdaBoostRegressor()]

各モデルの性能は時系列クロスバリデーションを用いて評価しました。時系列予測においてはデータのシャッフルが不適切であるため、scikit-learnライブラリのTimeSeriesSplitを使用しています。

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

次に、選択した各機械学習モデルの性能を格納する準備をします。

#Store the performance of each model
scores = []

各モデルについて平均二乗誤差を計算し、その結果を記録します。 

#Evaluate each model
for model in models:
  #User feedback
  print("Evaluating model: ",model)
  #Store the current score
  current_score = np.mean(np.abs(cross_val_score(model,train.iloc[:,1:-1],train.iloc[:,-1],cv=tscv,scoring='neg_mean_squared_error')))
  scores.append(current_score)

結果を可視化したところ、モデル3が最も優れたパフォーマンスを示しました。 モデル5は一見すると誤差がほぼゼロに見えますが、クロスバリデーションに失敗し、NaN値を返しているため有効な結果ではありません。これらの詳細については、次に示す表で確認できます。

sns.barplot(scores)
plt.ylabel('Cross Validated RMSE')
plt.xlabel('Model')
plt.title('Model Selection For The EURUSD Market')
plt.axhline(scores[3],linestyle=':',color='red')

図10:検証において最も高い性能を示したのはARDRegressionモデルである

 モデル3はARD回帰に対応しているため、このモデルをONNX形式でエクスポートする準備をします。

モデル 誤差
線形回帰 0.0001957533746919363
リッジ 0.000550907245398377
ラッソ 0.014059369238373157
ARDRegression 0.00018190369036281064
KNeighborsRegressor 0.005387854064255319
RadiusNeighborsRegressor nan
LinearSVR 0.0002872914823846638
RandomForestRegressor 0.0015833296216492855
BaggingRegressor 0.0016147744161974461
AdaBoostRegressor 0.0018082307134142561


モデルをONNX形式にエクスポートする

ARDモデルはONNX (Open Neural Network Exchange)形式を用いてエクスポートしました。ONNXは、機械学習モデルを言語非依存の形式で表現・実行可能にするフレームワークであり、開発者が多様な機械学習モデルを迅速に試作・実装できるようにします。

import onnx
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx import convert_sklearn
モデルの入力形状は、8個のfloat値からなる1行のテンソルとして定義します。 
initial_types = [('float_input',FloatTensorType([1,8]))]

次に、ARDモデルを学習データ全体で学習させます。 

model = ARDRegression()
model.fit(train.iloc[:,1:-1],train.iloc[:,-1])

その後、モデルをONNX形式に変換します。

onnx_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)

最後に、生成されたONNXモデルをファイルとして保存します。

onnx.save(onnx_proto,"EURUSD D1 ARDRegression.onnx")


改善案の実装

PythonからエクスポートしたONNXモデルは、EA内でリソースとして読み込まれます。 

//+------------------------------------------------------------------+
//| System resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD D1 ARDRegression.onnx" as const uchar onnx_buffer[];

さらに、モデルの入力次元および出力次元を定義するため、新たなシステム定義を追加します。 

#define ONNX_INPUTS   8
#define ONNX_OUTPUTS  1

それだけではなく、モデルおよびその予測結果を管理するためのグローバル変数も新たに定義する必要があります。

long     onnx_model;
vectorf  onnx_outputs;

初期化処理では、モデルの入力および出力形状を検証します。検証に失敗した場合は初期化を中断し、ユーザーにフィードバックを返します。正常に検証が完了した場合は、モデル出力をゼロで初期化します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
  
//--- Set up the ONNX model
   onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT);

//--- Define the model I/O shapes
   ulong onnx_input_shape[] = {1,ONNX_INPUTS};
   ulong onnx_output_shape[] = {1,ONNX_OUTPUTS};
   
//--- Validate the ONNX model
   else
      if(!OnnxSetInputShape(onnx_model,0,onnx_input_shape))
        {
         Comment("Failed to define the ONNX model input shape: ",GetLastError());
         return(INIT_FAILED);
        }

      else
         if(!OnnxSetOutputShape(onnx_model,0,onnx_output_shape))
           {
            Comment("Failed to define the ONNX model output shape: ",GetLastError());
            return(INIT_FAILED);
           }

         else
            if(onnx_model == INVALID_HANDLE)
              {
               Comment("Error occured setting up the ONNX model: ",GetLastError());
               return(INIT_FAILED);
              }

            //--- Final settings
            else
              {
              //--- Initialize the ONNX model outputs with a zero
              onnx_outputs = vectorf::Zeros(ONNX_OUTPUTS);

               //--- Good news: no errors
               return(INIT_SUCCEEDED);
              }
  }

アプリケーションの終了時には、ONNXモデルおよび確保されたリソースを解放し、ターミナルのコメントもクリアします。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   OnnxRelease(onnx_model);
   Comment("");
  }

新しい価格データが生成された際には、モデル入力をfloat型で構築し、ONNXランタイムを用いて予測を生成します。その予測結果に基づき、取引シグナルが生成されます。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

//--- Check if a new candle has formed
   if(time_stamp != time_current)
     {
      
      //--- Prepare our ONNX model inputs
      vectorf onnx_inputs = {(float)iOpen(SYMBOL,TF_MAIN,SHIFT),
                             (float)iHigh(SYMBOL,TF_MAIN,SHIFT),
                             (float)iLow(SYMBOL,TF_MAIN,SHIFT),
                             (float)iClose(SYMBOL,TF_MAIN,SHIFT),
                             (float)bb_upper[0],
                             (float)bb_mid[0],
                             (float)bb_lower[0],
                             (float)rsi[0]};
                             
      //--- Obtain a forecast from our ONNX model
      OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_outputs);
      Comment("EURUSD Model Forecast: ",onnx_outputs[0]);

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if(((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) || (onnx_outputs[0] < close))  
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if(((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) || (onnx_outputs[0] > close))
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }
     }
  }
//+------------------------------------------------------------------+

また、アプリケーションの最後には、すべてのシステム定数を必ず解除することを忘れてはいけません。

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef ONNX_INPUTS
#undef ONNX_OUTPUTS
//+------------------------------------------------------------------+

しかしながら、得られたエクイティカーブはさらなるパフォーマンス低下を示しました。 

図11:EAバージョン3によるエクイティカーブは、手法の見直しの必要性を示している

純利益は過去最低の6ドルに到達しており、取引頻度は増加したもののノイズが過剰に増えたことが示唆されます。

図12:詳細統計は、これまでの変更によって改善が達成されていないことを明確に示している


パフォーマンス改善に向けた再検討

観測されたパフォーマンスの低下を受け、モデリング手法を根本から見直す必要がありました。これまでの金融市場における統計モデリングの議論でも、価格そのものを直接予測するよりも、特定のテクニカル指標を対象にモデリングした方が高い精度を得られることが経験的に確認されています(参考記事は読者の便宜のために提示されています)。そのため、価格を直接予測するアプローチから、取引戦略で使用しているテクニカル指標そのものを予測対象とするアプローチへと重点を移しました。 
#Label the data
train['Target 1'] = train['BB Upper'].shift(-HORIZON)
train['Target 2'] = train['BB Mid'].shift(-HORIZON)
train['Target 3'] = train['BB Lower'].shift(-HORIZON)
train['Target 4'] = train['RSI'].shift(-HORIZON)

#Drop missing labels
train = train.iloc[:-HORIZON,:]

新しいモデルは、従来の1つの出力ではなく、4つの出力を生成するようになりました。

final_types = [('float_output',FloatTensorType([1,4]))]
さらに、システム内のノイズを低減するため、より慎重にデータを処理しました。この目的を達成するために、Z得点の正規化を適用し、データを適切にスケーリングしています。
Z1 = train.iloc[:,1:-4].mean()
Z2 = train.iloc[:,1:-4].std()
train.iloc[:,1:-4] = ((train.iloc[:,1:-4] - Z1) / Z2)

ARDモデルがアンダーフィット(過少適合)しているように見えたため、非線形関係をより適切に捉える目的でRandom Forest Regressorを選択しました。

model = RandomForestRegressor()

model.fit(train.iloc[:,1:-4],train.iloc[:,-4:])

 モデルをONNX形式へ変換し、内容が分かりやすい命名規則に従って保存しました。

onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12)
onnx.save(onnx_proto,"EURUSD D1 RandomForestRegressor.onnx")


改善の実装

その後、新しいONNXモデルをアプリケーションに再度読み込みました。
//+------------------------------------------------------------------+
//|                                                    Version 4.mq5 |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD D1 RandomForestRegressor.onnx" as const uchar onnx_buffer[];

過去の市場データをPythonで分析して得られた平均値および標準偏差は、精度低下や丸め誤差を避けるためfloat型として保存しました。 

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
//--- Column Mean Values
const float Z1[] = { (float)1.18132371,  (float)1.18577335,  (float)1.17706596,  (float)1.1812953 ,  (float)1.20514458,
                     (float)1.18303579,  (float)1.16092701,  (float)48.60276562};

//--- Column Standard Deviation
const float Z2[] = { (float)0.09684736,  (float)0.09665192,  (float)0.09686825,  (float)0.09684589,  (float)0.09614994,
                     (float)0.09556366,  (float)0.09612185,  (float)11.10783131};

すべての入力は推論前にスケーリングし、スケール差によって生じるノイズへの過適合を防ぐようにしました。さらに、新たな取引ルールとして、予測されたRSIおよびボリンジャーバンド中間線の変化を利用し、これまでのシグナルと組み合わせました。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

//--- Check if a new candle has formed
   if(time_stamp != time_current)
     {
      //--- Update the time
      time_stamp = time_current;

      //--- Prepare our ONNX model inputs
      vectorf onnx_inputs = {(float)iOpen(SYMBOL,TF_MAIN,SHIFT),
                             (float)iHigh(SYMBOL,TF_MAIN,SHIFT),
                             (float)iLow(SYMBOL,TF_MAIN,SHIFT),
                             (float)iClose(SYMBOL,TF_MAIN,SHIFT),
                             (float)bb_upper[0],
                             (float)bb_mid[0],
                             (float)bb_lower[0],
                             (float)rsi[0]};
      
      //--- Scale the model inputs appropriately
      for(int i = 0; i < ONNX_INPUTS;i++)
         {
            onnx_inputs[i] = ((onnx_inputs[i]-Z1[i])/Z2[i]);
         }
                  
      //--- Obtain a forecast from our ONNX model
      OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_outputs);
      Comment("EURUSD Model Forecast: ",onnx_outputs);

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if(((close > bb_upper[0]) && (rsi[0] > RSI_LEVEL_MAX)) || ((onnx_outputs[3] < rsi[0]) && (onnx_outputs[1] < bb_mid[0])))  
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if(((close < bb_lower[0]) && (rsi[0] < RSI_LEVEL_MIN)) || ((onnx_outputs[3] > rsi[0]) && (onnx_outputs[1] > bb_mid[0])))
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }
     }
  }
//+------------------------------------------------------------------+

その結果として得られたエクイティカーブは大幅な改善を示し、最大で約300ドル付近まで上昇するとともに、最低水準のエクイティもより高いレベルで維持されました。

図13:EAバージョン4によって生成されたエクイティカーブは、最終的に期待通りの結果を示している

総取引回数は78回まで増加し、純利益は95ドルに達しました。勝率は64パーセントから55パーセントへと低下しましたが、取引頻度の向上と収益性の向上という2つの目標は達成されています。

図14:詳細統計はベンチマークと比較して大幅な改善を示している



改善点の分析

多くの議論において、市場データに対して推論をおこなうために統計ライブラリを使用しています。しかし、現代の統計ライブラリは推論だけでなく、市場の背後にある構造を明らかにし、それを説明するための多様な分析も可能にしています。

そこでまず、RSIと将来の価格変動との間に存在すると考えている関係が実際に成立しているかどうかを評価します。この検証のために、終値とRSIの値の散布図を作成しました。各データポイントは、価格変動が弱気であれば青、強気であればオレンジで色分けしています。また、RSIが買われ過ぎとされ売り注文のトリガーとなる領域を一般化するために赤い線を描画し、同様にロングポジションがエントリーされる売られ過ぎ領域を緑の線で示しました。

強気と弱気のサンプルが明確に分離される自然な境界が観測されることが予想されますが、実際には、想定された領域の上下において価格変動が混在していることが確認されます。これは、RSIによって生成される売買シグナルに未処理のノイズが依然として存在していることを明確に示しています。

sns.scatterplot(data=train,x='Close',y='RSI',hue='Bin Target Threshold Price')
plt.axhline(data['RSI'].mean()+data['RSI'].std(),color='red')
plt.axhline(data['RSI'].mean()-data['RSI'].std(),color='green')
plt.grid()
plt.ylabel('RSI Reading')
plt.xlabel('EURUSD Close Price')
plt.title('Relationship Between RSI & EURUSD Return')

図15:RSI指標は、強気および弱気の価格変動を自然に識別できていないことを示している

同様の分析をボリンジャーバンドによって生成される売買シグナルにも拡張しました。まず、上側バンドと終値との差を分析し、これを時間経過に対してプロットしました。赤い線より上の点は、価格が上側バンドを上抜けたことを示しています。この領域では、本来であれば弱気のサンプルのみが観測されることが期待されます。しかし実際には、強気および弱気の結果が混在しており、価格が平均回帰する場合と、そのままトレンドを継続して上昇する場合の両方が存在することを示しています。ただし、ボリンジャーバンドにおける弱気と強気のサンプル比率は、RSIで観測されたものと比較すると、弱気側により強く偏っていることが確認されます。

sns.scatterplot(data=train,y=train['Close']-train['BB Upper'],x=np.arange(train.shape[0]),hue='Bin Target Threshold Price')
plt.grid()
plt.axhline(0,color='red')
plt.ylabel('Difference Between Price & BB Upper')
plt.xlabel('Historical Time')
plt.title('Relationship Between EURUSD Close & BB Upper')

図16:上側ボリンジャーバンドは、RSIよりも優れた意思決定の境界を設定しているように見える

下側ボリンジャーバンドにおいても同様の挙動が観察されます。価格が下側バンドを下抜けた場合、ロングポジションへのエントリーが想定されます。緑の線より下に位置するサンプルの多くが強気である点は一定の有効性を示唆しており、これは肯定的な結果と言えます。それにもかかわらず、ボリンジャーバンドはRSIよりも信頼性の高い意思決定境界を提供しているように見えます。

sns.scatterplot(data=train,y=train['Close']-train['BB Lower'],x=np.arange(train.shape[0]),hue='Bin Target Threshold Price')
plt.axhline(0,color='green')
plt.grid()
plt.ylabel('Difference Between Price & BB Lower')
plt.xlabel('Historical Time')
plt.title('Relationship Between EURUSD Close & BB Lower')

図17:上側および下側のボリンジャーバンドは、RSIよりも優れた意思決定境界を設定しているように見える


教師なし機械学習を用いた高次元取引戦略の探索

この時点で、市場挙動は人間が容易に把握できる範囲を超えた、より多くの変動次元によって規定されている可能性を検討できます。人間は通常、X、Y、Z軸で表現される3次元を超えた推論に困難を伴います。しかし、本研究で使用している市場データは8次元に及び、始値、高値、安値、終値、上側・中間・下側ボリンジャーバンド、RSIで構成されています。このことは、人間の直感では直接観測も理解も困難な高次元取引戦略が存在する可能性を示唆しています。
教師なし機械学習アルゴリズムは、このような高次元構造を検出し、人間にとって解釈可能な形へと変換する用途に適しています。多くの手法が存在しますが、本記事ではIsometric Mapping (Isomap).と呼ばれる非線形射影手法に焦点を当てます。これらは一般に多様体学習あるいは次元削減アルゴリズムと呼ばれます。その目的は、類似した観測値を近接させ、異なる観測値を可能な限り分離することです。

代表的な次元削減手法として主成分分析(PCA)が知られており、既存研究でも広く議論されています。しかしPCAは線形手法であり、市場データに存在する複雑な非線形関係を捉えきれない可能性があります。一方で、sklearn.manifoldライブラリに実装されているIsomapは、人間の直感を超えた非線形かつ高次元の関係性を明らかにし、取引戦略として成立し得る構造を抽出できる可能性があります。

まず、Isomapライブラリを読み込みます。 

from sklearn.manifold import Isomap

次にエンコーダをインスタンス化し、fit_transformメソッドを適用して、元の8次元データセットを2次元表現へと削減します。 

enc = Isomap()

manifold = pd.DataFrame(enc.fit_transform(train.iloc[:,1:9]))

manifold

図18:Isometric mappingにより市場データを2次元へ射影する

これらの学習された多様体を、元の学習データに追加します。

train['Iso 1'] = manifold.iloc[:,0]
train['Iso 2'] = manifold.iloc[:,1]

次に、学習された多様体成分と元の市場変数との相関を分析します。第1成分はすべての価格変数と正の相関を示しており、この成分の上昇が始値、高値、安値、終値の上昇と対応していることを示唆しています。一方、第2成分は負の相関を示しており、この成分の低下が価格上昇と関連していることを示しています。

sns.heatmap(train.iloc[:,1:].corr())
plt.title('EURUSD Training Data Correlation Heatmap')

図19:新しい学習データセットの相関行列

散布図をを用いて、8次元から2次元へ削減された市場データを可視化します。結果として得られる構造は完全に分離されているわけではありませんが、一定のパターンが確認できます。一部の領域では明確な弱気挙動が見られ、他の領域では強気の特徴が確認されます。ただし重なりも残存しており、下流の性能に影響するノイズが存在することが示唆されます。

sns.scatterplot(data=train,x='Iso 1',y='Iso 2',hue='Bin Target Threshold Price')
plt.grid()
plt.title('Visualizing Our High Dimensional Data in 2 Dimensions')

図20:次元削減アルゴリズムにより、本来であれば完全な可視化が困難な高次元データを2次元で表現している

従来、学習された多様体特徴量は元の目的変数の予測に使用されます。本記事ではこのアプローチを再解釈し、多様体成分そのものを代理の目的変数として扱います。その理由は、これらの成分が価格そのものよりも予測可能である可能性があるためです。この仮説を検証するため、価格、中間ボリンジャーバンド、RSI、および2つの多様体成分について予測精度を評価します。

scores = []

from sklearn.ensemble import RandomForestClassifier

scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target Threshold Price'],cv=tscv,scoring='accuracy'))))
scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target Threshold BB Mid'],cv=tscv,scoring='accuracy'))))
scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target Threshold RSI'],cv=tscv,scoring='accuracy'))))
scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target 1'],cv=tscv,scoring='accuracy'))))
scores.append(np.mean(np.abs(cross_val_score(RandomForestClassifier(),train.iloc[:,1:9],train['Bin Target 2'],cv=tscv,scoring='accuracy'))))

図21は結果の棒グラフを示しています。価格予測の精度は赤い点線で示されています。中間ボリンジャーバンドは移動平均であり平滑性が高いため、より高い精度を示しています。特に注目される点として、2番目に良いターゲットはRSIではなく第1Isomap成分であることが確認されます。このターゲットは人間の直接的な認識では到達できない高次元空間に存在しており、次元削減技術の有効性を示しています。ただし、以前の記事で述べた通り、統計指標の改善が必ずしも取引パフォーマンスの改善に直結するわけではありません。

sns.barplot(scores)
plt.xticks([0,1,2,3,4],['Price','BB Mid','RSI','Iso 1','Iso 2'])
plt.axhline(scores[0],color='red',linestyle=':')
plt.ylabel('Cross Validation Accuracy 100%')
plt.xlabel('Candidate Target')
plt.title('Our Accuracy Predicting Different Targets Related to The EURUSD')

図21:予測可能性が高いターゲットのうち第2位は、人間の認識範囲外の高次元構造に埋め込まれている

次に、この学習された多様体を予測することによる実質的な効果を評価します。従来の手順に従い、ランダムフォレスト回帰モデルを用いて第1Isomap成分を予測し、学習済みモデルをONNX形式でエクスポートします。モデル名はIsomapアルゴリズムによって生成されたマニホールドを予測することを反映するよう設定されています。

initial_types = [('float_input',FloatTensorType([1,8]))]

final_types = [('float_output',FloatTensorType([1,1]))]

model = RandomForestRegressor()

model.fit(train.iloc[:,1:9],train['Bin Target 1'])

onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12)

onnx.save(onnx_proto,"EURUSD D1 Iso 1 RandomForestRegressor.onnx")


改善の最終試行

これらの変更を実装した後、改訂版アプリケーションを検証します。まず、新しくエクスポートされたランダムフォレストモデルを読み込みます。 
//+------------------------------------------------------------------+
//| System resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD D1 Iso 1 RandomForestRegressor.onnx" as const uchar onnx_buffer[];

次に、モデルの入力と出力を定義します。前回の4出力とは異なり、本モデルは単一出力です。 

#define ONNX_INPUTS   8
#define ONNX_OUTPUTS  1

取引ルールは以下のように設定されます。予測値が0.5を超える場合は買い、0.5未満の場合は売ります。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

//--- Check if a new candle has formed
   if(time_stamp != time_current)
     {
      
      //--- Prepare our ONNX model inputs
      vectorf onnx_inputs = {(float)iOpen(SYMBOL,TF_MAIN,SHIFT),
                             (float)iHigh(SYMBOL,TF_MAIN,SHIFT),
                             (float)iLow(SYMBOL,TF_MAIN,SHIFT),
                             (float)iClose(SYMBOL,TF_MAIN,SHIFT),
                             (float)bb_upper[0],
                             (float)bb_mid[0],
                             (float)bb_lower[0],
                             (float)rsi[0]};
      
      //--- Scale the model inputs appropriately
      for(int i = 0; i < ONNX_INPUTS;i++)
         {
            onnx_inputs[i] = ((onnx_inputs[i]-Z1[i])/Z2[i]);
         }
                  
      //--- Obtain a forecast from our ONNX model
      OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_outputs);
      Comment("EURUSD Model Forecast: ",onnx_outputs);

      //--- Update current price levels
      double close = iClose(SYMBOL,TF_MAIN,SHIFT);

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Check for our trading signal
         if(onnx_outputs[0] < 0.5)  
           {
            Trade.Sell(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetBid(),TradeHelper.GetBid() + (atr[0] * ATR_MULTIPLE),TradeHelper.GetBid() - (atr[0] * ATR_MULTIPLE),"");
           }

         else
            if(onnx_outputs[0] > 0.5)
              {
               Trade.Buy(TradeHelper.MinVolume(),TradeHelper.GetSymbol(),TradeHelper.GetAsk(),TradeHelper.GetAsk() - (atr[0] * ATR_MULTIPLE),TradeHelper.GetAsk() + (atr[0] * ATR_MULTIPLE),"");
              }
        }
     }
  }
//+------------------------------------------------------------------+

結果として得られたエクイティカーブは期待通りの結果には達していません。散発的な利益スパイクは観測されるものの、全体としては不安定であり、初期残高付近で推移しています。それにもかかわらず、未実現のエクイティ上昇スパイクの存在は、高次元構造に潜在的なシグナルが存在する可能性を示唆しています。

図22:第5反復の取引アプリケーションのエクイティカーブは、高次元取引戦略の限界を示している

最後に最後に詳細な性能統計を確認すると、結果は混在しています。取引回数は83回へと増加し、初期の14回と比較して大幅に増加しています。しかし純利益は悪化し、勝率も48%へ低下しています。一方で、買い取引は比較的良好な結果を示しています。さらなる反復と分析の改善により、依然として未発見の高次元取引戦略が存在する可能性が示唆されます。

図23:最終版の取引アプリケーションの詳細分析は、許容できないレベルのノイズの存在を示している


結論

本記事では、古典的な取引戦略が現代の統計アルゴリズムによって再活性化され、より高い性能水準へ到達し得ることを示しています。アルゴリズム取引は定型的なプロセスではなく、その成功は継続性、推論、創造性、そして反復的な改善に依存します。さらに読者は、MetaTrader 5ターミナルから得られる豊富なデータセットが継続的に拡大していることを踏まえると、一部の取引戦略は人間の認識からは隠れたまま、高次元空間に埋め込まれた形で存在し得ることを理解することになります。

教師なし統計アルゴリズムの適用方法を再構築することで、本稿はMetaTrader 5ターミナル内の履歴データから高次元取引戦略を識別するための数理的に妥当な手法を提示しています。現代のコンピュータは金融時系列データにおける複雑な高次元構造を検出する能力を持ち、それにより人間の発想を超えた取引戦略の発見と学習が可能となります。これは共有する価値のある興味深い知見であり、さらなる研究が求められます。最終的に本稿は、アルゴリズム取引の最大の利点が、金融市場に対して既知であると考えていたことのうち、何が真であり何がそうでないかを明らかにする能力にある可能性を示しています。


ファイル名 ファイルの説明
Version_1.mq5 ボリンジャーバンドとRSIを組み合わせた初期のルールベースの試行です。高確率の取引シグナルを生成しましたが、その発生頻度は低いものでした。
Version_2.mq5
初期戦略の第2反復では手作業によるルールを追加しましたが、性能は望ましくなく、取引戦略におけるシグナルが大幅に減少しました。
Version_3.mq5
第3反復のEAは、適切な取引シグナルを捉える統計モデルの導入に失敗しました。
Version_4.mq5
実験の中で最も高い収益性を示した取引アプリケーションのバージョンです。
Version_5.mq5
最終バージョンでは履歴データから高次元取引戦略の学習を試みましたが、収益性のある形では達成できませんでした。
Fetch_Data_Bollinger_Bands_RSI_Strategy.mq5 市場データ分析に使用したJupyter Notebookです。
Bollinger_Band_RSI_Strategy.ipynb  Pythonでの分析のために履歴市場データをCSVへ書き出すために使用したMQL5スクリプトです。

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

MQL5入門(第36回):MQL5のAPIとWebRequest関数の習得(X) MQL5入門(第36回):MQL5のAPIとWebRequest関数の習得(X)
MQL5におけるHMAC-SHA256およびAPI署名の基本概念を紹介し、メッセージと秘密鍵を組み合わせることでリクエストを安全に認証する方法を説明します。これは、機密データを公開することなくAPI呼び出しに署名するための基盤となります。
プライスアクション分析ツールキットの開発(第55回):CPIミニローソク足オーバーレイによるバー内圧力の可視化 プライスアクション分析ツールキットの開発(第55回):CPIミニローソク足オーバーレイによるバー内圧力の可視化
価格チャート上にバー内の買い圧力と売り圧力を可視化するCLVベースのオーバーレイであるCandle Pressure Index(CPI、ローソク足圧力指数)の設計とMetaTrader 5への実装について解説します。本記事では、ローソク足の構造、圧力分類および可視化の仕組み、そして時間足や銘柄に依存せず一貫した動作を維持する、リペイントなしの遷移ベースアラートシステムに焦点を当てます。
ラリー・ウィリアムズの『市場の秘密』(第7回):Trade Day of the Week概念の実証研究 ラリー・ウィリアムズの『市場の秘密』(第7回):Trade Day of the Week概念の実証研究
ラリー・ウィリアムズのTrade Day of the Week (TDW)概念の実証研究です。時間ベースの市場バイアスを、MQL5を用いてどのように測定、検証、活用できるかを示します。曜日ごとの勝率やパフォーマンスを分析するための実践的なフレームワークを提示し、短期取引システムの改善に役立てる方法を解説します。
MQL5取引ツール(第12回):相関行列ダッシュボードのインタラクティブ機能の強化 MQL5取引ツール(第12回):相関行列ダッシュボードのインタラクティブ機能の強化
MQL5における相関行列ダッシュボードを強化し、パネルのドラッグ操作、最小化と最大化、ボタンや時間足に対するホバー効果、マウスイベント処理などを追加することで、ユーザー体験の向上を図ります。さらに、相関の強さに基づく銘柄の並び替え(昇順、降順)、相関値表示とp値表示の切り替え、ライトテーマとダークテーマの切り替え、動的なカラー更新も実装します。