English Русский 中文 Español Deutsch Português
preview
多通貨エキスパートアドバイザーの開発(第12回):プロップトレーディングレベルのリスクマネージャーの育成

多通貨エキスパートアドバイザーの開発(第12回):プロップトレーディングレベルのリスクマネージャーの育成

MetaTrader 5トレーディング | 6 11月 2024, 08:41
148 0
Yuriy Bykov
Yuriy Bykov

はじめに

連載全体を通して、リスク管理のトピックは何度も取り上げられてきました。標準化された取引戦略の概念が導入され、それによってテスト期間中のドローダウンレベルを10%以内に抑えられるように設定されています。ただし、取引戦略や戦略グループを標準化することで、特定のドローダウンを保証できるのは履歴期間に限られます。フォワードテスト期間や実際の取引口座においても、同じドローダウンレベルが達成されるかは予測できません。

最近、「手動取引のリスクマネージャー」および「アルゴリズム取引のリスクマネージャー」のような記事でもリスク管理が取り上げられました。これらの記事では、事前に設定された取引パラメータに従うかを自動的に監視する仕組みが提案されています。たとえば、1日、1週間、または1ヶ月の間に設定した損失レベルを超えた場合に、取引を一時停止するという方法です。

プロップファームから少し教訓を得よう(第1回)-導入編」稿も非常に興味深い内容でした。この記事では、プロップトレーディング会社が資金提供を受けたいトレーダーに課す取引要件について考察しています。こうした会社に対してさまざまな評価が見られる一方で、明確なリスク管理ルールの適用は、取引を成功に導く重要な要素のひとつです。そのため、プロップトレーディング会社が用いるリスク管理モデルを参考にして、自分自身のリスクマネージャーを実装することは合理的といえるでしょう。


モデルと概念

リスク管理者にとって、次の概念が重要となります。

  • 基本残高:他のすべてのパラメータ計算の基準となる口座の初期残高です。ここでは10,000を基準としています。
  • 日次ベース残高:現在の日次期間の開始時における取引口座の残高です。簡便化のため、日次期間の始まりは、D1時間枠の端末に新しいバーが表示されたタイミングと一致すると仮定します。
  • 1日の基本資金:各日次期間の開始時に口座にある資金の額を指します。
  • 1日レベル:その日次期間の初めにおける基本残高と資金の最高額であり、次の日次期間が始まるまで同じ値を保持します。
  • 最大日次損失:日次レベルからの下方偏差が一定量に達すると、現在の日次期間における取引が停止される基準値です。 取引は次の日次期間に再開されます。ここでの取引停止は、全てのポジションを完全にクローズする一連のアクションとして理解されます。この単純なモデルでは、最大日次損失に達した際には全ポジションがクローズされます。 
  • 最大総損失:基本残高からの下方偏差が一定値に達した場合に取引が完全に停止される基準です。この場合、次の期間にも取引は再開されません。このレベルに到達すると全ポジションがクローズされます。

取引の停止基準は、日次および合計の2つに限定します。これにより、プロップトレーディング会社が持たない週次または月次レベルを最初の実装で含めることなく、必要に応じて後で追加できるようにします。

プロップトレーディング会社ごとに、最大日次損失や総損失の計算方法には若干の違いが見られるため、リスクマネージャーには3つの異なる方法で最大損失を設定できる機能を設けています。

  • 預金通貨で固定:取引口座の通貨単位で損失額を直接設定する方法で、正の数値としてパラメータに渡します。
  • 基本残高のパーセンテージと:ここでは基本残高のパーセンテージとして損失を設定します。私たちのモデルの基本残高は定数値であるため(口座とEAが手動で設定された異なる基本残高値で再起動されるまで)、この方法で計算された最大損失も定数値になります。このケースを最初のケースに縮小することも可能ですが、通常は最大損失のパーセンテージが示されるため、別のケースとして残しておきます。
  • 1日のレベルのパーセンテージ:各日期間の初めに再計算し、その日次レベルの一定割合として最大損失を定めます。残高や資金が増えるとともに最大損失も増加します。この方法は主に日次損失の計算に用いられ、最大総損失は通常、基本残高に対して固定されます。

いつものように最小限の原則に従い、リスクマネージャークラスの実装を開始しましょう。まずはシンプルな実装からおこない、必要に応じて後で複雑さを追加していくことを検討します。


CVirtualRiskManagerクラス

このクラスの開発は、いくつかの段階を経て進められました。最初の段階では、完全に静的なオブジェクトとして設計され、あらゆるオブジェクトから自由に使用できるようにされていました。次に、リスクマネージャーのパラメータも最適化可能とし、それらを初期化文字列として保存できると便利であると考え、この目的のためにクラスはCFactorableクラスの子孫となるよう改良されました。また、リスクマネージャーを他の複数クラスで使用できるようにするために、シングルトンパターンも実装しました。しかし、リスクマネージャーが実際にはCVirtualAdvisor EAクラスのみで必要であることが判明したため、リスクマネージャークラスからシングルトンパターンの実装を取り除きました。

まず、リスクマネージャーの状態と制限を計算するための手法を列挙してみましょう。

// Possible risk manager states
enum ENUM_RM_STATE {
   RM_STATE_OK,            // Limits are not exceeded 
   RM_STATE_DAILY_LOSS,    // Daily limit is exceeded
   RM_STATE_OVERALL_LOSS   // Overall limit is exceeded
};


// Possible methods for calculating limits
enum ENUM_RM_CALC_LIMIT {
   RM_CALC_LIMIT_FIXED,          // Fixed (USD)
   RM_CALC_LIMIT_FIXED_PERCENT,  // Fixed (% from Base Balance)
   RM_CALC_LIMIT_PERCENT         // Relative (% from Daily Level)
};


リスクマネージャークラスの説明では、初期化文字列を通してコンストラクタに渡される入力を保存するためのプロパティを複数用意しています。また、残高、資金、利益などの現在の計算要素を保持するプロパティも追加します。Protectedセクションには、補助的なメソッドをいくつか宣言しておきましょう。一方、publicセクションでは、主にコンストラクタと各ティックを処理するメソッドのみを提供します。ここでは、保存・読み込みメソッドや文字列変換演算子について説明し、実装部分は後ほど記述することとします。

クラスの説明は以下のとおりです。

//+------------------------------------------------------------------+
//| Risk management class (risk manager)                             |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Main constructor parameters
   bool              m_isActive;             // Is the risk manager active?

   double            m_baseBalance;          // Base balance

   ENUM_RM_CALC_LIMIT m_calcDailyLossLimit;  // Method of calculating the maximum daily loss
   double            m_maxDailyLossLimit;    // Parameter of calculating the maximum daily loss

   ENUM_RM_CALC_LIMIT m_calcOverallLossLimit;// Method of calculating the total daily loss
   double            m_maxOverallLossLimit;  // Parameter of calculating the maximum total loss

// Current state
   ENUM_RM_STATE     m_state;

// Updated values
   double            m_balance;              // Current balance
   double            m_equity;               // Current equity
   double            m_profit;               // Current profit
   double            m_dailyProfit;          // Daily profit
   double            m_overallProfit;        // Total profit
   double            m_baseDailyBalance;     // Daily basic balance
   double            m_baseDailyEquity;      // Daily base balance
   double            m_baseDailyLevel;       // Daily base level
   double            m_virtualProfit;        // Profit of open virtual positions

// Managing the size of open positions
   double            m_prevDepoPart;         // Used part of the total balance

// Protected methods
   double            DailyLoss();            // Maximum daily loss
   double            OverallLoss();          // Maximum total loss

   void              UpdateProfit();         // Update current profit values
   void              UpdateBaseLevels();     // Updating daily base levels

   void              CheckLimits();          // Check for excess of permissible losses
   void              CheckDailyLimit();      // Check for excess of the permissible daily loss
   void              CheckOverallLimit();    // Check for excess of the permissible total loss

   double            VirtualProfit();        // Determine the real size of the virtual position

public:
                     CVirtualRiskManager(string p_params);     // Constructor

   virtual void      Tick();                 // Tick processing in risk manager 

   virtual bool      Load(const int f);      // Load status
   virtual bool      Save(const int f);      // Save status

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


リスクマネージャーオブジェクトのコンストラクタは、初期化文字列に6つの数値が含まれていることを前提としています。これらの数値は適切なデータ型に変換されたのち、オブジェクトの主要プロパティに割り当てられます。また、オブジェクト作成時に状態が通常状態(制限を超えていない)に設定されます。EAが1日の途中で再起動され、オブジェクトが再作成された場合は、保存されている情報を読み込むことで、状態を最後に保存した状態へ修正する必要があります。取引に割り当てられた口座残高の割合を設定する際も同様の手順を踏みます。コンストラクタで設定された値は、リスクマネージャーの保存情報を読み込む際に事前定義することが可能です。

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

// Read the initialization string and set the property values
   m_isActive = (bool) ReadLong(p_params);
   m_baseBalance = ReadDouble(p_params);
   m_calcDailyLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxDailyLossLimit = ReadDouble(p_params);
   m_calcOverallLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxOverallLossLimit = ReadDouble(p_params);

// Set the state: Limits are not exceeded
   m_state = RM_STATE_OK;

// Remember the share of the account balance allocated for trading
   m_prevDepoPart = CMoney::DepoPart();

// Update base daily levels
   UpdateBaseLevels();

// Adjust the base balance if it is not set
   if(m_baseBalance == 0) {
      m_baseBalance = m_balance;
   }
}


リスクマネージャーは、イベントハンドラ内の各ティックで主要な処理を実行します。この処理には、リスクマネージャーがアクティブであるかの確認、アクティブであれば必要に応じて現在の利益値と基本日次レベルの更新、さらに損失制限を超えていないかのチェックが含まれます。

//+------------------------------------------------------------------+
//| Tick processing in the risk manager                              |
//+------------------------------------------------------------------+
void CVirtualRiskManager::Tick() {
// If the risk manager is inactive, exit
   if(!m_isActive) {
      return;
   }

// Update the current profit values
   UpdateProfit();

// If a new daily period has begun, then we update the base daily levels
   if(IsNewBar(Symbol(), PERIOD_D1)) {
      UpdateBaseLevels();
   }

// Check for exceeding loss limits
   CheckLimits();
}


もう1点、特に留意すべき事項があります。それは、取引量の受信者が仮想ポジションを実際の市場ポジションに変換するために開発された構造や、仮想ポジションと実際のポジションサイズの間で必要なスケーリング係数を設定できる資本管理モジュールの活用です。これにより、戦略の取引ロジックに矛盾することなく、安全に市場ポジションをクローズすることが非常に容易になります。これを実現するには、資本管理モジュールのスケーリング係数を0に設定するだけです。

CMoney::DepoPart(0);               // Set the used portion of the total balance to 0


これに先立ち、m_prevDepoPartプロパティの以前の比率を記憶しておくことで、新しい日が訪れ、日次制限が更新された後にこの比率を元の値に戻すだけで、以前にクローズした実際のポジションを簡単に復元できます。 

CMoney::DepoPart(m_prevDepoPart);  // Return the used portion of the total balance


同時に、もちろん、ポジションが悪化した価格で再開されるか、改善された価格で再開されるかを事前に知ることはできません。しかし、リスクマネージャーを追加しても、すべての取引戦略のパフォーマンスに影響を与えないことは確かです。

では、リスクマネージャークラスの残りのメソッドを見ていきましょう。

UpdateProfits()メソッドでは、残高、資金、利益の現在の値を更新し、現在の資金と毎日のレベルとの差として日次利益を計算します。この値は、常に現在の利益と一致するわけではありません。新しい日次期間の開始以降にいくつかの取引がすでに終了している場合、差が生じることがあります。また、総損失は現在の資金と基本残高との差として計算されます。

//+------------------------------------------------------------------+
//| Updating current profit values                                   |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateProfit() {
   m_equity = AccountInfoDouble(ACCOUNT_EQUITY);
   m_balance = AccountInfoDouble(ACCOUNT_BALANCE);
   m_profit = m_equity - m_balance;
   m_dailyProfit = m_equity - m_baseDailyLevel;
   m_overallProfit = m_equity - m_baseBalance;
   m_virtualProfit = VirtualProfit();

   if(IsNewBar(Symbol(), PERIOD_H1) && PositionsTotal() > 0) {
      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
   }
}

このメソッドでは、いわゆる「現在の仮想利益」も計算します。これは、オープンしている仮想ポジションに基づいて算出されます。リスク管理の制限が発動された際、実際のポジションが存在しない場合でも、リスク管理によってクローズされた実際のポジションが開いたままであれば、いつでもおおよその利益を見積もることができます。ただし、残念ながら、この計算されたパラメータは完全に正確な結果をもたらすわけではなく、数パーセントの誤差が生じる可能性がありますが、それでも非常に有用です。

VirtualProfit() メソッドは、現在の仮想利益を計算するためのものです。このメソッドでは、仮想ボリュームレシーバーオブジェクトへのポインタを取得し、仮想ポジションの総数を調べる必要があります。その後、すべての仮想ポジションをループし、資金管理モジュールに対して各ポジションの仮想利益を計算し、現在の取引資金に基づいてスケーリングするように指示します。

//+------------------------------------------------------------------+
//| Determine the profit of open virtual positions                   |
//+------------------------------------------------------------------+
double CVirtualRiskManager::VirtualProfit() {
   // Access the receiver object
   CVirtualReceiver *m_receiver = CVirtualReceiver::Instance();
   
   double profit = 0;
   
   // Find the profit sum for all virtual positions
   FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i)));
   
   return profit;
}

このメソッドでは、新しいマクロ FORIを使用しました。これについては後ほど詳しく説明します。

新しい日次期間が始まると、基本日次残高、資金、およびレベルを再計算します。また、前日に日次損失の限度に達していた場合には、取引を復元し、開いている仮想ポジションに応じて実際のポジションを再開する必要があります。この処理は、UpdateBaseLevels()メソッドによっておこなわれます。

//+------------------------------------------------------------------+
//| Update daily base levels                                         |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateBaseLevels() {
// Update balance, funds and base daily level
   m_baseDailyBalance = m_balance;
   m_baseDailyEquity = m_equity;
   m_baseDailyLevel = MathMax(m_baseDailyBalance, m_baseDailyEquity);

   PrintFormat(__FUNCTION__" | DAILY UPDATE: Balance = %.2f | Equity = %.2f | Level = %.2f",
               m_baseDailyBalance, m_baseDailyEquity, m_baseDailyLevel);

// If the daily loss level was reached earlier, then
   if(m_state == RM_STATE_DAILY_LOSS) {
      // Restore the status to normal:
      CMoney::DepoPart(m_prevDepoPart);         // Return the used portion of the total balance
      m_state = RM_STATE_OK;                    // Set the risk manager to normal
      CVirtualReceiver::Instance().Changed();   // Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESTORE: depoPart = %.2f",
                  m_prevDepoPart);
   }
}


最大損失を、パラメータで指定された方法に従って計算するためには、DailyLoss()およびOverallLoss()の2つのメソッドを使用します。これらの実装は非常に類似していますが、唯一の違いは計算に使用される数値とメソッドのパラメータです。

//+------------------------------------------------------------------+
//| Maximum daily loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::DailyLoss() {
   if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxDailyLossLimit;
   } else if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxDailyLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxDailyLossLimit / 100;
   }
}

//+------------------------------------------------------------------+
//| Maximum total loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::OverallLoss() {
   if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxOverallLossLimit;
   } else if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxOverallLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxOverallLossLimit / 100;
   }
}


CheckLimits()メソッドは、1日の損失と総損失を確認するために、2つの補助メソッドを呼び出すだけのシンプルな構造を持っています。

//+------------------------------------------------------------------+
//| Check loss limits                                                |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckLimits() {
   CheckDailyLimit();      // Check daily limit
   CheckOverallLimit();    // Check total limit
}


日次損失チェックメソッドは、DailyLoss()メソッドを使用して最大許容日次損失限度を取得し、それを現在の日次利益と比較します。限度を超えると、リスク マネージャーは「日次限度超過」状態に切り替わり、使用済み取引残高のサイズを0に設定してポジションのクローズが開始されます。

//+------------------------------------------------------------------+
//| Check daily loss limit                                           |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckDailyLimit() {
// If daily loss is reached and positions are still open
   if(m_dailyProfit < -DailyLoss() && CMoney::DepoPart() > 0) {
   // Switch the risk manager to the achieved daily loss state:
      m_prevDepoPart = CMoney::DepoPart();   // Save the previous value of the used part of the total balance
      CMoney::DepoPart(0);                   // Set the used portion of the total balance to 0
      m_state = RM_STATE_DAILY_LOSS;         // Set the risk manager to the achieved daily loss state
      CVirtualReceiver::Instance().Changed();// Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESET: depoPart = %.2f",
                  CMoney::DepoPart());
   }
}


相損失チェックメソッドも同様に機能しますが、唯一の違いは総利益を総許容損失と比較する点です。総制限を超えると、リスクマネージャーは「総制限超過」状態に切り替わります。

取得したコードを現在のフォルダのVirtualRiskManager.mqhファイルに保存します。

次に、新しいリスクマネージャークラスを利用できるようにするために、以前に作成したプロジェクトファイルに加える必要のある変更や追加について確認してみましょう。


便利なマクロ

配列を操作するための便利なマクロのリストに、新しいマクロFORI(N, D)を追加しました。このマクロは、変数iを使用してループを構成し、式DN回実行することができます。

// 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 FORI(N, D)      { for(int i=0; i<N;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

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


СMoney資金管理クラス

このクラスでは、ボリュームのスケーリング係数を考慮して仮想ポジションの利益を計算するメソッドを追加します。具体的には、Volume()Volume()メソッドで同様の操作をおこない、仮想ポジションの計算サイズを決定します。これは、取引に現在利用可能な残高と仮想ポジションのボリュームに対応する残高に関する情報に基づき、これらの残高の比率に等しいスケーリング係数を求めることを意味します。その後、この係数を仮想ポジションのボリュームに掛けて、計算されたボリューム、すなわち取引アカウントで実際に開かれるボリュームを得ることができます。

したがって、まずVolume()メソッドから、スケーリング係数を見つけるコード部分を別のCoeff()メソッドに切り出すことから始めます。

//+------------------------------------------------------------------+
//| Calculate the virtual position volume scaling factor             |
//+------------------------------------------------------------------+
double CMoney::Coeff(CVirtualOrder *p_order) {
   // Request the normalized strategy balance for the virtual position
   double fittedBalance = p_order.FittedBalance();

   // If it is 0, then the scaling factor is 1
   if(fittedBalance == 0.0) {
      return 1;
   }

   // Otherwise, find the value of the total balance for trading
   double totalBalance = s_fixedBalance > 0 ? s_fixedBalance : AccountInfoDouble(ACCOUNT_BALANCE);

   // Return the volume scaling factor
   return totalBalance * s_depoPart / fittedBalance;
}


この後、Volume()メソッドとProfit()メソッドの実装は非常に似たものになります。具体的には、仮想ポジションから目的の値(ボリュームまたは利益)を取得し、その結果にスケーリング係数を掛けるという流れです。

//+------------------------------------------------------------------+
//| Determine the calculated size of the virtual position            |
//+------------------------------------------------------------------+
double CMoney::Volume(CVirtualOrder *p_order) {
   return p_order.Volume() * Coeff(p_order);
}

//+------------------------------------------------------------------+
//| Determining the calculated profit of a virtual position          |
//+------------------------------------------------------------------+
double CMoney::Profit(CVirtualOrder *p_order) {
   return p_order.Profit() * Coeff(p_order);
}


もちろん、クラスの説明に新しいメソッドを追加する必要があります。

//+------------------------------------------------------------------+
//| Basic money management class                                     |
//+------------------------------------------------------------------+
class CMoney {
   ...
   
   // Calculate the scaling factor of the virtual position volume
   static double     Coeff(CVirtualOrder *p_order);

public:
   CMoney() = delete;                  // Disable the constructor
   
   // Determine the calculated size of the virtual position
   static double     Volume(CVirtualOrder *p_order);
   
   // Determine the calculated profit of a virtual position  
   static double     Profit(CVirtualOrder *p_order);  

   ...
};

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


СVirtualFactoryクラス

作成したリスクマネージャークラスは、CFactorableクラスの子孫であるため、その作成可能性を確保するためには、CVirtualFactoryによって作成されたオブジェクトの構成を拡張する必要があります。具体的には、Create()静的メソッド内に、CVirtualRiskManagerクラスのオブジェクトを作成するためのコードブロックを追加します。

//+------------------------------------------------------------------+
//| 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 == "CVirtualRiskManager") {
         object = new CVirtualRiskManager(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      }
      
      ...

      return object;
   }
};

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


CVirtualAdvisor クラス

CVirtualAdvisor EAクラスにさらなる大きな変更を加える必要があります。リスクマネージャーオブジェクトはこのクラス内でのみ使用されることに決定したため、対応するプロパティをクラスの説明に追加します。

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   CVirtualReceiver     *m_receiver;      // Receiver object that brings positions to the market
   CVirtualInterface    *m_interface;     // Interface object to show the status to the user
   CVirtualRiskManager  *m_riskManager;   // Risk manager object

   ...
};


また、リスクマネージャーの初期化文字列が、戦略グループの初期化文字列の直後にEAの初期化文字列に埋め込まれることに同意しましょう。また、この初期化文字列をコンストラクタのriskManagerParams変数に読み込み、そこからリスクマネージャーを作成する処理を追加しましょう。

//+------------------------------------------------------------------+
//| 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 initialization string of the risk manager object
   string riskManagerParams = 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 the risk manager object 
      m_riskManager = NEW(riskManagerParams);
   }
}


コンストラクタでオブジェクトを作成したため、デストラクタでそのオブジェクトを削除することも考慮する必要があります。

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


最も重要な点は、関連するEAハンドラからリスク マネージャーのTick()ハンドラを呼び出すことです。リスクマネージャーハンドラは、市場ボリュームを調整する前に起動されるため、損失制限を超えた場合や、制限が更新された場合に、受信者は同じティックを処理する際に市場ポジションのオープンボリュームを調整することができます。

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   bool isNewBar = UpdateNewBar();

// If there is no new bar anywhere, and we only work on new bars, then exit
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Receiver handles virtual positions
   m_receiver.Tick();

// Start handling in strategies
   CAdvisor::Tick();

// Risk manager handles virtual positions
   m_riskManager.Tick();

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();

// Render the interface
   m_interface.Redraw();
}

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


SimpleVolumesExpertSingle EA

リスクマネージャーをテストするには、EAでパラメータを指定して必要な初期化文字列を生成する機能を追加するだけです。今のところ、リスクマネージャーの6つのパラメータすべてを個別のEA入力に移動してみましょう。

input group "===  Risk management"
input bool        rmIsActive_             = true;
input double      rmStartBaseBalance_     = 10000;
input ENUM_RM_CALC_LIMIT 
                  rmCalcDailyLossLimit_   = RM_CALC_LIMIT_FIXED;
input double      rmMaxDailyLossLimit_    = 200;
input ENUM_RM_CALC_LIMIT 
                  rmCalcOverallLossLimit_ = RM_CALC_LIMIT_FIXED;
input double      rmMaxOverallLossLimit_  = 500;


OnInit()関数では、リスクマネージャーの初期化文字列の作成を追加し、それをEAの初期化文字列に埋め込む必要があります。同時に、戦略とこの1つの戦略を含むグループの初期化文字列を作成するためのコードを少し書き直し、個々のオブジェクトの初期化文字列を異なる変数に分離します。

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

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

// Prepare the initialization string for a group with one strategy instance
   string groupParams = StringFormat(
                           "class CVirtualStrategyGroup(\n"
                           "       [\n"
                           "        %s\n"
                           "       ],%f\n"
                           "    )",
                           strategyParams, scale_
                        );

// Prepare the initialization string for the risk manager
   string riskManagerParams = StringFormat(
                                 "class CVirtualRiskManager(\n"
                                 "       %d,%.2f,%d,%.2f,%d,%.2f"
                                 "    )",
                                 rmIsActive_, rmStartBaseBalance_,
                                 rmCalcDailyLossLimit_, rmMaxDailyLossLimit_,
                                 rmCalcOverallLossLimit_, rmMaxOverallLossLimit_
                              );

// Prepare the initialization string for an EA with a group of a single strategy and the risk manager
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    %s,\n"
                            "    %s,\n"
                            "    %d,%s,%d\n"
                            ")",
                            groupParams,
                            riskManagerParams,
                            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);
}

取得したコードを現在のフォルダのSimpleVolumesExpertSingle.mq5ファイルに保存します。これで、リスクマネージャーの操作をテストする準備が整いました。


検証

開発の前段階で最適化中に取得された取引戦略インスタンスの1つのパラメータを使用します。この取引戦略インスタンスを「モデル戦略」と呼びます。モデル戦略のパラメータについては、図1に示されています。

図1:モデル戦略のパラメータ


これらのパラメータを使用し、リスクマネージャーをオフにして、2021~2022年の期間に単一のテスターパスを実行してみましょう。次の結果が得られます。

図2:リスクマネージャーなしで戦略結果をモデル化する


グラフは、選択された期間中にファンドにいくつかの顕著なドローダウンがあったことを示しています。最大のドローダウンは、2021年10月末(約USD 380)と2022年6月(約USD 840)に発生しました。

ここで、リスクマネージャーをオンにし、1日の最大損失限度額をUSD 150、最大総損失額をUSD 450に設定してみましょう。次の結果が得られます。


図3:リスクマネージャーなしのモデル戦略結果(最大損失:150ドルと450ドル)


グラフは、2021年10月にリスクマネージャーが損失を出す市場ポジションを2回クローズし、仮想ポジションは開いたままだったことを示しています。そのため、翌日、市場ポジションが再びオープンされました。残念ながら、再開はそれほど有利な価格で行われなかったため、バランスと資金による合計ドローダウンは、無効なリスクマネージャーの場合の株式によるドローダウンをわずかに上回りました。また、ポジションをクローズした後、戦略は小さな利益を受け取る代わりに(リスクマネージャーがいない場合のように)、いくらかの損失を被ったことも明らかになりました。

2022年6月には、リスクマネージャーがすでに7回トリガーされ、1日あたり150ドルの損失に達した時点で市場ポジションをクローズしました。ここでも、再開はより不利な価格で行われ、この一連の取引の結果として損失が発生したことがわかりました。しかし、このようなEAが、最大1日損失と総損失のパラメータを持つプロップトレーディング会社のデモ口座で動作した場合、リスクマネージャーなしでは口座は取引ルール違反で停止され、リスクマネージャーがあれば口座は引き続き機能し、結果としてわずかに少ない利益を受け取ることになります。

総損失をUSD 450に設定し、6月の総残高ドローダウンがUSD 1,000を超えたにもかかわらず、最大総損失には達しませんでした。これは基本残高から計算されるためです。言い換えれば、資金が(10,000 - 450) = USD 9550を下回った場合に達成されます。しかし、以前に蓄積された利益により、その期間の資金額は確実にUSD 10,000を下回りませんでした。そのため、EAは市場ポジションのオープンを伴い、動作を続けました。

それでは、総損失に達するトリガーをシミュレートしてみましょう。これをおこなうには、ポジションサイズのスケーリング係数を増やし、2021年10月にはまだ最大総損失を超えず、2022年6月に超過が発生するようにします。scale_ = 50に設定して結果を見てみましょう。

図4:リスクマネージャーなしのモデル戦略結果(最大損失:150ドルと450ドル)、scale_ = 50


ご覧のとおり、取引は2022年6月に終了しました。その後の期間、EAは1つのポジションも開きませんでした。これは、総損失限度額(USD 9550)に達したためです。また、1日の損失額は2021年10月だけでなく、他のいくつかの期間にも頻繁に達していることにも注目できます。

つまり、両方のリミッターが正常に動作していることになります。

リスクマネージャーは、プロップトレーディング会社以外でも役立ちます。例として、モデル戦略のリスクマネージャーのパラメータを最適化し、ポジションのサイズを増やしながら、許容されるドローダウンの10%を超えないようにしてみましょう。これをおこなうには、リスクマネージャーパラメータで、最大総損失を日次レベルの10%に設定します。最適化中に、日次レベルのパーセンテージとして計算される最大日次損失も調べます。


図5:リスクマネージャーによるモデル戦略最適化の結果


結果を見ると、リスクマネージャーを使用した場合、1年間の標準化された利益がUSD 1,560からUSD 2,276にほぼ1.5倍増加したことがわかります(結果列)。個別に表示した場合のベストパスは次のようになります。

図6:リスクマネージャーなしのモデル戦略結果(最大損失:7.6% および 10%、スケール_ = 88)


EAはテスト期間を通じて取引を継続したため、10%の全体的な制限を一度も超えなかったことがわかります。このことから、リスクマネージャーを取引戦略の各インスタンスに適用することには特別な意味がないことが明らかです。実際の口座で個別に起動する予定がないためです。しかし、1つのインスタンスで成功するものは、多数のインスタンスを持つEAにおいても同様に機能するはずです。このように、結果が表面的であっても、リスクマネージャーが有用であることは間違いありません。


    結論

    これで、取引用リスクマネージャーの基本的な実装が完了し、指定されたレベルの最大日次損失と総損失を順守できるようになりました。EAを再起動する際にステータスを保存およびロードする機能はまだサポートされていないため、実際のアカウントでの使用はお勧めできません。ただし、この変更により特に問題が発生することはないでしょう。後でこの点に戻ります。

    同時に、特定の曜日の特定の時間に取引を無効にしたり、重要な経済ニュースの発表中に新しいポジションを開くことを禁止したりするなど、さまざまな時間帯での取引制限機能を追加することも検討できます。リスクマネージャーの開発におけるその他の考慮点には、ポジションサイズのよりスムーズな変更(たとえば、制限の半分を超えた際にサイズを2倍に減少させる)や、より「インテリジェントな」ボリューム回復(たとえば、損失がポジション削減レベルを超えた場合のみおこなう)などがあります。

    これらの課題は後回しにします。とりあえず、EAの最適化の自動化に戻ります。最初の段階は前回の記事で既に実装されています。次は、2番目の段階に進みます。

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


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

    添付されたファイル |
    Macros.mqh (2.4 KB)
    Money.mqh (6.52 KB)
    VirtualFactory.mqh (4.46 KB)
    VirtualAdvisor.mqh (23.02 KB)
    ニューラルネットワークが簡単に(第90回):時系列の周波数補間(FITS) ニューラルネットワークが簡単に(第90回):時系列の周波数補間(FITS)
    FEDformer法を研究することで、時系列表現の周波数領域への扉を開きました。この新しい記事では、私たちが始めたトピックを続けます。分析をおこなうだけでなく、特定の分野におけるその後の状態を予測することができる手法について考えてみたいと思います。
    ニューラルネットワークの実践:直線関数 ニューラルネットワークの実践:直線関数
    この記事では、データベース内のデータを表現できる関数を取得するためのいくつかの方法について簡単に説明します。統計や確率の研究を用いて結果を解釈する方法については詳細に触れません。この問題の数学的側面について深く知りたい方にお任せします。これらの問いを検討することは、ニューラルネットワークの研究において非常に重要です。ここでは、このテーマを冷静に掘り下げていきます。
    PythonとMQL5でロボットを開発する(第2回):モデルの選択、作成、訓練、Pythonカスタムテスター PythonとMQL5でロボットを開発する(第2回):モデルの選択、作成、訓練、Pythonカスタムテスター
    PythonとMQL5で自動売買ロボットを開発する連載を続けます。今日は、モデルの選択と訓練、テスト、交差検証、グリッドサーチ、モデルアンサンブルの問題を解決します。
    ニューラルネットワークが簡単に(第89回):FEDformer (Frequency Enhanced Decomposition Transformer) ニューラルネットワークが簡単に(第89回):FEDformer (Frequency Enhanced Decomposition Transformer)
    これまで検討してきたすべてのモデルは、環境の状態を時系列として分析します。ただし、時系列は周波数特徴の形式で表現することもできます。この記事では、時系列の周波数成分を使用して将来の状態を予測するアルゴリズムを紹介します。