
MQL5で自己最適化エキスパートアドバイザーを構築する(第7回):複数期間での同時取引
テクニカル指標は現代の投資家に多くの機会を提供する一方で、同等の課題も存在します。テクニカル指標にはその固有の遅延など、よく知られた制約が多くあり、これについては広く議論されています。
本記事では、指標に使用する適切な期間を特定することに関連する、より微妙な課題に焦点を当てたいと思います。指標の期間は、多くのテクニカル指標で共通する一般的なパラメータであり、計算に必要な過去データの量を制御します。
一般的に、期間を小さく設定しすぎると、指標が市場のノイズを過剰に拾ってしまいます。一方で、期間を大きく設定しすぎると、市場の動きがすでに展開された後でシグナルが生成されることが多くなります。いずれの場合も、取引機会を逃し、パフォーマンスが低下します。
本記事で提案する解決策では、最適な期間を特定する複雑さを排除し、代わりに利用可能なすべての期間を同時に使用します。この目的を達成するために、読者に次元削減アルゴリズムと呼ばれる機械学習アルゴリズム群を紹介し、特に比較的新しいアルゴリズムであるUniform Manifold Approximation And Projection (UMAP)に焦点を当てます。その後、これらのアルゴリズムは、利用可能なすべてのデータを有意義に表現し、元のデータセット以上の洞察を得ることを可能にします。
さらに、本記事ではMQL5におけるオブジェクト指向プログラミング(OOP)の関連原則も考察します。これは、取引アプリケーションで必要な名前空間管理、メモリ使用効率、その他のルーチン操作を効率的に扱うための有用なクラスを構築するために不可欠です。作成する4つのクラスのうち、特にONNXモデルを利用するアプリケーションを迅速に開発できる専用クラスも構築します。扱う内容は非常に多岐にわたります。それでは始めましょう。
MQL5で必要なクラスの構築
前回の自己最適化エキスパートアドバイザー(EA)に関する議論では、RSIクラスを構築し、複数のRSI期間にわたる指標データを意味のある整理された方法で取得できるようにしました。その内容を未読の方は、こちらから簡単に確認できます。しかし、本記事ではRSIから離れ、代わりにWilliam's Percent Range (WPR)指標を使用します。
WPRは一般的にモメンタムオシレーターと見なされ、その総範囲は0から-100です。0から-20の値は弱気、-80から-100の値は強気と見なされます。この指標は基本的に、指定された期間内での最高値と比較して、現在の銘柄価格を評価する仕組みです。まず最初の目標は、RSIクラスとWPRクラスの両方で共有できるSingleBufferIndicatorという新しいクラスを構築することです。RSIクラスとWPRクラスが共通の親クラスを持つことで、両方の指標クラスで一貫した機能を実現できます。まずSingleBufferIndicatorクラスを定義し、そのクラスメンバーをリストアップして始めます。
この設計アプローチには多くの利点があります。たとえば、将来的にすべての指標クラスに追加したい新機能があった場合、親クラスであるSingleBufferIndicator.mqhを更新するだけで済み、そこから子クラスをコンパイルすることで更新内容が反映されます。継承はオブジェクト指向プログラミングにおいて不可欠な機能であり、1つのクラスを修正するだけで多くのクラスを効果的に管理できます。
図1:シングルバッファ指標のファミリーの継承ツリーの可視化
まずは、RSIクラスを設計した際に使用した機能を一般化し、バッファが1つしかない任意の指標に適用できるようにします。MetaTrader 5には多数の指標が用意されていますが、本記事ではバッファが1つの指標を対象としたクラス設計に限定しています。これは、バッファが複数ある指標では別途対応が必要であり、クラス設計の目的を明確化するためです。クラスを設計する際は、明確かつ具体的な目的を持たせることが望まれます。
すべての指標を、バッファの数に関わらず1つのクラスで処理しようとすると、1度に実現するのは非常に困難です。また、設計を慎重に行わないと、論理的なエラーや意図しないバグをコードに含める可能性があります。したがって、クラスの対象範囲を限定することで、成功の可能性を高めることができます。
//+------------------------------------------------------------------+ //| SingleBufferIndicator.mqh | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" class SingleBufferIndicator { public: //--- Class methods bool SetIndicatorValues(int buffer_size,bool set_as_series); double GetReadingAt(int index); bool SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series); double GetDifferencedReadingAt(int index); double GetCurrentReading(void); //--- Have the indicator values been copied to the buffer? bool indicator_values_initialized; bool indicator_differenced_values_initialized; //--- How far into the future we wish to forecast int forecast_horizon; //--- The buffer for our indicator double indicator_reading[]; vector indicator_differenced_values; //--- The current size of the buffer the user last requested int indicator_buffer_size; int indicator_differenced_buffer_size; //--- The handler for our indicator int indicator_handler; //--- The time frame our indicator should be applied on ENUM_TIMEFRAMES indicator_time_frame; //--- The price should the indicator be applied on ENUM_APPLIED_PRICE indicator_price; //--- Give the user feedback string user_feedback(int flag); //--- The Symbol our indicator should be applied on string indicator_symbol; //--- Our period int indicator_period; //--- Is our indicator valid? bool IsValid(void); //---- Testing the Single Buffer Indicator Class //--- This method should be deleted in production virtual void Test(void); }; //+------------------------------------------------------------------+
次に、指標ハンドラから指標バッファへ読み値をコピーするメソッドが必要です。このメソッドは2つのパラメータを持ちます。1つ目はコピーするデータの量を指定し、2つ目はデータをどの順序で並べるかを指定します。2つ目のパラメータがtrueの場合、データは過去から現在に向かって順序付けられます。
//+------------------------------------------------------------------+ //| Set our indicator values and our buffer size | //+------------------------------------------------------------------+ bool SingleBufferIndicator::SetIndicatorValues(int buffer_size,bool set_as_series) { //--- Buffer size indicator_buffer_size = buffer_size; CopyBuffer(this.indicator_handler,0,0,buffer_size,indicator_reading); //--- Should the array be set as series? if(set_as_series) ArraySetAsSeries(this.indicator_reading,true); indicator_values_initialized = true; //--- Did something go wrong? vector indicator_test; indicator_test.CopyIndicatorBuffer(indicator_handler,0,0,buffer_size); if(indicator_test.Sum() == 0) return(false); //--- Everything went fine. return(true); }
機械学習においては、変数の変化量を記録することのほうが、単純な値そのものよりも有益な場合があります。したがって、指標の読み値の変化量を計算し、それを指標バッファにコピーする専用のメソッドも作成します。
//+--------------------------------------------------------------+ //| Let's set the conditions for our differenced data | //+--------------------------------------------------------------+ bool SingleBufferIndicator::SetDifferencedIndicatorValues(int buffer_size,int differencing_period,bool set_as_series) { //--- Internal variables indicator_differenced_buffer_size = buffer_size; indicator_differenced_values = vector::Zeros(indicator_differenced_buffer_size); //--- Prepare to record the differences in our RSI readings double temp_buffer[]; int fetch = (indicator_differenced_buffer_size + (2 * differencing_period)); CopyBuffer(indicator_handler,0,0,fetch,temp_buffer); if(set_as_series) ArraySetAsSeries(temp_buffer,true); //--- Fill in our values iteratively for(int i = indicator_differenced_buffer_size;i > 1; i--) { indicator_differenced_values[i-1] = temp_buffer[i-1] - temp_buffer[i-1+differencing_period]; } //--- If the norm of a vector is 0, the vector is empty! if(indicator_differenced_values.Norm(VECTOR_NORM_P) != 0) { Print(user_feedback(2)); indicator_differenced_values_initialized = true; return(true); } indicator_differenced_values_initialized = false; Print(user_feedback(3)); return(false); }
指標の値をバッファにコピーするメソッドを定義したので、次にそのバッファ内のデータを取得するメソッドが必要です。指標バッファをクラスのpublicメンバーとして宣言すれば、簡単に値を取得することも可能です。しかし、その方法ではクラスを構築する本来の目的、すなわちオブジェクトへの読み書きを統一的に管理するという目的が損なわれてしまいます。
//--- Get a differenced value at a specific index double SingleBufferIndicator::GetDifferencedReadingAt(int index) { //--- Make sure we're not trying to call values beyond our index if(index > indicator_differenced_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Make sure our values have been set if(!indicator_differenced_values_initialized) { //--- The user is trying to use values before they were set in memory Print(user_feedback(1)); return(-1e10); } //--- Return the differenced value of our indicator at a specific index if((indicator_differenced_values_initialized) && (index < indicator_differenced_buffer_size)) return(indicator_differenced_values[index]); //--- Something went wrong. return(-1e10); }
前述のメソッドは指標の変化量を返すものでしたが、指標上に表示されている実際の値を返す対応するメソッドも必要です。
//+------------------------------------------------------------------+ //| Get a reading at a specific index from our RSI buffer | //+------------------------------------------------------------------+ double SingleBufferIndicator::GetReadingAt(int index) { //--- Is the user trying to call indexes beyond the buffer? if(index > indicator_buffer_size) { Print(user_feedback(4)); return(-1e10); } //--- Get the reading at the specified index if((indicator_values_initialized) && (index < indicator_buffer_size)) return(indicator_reading[index]); //--- User is trying to get values that were not set prior else { Print(user_feedback(1)); return(-1e10); } }
また、インデックス0の指標値、すなわち現在の指標の読み値を返す専用の関数を用意することも有用だと考えました。
//+------------------------------------------------------------------+ //| Get our current reading from the RSI indicator | //+------------------------------------------------------------------+ double SingleBufferIndicator::GetCurrentReading(void) { double temp[]; CopyBuffer(this.indicator_handler,0,0,1,temp); return(temp[0]); }
この関数は、指標ハンドラが正しく読み込まれているかを確認するためのものです。安全性を確保する上で有用な機能です。
//+------------------------------------------------------------------+ //| Check if our indicator handler is valid | //+------------------------------------------------------------------+ bool SingleBufferIndicator::IsValid(void) { return((this.indicator_handler != INVALID_HANDLE)); }
ユーザーが指標クラスを操作する際に、誤操作があった場合にはその内容を通知し、適切な解決方法を提示したいと考えています。
//+------------------------------------------------------------------+ //| Give the user feedback on the actions he is performing | //+------------------------------------------------------------------+ string SingleBufferIndicator::user_feedback(int flag) { string message; //--- Check if the indicator loaded correctly if(flag == 0) { //--- Check the indicator was loaded correctly if(IsValid()) message = "Indicator Class Loaded Correcrtly \nSymbol: " + (string) indicator_symbol + "\nPeriod: " + (string) indicator_period; return(message); //--- Something went wrong message = "Error loading Indicator: [ERROR] " + (string) GetLastError(); return(message); } //--- User tried getting indicator values before setting them if(flag == 1) { message = "Please set the indicator values before trying to fetch them from memory, call SetIndicatorValues()"; return(message); } //--- We sueccessfully set our differenced indicator values if(flag == 2) { message = "Succesfully set differenced indicator values."; return(message); } //--- Failed to set our differenced indicator values if(flag == 3) { message = "Failed to set our differenced indicator values: [ERROR] " + (string) GetLastError(); return(message); } //--- The user is trying to retrieve an index beyond the buffer size and must update the buffer size first if(flag == 4) { message = "The user is attempting to use call an index beyond the buffer size, update the buffer size first"; return(message); } //--- The class has been deactivated by the user if(flag == 5) { message = "Goodbye."; return(message); } //--- No feedback else return(""); }
これで、SingleBufferIndicatorクラスを親クラスとして継承するWPRクラスを構築できるようになります。全体の依存関係は、この記事に沿って作業する場合、図1のような構造になるはずです。
図2:指標クラスの依存関係ツリー
次に、WPRクラスで最初に行うステップに移ります。それは、SingleBufferIndicatorクラスをWPRクラスに組み込むことです。
//+------------------------------------------------------------------+ //| WPR.mqh | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Load the parent class | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\SingleBuffer\SingleBufferIndicator.mqh>
今回は、WPRクラスのクラスメンバーを定義する前に、コロン「:」の構文を用いてWPRクラスがSingleBufferIndicatorクラスを継承することを指定します。これがMQL5でクラスを継承する方法です。オブジェクト指向プログラミング(OOP)の概念に不慣れな読者のために説明すると、クラスを継承することで、SingleBufferIndicatorクラスで作成したメソッドをWPRクラス内から呼び出すことが可能になります。WPRクラスとRSIクラスの両方がSingleBufferIndicatorクラスを継承することで、両クラス間で一貫した機能を利用できるようになります。言い換えれば、SingleBufferIndicatorクラスに組み込んだすべてのpublicクラスメンバーは、それを継承した任意のクラスからすぐに利用可能です。
//+------------------------------------------------------------------+ //| This class will provide us with usefull functionality for the WPR| //+------------------------------------------------------------------+ class WPR : public SingleBufferIndicator { public: WPR(); WPR(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period); ~WPR(); };
WPRおよびRSI指標はいずれもバッファが1つしかありませんが、初期化にはそれぞれ異なるパラメータが必要です。したがって、コンストラクタは各指標インスタンスに特化したものとする方が適切です。指標ごとにコンストラクタのシグネチャが大きく異なる可能性があるためです。
//+------------------------------------------------------------------+ //| Our default constructor for our Indicator class | //+------------------------------------------------------------------+ void WPR::WPR() { indicator_values_initialized = false; indicator_symbol = "EURUSD"; indicator_time_frame = PERIOD_D1; indicator_period = 5; indicator_handler = iWPR(indicator_symbol,indicator_time_frame,indicator_period); //--- Give the user feedback on initilization Print(user_feedback(0)); //--- Remind the user they called the default constructor Print("Default Constructor Called: ",__FUNCSIG__," ",&this); }
このパラメトリックコンストラクタにより、ユーザーはWPR指標を初期化する際に、対象の銘柄、時間足、および期間を指定することができます。
//+------------------------------------------------------------------+ //| Our parametric constructor for our Indicator class | //+------------------------------------------------------------------+ void WPR::WPR(string user_symbol,ENUM_TIMEFRAMES user_time_frame,int user_period) { indicator_values_initialized = false; indicator_symbol = user_symbol; indicator_time_frame = user_time_frame; indicator_period = user_period; indicator_handler = iWPR(indicator_symbol,indicator_time_frame,indicator_period); //--- Give the user feedback on initilization Print(user_feedback(0)); }
クラスのデストラクタは、重要なフラグをリセットし、指標を解放してくれます。MQL5では、専用のクラスを構築してクリーンアップ処理をおこなうことは良い習慣です。これにより、開発者はクリーンアップ処理を常に手作業で繰り返す必要がなくなり、認知的負荷を軽減できます。
//+------------------------------------------------------------------+ //| Our destructor for our Indicator class | //+------------------------------------------------------------------+ void WPR::~WPR() { //--- Free up resources we don't need and reset our flags if(IndicatorRelease(indicator_handler)) { indicator_differenced_values_initialized = false; indicator_values_initialized = false; Print(user_feedback(5)); } } //+------------------------------------------------------------------+
次に必要となる機能は、新しいローソク足が完全に形成されたかを判別する能力です。ローソク足が形成された際に、特定のタスクを実行したい場合があります。そのため、この目的のために専用のクラスを作成します。場合によっては、異なる時間足のローソク足の形成を同時に追跡したいこともあります。まずは、Timeクラスで必要となるクラスメンバーを宣言することから始めます。
//+------------------------------------------------------------------+ //| Time.mqh | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" class Time { private: datetime time_stamp; datetime current_time; string selected_symbol; ENUM_TIMEFRAMES selected_time_frame; public: Time(string user_symbol,ENUM_TIMEFRAMES user_time_frame); bool NewCandle(void); ~Time(); };
このクラスにはデフォルトコンストラクタが存在しないことに注意してください。これは意図的におこなったものです。この特定のケースでは、デフォルトコンストラクタはあまり意味を持たないためです。
//+------------------------------------------------------------------+ //| Create our time object | //+------------------------------------------------------------------+ Time::Time(string user_symbol,ENUM_TIMEFRAMES user_time_frame) { selected_time_frame = user_time_frame; selected_symbol = user_symbol; current_time = iTime(user_symbol,selected_time_frame,0); time_stamp = iTime(user_symbol,selected_time_frame,0); }
現在、クラスのデストラクタは空です。
//+------------------------------------------------------------------+ //| Our destructor is currently empty | //+------------------------------------------------------------------+ Time::~Time() { } //+------------------------------------------------------------------+
最後に、新しいローソク足が形成されたかを知らせるメソッドが必要です。このメソッドは、新しいローソク足が形成されていればtrueを返し、定期的な処理を実行できるようにします。
//+------------------------------------------------------------------+ //| Check if a new candle has fully formed | //+------------------------------------------------------------------+ bool Time::NewCandle(void) { current_time = iTime(selected_symbol,selected_time_frame,0); //--- Check if a new candle has formed if(time_stamp != current_time) { time_stamp = current_time; return(true); } //--- No new candle has completely formed return(false); }
次に、ONNXオブジェクトを扱う専用のクラスも必要です。プロジェクトが大規模化・複雑化するにつれて、特定の手順を何度も繰り返すことは避けたいところです。最終的には、floatデータ型を受け取るすべてのONNXモデルに対応するONNXFloatクラスを用意するほうが効率的でしょう。執筆時点では、ONNXモデルを実行する際にfloatデータ型は安定したデータ型として広く利用されています。まずは、ONNXFloatクラスのクラスメンバーを定義することから始めましょう。
//+------------------------------------------------------------------+ //| ONNXFloat.mqh | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| This class will help us work with ONNX Float models. | //+------------------------------------------------------------------+ class ONNXFloat { private: //--- Our ONNX model handler long onnx_model; int onnx_outputs; public: //--- Is our Model Valid? bool OnnxModelIsValid(void); //--- Define the input shape of our model bool DefineOnnxInputShape(int n_index,int n_stacks,int n_input_params); //--- Define the output shape of our model bool DefineOnnxOutputShape(int n_index,int n_stacks, int n_output_params); vectorf Predict(const vectorf &model_inputs); //--- ONNXFloat class constructor ONNXFloat(const uchar &user_proto[]); //---- ONNXFloat class destructor ~ONNXFloat(); };
クラスのコンストラクタは、ONNXモデルのプロトタイプを受け取り、ユーザーが渡したバッファからONNXモデルを作成します。ONNXモデルのバッファは値渡しではなく参照渡しのみ可能であることに注意してください。ONNXモデルバッファ名の前に置かれたアンパサンド(&user_proto)は、このパラメータがメモリ上のオブジェクトへの参照であることを明示しています。関数のパラメータが参照渡しである場合、関数内で行った変更は関数外の元のパラメータにも反映されることをユーザーは理解している必要があります。
今回の場合、ONNXプロトタイプを編集する意図はないため、パラメータをconstに変更し、プログラマおよびコンパイラに対して変更がおこなわれないことを示しています。したがって、プログラマがこの指示を無視した場合、コンパイラはそれを許容しません。
//+------------------------------------------------------------------+ //| Parametric Constructor For Our ONNXFloat class | //+------------------------------------------------------------------+ ONNXFloat::ONNXFloat(const uchar &user_proto[]) { onnx_model = OnnxCreateFromBuffer(user_proto,ONNX_DATA_TYPE_FLOAT); if(OnnxModelIsValid()) Print("Volatility Doctor ONNXFloat Class Loaded Correctly: ",__FUNCSIG__," ",&this); else Print("Failed To Create The specified ONNX model: ",GetLastError()); }
ONNXFloatクラスのデストラクタは、ONNXモデルに割り当てたメモリを自動的に解放してくれます。
//+------------------------------------------------------------------+ //| Our ONNXFloat class destructor | //+------------------------------------------------------------------+ ONNXFloat::~ONNXFloat() { OnnxRelease(onnx_model); } //+------------------------------------------------------------------+
ONNXモデルが有効かどうかを示す専用の関数も必要です。この関数は、モデルが有効な場合にのみtrueを返すブール型のフラグを返します。
//+------------------------------------------------------------------+ //| A method that returns true if our ONNXFloat model is valid | //+------------------------------------------------------------------+ bool ONNXFloat::OnnxModelIsValid(void) { //--- Check if the model is valid if(onnx_model != INVALID_HANDLE) return(true); //--- Something went wrong return(false); }
ONNXモデルの入力形状を設定することは、準備段階として必要な手順であり、頻繁におこなうことが想定されます。
//+------------------------------------------------------------------+ //| Set the input shape of our ONNXFloat model | //+------------------------------------------------------------------+ bool ONNXFloat::DefineOnnxInputShape(int n_index,int n_stacks,int n_input_params) { const ulong model_input_shape[] = {n_stacks,n_input_params}; if(OnnxSetInputShape(onnx_model,n_index,model_input_shape)) { Print("Succefully specified ONNX model output shape: ",__FUNCTION__," ",&this); return(true); } //--- Something went wrong Print("Failed to set the passed ONNX model output shape: ",GetLastError()); return(false); }
ONNXモデルの出力形状についても同様です。
//+------------------------------------------------------------------+ //| Set the output shape of our model | //+------------------------------------------------------------------+ bool ONNXFloat::DefineOnnxOutputShape(int n_index,int n_stacks,int n_output_params) { const ulong model_output_shape[] = {n_output_params,n_stacks}; onnx_outputs = n_output_params; if(OnnxSetOutputShape(onnx_model,n_index,model_output_shape)) { Print("Succefully specified ONNX model input shape: ",__FUNCSIG__," ",&this); return(true); } //--- Something went wrong Print("Failed to set the passed ONNX model input shape: ",GetLastError()); return(false); }
最後に、predict関数が必要です。この関数はONNXモデルの入力データを参照で受け取りますが、入力データを変更する意図はないため、このパラメータをconstに指定しています。これにより、入力データが意図せず破損する副作用を防ぐことができ、さらにコンパイラに対して入力データを変更するような不注意な操作を禁止する指示となります。このような安全機能は非常に重要であり、プログラミング言語に組み込まれていることで、MQL5は第一級のプログラミング言語としての資質を備えていると言えます。
//+------------------------------------------------------------------+ //| Get a prediction from our model | //+------------------------------------------------------------------+ vectorf ONNXFloat::Predict(const vectorf &model_inputs) { vectorf model_output(onnx_outputs); if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output)) { vectorf res = model_output; return(res); } Comment("Failed to get a prediction from our ONNX model"); Print("ONNX Run Failed: ",GetLastError()); vectorf res = {10e8}; return(res); }
最後に必要となるクラスは、最小取引数量や現在の売気配価格など、有用な取引情報を取得する役割を担います。
//+------------------------------------------------------------------+ //| TradeInfo.mqh | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" class TradeInfo { private: string user_symbol; ENUM_TIMEFRAMES user_time_frame; double min_volume,max_volume,volume_step; public: TradeInfo(string selected_symbol,ENUM_TIMEFRAMES selected_time_frame); double MinVolume(void); double MaxVolume(void); double VolumeStep(void); double GetAsk(void); double GetBid(void); double GetClose(void); string GetSymbol(void); ~TradeInfo(); };
このパラメトリッククラスのコンストラクタは、対象の銘柄と時間足を指定する2つのパラメータを受け取ります。
//+------------------------------------------------------------------+ //| The constructor will load our symbol information | //+------------------------------------------------------------------+ TradeInfo::TradeInfo(string selected_symbol,ENUM_TIMEFRAMES selected_time_frame) { //--- Which symbol are you interested in? user_symbol = selected_symbol; user_time_frame = selected_time_frame; if(SymbolSelect(user_symbol,true)) { //--- Load symbol details min_volume = SymbolInfoDouble(user_symbol,SYMBOL_VOLUME_MIN); max_volume = SymbolInfoDouble(user_symbol,SYMBOL_VOLUME_MAX); volume_step = SymbolInfoDouble(user_symbol,SYMBOL_VOLUME_STEP); Print("Trade Info Loaded Successfully: ",__FUNCSIG__); } else { Print("Error Symbol Information Could Not Be Found For: ",selected_symbol," ",GetLastError()); } }
また、4つの主要な価格フィードの現在値を取得するメソッドも定義します。すなわち、これらの各メソッドはそれぞれ、現在のOpen、High、Low、Close価格を返します。
//+------------------------------------------------------------------+ //| Return the close of the selected symbol | //+------------------------------------------------------------------+ double TradeInfo::GetClose(void) { double res = iClose(user_symbol,user_time_frame,0); return(res); } //+------------------------------------------------------------------+ //| Return the open of the selected symbol | //+------------------------------------------------------------------+ double TradeInfo::GetOpen(void) { double res = iOpen(user_symbol,user_time_frame,0); return(res); } //+------------------------------------------------------------------+ //| Return the high of the selected symbol | //+------------------------------------------------------------------+ double TradeInfo::GetHigh(void) { double res = iHigh(user_symbol,user_time_frame,0); return(res); } //+------------------------------------------------------------------+ //| Return the low of the selected symbol | //+------------------------------------------------------------------+ double TradeInfo::GetLow(void) { double res = iLow(user_symbol,user_time_frame,0); return(res); }
複数の銘柄を扱う場合、現在のクラスインスタンスがどの銘柄に割り当てられているかを確認できる仕組みがあると便利です。
//+------------------------------------------------------------------+ //| Return the selected symbol | //+------------------------------------------------------------------+ string TradeInfo::GetSymbol(void) { string res = user_symbol; return(res); }
本クラスでは、現在の銘柄で許可されている取引数量に関する重要な情報を迅速に取得できるラッパーメソッドも提供します。
//+------------------------------------------------------------------+ //| Return the volume step allowed | //+------------------------------------------------------------------+ double TradeInfo::VolumeStep(void) { double res = volume_step; return(res); } //+------------------------------------------------------------------+ //| Return the minimum volume allowed | //+------------------------------------------------------------------+ double TradeInfo::MinVolume(void) { double res = min_volume; return(res); } //+------------------------------------------------------------------+ //| Return the maximum volume allowed | //+------------------------------------------------------------------+ double TradeInfo::MaxVolume(void) { double res = max_volume; return(res); }
また、現在のbid価格とask価格を簡単に提供できるクラスも必要です。
//+------------------------------------------------------------------+ //| Return the current ask | //+------------------------------------------------------------------+ double TradeInfo::GetAsk(void) { return(SymbolInfoDouble(GetSymbol(),SYMBOL_ASK)); } //+------------------------------------------------------------------+ //| Return the current bid | //+------------------------------------------------------------------+ double TradeInfo::GetBid(void) { return(SymbolInfoDouble(GetSymbol(),SYMBOL_BID)); }
現在、Timeクラスのデストラクタは空です。
//+------------------------------------------------------------------+ //| Destructor is currently empty | //+------------------------------------------------------------------+ TradeInfo::~TradeInfo() { } //+------------------------------------------------------------------+
全体として、ここまで順を追って作業してきた場合、依存関係のツリーは以下の図3のような構造になっているはずです。
図3:これらのクラスは、読者が追従しやすいように、私たちの依存関係ツリーと同様の構造で保持する必要があります。
次に、必要な市場データを取得するスクリプトを定義します。まず、4つの主要価格フィード(OHLC)を取得し、次にこれら4つの価格フィードの増分を取得し、最後に14本のWPR指標から指標データを書き出します。
//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define HORIZON 10 //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\WPR.mqh> //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ WPR *my_wpr_array[14]; string file_name = Symbol() + " WPR Algorithmic Input Selection.csv"; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //--- How much data should we store in our indicator buffer? int fetch = size + (2 * HORIZON); //--- Store pointers to our WPR objects for(int i = 0; i <= 13; i++) { //--- Create an WPR object my_wpr_array[i] = new WPR(Symbol(),PERIOD_CURRENT,((i+1) * 5)); //--- Set the WPR buffers my_wpr_array[i].SetIndicatorValues(fetch,true); my_wpr_array[i].SetDifferencedIndicatorValues(fetch,HORIZON,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","True Open","True High","True Low","True Close","Open","High","Low","Close","WPR 5","WPR 10","WPR 15","WPR 20","WPR 25","WPR 30","WPR 35","WPR 40","WPR 45","WPR 50","WPR 55","WPR 60","WPR 65","WPR 70","Diff WPR 5","Diff WPR 10","Diff WPR 15","Diff WPR 20","Diff WPR 25","Diff WPR 30","Diff WPR 35","Diff WPR 40","Diff WPR 45","Diff WPR 50","Diff WPR 55","Diff WPR 60","Diff WPR 65","Diff WPR 70"); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), 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), my_wpr_array[0].GetReadingAt(i), my_wpr_array[1].GetReadingAt(i), my_wpr_array[2].GetReadingAt(i), my_wpr_array[3].GetReadingAt(i), my_wpr_array[4].GetReadingAt(i), my_wpr_array[5].GetReadingAt(i), my_wpr_array[6].GetReadingAt(i), my_wpr_array[7].GetReadingAt(i), my_wpr_array[8].GetReadingAt(i), my_wpr_array[9].GetReadingAt(i), my_wpr_array[10].GetReadingAt(i), my_wpr_array[11].GetReadingAt(i), my_wpr_array[12].GetReadingAt(i), my_wpr_array[13].GetReadingAt(i), my_wpr_array[0].GetDifferencedReadingAt(i), my_wpr_array[1].GetDifferencedReadingAt(i), my_wpr_array[2].GetDifferencedReadingAt(i), my_wpr_array[3].GetDifferencedReadingAt(i), my_wpr_array[4].GetDifferencedReadingAt(i), my_wpr_array[5].GetDifferencedReadingAt(i), my_wpr_array[6].GetDifferencedReadingAt(i), my_wpr_array[7].GetDifferencedReadingAt(i), my_wpr_array[8].GetDifferencedReadingAt(i), my_wpr_array[9].GetDifferencedReadingAt(i), my_wpr_array[10].GetDifferencedReadingAt(i), my_wpr_array[11].GetDifferencedReadingAt(i), my_wpr_array[12].GetDifferencedReadingAt(i), my_wpr_array[13].GetDifferencedReadingAt(i) ); } } //--- Close the file FileClose(file_handle); //--- Delete our WPR object pointers for(int i = 0; i <= 13; i++) { delete my_wpr_array[i]; } } //+------------------------------------------------------------------+ #undef HORIZON
Pythonでデータを分析する
取得が完了したら、スクリプトを任意の市場に適用して、分析用の市場データを準備します。本記事ではEURGBPペアにスクリプトを適用しています。データの準備が整ったところで、分析用のPythonライブラリを読み込みましょう。
#Load the libraries import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt
データを読み込みます。
#Read in the data data = pd.read_csv("..\EURGBP WPR Algorithmic Input Selection.csv") #Label the data HORIZON = 10 data['Target'] = data['Close'].shift(-HORIZON) - data['Close'] #Drop the last 10 rows data = data.iloc[:-HORIZON,:]
入力とターゲットのコピーを作成します。
#Define inputs and target X = data.iloc[:,1:-1].copy() y = data.iloc[:,-1].copy()
データセット内の各数値列をスケーリングして中央に配置します。
#Store Z-scores Z1 = X.mean() Z2 = X.std() #Scale the data X = ((X - Z1)/ Z2)
精度をテストするために必要な数値ライブラリを読み込みます。
from sklearn.model_selection import cross_val_score,TimeSeriesSplit from sklearn.linear_model import Ridge
時系列交差検証オブジェクトを作成します。
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)sdvdsvds
交差検証された精度レベルを常に返すメソッドを定義します。
#Return our cross validated accuracy def score(f_model,f_X,f_y): return(np.mean(np.abs(cross_val_score(f_model,f_X,f_y,scoring='neg_mean_squared_error',cv=tscv,n_jobs=-1))))
新しいモデルを返す専用のメソッドも必要です。これにより、使用するモデルに不要なデータが流出することを防げます。
def get_model(): return(Ridge())
列をすべてゼロで埋めることで、市場の平均リターンを常に予測した場合の精度を測定できます。
X['Null'] = 0
次に、市場の平均リターンを常に予測した場合に生じる誤差(総平方和/TSS)を記録します。これにより、平均リターン予測による誤差の閾値が定義されます。この閾値を踏まえれば、この議論の範囲において、誤差レベルが0.000324を超えるモデルは、特に注目すべき有効性を持たないと自信を持って断言できます。
#This will be the last entry in our list of results #Record our error if we always predict the average market return (total sum of squares/TSS) tss = score(get_model(),X[['Null']],y) tss
0.00032439931180771236
次に、結果を管理するための配列を作成します。
res = []
最初に記録したい結果は、元の形のOHLC市場データを使用した場合の誤差レベルです。
#This will be our first entry in our list of results #Record our error using OHLC price data res.append(score(get_model(),X.iloc[:,:8],y))
次に、選択した14本のWPR指標期間のみを使用した場合の誤差レベルを確認します。
#Second #Record our error using just indicators res.append(score(get_model(),X.iloc[:,8:-1],y))
最後に、利用可能なすべてのデータを使用した場合の誤差レベルを記録します。
#Third #Record our error using all the data we have res.append(score(get_model(),X.iloc[:,:-1],y))
次に、UMAPライブラリを読み込みます。元のデータは36列ありますが、UMAPライブラリを使用することで、1列以上かつ元の列数未満の任意の列数でデータを表現できます。この新しいデータ表現は、元の形よりも情報量が多い場合があります。したがって、この意味で、次元削減アルゴリズムは、問題を記述するすべてのデータを効果的に活用できる手法群として考えることもできます。
import umap
元の列数より最大で2列少ない数の埋め込みを探索したいと考えます。
EPOCHS = X.iloc[:,:-1].shape[1] - 2
UMAPを用いてデータを反復的に埋め込みます。生成される埋め込み列数は1から開始し、1列ずつ増加させ、先ほど設定した上限まで繰り返します。
for i in range(EPOCHS): reducer = umap.UMAP(n_components=(i+1),metric='euclidean',random_state=0,transform_seed=0,n_neighbors=30) X_embedded = pd.DataFrame(reducer.fit_transform(X.iloc[:,:-1])) res.append(score(get_model(),X_embedded,y))
結果をまとめます。
res.append(tss)
赤の実線は、平均市場リターンを常に予測した場合に生じる誤差(TSS)であり、これは重要な誤差ベンチマークです。赤の点線は、私たちが達成できた最小の誤差レベルを示しています。これは、元のデータをUMAPアルゴリズムで2列に埋め込んだ際に構築されたモデルに対応します。この誤差レベルは、元の市場データを使用した場合よりもTSSを大幅に上回る性能を示しています。つまり、すべてのWPR期間を同時に、より意味のある方法で活用していることになります。
図4:2つのUMAP埋め込み成分を使用することで、元の形の市場データすべてを使用した同等モデルよりも優れた性能を達成した
次に、特定した最適なUMAP設定を用いてデータを変換します。
reducer = umap.UMAP(n_components=2,metric='euclidean',random_state=0,transform_seed=0,n_neighbors=30) X_embedded = pd.DataFrame(reducer.fit_transform(X.iloc[:,:-1]))
2つのクラスにラベルを付けます。これは後で、UMAPがデータに対してどのような変換をおこなっているかを可視化する際に役立ちます。
data['Class'] = 0 data.loc[data['Target'] > 0,'Class'] = 1
変換後のデータを格納するためのデータセットを準備します。
umap_data =pd.DataFrame(columns=['UMAP 1','UMAP 2'])
埋め込まれた価格レベルを保存します。
umap_data['UMAP 1'] = X_embedded.iloc[:,0] umap_data['UMAP 2'] = X_embedded.iloc[:,1]
UMAPを使用しない場合、データの次元数が多いため、意味のある可視化は困難です。実際、可能な限り行えるのは散布図のペアを作成することですが、36次元すべてを一度に効果的に可視化する方法はありません。以下の図5および図6では、赤い点が強気の価格変動を、黒い点が弱気の価格変動を示しています。
fig , axs = plt.subplots(2,2) fig.suptitle('Visualizing EURGBP 2002-2025 Daily Price Data') axs[0,0].scatter(data.loc[data['Target']>0 ,'Open'],data.loc[data['Target']>0 ,'Close'],color='red') axs[0,0].scatter(data.loc[data['Target']<0 ,'Open'],data.loc[data['Target']<0 ,'Close'],color='black') axs[0,1].scatter(data.loc[data['Target']>0 ,'True Open'],data.loc[data['Target']>0 ,'True Close'],color='red') axs[0,1].scatter(data.loc[data['Target']<0 ,'True Open'],data.loc[data['Target']<0 ,'True Close'],color='black') axs[1,1].scatter(data.loc[data['Target']>0 ,'WPR 5'],data.loc[data['Target']>0 ,'WPR 50'],color='red') axs[1,1].scatter(data.loc[data['Target']<0 ,'WPR 5'],data.loc[data['Target']<0 ,'WPR 50'],color='black') axs[1,0].scatter(data.loc[data['Target']>0 ,'WPR 15'],data.loc[data['Target']>0 ,'WPR 25'],color='red') axs[1,0].scatter(data.loc[data['Target']<0 ,'WPR 15'],data.loc[data['Target']<0 ,'WPR 25'],color='black')
図5:(左)始値の変化量と終値の変化量の関係、(右)実際の始値と終値
図6:(左)5期間と50期間のWPRの関係、(右)15期間と25期間のWPRの関係
ご覧の通り、図5および図6は意味のある解釈が難しく、データに明確なパターンは見られません。さらに、2次元散布図で2次元を超える現象を表現することは危険です。2変数間に関係があるように見えても、他の次元によって説明される可能性があり、1つのプロットでは表現できない場合があります。これにより、誤った発見や、実際には安定していない関係に過度な自信を持ってしまう恐れがあります。
しかし、UMAPを適用すると、すべてのデータを2次元で容易にプロットできます。一般的に、最初の埋め込みの低値と高値は、それぞれ弱気および強気の価格変動と関連していることが確認できます。
sns.scatterplot(x=X_embedded.iloc[:,0],y=X_embedded.iloc[:,1],hue=data['Class']) plt.grid() plt.ylabel('Second UMAP Embedding') plt.xlabel('First UMAP Embedding') plt.title('Visualizing The Most Effective Embedding We Found')
図7:元の市場データのUMAP埋め込み
次に、バックテスト用のモデルを準備します。必要なライブラリをインポートします。
from sklearn.model_selection import train_test_split
市場データを分割します。 トレーニングサンプルは2002年11月から2018年8月までを使用するため、バックテスト期間は2018年9月から開始します。
train , test = train_test_split(data,test_size=0.3,shuffle=False) train
図8:市場データを元の形式で表示する
次に、統計モデルを読み込みます。
from sklearn.neural_network import MLPRegressor
トレーニングデータをスケーリングします。
#Sample mean Z1 = train.iloc[:,1:-2].mean() #Sample standard deviation Z2 = train.iloc[:,1:-2].std() train_scaled = train.copy() train_scaled.iloc[:,1:-2] = ((train.iloc[:,1:-2] - Z1) / Z2)
トレーニングデータを埋め込みます。
reducer = umap.UMAP(n_components=2,metric='euclidean',random_state=0,transform_seed=0,n_neighbors=30) X_embedded = pd.DataFrame(reducer.fit_transform(train_scaled.iloc[:,1:-2],columns=['UMAP 1','UMAP 2']))
本フレームワークは2ステップのプロセスに従います。まず、UMAPアルゴリズムを近似するモデルを学習させます。これにより、MQL5でUMAPアルゴリズムを一から書き直す必要がなくなります。UMAPアルゴリズムは非常に高度で、ポスドク研究者のチームによって実装されました。アルゴリズムを数値的に安定した形で実装するには相当な労力が必要であり、このようなアルゴリズムを独自に実装することは、一般的に推奨される方法とはみなされません。
#Learn To Estimate UMAP Embeddings From The Data umap_model = MLPRegressor(shuffle=False,hidden_layer_sizes=(train.iloc[:,1:-2].shape[1],10,20,100,20,10,2),random_state=0,solver='lbfgs',activation='relu',learning_rate='constant',learning_rate_init=1e-4,power_t=1e-1) np.mean(np.abs(cross_val_score(umap_model,train.iloc[:,1:-2],X_embedded,scoring='neg_mean_squared_error',n_jobs=-1)))
11.2489992665160363
UMAP関数を学習させます。
umap_model.fit(train.iloc[:,1:-2],X_embedded) predictions = umap_model.predict(train.iloc[:,1:-2])
次に、UMAPによる市場埋め込みを入力として、EURGBPの市場リターンを予測するモデルが必要です。scikit-learnのニューラルネットワークモデルには重要なパラメータであるrandom_stateが存在します。このパラメータはニューラルネットワークの初期重みとバイアスに影響を与えます。問題の内容によっては、異なる初期状態でモデルを複数回学習させると、性能にかなりのばらつきが生じる場合があります。図9はその一例を示しています。
EPOCHS = 100 res = [] for i in range(EPOCHS): #Try different random states model = MLPRegressor(shuffle=False,early_stopping=False,hidden_layer_sizes=(2,1,10,20,1),activation='identity',solver='lbfgs',random_state=i,max_iter=int(2e5)) res.append(score(model,predictions,train['Target']))
結果を可視化します。
plt.plot(res,color='black') plt.axhline(np.min(res),color='red',linestyle=':') plt.scatter(res.index(np.min(res)),np.min(res),color='red') plt.grid() plt.ylabel('Cross Validated RMSE') plt.xlabel('Neural Network Random State') plt.title('Our Neural Network Performance With Different Initial Conditions')
図9:この問題におけるニューラルネットワークの最適な初期状態
私たちが選択したニューラルネットワークは、EURGBPの10日間市場リターンを予測する際に、平均市場リターンを常に予測する場合よりも38%低い誤差で予測できています。
tss = score(Ridge(),train[['Close']]*0,train['Target']) 1-(np.min(res)/tss)
0.3822093585025088
図9で特定した最適なrandom_stateを用いてモデルを学習させます。
embedded_model = MLPRegressor(shuffle=False,early_stopping=False,hidden_layer_sizes=(2,1,10,20,1),activation='identity',solver='lbfgs',random_state=res.index(np.min(res)),max_iter=int(2e5)) embedded_model.fit(predictions,train['Target'])
モデルをONNX形式に変換するために必要なライブラリを読み込みます。
import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
モデルのパラメータの形状を定義します。
umap_model_input_shape = [("float_input",FloatTensorType([1,train.iloc[:,1:-2].shape[1]]))] umap_model_output_shape = [("float_output",FloatTensorType([X_embedded.iloc[:,:].shape[1],1]))] embedded_model_input_shape = [("float_input",FloatTensorType([1,X_embedded.iloc[:,:].shape[1]]))] embedded_model_output_shape = [("float_output",FloatTensorType([1,1]))]
ONNXモデルをプロトタイプに変換します。
umap_proto = convert_sklearn(umap_model,initial_types=umap_model_input_shape,final_types=umap_model_output_shape,target_opset=12) embeded_proto = convert_sklearn(embedded_model,initial_types=embedded_model_input_shape,final_types=embedded_model_output_shape,target_opset=12)
プロトタイプをディスクに保存します。
onnx.save(umap_proto,"EURGBP WPR Ridge UMAP.onnx") onnx.save(embeded_proto,"EURGBP WPR Ridge EMBEDDED.onnx")
MQL5でのアプリケーション構築
それでは、アプリケーションの構築を始めましょう。まず、プログラム内で変更されないシステム定数を指定する必要があります。
//+------------------------------------------------------------------+ //| EURGBP Multiple Periods Analysis.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| REMINDER: | //| These ONNX models were trained with Daily EURGBP data ranging | //| from 24 November 2002 until 12 August 2018. Test the strategy | //| outside of these time periods, on the Daily Time-Frame for | //| reliable results. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ //--- ONNX Model I/O Parameters #define UMAP_INPUTS 36 #define UMAP_OUTPUTS 2 #define EMBEDDED_INPUTS 2 #define EMBEDDED_OUTPUTS 1 //--- Our forecasting periods #define HORIZON 10 //--- Our desired time frame #define SYSTEM_TIMEFRAME_1 PERIOD_D1
ONNXモデルを読み込みます。
//+------------------------------------------------------------------+ //| Load our ONNX models as resources | //+------------------------------------------------------------------+ //--- ONNX Model Prototypes #resource "\\Files\\EURGBP WPR UMAP.onnx" as const uchar umap_proto[]; #resource "\\Files\\EURGBP WPR EMBEDDED.onnx" as const uchar embedded_proto[];
次に、アプリケーションに必要なライブラリを読み込みます。
//+------------------------------------------------------------------+ //| Libraries We Need | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Indicators\WPR.mqh> #include <VolatilityDoctor\ONNX\OnnxFloat.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh>
プログラム全体で使用するグローバル変数を定義します。ここで注意すべき点は、定義するグローバル変数はごく少数で済むということです。これは、序論で述べた「OOPによりアプリケーションの名前空間を制御できる」という利点を示しています。使用するほとんどの変数やオブジェクトは、私たちが作成したクラスの中にきちんとまとめられています。
//+------------------------------------------------------------------+ //| Global varaibles | //+------------------------------------------------------------------+ CTrade Trade; TradeInfo *TradeInformation; //--- Our time object let's us know when a new candle has fully formed on the specified time-frame Time *eurgbp_daily; //--- All our different William's Percent Range Periods will be kept in a single array WPR *wpr_array[14]; //--- Our ONNX class objects have usefull functions designed for rapid ONNX development ONNXFloat *umap_onnx,*embedded_onnx; //--- Model forecast double expected_return; int position_timer;
Pythonでトレーニングデータをスケーリングする際に使用したZ1およびZ2スコアもコピーしました。
//--- The average column values from the training set double Z1[] = {7.84311120e-01, 7.87104135e-01, 7.81713516e-01, 7.84343731e-01, 5.23887980e-04, 5.26022077e-04, 5.25382257e-04, 5.25688880e-04, -5.08398234e+01, -5.07130228e+01, -5.05834313e+01, -5.04425081e+01, -5.02709031e+01, -5.01349627e+01, -5.00653250e+01, -5.01661938e+01, -5.03082375e+01, -5.04550339e+01, -5.05861939e+01, -5.06434696e+01, -5.07286211e+01, -5.07819768e+01, 1.96979782e-02, 5.29204133e-02, 4.12732506e-02, 3.20037455e-02, 2.61762719e-02, 2.34184127e-02, 2.62342592e-02, 3.32894491e-02, 3.81853070e-02, 3.85464026e-02, 3.85499926e-02, 3.94004124e-02, 4.02388908e-02, 4.02388908e-02 }; //--- The column standard deviation from the training set double Z2[] = {8.29473604e-02, 8.35406090e-02, 8.23981331e-02, 8.28950223e-02, 1.21995172e-02, 1.22880295e-02, 1.20471133e-02, 1.21798952e-02, 3.00742110e+01, 3.05948913e+01, 3.05244154e+01, 3.03776475e+01, 3.02862706e+01, 3.00844693e+01, 2.98788650e+01, 2.97182936e+01, 2.95133008e+01, 2.93983475e+01, 2.92679071e+01, 2.91072869e+01, 2.90154368e+01, 2.89821474e+01, 4.32293242e+01, 4.43537714e+01, 4.02730688e+01, 3.66106699e+01, 3.41930128e+01, 3.21743917e+01, 3.03647897e+01, 2.87462989e+01, 2.73771066e+01, 2.63857585e+01, 2.54625376e+01, 2.43656339e+01, 2.33983568e+01, 2.26334633e+01 };
初期化時に、指標を設定し、カスタムクラスを初期化します。クラスの読み込みに失敗した場合は、初期化処理を中断し、ユーザーに何が問題であったかを通知します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Do no display the indicators, they will clutter our view TesterHideIndicators(true); //--- Setup our pointers to our WPR objects update_indicators(); //--- Get trade information on the symbol TradeInformation = new TradeInfo(Symbol(),SYSTEM_TIMEFRAME_1); //--- Create our ONNXFloat objects umap_onnx = new ONNXFloat(umap_proto); embedded_onnx = new ONNXFloat(embedded_proto); //--- Create our Time management object eurgbp_daily = new Time(Symbol(),SYSTEM_TIMEFRAME_1); //--- Check if the models are valid if(!umap_onnx.OnnxModelIsValid()) return(INIT_FAILED); if(!embedded_onnx.OnnxModelIsValid()) return(INIT_FAILED); //--- Reset our position timer position_timer = 0; //--- Specify the models I/O shapes if(!umap_onnx.DefineOnnxInputShape(0,1,UMAP_INPUTS)) return(INIT_FAILED); if(!embedded_onnx.DefineOnnxInputShape(0,1,EMBEDDED_INPUTS)) return(INIT_FAILED); if(!umap_onnx.DefineOnnxOutputShape(0,1,UMAP_OUTPUTS)) return(INIT_FAILED); if(!embedded_onnx.DefineOnnxOutputShape(0,1,EMBEDDED_OUTPUTS)) return(INIT_FAILED); //--- return(INIT_SUCCEEDED); }
初期化解除時には、自分で作成したオブジェクトのポインタを削除して後片付けをおこないます。これはMQL5における良いプログラミング習慣であり、複数のインスタンスを同一マシン上で実行した場合でも、クリーンアップをおこなわないことで発生するメモリリークやバッファオーバーフローなどの問題を防ぎます。また、重要な点として、特にC言語などの経験がある開発者の方は、ポインタをメモリアドレスとして理解している場合があることに注意してください。
ここで重要な違いがあります。MQL5に組み込まれた安全機能では、メモリに直接アクセスすることは許可されていません。MetaQuotesの開発者は、各オブジェクトに対して一意の識別子を作成し、その識別子を関連するオブジェクトと結び付けるという回避策を考案しました。そのため、独学などでポインタに慣れている読者は、MQL5におけるポインタは開発者に実際のメモリアドレスを直接提供するものではないことに注意してください。MQL5の開発者は、メモリアドレスの直接操作をセキュリティ上の脆弱性と見なしたためです。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Delete the pointers for our custom objects delete umap_onnx; delete embedded_onnx; delete eurgbp_daily; //--- Delete all pointers to our WPR objects for(int i = 0; i <= 13; i++) { delete wpr_array[i]; } }
価格が更新されるたびに、Timeクラスを呼び出して新しい日足が形成されたかを確認します。新しい日足が形成されている場合は、指標の値を更新し、保有ポジションがなければ取引機会を探し、既に保有している場合はその取引を管理します。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Do we have a new daily candle? if(eurgbp_daily.NewCandle()) { static int i = 0; Print(i+=1); update_indicators(); if(PositionsTotal() == 0) { position_timer =0; find_setup(); } else if((PositionsTotal() > 0) && (position_timer < HORIZON)) position_timer += 1; else if((PositionsTotal() > 0) && (position_timer >= (HORIZON -1))) Trade.PositionClose(Symbol()); Comment("Position Timer: ",position_timer); } }
取引セットアップを探すには、関連する市場データを取得し、ONNXモデルの入力として準備するだけで済みます。各列の平均値を引き、列の標準偏差で割った後、最終的に入力データをvectorf型の定数ベクトルとして格納します。この定数ベクトルをONNXFloat.Predict()メソッドに渡すことで、モデルから予測値を取得できます。これらのクラスを構築することで、必要なコード行数を大幅に削減することができました。
//+------------------------------------------------------------------+ //| Find A Trading Setup For Us | //+------------------------------------------------------------------+ void find_setup(void) { //--- Update our indicators update_indicators(); //--- Prepare our input vector vectorf market_state(UMAP_INPUTS); //--- Fill in the Market Data that has to embedded into UMAP form market_state[0] = (float) iOpen(_Symbol,SYSTEM_TIMEFRAME_1,0); market_state[1] = (float) iHigh(_Symbol,SYSTEM_TIMEFRAME_1,0); market_state[2] = (float) iLow(_Symbol,SYSTEM_TIMEFRAME_1,0); market_state[3] = (float) iClose(_Symbol,SYSTEM_TIMEFRAME_1,0); market_state[4] = (float)(iOpen(_Symbol,SYSTEM_TIMEFRAME_1,0) - iOpen(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON)); market_state[5] = (float)(iHigh(_Symbol,SYSTEM_TIMEFRAME_1,0) - iHigh(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON)); market_state[6] = (float)(iLow(_Symbol,SYSTEM_TIMEFRAME_1,0) - iLow(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON)); market_state[7] = (float)(iClose(_Symbol,SYSTEM_TIMEFRAME_1,0) - iClose(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON)); market_state[8] = (float) wpr_array[0].GetReadingAt(0); market_state[9] = (float) wpr_array[1].GetReadingAt(0); market_state[10] = (float) wpr_array[2].GetReadingAt(0); market_state[11] = (float) wpr_array[3].GetReadingAt(0); market_state[12] = (float) wpr_array[4].GetReadingAt(0); market_state[13] = (float) wpr_array[5].GetReadingAt(0); market_state[14] = (float) wpr_array[6].GetReadingAt(0); market_state[15] = (float) wpr_array[7].GetReadingAt(0); market_state[16] = (float) wpr_array[8].GetReadingAt(0); market_state[17] = (float) wpr_array[9].GetReadingAt(0); market_state[18] = (float) wpr_array[10].GetReadingAt(0); market_state[19] = (float) wpr_array[11].GetReadingAt(0); market_state[20] = (float) wpr_array[12].GetReadingAt(0); market_state[21] = (float) wpr_array[13].GetReadingAt(0); market_state[22] = (float) wpr_array[0].GetDifferencedReadingAt(0); market_state[23] = (float) wpr_array[1].GetDifferencedReadingAt(0); market_state[24] = (float) wpr_array[2].GetDifferencedReadingAt(0); market_state[25] = (float) wpr_array[3].GetDifferencedReadingAt(0); market_state[26] = (float) wpr_array[4].GetDifferencedReadingAt(0); market_state[27] = (float) wpr_array[5].GetDifferencedReadingAt(0); market_state[27] = (float) wpr_array[6].GetDifferencedReadingAt(0); market_state[29] = (float) wpr_array[7].GetDifferencedReadingAt(0); market_state[30] = (float) wpr_array[8].GetDifferencedReadingAt(0); market_state[31] = (float) wpr_array[9].GetDifferencedReadingAt(0); market_state[32] = (float) wpr_array[10].GetDifferencedReadingAt(0); market_state[33] = (float) wpr_array[11].GetDifferencedReadingAt(0); market_state[34] = (float) wpr_array[12].GetDifferencedReadingAt(0); market_state[35] = (float) wpr_array[13].GetDifferencedReadingAt(0); //--- Standardize and scale each input for(int i =0; i < UMAP_INPUTS;i++) { market_state[i] = (float)((market_state[i] - Z1[i]) / Z2[i]); }; const vectorf onnx_inputs = market_state; const vectorf umap_predictions = umap_onnx.Predict(onnx_inputs); Print("UMAP Model Returned Embeddings: ",umap_predictions); const vectorf expected_eurgbp_return = embedded_onnx.Predict(umap_predictions); Print("Embeddings Model Expects EURGBP Returns: ",expected_eurgbp_return); expected_return = expected_eurgbp_return[0]; vector o,c; o.CopyRates(Symbol(),SYSTEM_TIMEFRAME_1,COPY_RATES_OPEN,0,HORIZON); c.CopyRates(Symbol(),SYSTEM_TIMEFRAME_1,COPY_RATES_CLOSE,0,HORIZON); bool bullish_reversal = o.Mean() < c.Mean(); bool bearish_reversal = o.Mean() > c.Mean(); if(bearish_reversal) { if(expected_return > 0) { Trade.Buy((TradeInformation.MinVolume()*2),Symbol(),TradeInformation.GetAsk(),0,0,""); return; } Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,""); return; } else if(bullish_reversal) { if(expected_return < 0) { Trade.Sell((TradeInformation.MinVolume()*2),Symbol(),TradeInformation.GetBid(),0,0,""); } Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,""); return; } }
以下は、テクニカル指標を更新するために呼び出すメソッドの実装です。
//+------------------------------------------------------------------+ //| Update our indicator readings | //+------------------------------------------------------------------+ void update_indicators(void) { //--- Store pointers to our WPR objects for(int i = 0; i <= 13; i++) { //--- Create an WPR object wpr_array[i] = new WPR(Symbol(),SYSTEM_TIMEFRAME_1,((i+1) * 5)); //--- Set the WPR buffers wpr_array[i].SetIndicatorValues(60,true); wpr_array[i].SetDifferencedIndicatorValues(60,HORIZON,true); } }
最後に、プログラムの終了時には、定義したシステム定数を必ず#undefで解除することを忘れないでください。
//+------------------------------------------------------------------+ //| Undefine system constants we no longer need | //+------------------------------------------------------------------+ #undef EMBEDDED_INPUTS #undef EMBEDDED_OUTPUTS #undef UMAP_INPUTS #undef UMAP_OUTPUTS #undef HORIZON #undef SYSTEM_TIMEFRAME_1 //+------------------------------------------------------------------+
アプリケーションを起動すると、図10のように表示されるはずです。これは想定通りであり、テスト中にターミナルが指標を表示しないように指示するコードを、初期化処理にもう1行追加するだけで済みます。
//--- Do no display the indicators, they will clutter our view TesterHideIndicators(true);
図10:使用している指標の数が多いため、最初は画面が乱雑になる
これが完了したら、バックテストを開始できます。トレーニングサンプルは2002年11月から2018年8月までの期間でしたので、バックテスト期間は2018年9月から現在までのはずです。しかし、私のインターネット接続が不安定だったため、ブローカーから履歴データを安全にダウンロードできませんでした。そのため、今回は2023年初めから現在までの期間でテストをおこないました。
図11:バックテスト期間の日付
過去の市場パフォーマンスを現実的に再現するため、可能な限り実際のティックをすべて使用することを推奨します。ただし、要求するデータ量が膨大になるため、ネットワークへの負荷は大きくなります。
図12:バックテストに使用した設定も重要である
構築したクラスは、バックテスト中に常にフィードバックを提供します。出力されるメッセージを確認することで、エラーの有無をチェックできます。図13の通り、クラスは期待通りに動作しており、エラーメッセージは記録されていません。
図13:構築したクラスはバックテスト中にフィードバックを提供する。フィードバックは正の内容で、保有ポジションがない場合は常にモデルの予測で終了する
また、戦略によって生成された資産曲線も可視化できます。資産曲線は長期的に上昇傾向を示しており、戦略のさらなる開発や、可能であれば損失を抑える追加の安全機能の検討を促します。
図14:取引戦略によって生成された資産曲線
さらに、取引戦略のパフォーマンスを詳細に分析することも可能です。図15の通り、戦略は全取引において58%の正確性を達成し、シャープレシオは0.90でした。
図15:未使用データに対する取引戦略のパフォーマンス詳細分析
結論
この議論を通じて、読者は、単なる価格予測にとどまらない、統計モデルの実務的な活用方法を理解できます。具体的には以下の点を示しました。
- 機械学習はマネーマネジメントに活用できる用:モデルの予測が取引シグナルと一致した場合にロットサイズを増加させることで、統計モデルに取引量の管理を任せることができます。つまり、MLモデルが「自信あり」と判断したときに、コンピュータがより大きなポジションを取れるようになります。
- 機械学習によるデータのより意味のある解析:次元削減と呼ばれる機械学習アルゴリズムを用いることで、大規模データセットから重要なパターンを抽出できます。
本記事の手法を応用すれば、WPR指標を好みの複数の指標に置き換え、次元削減をおこなうことで独自戦略の新しい表現を見つけ、取引パフォーマンスを改善できる可能性があります。実際、元の36列データから2列にUMAPで圧縮した場合でも、すべての市場データより優れたパフォーマンスを達成できました。
さらに、本記事で提案したUMAPアルゴリズムを使用することで、PCA(主成分分析)のような従来の手法にはない多くの利点を享受できます。主な利点を以下に示します。
- UMAPは非線形手法である:PCAなどの一般的な次元削減手法は、データに線形関係が存在することを前提としています。この前提が成り立たない場合、アルゴリズムはうまく機能しません。一方、UMAPは非線形関係を明示的に捉えることを目的として設計されています。「PCAより強力」と表現するよりも、「PCAより柔軟」と表現する方が適切です。
- UMAPはユークリッドではなく幾何学的である: UMAPは直線距離だけでなく、データの形状そのものを捉えます。PCAのようにデータを直線で切る手法とは異なり、UMAPはデータの形に沿って曲がります。データが平坦な世界に存在するとは仮定せず、リーマン多様体と呼ばれる曲面上に存在すると仮定することで、データの本来の幾何学的構造を保持します。UMAPはデータを平坦化するのではなく、流れに沿って形を維持するのです。
最後に、読者はMQL5におけるオブジェクト指向プログラミング(OOP)の価値を再認識できます。OOPは古い技術と見なされることもありますが、制御やエラー処理を単一のファイルに集中させることができる点で非常に有用です。冗長なコードを繰り返す手間を省き、アイデアを迅速かつ予測可能な結果で実行できる利点があります。
ファイル名 | ファイルの説明 |
---|---|
Use_All_Data.ipynb | 市場データを分析するために使用されるJupyter Notebook |
Fetch_Data_Algorithmic_Input_Selection.mq5 | 必要な市場データを取得するために使用されるMQL5スクリプト |
EURGBP_Multiple_Periods_Analysis.mq5 | 本記事で構築した、一度に14の異なるWPR期間を使用するEA |
EURGBP_WPR_Algorithmic_Input_Selection.csv | ブローカーから取得した過去の市場データ |
EURGBP_WPR_EMBEDDED.onnx | 36列のデータを2つのUMAP埋め込みに近似するONNXモデル |
EURGBP_WPR_UMAP.onnx | 2つのUMAP埋め込みを与えられた、EURGBP市場のリターンを予測するONNXモデル |
EURGBP_Multiple_Periods_Analysis.ex5 | EAのコンパイル済みバージョン |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18187
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。




- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索