多通貨エキスパートアドバイザーの開発(第21回):重要な実験の準備とコードの最適化
はじめに
前回の記事では、蓄積された価格データを考慮して新しい最終エキスパートアドバイザー(EA)を取得することを可能にする自動最適化コンベアの整理に着手しました。しかし、最終ステージの実装方法について依然として難しい判断が必要であり、完全な自動化にはまだ至っていません。これらの判断は、もし誤った選択をしてしまうと、多くの作業をやり直さなければならないため、非常に重要です。そのため、できる限り労力を節約し、正しい選択をしたいと強く考えています。そして、難しい決断を下すうえで最も役に立つものがあるとすれば……それは決断をあえて先延ばしにすることです。特に、それが許される状況であればなおさらです。
ただし、先延ばしにもさまざまな方法があります。単に選択の瞬間を遅らせるのではなく、気分転換になりそうでありながら、実際には正しい道筋を見出す、あるいは少なくとも決断への動機付けを高める助けとなる別の課題に取り組んでみましょう。
興味深い問い
パラメータ最適化の利用を巡る多くの議論における最大の障害は、取得したパラメータを将来の期間において、収益性およびドローダウンを所定の水準に保ったまま、どれだけ長く取引に使用できるのかという点です。そして、そもそもそれは可能なのかという問題でもあります。
将来におけるテスト結果の再現性は一般に信頼できず、戦略が「破綻する」かどうかは運次第に過ぎない、という見方は広く存在します。おそらく、取引戦略の開発者のほぼ全員が、この考えを信じたいと思っているでしょう。もしそうでなければ、膨大な労力をかけて開発やテストをおこなう意味が失われてしまうからです。
適切なパラメータを選択することで、戦略が一定期間は成功裏に機能し続けるという信頼性を高めようとする試みは、これまでにも繰り返しおこなわれてきました。定期的なEAパラメータの自動選択というテーマを、何らかの形で扱った記事も存在します。その中でも、@fxsaberによるValidate EAは、非常に興味深い実験をおこなうためのツールとして特筆すべき存在です。
このツールを使用すると、任意のEA(検証対象)を取り上げ、一定の期間(たとえば3年)を指定したうえで、次のプロセスを実行できます。まず、対象EAを一定期間(たとえば2か月)で最適化し、その後、最良の設定を用いて、たとえば2週間の期間、ストラテジーテスター上で取引をおこないます。各2週間の終了時点で、対象EAは直近2か月間で再度最適化され、さらに次の2週間の取引がおこなわれます。このプロセスは、指定した3年間の期間が終了するまで継続されます。
最終的な結果として、もしEAが実際に定期的に再最適化され、更新されたパラメータで稼働していたと仮定した場合に、3年間を通じてどのように取引していたかを示す取引レポートが得られます。ここで使用する時間間隔は、任意に設定可能であることは明らかです。もし、あるEAがこのような再最適化のもとで許容可能な結果を示すのであれば、それは実運用における高い潜在性を示唆するものとなります。
しかし、このツールには重要な制約があります。それは、検証対象のEAが、最適化をおこなうための入力パラメータを公開していなければならないという点です。たとえば、前回までの記事で、多数の単体インスタンスを組み合わせて作成した最終EAを考えると、これらにはポジションを建てる取引ロジックに影響を与える入力パラメータが存在しません。資金管理やリスク管理のパラメータについては、最適化が可能であるとしても、その意味は乏しいため、ここでは考慮しません。なぜなら、建てるポジションサイズを大きくすれば、当然ながら、より小さなポジションサイズで得られた結果よりも大きな利益が表示されることは明白だからです。
そこで、私たちが開発したEAに適用可能な形で、同様の仕組みを実装することを試みることにしましょう。
道筋の整理
一般的に、ほぼ同一のプロジェクトでデータベースを埋めるためのスクリプトが必要になります。主な違いは、最適化期間の開始日と終了日のみです。ステージ、ステージ内の作業、そして作業内のタスクの構成は、完全に同一であっても構いません。そのため現時点では、入力項目を少数に抑えたサービスEAを作成し、最適化期間の開始日と期間を含めるだけで十分です。開始日を探索する最適化モードでこれを実行することで、類似したプロジェクトをデータベースに投入できます。ほかにどのパラメータを入力に含めるのが妥当かについては、開発が進む中で決めていきます。
1つのプロジェクト内だけであっても、すべての最適化タスクを完全に実行するには長い時間がかかる場合があります。さらに、そのようなプロジェクトが1つではなく、10個以上必要になると、かなり時間のかかる作業になります。したがって、ステージEAの処理を何らかの方法で高速化できないか検討する意味があります。修正すべきボトルネックを特定するために、MetaEditorに含まれているプロファイラーを使用します。
次に、取得された複数の初期化文字列からどのように作業をシミュレーションするかを決める必要があります。各プロジェクトはタスク完了後、最終EAの初期化文字列を1つ提供します。この用途に特化した新しいテストEAを作成する必要がある可能性が高いですが、これはおそらく次回の記事まで延期します。
まずはテストEAのコード最適化から始めましょう。その後、データベースを埋めるためのスクリプト作成に取りかかります。
コードの最適化
本題の実装に入る前に、自動最適化に関わるEAのコードを高速化できる余地があるかを確認してみましょう。考えられるボトルネックを検出するため、前回の記事で作成した最終EAを研究対象として使用します。このEAは、単一の取引戦略のインスタンスを32個組み合わせたものです(2銘柄×1時間軸×16インスタンス=32)。もちろん、最終EAで想定される総インスタンス数よりははるかに少ないですが、最適化中の大部分の試行では、第1ステージでは1インスタンスのみ、第2ステージでも最大16インスタンスまでしか使用しません。したがって、このようなテスト用EAは今回の目的に完全に適しています。
それでは、履歴データ上でEAをプロファイリングモードで起動してみましょう。このモードで実行すると、プロファイリング用の特別なEAバージョンが自動的にコンパイルされ、ストラテジーテスター内で起動されます。以下に、リファレンスに記載されているプロファイリング使用方法の説明を引用します。
プロファイリングにはサンプリング方式が使用されます。プロファイラーはMQLプログラムの動作を一定間隔(約1秒間に10000回)で一時停止し、どのコード部分で何回停止が発生したかの統計を収集します。この際、コールスタックを解析し、各関数が全体のコード実行時間にどれだけ寄与しているかを判定します。
サンプリングは軽量かつ高精度な方法です。他の手法とは異なり、サンプリングでは解析対象のコードに一切変更を加えないため、実行速度に影響を与えません。
プロファイリングレポートは、関数またはプログラム行ごとに表示され、それぞれに以下の2つの指標が用意されています。
- Total CPU [unit,%]:関数がコールスタックに出現した回数
- Self CPU [unit of measurement,%]:指定された関数の内部で直接発生した「停止」の回数。この値はボトルネックを特定するうえで極めて重要です。統計的に見ると、より多くのプロセッサ時間を必要とする箇所で停止はより頻繁に発生します。
これらの値は、絶対値と全体に対する割合の両方で表示されます。
プロファイリング実行後の結果は以下のようになりました。

図1:調査対象EAコードのプロファイリング結果
デフォルトでは、プロファイリング結果の一覧には上位レベルに位置する大きな関数が表示されます。しかし、関数名の行をクリックすると、そこから呼び出された関数のネストされた一覧を確認できます。これにより、どのコード部分が最もCPU時間を消費しているかを、より正確に特定できます。
最初の2行には、予想どおりOnTick()ハンドラと、そこから呼び出されるCVirtualAdvisor::Tick()ハンドラが表示されています。初期化処理を除けば、EAが最も多くの時間を費やしているのは、受信するティックの処理であることは明らかです。しかし、結果の3行目と4行目については、妥当な疑問が生じます。
なぜ現在の銘柄を選択するメソッドの呼び出しがこれほど多いのでしょうか。なぜ銘柄のいくつかの整数プロパティを取得するだけで、これほど多くの時間が費やされているのでしょうか。これを詳しく見ていきましょう。
CSymbolInfo::Name(string name)メソッド呼び出しに対応する行を展開すると、ほぼすべての時間が、仮想ポジションをクローズする必要があるかどうかをチェックする関数からの呼び出しに費やされていることが分かります。
//+------------------------------------------------------------------+ //| Check the need to close by SL, TP or EX | //+------------------------------------------------------------------+ bool CVirtualOrder::CheckClose() { if(IsMarketOrder()) { // If this is a market virtual position, s_symbolInfo.Name(m_symbol); // Select the desired symbol s_symbolInfo.RefreshRates(); // Update information about current prices // ... } return false; }
このコードはかなり以前に書かれたものです。当時は、建てた仮想ポジションが正しく実ポジションに変換されることが重要でした。仮想ポジションをクローズすると、対応する実ポジションの一部または全体が即座に(もしくはほぼ即座に)クローズされる想定でした。そのため、このチェックはすべてのティックごと、かつすべての仮想ポジションに対して実行される必要がありました。
クラスの自己完結性を確保するために、各CVirtualOrderクラスのオブジェクトには、それぞれCSymbolInfoクラスのインスタンスを持たせ、そのインスタンスを通じて、必要な取引商品(銘柄)の価格や仕様に関するすべての情報を取得していました。その結果、1つの取引戦略インスタンスが3つの仮想ポジションを使用し、それが16インスタンス存在する場合、仮想ポジション配列内には16×3=48個のCSymbolInfo関連オブジェクトが存在することになります。EAに数百の取引戦略インスタンスが含まれ、さらにより多くの仮想ポジションを使用する場合、銘柄選択メソッドの呼び出し回数は何倍にも増加します。しかし、それは本当に必要なのでしょうか。
実際に銘柄名を選択するメソッドを呼び出す必要があるのはいつでしょうか。それは、仮想ポジションの銘柄が変更された場合だけです。前回のティック以降に銘柄が変わっていないのであれば、このメソッドを呼び出すのは無意味です。銘柄が変更されるのは、これまでに存在しなかった仮想ポジションを新しく建てた場合、あるいは別の銘柄で仮想ポジションを建てた場合に限られます。これは毎ティック発生するようなことではなく、はるかに低い頻度でしか起こりません。さらに、使用しているモデル戦略では、1つの仮想ポジションに対して銘柄が変更されることはありません。なぜなら、1つの取引戦略インスタンスは単一の銘柄のみを扱い、その戦略インスタンスに属するすべての仮想ポジションは同一の銘柄を使用するからです。
このような理由から、CSymbolInfoクラスのオブジェクトを取引戦略インスタンスのレベルに移動することも考えられますが、これでも冗長になる可能性があります。なぜなら、異なる取引戦略インスタンスが同じ銘柄を使用する場合があるからです。そこで、さらに上位のグローバルレベルに配置することにしました。このレベルでは、EAで使用される異なる銘柄の数と同じ数だけCSymbolInfoクラスのインスタンスを持てば十分です。各CSymbolInfoインスタンスは、新しい銘柄のプロパティにアクセスする必要が生じたときにのみ作成されます。一度作成されたインスタンスは、その銘柄に恒久的に割り当てられます。
書籍中の次の例に着想を得て、独自のCSymbolsMonitorクラスを作成します。この例とは異なり、既存の標準ライブラリクラスの機能を実質的に繰り返すだけの新しいクラスを作ることはしません。今回のクラスは、CSymbolInfoクラスの複数のオブジェクトを格納するコンテナとして機能し、使用される各銘柄ごとに個別の情報オブジェクトが作成されることを保証します。
コードのどこからでもアクセスできるようにするため、実装には再びシングルトンデザインパターンを使用します。このクラスの基盤となるのは、CSymbolInfoクラスのオブジェクトへのポインタを格納するm_symbols[]配列です。
//+--------------------------------------------------------------------+ //| Class for obtaining information about trading instruments (symbols)| //+--------------------------------------------------------------------+ class CSymbolsMonitor { protected: // Static pointer to a single class instance static CSymbolsMonitor *s_instance; // Array of information objects for different symbols CSymbolInfo *m_symbols[]; //--- Private methods CSymbolsMonitor() {} // Closed constructor public: ~CSymbolsMonitor(); // Destructor //--- Static methods static CSymbolsMonitor *Instance(); // Singleton - creating and getting a single instance // Tick handling for objects of different symbols void Tick(); // Operator for getting an object with information about a specific symbol CSymbolInfo* operator[](const string &symbol); }; // Initializing a static pointer to a single class instance CSymbolsMonitor *CSymbolsMonitor::s_instance = NULL;
単一インスタンスを作成するための静的メソッドの実装は、以前にすでに見てきた実装と同様です。デストラクタには、作成された情報オブジェクトを削除するためのループが含まれます。
//+------------------------------------------------------------------+ //| Singleton - creating and getting a single instance | //+------------------------------------------------------------------+ CSymbolsMonitor* CSymbolsMonitor::Instance() { if(!s_instance) { s_instance = new CSymbolsMonitor(); } return s_instance; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CSymbolsMonitor::~CSymbolsMonitor() { // Delete all created information objects for symbols FOREACH(m_symbols, if(!!m_symbols[i]) delete m_symbols[i]); }
公開されているティック処理メソッドは、銘柄仕様およびクオート情報の定期的な更新を提供します。仕様は時間の経過とともにまったく変更されない場合もありますが、念のため1日に1回更新するようにします。クオートは1分ごとに更新します。これは、EAの動作モードを1分足のオープン時のみに使用するためであり、1分OHLCモードおよび実ティックに基づく毎ティックモードにおいて、モデリング結果の再現性を高めるためです。
//+------------------------------------------------------------------+ //| Handle a tick for the array of virtual orders (positions) | //+------------------------------------------------------------------+ void CSymbolsMonitor::Tick() { // Update quotes every minute and specification once a day FOREACH(m_symbols, { if(IsNewBar(m_symbols[i].Name(), PERIOD_D1)) { m_symbols[i].Refresh(); } if(IsNewBar(m_symbols[i].Name(), PERIOD_M1)) { m_symbols[i].RefreshRates(); } }); }
最後に、銘柄名を指定して目的のオブジェクトへのポインタを取得するための、オーバーロードされたインデックス演算子を追加します。この演算子内で、これまでこの演算子を通じてアクセスされたことのない銘柄に対して、新しい情報オブジェクトが自動的に作成されます。
//+-------------------------------------------------------------------------+ //| Operator for getting an object with information about a specific symbol | //+-------------------------------------------------------------------------+ CSymbolInfo* CSymbolsMonitor::operator[](const string &name) { // Search for the information object for the given symbol in the array int i; SEARCH(m_symbols, m_symbols[i].Name() == name, i); // If found, return it if(i != -1) { return m_symbols[i]; } else { // Otherwise, create a new information object CSymbolInfo *s = new CSymbolInfo(); // Select the desired symbol for it if(s.Name(name)) { // If the selection is successful, update the quotes s.RefreshRates(); // Add to the array of information objects and return it APPEND(m_symbols, s); return s; } else { PrintFormat(__FUNCTION__" | ERROR: can't create symbol with name [%s]", name); } } return NULL; }
受け取ったコードを、現在のフォルダ内のSymbolsMonitor.mqhファイルに保存してください。次はいよいよ、作成したクラスを使用するコードの番です。
CVirtualAdvisorの変更
このクラスには、すでに単一インスタンスで存在し、特定のタスクを実行するいくつかのオブジェクトがあります。具体的には、仮想ポジションのボリュームを受信するレシーバー、リスクマネージャー、そしてユーザー情報インターフェースです。ここに銘柄モニターオブジェクトを追加します。より正確には、銘柄モニターオブジェクトへのポインタを保持するクラスフィールドを作成します。
class CVirtualAdvisor : public CAdvisor { protected: CSymbolsMonitor *m_symbols; // Symbol monitor object CVirtualReceiver *m_receiver; // Receiver object that brings positions to the market CVirtualInterface *m_interface; // Interface object to show the status to the user CVirtualRiskManager *m_riskManager; // Risk manager object ... public: ... };
銘柄モニターオブジェクトの生成は、コンストラクタが呼び出された際に、他のオブジェクトと同様にCSymbolsMonitor::Instance()静的メソッドを呼び出すことでおこないます。また、デストラクタではこのオブジェクトの削除を追加します。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(string p_params) { ... // If there are no read errors, if(IsValid()) { // Create a strategy group CREATE(CVirtualStrategyGroup, p_group, groupParams); // Initialize the symbol monitor with a static symbol monitor m_symbols = CSymbolsMonitor::Instance(); // Initialize the receiver with the static receiver m_receiver = CVirtualReceiver::Instance(p_magic); // Initialize the interface with the static interface m_interface = CVirtualInterface::Instance(p_magic); ... } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { if(!!m_symbols) delete m_symbols; // Remove the symbol monitor if(!!m_receiver) delete m_receiver; // Remove the recipient if(!!m_interface) delete m_interface; // Remove the interface if(!!m_riskManager) delete m_riskManager; // Remove risk manager DestroyNewBar(); // Remove the new bar tracking objects }
新しいティックハンドラにTick()メソッドの呼び出しを追加し、銘柄を監視できるようにします。ここでEAが使用するすべての銘柄のクオートが更新されます。
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Define a new bar for all required symbols and timeframes bool isNewBar = UpdateNewBar(); // If there is no new bar anywhere, and we only work on new bars, then exit if(!isNewBar && m_useOnlyNewBar) { return; } // Symbol monitor updates quotes m_symbols.Tick(); // Receiver handles virtual positions m_receiver.Tick(); // Start handling in strategies CAdvisor::Tick(); // Risk manager handles virtual positions m_riskManager.Tick(); // Adjusting market volumes m_receiver.Correct(); // Save status Save(); // Render the interface m_interface.Redraw(); }
この機会に、将来を見据えてこのクラスにChartEventイベントハンドラを追加します。現時点では、その中でm_interfaceインターフェースオブジェクトの同名メソッドを呼び出すだけです。このメソッドは、今の段階では何もしません。
VirtualAdvisor.mqhファイルに加えられた変更を現在のフォルダに保存します。
CVirtualOrderの修正
すでに述べたように、銘柄に関する情報の取得は仮想ポジションのクラス内でおこなわれます。したがって、まずこのクラスから変更を始めます。最初に、モニター(CSymbolsMonitorクラス)と、銘柄の情報オブジェクト(CSymbolInfoクラス)へのポインタを追加します。
class CVirtualOrder { private: //--- Static fields static ulong s_count; // Counter of all created CVirtualOrder objects CSymbolInfo *m_symbolInfo; // Object for getting symbol properties //--- Related recipient objects and strategies CSymbolsMonitor *m_symbols; CVirtualReceiver *m_receiver; CVirtualStrategy *m_strategy; ... }
クラスフィールドの構成にポインタを追加するということは、これらに作成済みオブジェクトへのポインタを代入する必要があることを意味します。また、これらのオブジェクトがこのクラスのオブジェクトのメソッド内で作成される場合には、正しく削除されるよう注意する必要があります。
銘柄モニターへのポインタの初期化と、銘柄情報オブジェクトへのポインタのクリアを追加します。銘柄モニターへのポインタを取得するために、CSymbolsMonitor::Instance()静的メソッドを呼び出します。単一のモニターオブジェクトの生成(存在しない場合)は、その内部でおこなわれます。デストラクタでは、情報オブジェクトが作成されていて、まだ削除されていない場合にそれを削除する処理を追加します。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualOrder::CVirtualOrder(CVirtualStrategy *p_strategy) : // Initialization list m_id(++s_count), // New ID = object counter + 1 ... m_point(0) { PrintFormat(__FUNCTION__ + "#%d | CREATED VirtualOrder", m_id); m_symbolInfo = NULL; m_symbols = CSymbolsMonitor::Instance(); } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CVirtualOrder::~CVirtualOrder() { if(!!m_symbolInfo) delete m_symbolInfo; }
コンストラクタではm_symbolInfo情報オブジェクトへのポインタ取得を追加していません。なぜなら、コンストラクタ呼び出し時点では、この仮想ポジションでどの銘柄が使用されるかが常に明確とは限らないからです。これは仮想ポジションを建てる、つまりCVirtualOrder::Open()メソッドが呼び出されたときに初めて明らかになります。そこで、銘柄情報オブジェクトへのポインタの初期化をこのメソッドに追加します。
//+------------------------------------------------------------------+ //| Open a virtual position (order) | //+------------------------------------------------------------------+ bool CVirtualOrder::Open(string symbol, // Symbol ENUM_ORDER_TYPE type, // Type (BUY or SELL) double lot, // Volume double price = 0, // Open price double sl = 0, // StopLoss level (price or points) double tp = 0, // TakeProfit level (price or points) string comment = "", // Comment datetime expiration = 0, // Expiration time bool inPoints = false // Are the SL and TP levels set in points? ) { if(IsOpen()) { // If the position is already open, then do nothing PrintFormat(__FUNCTION__ "#%d | ERROR: Order is opened already!", m_id); return false; } // Get a pointer to the information object for the desired symbol from the symbol monitor m_symbolInfo = m_symbols[symbol]; if(!!m_symbolInfo) { // Actions to open ... return true; } else { ... return false; } }
これで、銘柄モニターが銘柄のクオート情報の更新を担当するようになったため、CVirtualOrderクラスから、銘柄プロパティ情報オブジェクトm_symbolInfoに対するName()およびRefreshRates()メソッドの呼び出しをすべて削除できます。仮想ポジションを建てる際には、m_symbolInfoに、必要な銘柄がすでに選択されたオブジェクトへのポインタを保存します。すでに開いている仮想ポジションを管理する場合、RefreshRates()メソッドはこのティックですでに一度呼び出されています。これはCSymbolsMonitor::Tick()メソッド内で、すべての銘柄に対して銘柄モニターが実行しています。
再度プロファイリングをおこないましょう。結果は改善されていますが、SymbolInfoDouble()関数の呼び出しが依然として9%を占めています。簡単に調査したところ、これらの呼び出しはスプレッド値を取得するために必要であることが分かりました。しかし、この処理は、RefreshRates()メソッド呼び出し時にすでに取得されている価格(Ask−Bid)の差分を計算することで置き換えることができます。これにより、追加のSymbolInfoDouble()関数呼び出しは不要になります。
さらに、このクラスには、動作速度の向上とは直接関係せず、今回検討しているモデル戦略には必須ではない変更も加えられました。
- CVirtualStrategy::OnOpen()およびCVirtualStrategy::OnClose()ハンドラに、現在のオブジェクトを渡すようにした
- クローズされた仮想ポジションからの利益計算を追加した
- StopLossおよびTakeProfitレベル用のゲッターとセッターを追加した
- 仮想ポジションを建てる際に割り当てられる一意のチケットを追加した
おそらく、このライブラリは今後さらに大きな改修がおこなわれるでしょう。そのため、これらの変更の詳細な説明には立ち入りません。
最後に、変更した内容を現在のフォルダにあるVirtualOrder.mqhファイルに保存します。
戦略の修正
銘柄モニターを使用するために、取引戦略クラスにもいくつかの小さな修正をおこなう必要がありました。まず、仮想ポジションのクラスと同様に、m_symbolInfoメンバがオブジェクトそのものではなく、オブジェクトへのポインタを保持するように変更しました。
//+------------------------------------------------------------------+ //| Trading strategy using tick volumes | //+------------------------------------------------------------------+ class CSimpleVolumesStrategy : public CVirtualStrategy { protected: ... CSymbolInfo *m_symbolInfo; // Object for getting information about the symbol properties ... public: ... };
次に、コンストラクタでその初期化を追加しました。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSimpleVolumesStrategy::CSimpleVolumesStrategy(string p_params) { ... // Register the event handler for a new bar on the minimum timeframe //IsNewBar(m_symbol, PERIOD_M1); m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol]; ... }
ここでは、新バーイベントハンドラの登録をコメントアウトしています。これは、新バーの登録が今後は銘柄モニター内でおこなわれるためです。
次に、戦略コード内でおこなっていた現在価格の更新処理を削除しました。これは、エントリーシグナルをチェックするメソッドおよびポジションを実際に建てるメソッド内でおこなわれていた処理です。現在は、銘柄モニターがこの役割も担っているため、戦略側で更新する必要がなくなりました。
最後に、変更した内容を現在のフォルダにある SimpleVolumesStrategy.mqhファイルに保存します。
妥当性の確認
銘柄モニター追加に関連する変更をおこなう前後で、同一期間における対象EAのテスト結果を比較してみましょう。

図2:以前のバージョンと銘柄モニターを追加した現在のバージョンのテスト結果比較
ご覧のとおり、結果は全体的にはほぼ同じですが、いくつかの小さな違いがあります。分かりやすくするため、表形式で示します。
| バージョン | 利益 | 損失率 | 標準化された利益 |
|---|---|---|---|
| 以前のバージョン | 41 990.62 | 1 019.49 (0.10%) | 6 867.78 |
| 現在のバージョン | 42 793.27 | 1 158.38 (0.11%) | 6 159.87 |
レポート内の最初の取引を比較すると、以前のバージョンには存在していた追加ポジションが現在のバージョンにはなく、その逆のケースも確認できます。これは、おそらくテスターをEUR/GBPで起動した場合、EUR/GBPの新バーはmm:00に発生しますが、別の銘柄、たとえばGBP/USDではmm:00またはmm:20に発生する可能性があるためです。
この影響を排除するため、戦略に新バー発生を追加でチェックする処理を加えます。
//+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::Tick() override { if(IsNewBar(m_symbol, PERIOD_M1)) { // If their number is less than allowed if(m_ordersTotal < m_maxCountOfOrders) { // Get an open signal int signal = SignalForOpen(); if(signal == 1 /* || m_ordersTotal < 1 */) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } } }
この修正後、結果はさらに改善されました。現在のバージョンは、最も高い正規化利益を示しました。
| バージョン | 利益 | 損失率 | 標準化された利益 |
|---|---|---|---|
| 以前のバージョン | 46 565.39 | 1 079.93 (0.11%) | 7 189.77 |
| 現在のバージョン | 47 897.30 | 1 051.37 (0.10%) | 7 596.31 |
この結果から、加えた変更はそのまま採用することにします。それでは次に、データベースを埋めるためのスクリプト作成に進みましょう。
データベースにプロジェクトを追加する
スクリプトは作成せず、EAを作成しますが、挙動はスクリプトのようになります。すべての処理は初期化関数内で実行され、その後最初のティックでEAはアンロードされます。この実装により、チャート上でも、またパラメータを指定範囲内で変更して複数回実行したい場合にはオプティマイザでも実行できるようになります。
今回は初めての実装であるため、どの入力パラメータの構成が最も便利かを事前に深く考えることはせず、最低限動作するプロトタイプを作成することにします。最終的に得られたパラメータ一覧は以下のとおりです。
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "::: Database" sinput string fileName_ = "article.16373.db.sqlite"; // - Main database file input group "::: Project parameters" sinput string projectName_ = "SimpleVolumes"; // - Name sinput string projectVersion_ = "1.20"; // - Version sinput string symbols_ = "GBPUSD;EURUSD;EURGBP"; // - Symbols sinput string timeframes_ = "H1;M30;M15"; // - Timeframes input datetime fromDate_ = D'2018-01-01'; // - Start date input datetime toDate_ = D'2023-01-01'; // - End date
プロジェクト名とバージョンは明確です。その後に、セミコロン区切りで銘柄と時間足のリストを渡す2つのパラメータがあります。これらは、取引戦略の単一インスタンスを取得するために使用されます。各銘柄に対して、すべての時間軸が順番に使用されます。したがって、デフォルト値で3つの銘柄と3つの時間軸を指定した場合、合計9つの単一インスタンスが作成されます。
各単一インスタンスは、まず第1ステージの最適化を通過し、そのインスタンス専用に最適なパラメータの組み合わせが選択されます。より正確には、最適化中に多数の組み合わせを試し、その中から一定数の「良好な」ものを選択します。
この選択は第2ステージの最適化でおこなわれます。その結果、特定の銘柄と時間足で動作する複数の「良好な」インスタンスのグループが得られます。この第2ステージをすべての銘柄と時間足の組み合わせに対して繰り返すことで、各組み合わせごとに9つのグループが作成されます。
第3ステージでは、これら9つのグループを統合し、最終的なEAを作成するために使用できる初期化文字列を生成してライブラリに保存します。このEAには、これらすべてのグループの単一インスタンスが含まれます。
これらすべてのステージを順次実行するコードはすでに作成されており、必要な「指示」がデータベース内に生成されれば動作します。これまでは、これらを手動でデータベースに追加していましたが、今回この定型的な作業を開発したEAに移行したいと考えています。
このEAの残り2つのパラメータは、最適化区間の開始日と終了日を設定するためのものです。これらを使用して、定期的な再最適化をシミュレートし、再最適化後どの程度の期間、最終EAが最適化区間と同じ結果で取引できるかを確認します。
以上を踏まえると、初期化関数のコードは次のようになります。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Connect to the database DB::Connect(fileName_); // Create a project CreateProject(projectName_, projectVersion_, StringFormat("%s - %s", TimeToString(fromDate_, TIME_DATE), TimeToString(toDate_, TIME_DATE) ) ); // Create project stages CreateStages(); // Creating jobs and tasks CreateJobs(); // Queueing the project for execution QueueProject(); // Close the database DB::Close(); // Successful initialization return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Tick handling | //+------------------------------------------------------------------+ void OnTick() { // Since all work is done in OnInit(), delete the EA ExpertRemove(); }
つまり、まずプロジェクトテーブルにエントリを作成し、次にプロジェクトステージテーブルにステージを追加し、その後各ジョブに対して作業テーブルとタスクテーブルを埋めていきます。最後に、プロジェクトのステータスをQueuedに設定します。データベース内のトリガーにより、プロジェクトに属するすべてのステージ、ジョブ、タスクもQueuedステータスに移行します。
それでは、作成した関数のコードを詳しく見ていきましょう。最も単純なのはプロジェクト作成関数です。これは1つのSQLクエリを含み、データを挿入し、新しく作成されたレコードのIDをid_projectグローバル変数に保存します。
//+------------------------------------------------------------------+ //| Create a project | //+------------------------------------------------------------------+ void CreateProject(string name, string ver, string desc = "") { string query = StringFormat("INSERT INTO projects " " VALUES (NULL,'%s','%s','%s',NULL,'Done') RETURNING rowid;", name, ver, desc); PrintFormat(__FUNCTION__" | %s", query); id_project = DB::Insert(query); }
プロジェクトの説明として、最適化区間の開始日と終了日から文字列を生成しています。これにより、同じ取引戦略のバージョンであっても、異なる期間のプロジェクトを区別できます。
ステージ作成関数は少し長くなります。3つのステージを作成するために、3つのSQLクエリが必要です。もちろん、将来的にはステージ数が増える可能性がありますが、現時点では先ほど述べた3つに限定します。各ステージ作成後、そのIDをid_stage1、id_stage2、id_stage3グローバル変数に保存します。
//+------------------------------------------------------------------+ //| Create three stages | //+------------------------------------------------------------------+ void CreateStages() { // Stage 1 - single instance optimization string query1 = StringFormat("INSERT INTO stages VALUES(" "NULL," // id_stage "%I64u," // id_project "%s," // id_parent_stage "'%s'," // name "'%s'," // expert "'%s'," // symbol "'%s'," // period "%d," // optimization "%d," // model "'%s'," // from_date "'%s'," // to_date "%d," // forward_mode "'%s'," // forward_date "%d," // deposit "'%s'," // currency "%d," // profit_in_pips "%d," // leverage "%d," // execution_mode "%d," // optimization_criterion "'%s'" // status ") RETURNING rowid;", id_project, // id_project "NULL", // id_parent_stage "First", // name "SimpleVolumesStage1.ex5", // expert "GBPUSD", // symbol "H1", // period 2, // optimization 2, // model TimeToString(fromDate_, TIME_DATE), // from_date TimeToString(toDate_, TIME_DATE), // to_date 0, // forward_mode "0", // forward_date 1000000, // deposit "USD", // currency 0, // profit_in_pips 200, // leverage 0, // execution_mode 7, // optimization_criterion "Done" // status ); PrintFormat(__FUNCTION__" | %s", query1); id_stage1 = DB::Insert(query1); // Stage 2 - selection of a good group of single specimens string query2 = StringFormat("INSERT INTO stages VALUES(" "NULL," // id_stage "%I64u," // id_project "%d," // id_parent_stage "'%s'," // name "'%s'," // expert "'%s'," // symbol "'%s'," // period "%d," // optimization "%d," // model "'%s'," // from_date "'%s'," // to_date "%d," // forward_mode "'%s'," // forward_date "%d," // deposit "'%s'," // currency "%d," // profit_in_pips "%d," // leverage "%d," // execution_mode "%d," // optimization_criterion "'%s'" // status ") RETURNING rowid;", id_project, // id_project id_stage1, // id_parent_stage "Second", // name "SimpleVolumesStage2.ex5", // expert "GBPUSD", // symbol "H1", // period 2, // optimization 2, // model TimeToString(fromDate_, TIME_DATE), // from_date TimeToString(toDate_, TIME_DATE), // to_date 0, // forward_mode "0", // forward_date 1000000, // deposit "USD", // currency 0, // profit_in_pips 200, // leverage 0, // execution_mode 7, // optimization_criterion "Done" // status ); PrintFormat(__FUNCTION__" | %s", query2); id_stage2 = DB::Insert(query2); // Stage 3 - saving the initialization string of the final EA to the library string query3 = StringFormat("INSERT INTO stages VALUES(" "NULL," // id_stage "%I64u," // id_project "%d," // id_parent_stage "'%s'," // name "'%s'," // expert "'%s'," // symbol "'%s'," // period "%d," // optimization "%d," // model "'%s'," // from_date "'%s'," // to_date "%d," // forward_mode "'%s'," // forward_date "%d," // deposit "'%s'," // currency "%d," // profit_in_pips "%d," // leverage "%d," // execution_mode "%d," // optimization_criterion "'%s'" // status ") RETURNING rowid;", id_project, // id_project id_stage2, // id_parent_stage "Save to library", // name "SimpleVolumesStage3.ex5", // expert "GBPUSD", // symbol "H1", // period 0, // optimization 2, // model TimeToString(fromDate_, TIME_DATE), // from_date TimeToString(toDate_, TIME_DATE), // to_date 0, // forward_mode "0", // forward_date 1000000, // deposit "USD", // currency 0, // profit_in_pips 200, // leverage 0, // execution_mode 7, // optimization_criterion "Done" // status ); PrintFormat(__FUNCTION__" | %s", query3); id_stage3 = DB::Insert(query3); }
各ステージでは、名前、親ステージのID、そしてそのステージ用のEA名を指定します。その他のフィールド、たとえば最適化区間や初期証拠金などは、ほとんどのステージで共通になります。
主な処理はCreateJobs()関数に集中します。各ジョブは、銘柄と時間足の1つの組み合わせに対応します。まず、入力パラメータに指定されたすべての銘柄と時間軸から配列を作成します。時間軸については、文字列からENUM_TIMEFRAMES型の値に変換するStringToTimeframe()関数を追加しています。
// Array of symbols for strategies string symbols[]; StringSplit(symbols_, ';', symbols); // Array of timeframes for strategies ENUM_TIMEFRAMES timeframes[]; string sTimeframes[]; StringSplit(timeframes_, ';', sTimeframes); FOREACH(sTimeframes, APPEND(timeframes, StringToTimeframe(sTimeframes[i])));
次に、二重ループで銘柄と時間軸のすべての組み合わせを走査し、カスタム基準を使用した3つの最適化タスクを作成します。
// Stage 1 FOREACH(symbols, { for(int j = 0; j < ArraySize(timeframes); j++) { // Use the optimization parameters template for the first stage string params = StringFormat(paramsTemplate1, ""); // Request to create the first stage job for a given symbol and timeframe string query = StringFormat("INSERT INTO jobs " " VALUES (NULL,%I64u,'%s','%s','%s','Done') " " RETURNING rowid;", id_stage1, symbols[i], IntegerToString(timeframes[j]), params); ulong id_job = DB::Insert(query); // Add the created job ID to the array APPEND(id_jobs1, id_job); // Create three tasks for this job for(int i = 0; i < 3; i++) { query = StringFormat("INSERT INTO tasks " " VALUES (NULL,%I64u,%d,NULL,NULL,'Done');", id_job, 6); DB::Execute(query); } } });
このタスク数は、一方では1つの組み合わせあたり少なくとも1万〜2万回の最適化パスを確保するためであり、もう一方では最適化時間が過度に長くならないようにするために選ばれています。3つすべてのタスクで同じカスタム基準を使用しています。これは、この取引戦略の遺伝的アルゴリズムが、実行ごとにほぼ必ず異なるパラメータ組み合わせへ収束するためです。そのため、単一インスタンスに対して十分に多様な良好パラメータの選択肢が得られます。
将来的には、タスク数や最適化基準をスクリプトの入力パラメータに含めることもできますが、現時点ではコード内にハードコーディングしています。
第1ステージの各ジョブでは、paramsTemplate1グローバル変数で指定された同一の最適化パラメータテンプレートを使用します。
// Template of optimization parameters at the first stage string paramsTemplate1 = "; === Open signal parameters\n" "signalPeriod_=212||12||40||240||Y\n" "signalDeviation_=0.1||0.1||0.1||2.0||Y\n" "signaAddlDeviation_=0.8||0.1||0.1||2.0||Y\n" "; === Pending order parameters\n" "openDistance_=10||0||10||250||Y\n" "stopLevel_=16000||200.0||200.0||20000.0||Y\n" "takeLevel_=240||100||10||2000.0||Y\n" "ordersExpiration_=22000||1000||1000||60000||Y\n" "; === Capital management parameters\n" "maxCountOfOrders_=3||3||1||30||N\n";
追加したジョブのIDは、後で第2ステージのジョブ作成に使用するため、id_jobs1配列に保存します。
第2ステージのジョブ作成では、paramsTemplate2グローバル変数で指定されたテンプレートを使用しますが、ここには可変部分があります。
// Template of optimization parameters for the second stage string paramsTemplate2 = "idParentJob_=%s\n" "useClusters_=false||false||0||true||N\n" "minCustomOntester_=500.0||0.0||0.000000||0.000000||N\n" "minTrades_=40||40||1||400||N\n" "minSharpeRatio_=0.7||0.7||0.070000||7.000000||N\n" "count_=8||8||1||80||N\n";
「idParentJob_=」の後に続く値は、特定の銘柄と時間軸の組み合わせに対応する第1ステージジョブのIDです。これらの値は第1ステージジョブ作成前には分からないため、id_jobs1配列から取得し、第2ステージジョブ作成直前にテンプレートへ差し込みます。
このテンプレート内のcount_パラメータは8に設定されています。つまり、8つの単一インスタンスからなるグループを収集します。第2ステージのEAでは1〜16の範囲で設定可能ですが、第1ステージのタスク数と同様、少なすぎず多すぎないという理由で8を選択しました。後で入力パラメータに移す可能性もあります。
// Stage 2 int k = 0; FOREACH(symbols, { for(int j = 0; j < ArraySize(timeframes); j++) { // Use the optimization parameters template for the second stage string params = StringFormat(paramsTemplate2, IntegerToString(id_jobs1[k])); // Request to create a second stage job for a given symbol and timeframe string query = StringFormat("INSERT INTO jobs " " VALUES (NULL,%I64u,'%s','%s','%s','Done') " " RETURNING rowid;", id_stage2, symbols[i], IntegerToString(timeframes[j]), params); ulong id_job = DB::Insert(query); // Add the created job ID to the array APPEND(id_jobs2, id_job); k++; // Create one task for this job query = StringFormat("INSERT INTO tasks " " VALUES (NULL,%I64u,%d,NULL,NULL,'Done');", id_job, 6); DB::Execute(query); } });
第2ステージでは、1つのジョブにつき1つの最適化タスクのみを作成します。1回の最適化で十分に良好なグループが選択されるためです。最適化基準としてはユーザー定義基準を使用します。
追加したジョブIDはid_jobs2配列にも保存します。最終的には使用しませんでしたが、将来ステージ追加時に役立つ可能性があるため削除していません。
第3ステージでは、パラメータテンプレートには最終グループ名のみが含まれます。この名前でライブラリに登録されます。
// Template of optimization parameters at the third stage string paramsTemplate3 = "groupName_=%s\n" "passes_=";
最終グループ名は、プロジェクト名、バージョン、最適化区間の終了日から生成し、第3ステージのジョブ作成用テンプレートに差し込みます。第3ステージでは、それまでのすべての結果をまとめるため、1つのジョブと1つのタスクのみを作成します。
// Stage 3 // Use the optimization parameters template for the third stage string params = StringFormat(paramsTemplate3, projectName_ + "_v." + projectVersion_ + "_" + TimeToString(toDate_, TIME_DATE)); // // Request to create a third stage job string query = StringFormat("INSERT INTO jobs " " VALUES (NULL,%I64u,'%s','%s','%s','Done') " " RETURNING rowid;", id_stage3, "GBPUSD", "D1", params); ulong id_job = DB::Insert(query); // Create one task for this job query = StringFormat("INSERT INTO tasks " " VALUES (NULL,%I64u,%d,NULL,NULL,'Done');", id_job, 0); DB::Execute(query);
最後に、プロジェクトを実行待ち状態にするため、ステータスを変更します。
//+------------------------------------------------------------------+ //| Queueing the project for execution | //+------------------------------------------------------------------+ void QueueProject() { string query = StringFormat("UPDATE projects SET status='Queued' WHERE id_project=%d;", id_project); DB::Execute(query); }
変更内容を現在のフォルダにあるCreateProject.mq5という新しいファイルに保存します。
もう1つ重要な点があります。データベース構造は今後も固定される可能性が高いため、ライブラリに統合することができます。この目的のため、SQLコマンドの集合としてデータベース構造を定義したdb.schema.sqlファイルを作成し、Database.mqhにリソースとして接続しました。
// Import sql file for creating DB structure #resource "db.schema.sql" as string dbSchema
さらに、Connect()メソッドのロジックを少し変更しました。指定された名前のデータベースが存在しない場合、リソースとして読み込まれたSQLファイルのコマンドを使って自動的に作成されます。同時に、どこからも使われなくなったExecuteFile()メソッドは削除しました。
これで、実装したコードを実際に実行してみる準備が整いました。
データベースへの入力
同時に多くのプロジェクトを生成するのではなく、今回は4つに制限します。そのためには、任意のチャートにEAスクリプトを4回配置し、それぞれ必要なパラメータを設定するだけです。すべてのパラメータの値は終了日以外はデフォルト値のままにします。終了日はテスト期間に1か月ずつ追加することで変更します。
その結果、データベースにはおおよそ以下のような内容が格納されます。プロジェクトテーブルには4つのプロジェクトが存在します。

ステージテーブルには各プロジェクトごとに4つのステージがあります。さらに「Single tester pass」という追加のステージがプロジェクト作成時に自動で作成されます。これは、自動最適化コンベアの外で単独のストラテジーテスターパスを実行したい場合に使用されます。

対応するジョブはジョブテーブルにも追加されている

プロジェクトを実行にかけた後、結果はおおよそ4日間で得られました。性能最適化をおこなったにもかかわらず、これは決して短時間ではありません。しかし、割り当てられないほど長くもありません。strategy_groupsグループライブラリテーブルで確認できます。

passesテーブルでid_passを確認すると、初期化文字列を見ることができます。以下が例です。

または、SimpleVolumesStage3.ex5の第3ステージEAにパスIDを入力として渡し、選択した期間でテスターを実行することもできます。


図3:SimpleVolumesStage3.ex5 EAのパス結果(id_pass=876663、期間2018.01.01 - 2023.01.01)
今回はここで一旦終了し、次回以降の記事で得られた結果のより詳細な分析をおこないます。
結論
こうして、自動最適化コンベアを起動するタスクを自動的に作成する機能を手に入れました。コンベアには3つのステージが含まれます。これはまだ試作段階に過ぎず、今後の開発の方向性を見定めるためのものです。プロジェクトごとにコンベアステージ完了後に最終EAの初期化文字列を自動で統合・置換する機能の実装は、まだ課題として残っています。
しかし、一つだけ確実に言えることがあります。コンベア内での最適化タスクの実行順序はあまり良くないということです。現在は、第2ステージを開始する前に第1ステージの全作業が完了するのを待つ必要があります。同様に、第3ステージも第2ステージの全作業が完了するまで開始されません。もし、最終EAの初期化文字列を「ホット」置換し、最適化中に並行して口座で連続稼働させるような実装をおこなう場合、更新を小さく頻繁におこなうことが可能です。これにより結果が改善される可能性はありますが、あくまで検証が必要な仮説に過ぎません。
また、開発されたEAスクリプトは、今回検討したモデル取引戦略用の最適化プロジェクト作成に特化しています。別の戦略では、ソースコードに若干の変更が必要です。最低限、第1ステージの最適化用入力パラメータ文字列のテンプレートを変更する必要があります。現在は、このテンプレートを直接入力として設定するのは不便なため、まだ入力に移していません。しかし今後、EAスクリプトがファイルから読み込む形でプロジェクト作成タスクを記述するフォーマットを開発する予定です。
ご精読ありがとうございました。またすぐにお会いしましょう。
重要な注意事項
この記事および連載のこれまでのすべての記事で提示された結果は、過去のテストデータのみに基づいており、将来の利益を保証するものではありません。このプロジェクトでの作業は研究的な性質のものであり、公開された結果はすべて、自己責任で使用されるべきです。
アーカイブ内容
| # | 名前 | バージョン | 詳細 | 最近の変更 |
|---|---|---|---|---|
| MQL5/Experts/Article.16373 | ||||
| 1 | Advisor.mqh | 1.04 | EA基本クラス | 第10回 |
| 2 | ClusteringStage1.py | 1.01 | 最適化の第1ステージの結果をクラスタリングするプログラム | 第20回 |
| 3 | CreateProject.mq5 | 1.00 | ステージ、ジョブ、最適化タスクを含むプロジェクトを作成するための EAスクリプト | 第21回 |
| 4 | Database.mqh | 1.09 | データベースを扱うクラス | 第21回 |
| 5 | db.schema.sql | 1.05 | データベース構造 | 第20回 |
| 6 | ExpertHistory.mqh | 1.00 | 取引履歴をファイルにエクスポートするクラス | 第16回 |
| 7 | ExportedGroupsLibrary.mqh | - | 戦略グループ名とその初期化文字列の配列をリストした生成されたファイル | 第17回 |
| 8 | Factorable.mqh | 1.02 | 文字列から作成されたオブジェクトの基本クラス | 第19回 |
| 9 | GroupsLibrary.mqh | 1.01 | 選択された戦略グループのライブラリを操作するためのクラス | 第18回 |
| 10 | HistoryReceiverExpert.mq5 | 1.00 | リスクマネージャーとの取引履歴を再生するためのEA | 第16回 |
| 11 | HistoryStrategy.mqh | 1.00 | 取引履歴を再生するための取引戦略のクラス | 第16回 |
| 12 | Interface.mqh | 1.00 | さまざまなオブジェクトを視覚化するための基本クラス | 第4回 |
| 13 | LibraryExport.mq5 | 1.01 | 選択したパスの初期化文字列をライブラリからExportedGroupsLibrary.mqhファイルに保存するEA | 第18回 |
| 14 | Macros.mqh | 1.02 | 配列操作に便利なマクロ | 第16回 |
| 15 | Money.mqh | 1.01 | 基本的なお金の管理クラス | 第12回 |
| 16 | NewBarEvent.mqh | 1.00 | 特定の銘柄の新しいバーを定義するクラス | 第8回 |
| 17 | Optimization.mq5 | 1.03 | 最適化タスクの起動を管理するEA | 第19回 |
| 18 | Optimizer.mqh | 1.01 | プロジェクト自動最適化マネージャーのクラス | 第20回 |
| 19 | OptimizerTask.mqh | 1.01 | 最適化タスククラス | 第20回 |
| 20 | Receiver.mqh | 1.04 | オープンボリュームを市場ポジションに変換するための基本クラス | 第12回 |
| 21 | SimpleHistoryReceiverExpert.mq5 | 1.00 | 取引履歴を再生するための簡易EA | 第16回 |
| 22 | SimpleVolumesExpert.mq5 | 1.20 | 複数のモデル戦略グループを並列操作するためのEA。パラメータは組み込みのグループライブラリから取得されます。 | 第17回 |
| 23 | SimpleVolumesStage1.mq5 | 1.18 | 取引戦略単一インスタンス最適化EA(第1ステージ) | 第19回 |
| 24 | SimpleVolumesStage2.mq5 | 1.02 | 取引戦略単一インスタンス最適化EA(第2ステージ) | 第19回 |
| 25 | SimpleVolumesStage3.mq5 | 1.02 | 生成された標準化された戦略グループを、指定された名前のグループのライブラリに保存するEA | 第20回 |
| 26 | SimpleVolumesStrategy.mqh | 1.10 | ティックボリュームを使用した取引戦略のクラス | 第21回 |
| 27 | Strategy.mqh | 1.04 | 取引戦略基本クラス | 第10回 |
| 28 | SymbolsMonitor.mqh | 1.00 | 取引商品(銘柄)に関する情報を取得するためのクラス | 第21回 |
| 29 | TesterHandler.mqh | 1.05 | 最適化イベント処理クラス | 第19回 |
| 30 | VirtualAdvisor.mqh | 1.08 | 仮想ポジション(注文)を扱うEAのクラス | 第21回 |
| 31 | VirtualChartOrder.mqh | 1.01 | グラフィカル仮想ポジションクラス | 第18回 |
| 32 | VirtualFactory.mqh | 1.04 | オブジェクトファクトリクラス | 第16回 |
| 33 | VirtualHistoryAdvisor.mqh | 1.00 | トレード履歴再生EAクラス | 第16回 |
| 34 | VirtualInterface.mqh | 1.00 | EAGUIクラス | 第4回 |
| 35 | VirtualOrder.mqh | 1.08 | 仮想注文とポジションのクラス | 第21回 |
| 36 | VirtualReceiver.mqh | 1.03 | オープンボリュームを市場ポジションに変換するクラス(レシーバー) | 第12回 |
| 37 | VirtualRiskManager.mqh | 1.02 | リスクマネジメントクラス(リスクマネージャー) | 第15回 |
| 38 | VirtualStrategy.mqh | 1.05 | 仮想ポジションを使った取引戦略のクラス | 第15回 |
| 39 | VirtualStrategyGroup.mqh | 1.00 | 取引戦略グループのクラス | 第11回 |
| 40 | VirtualSymbolReceiver.mqh | 1.00 | 銘柄レシーバークラス | 第3回 |
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/16373
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
取引におけるニューラルネットワーク:金融市場向けマルチモーダルツール拡張エージェント(最終部)
取引におけるニューラルネットワーク:層状メモリを持つエージェント(最終回)
取引におけるニューラルネットワーク:金融市場向けマルチモーダルツール拡張エージェント(FinAgent)
共和分株式による統計的裁定取引(第6回):スコアリングシステム
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
多通貨エキスパートアドバイザーの開発(第21回)が掲載されました:重要な実験の準備とコードの最適化:
著者:Yuriy Bykov
残念ながら、 のように、すべてが単純ではない。 第三段階の Expert Advisorを 起動できるようにするには、 最適化パイプラインの前の 段階の結果 として得られたパス の IDを指定する必要が ある 。 を取得する 方法は 、 の 記事で説明されている。
理解した。しかし、あなたはあなたの仕事をより簡単に説明するために多くの努力を取っているので、あなたが作成している一連のEAの操作/最適化を教えるためのビデオチュートリアルを作成することができれば、さらに素晴らしいでしょう。ありがとうございます。
理解しています。しかし、あなたはあなたの仕事をより簡単に説明するために多くの努力を払っているので、あなたが作成している一連のEAの操作/最適化を教えるためのビデオチュートリアルを作成することができれば、さらに素晴らしいと思います。ありがとうございます。
こんにちは、ご提案ありがとうございます。実際に記事用の動画を収録できるかはお約束できませんが、記事の読者に役立つ動画をどのような形でどのように作れるか考えてみます。
こんにちは、ご提案ありがとうございます。実際に記事用の動画を録画できるようになるとは約束できませんが、記事の読者に役立つ動画をどのような形でどのように作れるか考えてみます。
ありがとうございます。数秒の簡単なもので十分です。MT5でのストラテジーのテストや最適化は、MT4で行っていたものよりも複雑なので、移行中の人には難しいと感じることもあります。あなたができることは、記事に掲載している結果を得るために使用している正確な設定を示すことだけです。
HI Download Last Part Files (21) How I Can User This Advisor Can u Help me please