
多通貨エキスパートアドバイザーの開発(第12回):プロップトレーディングレベルのリスクマネージャーの育成
はじめに
本連載全体を通して、リスク管理のトピックは何度も取り上げられてきました。標準化された取引戦略の概念が導入され、それによってテスト期間中のドローダウンレベルを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を使用してループを構成し、式DをN回実行することができます。
// 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





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