English Русский Deutsch
preview
多通貨エキスパートアドバイザーの開発(第3回):アーキテクチャの改訂

多通貨エキスパートアドバイザーの開発(第3回):アーキテクチャの改訂

MetaTrader 5トレーディング | 20 6月 2024, 10:56
167 0
Yuriy Bykov
Yuriy Bykov

はじめに

前回の記事では、様々な取引戦略と同時に機能する多通貨EAの開発に着手しました。2番目の記事で提示されたソリューションは、最初の記事で提示されたものとはすでに大きく異なっています。これは、私たちがまだ最良の選択肢を模索している段階であることを示しています。

開発されたシステムを改善する方法を理解するために、実装の細かい部分から抽象化して全体を見てみましょう。そのために、短いとはいえ、システムの顕著な進化をたどってみましょう。


最初の動作モード

EAオブジェクト(CAdvisorクラスまたはその子孫)を割り当てましたが、これは取引戦略オブジェクト(CStrategyクラスまたはその子孫)のアグリゲーターです。EA操作の最初に、OnInit()ハンドラで以下のことが起こります。

  • EAオブジェクトが作成される
  • 取引戦略のオブジェクトが作成され、EAの取引戦略の配列に追加される

OnTick()イベントハンドラでは、次のことが起こります。

  • CAdvisor::Tick()メソッドがEAオブジェクトのために呼ばれる
  • このメソッドは、すべての戦略を反復処理し、それらのCStrategy::Tick()メソッドを呼び出す
  • CStrategy::Tick()内の戦略は、マーケットポジションのオープンとクローズに必要なすべての操作を実行する

これを図式化するとこうなります。



図1:最初の記事からの動作モード

図1:最初の記事からの動作モード

このモードの利点は、ある取引戦略に従ったEAのソースコードがあれば、比較的簡単な操作で他の取引戦略のインスタンスとEAを連動させることができることでした。

しかし、主な欠点がすぐに浮かび上がりました。複数の戦略を組み合わせる場合、戦略の各インスタンスで建てるポジションのサイズをある程度小さくしなければなりません。このため、一部の、あるいはすべての戦略インスタンスが取引から完全に排除される可能性があります。並行作業に含める戦略インスタンスが多ければ多いほど、あるいは初期預託金が少なければ少ないほど、そのような結果になる可能性は高くなります。

また、複数の戦略インスタンスが連動する場合、同じ大きさのポジションが逆に建てられる事態が発生しました。総ボリュームでは、これはポジションがないのと同じですが、反対側のポジションにはスワップが発生し続けました。


2番目の動作モード

その欠点をなくすため、マーケットポジションを持つすべての動作を別の場所に移し、取引戦略が直接マーケットポジションを建てる機能をなくすことにしました。このため、既成の戦略の練り直しがやや複雑になりますが、最初のモードの主な欠点がなくなることで補えるので、それほど大きな損失ではありません。

動作モードに 2 つの新しいエンティティが表示されます。仮想ポジション(CVirtualOrderクラス)と、戦略からのボリュームの受信者(CReceiverクラスとその子孫)です。

EA動作の最初に、OnInit()ハンドラで以下のことが起こります。

  • 受信者オブジェクトが作成されます。
  • EAオブジェクトが作成され、作成された受信者が渡される
  • 取引戦略のオブジェクトが作成され、EAの取引戦略の配列に追加される
  • 各戦略は、必要な数の仮想ポジションオブジェクトの配列を独自に作成する

OnTick()イベントハンドラでは、次のことが起こります。

  • CAdvisor::Tick()メソッドが EA オブジェクトのために呼ばれる
  • このメソッドは、すべての戦略を反復処理し、それらのCStrategy::Tick()メソッドを呼び出す
  • CStrategy::Tick()内の戦略は、仮想ポジションのオープンとクローズに必要なすべての操作を実行する未決済の仮想ポジションの構成比率の変更に関連する何らかのイベントが発生した場合、戦略はフラグを設定することでその変更を記憶します。
  • 少なくとも1つの戦略が変更フラグをセットしていれば、受信者はマーケットポジションのオープンボリュームを調整するメソッドを起動します。調整が成功すると、すべての戦略の変更フラグがリセットされます。

これを図式化するとこうなります。

図2:2番目の記事の動作モード

図2:2番目の記事の動作モード

この操作モードでは、ある戦略インスタンスが未決済の市場ポジションの大きさに何ら影響を与えないという事実に直面することはなくなります。逆に、非常に小さな仮想ボリュームを建てるインスタンスでさえ、マーケットポジションの最小許容ボリュームを超えて、複数の戦略インスタンスからの仮想ポジションの総ボリュームを圧倒する、まさにそのドロップになる可能性があります。この場合、真のマーケットポジションが開かれることになります。

その過程で、スワップの節約、残高への負荷の軽減、ドローダウンの減少、取引の品質評価(シャープレシオ、プロフィットファクター)の向上など、嬉しい変化もありました。

2つ目のモードをテストしているとき、私は次のことに気づきました。

  • 各戦略はまず、すでに建てられている仮想ポジションを処理し、トリガーとなるStopLossとTakeProfitのレベルを決定します。いずれかのレベルに達した場合、その仮想ポジションはクローズされます。したがって、この処理は直ちにCVirtualOrderクラスのメソッドに移されました。しかし、この解答はまだ一般化としては不十分なようです。
  • 新しい必須エンティティを追加することで、基本クラスの構成を拡張しました。仮想ポジションを扱いたくない場合でも、単に「空の」オブジェクトを渡すだけで、このような基本クラスを使用することができます。例えば、空のスタブメソッドだけを含むCReceiverクラスオブジェクトを作成することができます。しかし、これも一時的なソリューションであり、手直しが必要なようです。
  • CStrategy基本クラスに新しいメソッドと、仮想ポジションの構成の変化を基本クラスが追跡するためのプロパティを追加し、CAdvisor基本クラスでこれらのメソッドを使用することに波及しました。繰り返しになりますが、これは可能性を狭め、基本クラスに特殊すぎる実装を押し付ける一歩のように見えます。
  • 開発されたCVolumeReceiver受信者クラスは、各戦略の仮想ボリュームのデータを必要とするため、CStrategy基本クラスは、仮想ポジションの総ボリュームを返すVolume()メソッドを受信します。しかし、そうすることで、取引戦略の1つのインスタンス内で複数の銘柄の仮想ポジションを建てる機能を遮断しています。この場合、総ボリュームは意味を失います。このソリューションは、単一銘柄戦略のテストには適していますが、それ以上のものではありません。
  • EA戦略へのポインタをCReceiverクラスに格納するために配列を使用し、受信者が戦略のオープン仮想ボリュームを見つけるためにそれを使用できるようにしました。そのため、EAと受信機で戦略配列を埋めるコードが重複してしまいました。
  • 戦略の配列に受信者を追加する際、戦略はその銘柄を尋ねられ、使用される銘柄の配列に追加されます。この機能をCVolumeReceiverクラスで使用しました。受信者は、銘柄配列に追加された銘柄のみを扱います。この動作の結果として生じる制限についてはすでに述べました。
リストアップされた欠点の分析とコメントでの議論に基づき、以下の変更を加えます。
  • CStrategy基本クラスとCAdvisor基本クラスをできるだけ整頓します。仮想取引を使用したEAの開発ブランチを作成するために、カスタム派生クラスであるCVirtualStrategyCVirtualAdvisorを書きます。これらのクラスは今後、特定の戦略やEAの親クラスとなります。
  • 仮想ポジションのクラスを拡大する時です。各仮想ポジションは、仮想ボリュームを市場にもたらす役割を担う受信者オブジェクトへのポインタと、仮想ポジションのオープン/クローズを決定する取引戦略オブジェクトへのポインタを受け取る必要があります。これにより、仮想ポジションのオープン/クローズの操作について、利害関係者に通知できるようになります。
  • 戦略インスタンスに属する複数の配列に分散させるのではなく、すべての仮想ポジションのストレージを1つの配列に移しましょう。各戦略のインスタンスは、その操作のためにこの配列からいくつかの要素を要求します。共通配列の所有者は、ボリュームの受け手となります。
  • 1つのEAに受信者は1つしかありません。したがって、シングルトンとして実装します。その唯一のインスタンスは、必要なすべての場所で利用できます。この実装をCVirtualReceiver派生クラスとして正式化します。
  • 新しいエンティティである銘柄受信者(CVirtualSymbolReceiverクラス)の配列を受信者に追加します。各銘柄レシーバーは、その銘柄の仮想ポジションでのみ動作し、銘柄受信者を開くと自動的に銘柄受信者に装着され、閉じると解除されます。
このすべてを実行してみましょう。


基本クラスの整理

CStrategyCAdvisorの基本クラスに必要なものだけを残しておきましょう。CStartegyの場合、OnTickイベントを処理するメソッドだけを残すと、コードは以下のように簡潔になります。

//+------------------------------------------------------------------+
//| Base class of the trading strategy                               |
//+------------------------------------------------------------------+
class CStrategy {
public:
   virtual void      Tick() = 0; // Handle OnTick events
};

それ以外はすべて、クラスの子孫にあります。

CAdvisor基本クラスには、小さなファイルMacros.mqhをインクルードします。このファイルには、通常の配列の操作に便利なマクロがいくつか含まれています。

  • APPEND(A, V):Aの配列の最後に Vの要素を追加する
  • FIND(A, V, I):Aに等しい配列要素をI変数に書き込む。要素が見つからなければ、I変数に-1が格納される
  • ADD(A, V):A配列の末尾にV要素を追加する
  • FOREACH(A, D):A配列のインデックス(インデックスはローカル変数iにある)をループし、ボディ内でDアクションを実行する
  • REMOVE_AT(A, I):インデックスIの位置にあるA配列から要素を取り除き、後続の要素をシフトして配列サイズを縮小する
  • REMOVE(A, V)- Aの配列からVに等しい要素を削除する

// Useful macros for array operations
#ifndef __MACROS_INCLUDE__
#define APPEND(A, V)    A[ArrayResize(A, ArraySize(A) + 1) - 1] = V;
#define FIND(A, V, I)   { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } }
#define ADD(A, V)       { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } }
#define FOREACH(A, D)   { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
#define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);}
#define REMOVE(A, V)    { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) }
#define __MACROS_INCLUDE__
#endif
//+------------------------------------------------------------------+

これらのマクロは、他のファイルでも使用されます。このようにすることで、コードをよりコンパクトに、しかし読みやすくし、追加関数の呼び出しを避けることができるからです。

CAdvisorクラスから、受信者が遭遇したすべての場所を削除し、OnTickイベント処理メソッドに対応する戦略ハンドラの呼び出しだけを残します。コードは以下のようになります。

#include "Macros.mqh"
#include "Strategy.mqh"

//+------------------------------------------------------------------+
//| EA base class                                                    |
//+------------------------------------------------------------------+
class CAdvisor {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
public:
                    ~CAdvisor();                // Destructor
   virtual void      Tick();                    // OnTick event handler
   virtual void      Add(CStrategy *strategy);  // Method for adding a strategy
};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CAdvisor::~CAdvisor() {
// Delete all strategy objects
   FOREACH(m_strategies, delete m_strategies[i]);
}

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CAdvisor::Tick(void) {
// Call OnTick handling for all strategies
   FOREACH(m_strategies, m_strategies[i].Tick());
}

//+------------------------------------------------------------------+
//| Strategy adding method                                           |
//+------------------------------------------------------------------+
void CAdvisor::Add(CStrategy *strategy) {
   APPEND(m_strategies, strategy);  // Add the strategy to the end of the array
}
//+------------------------------------------------------------------+

これらのクラスは、現在のフォルダ内のStrategy.mqhAdvisor.mqhファイルに残ります。

それでは、派生した戦略とEAクラスに必要なコードを移しましょう。これは仮想ポジションで機能するはずです。

CStrategyから継承したCVirtualStrategyクラスを作成します。次のフィールドとメソッドを追加してみましょう。

  • 仮想ポジション(注文)の配列
  • ポジションと注文の総数
  • 未決済の仮想ポジションと注文を数えるメソッド
  • 仮想ポジション(注文)のオープン/クローズを処理するメソッド
今のところ、仮想ポジションのオープン/クローズを処理するメソッドは、単に仮想ポジションを再計算するメソッドを呼び出し、m_ordersTotalフィールド値を更新します。まだ他に何もする必要はありません。おそらく、それは後でやることになるでしょう。従って、今のところ、これらのメソッドは、仮想ポジションのカウントメソッドとは別に作られています。

#include "Strategy.mqh"
#include "VirtualOrder.mqh"

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
protected:
   CVirtualOrder     *m_orders[];   // Array of virtual positions (orders)
   int               m_ordersTotal; // Total number of open positions and orders

   virtual void      CountOrders(); // Calculate the number of open positions and orders

public:
   virtual void      OnOpen();      // Event handler for opening a virtual position (order)
   virtual void      OnClose();     // Event handler for closing a virtual position (order)
};

//+------------------------------------------------------------------+
//| Counting open virtual positions and orders                       |
//+------------------------------------------------------------------+
void CVirtualStrategy::CountOrders() {
   m_ordersTotal = 0;
   FOREACH(m_orders, if(m_orders[i].IsOpen()) { m_ordersTotal += 1; })
}

//+------------------------------------------------------------------+
//| Event handler for opening a virtual position (order)             |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnOpen() {
   CountOrders();
}

//+------------------------------------------------------------------+
//| Event handler for closing a virtual position (order)             |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnClose() {
   CountOrders();
}

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

CAdvisorの基本クラスから受信者の作業を取り除いたので、それは新しいCVirtualAdvisorの子クラスに移されるべきです。このクラスでは、ボリュームの受信者のオブジェクトへのポインタを格納するためのm_receiverフィールドを追加します。

コンストラクタの中で、このフィールドは、CVirtualReceiver::Instance()静的メソッドが呼ばれたときに正確に生成される、唯一の受信者オブジェクトへのポインタで初期化されます。デストラクタは、オブジェクトが適切に削除されるようにします。

また、OnTickイベントハンドラに新しいアクションを追加します。戦略でこのイベントのハンドラを起動する前に、まず受信者でこのイベントのハンドラを起動します。戦略がイベントを処理した後、オープンボリュームを調整する受信側のメソッドを起動します。受信者がすべての仮想ポジションの所有者となった場合、それ自身が変更の有無を判断することができます。したがって、取引戦略クラスには変更を追跡する実装がないので、基本戦略クラスからだけでなく、完全に削除します。

#include "Advisor.mqh"
#include "VirtualReceiver.mqh"

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   CVirtualReceiver  *m_receiver; // Receiver object that brings positions to the market

public:
                     CVirtualAdvisor(ulong p_magic = 1); // Constructor
                    ~CVirtualAdvisor();                  // Destructor
   virtual void      Tick() override;                    // OnTick event handler

};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1) :
// Initialize the receiver with a static receiver
   m_receiver(CVirtualReceiver::Instance(p_magic)) {};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   delete m_receiver;         // Remove the recipient
}

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Receiver handles virtual positions
   m_receiver.Tick();
   
// Start handling in strategies
   CAdvisor::Tick();
   
// Adjusting market volumes
   m_receiver.Correct();
}
//+------------------------------------------------------------------+

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


仮想ポジションのクラスの拡大

仮想ポジションクラスは、m_receiverm_strategyオブジェクトへのポインタを受け取ります。これらのフィールドの値はコンストラクタのパラメータを通して渡さなければならないので、同様に変更します。また、仮想ポジションのprivateプロパティ用のゲッター、Id()Symbol()も追加しました。クラスの説明に追加されたコードを表示します。

//+------------------------------------------------------------------+
//| Class of virtual orders and positions                            |
//+------------------------------------------------------------------+
class CVirtualOrder {
private:
//--- Static fields...
   
//--- Related recipient objects and strategies
   CVirtualReceiver  *m_receiver;
   CVirtualStrategy  *m_strategy;

//--- Order (position) properties ...
   
//--- Closed order (position) properties ...
   
//--- Private methods
   
public:
                     CVirtualOrder(
      CVirtualReceiver *p_receiver,
      CVirtualStrategy *p_strategy
   );                                  // Constructor

//--- Methods for checking the position (order) status ...
   

//--- Methods for receiving position (order) properties ...
   ulong             Id() {            // ID
      return m_id;
   }
   string            Symbol() {        // Symbol
      return m_symbol;
   }

//--- Methods for handling positions (orders) ...
  
};

コンストラクタの実装では、コンストラクタのパラメータから新しいフィールドの値を設定するために、初期化リストに2行を追加しただけです。

CVirtualOrder::CVirtualOrder(CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy) :
// Initialization list
   m_id(++s_count),  // New ID = object counter + 1
   m_receiver(p_receiver),
   m_strategy(p_strategy),
   ...,
   m_point(0) {
}

受信者と戦略の通知は、仮想ポジションがオープンまたはクローズされたときにのみおこなわれるべきです。これはOpen()メソッドとClose()メソッドでしか起こりません。少しコードを追加してみましょう。

//+------------------------------------------------------------------+
//| Open a virtual position                                          |
//+------------------------------------------------------------------+
bool CVirtualOrder::Open(...) {
   // If the position is already open, then do nothing ...

   if(s_symbolInfo.Name(symbol)) {  // Select the desired symbol
      // Update information about current prices ...

      // Initialize position properties ...
  
      // Depending on the direction, set the opening price, as well as the SL and TP levels ...
            
      // Notify the recipient and the strategy that the position (order) is open
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();

      ...

      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Close a position                                                 |
//+------------------------------------------------------------------+
void CVirtualOrder::Close() {
   if(IsOpen()) { // If the position is open
      ...
      // Define the closure reason to be displayed in the log ...
     
      // Save the close price depending on the type ...
    
      // Notify the recipient and the strategy that the position (order) is open
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
   }
}

OnOpen()OnClose()の受信ハンドラに、現在の仮想ポジションオブジェクトへのポインタを渡します。戦略ハンドラではまだその必要性がないため、パラメータなしで実装されています。

このコードは、カレントフォルダのVirtualOrder.mqhという同名のファイルに残ります。


新しい受信クラスの導入

それでは、与えられたクラスのインスタンスの一意性を保証するために、CVirtualReceiver受信クラスを実装しましょう。そのために、シングルトンと呼ばれる標準的なデザインパターンを使用します。以下をおこなう必要があります。

  • クラスのコンストラクタを非公開にする
  • クラスオブジェクトへのポインタを格納する静的クラスフィールドを追加する
  • このクラスのインスタンスを作成するか、すでに存在するインスタンスを返す静的メソッドを追加する

//+------------------------------------------------------------------+
//| Class for converting open volumes to market positions (receiver) |
//+------------------------------------------------------------------+
class CVirtualReceiver : public CReceiver {
protected:
// Static pointer to a single class instance
   static   CVirtualReceiver *s_instance;

   ...

   CVirtualReceiver(ulong p_magic = 0);   // Private constructor

public:
//--- Static methods
   static
   CVirtualReceiver  *Instance(ulong p_magic = 0);    // Singleton - creating and getting a single instance

   ...
};

// Initializing a static pointer to a single class instance
CVirtualReceiver *CVirtualReceiver::s_instance = NULL;


//+------------------------------------------------------------------+
//| Singleton - creating and getting a single instance               |
//+------------------------------------------------------------------+
CVirtualReceiver* CVirtualReceiver::Instance(ulong p_magic = 0) {
   if(!s_instance) {
      s_instance = new CVirtualReceiver(p_magic);
   }
   return s_instance;
}

次に、すべての仮想ポジションを格納するためのm_orders配列をクラスに追加します。各戦略インスタンスは、一定数の仮想ポジションを受信者に要求します。これをおこなうには、Get()静的メソッドを追加します。このメソッドは、必要な数の仮想ポジションオブジェクトを作成し、そのポインタを受信者配列と戦略仮想ポジション配列に追加します。

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualOrder     *m_orders[];         // Array of virtual positions
   
   ...

public:
//--- Static methods
   ...
   static void       Get(CVirtualStrategy *strategy,
                         CVirtualOrder *&orders[],
                         int n); // Allocate the necessary amount of virtual positions to the strategy
   ...
};

...

//+------------------------------------------------------------------+
//| Allocate the necessary amount of virtual positions to strategy   |
//+------------------------------------------------------------------+
static void CVirtualReceiver::Get(CVirtualStrategy *strategy,   // Strategy
                                  CVirtualOrder *&orders[],     // Array of strategy positions
                                  int n                         // Required number
                                 ) {
   CVirtualReceiver *self = Instance();   // Receiver singleton
   ArrayResize(orders, n);                // Expand the array of virtual positions
   FOREACH(orders,
           orders[i] = new CVirtualOrder(self, strategy); // Fill the array with new objects
           APPEND(self.m_orders, orders[i])) // Register the created virtual position
   ...
}

それでは、銘柄受信オブジェクトへのポインタの配列(CVirtualSymbolReceiverクラス)をクラスに追加しましょう。このクラスはまだ作成されていませんが、このクラスが何をすべきかはすでにわかっています。単一シンボルの仮想ボリュームに応じて市場ポジションを直接オープン/クローズすることです。したがって、銘柄受信者のオブジェクトの数は、EAで使用されるさまざまな銘柄の数に等しいと言えます。このクラスはCReceiverの子孫とし、Correct()メソッドを持つことにします。これは主な有用な作業を実行します。必要な補助メソッドも追加します。

これは後回しにして、CVirtualReceiverクラスに戻ってCorrect()メソッドの仮想オーバーライドを追加しましょう。

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualSymbolReceiver *m_symbolReceivers[];       // Array of recipients for individual symbols

public:
   ...
//--- Public methods
   virtual bool      Correct() override;              // Adjustment of open volumes
};

Correct()メソッドの実装は非常にシンプルになりました。主な作業を階層の下位レベルに移すためです。今のところ、すべての銘柄受信者をループして、そのCorrect()メソッドを呼び出すだけで十分です。

不要な呼び出しの回数を減らすために、IsTradeAllowed()メソッドを追加することで、取引が一般的に許可されていることを事前に確認し、質問に答えるようにします。m_isChangedクラスフィールドも追加します。これは、仮想ポジションの変更のフラグとして機能するはずです。また、調整を依頼する前に確認します。

class CVirtualReceiver : public CReceiver {
   ...
   bool              m_isChanged;         // Are there any changes in open positions?
   ...
   bool              IsTradeAllowed();    // Is trading available?

public:
   ...

   virtual bool      Correct() override;  // Adjustment of open volumes
};
//+------------------------------------------------------------------+
//| Adjust open volumes                                              |
//+------------------------------------------------------------------+
bool CVirtualReceiver::Correct() {
   bool res = true;
   if(m_isChanged && IsTradeAllowed()) {
      // If there are changes, then we call the adjustment of the recipients of individual symbols
      FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct());
      m_isChanged = !res;
   }
   return res;
}

IsTradeAllowed()メソッドで、端末と取引口座のステータスを確認し、実際の取引が可能かどうかを判断します。

//+------------------------------------------------------------------+
//| Is trading available?                                            |
//+------------------------------------------------------------------+
bool CVirtualReceiver::IsTradeAllowed() {
   return (true
           && MQLInfoInteger(MQL_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
           && AccountInfoInteger(ACCOUNT_TRADE_EXPERT)
           && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_CONNECTED)
          );
}

Correct()メソッドで変更フラグを適用しました。ボリューム調整が成功した場合、フラグはリセットされました。しかし、このフラグはどこで設定するべきなのでしょうか。明らかに、これは仮想ポジションがオープンまたはクローズされた場合に発生するはずです。CVirtualOrderクラスでは,特に,CVirtualReceiverクラスにはまだ存在しない OnOpen()とOnClose()メソッドの呼び出しを,open/closeメソッドに追加しました。その中で変更フラグを設定します。

さらに、これらのハンドラの変更について、希望する銘柄の受信者に通知しなければなりません。ある銘柄の最初の仮想ポジションを建てるとき、対応する銘柄受信者はまだ存在しないので、作成して通知する必要があります。その後、指定された銘柄の仮想ポジションをオープン/クローズする操作では、すでに対応する銘柄の受信者が存在するので、それを通知するだけでよいのです。

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Public methods
   void              OnOpen(CVirtualOrder *p_order);  // Handle virtual position opening
   void              OnClose(CVirtualOrder *p_order); // Handle virtual position closing
   ...
};

//+------------------------------------------------------------------+
//| Handle opening a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();      // Define position symbol
   CVirtualSymbolReceiver *symbolReceiver;
   int i;
   FIND(m_symbolReceivers, symbol, i);    // Search for the symbol recipient

   if(i == -1) {
      // If not found, then create a new recipient for the symbol
      symbolReceiver = new CVirtualSymbolReceiver(m_magic, symbol);
      // and add it to the array of symbol recipients 
      APPEND(m_symbolReceivers, symbolReceiver);
   } else {
      // If found, then take it
      symbolReceiver = m_symbolReceivers[i];
   }
   
   symbolReceiver.Open(p_order); // Notify the symbol recipient about the new position
   m_isChanged = true;           // Remember that there are changes
}

//+------------------------------------------------------------------+
//| Handle closing a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();   // Define position symbol
   int i;
   FIND(m_symbolReceivers, symbol, i); // Search for the symbol recipient

   if(i != -1) {
      m_symbolReceivers[i].Close(p_order);   // Notify the symbol recipient about closing a position
      m_isChanged = true;                    // Remember that there are changes
   }
}

取引戦略のシグナルに基づいて仮想ポジションをオープン/クローズするだけでなく、ストップロスまたはテイクプロフィットのレベルに達したときにクローズすることもできます。CVirtualOrderクラスでは,このためにTick()メソッドを用意しています。レベルを確認し、必要であれば仮想ポジションをクローズします。すべてのティック、すべての仮想ポジションに対して呼び出されるべきです。これはまさにCVirtualReceiverクラスのTick()メソッドがおこなうことです。クラスを追加しましょう。

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Public methods
   void              Tick();     // Handle a tick for the array of virtual orders (positions)
   ...
};

//+------------------------------------------------------------------+
//| Handle a tick for the array of virtual orders (positions)        |
//+------------------------------------------------------------------+
void CVirtualReceiver::Tick() {
   FOREACH(m_orders, m_orders[i].Tick());
}

最後に、仮想ポジションオブジェクトに割り当てられたメモリを正しく解放します。これらはすべてm_orders配列に入っているので、デストラクタを追加し、そこで削除します。

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CVirtualReceiver::~CVirtualReceiver() {
   FOREACH(m_orders, delete m_orders[i]); // Remove virtual positions
}

出来上がったコードを、現在のフォルダのVirtualReceiver.mqhファイルに保存します。


銘柄受信機の実装

残るは,最後のクラスCVirtualSymbolReceiver を実装して,操作モードが使用に適した完成形になるようにすることです。このクラスの主な内容は、前回の記事のCVolumeReceiverクラスから引き継ぎ、各仮想ポジションの銘柄の決定と銘柄の列挙に関連する箇所を削除しながら、調整をおこないます。

このクラスのオブジェクトは、仮想ポジションオブジェクトへのポインタの配列も持つが、ここではその構成は常に変化します。この配列には、仮想ポジションのみが含まれるようにします。すると、仮想ポジションをオープン/クローズするときに何をすべきかが明確になります。仮想ポジションが建てられたらすぐに、対応する銘柄受信者の配列に追加し、クローズされたらすぐに配列から削除します。

また、仮想ポジションの構成に変化があることを示すフラグがあると便利です。こうすることで、すべてのティックについて不必要な確認を避けることができます。

銘柄、ポジションの配列、変更フラグのフィールドと、開閉を処理する2つのメソッドをクラスに追加します。

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Symbol
   CVirtualOrder     *m_orders[];      // Array of open virtual positions
   bool              m_isChanged;      // Are there any changes in the composition of virtual positions?

   ...   

public:
   ...
   void              Open(CVirtualOrder *p_order);    // Register opening a virtual position
   void              Close(CVirtualOrder *p_order);   // Register closing a virtual position 
   ...
};

渡された仮想ポジションを配列から追加/削除し、変更の有無を示すフラグを設定します。

//+------------------------------------------------------------------+
//| Register opening a virtual position                              |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Open(CVirtualOrder *p_order) {
   APPEND(m_orders, p_order); // Add a position to the array
   m_isChanged = true;        // Set the changes flag
}

//+------------------------------------------------------------------+
//| Register closing a virtual position                              |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Close(CVirtualOrder *p_order) {
   REMOVE(m_orders, p_order); // Remove a position from the array
   m_isChanged = true;        // Set the changes flag
}

また、銘柄名から目的の銘柄受取人を検索する必要があります。FIND(A,V,I)マクロから通常の線形探索アルゴリズムを使用するために、インスタンス銘柄が渡された文字列と一致する場合にtrueを返す、銘柄受信者と文字列を比較するオーバーロード演算子を追加してみましょう。

class CVirtualSymbolReceiver : public CReceiver {
   ...

public:
   ...
   bool              operator==(const string symbol) {// Operator for comparing by a symbol name
      return m_symbol == symbol;
   }
   ...
};

以下はCVirtualSymbolReceiverクラスの完全な説明です。すべてのメソッドの具体的な実装については、添付ファイルを参照してください。

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Symbol
   CVirtualOrder     *m_orders[];      // Array of open virtual positions
   bool              m_isChanged;      // Are there any changes in the composition of virtual positions?

   bool              m_isNetting;      // Is this a netting account?

   double            m_minMargin;      // Minimum margin for opening

   CPositionInfo     m_position;       // Object for obtaining properties of market positions
   CSymbolInfo       m_symbolInfo;     // Object for getting symbol properties
   CTrade            m_trade;          // Object for performing trading operations

   double            MarketVolume();   // Volume of open market positions
   double            VirtualVolume();  // Volume of open virtual positions
   bool              IsTradeAllowed(); // Is trading by symbol available? 

   // Required volume difference
   double            DiffVolume(double marketVolume, double virtualVolume);

   // Volume correction for the required difference
   bool              Correct(double oldVolume, double diffVolume);

   // Auxiliary opening methods
   bool              ClearOpen(double diffVolume);
   bool              AddBuy(double volume);
   bool              AddSell(double volume);
   
   // Auxiliary closing methods
   bool              CloseBuyPartial(double volume);
   bool              CloseSellPartial(double volume);
   bool              CloseHedgingPartial(double volume, ENUM_POSITION_TYPE type);
   bool              CloseFull();

   // Check margin requirements
   bool              FreeMarginCheck(double volume, ENUM_ORDER_TYPE type);

public:
                     CVirtualSymbolReceiver(ulong p_magic, string p_symbol);  // Constructor
   bool              operator==(const string symbol) {// Operator for comparing by a symbol name
      return m_symbol == symbol;
   }
   void              Open(CVirtualOrder *p_order);    // Register opening a virtual position
   void              Close(CVirtualOrder *p_order);   // Register closing a virtual position 
   
   virtual bool      Correct() override;              // Adjustment of open volumes
};

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


結果の比較

その結果、動作モードは次のように表されます。


図3:今回の記事の動作モード

図3:現在の記事の動作モード

さて、ここからが最も興味深いところです。前回と同じパラメータで9インスタンスの戦略を使用するEAをコンパイルしてみましょう。前回の記事と同じようなEAをテストしてみましょう。


図4:前回のEAの結果


図5:今回のEAの結果

一般的に、結果はほとんど同じです。残高グラフは一般的に区別がつきません。レポートに見られるわずかな違いは、さまざまな理由によるものであり、今後さらに分析されます。


さらなる可能性の評価

前回の記事の議論では、論理的な質問がありました。問題のアプローチを使用して得られる最も魅力的な取引結果は何であるかということです。これまでのところ、グラフは5年間で20%のリターンを示していますが、これは特に魅力的なものではありません。

今のところ、この質問に対する答えは次のようになります。まず、選択したシンプルな戦略による結果と、共同作業の実施による結果を明確に分ける必要があります。

単純な戦略を別の戦略に変えると、最初のカテゴリーの結果は変わります。単純な戦略の個々のインスタンスが示す結果が良ければ良いほど、その全体的な結果も良くなることは明らかです。ここで紹介する結果は、1つの取引アイデアで得られたものであり、当初はその品質と適合性によって正確に決定されました。これらの結果は、単純にテスト区間の利益/ドローダウン比率で評価します。

2つ目のカテゴリは、共同作業と単独作業の比較結果です。ここでは、株式成長曲線のグラフの直線性の改善、ドローダウンの減少など、他のパラメータに基づいて評価がおこなわれます。というのも、1つ目のカテゴリで特に傑出しているわけではない成績も、彼らの力を借りれば許容できるレベルにまで引き上げられる可能性があるからです。

しかし、すべての結果を得るためには、まず可変ロット取引を実施することが望ましいです。これがなければ、テスト結果に基づいて収益性/ドローダウン率を推定することさえ難しくなります。 

少額の初期預託金で、5年間(2018.01.01~2023.01.01)の最大許容ドローダウン50%のポジションのサイズについて、新たな最適値を選択してみましょう。以下は、この記事のEAを、ポジションサイズの倍率は異なりますが、初期預金1000ドルで5年間すべて一定で実行した結果です。前回の記事では、ポジションサイズは預託金10,000ドルに較正されたため、depoPart_の初期値は約10倍に縮小されました。


図6:異なるポジションサイズでのテスト結果

最小のdepoPart_= 0.04では、残高に比例して再計算されたそれらのボリュームが0.01未満であるため、EAは実際のポジションを建てていないことがわかります。しかし、次の倍率値depoPart_= 0.06から、マーケットポジションが建てられています。

最大depoPart_ = 0.4の場合、約22,800ドルの利益が得られます。ただし、ここで示したドローダウンは、全ランで発生した相対的なドローダウンです。しかし、23,000人の10%と1,000人はまったく異なる値です。従って、ぜひとも単走の結果を見るべきです。



図7:最大depoPart_ = 0.4でのテスト結果

ご覧のように、1167ドルのドローダウンが実際に達成され、達成時点では現在の残高のわずか9.99%でしたが、テスト期間の開始がこの不快な瞬間の直前にあったら、預金全体を失っていたでしょう。したがって、このポジションサイズは使用できません。

depoPart_= 0.2のときの結果を見てみましょう。



図8:depoPart_= 0.2でのテスト結果


ここでは、最大ドローダウンは494ドルを超えありませんでした。つまり、1000ドルの初期預金の約50%です。したがって、このようなポジションサイズであれば、検討中の5年間の期間の開始時期が可能な限り悪く選択されたとしても、預金全額が失われることはありません。

このポジションサイズでは、1年後(2022年)のテスト結果は以下のようになります。



図9:2022年、depoPart_= 0.2でのテスト結果

つまり、予想される最大ドローダウンを約50%とすると、年間約150%の利益が得られることになります。

この結果は有望に見えますが、玉にきずがあります。パラメータの最適化に含まれなかった2023年の結果は、明らかに悪化しています。



図10:2023年、depoPart_ = 0.2でのテスト結果

もちろん、年末のテスト結果では40%の利益を得ましたが、12カ月中8カ月は持続的な成長がありませんでした。この問題が主なものであるように思われます。本連載では、この問題を解決するためのさまざまなアプローチについて考えてみます。


結論

この記事では、前編のコードを簡素化し最適化することで、コードのさらなる発展に備えました。様々な取引戦略を活用する能力を制限する可能性のある、以前に指摘されたいくつかの欠陥に対処しました。テストの結果、新しい実装は以前の実装より悪くありませんでした。実装のスピードは変化しませんでしたが、戦略のインスタンス数が何倍にも増えて初めて成長が現れる可能性があります。

そのためには、戦略の入力パラメータをどのように保存し、それらをどのようにパラメータライブラリに組み合わせ、単一の戦略インスタンスを最適化した結果として得られる組み合わせの中から最適なものをどのように選択するかを、最終的に考え出す必要があります。

次回以降も、選ばれた方向で作業を続けていきます。



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

最適化アルゴリズムを使用してEAパラメータをオンザフライで設定する 最適化アルゴリズムを使用してEAパラメータをオンザフライで設定する
この記事では、最適化アルゴリズムを使用して最適なEAパラメータをオンザフライで見つけることや、取引操作とEAロジックの仮想化について、実践的な側面から論じています。この記事は、最適化アルゴリズムをEAに実装するためのインストラクションとして使用できます。
複数の商品を同時に取引する際のリスクバランス 複数の商品を同時に取引する際のリスクバランス
この記事では、初心者が複数の商品を同時に取引する際のリスクバランスを取るためのスクリプトの実装をゼロから書けるようにします。また、経験豊富なユーザーは、この記事で提案されたオプションに関連して、ソリューションを実行するための新しいアイデアが得られるかもしれません。
母集団最適化アルゴリズム:社会集団の進化(ESG) 母集団最適化アルゴリズム:社会集団の進化(ESG)
多母集団アルゴリズムの構成原理を考えます。この種のアルゴリズムの一例として、新しいカスタムアルゴリズムであるESG (Evolution of Social Groups)を見てみましょう。このアルゴリズムの基本概念、母集団相互作用メカニズム、利点を分析し、最適化問題におけるパフォーマンスを検証します。
取引におけるトレーリングストップ 取引におけるトレーリングストップ
この記事では、取引でのトレーリングストップの使い方について説明します。トレーリングストップがどの程度有用で効果的なのか、どのように利用できるのかを評価します。トレーリングストップの効率は、価格のボラティリティと損切りレベルの選択に大きく左右されます。損切りを設定するには、さまざまなアプローチを用いることができます。