古典的な戦略を再構築する(第14回):移動平均クロスオーバーの徹底解説
本記事では、従来の移動平均クロスオーバー戦略を取り上げ、その典型的な問題点を克服するための複数のアプローチを提示します。この戦略は、よく知られている通りノイズの影響を受けやすく、シグナルの発生が遅れがちであり、さらに広く利用されているため優位性が薄れやすいという課題があります。簡単に言うと、従来の移動平均クロスオーバーによる売買シグナルは反転が早く、頻繁であるため、安定した収益を得ることが難しい傾向があります。また、ダマし(フェイクブレイクアウト)によって早期エントリーが発生しやすい点も問題です。
従来の形のままでは、この戦略は市場ノイズに対して脆弱であり、弱い取引や収益性の低い取引を適切に除外する仕組みが十分ではありません。本記事で紹介する手法では、よりノイズ耐性の高い売買シグナルのフィルタを構築することで、これらの課題の克服を目指します。特に、クロスオーバーによって生成されるシグナルから弱いトレードを除外するための、5つの異なるアプローチについて検証します。
移動平均クロスオーバーは、統計モデルにとって学習対象として非常に有用であると考えられます。本記事で構築した統計モデルは、移動平均クロスオーバーに内在する誤差、すなわち人間がルールベースで工夫しても取り除くことが難しかった誤差を学習しました。戦略の最適化において人間の直感が有効に機能する範囲には限界がありますが、その限界を超える部分については、統計モデルが補完的に役割を果たすことが可能です。
MQL5で始める
今回の議論では同一戦略の複数のバリエーションを扱うため、各反復で同じ情報を繰り返さないよう、すべてのバックテストを通じて共通のパラメータを使用することが重要です。したがって、これからテストする5つのバージョンのアプリケーションはすべて、図1に示すように1つのフォルダに格納されています。

図1:移動平均クロスオーバー戦略のすべてのバージョンを視覚化して評価する
すべてのテストで使用する期間は、2022年1月から本稿執筆時点である2025年までに固定します。

図2:取引戦略の全バージョンについて選択したテスト期間

図3:バックテスト設定の選択
ベースラインの確立
すべてのプロセスと同様に、まずはベースラインとなるパフォーマンス水準を確立することから始めます。そのために、多くのトレーダーの間で一般的に受け入れられている標準的な形式で本戦略を実装します。具体的には、元の戦略では2本の移動平均を使用し、1つは短期、もう1つは長期の期間を持ちます。従来の設定では、短期移動平均が長期移動平均を上抜けた場合に強気(買い)ポジションを取り、逆に下抜けた場合には弱気(売り)ポジションを開くことを検討します。このシンプルな構成は多くのトレーダーに共通認識として受け入れられているため、ベースラインとして採用しました。
この戦略では、エントリーおよびエグジットのルールが定義されており、決済についてはATRを用いてストップロスおよびテイクプロフィットの水準を設定しました。なお、収益性の変化がエントリーの意思決定ルールの改善によるものとなるよう、トレーリング設定は用いず、同一幅のテイクプロフィットおよびストップロスを採用しています。
//+------------------------------------------------------------------+ //| MA Crossover V1.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" //+------------------------------------------------------------------+ //| Technical Indicators | //+------------------------------------------------------------------+ int ma_fast_handler,ma_slow_handler,atr_handler; double ma_fast_reading[],ma_slow_reading[],atr_reading[]; //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ double ask,bid; //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup our indicators ma_fast_handler = iMA("EURUSD",PERIOD_D1,30,0,MODE_SMA,PRICE_CLOSE); ma_slow_handler = iMA("EURUSD",PERIOD_D1,60,0,MODE_SMA,PRICE_CLOSE); atr_handler = iATR("EURUSD",PERIOD_D1,14); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Free up memory we are no longer using when the application is off IndicatorRelease(ma_fast_handler); IndicatorRelease(ma_slow_handler); IndicatorRelease(atr_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- When price levels change datetime current_time = iTime("EURUSD",PERIOD_D1,0); static datetime time_stamp; //--- Update the time if(current_time != time_stamp) { time_stamp = current_time; //--- Fetch indicator current readings CopyBuffer(ma_fast_handler,0,0,1,ma_fast_reading); CopyBuffer(ma_slow_handler,0,0,1,ma_slow_reading); CopyBuffer(atr_handler,0,0,1,atr_reading); ask = SymbolInfoDouble("EURUSD",SYMBOL_ASK); bid = SymbolInfoDouble("EURUSD",SYMBOL_BID); //--- If we have no open positions if(PositionsTotal() == 0) { //--- Trading rules if(ma_fast_reading[0] > ma_slow_reading[0]) { //--- Buy signal Trade.Buy(0.01,"EURUSD",ask,ask-(atr_reading[0] * 2),ask+(atr_reading[0] * 2),""); } else if(ma_fast_reading[0] < ma_slow_reading[0]) { //--- Sell signal Trade.Sell(0.01,"EURUSD",bid,bid+(atr_reading[0] * 2),bid-(atr_reading[0] * 2),""); } } } } //+------------------------------------------------------------------+
記事の導入部で説明した通り、この戦略の元のバージョンは収益性がなくノイズの影響を強く受けるため、テスト期間にわたるバックテストにおいてエクイティカーブのパフォーマンスが低調となった理由が説明できます。

図4:古典的な取引戦略によって生成されたエクイティカーブの可視化
戦略の詳細分析に進むと、戦略によって実行された取引の大半が非収益的であったことが確認されます。これは望ましい状態ではありません。さらに、この戦略はプロフィットファクターが1未満であり、期待収益も0未満であるため、長期的には投資資本を減少させることが想定されます。このような戦略は通常であれば放棄し、完全に置き換える判断がなされます。しかし本記事では、既存の戦略を改良する可能性について検討します。
前述の通り、同じ情報の繰り返しを避けるため、本記事では変更が加えられたコード部分のみに焦点を当て、変更のない部分については省略します。複数回のテストおよび調整、反復を経て、これらの修正が手動で検討可能な範囲で最も安定した改善であると判断しました。具体的には、価格の極端なヒゲが短期・長期移動平均に対して、上側または下側に形成されているかを確認することで、より適切なロング/ショートのエントリーを見つけることが可能となります。

図5:取引戦略の元のバージョンによるバックテスト結果
ベースライン改善のための最初の試み
ベースラインを確立したところで、取引戦略の改善に取り組み、ノイズに起因する取引数を減らすためのより厳格なフィルタを追加していきます。複数の異なる設定を手動で検証した結果、ローソク足のヒゲの先端を移動平均線と比較することで、市場バイアスを示唆する有効な手法が得られることが分かりました。具体的には、下ヒゲが短期移動平均より上に位置している場合にはロングポジションを検討し、上ヒゲが長期移動平均より下に位置している場合にはショートポジションを検討します。
//--- If we have no open positions if(PositionsTotal() == 0) { //--- Trading rules if((ma_fast_reading[0] > ma_slow_reading[0]) && (low > ma_fast_reading[0])) { //--- Buy signal Trade.Buy(0.01,"EURUSD",ask,ask-(atr_reading[0] * 2),ask+(atr_reading[0] * 2),""); } else if((ma_fast_reading[0] < ma_slow_reading[0]) && (high < ma_slow_reading[0])) { //--- Sell signal Trade.Sell(0.01,"EURUSD",bid,bid+(atr_reading[0] * 2),bid-(atr_reading[0] * 2),""); } }
新しいルールセットによって得られたエクイティカーブは、依然として全体的には不安定です。しかし、元の戦略で見られた支配的な下降トレンドと比較すると、上昇トレンドが優勢になっている点は改善といえます。さらに詳しく見ると、決済時の残高を上回る含み益のピークが発生している場面が確認できます。これは、現時点ではまだ捉えきれていない有用なシグナルが戦略内に残っていることを示唆しています。

図6:アプリケーションへの変更により、売買ロジックから得られるエクイティカーブに望ましい改善が見られた
戦略の詳細な統計を確認すると、顕著な改善が見られます。まず、総純利益は大きくマイナスの状態からスタートしていましたが、最終的にプラスへと転じました。また、バックテスト期間全体における総損失は減少しており、元の戦略と比較してリスクが低減していることを意味します。さらに、ノイズの多い元の戦略では135回の取引がおこなわれていましたが、改善後の戦略では107回と取引数を減らしつつ、より高い利益を実現しています。取引精度も、全体的に損失が優勢な状態から、わずかながら利益が上回る状態へと改善しました。また、期待収益も元の戦略より向上しています。
図7:手動による戦略改善により、初期テストで見られた口座残高のマイナス問題が解消された
ベースラインを超えるための第2の試み
これらの結果を踏まえ、さらに優れた結果を目指して再度改善に取り組みました。しかし、この時点では人間の直感による改善には限界が見え始めていたため、市場の過去データから直接取引ルールを学習するアプローチに移行しました。まず、過去の市場データを取得し、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_FAST 30 //--- Moving Average Fast Period
#define MA_PERIOD_SLOW 60 //--- Moving Average Slow Period
#define MA_TYPE MODE_SMA //--- Type of moving average we have
#define HORIZON 5 //--- Forecast horizon
//--- Our handlers for our indicators
int ma_fast_handle,ma_slow_handle;
//--- Data structures to store the readings from our indicators
double ma_fast_reading[],ma_slow_reading[];
//--- File name
string file_name = Symbol() + " Cross Over Data.csv";
//--- Amount of data requested
input int size = 3000;
//+------------------------------------------------------------------+
//| Our script execution |
//+------------------------------------------------------------------+
void OnStart()
{
int fetch = size + (HORIZON * 2);
//---Setup our technical indicators
ma_fast_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD_FAST,0,MA_TYPE,PRICE_CLOSE);
ma_slow_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD_SLOW,0,MA_TYPE,PRICE_OPEN);
//---Set the values as series
CopyBuffer(ma_fast_handle,0,0,fetch,ma_fast_reading);
ArraySetAsSeries(ma_fast_reading,true);
CopyBuffer(ma_slow_handle,0,0,fetch,ma_slow_reading);
ArraySetAsSeries(ma_slow_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
"Time",
//--- OHLC
"Open",
"High",
"Low",
"Close",
"MA F",
"MA S"
);
}
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),
ma_fast_reading[i],
ma_slow_reading[i]
);
}
}
//--- Close the file
FileClose(file_handle);
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Undefine system constants |
//+------------------------------------------------------------------+
#undef HORIZON
#undef MA_PERIOD_FAST
#undef MA_PERIOD_SLOW
#undef MA_TYPE
//+------------------------------------------------------------------+ Pythonでの市場データ分析
その後、Pythonを使ってスクリプトを実行し、データを分析し、そこから取引ルールを学習しました。 まず、標準的なPythonライブラリをインポートすることから始めました。
#Import the standard python libraries import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns
次に、スクリプトによって生成されたCSVファイルを読み込みます。
#Read in the data data = pd.read_csv("/content/EURUSD Cross Over Data.csv")
統計モデルを用いてどの程度先の未来まで予測したいかを定義しました。
#Label the data #Define the forecast horizon HORIZON = 5
次に、関心のある価格データにおける変化に基づいてデータのラベリングをおこないました。この例では、モデルが学習可能な適切なターゲットが3つ存在することが分かりました。主要なターゲットは市場における期待リターンであり、追加の2つのターゲットは移動平均のクロスオーバーの変化を表しています。
#Define targets data['Target'] = data['Close'].shift(-HORIZON) - data['Close'] data['Target 2'] = data['MA F'].shift(-HORIZON) - data['MA F'] data['Target 3'] = data['MA S'].shift(-HORIZON) - data['MA S'] #Drop missing rows of data data = data.iloc[:-HORIZON,:]そこから、データを訓練用とテスト用に分割しました。
#Separate the test dates train = data.iloc[:(-365*4),:] test = data.iloc[(-365*4):,:]次に、選択した統計モデルを読み込みました。
from sklearn.linear_model import LinearRegression次に、時系列クロスバリデーションのパラメータを定義しました。
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)入力とターゲットを分離しました。X = train.iloc[:,1:-3] y = train.iloc[:,-3:]このステップでは、ほぼモデルの学習とONNX形式へのエクスポートが可能な状態になっていました。ONNX (Open Neural Network Exchange)は、モデルを生成した学習フレームワークに依存せず、機械学習モデルを構築および共有できる国際的に広く利用されているフォーマットです。
import onnx from skl2onnx.common.data_types import FloatTensorType from skl2onnx import convert_sklearn
次に、モデルを初期化し、過去の市場データに適合させました。
model = LinearRegression() model.fit(X,y)
その後、モデルの入力形状を定義しました。
initial_types = [('float_input',FloatTensorType([1,X.shape[1]]))]
そして、モデルの出力形状を定義しました。
final_types = [('float_output',FloatTensorType([1,3]))]
最後に、モデルをONNXプロトタイプとして保存しました。
onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12) onnx.save(onnx_proto,'EURUSD Detailed RF.onnx')
MQL5での改善点の実現
モデルがONNXプロトタイプとして保存されると、取引アプリケーションにインポートできるようになります。
//+------------------------------------------------------------------+ //| Resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD MA.onnx" as const uchar onnx_proto[];
まず、ONNXモデルに関連付けられる新しいグローバル変数を定義します。これらの新しい変数は、モデルから得られる入力と出力を処理する役割を担います。また、モデルから予測を実行するためのハンドラも定義します。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ vectorf model_inputs,model_outputs; long model;
初期化後、モデルを市場で使用できるように準備するために、いくつかの重要な手順を実行する必要があります。まず、インポートしたONNXバッファからモデルを設定する必要があります。次に、モデルの入力形状と出力形状を定義します。モデルは6つの入力を受け取り、3つの予測結果を出力します。そこから、モデルの入力形状と出力形状を定義し、モデルが正常に作成されたことを確認します。アプリケーションが使用されなくなった場合、メモリを解放するためにONNXモデルをリリースします。新たな価格水準が受信された場合でも、売買ロジックの大部分は変わりません。改善すべき重要な点はいくつかありますが、まずは必要なモデル入力をすべて浮動小数点ベクトルに格納することです。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup the ONNX model model = OnnxCreateFromBuffer(onnx_proto,ONNX_DATA_TYPE_FLOAT); //--- Define the model parameter shape ulong input_shape[] = {1,6}; ulong output_shape[] = {1,3}; OnnxSetInputShape(model,0,input_shape); OnnxSetOutputShape(model,0,output_shape); model_inputs = vectorf::Zeros(6); model_outputs = vectorf::Zeros(3); if(model != INVALID_HANDLE) { return(INIT_SUCCEEDED); } //--- return(INIT_FAILED); }
ONNXモデルが使用されなくなった場合、メモリリソースを解放するためにモデルをリリースします。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Free up memory we are no longer using when the application is off OnnxRelease(model); }
必要な入力データの保存が正常に完了した後、未決済のポジションがない場合にモデルから予測値を取得します。その後、このモデルの予測は取引ルールに対する追加のフィルターとして機能します。したがって、モデルが予測するリターン値が0を上回る場合はロングポジションを取ることが許可され、逆に負のリターンを予測する場合はショートポジションを取ることが許可されます。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- When price levels change datetime current_time = iTime("EURUSD",PERIOD_D1,0); static datetime time_stamp; //--- Update the time if(current_time != time_stamp) { time_stamp = current_time; //--- Fetch indicator current readings CopyBuffer(ma_fast_handler,0,0,1,ma_fast_reading); CopyBuffer(ma_slow_handler,0,0,1,ma_slow_reading); CopyBuffer(atr_handler,0,0,1,atr_reading); double open = iOpen("EURUSD",PERIOD_D1,0); double close = iClose("EURUSD",PERIOD_D1,0); double high = iHigh("EURUSD",PERIOD_D1,0); double low = iLow("EURUSD",PERIOD_D1,0); model_inputs[0] = (float) open; model_inputs[1] = (float) high; model_inputs[2] = (float) low; model_inputs[3] = (float) close; model_inputs[4] = (float) ma_fast_reading[0]; model_inputs[5] = (float) ma_slow_reading[0]; ask = SymbolInfoDouble("EURUSD",SYMBOL_ASK); bid = SymbolInfoDouble("EURUSD",SYMBOL_BID); //--- If we have no open positions if(PositionsTotal() == 0) { if(!(OnnxRun(model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_outputs))) { Comment("Failed to obtain a forecast from our model: ",GetLastError()); } else { Comment("Forecast: ",model_outputs); //--- Trading rules if((ma_fast_reading[0] > ma_slow_reading[0]) && (low > ma_fast_reading[0]) && (model_outputs[0] > 0)) { //--- Buy signal Trade.Buy(0.01,"EURUSD",ask,ask-(atr_reading[0] * 2),ask+(atr_reading[0] * 2),""); } else if((ma_fast_reading[0] < ma_slow_reading[0]) && (high < ma_slow_reading[0]) && (model_outputs[0] < 0)) { //--- Sell signal Trade.Sell(0.01,"EURUSD",bid,bid+(atr_reading[0] * 2),bid-(atr_reading[0] * 2),""); } } } } } //+------------------------------------------------------------------+
更新版の戦略によって生成されたエクイティカーブを見ると、明確な違いがすぐに確認できます。口座残高のボラティリティは大幅に抑制されています。この新しいアプローチでは、口座残高の変動に対してより安定して制御されていることが分かります。さらに、より強く明確な上昇トレンドも確認でき、3年間の検証期間を通じて残高が安定的に成長していたことを示唆しています。したがって、新しい戦略は初期の戦略よりも確実に堅牢であるように見えます。

図8:取引戦略は現在、時間経過とともに健全な資産推移を示している
詳細な結果を見ると、総純利益は直前の初期バージョンから2倍に増加していることが分かります。現在の純利益は120ドルであり、それに加えてテスト期間中に累積したグロス損失は大幅に減少しています。元の戦略ではグロス損失が900ドルに達していましたが、新バージョンでは300ドルにとどまっています。それにもかかわらず、総純利益は2倍以上に増加しており、このバージョンの戦略が明らかにより効果的であることを示しています。期待収益やプロフィットファクターも、ようやく初期値より健全な水準に改善されました。特に注目すべき点として、総取引数が以前のほぼ半分に減少しており、より少ない取引回数でより大きな利益を生み出していることが分かります。
しかしながら、アプリケーションによって実行される取引の分布が、市場の実態を十分に反映していない点は懸念材料です。このバックテストでは、3年間で売りが14回、買いが45回実行されています。これは大きく偏っており、戦略を駆動する統計モデルに根本的な問題がある可能性を示しています。

図9:戦略に組み込まれた統計モデルは新たな問題も引き起こした
改善点をさらに掘り下げる
この問題についてさらに検討した結果、より柔軟な統計モデルを用いることで実務的な改善が可能であると考えられました。これまで使用していた線形回帰モデルはベースラインの構築には適していますが、関係性の構造に対して非常に強い仮定を持っています。そのため、線形モデルをランダムフォレスト回帰に置き換えることにします。ランダムフォレストは、従来の線形モデルでは捉えられないデータ内の非線形な効果を学習することが可能です。
Pythonノートブックに必要な変更はごくわずかだったので、ここでは変更する必要があった箇所のみに焦点を当てて説明します。新しいモデルはランダムフォレスト回帰器として定義され、その後モデルを学習し、ONNX形式へエクスポートします。
model = RandomForestRegressor() model.fit(X,y) onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12) onnx.save(onnx_proto,'EURUSD Detailed RF.onnx')
MQL5における成長の余地を育む
その後、モデルをMQL5アプリケーションへインポートし、新たな重要な改善として、従来の取引ルールを新しいルールへと置き換えます。これらの新ルールは、より柔軟性の高い統計モデルによって完全に駆動されます。今回採用したランダムフォレストモデルは、当初使用していた線形モデルよりも多くの市場関係性を捉えることができるため、理論上は、従来のようなクラシカルな取引ルールを用いることなく、すべての売買判断を単独でおこなううことが可能であると期待されます。
//--- Trading rules if(((model_outputs[0] > 0) && (model_outputs[1] > 0) && (model_outputs[2] > 0)) || ((ma_fast_reading[0] > ma_slow_reading[0]) && (low > ma_fast_reading[0]))) { //--- Buy signal Trade.Buy(0.01,"EURUSD",ask,ask-(atr_reading[0] * 2),ask+(atr_reading[0] * 2),""); } else if(((model_outputs[0] < 0) && (model_outputs[1] < 0) && (model_outputs[2] < 0)) || ((ma_fast_reading[0] < ma_slow_reading[0]) && (low < ma_slow_reading[0]))) { //--- Sell signal Trade.Sell(0.01,"EURUSD",bid,bid+(atr_reading[0] * 2),bid-(atr_reading[0] * 2),""); }
より強力な非線形モデルによって生成された新しいエクイティカーブを確認すると、初回のテストでは到達できなかった新たな高値へと口座残高が上昇していることが分かります。しかしながら、取引戦略の挙動におけるボラティリティの高い側面も見え始めています。具体的には、2022年12月頃から2024年3月頃にかけて、大きなドローダウンが発生しています。最終的には回復しているものの、この点はやや残念です。というのも、その直前の初期バージョンの戦略では、このような大きなボラティリティの期間は見られなかったためです。ただし、全体としては口座残高は依然として右肩上がりの傾向を維持しており、戦略自体の有効性は保たれていると考えられます。

図10:非線形統計モデルの導入により、同一戦略からより高いパフォーマンス水準を達成できた
詳細な結果を確認すると、今回の変更の影響が見て取れます。総純利益は初期と比較してほぼ同程度です。一方で、総損失は2倍以上に増加しており、総取引回数も増加しています。総利益はわずかに改善しているものの、この新しいバージョンでは、従来とほぼ同じ結果を得るために、より多くの処理をおこなっています。ただし重要な改善点として、取引の分布が市場の特性をより適切に反映するようになっています。従来のバージョンではこの点が十分ではありませんでした。

図11:新しい非線形の教師ありモデルにより、従来の線形モデルが学習していたバイアスが修正された
最後の試み
最後に、取引戦略のパフォーマンスをさらに向上させるための最終的な試行をおこないます。そのために、同一市場に関する追加データの取得を検討しました。これは、元のデータセットには含まれていなかった新たな特徴量を作成することで実現できます。具体的には、主要なマーケットデータの変動を捉えるために、多数の特徴量を手動で定義しました。移動平均指標同士の差分、価格と移動平均の差分、さらに4種類の価格チャネル間の差分などを計算することで、最終的に約20の入力特徴量を定義しました。なお、当初は6つの特徴量のみを使用していました。
//+------------------------------------------------------------------+ //| 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_FAST 30 //--- Moving Average Fast Period #define MA_PERIOD_SLOW 60 //--- Moving Average Slow Period #define MA_TYPE MODE_SMA //--- Type of moving average we have #define HORIZON 5 //--- Forecast horizon //--- Our handlers for our indicators int ma_fast_handle,ma_slow_handle; //--- Data structures to store the readings from our indicators double ma_fast_reading[],ma_slow_reading[]; //--- File name string file_name = Symbol() + " Cross Over Data.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { int fetch = size + (HORIZON * 2); //---Setup our technical indicatorsa ma_fast_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD_FAST,0,MA_TYPE,PRICE_CLOSE); ma_slow_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD_SLOW,0,MA_TYPE,PRICE_OPEN); //---Set the values as series CopyBuffer(ma_fast_handle,0,0,fetch,ma_fast_reading); ArraySetAsSeries(ma_fast_reading,true); CopyBuffer(ma_slow_handle,0,0,fetch,ma_slow_reading); ArraySetAsSeries(ma_slow_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 "Time", //--- OHLC "Open", "High", "Low", "Close", //--- Moving averages "MA F", "MA S", //--- Growth in OHLC channels "Delta O", "Delta H", "Delta L", "Delta C", //--- Growth in MA Channels "Delta MA F", "Delta MA S", //--- Growth Across OHLC Channels "Delta O - H", "Delta O - L", "Delta O - C", "Delta H - L", "Delta H - C", "Delta L - C", //--- Growth Between Price and the moving averages "Delta C - MA F", "Delta C - MA S" ); } 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), //--- Moving Averages ma_fast_reading[i], ma_slow_reading[i], //--- Growth in OHLC channels 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 Channels ma_fast_reading[i] - ma_fast_reading[i+HORIZON], ma_slow_reading[i] - ma_slow_reading[i+HORIZON], //--- Growth across OHLC channels iOpen(_Symbol,PERIOD_CURRENT,i+HORIZON) - iHigh(_Symbol,PERIOD_CURRENT,i+HORIZON), iOpen(_Symbol,PERIOD_CURRENT,i+HORIZON) - iLow(_Symbol,PERIOD_CURRENT,i+HORIZON), iOpen(_Symbol,PERIOD_CURRENT,i+HORIZON) - iClose(_Symbol,PERIOD_CURRENT,i+HORIZON), iHigh(_Symbol,PERIOD_CURRENT,i+HORIZON) - iLow(_Symbol,PERIOD_CURRENT,i+HORIZON), iHigh(_Symbol,PERIOD_CURRENT,i+HORIZON) - iClose(_Symbol,PERIOD_CURRENT,i+HORIZON), iLow(_Symbol,PERIOD_CURRENT,i+HORIZON) - iClose(_Symbol,PERIOD_CURRENT,i+HORIZON), //--- Growth between price and the moving averages iClose(_Symbol,PERIOD_CURRENT,i+HORIZON) - ma_fast_reading[i+HORIZON], iClose(_Symbol,PERIOD_CURRENT,i+HORIZON) - ma_slow_reading[i+HORIZON] ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef HORIZON #undef MA_PERIOD_FAST #undef MA_PERIOD_SLOW #undef MA_TYPE //+------------------------------------------------------------------+
Pythonでのデータ分析
その後、標準のPythonライブラリを読み込みます。
from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import TimeSeriesSplit,cross_val_score
次に、時系列交差検証オブジェクトを作成します。
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON) get_modelという新しい関数を作成します。この関数は、先ほど使用したのと同じランダムフォレストモデルの新しいインスタンスを返します。
def get_model(): return(RandomForestRegressor())
ここでは、新たに作成したすべての特徴量による改善効果を慎重に評価していきます。そのために、2種類の入力データを定義します。1つ目はすべての特徴量を含む入力、2つ目は従来の基本的な入力(始値、高値、安値、終値および移動平均)です。その上で、予測対象(ターゲット)も定義します。
X = train.iloc[:,1:-3] X_classic = train.iloc[:,1:7] y = train.iloc[:,-3:]
次に、各ターゲットに対する予測性能の改善度を測定します。そのために、各ターゲットごとに性能を格納する配列を用意し、それぞれに対して、従来の入力データを用いた場合の性能と、新たに取得したすべてのデータを用いた場合の性能を追加していきます。
target_1 = [] target_2 = [] target_3 = []
ご覧の通り、将来のEUR/USDリターンを予測する場合においては、今回取得した詳細な市場データによって性能が大幅に改善されています。誤差は半分以下に低減しているように見受けられます。
target_1.append(np.mean(np.abs(cross_val_score(get_model(),X_classic,y.iloc[:,0],cv=tscv,scoring='neg_mean_squared_error')))) target_1.append(np.mean(np.abs(cross_val_score(get_model(),X,y.iloc[:,0],cv=tscv,scoring='neg_mean_squared_error'))))

図12:詳細な市場データにより、アウトオブサンプルでのEUR/USDリターン予測精度が向上した
同様に、短期移動平均(30期間移動平均)の予測においても改善が確認されました。手動で作成した新しい市場データにより、誤差は非常に低い水準まで改善されており、二乗平均平方根誤差(RMSE)において顕著な向上が見られます。
target_2.append(np.mean(np.abs(cross_val_score(get_model(),X_classic,y.iloc[:,1],cv=tscv,scoring='neg_mean_squared_error')))) target_2.append(np.mean(np.abs(cross_val_score(get_model(),X,y.iloc[:,1],cv=tscv,scoring='neg_mean_squared_error'))))

図13:より詳細な市場データを用いることで、30期間移動平均の将来値予測においても誤差の低減が確認された
一方で、長期移動平均(60期間移動平均)については、手動で作成した詳細データの恩恵はそれほど大きくありませんでした。つまり、今回の取り組みによる効果は主に最初の2つのターゲットに対して顕著に現れていると考えられます。
target_3.append(np.mean(np.abs(cross_val_score(get_model(),X_classic,y.iloc[:,2],cv=tscv,scoring='neg_mean_squared_error')))) target_3.append(np.mean(np.abs(cross_val_score(get_model(),X,y.iloc[:,2],cv=tscv,scoring='neg_mean_squared_error'))))

図14:60期間移動平均は依然として予測が難しく、今回作成した詳細な市場データによる改善効果も限定的であった
MQL5での改善点の実装
そこから、アプリケーションにいくつかの変更を加える必要があります。まず、入力データの形状を変更する必要があります。初期状態では6でしたが、現在は20に設定する必要があります。。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup our indicators ma_fast_handler = iMA("EURUSD",PERIOD_D1,30,0,MODE_SMA,PRICE_CLOSE); ma_slow_handler = iMA("EURUSD",PERIOD_D1,60,0,MODE_SMA,PRICE_CLOSE); atr_handler = iATR("EURUSD",PERIOD_D1,14); //--- Setup the ONNX model model = OnnxCreateFromBuffer(onnx_proto,ONNX_DATA_TYPE_FLOAT); //--- Define the model parameter shape ulong input_shape[] = {1,20}; ulong output_shape[] = {1,3}; OnnxSetInputShape(model,0,input_shape); OnnxSetOutputShape(model,0,output_shape); model_inputs = vectorf::Zeros(20); model_outputs = vectorf::Zeros(3); if(model != INVALID_HANDLE) { return(INIT_SUCCEEDED); } //--- return(INIT_FAILED); }
さらに、価格データが更新される際には、モデルへの入力として扱う特徴量が大幅に増加しますが、モデルの出力の扱い方については、従来とほぼ同様のままで問題ありません。
//--- Update the time if(current_time != time_stamp) { time_stamp = current_time; //--- Fetch indicator current readings CopyBuffer(ma_fast_handler,0,0,10,ma_fast_reading); CopyBuffer(ma_slow_handler,0,0,10,ma_slow_reading); CopyBuffer(atr_handler,0,0,10,atr_reading); double open = iOpen("EURUSD",PERIOD_D1,0); double close = iClose("EURUSD",PERIOD_D1,0); double high = iHigh("EURUSD",PERIOD_D1,0); double low = iLow("EURUSD",PERIOD_D1,0); model_inputs[0] = (float) open; model_inputs[1] = (float) high; model_inputs[2] = (float) low; model_inputs[3] = (float) close; model_inputs[4] = (float) ma_fast_reading[0]; model_inputs[5] = (float) ma_slow_reading[0]; model_inputs[6] = (float) (iOpen(_Symbol,PERIOD_CURRENT,0) - iOpen(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[7] = (float) (iHigh(_Symbol,PERIOD_CURRENT,0) - iHigh(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[8] = (float) (iLow(_Symbol,PERIOD_CURRENT,0) - iLow(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[9] = (float) (iClose(_Symbol,PERIOD_CURRENT,0) - iClose(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[10] = (float) (ma_fast_reading[0] - ma_fast_reading[0+HORIZON]); model_inputs[11] = (float) (ma_slow_reading[0] - ma_slow_reading[0+HORIZON]); model_inputs[12] = (float) (iOpen(_Symbol,PERIOD_CURRENT,0+HORIZON) - iHigh(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[13] = (float) (iOpen(_Symbol,PERIOD_CURRENT,0+HORIZON) - iLow(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[14] = (float) (iOpen(_Symbol,PERIOD_CURRENT,0+HORIZON) - iClose(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[15] = (float) (iHigh(_Symbol,PERIOD_CURRENT,0+HORIZON) - iLow(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[16] = (float) (iHigh(_Symbol,PERIOD_CURRENT,0+HORIZON) - iClose(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[17] = (float) (iLow(_Symbol,PERIOD_CURRENT,0+HORIZON) - iClose(_Symbol,PERIOD_CURRENT,0+HORIZON)); model_inputs[18] = (float) (iClose(_Symbol,PERIOD_CURRENT,0+HORIZON) - ma_fast_reading[0+HORIZON]); model_inputs[19] = (float) (iClose(_Symbol,PERIOD_CURRENT,0+HORIZON) - ma_slow_reading[0+HORIZON]); ask = SymbolInfoDouble("EURUSD",SYMBOL_ASK); bid = SymbolInfoDouble("EURUSD",SYMBOL_BID); //--- If we have no open positions if(PositionsTotal() == 0) { if(!(OnnxRun(model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_outputs))) { Comment("Failed to obtain a forecast from our model: ",GetLastError()); } else { Comment("Forecast: ",model_outputs); //--- Trading rules if(((model_outputs[0] > 0) && (model_outputs[1] > 0) && (model_outputs[2] > 0)) || ((ma_fast_reading[0] > ma_slow_reading[0]) && (low > ma_fast_reading[0]))) { //--- Buy signal Trade.Buy(0.01,"EURUSD",ask,ask-(atr_reading[0] * 2),ask+(atr_reading[0] * 2),""); } else if(((model_outputs[0] < 0) && (model_outputs[1] < 0) && (model_outputs[2] < 0)) || ((ma_fast_reading[0] < ma_slow_reading[0]) && (low < ma_slow_reading[0]))) { //--- Sell signal Trade.Sell(0.01,"EURUSD",bid,bid+(atr_reading[0] * 2),bid-(atr_reading[0] * 2),""); } } } } }
最後に、今回の「ビッグデータ」アプローチによる市場分析で得られたエクイティカーブを確認すると、残念ながらシステム全体に多くのノイズが混入していることが分かります。その結果、システムはもはや収益性を維持できておらず、ボラティリティが高く、これまで見られていた右肩上がりのトレンドも失われています。

図15:今回生成された新しいエクイティカーブは、過度にボラタイルな挙動を示している
さらに、詳細な統計的パフォーマンス分析を見ると、全体的な性能が悪化していることが確認できます。再びロングエントリーに偏ったバイアス付きのエントリー問題が発生しており、期待収益は再びマイナスに転じています。また、総純利益もマイナスとなっています。これらの結果から、本稿の検討においてこれまでに作成したバージョンの中では、バージョン4が最も優れた性能を示していたことが明確に分かります。

図16:アプリケーションの最終バージョンによってもたらされた結果の詳細な分析
結論
本記事では、一般的には時代遅れであり過度に広く利用されているため収益性がないと考えられている戦略であっても、改善が可能であることを読者に示しました。一般的な議論とは異なり、これらの戦略は慎重に改善し再設計することで、新たなパフォーマンス水準へと到達させることができます。戦略の弱点を丁寧に特定することで、必要な改善点を導くための有益な手がかりを得ることができます。本記事では、戦略に混入しているノイズを低減する方法についても解説しており、読者自身が保有する従来型の戦略についても、少しの工夫によって依然として活用可能な領域が存在することを示しています。
また本記事は、古典的な教師あり機械学習における代表的な落とし穴の一つについても示しました。関連連載「機械学習の限界を克服する」で議論した通り、統計モデルの性能を測定するために用いられる誤差指標は、アルゴリズムトレーダーが重視する実際のパフォーマンス指標と必ずしも一致するものではありません。本テーマの復習が必要な読者は、別途こちらを参照してください。
既に内容をご理解いただいている読者にとっては、RMSEを無批判に信頼することの危険性がより明確になったと考えられます。詳細な市場データを用いた分析では、EUR/USDリターンおよび30期間SMAの予測においてアウトオブサンプルRMSEが大幅に改善されていました。しかしながら、このRMSEの改善は、必ずしも収益性の向上には結びつかず、むしろ望ましくない影響をもたらしました。したがって、読者は古典的な教師あり統計学習の限界について、より深く理解することができたはずです。
| ファイル名 | ファイルの説明 |
|---|---|
| Fetch Data.mq5 | EUR/USD為替レートの基本的な市場データ(4つのOHLC価格および2つの移動平均)を取得するために作成したスクリプト |
| Fetch Data 2.mq5 | EUR/USD為替レートの詳細な市場データ(20列)を取得するために作成したスクリプト |
| MA_Crossover_V1.mq5 | 移動平均クロスオーバー戦略の最も広く知られるバージョンであり、ベースライン性能を確立するために実装されたもの |
| MA_Crossover_V2.mq5 | 手動で考え得る範囲で改善を加え、戦略の長期的パフォーマンスを向上させたバージョン |
| MA_Crossover_V3.mq5 | 簡易的な統計モデルによって導かれたが、ロングエントリーへのバイアスを学習してしまったバージョン |
| MA_Crossover_V4.mq5 | 前バージョンのバイアスを修正しつつ、収益性を維持した最良の戦略バージョン |
| MA_Crossover_V5.mq5 | EUR/USDの大規模かつ詳細な観測データを用いて構築した最終バージョンの戦略 |
| Advanced_Moving_Averages.ipynb | 市場データの分析に使用したJupyter notebook |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20488
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5でかぎ足をマスターする(第2回):かぎ足ベース自動売買の実装
共和分株式による統計的裁定取引(第8回):ポートフォリオのリバランスのためのローリングウィンドウ固有ベクトル比較
機械学習の限界を克服する(第9回):自己教師あり学習を用いた金融における相関ベース特徴学習
利益強化アーキテクチャ:多層型口座保護
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索