English Русский 中文 Español Português
preview
多通貨エキスパートアドバイザーの開発(第22回):設定のホットスワップへの移行を開始する

多通貨エキスパートアドバイザーの開発(第22回):設定のホットスワップへの移行を開始する

MetaTrader 5テスター |
18 0
Yuriy Bykov
Yuriy Bykov

はじめに

本連載の前回までの2回において、取引EAの自動最適化に関するさらなる実験に向けた重要な準備をおこなってきました。主な焦点は、現在3つのステージから構成される最適化パイプラインの構築でした。

  1. 特定の銘柄と時間足の組み合わせに対して単一戦略インスタンスを最適化する
  2. 第1ステージで得られた最良の単体インスタンスからグループを形成する
  3. 形成されたグループを組み合わせた最終EAの初期化文字列を生成し、ライブラリに保存する

パイプライン自体の作成を自動化できるようにするため、専用のEAスクリプトが開発されました。このスクリプトにより、指定されたパラメータやテンプレートに基づいて、最適化プロジェクトをデータベースに登録し、それに対するステージ、ジョブ、タスクを作成することが可能になります。このアプローチにより、ステージからステージへと移行しながら、所定の順序で最適化タスクを実行することができます。

また、プロファイリングやコード最適化によってパフォーマンスを向上させる方法についても検討しました。主な焦点は、取引商品(銘柄)に関する情報の取得を管理するオブジェクトの扱いでした。これにより、価格データや銘柄仕様データを取得するために必要なメソッド呼び出しの回数を大幅に削減することができました。

これらの作業の結果、さらなる実験や分析に使用可能な結果を自動的に生成できるようになりました。これにより、再最適化の頻度や実行順序が取引パフォーマンスにどのような影響を与えるかといった仮説を検証する道が開かれました。

本記事では、最終EAのパラメータを読み込むための新しいメカニズムの実装について詳しく解説します。このメカニズムにより、ストラテジーテスターでの単一実行中、または取引口座上で最終EAが稼働している最中において、取引戦略の単一インスタンスの構成やパラメータを部分的または完全に置き換えることが可能になります。


道筋の整理

では、私たちが達成したいことをもう少し詳しく説明してみましょう。理想的には、システムは次のような動作をすることを想定しています。

  1. 最適化期間の終了日として現在の日付を設定したプロジェクトが生成されます。 
  2. 生成されたプロジェクトはパイプラインで実行されます。その実行には数日から数週間かかることがあります。
  3. プロジェクトの結果が最終EAに読み込まれます。最終EAがまだ取引を開始していない場合は、実口座上で起動します。すでに口座上で稼働していた場合は、パイプラインを通過した直近のプロジェクトで得られた新しいパラメータに置き換えられます。
  4. ポイント1に移ります。

それぞれのポイントについて考えてみましょう。まず最初のポイントの実装には、前回の記事で作成したプロジェクト生成スクリプトEAを使用できます。このEAでは、パラメータを使って最適化の終了日を選択することが可能です。ただし、現状では手動でしか起動できません。これは、プロジェクト実行パイプライン(「コンベア」)に新たなステージを追加し、現在のプロジェクトのすべてのステージが完了した後に新しいプロジェクトを生成するようにすれば解決できます。この場合、最初の一度だけ手動で実行すれば済みます。

次に2つ目のポイントですが、必要なのはパラメータで指定されたデータベースを持つOptimization.ex5 EAがインストールされたターミナルだけです。このEAには、新たに未処理のプロジェクトタスクが発生すると、それらがキューの順序に従って実行されます。新しいプロジェクトを作成するステージの直前の最終ステージでは、何らかの形でプロジェクト最適化の結果を最終EAに渡す必要があります。

3つ目のポイントが最も難しい部分です。現在、最終EAへのパラメータ受け渡しの単一の方法は実装済みですが、依然として手動操作が必要です。具体的には、別のEAを実行してパラメータライブラリをファイルに書き出し、そのファイルをプロジェクトフォルダにコピーして、最終EAを再コンパイルする必要があります。現在ではこれらの操作をプログラムコードに委譲することは可能ですが、構造自体が不必要に煩雑に見えてきます。もっと簡単で信頼性の高い方法にしたいところです。

さらに、現在の最終EAへのパラメータ受け渡し方法の欠点は、パラメータの部分的な置き換えができないことです。完全置き換えのみ可能であり、もし開かれているポジションがあればすべてクローズされ、取引は最初から再開されます。この欠点は、既存の方法の枠内では根本的に解決できません。

ここでいう「パラメータ」とは、最終EA内で並列に動作する単一取引戦略の多数のインスタンスのパラメータを指します。古いパラメータを新しいパラメータに瞬時に置き換えると、たとえほとんど同一であっても、現在の実装では以前に開かれた仮想ポジションの情報を正しく読み込むことはおそらくできません。これは、最終EAの初期化文字列における単一インスタンスのパラメータの数と順序が完全に一致している場合にのみ可能です。

部分的なパラメータ置き換えを可能にするには、古いパラメータと新しいパラメータの両方を同時に管理する仕組みが必要です。この場合、スムーズな移行アルゴリズムを開発し、一部の単体インスタンスはそのまま残すことができます。その仮想ポジションは引き続き稼働します。新しいパラメータに含まれないインスタンスのポジションは正しくクローズされ、新しく追加されたインスタンスは最初から取引を開始します。

ここまで来ると、望んでいたよりも大幅な変更が必要になることが見えてきます。しかし、望む結果を達成する他の方法が見えない以上、早めに変更の必要性を受け入れる方がよいでしょう。間違った方向に進み続けると、後から正しい方向に移行するのがますます難しくなります。

ここで、EAの稼働情報をすべてデータベースに格納するという、これまで避けてきた方法を検討する必要があります。しかも、最適化で使用されるデータベースは非常に大きく(プロジェクトごとに数ギガバイト)、最終EAが実際に必要とする情報はごくわずかなので、最終EAから直接利用できる必要はありません。別のデータベースとして管理するのが妥当です。

さらに、自動最適化ステージの順序を並び替えられるようにしたいと考えています。前回の第20回では、銘柄と時間足でグループ化する方法について触れました。しかし、その時点では、部分的なパラメータ置き換えが不可能だったため、この順序を選択する必要はありませんでした。今後、すべてがうまく動作すれば、この順序の方が望ましい結果になる可能性があります。まずは、最終EAのために別のデータベースを使用し、単一インスタンスの取引戦略のパラメータをホットスワップできるように移行することから始めましょう。


初期化文字列の変換

今回の課題は非常に大規模なため、小さなステップで進めていきます。まず、EAデータベースに単一の取引戦略インスタンスに関する情報を格納する必要があることから始めましょう。この情報は現在、EAの初期化文字列に含まれています。EAは、この情報を最適化データベースから取得することもできますし、コンパイル時にパラメータライブラリから取得された、EAコードに組み込まれたデータ(文字列定数)から取得することもできます。最初の方法は最適化EA(SimpleVolumesStage2.mq5およびSimpleVolumesStage3.mq5)で使用され、2番目の方法は最終EA(SimpleVolumesExpert.mq5)で使用されています。

私たちはこれに第3の方法を追加したいと考えています。初期化文字列を、異なる単一取引戦略インスタンスに関連する部分に分割し、それらの部分をEAデータベースに格納します。EAは自身のデータベースからこれらを読み取り、分割された部分から完全な初期化文字列を形成できるようになります。この文字列は、すべての後続作業を実行するEAオブジェクトを作成するために使用されます。

初期化文字列をどのように分割できるかを理解するために、前回の記事から典型的な例を見てみましょう。文字列は非常に大きい(約200行程度)ため、ここでは構造を把握するために必要最小限の部分のみを示します。

class CVirtualStrategyGroup([
    class CVirtualStrategyGroup([
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,1.00,1.30,80,3200.00,930.00,12000,3)
        ],8.428150),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,172,1.40,1.20,140,2200.00,1220.00,19000,3)
        ],12.357884),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,1.20,0.10,0,1800.00,780.00,8000,3)
        ],4.756016),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,172,0.30,0.10,150,4400.00,1000.00,1000,3)
        ],4.459508),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.50,1.10,200,2800.00,1030.00,32000,3)
        ],5.021593),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,172,1.40,1.70,100,200.00,1640.00,32000,3)
        ],18.155410),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.40,160,8400.00,1080.00,44000,3)
        ],4.313320),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,52,0.50,1.00,110,3600.00,1030.00,53000,3)
        ],4.490144),
    ],4.615527),
    class CVirtualStrategyGroup([
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.80,240,4800.00,1620.00,57000,3)
        ],6.805962),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,52,0.50,1.80,40,400.00,930.00,53000,3)
        ],11.825922),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,212,1.30,1.50,160,600.00,1000.00,28000,3)
        ],16.866251),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.30,1.50,30,3000.00,1280.00,28000,3)
        ],5.824790),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,1.30,0.10,10,2000.00,780.00,1000,3)
        ],3.476085),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.10,0,16000.00,700.00,11000,3)
        ],4.522636),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,52,0.40,1.80,80,2200.00,360.00,25000,3)
        ],8.206812),
        class CVirtualStrategyGroup([
            class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.10,0,19200.00,700.00,44000,3)
        ],2.698618),
    ],5.362505),
    class CVirtualStrategyGroup([
        ...
    ],5.149065),
    
    ...
    
    class CVirtualStrategyGroup([
        ...
    ],2.718278),
],2.072066)

この初期化文字列は、第1レベル、第2レベル、第3レベルの取引戦略の入れ子になったグループで構成されています。単一の取引戦略インスタンスは、第3レベルのグループ内にのみ入れ子になっています。各インスタンスには、指定されたパラメータがあります。各グループにはスケーリング係数があり、これは第1レベル、第2レベル、第3レベルすべてに存在します。スケーリング係数の使用については、第5回で説明しました。スケーリング係数は、テスト期間中に達成された最大ドローダウンを10%の値に正規化するために必要です。さらに、複数の入れ子グループや複数の入れ子戦略インスタンスを含むグループの場合、そのグループのスケーリング係数の値は、まずグループ内の要素数で割られ、その新しい係数がすべての入れ子要素に適用されます。VirtualStrategyGroup.mqhファイルのコードは次のようになります。

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualStrategyGroup::CVirtualStrategyGroup(string p_params) {
// Save the initialization string
   m_params = p_params;

   ...

// Read the scaling factor
   m_scale = ReadDouble(p_params);

// Correct it if necessary
   if(m_scale <= 0.0) {
      m_scale = 1.0;
   }

   if(ArraySize(m_groups) > 0 && ArraySize(m_strategies) == 0) {
      // If we filled the array of groups, and the array of strategies is empty, then
      PrintFormat(__FUNCTION__" | Scale = %.2f, total groups = %d", m_scale, ArraySize(m_groups));
      // Scale all groups
      Scale(m_scale / ArraySize(m_groups));
   } else if(ArraySize(m_strategies) > 0 && ArraySize(m_groups) == 0) {
      // If we filled the array of strategies, and the array of groups is empty, then
      PrintFormat(__FUNCTION__" | Scale = %.2f, total strategies = %d", m_scale, ArraySize(m_strategies));
      // Scale all strategies
      Scale(m_scale / ArraySize(m_strategies));
   } else {
      // Otherwise, report an error in the initialization string
      SetInvalid(__FUNCTION__, StringFormat("Groups or strategies not found in Params:\n%s", p_params));
   }
}

このように、初期化文字列は階層構造を持っており、上位レベルには戦略のグループが配置され、戦略自体は最下位に位置しています。戦略グループが複数の戦略を含むことも可能ですが、プロジェクトの開発を進める中で、1つのグループに複数の戦略を入れるよりも、各戦略のインスタンスを下位レベルの個別グループにラップする方が便利であるという結論に至りました。これが第3レベルの由来です。第1および第2レベルは、最初のステージの最適化結果をグループ化したもの、さらにパイプライン上で第2ステージの最適化結果をグループ化したものに対応しています。

もちろん、データベース内にテーブル構造を作成して、戦略とグループの間の既存の階層を保持することは可能です。しかし、果たして本当にそれが必要でしょうか。実際のところ、最適化パイプラインでは階層構造が必要ですが、取引口座上で最終EAが動作する際に重要なのは、正しく計算されたスケーリング係数を持つ単一取引戦略インスタンスのリストだけです。このようなリストであれば、データベースに格納するための単純なテーブル1つで十分です。したがって、初期化文字列からこのようなリストを生成するメソッドと、対応する乗数を持つ単一取引戦略インスタンスのリストを使って最終EA用の初期化文字列を形成する逆のメソッドを追加することにします。 


戦略リストのエクスポート

まず、EA戦略のリストを取得するためのメソッドから始めましょう。このメソッドはEAクラスのメソッドである必要があります。なぜなら、保存用に変換したい情報はすべてEA内にあるからです。では、単一の取引戦略インスタンスごとに何を保存したいのでしょうか。まず第一に、初期化パラメータとスケーリング係数です。

前の段落が書かれた時点では、この作業をおこなうコードのかけらさえ存在していませんでした。実装方法を自由に選べる不確定性のため、特定の方法に決めることができなかったのです。将来的な利用を考慮してどのように作るのがよいか、多くの疑問が生じました。しかし、将来何が必要で何が不要になるかの明確なイメージがなかったため、最も単純な選択さえできませんでした。たとえば、EAが使用するデータベースのファイル名にバージョン番号を含めるべきでしょうか。また、マジックナンバーはどうするべきでしょうか。この名前は最終EAのパラメータで指定するべきでしょうか、それとも戦略名とマジックナンバーから指定のアルゴリズムで生成するべきでしょうか。それとも別の方法でしょうか。

一般に、このような場合、無限に続く疑問の悪循環を打破する方法は1つしかありません。完璧でなくても、まずは何らかの選択をおこなうことです。その選択をもとに次の判断をおこない、さらに次へと進めます。さもなければ、前に進むことはできません。コードを書いた今では、開発過程で踏んだステップを落ち着いて振り返ることができます。最終コードに反映されなかった解決策もあれば、調整の対象となったものもありますが、すべてが現在の状態に到達する助けとなっています。これをさらに説明していきましょう。

では、戦略リストのエクスポートに取り組みます。まず、このメソッドがどこから呼ばれるかを決めましょう。ここでは、これまでに最終EA用の戦略グループをエクスポートしていた第3ステージのEAとします。しかし前述の通り、最終EAでこの情報を使用するには、追加の操作が必要でした。第3ステージの出力では、最適化データベースのstrategy_groupsテーブルに割り当てられた名前を持つパスのIDのみを受け取りました。これは、第21回で作業をおこなった際の最適化後の内容です。

これら4つのテストパスそれぞれには、単一取引戦略インスタンスのグループの初期化文字列が保存されています。これらは、開始日(2018.01.01)が同じで、グループ名に指定された終了日がわずかに異なるテスト期間で最適化によって選択されたものです。

SimpleVolumesStage3.mq5ファイルでは、このエクスポートを実行していた関数の呼び出しを、まだ存在していない別の関数の呼び出しに置き換えます。

//+------------------------------------------------------------------+
//| 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_, fileName_);
      expert.Export(groupName_, advFileName_);
   }
   
   return res;
}

CVirtualAdvisorEAクラスに新しいメソッドExport()を追加します。このメソッドに渡されるパラメータは、新しいグループの名前と、エクスポート先となるEAデータベースファイルの名前です。なお、これは新しいデータベースであり、以前使用していた最適化データベースではないことに注意してください。この引数に値を割り当てるため、第3ステージEAに以下の入力を追加します。

input group "::: Saving to library"
input string groupName_  = "SimpleVolumes_v.1.20_2023.01.01";      // - Version name (if empty - not saving) 
input string advFileName_  = "SimpleVolumes-27183.test.db.sqlite"; // - EA database name

これまで、EAクラスレベルでデータベースを直接操作したことはありません。SQLクエリを直接生成するすべてのメソッドは、別の静的クラスCTesterHandlerに移動されています。したがって、この構造を崩さず、受け取った引数を新しいメソッドCTesterHandler::Export()に渡し、EA戦略の配列を追加します。

//+------------------------------------------------------------------+
//| Export the current strategy group to the specified EA database   |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Export(string p_groupName, string p_advFileName) {
   CTesterHandler::Export(m_strategies, p_groupName, p_advFileName);
}

このメソッドを実装するためには、EAデータベース内のテーブル構造を決定する必要があります。また、新しいデータベースが存在することにより、異なるデータベースへの接続を可能にする仕組みを整備する必要があります。


複数データベースへのアクセス

長い検討の末、次の方法に決めました。既存のCDatabaseクラスを修正し、データベースファイル名だけでなく、そのタイプも指定できるようにします。新しいデータベースタイプを考慮すると、次の3種類を扱う必要があります。

  • 最適化データベース:自動最適化プロジェクトを管理するため、およびオート最適化パイプライン内で実行されたストラテジーテスターのパスに関する情報を保存するために使用します。
  • グループ選択用データベース(簡略化された最適化データベース):自動最適化パイプラインの第2ステージで、必要な最適化データベースの一部をリモートテストエージェントに送信するために使用します。
  • エキスパートデータベース(最終EA用):取引口座上で稼働する最終EAが、使用する単一取引戦略インスタンスのグループ構成を含む必要なすべての作業情報を保存するために使用するデータベースです。

それぞれのデータベースタイプを作成するSQLコードを格納する3つのファイルを作成し、それらをDatabase.mqhファイルにリソースとして接続します。また、3種類のデータベースタイプ用の列挙型を作成します。

// Import SQL files for creating database structures of different types
#resource "db.opt.schema.sql" as string dbOptSchema
#resource "db.cut.schema.sql" as string dbCutSchema
#resource "db.adv.schema.sql" as string dbAdvSchema

// Database type
enum ENUM_DB_TYPE {
   DB_TYPE_OPT,   // Optimization database
   DB_TYPE_CUT,   // Database for group selection (stripped down optimization database)
   DB_TYPE_ADV,   // EA (final EA) database
};

これで、3種類のいずれのデータベースも(もちろん適切な内容で埋める場合に限りますが)作成スクリプトにアクセスできるようになったため、Connect()メソッドの接続ロジックを変更できます。渡された名前のデータベースが存在しない場合、エラーメッセージを出すのではなく、スクリプトからデータベースを作成し、新たに作成されたデータベースに接続するようにします。

ただし、どのタイプのデータベースが必要かを判断するために、接続メソッドに引数として希望のデータベースタイプを渡せる入力を追加します。既存コードの編集を最小限にするため、このパラメータのデフォルト値は最適化データベースタイプに設定します。これまでほとんどの接続で使用していたタイプだからです。

//+------------------------------------------------------------------+
//| Create an empty DB                                               |
//+------------------------------------------------------------------+
void CDatabase::Create(string p_schema) {
   bool res = Execute(p_schema);
   if(res) {
      PrintFormat(__FUNCTION__" | Database successfully created from %s", "db.*.schema.sql");
   }
}

//+------------------------------------------------------------------+
//| Check connection to the database with the given name             |
//+------------------------------------------------------------------+
bool CDatabase::Connect(string p_fileName, ENUM_DB_TYPE p_dbType = DB_TYPE_OPT) {
// If the database is open, close it
   Close();

// If a file name is specified, save it
   s_fileName = p_fileName;

// Set the shared folder flag for the optimization and EA databases
   s_common = (p_dbType != DB_TYPE_CUT ? DATABASE_OPEN_COMMON : 0);

// Open the database
// Try to open an existing DB file
   s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | s_common);

// If the DB file is not found, try to create it when opening
   if(!IsOpen()) {
      s_db = DatabaseOpen(s_fileName,
                          DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | s_common);

      // Report an error in case of failure
      if(!IsOpen()) {
         PrintFormat(__FUNCTION__" | ERROR: %s Connect failed with code %d",
                     s_fileName, GetLastError());
         return false;
      }
      if(p_dbType == DB_TYPE_OPT) {
         Create(dbOptSchema);
      } else if(p_dbType == DB_TYPE_CUT) {
         Create(dbCutSchema);
      } else {
         Create(dbAdvSchema);
      }
   }

   return true;
}

なお、最適化データベースとEAデータベースはターミナルの共有フォルダに格納し、グループ選択用データベースはターミナルの作業フォルダに格納することにしました。そうしなければ、テストエージェントへの自動送信をおこなうことができません。


EAデータベース

EAデータベースに生成された戦略グループの情報を保存するため、strategy_groupsテーブルとstrategiesテーブルの2つを使用することにしました。構造は以下の通りです。

CREATE TABLE strategies (
    id_strategy INTEGER PRIMARY KEY AUTOINCREMENT
                        NOT NULL,
    id_group    INTEGER REFERENCES strategy_groups (id_group) ON DELETE CASCADE
                                                              ON UPDATE CASCADE,
    hash        TEXT    NOT NULL,
    params      TEXT    NOT NULL
);

CREATE TABLE strategy_groups (
    id_group    INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT,
    from_date   TEXT,
    to_date     TEXT,
    create_date TEXT
);

ご覧の通り、strategiesテーブルの各エントリは、strategy_groupsテーブルのいずれかのエントリを参照しています。そのため、このデータベースには複数の異なる戦略グループを同時に保存することが可能です。

strategiesテーブルのhashフィールドには、単一取引戦略インスタンスのパラメータのハッシュ値を格納します。これにより、あるグループの単一インスタンスが別のグループのインスタンスと同一かどうかを後で確認することが可能になります。

strategiesテーブルのparamsには、単一取引戦略インスタンスの初期化文字列を保存します。このインスタンスから、最終EAでEAオブジェクト(CVirtualAdvisorクラス)を作成するために、戦略グループ全体の共通初期化文字列を形成することができます。

strategy_groupsテーブルのfrom_dateフィールドとto_dateフィールドには、このグループを取得するために使用される最適化間隔の開始日と終了日が引き続き保存されます。現時点では、単に空欄のままにしておきます。


戦略の再エクスポート

これで、TesterHandler.mqh内で戦略グループをEAデータベースにエクスポートするメソッドを実装する準備が整いました。そのためには、まず必要なデータベースに接続し、strategy_groupsテーブルに新しい戦略グループのレコードを作成し、グループ内の各戦略について現在のスケーリング係数を使用して初期化文字列(class CVirtualStrategyGroup([strategy], scale)でラップ)を生成し、それをstrategiesテーブルに保存する必要があります。

//+------------------------------------------------------------------+
//| Export an array of strategies to the specified EA database       |
//| as a new group of strategies                                     |
//+------------------------------------------------------------------+
void CTesterHandler::Export(CStrategy* &p_strategies[], string p_groupName, string p_advFileName) {
// Connect to the required EA database
   if(DB::Connect(p_advFileName, DB_TYPE_ADV)) {

      string fromDate = "";   // Start date of the optimization interval
      string toDate = "";     // End date of the optimization interval

      // Create an entry for a new strategy group
      string query = StringFormat("INSERT INTO strategy_groups VALUES(NULL, '%s', '%s', '%s', NULL) RETURNING rowid;",
                                  p_groupName, fromDate, toDate);
      ulong groupId = DB::Insert(query);

      PrintFormat(__FUNCTION__" | Export %d strategies into new group [%s] with ID=%I64u",
                  ArraySize(p_strategies), p_groupName, groupId);

      // For each strategy
      FOREACH(p_strategies, {
         CVirtualStrategy *strategy = p_strategies[i];
         // Form an initialization string as a group of one strategy with a normalizing factor
         string params = StringFormat("class CVirtualStrategyGroup([%s],%0.5f)",
                                      ~strategy,
                                      strategy.Scale());
                                      
         // Save it in the EA database with the new group ID specified
         string query = StringFormat("INSERT INTO strategies "
                                     "VALUES (NULL, %I64u, '%s', '%s')",
                                     groupId, strategy.Hash(~strategy), params);
         DB::Execute(query);
      });

      // Close the database
      DB::Close();
   }
}

戦略パラメータからハッシュ値を計算するために、既存のメソッドをEAクラスからCFactorable親クラスに移動しました。そのため、現在ではこのクラスを継承するすべてのクラス、取引戦略クラスも含めて利用可能になっています。

これで、最適化プロジェクトの第3ステージを再実行すると、strategiesテーブルには単一の取引戦略インスタンスを持つエントリが表示されます。

また、strategy_groupテーブルには各プロジェクトの最終グループのエントリが表示されます。

これで戦略のエクスポートは整理できました。次は逆の操作、すなわちこれらのグループを最終EAにインポートする処理に進みます。


戦略のインポート

ここでは、以前実装した戦略グループのエクスポート方法を完全に廃止するつもりはありません。新しい方法と古い方法の両方を並行して使用できるようにします。新しい方法がうまくいけば、古い方法を廃止することを検討すればよいでしょう。

最終EAであるSimpleVolumesExpert.mq5に、新しい入力newGroupId_を追加します。これにより、新ライブラリから戦略グループIDの値を設定できるようにします。

input group "::: Use a strategy group"
input ENUM_GROUPS_LIBRARY groupId_     = -1// - Group from the old library OR:
input int                 newGroupId_  = 0// - ID of the group from the new library (0 - last)

さらに、最終EAの名前用の定数を追加します。

#define __NAME__ "SimpleVolumes"

最終EAの初期化関数では、まずgroupId_パラメータで古いライブラリのグループが選択されているか確認します。選択されていなければ、新しいライブラリから初期化文字列を取得します。そのために、CVirtualAdvisor EAクラスに新たに2つの静的メソッドFileName()およびImport()を追加します。これらはEAオブジェクトが作成される前に呼び出すことができます。

//+------------------------------------------------------------------+
//| 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_];
   } else {
      // Take the initialization string from the new library for the selected group
      // (from the EA database)
      strategiesParams = CVirtualAdvisor::Import(
                            CVirtualAdvisor::FileName(__NAME__, magic_),
                            newGroupId_
                         );
   }

// 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);
}

VirtualAdvisor.mqhファイルでさらに変更をおこないます。先ほど述べた2つのメソッドを追加します。

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   // ...
public:
   // ...

   // Name of the file with the EA database
   static string     FileName(string p_name, ulong p_magic = 1);
   
   // Get the strategy group initialization string 
   // from the EA database with the given ID
   static string     Import(string p_fileName, int p_groupId = 0);
   
};

FileName()メソッドでは、EAデータベースファイル名の生成ルールを定めます。最終EAの名前とマジックナンバーを含めることで、異なるマジックナンバーのEAは常に異なるデータベースを使用します。また、ストラテジーテスターでEAが起動された場合には自動的に「.test」接尾辞を追加します。これにより、テスターでEAを動かした際に、取引口座上で既に稼働中のEAのデータベース情報を誤って上書きすることを防ぎます。

//+------------------------------------------------------------------+
//| Name of the file with the EA database                            |
//+------------------------------------------------------------------+
string CVirtualAdvisor::FileName(string p_name, ulong p_magic = 1) {
   return StringFormat("%s-%d%s.db.sqlite",
                       (p_name != "" ? p_name : "Expert"),
                       p_magic,
                       (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                      );
}

Import()メソッドでは、EAデータベースから指定されたグループに属する単一取引戦略インスタンスの初期化文字列のリストを取得します。指定グループIDがゼロの場合は、最後に作成されたグループの戦略リストをロードします。

取得したリストから、戦略初期化文字列をカンマで結合して、グループ初期化文字列内の所定の位置に挿入します。初期化文字列内のグループのスケーリング係数は、戦略数に設定します。これは、このグループ初期化文字列を使ってEAを作成する際に、全戦略のスケーリング係数がEAデータベースに保存されたものと一致するようにするためです。作成プロセス中、グループ内のすべての戦略の乗数は自動的に戦略数で割られます。このため、グループ乗数をあらかじめ戦略数倍に増やしておくことで、正しい値を維持できます。

//+------------------------------------------------------------------+
//| Get the strategy group initialization string                     |
//| from the EA database with the given ID                           |
//+------------------------------------------------------------------+
string CVirtualAdvisor::Import(string p_fileName, int p_groupId = 0) {
   string params[];   // Array for strategy initialization strings
   
   // Request to get strategies of a given group or the last group
   string query = StringFormat("SELECT id_group, params "
                               "  FROM strategies"
                               " WHERE id_group = %s;",
                               (p_groupId > 0 ? (string) p_groupId 
                                : "(SELECT MAX(id_group) FROM strategy_groups)"));

// Open EA database
   if(DB::Connect(p_fileName, DB_TYPE_ADV)) {
      // Execute the request
      int request = DatabasePrepare(DB::Id(), query);

      // If there is no error
      if(request != INVALID_HANDLE) {
         // Data structure for reading a single string of a query result 
         struct Row {
            int      groupId;
            string   params;
         } row;

         // Read data from the first result string
         while(DatabaseReadBind(request, row)) {
            // Remember the strategy group ID 
            // in the static property of the EA class
            s_groupId = row.groupId;
            
            // Add another strategy initialization string to the array
            APPEND(params, row.params);
         }
      } else {
         // Report an error if necessary
         PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", 
                     query, GetLastError());
      }

      // Close the EA database
      DB::Close();
   }

   // Strategy group initialization string
   string groupParams = NULL;

   // Total number of strategies in the group
   int totalStrategies = ArraySize(params);
   
   // If there are strategies, then
   if(totalStrategies > 0) {
      // Concatenate their initialization strings with commas
      JOIN(params, groupParams, ",");
      
      // Create a strategy group initialization string
      groupParams = StringFormat("class CVirtualStrategyGroup([%s], %.5f)",
                                 groupParams,
                                 totalStrategies);
   }

   // Return the strategy group initialization string
   return groupParams;
}

このメソッドは完全に純粋ではありません。グループ初期化文字列を返すだけでなく、ロードされた戦略グループのIDをCVirtualAdvisor::s_groupIdの静的プロパティに設定する処理もおこなっています。この方法は、どのグループがライブラリから読み込まれたかを記憶するために、非常にシンプルで信頼性の高い手段となっています。見た目はあまり美しくありませんが、実用的です。


最終EAデータの転送

すでに、最終EAが使用する単一取引戦略インスタンスのパラメータを保存するための別データベースを用意したので、ここで中途半端に止まらず、取引口座上での最終EAの運用に関する残りの情報の保存も同じデータベースに移行します。以前は、このような情報はCVitrualAdvisor::Save()メソッドを使って別ファイルに保存され、必要に応じてCVitrualAdvisor::Load()メソッドで読み込まれていました。

ファイルに保存されていた情報は以下の通りです。

  • EA全般のパラメータ:最後の保存時刻など。現時点ではそれだけですが、将来的にリストは拡張される可能性があります。
  • 各戦略のデータ:仮想ポジションのリストや戦略が保存する必要のあるその他のデータ。現在使用している戦略では追加データは不要ですが、他の種類の戦略では必要になる場合があります。
  • リスクマネージャーのデータ:現在の状態、最新の残高や証拠金レベル、ポジションサイズの乗数など。

以前の実装方法の欠点は、データファイルを読み取る際にすべてを一括で解釈するしかなかったことです。たとえば、初期化文字列の戦略数を増やして最終EAを再起動したい場合、保存済みデータファイルを正しく読み込むことができません。読み込み時、最終EAは追加された戦略の情報もファイルに含まれていることを期待します。しかし実際には存在しません。そのため、読み込みメソッドはファイル中の次のデータを追加戦略関連の情報として解釈しようとしますが、実際にはそれはリスクマネージャーのデータであり、当然正しく処理されません。

この問題を解決するためには、最終EAの作業情報を厳密に連続的に保存する方法から離れ、データベースを利用することが非常に有効です。そこで、キーと値の形式(Key-Value)で任意のデータを簡単に保存できるストレージをデータベース内に整備します。


Key-Valueストレージ

上で任意のデータを保存すると述べましたが、ここで課題をあまり広く設定する必要はありません。現在、最終EAのデータファイルに保存されている内容を確認すると、個々の数値(整数および実数)や仮想ポジションオブジェクトを保存できれば十分です。各戦略には固定サイズの仮想ポジション配列があることも思い出してください。このサイズは戦略初期化パラメータで指定されています。そのため、仮想ポジションオブジェクトは常に何らかの配列の一部として存在します。また将来に備えて、個々の数値だけでなく、異なる型の数値配列も保存できるようにします。

これらを考慮して、以下のメソッドを持つ新しい静的クラスを作成します。

  • データベースへの接続:Connect()/Close()
  • 異なる型の値の設定:Set(...)
  • 異なる型の値の読み取り:Get(...)
最終的に以下のようになりました。

//+------------------------------------------------------------------+
//| Class for working with the EA database in the form of            |
//| Key-Value storage for properties and virtual positions           |
//+------------------------------------------------------------------+
class CStorage {
protected:  
   static bool       s_res; // Result of all database read/write operations
public:
   // Connect to the EA database
   static bool       Connect(string p_fileName);
   
   // Close connection to the database
   static void       Close();

   // Save a virtual order/position
   static void       Set(int i, CVirtualOrder* order);

   // Store a single value of an arbitrary simple type
   template<typename T>
   static void       Set(string key, const T &value);

   // Store an array of values of an arbitrary simple type
   template<typename T>
   static void       Set(string key, const T &values[]);

   // Get the value as a string for the given key
   static string     Get(string key);

   // Get an array of virtual orders/positions for a given strategy hash
   static bool       Get(string key, CVirtualOrder* &orders[]);

   // Get the value for a given key into a variable of an arbitrary simple type
   template<typename T>
   static bool       Get(string key, T &value);

   // Get an array of values of a simple type by a given key into a variable
   template<typename T>
   static bool       CStorage::Get(string key, T &values[]);

   // Result of operations
   static bool       Res() {
      return s_res;
   }
};

s_res静的プロパティとその読み取りメソッドを追加しました。これにより、データベースの読み書き操作中に発生したエラーの有無を記録できます。

このクラスは最終EAの状態を保存・読み込みするためだけに使用されるため、データベース接続もこれらの操作時のみおこないます。接続が閉じられるまで、他の意味のある操作はおこないません。そのため、接続メソッドでトランザクションを開始し、接続終了時に確認または取り消しをおこなうようにします。

//+------------------------------------------------------------------+
//| Connect to the EA database                                       |
//+------------------------------------------------------------------+
bool CStorage::Connect(string p_fileName) {
   // Connect to the EA database
   if(DB::Connect(p_fileName, DB_TYPE_ADV)) {
      // No errors yet
      s_res = true;
      
      // Start a transaction
      DatabaseTransactionBegin(DB::Id());
      
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Close the database connection                                    |
//+------------------------------------------------------------------+
void CStorage::Close() {
   // If there are no errors,
   if(s_res) {
      // Confirm the transaction
      DatabaseTransactionCommit(DB::Id());
   } else {
      // Otherwise, cancel the transaction
      DatabaseTransactionRollback(DB::Id());
   }
   
   // Close connection to the database
   DB::Close();
}

次に、最終EAのデータベース構造に2つのテーブルを追加します。

最初のテーブル(strorage)は、個々の数値や数値配列を保存します。文字列も保存可能です。2番目のテーブル(storage_orders)には、異なる取引戦略インスタンスの仮想ポジション配列の情報を保存するため、strategy_hashstrategy_indexの列がテーブルの先頭に配置されています。これらの列は、それぞれ戦略パラメータのハッシュ値(各戦略ごとに一意)と、戦略の仮想ポジション配列内のポジションのインデックスを格納します。このようにすることで、各戦略インスタンスの仮想ポジションが正確に識別され、保存・読み込みの際に混同されないようにしています。

べての個別数値は、Set()テンプレートメソッドを呼び出して保存します。このメソッドは、キー名の文字列と任意の単純型Tの変数をパラメータとして受け取ります。ここでいう「任意の単純型」とは、intulongdoubleなどを指します。保存の際には、変数の値を文字列型に変換し、SQLクエリを生成してデータベースに格納します。

//+------------------------------------------------------------------+
//| Store a single value of an arbitrary simple type                 |
//+------------------------------------------------------------------+
template<typename T>
void CStorage::Set(string key, const T &value) {
// Escape single quotes (can't avoid using them yet)
// StringReplace(key, "'", "\\'");
// StringReplace(value, "'", "\\'");

// Request to save the value
   string query = StringFormat("REPLACE INTO storage(key, value) VALUES('%s', '%s');",
                               key, (string) value);

// Execute the request
   s_res &= DatabaseExecute(DB::Id(), query);

   if(!s_res) {
      // Report an error if necessary
      PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n"
                  "%s\n"
                  "error code = %d",
                  query, GetLastError());
   }
}

単一のキーに対して単純型の配列を保存したい場合は、まず渡された配列のすべての値を区切り文字で連結して1つの文字列を作成します。このとき、区切り文字としてカンマ(,)を使用します。この処理は、同じ名前Set()の別のテンプレートメソッドでおこなわれます。ただし、このメソッドでは2番目のパラメータとして単純型の変数ではなく、単純型の配列への参照を受け取ります。

//+------------------------------------------------------------------+
//| Store an array of values of an arbitrary simple type             |
//+------------------------------------------------------------------+
template<typename T>
void CStorage::Set(string key, const T &values[]) {
   string value = "";
   
   // Concatenate all values from the array into one string separated by commas
   JOIN(values, value, ",");
   
   // Save a string with a specified key
   Set(key, value);
}

逆操作、つまりデータベースからの読み取りをおこなうために、Get()メソッドを追加します。このメソッドは、指定されたキーに対応する行をデータベースから取得して返します。必要な単純型の値を取得する場合には、同じ名前Get()のテンプレートメソッドを作成します。このメソッドは、さらに任意の単純型の変数への参照を第2引数として受け取ります。このメソッドでは、まずデータベースから値を文字列として取得し、取得できた場合にはその文字列を要求された型に変換して、渡された変数に代入します。

//+------------------------------------------------------------------+
//| Get the value as a string for the given key                      |
//+------------------------------------------------------------------+
string CStorage::Get(string key) {
   string value = NULL; // Return value

// Request to get the value
   string query = StringFormat("SELECT value FROM storage WHERE key='%s'", key);

// Execute the request
   int request = DatabasePrepare(DB::Id(), query);

// If there is no error
   if(request != INVALID_HANDLE) {
      // Read data from the first result string
      DatabaseRead(request);

      if(!DatabaseColumnText(request, 0, value)) {
         // Report an error if necessary
         PrintFormat(__FUNCTION__" | ERROR: Reading row in DB [adv] for request \n%s\n"
                     "failed with code %d",
                     query, GetLastError());
      }
   } else {
      // Report an error if necessary
      PrintFormat(__FUNCTION__" | ERROR: Request in DB [adv] \n%s\nfailed with code %d",
                  query, GetLastError());
   }

   return value;
}

//+------------------------------------------------------------------+
//| Get the value for a given key into a variable                    |
//| of an arbitrary simple type                                      |
//+------------------------------------------------------------------+
template<typename T>
bool CStorage::Get(string key, T &value) {
// Get the value as a string
   string res = Get(key);

// If the value is received
   if(res != NULL) {
      // Cast it to type T and assign it to the target variable
      value = (T) res;
      return true;
   }
   return false;
}

追加したメソッドを使って、最終EAの状態を保存および読み込みできるようにします。


EAの保存とダウンロード

CVirtualAdvisor::Save()メソッド(EAの状態を保存するメソッド)では、まずEAデータベースに接続し、保存する必要のある情報をすべて格納します。このとき、直接CStorageクラスのメソッドを呼び出すか、保存が必要なオブジェクトに対しては間接的にそのオブジェクトのSave()/Load()メソッドを呼び出してデータベースに保存します。

現状では、直接保存する値は、仮想ポジションの構成に最後に変更があった時刻と戦略グループのIDの2つだけです。その後、すべての戦略に対してループ内でSave()メソッドを呼び出します。最後にリスクマネージャの保存メソッドを呼び出します。これらの各メソッドも、EAデータベースに保存するように変更を加える必要があります。

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
// Save status if:
   if(true
// later changes appeared
         && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
// currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
// and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      // If the connection to the EA database is established
      if(CStorage::Connect(m_fileName)) {
         // Save the last modification time
         CStorage::Set("CVirtualReceiver::s_lastChangeTime", CVirtualReceiver::s_lastChangeTime);
         CStorage::Set("CVirtualAdvisor::s_groupId", CVirtualAdvisor::s_groupId);

         // Save all strategies
         FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save());

         // Save the risk manager
         m_riskManager.Save();

         // Update the last save time
         m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
         PrintFormat(__FUNCTION__" | OK at %s to %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS),
                     m_fileName);

         // Close the connection
         CStorage::Close();

         // Return the result
         return CStorage::Res();
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Can't open database [%s], LastError=%d",
                     m_fileName, GetLastError());
         return false;
      }
   }
   return true;
}

CVirtualAdvisor::Load()メソッド(EA状態の読み込みメソッド)では、逆の操作をおこないます。まずデータベースから最後に変更された時刻の値と戦略グループIDを読み込み、その後、各戦略およびリスクマネージャが自身の情報を読み込みます。最終変更時刻が将来の日時になっていた場合は、それ以降の情報は読み込まず無視します。この状況は、たとえばストラテジーテスターで再度視覚的にテストを実行した場合に発生する可能性があります。前回のテストではテスト終了時に情報が保存されており、2回目のテスト開始時にEAが前回と同じデータベースを使用すると、以前の情報を無視して最初から作業を開始する必要があります。

読み込みメソッドが呼ばれる時点で、EAオブジェクトはすでに戦略グループ付きで作成されています。この戦略グループのIDはEAの入力から取得され、CVirtualAdvisor::Import()メソッド内でCVirtualAdvisor::s_groupId静的プロパティに保存されています。そのため、EAデータベースから戦略グループIDを読み込む際に既存の値と比較することができます。もし異なれば、最終EAが新しい戦略グループで再起動されたことを意味し、追加の処理が必要になる可能性があります。しかし、この場合に必ず実行するべき処理がまだ完全には決まっていないため、コード内には将来的な対応のためのコメントを残しておきます。

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
   bool res = true;
   ulong groupId = 0;

// Load status if:
   if(true
// file exists
         && FileIsExist(m_fileName, FILE_COMMON)
// currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
// and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      // If the connection to the EA database is established
      if(CStorage::Connect(m_fileName)) {
         // Download the last modification time
         res &= CStorage::Get("CVirtualReceiver::s_lastChangeTime", m_lastSaveTime);

         // Download the saved strategy group ID
         res &= CStorage::Get("CVirtualAdvisor::s_groupId", groupId);

         // If the last modification time is in the future, then ignore the download
         if(m_lastSaveTime > TimeCurrent()) {
            PrintFormat(__FUNCTION__" | IGNORE LAST SAVE at %s in the future",
                        TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));
            m_lastSaveTime = 0;
            return true;
         }

         PrintFormat(__FUNCTION__" | LAST SAVE at %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

         if(groupId != CVirtualAdvisor::s_groupId) {
            // Actions when launching an EA with a new group of strategies.
            // Nothing is happening here yet
         }

         // Load all strategies
         FOREACH(m_strategies, {
            res &= ((CVirtualStrategy*) m_strategies[i]).Load();
            if(!res) break;
         });

         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_fileName);
         }

         // Download the risk manager
         res &= m_riskManager.Load();

         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_fileName);
         }

         // Close the connection
         CStorage::Close();

         return res;
      }
   }

   return true;
}

では、さらに一段階下に降りて、戦略の保存・読み込みメソッドの実装について見ていきます。


戦略の保存とダウンロード

CVirtualStrategyクラスでは、これらのメソッドで仮想ポジションを使用するすべての戦略に共通する処理のみを実装します。各戦略は、保存や読み込みが必要な仮想ポジションオブジェクトの配列を持っています。詳細な実装はさらに下位レベルに任せ、ここでは専用に作成したCStorageクラスのメソッドだけを呼び出すようにします。

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
void CVirtualStrategy::Save() {
// Save virtual positions (orders) of the strategy
   FOREACH(m_orders, CStorage::Set(i, m_orders[i]));
}

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Load() {
   bool res = true;
   
// Download virtual positions (orders) of the strategy
   res = CStorage::Get(this.Hash(), m_orders);

   return res;
}

CVirtualStrategyクラスの派生クラス(CSimpleVolumnesStrategyを含む)の場合、仮想ポジションの配列に関連して追加データを保存する必要が出てくることもあります。現在のモデル戦略は非常にシンプルで、仮想ポジションのリスト以外に保存するものはありません。しかし、仮にティックボリュームの配列や平均ティックボリュームの値を保存したい場合を考えます。Save()とLoad()メソッドは仮想メソッドとして宣言されているため、派生クラスでオーバーライドできます。このとき、必要なデータの保存や読み込み処理を追加しつつ、基本クラスのメソッドを呼び出して仮想ポジションの保存や読み込みをおこないます。

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::Save() {
   double avrVolume = ArrayAverage(m_volumes);

// Let's form the common part of the key with the type and hash of the strategy
   string key = "CSimpleVolumesStrategy[" + this.Hash() + "]";

// Save the average tick volume
   CStorage::Set(key + ".avrVolume", avrVolume);

// Save the array of tick volumes
   CStorage::Set(key + ".m_volumes", m_volumes);

// Call the base class method (to save virtual positions)
   CVirtualStrategy::Save();
}

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Load() {
   bool res = true;

   double avrVolume = 0;

// Let's form the common part of the key with the type and hash of the strategy
   string key = "CSimpleVolumesStrategy[" + this.Hash() + "]";

// Load the tick volume array
   res &= CStorage::Get(key + ".avrVolume", avrVolume);

// Load the tick volume array
   res &= CStorage::Get(key + ".m_volumes", m_volumes);

// Call the base class method (to load virtual positions)
   res &= CVirtualStrategy::Load();

   return res;
}

あとは、仮想ポジションの保存と読み込みを実装するだけです。


仮想ポジションの保存と読み込み

以前は、CVirtualOrderクラスのSave()Load()メソッドが、現在の仮想ポジションオブジェクトに必要な情報を直接データファイルに保存していました。しかし、ここでは構造を少し変更します。まず、仮想ポジションに必要なすべてのデータを格納するシンプルな構造体CVirtualOrderStructを追加します。

// Structure for reading/writing 
// basic properties of a virtual order/position from the database
struct VirtualOrderStruct {
   string            strategyHash;
   int               strategyIndex;
   ulong             ticket;
   string            symbol;
   double            lot;
   ENUM_ORDER_TYPE   type;
   datetime          openTime;
   double            openPrice;
   double            stopLoss;
   double            takeProfit;
   datetime          closeTime;
   double            closePrice;
   datetime          expiration;
   string            comment;
   double            point;
};

仮想ポジションオブジェクトとは異なり、作成されたすべてのインスタンスが厳密に記録され、取引ボリューム受信モジュールで自動的に処理されるわけではありません。このような構造体は、必要に応じていつでも、何度でも作成することができます。ここでは、この構造体を使って、仮想ポジションオブジェクトと、CStorageクラスで実装されたEAデータベースへの保存と読み込みメソッドとの間で情報を受け渡します。その結果、仮想ポジションクラス自身のSave()とLoad()メソッドは、渡された構造体に値を設定する、あるいは渡された構造体のフィールドの値を自分のプロパティに書き込むだけで済むようになります。

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
void CVirtualOrder::Load(const VirtualOrderStruct &o) {
   m_ticket = o.ticket;
   m_symbol = o.symbol;
   m_lot = o.lot;
   m_type = o.type;
   m_openPrice = o.openPrice;
   m_stopLoss = o.stopLoss;
   m_takeProfit = o.takeProfit;
   m_openTime = o.openTime;
   m_closePrice = o.closePrice;
   m_closeTime = o.closeTime;
   m_expiration = o.expiration;
   m_comment = o.comment;
   m_point = o.point;

   PrintFormat(__FUNCTION__" | %s", ~this);

   s_ticket = MathMax(s_ticket, m_ticket);
   
   m_symbolInfo = m_symbols[m_symbol];

// Notify the recipient and the strategy that the position (order) is open
   if(IsOpen()) {
      m_receiver.OnOpen(&this);
      m_strategy.OnOpen(&this);
   } else {
      m_receiver.OnClose(&this);
      m_strategy.OnClose(&this);
   }
}

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
void CVirtualOrder::Save(VirtualOrderStruct &o) {
   o.ticket = m_ticket;
   o.symbol = m_symbol;
   o.lot = m_lot;
   o.type = m_type;
   o.openPrice = m_openPrice;
   o.stopLoss = m_stopLoss;
   o.takeProfit = m_takeProfit;
   o.openTime = m_openTime;
   o.closePrice = m_closePrice;
   o.closeTime = m_closeTime;
   o.expiration = m_expiration;
   o.comment = m_comment;
   o.point = m_point;
}

最後に、EAデータベース内に作成したstorage_ordersテーブルを使って、各仮想ポジションのプロパティを保存します。この処理を担当するのがCStorage::Set()メソッドです。このメソッドは、仮想ポジションのインデックスと仮想ポジションオブジェクト自体を受け取るようになっています。

//+------------------------------------------------------------------+
//| Save a virtual order/position                                    |
//+------------------------------------------------------------------+
void CStorage::Set(int i, CVirtualOrder* order) {
   VirtualOrderStruct o;   // Structure for virtual position data
   order.Save(o);          // Fill it

// Escape quotes in the comment
   StringReplace(o.comment, "'", "\\'");

// Request to save
   string query = StringFormat("REPLACE INTO storage_orders VALUES("
                               "'%s',%d,%I64u,"
                               "'%s',%.2f,%d,%I64d,%f,%f,%f,%I64d,%f,%I64d,'%s',%f);",
                               order.Strategy().Hash(), i, o.ticket,
                               o.symbol, o.lot, o.type,
                               o.openTime, o.openPrice,
                               o.stopLoss, o.takeProfit,
                               o.closeTime, o.closePrice,
                               o.expiration, o.comment,
                               o.point);

// Execute the request
   s_res &= DatabaseExecute(DB::Id(), query);

   if(!s_res) {
      // Report an error if necessary
      PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n"
                  "%s\n"
                  "error code = %d",
                  query, GetLastError());
   }
}

CStorage::Get()メソッドは、第2引数として受け取った仮想ポジションオブジェクトの配列に対して、情報を読み込みます。このメソッドは、第1引数として渡されたハッシュ値に対応する戦略の仮想ポジション情報を、storage_ordersテーブルから取得して配列に格納します。

//+------------------------------------------------------------------+
//| Get an array of virtual orders/positions                         |
//| by the given strategy hash                                       |
//+------------------------------------------------------------------+
bool CStorage::Get(string key, CVirtualOrder* &orders[]) {
// Request to obtain data on virtual positions
   string query = StringFormat("SELECT * FROM storage_orders "
                               " WHERE strategy_hash = '%s' "
                               " ORDER BY strategy_index ASC;",
                               key);

// Execute the request
   int request = DatabasePrepare(DB::Id(), query);

// If there is no error
   if(request != INVALID_HANDLE) {
      // Structure for virtual position information 
      VirtualOrderStruct row;
      
      // Read the data from the query result string by string
      while(DatabaseReadBind(request, row)) {
         orders[row.strategyIndex].Load(row);
      }
   } else {
      // Save the error and report it if necessary
      s_res = false;
      PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n"
                  "%s\n"
                  "error code = %d",
                  query, GetLastError());
   }

   return s_res;
}

これで、最終EAの動作情報を別データベースに格納する形への移行に関する大部分の変更は完了です。 


簡単なテスト

大きな変更をおこなったにもかかわらず、まだ最終EAの稼働中に設定をホットスワップする段階には到達していません。しかし、最終EAの初期化機構が正しく動作することは確認できます。

そのために、最適化データベースから初期化文字列配列を旧方式と新方式の両方でエクスポートしました。これにより、4つの戦略グループの情報が ExportedGroupsLibrary.mqhファイルと、EAデータベースSimpleVolumes-27183.test.db.sqliteの両方に存在する状態になっています。次に、最終EAであるSimpleVolumesExpert.mq5をコンパイルします。

入力値を次のように設定すると、

選択された初期化文字列は最終EAの内部配列から読み込まれます。この配列はコンパイル時にExportedGroupsLibrary.mqhファイル内のデータから埋められたものです(旧方式)。

パラメータ値をこのように指定すると、

初期化文字列はEAデータベースから取得した情報に基づいて生成されます(新方式)。

最終EAを旧方式の初期化で短期間(たとえば、過去1か月間)動作させると、結果は以下のようになります。

旧方式による最終EAの動作結果

ここで、新しい初期化方法を使用して、同じ時間間隔で最終EAを実行してみましょう。以下が結果です。

新方式による最終EAの動作結果

ご覧の通り、旧方式と新方式の両方で得られた結果は完全に同一です。


結論

今回取り組んだ課題は、当初想定していたよりもやや難しいものでした。すべての期待される成果はまだ達成できていませんが、今後のテストと開発に十分対応できる完全に機能するソリューションは得られました。これにより、最終的に稼働中のEAが使用するデータベースに対して、直接新しい取引戦略グループをエクスポートして最適化プロジェクトを実行できるようになりました。ただし、このメカニズムの正確性については、今後の検証が必要です。

通常通り、まずはストラテジーテスター上でEAを動作させ、期待する挙動をシミュレーションすることでテストを開始します。そこで得られた結果が満足できるものであれば、その後、テスターでの動作ではなく実際の最終EAに適用して使用します。次回その点について詳しく触れます。

ご精読ありがとうございました。またすぐにお会いしましょう。

重要な警告:

この記事および連載のこれまでのすべての記事で提示された結果は、過去のテストデータのみに基づいており、将来の利益を保証するものではありません。このプロジェクトでの作業は研究的な性質のものであり、公開された結果はすべて、自己責任で使用されるべきです。


アーカイブ内容

#
 名前
バージョン  詳細  最近の変更
MQL5/Experts/Article.16452
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.10 データベースを扱うクラス 第22回
5 db.adv.schema.sql 1.00
最終EAのデータベース構造 第22回
6 db.cut.schema.sql
1.00 簡略化された最適化データベースの構造
第22回
7 db.opt.schema.sql
1.05  最適化データベース構造
第22回
8 ExpertHistory.mqh 1.00 取引履歴をファイルにエクスポートするクラス 第16回
9 ExportedGroupsLibrary.mqh -
戦略グループ名とその初期化文字列の配列をリストした生成されたファイル 第22回
10 Factorable.mqh 1.03 文字列から作成されたオブジェクトの基本クラス 第22回
11 GroupsLibrary.mqh 1.01 選択された戦略グループのライブラリを操作するためのクラス 第18回
12 HistoryReceiverExpert.mq5 1.00 リスクマネージャーとの取引履歴を再生するためのEA 第16回
13 HistoryStrategy.mqh  1.00 取引履歴を再生するための取引戦略のクラス  第16回
14 Interface.mqh 1.00 さまざまなオブジェクトを視覚化するための基本クラス 第4回
15 LibraryExport.mq5 1.01 選択したパスの初期化文字列をライブラリからExportedGroupsLibrary.mqhファイルに保存するEA 第18回
16 Macros.mqh 1.05 配列操作に便利なマクロ 第22回
17 Money.mqh 1.01  基本的なお金の管理クラス 第12回
18 NewBarEvent.mqh 1.00  特定の銘柄の新しいバーを定義するクラス  第8回
19 Optimization.mq5  1.04 最適化タスクの起動を管理するEA 第22回
20 Optimizer.mqh 1.03 プロジェクト自動最適化マネージャーのクラス 第22回
21 OptimizerTask.mqh 1.03 最適化タスククラス 第22回
22 Receiver.mqh 1.04  オープンボリュームを市場ポジションに変換するための基本クラス  第12回
23 SimpleHistoryReceiverExpert.mq5 1.00 取引履歴を再生するための簡易EA   第16回
24 SimpleVolumesExpert.mq5 1.21 複数のモデル戦略グループを並列操作するための最終EAパラメータは組み込みのグループライブラリから取得されます。 第22回
25 SimpleVolumesStage1.mq5
1.18 取引戦略単一インスタンス最適化EA(第1ステージ)  第19回
26 SimpleVolumesStage2.mq5
1.02 取引戦略単一インスタンス最適化EA(第2ステージ)
第19回
27 SimpleVolumesStage3.mq5 1.03 生成された標準化された戦略グループを、指定された名前のグループのライブラリに保存するEA 第22回
28 SimpleVolumesStrategy.mqh 1.11  ティックボリュームを使用した取引戦略のクラス 第22回
29 Storage.mqh  1.00 最終的なEAのキー値ストレージを扱うクラス 第22回
30 Strategy.mqh 1.04  取引戦略基本クラス 第10回
31 SymbolsMonitor.mqh  1.00 取引商品(銘柄)に関する情報を取得するためのクラス 第21回
32 TesterHandler.mqh  1.06 最適化イベント処理クラス  第22回
33 VirtualAdvisor.mqh  1.09  仮想ポジション(注文)を扱うEAのクラス 第22回
34 VirtualChartOrder.mqh  1.01  グラフィカル仮想ポジションクラス 第18回
35 VirtualFactory.mqh 1.04  オブジェクトファクトリクラス  第16回
36 VirtualHistoryAdvisor.mqh 1.00  取引履歴再生EAクラス  第16回
37 VirtualInterface.mqh  1.00  EAGUIクラス  第4回
38 VirtualOrder.mqh 1.09  仮想注文とポジションのクラス  第22回
39 VirtualReceiver.mqh 1.03  オープンボリュームを市場ポジションに変換するクラス(レシーバー)  第12回
40 VirtualRiskManager.mqh  1.02  リスクマネジメントクラス(リスクマネージャー)  第15回
41 VirtualStrategy.mqh 1.08  仮想ポジションを使った取引戦略のクラス  第22回
42 VirtualStrategyGroup.mqh  1.00  取引戦略グループのクラス 第11回
43 VirtualSymbolReceiver.mqh  1.00 銘柄レシーバークラス  第3回
  MQL5/Common/Files   共有ターミナルフォルダ   
44 SimpleVolumes-27183.test.db.sqlite - 4つの戦略グループが追加されたEAデータベース  

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/16452

添付されたファイル |
MQL5.zip (738.17 KB)
取引におけるニューラルネットワーク:2次元接続空間モデル(Chimera) 取引におけるニューラルネットワーク:2次元接続空間モデル(Chimera)
この記事では、革新的なChimeraフレームワークについて解説します。Chimeraは二次元状態空間モデルを用い、ニューラルネットワークで多変量時系列を解析する手法です。この方法は、従来手法やTransformerアーキテクチャを上回る低い計算コストで高い精度を実現します実現します。
円探索アルゴリズム(CSA) 円探索アルゴリズム(CSA)
本記事では、円の幾何学的性質に基づいた新しいメタヒューリスティック最適化アルゴリズム「円探索アルゴリズム(Circle Search Algorithm, CSA)」を紹介します。本アルゴリズムは、最適解を探索するために点を接線に沿って移動させる原理を使用し、大域探索と局所探索のフェーズを組み合わせています。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
外国為替におけるフィボナッチ(第1回):価格と時間の関係を調べる 外国為替におけるフィボナッチ(第1回):価格と時間の関係を調べる
市場はフィボナッチに基づく関係性をどのように観測しているのでしょうか。各項が直前の2つの項の和になっているこの数列(1, 1, 2, 3, 5, 8, 13, 21...)は、ウサギの個体数の増加を説明するだけのものではありません。私たちは、「世界のあらゆるものは数の一定の関係に従う」というピタゴラス派の仮説を考察します。