English Русский 中文 Español Deutsch Português
preview
多通貨エキスパートアドバイザーの開発(第10回):文字列からオブジェクトを作成する

多通貨エキスパートアドバイザーの開発(第10回):文字列からオブジェクトを作成する

MetaTrader 5トレーディング | 15 10月 2024, 10:51
133 0
Yuriy Bykov
Yuriy Bykov

はじめに

前回の記事では、EA開発における各ステージの大まかな計画について説明しました。各ステージは、その後のステージで使用される一定量の情報を生成します。この情報をデータベースに保存することを決定し、そこにテーブルを作成して、様々なEAのストラテジーテスターにおけるシングルパスの結果を格納できるようにしました。

次のステップでこの情報を利用するためには、データベースに保存された情報から必要なオブジェクト(取引戦略、そのグループ、EAなど)を生成する方法が求められます。オブジェクトを直接データベースに保存するオプションは存在しないため、最も効果的なアプローチは、オブジェクトのプロパティを全て文字列に変換し、データベースに保存することです。その後、この文字列をデータベースから読み出し、必要なオブジェクトを再生成することができます。

文字列からオブジェクトを生成する方法はいくつか考えられます。例えば、デフォルトのパラメータを持つ対象クラスのオブジェクトを作成し、特定のメソッドや関数を使用してデータベースから取得した文字列を解析し、対応する値をオブジェクトのプロパティに代入することができます。また、1つの文字列を引数に取るオブジェクトのコンストラクタを新たに作成し、その文字列をコンストラクタ内でパーツに分解して、適切な値をオブジェクトのプロパティに割り当てる方法もあります。どちらのオプションがより適しているかを理解するために、まずはオブジェクトに関する情報をデータベースに保存する方法を検討してみましょう。


オブジェクトに関する情報の保存

前回の記事で作成したデータベースのテーブルを開いて、最後の列を確認してみましょう。paramsおよびinputs列には、CSimpleVolumesStrategyクラスの取引戦略オブジェクトを文字列に変換した結果と、1回の最適化パスの入力が保存されています。

図1:適用された戦略とテストパラメータに関する情報を含むパステーブルのフラグメント


inputs列には入力の名前が記載されていますが、これは戦略オブジェクトのプロパティ名と完全には一致しない場合があります。また、銘柄や期間などの重要なパラメータが欠落しています。そのため、オブジェクトを再作成する際には、params列の情報を使用する方が便利です。

戦略オブジェクトを文字列に変換する実装について振り返ってみましょう。連載第4回では、EAの状態をファイルに保存し、再起動後にそれを復元できるようにしました。この実装では、他の類似EAのデータを誤って使用しないように、使用する戦略の全インスタンスのパラメータをファイルに保存することを決定しました。

言い換えれば、当初の課題は、異なるパラメータを持つ取引戦略のインスタンスが生成する文字列が異なることを保証することでした。そのため、このような文字列に基づいて新しい取引戦略オブジェクトを作成する可能性については、特に問題視していませんでした。第9回では、この情報をデータベースに追加するプロセスをデバッグすることが目的だったため、既存の文字列変換メカニズムをそのまま利用しました。


実装

今こそ、このような文字列を使ってオブジェクトを再現する方法を考える時です。具体的には、次のような文字列があります。

class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3)

これをCSimpleVolumesStrategyクラスのコンストラクタに渡すためには、以下の手順を踏む必要があります。

  • 最初の開き括弧の前にある部分を削除する
  • 残りの部分を閉じ括弧までコンマで分割する
  • 得られた各部分を、必要に応じて数値に変換し、対応するオブジェクトプロパティに割り当てる

これらの手順を見てみると、最初のアクションはより高いレベルで実行できることがわかります。実際、まずこの行からクラス名を抽出することで、生成されるオブジェクトのクラスを特定できます。それにより、コンストラクタには括弧内の文字列のみを渡すことが可能になります。

さらに、文字列からオブジェクトを生成するニーズは、この単一のクラスに限定されません。まず、取引戦略は1つだけではありません。次に、CVirtualStrategyGroupクラスのオブジェクト、つまり異なるパラメータを持つ複数の取引戦略インスタンスのグループを作成する必要があります。これは、以前に選択した複数のグループを一つにまとめる際に役立ちます。最後に、文字列からEAオブジェクト(CVirtualAdvisorクラス)を生成する機能を提供することにも意味があります。これにより、使用するすべての戦略グループのテキスト記述をファイルからロードできる汎用EAを作成できるようになります。ファイル内の記述を変更することで、EAを再コンパイルすることなく、そのファイルに含まれる戦略の構成を完全に更新することが可能になります。

CVirtualStrategyGroupクラスのオブジェクトの初期化文字列は、次のようになると想像できます。

class CVirtualStrategyGroup([
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3),
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,27,0.70,0.90,60,10000.00,550.00,10000,3),
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,37,0.70,0.90,80,10000.00,150.00,10000,3)
], 0.33)

CVirtualStrategyGroupクラスのコンストラクタの最初のパラメータは、取引戦略オブジェクトの配列、もしくは取引戦略グループオブジェクトの配列です。したがって、これらを表現する文字列の一部を正しく解析する方法を学ぶ必要があります。ここで、リスト(配列)を表現する際には、JSONやPythonで使われている標準的な記法を活用しました。リストの要素はカンマで区切られ、角括弧 [ ] の中に収められます。

さらに、カンマ区切りの要素だけでなく、入れ子になった別のクラスオブジェクトを表す部分も文字列から抽出する方法を考慮する必要があります。幸いなことに、以前に取引戦略オブジェクトを文字列に変換するために typename()関数を使用しました。この関数は、オブジェクトのクラス名をclassで始まる文字列として返す役割を持っていました。これにより、文字列を解析する際に、そのクラス名が登場する部分から、次に続くのが単純な値(数値や文字列)ではなく、特定のクラスのオブジェクトであることを認識できるようになったのです。

こうした状況を踏まえると、ファクトリーデザインパターンを実装する必要があることが見えてきます。このデザインパターンは、特定のオブジェクトが要求に応じて異なるクラスのオブジェクトを生成する場合に適しています。ファクトリーが生成するオブジェクトは、通常、共通の祖先を持つクラス階層に属しています。そこで、初期化文字列からオブジェクトを生成できるすべてのクラスが最終的に継承する共通のクラスを新たに作成することから始めるとよいでしょう。


新しい基本クラス

これまでのところ、継承階層に参加している基本クラスは以下の通りです。

  • СAdvisorCVirtualAdvisorクラスから派生したEAを作成するためのクラス
  • CS戦略:取引戦略を作成するためのクラス。CSimpleVolumesStrategyはこのクラスから派生
  • CVirtualStrategyGroup:取引戦略グループのクラス。継承されず、今後される見込みもない
  • 以上

これ以外に、文字列で初期化する機能を必要とする子孫クラスを持つ基本クラスは存在しないと思います。つまり、これら3つのクラスは共通の祖先を持つ必要があり、その祖先クラスには、文字列で初期化するために必要な補助メソッドがすべて集約されています。

新しい祖先クラスの名前には、まだ大きな意味を持たせていませんが、クラスの子孫はファクトリーで生成できるため、「ファクトリー可能」であることを何とか強調したいと考えていました。しかし、コードを開発していく中で、「y」の文字がどこかで抜け落ち、結果としてCFactorableという名前だけが残りました。

当初のクラスは以下のようなものでした。

//+------------------------------------------------------------------+
//| Base class of objects created from a string                      |
//+------------------------------------------------------------------+
class CFactorable {
protected:
   virtual void      Init(string p_params) = 0;
public:
   virtual string    operator~() = 0;

   static string     Read(string &p_params);
};

そのため、このクラスの子孫には、入力された初期化文字列をオブジェクトのプロパティ値に変換するための処理をおこなうInit()メソッドと、プロパティを初期化文字列に逆変換するためのチルダ演算子を実装する必要がありました。また、Read()というstaticメソッドの存在し、初期化文字列からデータを読み取れるようにする必要があります。ここでの「データ部分」とは、他のオブジェクトの有効な初期化文字列や、他のデータ部分の配列、数値、文字列定数などの部分文字列を指します。

実装は動作しましたが、大幅な変更を加えることにしました。

まず、Init()メソッドが登場したのは、古いオブジェクトコンストラクタと新しい(初期化文字列を受け取る)コンストラクタの両方を維持したかったためです。コードの重複を避けるため、一度Init()メソッドで実装し、複数の可能性のあるコンストラクタから呼び出していました。しかし、最終的には異なるコンストラクタは不要だと判明しました。新しいコンストラクタを1つ追加するだけで十分です。結果として、Init()メソッドのコードは新しいコンストラクタに移され、メソッド自体は削除されました。

次に、初期の実装には、初期化文字列の妥当性やエラーレポートをチェックする仕組みがありませんでした。初期化文字列は自動生成されるため、エラーが発生することはほとんどないと思われますが、万が一、生成された文字列で何か問題が発生した場合、それをすぐに察知できれば便利です。この目的のために、新しい論理プロパティm_isValidを追加しました。このプロパティは、オブジェクトのコンストラクタが正しく実行されたか、あるいは初期化文字列にエラーが含まれていたかを示します。このプロパティはprivateで、、値を取得するためのIsValid()メソッドと、エラーが発生した際にプロパティをfalseに設定するSetInvalid()メソッドが追加されます。さらに、SetInvalid()メソッドは値をfalseにしか設定できませんが、プロパティは初期状態で常にtrueです。

第三に、Read()メソッドは、チェックとエラーハンドリングの実装が煩雑になりすぎたため、初期化文字列から異なるタイプのデータを読み取る専用のメソッドに分割されました。また、データの読み取りを補助するprivateメソッドも追加されました。データ読み取りメソッドでは、渡された初期化文字列が変更されることも記しておくべきです。データの次の部分を正常に読み取ると、その結果が返され、読み取られた部分は文字列から削除されます。

第四に、オブジェクトを初期化文字列に戻すメソッドについては、生成されたオブジェクトのパラメータとともに元の初期化文字列を記憶しておくことで、異なるクラスのオブジェクトでもほぼ同様に処理できます。そこで、オブジェクトのコンストラクタに初期化文字列を保存するため、基本クラスにm_paramsプロパティを追加しました。

以上の変更を反映した結果、CFactorableクラスの宣言は次のようになります。

//+------------------------------------------------------------------+
//| Base class of objects created from a string                      |
//+------------------------------------------------------------------+
class CFactorable {
private:
   bool              m_isValid;  // Is the object valid?

   // Clear empty characters from left and right in the initialization string 
   static void       Trim(string &p_params);

   // Find a matching closing bracket in the initialization string
   static int        FindCloseBracket(string &p_params, char closeBraket = ')');

   // Clear the initialization string with a check for the current object validity 
   bool              CheckTrimParams(string &p_params);

protected:
   string            m_params;   // Current object initialization string

   // Set the current object to the invalid state 
   void              SetInvalid(string function = NULL, string message = NULL);

public:
                     CFactorable() : m_isValid(true) {}  // Constructor
   bool              IsValid();                          // Is the object valid?

   // Convert object to string
   virtual string    operator~() = 0;

   // Does the initialization string start with the object definition?
   static bool       IsObject(string &p_params, const string className = "");

   // Does the initialization string start with defining an object of the desired class?
   static bool       IsObjectOf(string &p_params, const string className);

   // Read the object class name from the initialization string 
   static string     ReadClassName(string &p_params, bool p_removeClassName = true);

   // Read an object from the initialization string 
   string            ReadObject(string &p_params);
   
   // Read an array from the initialization string as a string 
   string            ReadArrayString(string &p_params);
   
   // Read a string from the initialization string
   string            ReadString(string &p_params);
   
   // Read a number from the initialization string as a string
   string            ReadNumber(string &p_params);
   
   // Read a real number from the initialization string
   double            ReadDouble(string &p_params);
   
   // Read an integer from the initialization string
   long              ReadLong(string &p_params);
};


クラスメソッドの具体的な実装についてはここでは触れませんが、すべての読み取りメソッドがほぼ同じ手順を踏んでいる点に注目する必要があります。まず、初期化文字列が空でなく、オブジェクトが有効であるかどうかを確認します。例えば、以前に初期化文字列からデータの一部を読み取る操作が失敗し、その結果、オブジェクトが無効になっている可能性があります。そのため、こうした確認は、明らかに不具合のあるオブジェクトに対して不必要な処理を実行することを防ぎます。

次に、初期化文字列が正しい型(オブジェクト、配列、文字列、数値)のデータを含んでいるかどうかを確認します。正しい型である場合、初期化文字列内でデータの終端を特定します。その位置の左側にある部分は戻り値として使用され、右側にある部分は初期化文字列に置き換えられます。

もし確認の段階で否定的な結果が出た場合、現在のオブジェクトを無効な状態に設定するメソッドを呼び出し、エラーが発生した箇所やその内容に関する情報をオブジェクトに渡します。

クラスのコードを現在のフォルダのFactorable.mqhファイルに保存します。


オブジェクトファクトリー

オブジェクトの初期化文字列には常にクラス名が含まれているため、オブジェクトの「ファクトリー」として機能するpublic関数やstaticメソッドを作成できます。このファクトリーでは、初期化文字列を渡すことで、指定されたクラスのオブジェクトを生成し、そのポインタを返します。

もちろん、特定のクラス名が固定されているオブジェクトに関しては、ファクトリーは不要です。標準的な方法として、new演算子を使用し、初期化文字列やオブジェクトのパラメータをコンストラクタに渡してオブジェクトを生成できます。しかし、異なるクラスのオブジェクトを生成する必要がある場合(例えば、異なる取引戦略のオブジェクトなど)、new演算子は役に立ちません。なぜなら、まず生成するオブジェクトのクラスを動的に定義する必要があるからです。この作業はファクトリーに任せましょう。具体的には、ファクトリーの唯一の静的メソッドであるCreate()に処理を委ねます。

//+------------------------------------------------------------------+
//| Object factory class                                             |
//+------------------------------------------------------------------+
class CVirtualFactory {
public:
   // Create an object from the initialization string
   static CFactorable* Create(string p_params) {
      // Read the object class name
      string className = CFactorable::ReadClassName(p_params);
      
      // Pointer to the object being created
      CFactorable* object = NULL;

      // Call the corresponding constructor  depending on the class name
      if(className == "CVirtualAdvisor") {
         object = new CVirtualAdvisor(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      }

      // If the object is not created or is created in the invalid state, report an error
      if(!object) {
         PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\nclass %s(%s)",
                     className, p_params);
      } else if(!object.IsValid()) {
         PrintFormat(__FUNCTION__
                     " | ERROR: Created object is invalid for:\nclass %s(%s)",
                     className, p_params);
         delete object; // Remove the invalid object
         object = NULL;
      }

      return object;
   }
};

このコードを現在のフォルダのVirtualFactory.mqhファイルに保存します。

将来ファクトリーを使いやすくするために、2つの便利なマクロを作成します。最初のマクロは,CVirtualFactory::Create()メソッドを呼び出して、初期化文字列からオブジェクトを生成し、その文字列自体を置き換えます。

// Create an object in the factory from a string
#define NEW(Params) CVirtualFactory::Create(Params)

2つ目のマクロは、CFactorableクラスの子孫クラスのコンストラクタからのみ実行されるよう設計されています。つまり、メインオブジェクトが作成され、そのコンストラクタ内で初期化文字列を使って、他の(ネストされた)オブジェクトを生成する場合に使用されます。このマクロは、作成されたオブジェクトのクラス名(Class)、作成されたオブジェクトへのポインタを受け取る変数名(Object)、初期化文字列(Params)の3つのパラメータを取ります。

まず、マクロは指定されたクラス名と変数名でポインタ変数を宣言し、それをNULLで初期化します。次に、メインオブジェクトが有効かどうかを確認します。有効であれば、NEW()マクロを呼び出して、ファクトリー内のオブジェクト生成メソッドを実行します。その後、生成されたポインタを正しいクラスにキャストします。このために、dynamic_cast<>()演算子を使用することで、ファクトリーが必要なクラスとは異なるオブジェクトを生成した場合の実行時エラーを防ぎます。この場合、オブジェクトポインタはNULLのままで、プログラムの実行は継続されます。次に、生成されたポインタが有効かどうかを確認し、無効な場合はメインオブジェクトを無効な状態に設定してエラーを報告し、メインオブジェクトのコンストラクタの実行を中断します。

マクロはこんな感じです。

// Creating a child object in the factory from a string with verification.
// Called only from the current object constructor.
// If the object is not created, the current object becomes invalid
// and exit from the constructor is performed
#define CREATE(Class, Object, Params)                                                                       \
    Class *Object = NULL;                                                                                   \
    if (IsValid()) {                                                                                        \
       Object = dynamic_cast<C*> (NEW(Params));                                                             \
       if(!Object) {                                                                                        \
          SetInvalid(__FUNCTION__, StringFormat("Expected Object of class %s() at line %d in Params:\n%s",  \
                                                #Class, __LINE__, Params));                                 \
          return;                                                                                           \
       }                                                                                                    \
    }                                                                                                       \

これらのマクロをFactorable.mqhファイルの先頭に追加します。


以前の基本クラスの修正

CFactorableクラスをこれまでのすべての基本クラス(СAdvisorСStrategyСVirtualStrategyGroup)のベースとして追加します。最初の2つには、これ以上変更は必要ないでしょう。

//+------------------------------------------------------------------+
//| EA base class                                                    |
//+------------------------------------------------------------------+
class CAdvisor : public CFactorable {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
   virtual void      Add(CStrategy *strategy);  // Method for adding a strategy
public:
                    ~CAdvisor();                // Destructor
   virtual void      Tick();                    // OnTick event handler
   virtual double    Tester() {
      return 0;
   }
};
//+------------------------------------------------------------------+
//| Base class of the trading strategy                               |
//+------------------------------------------------------------------+
class CStrategy : public CFactorable {
public:                     
   virtual void      Tick() = 0; // Handle OnTick events
};

СVirtualStrategyGroupは大きな変更を遂げました。もはや抽象基本クラスではなくなったため、初期化文字列からオブジェクトを生成するためのコンストラクタを実装する必要が出てきました。これにより、戦略の配列またはグループの配列を受け取る2つの別々のコンストラクタは削除されました。また、文字列への変換方法も変更されました。メソッドでは、保存された初期化文字列にクラス名をパラメータとともに追加するだけの簡単な実装になっています。一方、Scale()スケーリングメソッドに変更はありません。

//+------------------------------------------------------------------+
//| Class of trading strategies group(s)                             |
//+------------------------------------------------------------------+
class CVirtualStrategyGroup : public CFactorable {
protected:
   double            m_scale;                // Scaling factor
   void              Scale(double p_scale);  // Scaling the normalized balance
public:
                     CVirtualStrategyGroup(string p_params); // Constructor

   virtual string    operator~() override;      // Convert object to string

   CVirtualStrategy      *m_strategies[];       // Array of strategies
   CVirtualStrategyGroup *m_groups[];           // Array of strategy groups
};

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

// Read the initialization string of the array of strategies or groups
   string items = ReadArrayString(p_params);

// Until the string is empty
   while(items != NULL) {
      // Read the initialization string of one strategy or group object
      string itemParams = ReadObject(items);

      // If this is a group of strategies,
      if(IsObjectOf(itemParams, "CVirtualStrategyGroup")) {
         // Create a strategy group and add it to the groups array
         CREATE(CVirtualStrategyGroup, group, itemParams);
         APPEND(m_groups, group);
      } else {
         // Otherwise, create a strategy and add it to the array of strategies
         CREATE(CVirtualStrategy, strategy, itemParams);
         APPEND(m_strategies, strategy);
      }
   }

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

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CVirtualStrategyGroup::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}


    ... 

VirtualStrategyGroup.mqhファイルに加えた変更を現在のフォルダに保存します。

EAクラスの変更

前の記事で、CVirtualAdvisorEAクラスはInit()メソッドを受け取り、異なるEAコンストラクタのためのコードの重複を取り除くことになっていました。以前は、第1引数として1つの戦略を受け取るコンストラクタと、第1引数に戦略グループオブジェクトを受け取るコンストラクタが存在していました。戦略グループを受け入れるコンストラクタだけを残すことには、おそらく異論はないでしょう。1つの取引戦略インスタンスを使用する場合、単にその1つの戦略でグループを作成し、そのグループをEAコンストラクタに渡すだけです。このようにすることで、Init()メソッドや追加のコンストラクタは不要になります。その結果、初期化文字列からEAオブジェクトを生成するためのコンストラクタを1つだけ残す形となります。

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

public:
                     CVirtualAdvisor(string p_param);    // Constructor
                    ~CVirtualAdvisor();         // Destructor

   virtual string    operator~() override;      // Convert object to string

   ...
};

...

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

// Read the initialization string of the strategy group object
   string groupParams = ReadObject(p_params);

// Read the magic number
   ulong p_magic = ReadLong(p_params);

// Read the EA name
   string p_name = ReadString(p_params);

// Read the work flag only at the bar opening
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// If there are no read errors,
   if(IsValid()) {
      // Create a strategy group
      CREATE(CVirtualStrategyGroup, p_group, groupParams);

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

      m_name = StringFormat("%s-%d%s.csv",
                            (p_name != "" ? p_name : "Expert"),
                            p_magic,
                            (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                           );

      // Save the work (test) start time
      m_fromDate = TimeCurrent();

      // Reset the last save time
      m_lastSaveTime = 0;

      // Add the contents of the group to the EA
      Add(p_group);

      // Remove the group object
      delete p_group;
   }
}

コンストラクタでは、まず初期化文字列からすべてのデータを読み込みます。この段階で何らかの不一致が検出されると、現在作成中のEAオブジェクトは無効な状態となります。問題がなければ、コンストラクタは戦略グループを作成し、その戦略を戦略配列に追加し、初期化文字列から読み込んだデータに基づいて残りのプロパティを設定します。

しかし、現在の実装では、レシーバーオブジェクトとインターフェイスオブジェクトを生成する前に妥当性チェックが行われるため、これらのオブジェクトが生成されない可能性があります。したがって、デストラクタでは、これらのオブジェクトを削除する前に、そのオブジェクトへのポインタが有効かどうかを確認する必要があります。

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   if(!!m_receiver)  delete m_receiver;         // Remove the recipient
   if(!!m_interface) delete m_interface;        // Remove the interface
   DestroyNewBar();           // Remove the new bar tracking objects 
}

現在のフォルダのVirtualAdvisor.mqhファイルに変更を保存します。

取引戦略クラスの変更

CSimpleVolumesStrategy戦略クラスでは、個別のパラメータを受け取るコンストラクタを削除し、CFactorableクラスのメソッドを用いて初期化文字列を受け入れるコンストラクタのコードに書き換えました。

コンストラクタでは、まずm_paramsプロパティに保存された初期化文字列からパラメータを読み込みます。読み込み中に戦略オブジェクトが無効になるようなエラーが発生しなければ、オブジェクトを初期化するための基本的なアクションを実行します。具体的には、仮想ポジションの配列を設定し、指標を初期化し、分足時間枠で新しいバーのイベントハンドラを登録します。

また、オブジェクトを文字列に変換するメソッドも変更されました。パラメータから文字列を形成する代わりに、クラス名と保存された初期化文字列を単純に連結する形になりました。これは、前述の2つのクラスで採用された方法と同様です。 

//+------------------------------------------------------------------+
//| Trading strategy using tick volumes                              |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
   ...

public:
   //--- Public methods
                     CSimpleVolumesStrategy(string p_params); // Constructor

   virtual string    operator~() override;         // Convert object to string

   virtual void      Tick() override;              // OnTick event handler
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(string p_params) {
// Save the initialization string
   m_params = p_params;
   
// Read the parameters from the initialization string
   m_symbol = ReadString(p_params);
   m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params);
   m_signalPeriod = (int) ReadLong(p_params);
   m_signalDeviation = ReadDouble(p_params);
   m_signaAddlDeviation = ReadDouble(p_params);
   m_openDistance = (int) ReadLong(p_params);
   m_stopLevel = ReadDouble(p_params);
   m_takeLevel = ReadDouble(p_params);
   m_ordersExpiration = (int) ReadLong(p_params);
   m_maxCountOfOrders = (int) ReadLong(p_params);
   m_fittedBalance = ReadDouble(p_params);

// If there are no read errors,
   if(IsValid()) {
      // Request the required number of virtual positions
      CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);

      // Load the indicator to get tick volumes
      m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);

      // If the indicator is loaded successfully
      if(m_iVolumesHandle != INVALID_HANDLE) {

         // Set the size of the tick volume receiving array and the required addressing
         ArrayResize(m_volumes, m_signalPeriod);
         ArraySetAsSeries(m_volumes, true);

         // Register the event handler for a new bar on the minimum timeframe
         IsNewBar(m_symbol, PERIOD_M1);
      } else {
         // Otherwise, set the object state to invalid
         SetInvalid(__FUNCTION__, "Can't load iVolumes()");
      }
   }
}

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CSimpleVolumesStrategy::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}

また,Save()メソッドとLoad()メソッドもこのクラスから削除しました。というのも、親クラスであるCVirtualStrategyに実装されているメソッドで、このクラスに割り当てられていたタスクを十分に実行できることが判明したためです。

現在のフォルダのCSimpleVolumesStrategy.mqhファイルに変更を保存します。


取引戦略の単一インスタンスのEA

単一の取引戦略インスタンスのパラメータを最適化するためには、OnInit()初期化関数のみを変更すれば十分です。この関数では、EA入力から売買戦略オブジェクトを初期化するための文字列を作成し、それを使ってEAオブジェクトを初期化文字列に代入します。

初期化文字列からデータを読み取るメソッドを実装したことにより、文字列内にスペースや改行を自由に追加できるようになりました。これにより、ログへの出力やデータベースへのエントリの際に、初期化文字列はおおよそ次のようなフォーマットで表示されます。

Core 1  2023.01.01 00:00:00   OnInit | Expert Params:
Core 1  2023.01.01 00:00:00   class CVirtualAdvisor(
Core 1  2023.01.01 00:00:00       class CVirtualStrategyGroup(
Core 1  2023.01.01 00:00:00          [
Core 1  2023.01.01 00:00:00           class CSimpleVolumesStrategy("EURGBP",16385,17,0.70,0.90,150,10000.00,85.00,10000,3,0.00)
Core 1  2023.01.01 00:00:00          ],1
Core 1  2023.01.01 00:00:00       ),
Core 1  2023.01.01 00:00:00       ,27181,SimpleVolumesSingle,1
Core 1  2023.01.01 00:00:00   )

OnDeinit()関数では、EAオブジェクトを削除する前に、EAオブジェクトへのポインタが正しいことを確認する必要があります。理論的には、誤った初期化文字列があると、ファクトリーによってEAオブジェクトが早期に削除される可能性があるからです。

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   CMoney::FixedBalance(fixedBalance_);

// Prepare the initialization string for a single strategy instance
   string strategyParams = StringFormat(
                              "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d,%.2f)",
                              symbol_, timeframe_,
                              signalPeriod_, signalDeviation_, signaAddlDeviation_,
                              openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                              maxCountOfOrders_, 0
                           );

// Prepare the initialization string for an EA with a group of a single strategy
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],1\n"
                            "    ),\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            strategyParams, magic_, "SimpleVolumesSingle", true
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

...

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if(!!expert) delete expert;
}

得られたコードを現在のフォルダのSimpleVolumesExpertSingle.mq5に保存します。


複数インスタンスのEA

複数の取引戦略インスタンスを使用してEAをテストするには、第8部で負荷テストに使用したEAを活用します。OnInit()関数内で、EAの作成メカニズムをこの記事で開発したものに置き換えます。具体的には、まずCSVファイルから戦略のパラメータを読み込み、それに基づいて戦略配列の初期化文字列を補完します。次に、この初期化文字列を使用して戦略グループおよびEA全体の初期化文字列を生成します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Load strategy parameter sets
   int totalParams = LoadParams(fileName_, strategyParams);

// If nothing is loaded, report an error
   if(totalParams == 0) {
      PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  fileName_);
      return(INIT_PARAMETERS_INCORRECT);
   }

// Report an error if
   if(count_ < 1) { // number of instances is less than 1
      return INIT_PARAMETERS_INCORRECT;
   }

   ArrayResize(strategyParams, count_);

// Set parameters in the money management class
   CMoney::DepoPart(expectedDrawdown_ / 10.0);
   CMoney::FixedBalance(fixedBalance_);

// Prepare the initialization string for the array of strategy instances
   string strategiesParams;
   FOREACH(strategyParams, strategiesParams += StringFormat(" class CSimpleVolumesStrategy(%s),\n      ",
                                                            strategyParams[i % totalParams]));

// Prepare the initialization string for an EA with the strategy group
   string expertParams = StringFormat("class CVirtualAdvisor(\n"
                                      "   class CVirtualStrategyGroup(\n"
                                      "      [\n"
                                      "      %s],\n"
                                      "      %.2f\n"
                                      "   ),\n"
                                      "   %d,%s,%d\n"
                                      ")",
                                      strategiesParams, scale_,
                                      magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_);
   
// Create an EA handling virtual positions
   expert = NEW(expertParams);

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

以前のEAと同様に、OnDeinit()関数は、EAオブジェクトを削除する前に、EAオブジェクトへのポインタの有効性をチェックする機能を受け取ります。

得られたコードを現在のフォルダのBenchmarkInstancesExpert.mq5ファイルに保存します。


機能性のチェック

第8回BenchmarkInstancesExpert.mq5 EAと、今回の記事で紹介する同じEAを使用してみましょう。これらを同じパラメータで起動します。Params_SV_EURGBP_H1.csvファイルから256インスタンスの取引戦略を読み込み、テスト期間は2022年とします。


図2:2つのEAバージョンのテスト結果は同じです。


結果は同じでした。そのため、画像では1つのインスタンスとして表示されています。これは非常に良いことです。


結論

初期化文字列を使用して、必要なオブジェクトをすべて作成する機能を無事に実現しました。これまではほぼ手作業でこれらの行を生成してきましたが、将来的にはデータベースから直接読み取ることができるようになるでしょう。これが、既存のコードを改訂し始めた理由です。

オブジェクトの作成方法が異なるEAをテストした結果が同じであったこと、つまり同じ取引戦略インスタンスのセットを使用したことで、変更が正当化されました。 

次のステップとして、最初の計画段階における自動化、すなわち取引戦略の単一インスタンスのパラメータを選択するためのEA最適化プロセスを順次起動する作業に進むことができます。この点については、今後の記事で詳しく紹介する予定です。

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



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

リプレイシステムの開発(第46回):Chart Tradeプロジェクト(V) リプレイシステムの開発(第46回):Chart Tradeプロジェクト(V)
アプリケーションを動作させるために必要なファイルを探すのに時間を浪費していませんか。すべてを実行ファイルに含めてみてはどうでしょうか。そうすれば、ファイルを探す必要がなくなります。多くの人がこのような配布・保管方法を採用していることは知っていますが、少なくとも、実行ファイルの配布や保管に関してはもっと適切な方法があります。ここで紹介する方法は、MQL5だけでなく、MetaTrader 5そのものを優れたアシスタントとして使うことができるので、非常に便利です。しかも、理解するのはそれほど難しくありません。
ニューラルネットワークが簡単に(第88回):Time-series Dense Encoder (TiDE) ニューラルネットワークが簡単に(第88回):Time-series Dense Encoder (TiDE)
研究者たちは、より正確な予測を得るために、しばしばモデルを複雑化します。しかし、その結果として、モデルの訓練やメンテナンスにかかるコストも増加します。この増大したコストは常に正当化されるのでしょうか。本記事では、シンプルで高速な線形モデルの特性を活かし、複雑なアーキテクチャを持つ最新モデルに匹敵する結果を示すアルゴリズムを紹介します。
亀甲進化アルゴリズム(TSEA) 亀甲進化アルゴリズム(TSEA)
これは、亀の甲羅の進化にインスパイアされたユニークな最適化アルゴリズムです。TSEAアルゴリズムは、問題に対する最適解を表す構造化された皮膚領域が徐々に形成される様子をエミュレートします。最良の解は「硬く」なり、外側に近い位置に配置され、成功しなかった解は「柔らかい」ままで内側に留まります。このアルゴリズムは、質と距離に基づく解のクラスタリングを利用し、成功率の低い選択肢を保持しながら、柔軟性と適応性を提供します。
SMAとEMAを使った自動最適化された利益確定と指標パラメータの例 SMAとEMAを使った自動最適化された利益確定と指標パラメータの例
この記事では、機械学習とテクニカル分析を組み合わせた、FX取引向けの高度なEAを紹介します。アップル株取引を中心に、適応的な最適化やリスク管理、複数の取引戦略を活用しています。バックテストでは、収益性が高い一方で、大きなドローダウンを伴う結果が得られており、さらなる改良の余地が示唆されています。