
多通貨エキスパートアドバイザーの開発(第17回):実際の取引に向けたさらなる準備
はじめに
以前の記事では、リアル口座での運用に向けたEAの改良について取り上げました。これまでは主に、ストラテジーテスター上で満足できる結果を得ることに注力してきましたが、実際の取引環境では、さらに多くの準備が必要です。
端末の再起動後にEAの動作を復元すること、取引銘柄の名称が多少異なる場合にも対応すること、指定した指標に到達した際に自動的に取引を終了することなどに加えて、次の課題にも直面しています。それは、取引戦略インスタンスおよびそのグループの最適化結果を格納したデータベースから、初期化文字列を直接取得している点です。
EAを起動するには、共有端末フォルダにデータベースファイルを配置する必要があります。しかし、データベースのサイズはすでに数ギガバイトに達しており、今後さらに拡大すると見込まれます。このため、データベースをEAに不可欠な構成要素とするのは現実的ではありません。実際に必要なのは、データベースに格納された情報のごく一部に過ぎないのです。したがって、EA内で必要な情報のみを抽出・利用できる仕組みを実装する必要があります。
パスのマッピング
テストの2段階の自動化について検討・実装してきたことを思い出しましょう。第1段階では、取引戦略の単一インスタンスのパラメータを最適化しました(第11回)。このモデル取引戦略では、1つの取引銘柄と1つの時間枠のみを使用するため、銘柄と時間枠を変更しながら、オプティマイザを順番に実行していきました。銘柄と時間枠の各組み合わせに対して、異なる最適化基準に基づく最適化を順に実施しました。最適化パスのすべての結果は、データベースの「passes」テーブルに記録されました。
第2段階では、第1段階で得られたパラメータセットのグループ化を最適化し、それらを組み合わせて使用することで最良の結果が得られるようにしました(第6回と第13回)。第1段階と同様に、同じ銘柄と時間枠のペアに属するパラメータセットを1つのグループにまとめました。最適化中に検討されたすべてのグループの結果情報も、データベースに保存されています。
第3段階では、もはや標準のストラテジーテスターオプティマイザは使用していないため、自動化についてはここでは扱いません。この段階では、第2段階で見つかった最良のグループの中から、各銘柄と時間枠の組み合わせごとに1つを選択しました。具体的には、3つの銘柄(EURGBP、EURUSD、GBPUSD)と3つの時間枠(H1、M30、M15)に対して最適化をおこないました。したがって、第3段階の結果として、9つのグループが選ばれることになります。しかし、テスター上での計算を簡素化・高速化するため、直近の記事では3つの最良グループ(異なる銘柄かつすべてH1時間枠)に絞って取り扱いました。
第3段階の成果物は、「passes」テーブルから選択された行識別子のセットであり、これらを入力パラメータとして最終版EAであるSimpleVolumesExpert.mq5に渡しました。
input string passes_ = "734469," "736121," "776928"; // - Comma-separated pass IDs
EAテストを開始する前に、このパラメータを変更することができます。これにより、データベースの「passes」テーブルにある利用可能なグループのセットから、任意のサブセット(より正確には247文字以内の長さに収まるサブセット)を指定して、最終的なEAを実行できるようになりました。これは、MQL5言語における入力文字列パラメータの値の長さ制限によるものです。ドキュメントによると、文字列パラメータの最大長は、パラメータ名の長さに応じて191~253文字となっています。
したがって、おおよそ40グループ以上を作業対象に含める場合、この方法では対応できません。たとえば、inputというキーワードを削除して、passes_変数を単なる通常の文字列変数に変更する必要が出てくるでしょう。この場合、必要なグループのセットはソースコード内でのみ指定できるようになります。とはいえ、現時点ではこのような大規模なセットを使用する必要はありません。さらに、第5回でおこなった実験からも、大量の取引戦略のインスタンスや戦略グループを1つにまとめるより、単一インスタンスを複数の小さなサブグループに分割し、そこから少数の新しいグループを組み立てる方が利益率が高いことが分かっています。これらの新しいグループは、さらに1つの最終グループにまとめることも、再びサブグループに分割して段階的に統合していくことも可能です。つまり、統合の各ステップでは、比較的少数の戦略またはグループを単位として扱うことになります。
EAが最適化パス結果を格納したデータベースにアクセスできる場合、必要な最適化パスIDのリストを入力パラメータで渡すだけで十分です。EAは、指定されたパスに参加した取引戦略グループの初期化文字列をデータベースから自動で取得し、それらをもとに、リストされたすべての取引戦略を含むEAオブジェクトの初期化文字列を構築します。これにより、EAはすべての対象戦略インスタンスを活用して取引をおこないます。
一方、データベースにアクセスできない場合には、EAが必要な取引戦略構成を含む初期化文字列を何らかの形で生成する必要があります。たとえば、初期化文字列をファイルに保存し、そのファイル名をEAに渡して読み込ませる方法が考えられます。または、追加の.mqhライブラリファイルを使って、初期化文字列をソースコードに直接埋め込むことも可能です。さらに、両者を組み合わせ、ファイルに保存してMetaEditorの[編集]>[挿入]>[ファイル]機能を使ってインポートする方法もあります。
しかし、1つのEAで複数のグループから選択できるようにし、入力で選択できる方式を採用すると、すぐにスケーラビリティの限界が見えてきます。手作業による反復作業が増えてしまうためです。そこで、問題を少し異なる形で定義し直しましょう。良質な初期化文字列のライブラリを構築し、そこから必要に応じて1つを選んで現在のEAで使用できるようにするのです。このライブラリはEAに統合され、外部ファイルを別途用意する必要がない構成にします。
以上を踏まえ、今後の作業は次の段階に分けられます。
- グループの選択と保存:選択したグループを後で利用できるように初期化文字列を保存できるツールを作成します。グループ名、簡単な説明、構成の概要、作成日などの付加情報も保存できるようにします。
- ライブラリの形成:選択されたグループから、EAの特定リリース用に使用するグループを決定し、必要な情報を含むインクルードファイルを作成します。
- 最終EAの作成:これまでのEAを修正し、作成したグループライブラリを用いて、最適化データベースに依存しない最終版EAに仕上げます。使用される取引戦略グループに関する必要な情報はすべて最適化データベースに含まれるため、このEAは最適化データベースにアクセスする必要がなくなります。
それでは、これらの実装に取り組んでいきましょう。
過去の業績の振り返り
ここで述べた手順は、第9回で説明したステージ8の実装プロトタイプにあたります。あの記事では、優れた取引パフォーマンスを持つ既製EAを完成させるために必要な一連のステージを列挙しました。ステージ8では、異なる取引戦略、銘柄、時間枠、その他のパラメータで見つかったベストなグループを、最終的に1つのEAにまとめることを目指していました。しかし、「最適なグループを具体的にどのように選択するのか」という問題については、まだ詳細に検討していません。
一方で、この問題の答えは意外と単純かもしれません。例えば、各グループのパフォーマンス(総利益、シャープレシオ、正規化平均年間利益など)に基づいて、単純に最も良い結果を選び出すという方法が考えられます。しかし一方で、もっと複雑な答えが求められる可能性もあります。たとえば、より良いテスト結果を得るには、より複雑な基準を使ってグループを選定すべきかもしれません。あるいは、最も成績が良いグループの中にも、逆に最終EAに含めない方が全体のパフォーマンスが向上するものが存在するかもしれません。このテーマについては、いずれにせよ、独立した詳細な研究が必要になりそうです。
また、グループをサブグループに分割し、その正規化をおこなう最適な方法についても、別途詳しく検討する必要があります。この問題には、第5回でもすでに触れています。テスト段階の自動化を開始する前、私たちは3つの取引銘柄それぞれに対して3つずつ、合計9つの単一インスタンスを手動で選択しました。
このとき、最初に各銘柄ごとに3つの戦略をまとめた正規化グループを作成し、それらをさらに1つの最終正規化グループに統合する、という手順を踏むと、単純に9つの戦略インスタンスをまとめる場合よりもテスト結果がわずかに向上することが分かりました。しかし、このグループ化手法が常に最適であるとは限りません。別の取引戦略群では、単純にまとめた方が良い結果になる可能性もあります。この点についても、さらなる研究の余地があります。
幸い、これら2つの課題については、現時点では後回しにすることが可能です。これらを本格的に探求するには、まだ実装されていない補助ツールが必要であり、それらなしでは作業効率が著しく低下し、時間も大幅にかかってしまうからです。
グループの選択と保存
一見すると、必要なものはすべて揃っているように思えるかもしれません。前回作成したSimpleVolumesExpert.mq5 EAを使い、passes_入力にパスIDをカンマ区切りで設定し、単一のテスターパスを起動して、必要な初期化文字列をデータベースに保存すればよいだけのように見えます。足りないのは、いくつかの追加データだけのように思えました。しかし、実際にはパスに関する情報がデータベースに登録されないことが判明しました。
ポイントは、最適化パスの結果のみがデータベースにアップロードされており、単一パスの結果はアップロードされていない点です。ご存じの通り、アップロード処理は、上位レベルのOnTesterPass()ハンドラから呼び出されるCTesterHandler::ProcessFrames()メソッド内で実行されます。
//+------------------------------------------------------------------+ //| Handling incoming frames | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrames(void) { // Open the database DB::Connect(); // Variables for reading data from frames ... // Go through frames and read data from them while(FrameNext(pass, name, id, value, data)) { // Convert the array of characters read from the frame into a string values = CharArrayToString(data); // Form a string with names and values of the pass parameters inputs = GetFrameInputs(pass); // Form an SQL query from the received data query = StringFormat("INSERT INTO passes " "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');", s_idTask, pass, values, inputs, TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS)); // Add it to the SQL query array APPEND(queries, query); } // Execute all requests DB::ExecuteTransaction(queries); // Close the database DB::Close(); }
単一パスが起動されると、単一パスイベントモデルではハンドラーが用意されていないため、ハンドラは呼び出されません。このハンドラは、データフレーム収集モードで実行されているエキスパートアドバイザー(EA)でのみ呼び出されます。最適化が開始されると、このモードでEAのインスタンスが自動的に起動されますが、単一パスが開始された場合には起動されません。そのため、現状の実装では、単一パスに関する情報はデータベースに保存されないことがわかります。
もちろん、すべてをそのままにして、不要なパラメータに従って最適化を開始するEAを開発することも可能です。この場合、最適化の目的は最初のパス結果を取得することであり、その後すぐに最適化を停止させます。こうすれば、パス結果をデータベースに保存できますが、この方法はあまりに不格好なので、別のアプローチを取ることにします。
単一パスを実行する場合、EAでは完了時にOnTester()ハンドラが呼び出されます。したがって、単一パスの結果を保存するコードを、OnTester()ハンドラ自身、またはそこから呼び出されるメソッドのいずれかに挿入する必要があります。最適な挿入場所は、おそらくCTesterHandler::Tester()メソッドでしょう。ただし、このメソッドは最適化パス完了時にも呼び出されるため、その点を考慮する必要があります。現在このメソッドには、データフレームメカニズムを通じて最適化パスの結果を生成・送信するコードが実装されています。
単一パスが開始された場合でも、フレームデータ自体は生成されますが、データフレームが作成されたとしても実際には利用できません。単一パスモードでEAを起動し、FrameAdd()関数でフレームを作成した後にFrameNext()関数でフレーム取得を試みても、FrameNext()は作成されたフレームを読み取らず、あたかもフレームが存在しないかのように動作します。
そこで、次のように対処します。CTesterHandler::Tester()メソッド内で、このパスが単一パスなのか、最適化の一部なのかを判定します。結果に応じて、単一パスの場合は即座にパス結果をデータベースに保存し、最適化パスの場合はデータフレームを作成してメインEAへ送信します。このために、単一パスの保存をおこなう新しいメソッドと、passesテーブルに必要なデータを挿入するSQLクエリを生成する補助メソッドを追加しましょう。なお、SQLクエリ生成処理はこれまで一箇所のみで使われていましたが、今後は二箇所で使われるため、共通化して別メソッドにまとめることにします。
//+------------------------------------------------------------------+ //| Optimization event handling class | //+------------------------------------------------------------------+ class CTesterHandler { ... static void ProcessFrame(string values); // Handle single pass data // Generate SQL query to insert pass results static string GetInsertQuery(string values, string inputs, ulong pass = 0); public: ... };
GetInsertQuery()はすでに実装されています。やるべきことは、ProcessFrames()メソッド内のコードブロックを移動し、適切な場所でProcessFrames()メソッドから呼び出すだけです。
//+------------------------------------------------------------------+ //| Generate SQL query to insert pass results | //+------------------------------------------------------------------+ string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) { return StringFormat("INSERT INTO passes " "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');", s_idTask, pass, values, inputs, TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS)); } //+------------------------------------------------------------------+ //| Handling incoming frames | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrames(void) { ... // Go through frames and read data from them while(FrameNext(pass, name, id, value, data)) { // Convert the array of characters read from the frame into a string values = CharArrayToString(data); // Form a string with names and values of the pass parameters inputs = GetFrameInputs(pass); // Form an SQL query from the received data query = GetInsertQuery(values, inputs, pass); // Add it to the SQL query array APPEND(queries, query); } ... }
単一パスのデータを保存するために、パスに関するデータ(passesテーブルに挿入するためのSQLクエリの一部)を含む文字列(SQLクエリの一部)をパラメータとして受け取る新しいメソッドProcessFrame()を呼び出します。メソッド内部では、データベースに接続し、最終的なSQLクエリを作成して実行するだけです。
//+------------------------------------------------------------------+ //| Handle single pass data | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrame(string values) { // Open the database DB::Connect(); // Form an SQL query from the received data string query = GetInsertQuery(values, "", 0); // Execute the request DB::Execute(query); // Close the database DB::Close(); }
追加されたメソッドを考慮して、パス完了イベントハンドラを次のように変更できます。
//+------------------------------------------------------------------+ //| Handling completion of tester pass for agent | //+------------------------------------------------------------------+ void CTesterHandler::Tester(double custom, // Custom criteria string params // Description of EA parameters in the current pass ) { ... // Generate a string with pass data data = StringFormat("%s,'%s'", data, params); // If this is a pass within the optimization, if(MQLInfoInteger(MQL_OPTIMIZATION)) { // Open a file to write a frame data int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI); // Write a description of the EA parameters FileWriteString(f, data); // Close the file FileClose(f); // Create a frame with data from the recorded file and send it to the main terminal if(!FrameAdd("", 0, 0, s_fileName)) { PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError()); } } else { // Otherwise, it is a single pass, call the method to add its results to the database CTesterHandler::ProcessFrame(data); } }
現在のフォルダ内のTesterHandler.mqhファイルに加えられた変更を保存します。
現在、各単一パス後にその結果がデータベースに入力されます。現在のタスクにおいて、パスのさまざまな統計パラメータにはあまり関心がありません。私たちにとって最も重要なのは、パスで使用される正規化された戦略グループの保存された初期化文字列です。この保存された文字列が最も必要です。
しかし、passesテーブルの列に必要な初期化文字列が存在するだけでは、それらをさらに便利に活用することはできません。初期化文字列にいくつかの情報を追加することも考えましたが、このテーブルの大多数の行は最適化パスの結果を格納しているため、追加情報が必要ないことがほとんどです。そのため、passesテーブルの列セットを拡張することは避けるべきです。
そこで、選択された結果を格納するための新しいテーブルを作成しましょう。これは、ライブラリ形成段階にすでに関連している作業です。
ライブラリの形成
他のデータベーステーブルから取得できる情報を含む冗長なフィールドで新しいテーブルを過負荷にしないようにしましょう。例えば、新しいテーブルのエントリが、外部キーを介してパステーブル(passes)のエントリと関連付けられている場合、作成日などの情報はすでに存在しています。また、パスIDを使用することで、関連する情報のチェーンを構築し、このパスがどのプロジェクトに属しているか、さらにそのパスで使用されている戦略グループを特定できます。
これを踏まえて、以下のフィールドセットを持つstrategy_groupsテーブルを作成しましょう。
- id_pass:passesテーブルからのパスID(外部キー)
- name:戦略グループ選択入力の列挙を生成するために使用される戦略グループの名前
必要なテーブルを作成するためのSQLコードは次のようになります。
-- Table: strategy_groups DROP TABLE IF EXISTS strategy_groups; CREATE TABLE strategy_groups ( id_pass INTEGER REFERENCES passes (id_pass) ON DELETE CASCADE ON UPDATE CASCADE PRIMARY KEY, name TEXT );
以降のアクションのほとんどを実行するために、CGroupsLibrary補助クラスを作成しましょう。そのタスクには、データベースから戦略グループに関する情報を挿入・取得することや、最終的なEAで使用する適切なグループのライブラリを含むmqhファイルを生成することが含まれます。これについては後ほど詳しく説明します。とりあえず、ライブラリを構成するために使用するEAを作成しましょう。
既存のSimpleVolumesExpert.mq5 Aは必要な機能をほぼ全て実行しますが、まだ改善が必要です。このEAを最終的なEAの最終バージョンとして使用する予定ですので、新しい名前であるSimpleVolumesStage3.mq5として保存しましょう。その後、新しいファイルに必要な変更を加えます。足りない点は2つです。1つ目は、現在選択されているパスに基づいて形成されたグループの名前を指定する機能(passes_パラメータ内)、2つ目は、このグループの初期化文字列を新しいstrategy_groupsテーブルに保存する機能です。
前者は非常に簡単に実装できます。後で使用するためのグループ名として使用する新しいEA入力を追加しましょう。もしこのパラメータが空であれば、ライブラリへの保存はおこなわれません。
input group "::: Saving to library" input string groupName_ = ""; // - Group name (if empty - no saving)
しかし、前者の場合はもう少し手間がかかります。strategy_groupsテーブルにデータを挿入するには、現在のパスレコードがpassesテーブルに挿入されたときに割り当てられたIDを知る必要があります。このIDの値はデータベース自体によって自動的に割り当てられるため(クエリではその値の代わりにNULLを渡すだけです)、コード内でどの変数にも格納されません。したがって、現状では他の必要な場所でこのIDを使用することができません。この値を何らかの方法で取得する必要があります。
この作業にはいくつかの方法があります。例えば、新しい行に割り当てられる識別子が増加するシーケンスであることがわかっているので、挿入後に現在の最大IDを選択する方法があります。これが可能なのは、passesテーブルに新しい行が現在挿入されていないことが確実にわかっている場合のみです。しかし、もし別の第一段階または第二段階の最適化が現在並行して実行されている場合、その結果が同じデータベースに挿入される可能性があります。この場合、最後に挿入されたIDがライブラリを形成するために起動したパスに対応するものであるとは限りません。一般的に、この方法は、いくつかの制限を受け入れ、それを覚えておく準備ができている場合にのみ実行できます。
より信頼性の高い方法は次の通りです。挿入するためのSQLクエリを少し変更して、新しいテーブル行の生成されたIDを返すクエリに変えることができます。これを実現するために、SQLクエリの最後に「RETURNING rowid」演算子を追加します。この変更をGetInsertQuery()メソッドに適用してみましょう。このメソッドは、passesテーブルに新しい行を挿入するためのSQLクエリを生成します。passesテーブルのID列はid_passという名前ですが、適切な型(INTEGER PRIMARY KEY AUTOINCREMENT)を持ち、SQLiteテーブルに自動的に存在する非表示のrowid列を置き換えるので、rowidという名前を使うことができます。
//+------------------------------------------------------------------+ //| Generate SQL query to insert pass results | //+------------------------------------------------------------------+ string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) { return StringFormat("INSERT INTO passes " "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s') RETURNING rowid;", s_idTask, pass, values, inputs, TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS)); }
このリクエストを送信するMQL5コードも変更する必要があります。現在、そのためにDB::Execute(query)メソッドを使用していますが、これは渡されたクエリがデータを返さないことを前提としています。
そのため、CDatabaseクラスに新しいメソッドInsert()が追加されます。このメソッドは、渡された挿入クエリを実行し、単一の読み取り結果を返します。内部では、DatabaseExecute()関数の代わりにDatabasePrepare()関数を使用して、クエリ結果にアクセスできるようになります。
//+------------------------------------------------------------------+ //| Class for handling the database | //+------------------------------------------------------------------+ class CDatabase { ... public: ... // Execute a query to the database for insertion with return of the new entry ID static ulong Insert(string query); }; ... //+------------------------------------------------------------------+ //| Execute a query to the database for insertion returning the | //| new entry ID | //+------------------------------------------------------------------+ ulong CDatabase::Insert(string query) { ulong res = 0; // Execute the request int request = DatabasePrepare(s_db, query); // If there is no error if(request != INVALID_HANDLE) { // Data structure for reading a single string of a query result struct Row { int rowid; } row; // Read data from the first result string if(DatabaseReadBind(request, row)) { res = row.rowid; } else { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Reading row for request \n%s\nfailed with code %d", query, GetLastError()); } } else { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Request \n%s\nfailed with code %d", query, GetLastError()); } return res; } //+------------------------------------------------------------------+
送信されたクエリが実際にINSERTクエリであること、IDを返すコマンドが含まれていること、返された値が複合値ではないことなど、追加のチェックを行ってこのメソッドを複雑にしないことにしました。これらの条件に違反すると、コード実行時にエラーが発生しますが、このメソッドはプロジェクト内で1か所のみ使用されるため、正しいリクエストを渡せるようにすることにしました。
現在のフォルダのDatabase.mqhファイルに変更を保存します。
実装中に次に発生した問題は、受信時点でID値を処理することで既存のメソッドに外部機能と追加のパラメータを渡す必要が生じたため、ID値を上位レベルのコードにどのように渡すかということでした。そこで、CTesterHandlerクラスにs_idPassというstaticプロパティを追加しました。現在のパスのIDがここに書き込まれ、これによりプログラムの任意の時点でその値を取得できるようになりました。
//+------------------------------------------------------------------+ //| Optimization event handling class | //+------------------------------------------------------------------+ class CTesterHandler { ... public: ... static ulong s_idPass; }; ... ulong CTesterHandler::s_idPass = 0; ... //+------------------------------------------------------------------+ //| Handle single pass data | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrame(string values) { // Open the database DB::Connect(); // Form an SQL query from the received data string query = GetInsertQuery(values, "", 0); // Execute the request s_idPass = DB::Insert(query); // Close the database DB::Close(); }
現在のフォルダ内のTesterHandler.mqhファイルに加えられた変更を保存します。
ここで、宣言されたCGroupsLibrary補助クラスに戻ります。最終的には、2つのpublicメソッド(1つはprivateメソッド、もう1つはstatic配列)を宣言する必要がありました。
//+------------------------------------------------------------------+ //| Class for working with a library of selected strategy groups | //+------------------------------------------------------------------+ class CGroupsLibrary { private: // Exporting group names and initialization strings extracted from the database as MQL5 code static void ExportParams(string &p_names[], string &p_params[]); public: // Add the pass name and ID to the database static void Add(ulong p_idPass, string p_name); // Export passes to mqh file static void Export(string p_idPasses); // Array to fill with initialization strings from mqh file static string s_params[]; };
ライブラリ形成EAでは、Add()メソッドのみが使用されます。ライブラリに保存するには、保存するパスIDとグループ名を受け取ります。メソッドコード自体は非常にシンプルです。そのメソッドのコード自体は非常に簡単です。入力データからstrategy_groupsテーブルに新しいエントリを挿入するためのSQLクエリを作成し、それを実行します。
//+------------------------------------------------------------------+ //| Add the pass name and ID to the database | //+------------------------------------------------------------------+ void CGroupsLibrary::Add(ulong p_idPass, string p_name) { string query = StringFormat("INSERT INTO strategy_groups VALUES(%d, '%s')", p_idPass, p_name); // Open the database if(DB::Connect()) { // Execute the request DB::Execute(query); // Close the database DB::Close(); } }
ライブラリ形成ツールの開発を完了するには、テスターパスが完了したら、SimpleVolumesStage3.mq5 EAにAdd()メソッドの呼び出しを追加するだけです。
//+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { // Handle the completion of the pass in the EA object double res = expert.Tester(); // If the group name is not empty, save the pass to the library if(groupName_ != "") { CGroupsLibrary::Add(CTesterHandler::s_idPass, groupName_); } return res; }
現在のフォルダ内のSimpleVolumesStage3.mq5ファイルとGroupsLibrary.mqhファイルに加えた変更を保存しましょう。CGroupsLibraryクラスの残りのメソッドのスタブを追加すると、コンパイル済みのSimpleVolumesStage3.mq5 EAを使用できるようになります。
ライブラリへの入力
以前選択した9つの良いパスIDからライブラリを形成してみましょう。そのために、SimpleVolumesStage3.ex5 EAをテスターで起動し、passes_入力から9つのIDの中から選んださまざまな組み合わせを指定します。groupName_入力には、現在の単一の取引戦略インスタンスを1つのグループにまとめたグループの構成を反映する明確な名前を設定します。
いくつかの実行後、異なるグループでおこなったパスに関するいくつかのパラメータを追加して、strategy_groupsテーブルに表示される結果を確認しましょう。たとえば、次のSQLクエリが役立ちます。
SELECT sg.id_pass, sg.name, p.custom_ontester, p.sharpe_ratio, p.profit, p.profit_factor, p.equity_dd_relative FROM strategy_groups sg JOIN passes p ON sg.id_pass = p.id_pass;
クエリの結果、次のテーブルが生成されました。
図1:グループライブラリの構成
name列には、取引商品(銘柄)、時間枠、およびこのグループで使用される取引戦略インスタンスの数を反映したグループ名が表示されます。たとえば、「EUR-GBP-USD」が表示されている場合、このグループには次の3つの銘柄、EURGBP、EURUSD、GBPUSDに対する取引戦略インスタンスが含まれていることを意味します。グループ名が「Only EURGBP」で始まる場合、EURGBP銘柄に対する戦略インスタンスのみが含まれます。使用される時間枠も同様に表示されます。取引戦略インスタンスの数は、名前の末尾に記載されています。たとえば、「3x16 items」は、このグループが3つの標準化されたグループ(それぞれ16個の戦略)を組み合わせたものであることを示しています。
custom_ontester列には、各グループの正規化された平均年間利益が表示されます。このパラメータの値の範囲は予想を上回っていたため、今後はこの現象の理由を理解する必要があります。たとえば、GBPUSDのみを使用したグループの結果は、複数銘柄を使用したグループよりも大幅に高くなっていました。最良の結果は、20行目に保存されています。このグループには、各銘柄と1つ以上の時間枠で最良の結果をもたらすサブグループが含まれています。
ライブラリのエクスポート
次のステップは、グループライブラリをデータベースから最終的なEAに接続可能なmqhファイルにエクスポートすることです。これを実行するには、エクスポートを担当するCGroupsLibraryクラスのメソッドと、これらのメソッドを実行するために使用されるもう1つの補助EAを実装します。
Export()メソッドでは、データベースからライブラリグループの名前と初期化文字列を取得し、対応する配列に追加します。生成された配列は次のメソッドであるExportParams()に渡されます。
//+------------------------------------------------------------------+ //| Exporting passes to mqh file | //+------------------------------------------------------------------+ void CGroupsLibrary::Export(string p_idPasses) { // Array of group names string names[]; // Array of group initialization strings string params[]; // If the connection to the main database is established, if(DB::Connect()) { // Form a request to receive passes with the specified IDs string query = "SELECT sg.id_pass," " sg.name," " p.params" " FROM strategy_groups sg" " JOIN" " passes p ON sg.id_pass = p.id_pass"; query = StringFormat("%s " "WHERE p.id_pass IN (%s);", query, p_idPasses); // Prepare and execute the request int request = DatabasePrepare(DB::Id(), query); // If the request is successful if(request != INVALID_HANDLE) { // Structure for reading results struct Row { ulong idPass; string name; string params; } row; // For all query results, add the name and initialization string to the arrays while(DatabaseReadBind(request, row)) { APPEND(names, row.name); APPEND(params, row.params); } } DB::Close(); // Export to mqh file ExportParams(names, params); } }
ExportParams()メソッドでは、MQL5コードで文字列を形成し、指定された名前ENUM_GROUPS_LIBRARYの列挙を作成して、要素を入力します。各要素にはグループ名を含むコメントが付きます。次に、コードはstatic文字列配列 CGroupsLibrary::s_params[]を宣言します。この配列には、ライブラリからのグループの初期化文字列が入力されます。各初期化文字列は前処理されます。つまり、すべての改行はスペースに置き換えられ、二重引用符の前にバックスラッシュが追加されます。これは、生成されたコード内で初期化文字列を二重引用符で囲むために必要です。
data変数でコードが完全に形成されたら、ExportedGroupsLibrary.mqhという名前のファイルを作成し、生成されたコードをそこに保存します。
//+------------------------------------------------------------------+ //| Export group names extracted from the database and | //| initialization strings in the form of MQL5 code | //+------------------------------------------------------------------+ void CGroupsLibrary::ExportParams(string &p_names[], string &p_params[]) { // ENUM_GROUPS_LIBRARY enumeration header string data = "enum ENUM_GROUPS_LIBRARY {\n"; // Fill the enumeration with group names FOREACH(p_names, { data += StringFormat(" GL_PARAMS_%d, // %s\n", i, p_names[i]); }); // Close the enumeration data += "};\n\n"; // Group initialization string array header and its opening bracket data += "string CGroupsLibrary::s_params[] = {"; // Fill the array by replacing invalid characters in the initialization strings string param; FOREACH(p_names, { param = p_params[i]; StringReplace(param, "\r", ""); StringReplace(param, "\n", " "); StringReplace(param, "\"", "\\\""); data += StringFormat("\"%s\",\n", param); }); // Close the array data += "};\n"; // Open the file to write data int f = FileOpen("ExportedGroupsLibrary.mqh", FILE_WRITE | FILE_TXT | FILE_ANSI); // Write the generated code FileWriteString(f, data); // Close the file FileClose(f); }
次に来る部分は非常に重要です。
// Connecting the exported mqh file. // It will initialize the CGroupsLibrary::s_params[] static variable // and ENUM_GROUPS_LIBRARY enumeration #include "ExportedGroupsLibrary.mqh"
エクスポート後に受信されるファイルをGroupsLibrary.mqhファイルに直接インクルードします。この場合、最終的なEAでは、エクスポートされたライブラリを使用できるようにするために、このファイルのみをインクルードする必要があります。このアプローチには、ちょっとした不便さがあります。ライブラリのエクスポートを処理するEAをコンパイルできるようにするには、エクスポート後にのみ表示されるExportedGroupsLibrary.mqhファイルがすでに存在している必要があります。ただし、このファイルの存在のみが重要であり、その内容は重要ではありません。したがって、現在のフォルダにこの名前の空のファイルを作成するだけで、コンパイルはエラーなしで続行されます。
EAメソッドを実行するには、これを実行するスクリプトまたはEAが必要です。次のようになります。
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "::: Exporting from library" input string passes_ = "802150,802151,802152,802153,802154," "802155,802156,802157,802158,802159," "802160,802161,802162,802164,802165," "802166,802167,802168,802169,802173"; // - Comma-separated IDs of the saved passes //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Call the group library export method CGroupsLibrary::Export(passes_); // Successful initialization return(INIT_SUCCEEDED); } void OnTick() { ExpertRemove(); }
passes_パラメータを変更することで、ライブラリからデータベースにグループをエクスポートする際の構成と順序を選択できます。チャート上でEAを1回実行すると、端末データフォルダにExportedGroupsLibrary.mqhファイルが表示されます。 プロジェクトコードが含まれている現在のフォルダに転送する必要があります。
最終的なEAの作成
ついに最終段階に到達しました。残っているのは、SimpleVolumesExpert.mq5 EAにいくつかの小さな変更を加えることだけです。まず、GroupsLibrary.mqhファイルをインクルードする必要があります。
#include "GroupsLibrary.mqh"
次に、passes_入力を新しいものに置き換えて、ライブラリからグループを選択できるようにします。
input group "::: Selection for the group" input ENUM_GROUPS_LIBRARY groupId_ = -1; // - Group from the library
OnInit()関数では、以前のようにIDを渡してデータベースから初期化文字列を取得する代わりに、groupId_入力の選択された値に対応するインデックスを持つCGroupsLibrary::s_params[]配列から初期化文字列を取得するだけです。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Initialization string with strategy parameter sets string strategiesParams = NULL; // If the selected strategy group index from the library is valid, then if(groupId_ >= 0 && groupId_ < ArraySize(CGroupsLibrary::s_params)) { // Take the initialization string from the library for the selected group strategiesParams = CGroupsLibrary::s_params[groupId_]; } // If the strategy group from the library is not specified, then we interrupt the operation if(strategiesParams == NULL) { return INIT_FAILED; } ... // Successful initialization return(INIT_SUCCEEDED); }
現在のフォルダ内のSimpleVolumesExpert.mq5ファイルに加えた変更を保存します。
ENUM_GROUPS_LIBRARY列挙要素に名前付きのコメントを追加したので、EAパラメータを選択するためのダイアログで、単なる数字のシーケンスではなく、わかりやすい名前が表示されるようになります。
図2:EAパラメータにおいて、ライブラリからグループを名前で選択する
リストの最後のグループでEAを実行し、結果を見てみましょう。
図3:ライブラリから最も魅力的なグループを使用して最終的なEAをテストした結果
平均年間正規化利益指標の結果が、データベースに保存されている結果に近いことは明らかです。小さな違いは、主に最終的なEAが標準化されたグループを使用したという事実によるものです(これは、使用されたデポジットの約10%である最大相対ドローダウンの値を見ることで確認できます)。SimpleVolumesStage3.ex5 EAでこのグループの初期化文字列を生成する際、パス中にグループはまだ標準化されていなかったため、そこでのドローダウンは約5.4%でした。
結論
最適化プロセスで入力されたデータベースから独立して動作できる最終的なEAを受け取りました。おそらく、この問題に再び取り組むことになるでしょう。なぜなら、実践を重ねることで独自の調整が加えられ、この記事で提案した方法が他の方法よりも不便であることが判明するかもしれないからです。しかし、いずれにせよ、設定された目標を達成したことは前進です。
この記事のコードに取り組んでいる間に、新たに調査が必要な状況が発見されました。例えば、このEAのテスト結果は、クォートサーバーだけでなく、ストラテジーテスター設定でメインとして選択された銘柄にも影響されることが判明しました。第一段階と第二段階の最適化自動化には、いくつかの調整が必要になるかもしれません。次回その点について詳しく触れます。
最後に、以前から暗黙的に存在していた警告を述べておきます。前の部分では、提案された方向に従えば保証された利益が得られるとは一度も言っていません。それどころか、いくつかの点では残念なテスト結果が出ました。また、EAを実際の取引用に準備するために費やした努力にもかかわらず、実際の口座でEAが正しく動作することを保証するために、可能なことと不可能なことすべてを行ったとは言えなくなる可能性があります。これは目指すべき完璧な結果ですが、それを達成するのは常に不確かな未来の問題のように感じます。しかし、これは私たちがそれに近づくことを妨げるものではありません。
この記事および連載のこれまでのすべての記事で提示された結果は、過去のテストデータのみに基づいており、将来の利益を保証するものではありません。このプロジェクトでの作業は研究的な性質のものであり、公開された結果はすべて、自己責任で使用されるべきです。
ご精読ありがとうございました。またすぐにお会いしましょう。
アーカイブ内容
# | 名前 | バージョン | 詳細 | 最近の変更 |
---|---|---|---|---|
MQL5/Experts/Article.15360 | ||||
1 | Advisor.mqh | 1.04 | EA基本クラス | 第10回 |
2 | Database.mqh | 1.04 | データベースを扱うクラス | 第17回 |
3 | ExpertHistory.mqh | 1.00 | 取引履歴をファイルにエクスポートするクラス | 第16回 |
4 | エクスポートされたグループライブラリ.mqh | — | 戦略グループ名とその初期化文字列の配列をリストした生成されたファイル | 第17回 |
5 | Factorable.mqh | 1.01 | 文字列から作成されたオブジェクトの基本クラス | 第10回 |
6 | グループライブラリ.mqh | 1.00 | 選択された戦略グループのライブラリを操作するためのクラス | 第17回 |
7 | HistoryReceiverExpert.mq5 | 1.00 | リスクマネージャーとの取引履歴を再生するためのEA | 第16回 |
8 | HistoryStrategy.mqh | 1.00 | 取引履歴を再生するための取引戦略のクラス | 第16回 |
9 | Interface.mqh | 1.00 | さまざまなオブジェクトを視覚化するための基本クラス | 第4回 |
10 | ライブラリエクスポート.mq5 | 1.00 | 選択したパスの初期化文字列をライブラリからExportedGroupsLibrary.mqhファイルに保存するEA | 第17回 |
11 | Macros.mqh | 1.02 | 配列操作に便利なマクロ | 第16回 |
12 | Money.mqh | 1.01 | 基本的なお金の管理クラス | 第12回 |
13 | NewBarEvent.mqh | 1.00 | 特定の銘柄の新しいバーを定義するクラス | 第8回 |
14 | Receiver.mqh | 1.04 | オープンボリュームを市場ポジションに変換するための基本クラス | 第12回 |
15 | SimpleHistoryReceiverExpert.mq5 | 1.00 | 取引履歴を再生するための簡易EA | 第16回 |
16 | SimpleVolumesExpert.mq5 | 1.20 | 複数のモデル戦略グループを並列操作するためのEA。パラメータは組み込みのグループライブラリから取得されます。 | 第17回 |
17 | シンプルボリュームステージ3.mq5 | 1.00 | 生成された標準化された戦略グループを、指定された名前のグループのライブラリに保存するEA。 | 第17回 |
18 | SimpleVolumesStrategy.mqh | 1.09 | ティックボリュームを使用した取引戦略のクラス | 第15回 |
19 | Strategy.mqh | 1.04 | 取引戦略基本クラス | 第10回 |
20 | TesterHandler.mqh | 1.03 | 最適化イベント処理クラス | 第17回 |
21 | VirtualAdvisor.mqh | 1.06 | 仮想ポジション(注文)を扱うEAのクラス | 第15回 |
22 | VirtualChartOrder.mqh | 1.00 | グラフィカル仮想位置クラス | 第4回 |
23 | VirtualFactory.mqh | 1.04 | オブジェクトファクトリクラス | 第16回 |
24 | VirtualHistoryAdvisor.mqh | 1.00 | トレード履歴再生EAクラス | 第16回 |
25 | VirtualInterface.mqh | 1.00 | EAGUIクラス | 第4回 |
26 | VirtualOrder.mqh | 1.04 | 仮想注文とポジションのクラス | 第8回 |
27 | VirtualReceiver.mqh | 1.03 | オープンボリュームを市場ポジションに変換するクラス(レシーバー) | 第12回 |
28 | VirtualRiskManager.mqh | 1.02 | リスクマネジメントクラス(リスクマネージャー) | 第15回 |
29 | VirtualStrategy.mqh | 1.05 | 仮想ポジションを使った取引戦略のクラス | 第15回 |
30 | VirtualStrategyGroup.mqh | 1.00 | 取引戦略グループのクラス | 第11回 |
31 | VirtualSymbolReceiver.mqh | 1.00 | 銘柄レシーバークラス | 第3回 |
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/15360





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