MQL5で自己最適化エキスパートアドバイザーを構築する
はじめに
今日のダイナミックな金融市場において、自己最適化自動システムは不可欠です。デジタル時代の市場は、特に高頻度トレーダーによるアルゴリズム取引の普及により、著しく不安定になっています。SECのワーキングペーパー高頻度取引記事によると、欧米では高頻度取引が全取引の半分近くを占めています。
MQL5は、ある種の信念に反して、このタスクに理想的に適しています。そのAPIは広範な行列関数とベクトル関数を提供し、コンパクトな機械学習モデルの作成を可能にします。この紹介では、自己最適化ボットを構築するためにMQL5を使用することに重点を置いています。オブジェクト指向プログラミングのアプローチは、反復的なコーディングを減らし、さまざまな時間枠や市場条件への適応性を高めます。
ONNXやPythonのような代用品ではなく、MQL5の行列とベクトルの機能を選択することには、かなりの利点があります。ONNXモデルを使用する場合、各取引銘柄ごとに個別のモデルインスタンスが必要となり、時間枠の調整など細かなパラメータ変更のために新しいモデルが必要となります。しかしMQL5は、様々な条件下で多数のモデルを管理する必要なく、適応性を提供します。
あらすじ:自己最適化EAの開発
EAがどの程度効果的に機能しているかを評価する枠組みが必要です。決定的なパフォーマンス指標が定義されれば、それに従って選択した指標を最大化または最小化することができます。価格予測のための教師あり機械学習モデルを構築する場合、私たちの目的は予測値と実際の観測値との誤差を最小化することです。一方、強化学習問題では、割引後の期待報酬の合計を最大化することが目標となります。
この記事では、EAが予測した将来の予想価格と、実際に観測された将来の価格との差を最小化します。これは、これらの価格差の絶対値を計算することで実現できます。
この記事では、自己最適化EAを構築するための基本的な側面について説明します。今後の記事では、MQL5 APIのより高度な機能を使用して自己最適化EAを作成するための、より高度な方法論について掘り下げていきます。
この記事を読むと、次のことがわかるようになります。
- 便利な行列関数とベクトル関数のセレクション
- MQL5におけるオブジェクト指向プログラミングの基礎概念
- MQL5で動的かつ自己適応EAを構築するためのフレームワーク
勾配降下による自己最適化
私たちの目標は、現在の市場状況に合わせて一貫して再調整できるEAを設計することです。これを実現するために、MQL5で勾配降下アルゴリズムを実装します。勾配降下アルゴリズムに馴染みのない読者のために、DJが音響機材を設定するプロセスにアルゴリズムを例えてみるといいかもしれません。あなたがDJだと想像してください。機材の電源を入れると、音量が大きすぎます。次はどうしますか。音量を下げることになるでしょう。しかし、今度は音量が小さすぎるので、音量を上げることになるでしょう。この増減のせめぎ合いは、バランスの取れたレベルを見つけるまで続きます。
勾配降下アルゴリズムも同様の方法で機能します。まず、私たちがいる市場のモデルにおけるランダムな係数から始めます。そして、現在の係数によって生じる誤差を測定します。DJがおこなうのと同様に、誤差が大きくなる方向とは逆方向に、モデルの係数を繰り返し調整します。線形モデルの係数など、入力に対して導関数をとり、誤差がどの方向に増加しているかを推測します。
さらに、係数と並んで勾配降下におけるもう1つの重要なパラメータは学習率です。学習率は、DJが音量調節を調整するたびに、どれくらい音量を変えるかに似ています。学習率は、モデルのパラメータを調整するたびに取るステップの大きさを支配します。学習率が大きすぎたり小さすぎたりすると、モデルは最適に学習できなくなります。
理想的なアプローチは、時間枠やデータスコープを変更しても、各市場のシナリオに応じて学習率や係数を動的に最適化するようにEAを設計することです。この順応性によって、私たちは効果的に市場の正しい側に立ち、ネイティブソリューションの可能性を最大限に活用して、制限のない取引をおこなうことができるようになります。
取引戦略
取引戦略は、テクニカル分析と機械学習のハイブリッドアプローチとなります。移動平均線を用いて、市場のトレンドを判断します。価格が移動平均線を上回れば強気、下回れば弱気と判断します。相場のトレンドを推し量ったら、次に相対力指数(RSI)とウィリアムズパーセントレンジ(WPR)の2つの裏付け指標を確認します。
RSI指標は、0から100の間の数値を示します。通常、RSIの数値が70を上回れば買われすぎと判断して売り、30を下回れば売られすぎと判断して買うべきです。この戦略は、株式や商品のように限られた数しか存在しない証券を取引する場合には有効ですが、通貨ペアを取引する場合には、この戦略は直感的に意味をなしません。通貨は売られすぎたり買われすぎたりすることはなく、中央銀行が必要と判断すれば通貨を作り出す料をいくらでも増減できます。したがって、私たちの戦略では、RSIの数値が50を上回ったら、売るのではなく買いたいし、50を下回ったら、買うのではなく売りたいでしょう。
WPR指標は、0から-100までの数値を表示します。RSIと同様、WPR指標は買われすぎと売られすぎのゾーンを特定します。しかし、通貨が買われすぎたり売られすぎたりすることはなく、通貨の供給は無制限であるため、私たちの戦略ではWPRを少し異なるように解釈します。この戦略では、WPR指標が-20を上回ったら買いシグナルとし、-80を下回ったら売りシグナルとします。
もし3つの指標がすべて同じ側に揃えば、最終的に私たちのモデルに将来の予想価格を予測させることになります。モデルの予測が、指標を分析した私たちのセンチメントと一致すればポジションを建て、そうでなければ、モデルと指標が矛盾するシグナルを出していれば、それらが一致するまで待機します。
利食いと損切りのレベルも、現在の市場のボラティリティレベルを使って動的に設定され、価格と移動平均の差の絶対値を取ります。損切りと利食いは、移動平均と終値の間の高さの絶対値の2倍とします。その根拠は、閑散とした相場状況では損切りと利食いがタイトになり、不安定な相場日には損切りと利食い十分に広くなることです。要するに、システム全体が、私たちの介入なしにダイナミックに調整されるのです。
MQL5での実装
始めるには、まず機械学習モデルのクラスを定義する必要があります。オブジェクト指向プログラミング(OOP)の使用には、特にデータサイエンスプロジェクトにおいて多くの利点があります。機械学習モデルを構築し、そのコードを手作業でコピーして、手持ちのすべてのEAに挿入した場合を想像してみてください。数日後、あなたはコードの関数の1つにミスがあったことに気づきます。もしOOPの設計原則を使っていなければ、コピーしたコードの各インスタンスを手作業で調べ、1つ1つ修正しなければなりません。しかし、もしOOP設計の原則を採用しているのであれば、クラスを修正し、他のプログラムをコンパイルし直すだけです。要するに、OOPの設計原則は、コードの何千もの異なるインスタンスを明確かつ正確に制御することができます。
MetaTrader 5エディターで新しいクラスを構築することから始めます。
図1:MQL5で新しいクラスを作成する
そこからクラスの名前を設定します。クラスがIncludeフォルダに保存されていることを確認してください。さらに、各クラスに独自のフォルダを割り当て、そのフォルダにクラスと同じ名前を付けることをお勧めします。そうすることで、将来これらのクラスを見つけるのが簡単になります。
図2:線形回帰クラスの構築
上記のステップに従えば、MQL5ウィザードはこのようなコードを作成するのに役立つでしょう。
class LinearRegegression { private: public: LinearRegegression(); ~LinearRegegression(); }; LinearRegegression::LinearRegegression() { } LinearRegegression::~LinearRegegression() { }
MQL5でOOPを扱うのが初めての方は、上記のコードを一緒にご覧ください。一番上にはクラスの定義があります。classキーワードは、このコード全体をクラスとして定義するもので、classキーワードの後にはクラスの名前が入ります。そこからクラスの本体に入ります。privateキーワードは、クラスの外からアクセスできない変数や関数を定義するのに対し、publicキーワードは、クラスの外からアクセスできる変数や関数を定義します。クラス定義の中にすでに2つの関数があることに注目してください。
最初の関数LinearRegression()はコンストラクタと呼ばれます。これは、クラスの新しいインスタンスを起動するたびに最初に呼び出される関数で、最後の関数~LinearRegression()はデストラクタと呼ばれます。デストラクタは、チャートのクラスを削除するたびに呼び出される最後の関数です。
線形回帰モデルを計算するために使用する変数の定義に進みます。
- max_learning_rate_power:良い学習率をどれだけ高く探索するかを定義
- fetch:市場から分析したいローソク足の数
- Startとpredict:いつデータの取得を開始し、そこから予測をおこなうかを定義
- look_ahead:何ステップ先の未来を予測するかを定義
- mae_array:エラーメトリックを格納する配列
- trained:モデルが訓練され、使用可能かどうかを示すフラグ
- epochs_power:モデルの学習に使用するエポック数を定義
- mae_trainとmae_validation:訓練と検証のエラーメトリクスを格納するベクトル
- x_validation、とy_validation、x_train、y_train:xとyの検証用と訓練用のベクトル(訓練データと検証データが含まれる)
- mとb:モデルに適切なmとbの係数の推定値が含まれるベクトル
- forecast:モデルの予測
- learning_rate_power:学習率を定義するために0.1を上げる冪乗
- epochs:モデルを訓練する回数
- n:常にfetchと等しいデータの行数
- output_end、output_start、input_end、input_start:訓練とテストの分割を定義
private: //This is the highest power that we will raise ten to, as we are searching for coefficients ulong max_learning_rate_power; //This is how many bars we should fetch int fetch; //This is where we will start collecting data, it is the end of our validation data set. datetime start,predict; //This is how many steps into the future we want to forecast int look_ahead; //This is the array that will contain our MAE readings from testing different learning rates on the validation data double mae_array[30]; //Trained flag to inform us if the model has been fit and optimised succesfuly and is ready for use bool trained; //The number to raise the power of 10 buy when calculating the number of epochs int epochs_power; //Our error metrics vector mae_train,mae_validation; //This vector contains our inputs validation and training set vector x_validation,x_train; //This vector contains our outputs validation and training set vector y_validation,y_train; //This vector contains our predictions on the validation set vector y_hat_validation,y_hat_train; //This vector contains our gradient coefficient vector m; //This vector contains our model bias vector b; //This is our model's forecast double forecast; //This is our current learning rate power ulong learning_rate_power; //This is the learning rate power we are currently evaluating int lr_error_index; //This is our current learning rate double learning_rate; //This is the number of rounds we will allow whilst training our model double epochs; //This is used in calculations, it is the number of rows in our data, or the fetch size. ulong n; //These are the times for our input and output data datetime output_end,output_start,input_end,input_start; //These are the index times for our input and output data int index_output_end,index_output_start,index_input_end,index_input_start; //This is the value we will use to scale our data double first_reading; bool allowed_to_evaluate; //Update the learning rate bool UpdateLearningRate(void); //Update the number of epochs bool UpdateEpochs(void); //Set the number of epochs bool SetEpochs(int _epochs_power); //Reset the number of epochs bool ResetEpochs(void); //Reset the learning rate bool ResetLearningRate(void); //This function will fit the coeffeicients bool Fit(void); //This function evaluates the current settings bool Evaluate(ulong _index,int _epochs_power); //This function will scale the input data bool ScaleInputs(void); //This function sets the learning rate bool SetLearningRate(ulong _learning_rate_power);
次に、クラス内のpublic定義に移ります。
public: //Constructor LinearRegression(); //Fetch Current Validation Data bool GetCurrentValidationData(void); //Initialise the LinearRegressor Model void Init(int _fetch,int _look_ahead); //Function to determine if the model has been trained and is ready for use. bool Trained(void); //A function to train the model using the best learning rate and the most recent prices bool Train(void); //A function to predict future price using the current price. double Predict(void); //Destructor ~LinearRegression();
上記のコードでは、クラス内の関数と各関数のシグネチャを定義しましたが、まだ各関数を実装していません。コンストラクタの実装から始めましょう。
このコンストラクタは入力を取りません。これはデフォルトあるいはノンパラメトリックコンストラクタと呼ばれます。さらに、コンストラクタには戻り値の型がなく、voidですらないことにご注意ください。
LinearRegression::LinearRegression() { Print("Current Symbol: ",_Symbol); }
このコンストラクタは、設計上、現在の取引銘柄を表示する以外には何も実行しません。この意図的な設計上の選択によって、EA入力から入力を取得し、それらの入力に基づいて線形回帰オブジェクトを初期化するために使用することができます。特に、コンストラクタは変数やデフォルト値の設定を控えており、タスクはInit()関数を定義した別のメソッドのために予約されています。つまり、コンストラクタをInit()関数から分離することは、EAの設定から動的に入力を収集することを可能にするため、非常に有利です。もしコンストラクタが変数の初期化を担当していたら、この動的な入力収集は制約されていたでしょう。
それでは、変数をデフォルト値に初期化するInit()関数を定義しましょう。変数を初期化した後、Initメソッドは自動的に入力のスケーリングとモデルの学習を試みます。
void LinearRegression::Init(int _fetch,int _look_ahead) { //Clear The Chart ObjectsDeleteAll(0); //Allow evaluations allowed_to_evaluate = true; //Epochs power epochs_power =4; //Set the number of epochs epochs = 5 * MathPow(10,epochs_power); //Has the model been trained? trained = false; //Set the maximum learning rate power max_learning_rate_power = 30; //Set the end of our validation data start = iTime(_Symbol,PERIOD_CURRENT,1); //This is how much data we're going to fetch this.fetch = _fetch - 1; //This is how far into the future we want to forecast this.look_ahead = _look_ahead + 1; //Set the gradient coefficient to a random value m = vector::Zeros(1); //Set the bias to a random value b = vector::Zeros(1); //Set the forecast to 0 forecast = 0; //Our model's learning rate will start at 0 learning_rate_power = 0; //This is the learning rate we are evaluting lr_error_index = 0; mae_train = vector::Full(1,MathPow(10,100)); mae_validation = vector::Full(30,MathPow(10,10000)); //Set the initial learning rate learning_rate = MathPow(0.1,(learning_rate_power)); //Set the number of rows n = fetch; if(GetCurrentValidationData()) { //Scale the data ScaleInputs(); //Fit the model Fit(); } }
predict関数は、パラメータなしでdoubleデータ型を返すように設計されています。関数をクラスメンバーとして指定するには、クラス名の前にコロン2つと関数名をつけることにご注意ください。
predict関数は、まずTrained()関数を呼び出して、モデルが訓練を受けたかどうかを確認します。モデルの訓練状態が確認されたら、リアルタイムデータ、具体的には現在価格と終値、そして予測コンテキストのためのタイムスタンプデータの収集に進みます。現在の価格にmを掛け、bを加えて予測価格を算出します。 そして予測を返すか、モデルが訓練されていない場合は0を返します。
double LinearRegression::Predict(void) { if(Trained()) { double _current_reading = iClose(_Symbol,PERIOD_CURRENT,0); predict = iTime(_Symbol,PERIOD_CURRENT,0); double prediction = (m[0]*_current_reading)+b[0]; if(prediction > _current_reading) { Comment("Buy, forecast: ",prediction); } else if(prediction < _current_reading) { Comment("Sell, forecast: ",prediction); } ObjectCreate(0,"prediction point",OBJ_VLINE,0,predict,0); ObjectCreate(0,"forecast",OBJ_HLINE,0,predict,prediction); return(prediction); } return(0); }
次の関数は、訓練データと検証データを収集することで、常に最新の市場データを取得します。この処理は、過去の価格データをベクトルに転送するために特別に設計されたcopy_rates vector関数によって達成されます。
データをフェッチした後、ベクトルサイズ関数を使って、ベクトルが同じサイズであることを確認しなければなりません。
bool LinearRegression::GetCurrentValidationData(void) { //Indexes index_output_end = 1; index_output_start = index_output_end + fetch; index_input_end = index_output_end + look_ahead; index_input_start = index_output_start + look_ahead; //Assigning time stamps output_end = iTime(Symbol(),PERIOD_CURRENT,index_output_end); output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start); input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end); input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start); //Get the output data if(!y_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch)) { Print("Failed to get market data: ",GetLastError()); return(false); } //Get the input data if(!x_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch)) { Print("Failed to get market data: ",GetLastError()); return(false); } //Print the vectors we have if(x_validation.Size() != y_validation.Size()) { Print("Failed to get market data: Our vectors aren't the same length."); return(false); } //Print the vectors and plot the data points Print("X validation: ",x_validation); ObjectCreate(0,"X validation end",OBJ_VLINE,0,input_end,0); ObjectCreate(0,"X validation start",OBJ_VLINE,0,input_start,0); //Print the vectors and plot the data points Print("y validation: ",y_validation); ObjectCreate(0,"y validation end",OBJ_VLINE,0,output_end,0); ObjectCreate(0,"y validation start",OBJ_VLINE,0,output_start,0); //Set the training data index_output_end = index_input_start + (look_ahead * 2); index_output_start = index_output_end + fetch; index_input_end = index_output_end + look_ahead; index_input_start = index_output_start + look_ahead; //Assigning time stamps output_end = iTime(Symbol(),PERIOD_CURRENT,index_output_end); output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start); input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end); input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start); //Copy the training data if(!y_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch)) { Print("Error fetching training data ",GetLastError()); } //Copy the training data if(!x_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch)) { Print("Error fetching training data ",GetLastError()); } //Check if the data matches if(x_train.Size() != y_train.Size()) { Print("Error fetching training dataL: The x and y vectors are not the same size"); } //Print the vectors and plot the data points Print("X training: ",x_train); ObjectCreate(0,"X training end",OBJ_VLINE,0,input_end,0); ObjectCreate(0,"X training start",OBJ_VLINE,0,input_start,0); Print("y training: ",y_train); ObjectCreate(0,"y training end",OBJ_VLINE,0,output_end,0); ObjectCreate(0,"y training start",OBJ_VLINE,0,output_start,0); return(true); }
ここでフィット関数を定義します。この関数は、まずmとbの現在の値を用いて、訓練データに対する予測を生成します。その後、実際のYオブザベーションと私たちの予測したYオブザベーションの差の絶対値を計算することで、訓練データ内の誤差を評価します。
誤差が決まったら、別の効率的なベクトル関数Meanを使って誤差ベクトルの算術平均を計算します。
これに続いて、mとbに関する誤差の導関数を近似することで、勾配降下アルゴリズムを実装します。これらの微分近似は、導出された微分の何分の一かで係数を更新する際の指針となります。
シナリオによっては、NaNや無限大のような無効な係数が得られる可能性があるため、係数の更新時には新しい係数を検証することが不可欠です。この検証ステップは、更新された係数の完全性と使いやすさを保証するために極めて重要です。
bool LinearRegression::Fit() { Print("Fitting a linear regression on the training set with learning rate ",learning_rate_power); Print("Evalutaions: ",allowed_to_evaluate); for(int i =0; i < epochs;i++) { //Measure error y_hat_train = (m[0]*x_train) + b[0]; vector y_minus_y_hat = (y_train - y_hat_train); vector y_minus_y_hat_sqaured = MathAbs((y_train - y_hat_train)); mae_train.Set(0,( y_minus_y_hat_sqaured.Mean())); vector x_times_y_minus_y_hat = (x_train*(y_train -y_hat_train)); //Aproximate the derivatives double derivative_m = (-2.0/n) * x_times_y_minus_y_hat.Sum(); double derivative_b = (-2.0/n) * y_minus_y_hat.Sum(); //Update the linear parameters m[0] = m[0] - (learning_rate * derivative_m); b[0] = b[0] - (learning_rate * derivative_b); } //Finished fitting the coefficients Print("Fit on training data complete.\nm: ",m[0]," b: ",b[0]," mae ",mae_train[0],"\nlearning rate: ",learning_rate); if(allowed_to_evaluate) { Evaluate(learning_rate_power,epochs_power); } //Return true return(true); }
次にevaluate関数の定義に移ります。この関数は、取引する各銘柄に最適な学習率を選択する役割を担っています。まずは係数の妥当性を検証することから始めます。係数が0またはNaN値を含む場合は、リセットされます。
この検証プロセスの理論的根拠は、様々な学習率において最も低い検証誤差をもたらす係数を細心の注意を払って選択することです。検証誤差が大きい無効な係数は、係数選択段階で考慮から除外されます。逆に、有効な係数はさらなる分析のために保存されます。
その後、これらの保存された係数を使用して、検証データに対する予測を生成し、誤差を評価します。この反復プロセスには、学習率の更新、モデルのフィッティング、誤差の評価が含まれます。最大反復限界は30に設定され、これは最大学習率のパワーに相当します。
評価関数全体を通して、連続的な確認により、インデックスが最大学習率パワーの範囲内に留まることを保証します。絶対誤差データをベクトルに集めて効率的に処理し、vector.Min()やArgmin()のようなベクトル関数を用いて、最小の検証誤差に関連する学習率パワーを特定します。
//This function evaluates the current coefficient settings and learning rate bool LinearRegression::Evaluate(ulong _index) { Print("Evaluating the coefficients m:",m[0]," b: ",b[0]," at learning rate: ",learning_rate); //First check if the coefficient and learning rate are valid if((m.HasNan() > 0 || b.HasNan() > 0 || m[0] == 0 || b[0] == 0 || _index > max_learning_rate_power) && (_index < max_learning_rate_power)) { Print("Coefficients are invalid"); m[0] = 0; b[0] = 0 ; mae_array[_index] = MathPow(10,100000); //Update the learning rate UpdateLearningRate(); //Fit the model again Fit(); } else { //Validation predictions if(_index < max_learning_rate_power) { Print("Coefficients are valid, solution at index ",_index); y_hat_validation = (m[0] * x_validation) + b[0]; vector y_minus_y_hat_squared = MathAbs(y_validation - y_hat_validation); //If everything is fine, let's assess the validation mae mae_array[_index] = (1.0/n) * y_minus_y_hat_squared.Sum(); //What was the validation error? Print("Validation error: ",(1.0/n) * y_minus_y_hat_squared.Sum()); //Update the learning rate UpdateLearningRate(); //Fit the model again Fit(); } } if(_index == max_learning_rate_power) { for(int i = 0; i < max_learning_rate_power;i++) { mae_validation[i] = mae_array[i]; } allowed_to_evaluate = false; trained = true; Print("Validation mae: \n",mae_validation); Print("Lowest validation mae: ",mae_validation.Min()); ulong chosen_learning_rate = mae_validation.ArgMin(); Print("Chosen learning rate ",MathPow(0.1,(chosen_learning_rate))); SetLearningRate(chosen_learning_rate); Fit(); } return(true); }
また、入力をスケーリングする関数も定義します。この関数を理解するのは簡単で、すべての入力を学習ベクトルの最初の項目で割ります。
//This function will scale our inputs bool LinearRegression::ScaleInputs(void) { //Set the first reading first_reading = x_train[0]; x_train = x_train / first_reading; x_validation = x_validation / first_reading; return(true); }
そこからdestrcutorを定義し、destrcutorは先ほど最適化した係数をすべてリセットします。
LinearRegression::~LinearRegression()
{
ResetLearningRate();
ResetLastError();
}
デストラクタによって呼び出される関数は、以下のように定義されています。
bool LinearRegression::ResetLearningRate(void) { learning_rate_power = 0; learning_rate = MathPow(0.1,learning_rate_power); return(true); }
これをすべてまとめると、クラスの定義になります。
#property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com" #property version "1.00" class LinearRegression { private: //This is the highest power that we will raise ten to, as we are searching for coefficients ulong max_learning_rate_power; //This is how many bars we should fetch int fetch; //This is where we will start collecting data, it is the end of our validation data set. datetime start,predict; //This is how many steps into the future we want to forecast int look_ahead; //This is the array that will contain our MAE readings from testing different learning rates on the validation data double mae_array[30]; //Trained flag to inform us if the model has been fit and optimised succesfuly and is ready for use bool trained; //The number to raise the power of 10 buy when calculating the number of epochs int epochs_power; //Our error metrics vector mae_train,mae_validation; //This vector contains our inputs validation and training set vector x_validation,x_train; //This vector contains our outputs validation and training set vector y_validation,y_train; //This vector contains our predictions on the validation set vector y_hat_validation,y_hat_train; //This vector contains our gradient coefficient vector m; //This vector contains our model bias vector b; //This is our model's forecast double forecast; //This is our current learning rate power ulong learning_rate_power; //This is the learning rate power we are currently evaluating int lr_error_index; //This is our current learning rate double learning_rate; //This is the number of rounds we will allow whilst training our model double epochs; //This is used in calculations, it is the number of rows in our data, or the fetch size. ulong n; //These are the times for our input and output data datetime output_end,output_start,input_end,input_start; //These are the index times for our input and output data int index_output_end,index_output_start,index_input_end,index_input_start; //This is the value we will use to scale our data double first_reading; bool allowed_to_evaluate; //Update the learning rate bool UpdateLearningRate(void); //Update the number of epochs bool UpdateEpochs(void); //Set the number of epochs bool SetEpochs(int _epochs_power); //Reset the number of epochs bool ResetEpochs(void); //Reset the learning rate bool ResetLearningRate(void); //This function will fit the coeffeicients bool Fit(void); //This function evaluates the current settings bool Evaluate(ulong _index); //This function will scale the input data bool ScaleInputs(void); //This function sets the learning rate bool SetLearningRate(ulong _learning_rate_power); public: //Constructor LinearRegression(); //Fetch Current Validation Data bool GetCurrentValidationData(void); //Initialise the LinearRegressor Model void Init(int _fetch,int _look_ahead); //Function to determine if the model has been trained and is ready for use. bool Trained(void); //A function to train the model using the best learning rate and the most recent prices bool Train(void); //A function to predict future price using the current price. double Predict(void); //Destructor ~LinearRegression(); }; bool LinearRegression::UpdateEpochs(void) { epochs_power = epochs_power + 1; epochs = MathPow(10,epochs_power); return(true); } bool LinearRegression::ResetEpochs(void) { epochs_power = 0 ; epochs = MathPow(10,epochs_power); return(true); } bool LinearRegression::SetEpochs(int _epochs_power) { epochs_power = _epochs_power; epochs = MathPow(10,epochs_power); return(true); } double LinearRegression::Predict(void) { if(Trained()) { double _current_reading = iClose(_Symbol,PERIOD_CURRENT,0); predict = iTime(_Symbol,PERIOD_CURRENT,0); double prediction = (m[0]*_current_reading)+b[0]; if(prediction > _current_reading) { Comment("Buy, forecast: ",prediction); } else if(prediction < _current_reading) { Comment("Sell, forecast: ",prediction); } ObjectCreate(0,"prediction point",OBJ_VLINE,0,predict,0); ObjectCreate(0,"forecast",OBJ_HLINE,0,predict,prediction); return(prediction); } return(0); } bool LinearRegression::GetCurrentValidationData(void) { //Indexes index_output_end = 1; index_output_start = index_output_end + fetch; index_input_end = index_output_end + look_ahead; index_input_start = index_output_start + look_ahead; //Assigning time stamps output_end = iTime(Symbol(),PERIOD_CURRENT,index_output_end); output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start); input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end); input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start); //Get the output data if(!y_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch)) { Print("Failed to get market data: ",GetLastError()); return(false); } //Get the input data if(!x_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch)) { Print("Failed to get market data: ",GetLastError()); return(false); } //Print the vectors we have if(x_validation.Size() != y_validation.Size()) { Print("Failed to get market data: Our vectors aren't the same length."); return(false); } //Print the vectors and plot the data points Print("X validation: ",x_validation); ObjectCreate(0,"X validation end",OBJ_VLINE,0,input_end,0); ObjectCreate(0,"X validation start",OBJ_VLINE,0,input_start,0); //Print the vectors and plot the data points Print("y validation: ",y_validation); ObjectCreate(0,"y validation end",OBJ_VLINE,0,output_end,0); ObjectCreate(0,"y validation start",OBJ_VLINE,0,output_start,0); //Set the training data index_output_end = index_input_start + (look_ahead * 2); index_output_start = index_output_end + fetch; index_input_end = index_output_end + look_ahead; index_input_start = index_output_start + look_ahead; //Assigning time stamps output_end = iTime(Symbol(),PERIOD_CURRENT,index_output_end); output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start); input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end); input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start); //Copy the training data if(!y_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch)) { Print("Error fetching training data ",GetLastError()); } //Copy the training data if(!x_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch)) { Print("Error fetching training data ",GetLastError()); } //Check if the data matches if(x_train.Size() != y_train.Size()) { Print("Error fetching training dataL: The x and y vectors are not the same size"); } //Print the vectors and plot the data points Print("X training: ",x_train); ObjectCreate(0,"X training end",OBJ_VLINE,0,input_end,0); ObjectCreate(0,"X training start",OBJ_VLINE,0,input_start,0); Print("y training: ",y_train); ObjectCreate(0,"y training end",OBJ_VLINE,0,output_end,0); ObjectCreate(0,"y training start",OBJ_VLINE,0,output_start,0); return(true); } bool LinearRegression::Train(void) { m = vector::Zeros(1); //Set the bias to a random value b = vector::Zeros(1); forecast = 0; if(GetCurrentValidationData()) { if(Fit()) { Print("Model last updated: ",iTime(_Symbol,PERIOD_CURRENT,0)); return(true); } } return(false); } void LinearRegression::Init(int _fetch,int _look_ahead) { //Clear The Chart ObjectsDeleteAll(0); //Allow evaluations allowed_to_evaluate = true; //Epochs power epochs_power =4; //Set the number of epochs epochs = 5 * MathPow(10,epochs_power); //Has the model been trained? trained = false; //Set the maximum learning rate power max_learning_rate_power = 30; //Set the end of our validation data start = iTime(_Symbol,PERIOD_CURRENT,1); //This is how much data we're going to fetch this.fetch = _fetch - 1; //This is how far into the future we want to forecast this.look_ahead = _look_ahead + 1; //Set the gradient coefficient to a random value m = vector::Zeros(1); //Set the bias to a random value b = vector::Zeros(1); //Set the forecast to 0 forecast = 0; //Our model's learning rate will start at 0 learning_rate_power = 0; //This is the learning rate we are evaluting lr_error_index = 0; mae_train = vector::Full(1,MathPow(10,100)); mae_validation = vector::Full(30,MathPow(10,10000)); //Set the initial learning rate learning_rate = MathPow(0.1,(learning_rate_power)); //Set the number of rows n = fetch; if(GetCurrentValidationData()) { //Scale the data ScaleInputs(); //Fit the model Fit(); } } bool LinearRegression::Trained(void) { return(trained); } bool LinearRegression::SetLearningRate(ulong _learning_rate_power) { learning_rate_power = _learning_rate_power; learning_rate = MathPow(0.1,(learning_rate_power)); return(true); } bool LinearRegression::UpdateLearningRate(void) { learning_rate_power = learning_rate_power + 1; learning_rate = MathPow(0.1,(learning_rate_power)); Print("New learning rate: ",learning_rate," learning rate power: ",learning_rate_power); return(true); } bool LinearRegression::ResetLearningRate(void) { learning_rate_power = 0; learning_rate = MathPow(0.1,learning_rate_power); return(true); } LinearRegression::LinearRegression() { Print("Current Symbol: ",_Symbol); } bool LinearRegression::Fit() { Print("Fitting a linear regression on the training set with learning rate ",learning_rate_power); Print("Evalutaions: ",allowed_to_evaluate); for(int i =0; i < epochs;i++) { //Measure error y_hat_train = (m[0]*x_train) + b[0]; vector y_minus_y_hat = (y_train - y_hat_train); vector y_minus_y_hat_sqaured = MathAbs((y_train - y_hat_train)); mae_train.Set(0,( y_minus_y_hat_sqaured.Mean())); vector x_times_y_minus_y_hat = (x_train*(y_train -y_hat_train)); //Aproximate the derivatives double derivative_m = (-2.0/n) * x_times_y_minus_y_hat.Sum(); double derivative_b = (-2.0/n) * y_minus_y_hat.Sum(); //Update the linear parameters m[0] = m[0] - (learning_rate * derivative_m); b[0] = b[0] - (learning_rate * derivative_b); } //Finished fitting the coefficients Print("Fit on training data complete.\nm: ",m[0]," b: ",b[0]," mae ",mae_train[0],"\nlearning rate: ",learning_rate); if(allowed_to_evaluate) { Evaluate(learning_rate_power); } //Return true return(true); } //This function evaluates the current coefficient settings and learning rate bool LinearRegression::Evaluate(ulong _index) { Print("Evaluating the coefficients m:",m[0]," b: ",b[0]," at learning rate: ",learning_rate); //First check if the coefficient and learning rate are valid if((m.HasNan() > 0 || b.HasNan() > 0 || m[0] == 0 || b[0] == 0 || _index > max_learning_rate_power) && (_index < max_learning_rate_power)) { Print("Coefficients are invalid"); m[0] = 0; b[0] = 0 ; mae_array[_index] = MathPow(10,100000); //Update the learning rate UpdateLearningRate(); //Fit the model again Fit(); } else { //Validation predictions if(_index < max_learning_rate_power) { Print("Coefficients are valid, solution at index ",_index); y_hat_validation = (m[0] * x_validation) + b[0]; vector y_minus_y_hat_squared = MathAbs(y_validation - y_hat_validation); //If everything is fine, let's assess the validation mae mae_array[_index] = (1.0/n) * y_minus_y_hat_squared.Sum(); //What was the validation error? Print("Validation error: ",(1.0/n) * y_minus_y_hat_squared.Sum()); //Update the learning rate UpdateLearningRate(); //Fit the model again Fit(); } } if(_index == max_learning_rate_power) { for(int i = 0; i < max_learning_rate_power;i++) { mae_validation[i] = mae_array[i]; } allowed_to_evaluate = false; trained = true; Print("Validation mae: \n",mae_validation); Print("Lowest validation mae: ",mae_validation.Min()); ulong chosen_learning_rate = mae_validation.ArgMin(); Print("Chosen learning rate ",MathPow(0.1,(chosen_learning_rate))); SetLearningRate(chosen_learning_rate); Fit(); } return(true); } //This function will scale our inputs bool LinearRegression::ScaleInputs(void) { //Set the first reading first_reading = x_train[0]; x_train = x_train / first_reading; x_validation = x_validation / first_reading; return(true); } LinearRegression::~LinearRegression() { ResetLearningRate(); ResetEpochs(); ResetLastError(); }
LinearRegressionクラスを定義したので、EAで使用する準備ができました。
まず、新しいEAを作成し、クラスをに含めます。
#property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com" #property version "1.00" //Include our linear regression class #include <LinearRegression/LinearRegression.mqh> LinearRegression ExtLinearRegression;
上記のコードでは、LinearRegressionクラスのデフォルトのコンストラクタを呼び出しています。
そこからは、他の便利なクラスも含まれています。
//Include the trade class #include <Trade/Trade.mqh> CTrade Trade;
EAが必要とする入力を定義します。
//Inputs int input look_ahead = 10; //How many steps into the future should we forecast? int input fetch_data = 100; //How much data should we fetch? int input ma_period = 10; //Moving Average Period int input rsi_period = 10; //RSI Period int input wr_period = 10; //Williams Percent R Period
また、許容される最小取引量や指標バッファを格納するためのベクトルなど、テクニカル分析に役立つその他の変数も定義します。
//Technical Analysis double min_volume =SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); //Indicator Handlers int ma_handler,rsi_handler,wr_handler,total_time; vector ma_vector,rsi_vector,wr_vector; double _price; ulong _ticket;
完了したら、EAのOnInit()ハンドラを定義する準備ができます。ハンドラは、ユーザーがEAに渡したパラメータを使用して線形回帰オブジェクトを初期化し、テクニカル指標を設定します。
int OnInit() { //Setup our model ExtLinearRegression.Init(fetch_data,look_ahead); //Keep Track Of Time total_time = 0; //Set up our technical indicators ma_handler = iMA(_Symbol,PERIOD_CURRENT,ma_period,0,MODE_EMA,PRICE_CLOSE); rsi_handler = iRSI(_Symbol,PERIOD_CURRENT,rsi_period,PRICE_CLOSE); wr_handler = iWPR(_Symbol,PERIOD_CURRENT,wr_period); return(INIT_SUCCEEDED); }
ここで、OnTick関数にたどり着きます。OnTick関数は時間を追跡し、新しいローソク足の後に特定のアクションを実行し、ティックごとにいくつかのアクションを実行することができます。新しいローソク足が出るたびに、経過したローソク足の合計がユーザーが選択した予測ホライズンより大きい場合、実装したTrain関数を使ってモデルを再度訓練する必要があります。さらに、別の便利なベクトル関数CopyIndicatorBufferを使って、記録されている指標の値を更新したいと思います。これを担当する関数を作成しました。最後に、未決済ポジションがある場合に未決済ポジションの管理を担当する関数を作成します。
void OnTick() { //--- static datetime time_stamp; datetime current_time = iTime(_Symbol,PERIOD_CURRENT,0); if(time_stamp != current_time) { //Update the values of the indicators update_vectors(); total_time += 1; if(total_time > look_ahead) { total_time = 0; //Let the model adapt to the market dynamically ExtLinearRegression.Train(); } //If our model is ready then let's start trading if(ExtLinearRegression.Trained()) { if(PositionsTotal() == 0) { analyse_indicators(); } } if(PositionsTotal() == 1) { //Get position ticket _ticket = PositionGetTicket(0); //Manage the position manage_position(_ticket); } time_stamp = current_time; } }
この関数は、証券会社から入手可能な最新のバーを取得する責任を負います。
void update_vectors(void) { //Get the current reading of our indicators ma_vector.CopyIndicatorBuffer(ma_handler,0,1,1); rsi_vector.CopyIndicatorBuffer(rsi_handler,0,1,1); wr_vector.CopyIndicatorBuffer(wr_handler,0,1,1); _price = iClose(_Symbol,PERIOD_CURRENT,1); }
この関数は、指標とモデルの予測を統合する役割を担っています。たいすべてが揃えば取引を開始できますが、そうでなければ揃うのを待機することになります。
void analyse_indicators(void) { double forecast = ExtLinearRegression.Predict(); Comment("Forecast: ",forecast," Price: ",_price); //If price is above the moving average, check if the other indicators also confirm the buy signal if(_price - ma_vector[0] > 0) { if(rsi_vector[0] > 50) { if(wr_vector[0] > -20) { if(forecast > _price) { Trade.Buy(min_volume,_Symbol,SymbolInfoDouble(_Symbol,SYMBOL_ASK),0,0); } } } } //If price is below the moving average, check if the other indicators also confirm the sell signal if(_price - ma_vector[0] < 0) { if(rsi_vector[0] < 50) { if(wr_vector[0] < -80) { if( forecast < _price) { Trade.Sell(min_volume,_Symbol,SymbolInfoDouble(_Symbol,SYMBOL_BID),0,0); } } } }
この関数は、私たちが保有するすべての未決済ポジションを管理し、市場の現在のボラティリティレベルに基づいて損切りと利食いを動的に設定する役割を担っています。ポジションに損切りや利食いがない場合のみ、ポジションを修正します。
void manage_position(ulong m_ticket) { if(PositionSelectByTicket(m_ticket)) { double volatility = 2 * MathAbs(ma_vector[0] - _price); double entry = PositionGetDouble(POSITION_PRICE_OPEN); double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { double new_sl = _price - volatility; double new_tp = _price + volatility; if(current_sl == 0 || current_tp == 0) { Trade.PositionModify(m_ticket,new_sl,new_tp); } } if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { double new_sl = _price + volatility; double new_tp = _price - volatility; if(current_sl == 0 || current_tp == 0) { Trade.PositionModify(m_ticket,new_sl,new_tp); } } } }
EAは次のようになります。
図3:自己最適化EA
図4:自己最適化EAの入力
EAを銘柄に適用すると、[エキスパート]タブでその計算結果を見ることができます。
図5:EAによる計算
図6:EAのバックテスト
推奨
この記事では、自己最適化EAを構築するための最もシンプルな方法を伝授しました。しかし、これは可能な限り最良のアプローチではなく、最適な係数の手動検索です。理想的なソリューションは、より高度な行列とベクトル計算を採用して、最適な係数を自動的に見つけることでしょう。実際、行列関数とベクトル関数を使えば、forループを一度も使わずに線形回帰モデルを構築することができます。コードはよりコンパクトになり、係数はより数値的に安定します。手動検索は必ずしも解決策を保証するものではありません。
結論
MQL5 APIにある強力な行列関数とベクトル関数のおかげで、MQL5で自己適応型EAを構築するのは簡単です。実際のところ、MQL5で構築できるものは、APIを理解することによってのみ制限されます。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/14630
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索