English Русский Español Deutsch Português
preview
手動取引のリスクマネージャー

手動取引のリスクマネージャー

MetaTrader 5 | 16 8月 2024, 16:01
139 0
Aleksandr Seredin
Aleksandr Seredin

内容


はじめに

皆さん、こんにちは。今回は、リスク管理の方法論について話を続けます。前回の「複数の商品を同時に取引する際のリスクバランス」稿では、リスクの基本的な概念についてお話ししました。今回は、安全な取引のための基本的なリスクマネージャークラスをゼロから実装していきます。また、取引システムのリスクを制限することが、取引戦略の有効性にどのような影響を与えるかも見ていきます。

リスクマネージャーは私の最初のクラスで、プログラミングの基礎を学んだ直後の2019年に書いたものです。その時、私は自分の経験から、トレーダーの心理状態が取引の有効性、特に取引の意思決定の「一貫性」と「公平性」に大きく影響することを理解しました。ギャンブル、感情的な取引、できるだけ早く損失を補おうとすることによって膨らんだリスクは、テストで非常に良い結果を示した効果的な取引戦略を使用していたとしても、口座を枯渇させる可能性があります。

この記事の目的は、リスクマネジャーを使用したリスク制御がその有効性と信頼性を高めることを示すことです。このテーゼを確認するために、手動取引用のシンプルな基本リスクマネージャークラスをゼロから作成し、非常にシンプルなフラクタルブレイクアウト戦略を使用してテストします。


機能の定義

手動取引に特化したアルゴリズムを実装する際、1日、1週間、1ヶ月の時間的リスク制限の管理のみを実装します。実際の損失額がユーザーによって設定された限度額に達するかそれを超えると、EAは自動的にすべてのポジションをクローズし、それ以上の取引が不可能であることをユーザーに通知しなければなりません。ここで注意しなければならないのは、この情報は純粋に「助言的なもの」であり、チャートの左下隅にあるコメント欄にEAとともに表示されるということです。これは、手動取引に特化したリスクマネージャーを作成しているためで、「どうしても必要な場合」には、ユーザーはいつでもこのEAをチャートから削除して取引を継続することができます。しかし、これを実行することは本当にお勧めしません。なぜなら、市場が自分に不利な方向に動いた場合、手動取引で何が間違っていたのかを正確に把握しようとするよりも、翌日に取引に戻って大きな損失を回避する方がよいからです。このクラスをアルゴリズム取引に組み込む場合は、限度に達したときの注文送信の制限を実装し、できればこのクラスをEA構造に直接組み込む必要があります。これについてはもう少し詳しくお話しします。


入力パラメータとクラスコンストラクタ

期間によるリスク制御と日次利益率の達成基準のみを実施することに決めました。そのために、ユーザーが各期間の預金のパーセンテージとしてリスク値を手動で入力できるように、メモリクラス修飾子input持つdouble型の変数をいくつか導入し、また利益を確定するための目標日次利益パーセンテージも導入します。目標日次利益の制御を示すために、トレーダーが各エントリを個別に検討し、選択した商品間に相関関係がないことを確信している場合にこの機能を有効/無効にする機能として、bool型の追加変数を導入します。このタイプのスイッチ変数は「フラグ」とも呼ばれます。次のコードをグローバルレベルで宣言してみましょう。便宜上、以前はgroupキーワードを使用して名前付きブロックに「ラップ」していました。

input group "RiskManagerBaseClass"
input double inp_riskperday    = 1;          // risk per day as a percentage of deposit
input double inp_riskperweek   = 3;          // risk per week
input double inp_riskpermonth  = 9;          // risk per month
input double inp_plandayprofit = 3;          // target daily profit

input bool dayProfitControl = true;          // whether to close positions after reaching daily profit

宣言された変数は、以下のロジックに従ってデフォルト値で初期化されます。このクラスは日中取引に最適ですが、中期取引や投資にも使用できるので、日次リスクから始めることにします。もちろん、中期的な取引や投資家としての取引では、日中リスクを制御する意味はないので、日次リスクと週次リスクに同じ値を設定できます。さらに、長期投資のみをおこなう場合は、すべての限度を毎月のドローダウンに等しく設定することができます。ここでは、日中取引のデフォルトパラメータのロジックについて見ていきます。

1日のリスクを預金の1%とすることに決めました。1日の限度額を超えた場合は、明日まで端末を閉鎖します。次に、1週間の制限を以下のように定義します。通常、1週間の取引日は5日で、3日連続で損失が出た場合は、次の週の初めまで取引を停止します。単純に、今週は相場を理解していなかったか、何かが変わった可能性が高く、このまま取引を続ければ、この期間に大きな損失を積み上げてしまい、次の週を犠牲にしてもカバーできなくなるからです。同様のロジックが、日中取引で月間の指値を設定する場合にも適用されます。1ヶ月のうち3週間が不採算週であった場合、4週目の取引はおこなわない方がよいという条件を受け入れます。将来の期間を犠牲にして利回り曲線を「改善」するには、多くの時間がかかるからです。また、別の月に大きな損失を出して投資家を「脅かす」こともしたくありません。

お使いの取引システムの特性を考慮し、1日のリスクに基づいて1日の目標利益の大きさを設定します。ここで考慮すべきことは次の通りです。まず、相関性のある商品を取引しているかどうか、取引システムがエントリシグナルを出す頻度、個々の取引についてストップロスとテイクプロフィットの割合を固定して取引しているかどうか、預金の大きさなどです。ストップロスなし、リスクマネージャーなしの取引はまったく推奨しないことを言っておきます。この場合、預金を失うのは時間の問題です。そのため、各取引に個別にストップを設定するか、リスクマネージャーを使用して期間ごとにリスクを制限します。現在のデフォルトパラメータの例では、日次リスクに対する日次利益の条件を1から3に設定しています。また、これらのパラメータは、ストップロスとテイクプロフィットの比率(1~3)を通じて、各取引のリスクプロフィット可能性を設定することと並行して使用するのがよいでしょう(テイクプロフィットはストップロスより大きい)。

制限の構造は次のように描くことができます。

図1:制限の構造

図1:制限の構造

次に、classキーワードを使用して、カスタムデータ型 RiskManagerBaseを宣言します。入力パラメータは、カスタムRiskManagerBaseクラス内に格納する必要があります。入力パラメータはパーセンテージで測定され、制限は預金通貨で追跡されるため、カスタムクラスにprotectedアクセス修飾子付きのdouble型の対応するフィールドをいくつか入力する必要があります。 

protected:

   double    riskperday,                     // risk per day as a percentage of deposit
             riskperweek,                    // risk per week as a percentage of deposit
             riskpermonth,                   // risk per month as a percentage of deposit
             plandayprofit                   // target daily profit as a percentage of deposit
             ;

   double    RiskPerDay,                     // risk per day in currency
             RiskPerWeek,                    // risk per week in currency
             RiskPerMonth,                   // risk per month in currency
             StartBalance,                   // account balance at the EA start time, in currency
             StartEquity,                    // account equity at the limit update time, in currency
             PlanDayEquity,                  // target account equity value per day, in currency
             PlanDayProfit                   // target daily profit, in currency
             ;

   double    CurrentEquity,                  // current equity value
             CurrentBallance;                // current balance

入力されたパラメータに基づいて、預金通貨の期間ごとのリスク限度を計算する便宜のために、アクセス修飾子をprotected にして、クラス内でRefreshLimits()メソッドを宣言します。このメソッドをクラス外で次のように説明しましょう。将来的に、得られたデータの正しさを確認する機能を備えたメソッドを拡張する必要がある場合に備えて、bool型の戻り値の型を提供する予定です。とりあえず、このメソッドを次のような形で説明します。

//+------------------------------------------------------------------+
//|                        RefreshLimits                             |
//+------------------------------------------------------------------+
bool RiskManagerBase::RefreshLimits(void)
  {
   CurrentEquity    = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2);   // request current equity value
   CurrentBallance  = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2);  // request current balance

   StartBalance     = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2);  // set start balance
   StartEquity      = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2);   // request current equity value

   PlanDayProfit    = NormalizeDouble(StartEquity * plandayprofit/100,2);     // target daily profit, in currency
   PlanDayEquity    = NormalizeDouble(StartEquity + PlanDayProfit/100,2);     // target equity, in currency

   RiskPerDay       = NormalizeDouble(StartEquity * riskperday/100,2);        // risk per day in currency
   RiskPerWeek      = NormalizeDouble(StartEquity * riskperweek/100,2);       // risk per week in currency
   RiskPerMonth     = NormalizeDouble(StartEquity * riskpermonth/100,2);      // risk per month in currency

   return(true);
  }

便利な方法は、期間を変更する際に限度値を再計算する必要があるときや、クラスのコンストラクタを呼び出す際にフィールドの値を最初に変更するときに、コード内でこのメソッドを毎回呼び出すことです。フィールドの開始値を初期化するために、クラスのコンストラクタに以下のコードを書きます。

//+------------------------------------------------------------------+
//|                        RiskManagerBase                           |
//+------------------------------------------------------------------+
RiskManagerBase::RiskManagerBase()
  {
   riskperday         = inp_riskperday;                                 // set the value for the internal variable
   riskperweek        = inp_riskperweek;                                // set the value for the internal variable
   riskpermonth       = inp_riskpermonth;                               // set the value for the internal variable
   plandayprofit      = inp_plandayprofit;                              // set the value for the internal variable

   RefreshLimits();                                                     // update limits
  }

入力パラメータのロジックとクラスの開始データ状態を決めたら、次は限度計算の実装に移ります。


リスク制限期間との連携

リスク制限期間と連動させるには、protectedアクセスタイプを持つ追加の変数が必要です。まず、設定されたリスク制限に到達した際のデータを格納するbool型変数の形式で各期間の独自のフラグと、すべての制限が同時に利用可能な場合にのみ取引を続行できる可能性を通知するメイン フラグを宣言します。これは、1ヶ月の限度をすでに超えているにもかかわらず、1日の限度が残っているために取引が許可されているといった状況を避けるために必要なことです。これにより、次の時間帯までに限度に達した場合ごとに、取引が制限されます。毎日の利益と新しい取引日の開始を制御するために、同じ型の変数も必要です。さらに、各期間(日、週、月)の実際の損益情報を格納するために、double型のフィールドを追加します。さらに、取引業務におけるスワップと手数料については、個別の価値を提供します。

   bool              RiskTradePermission;    // general variable - whether opening of new trades is allowed
   bool              RiskDayPermission;      // flag prohibiting trading if daily limit is reached
   bool              RiskWeekPermission;     // flag to prohibit trading if daily limit is reached
   bool              RiskMonthPermission;    // flag to prohibit trading if monthly limit is reached

   bool              DayProfitArrive;        // variable to control if daily target profit is achieved
   bool              NewTradeDay;            // variable for a new trading day

   //--- actual limits
   double            DayorderLoss;           // accumulated daily loss
   double            DayorderProfit;         // accumulated daily profit
   double            WeekorderLoss;          // accumulated weekly loss
   double            WeekorderProfit;        // accumulated weekly profit
   double            MonthorderLoss;         // accumulated monthly loss
   double            MonthorderProfit;       // accumulated monthly profit
   double            MonthOrderSwap;         // monthly swap
   double            MonthOrderCommis;       // monthly commission

将来的に意思決定ツールから発生する損失と、異なるブローカーの手数料およびスワップ要件に関連する損失を分離できるようにするため、特に手数料およびスワップから発生する費用を対応する期間の損失に含めていません。さて、クラスの対応するフィールドを宣言したので、次は制限の使用を制御しましょう。


制限の使用を制御する

限度の実際の使用を制御するためには、新しい期間の開始に関するイベントと、取引操作の完了に関するイベントを処理する必要があります。実際に使用された限度を正しく追跡するために、クラスのprotectedアクセス領域で内部メソッドForOnTrade()を発表します。

まず、現在時刻と日、週、月の開始時刻を表す変数をメソッドに用意する必要があります。このような目的のために、MqlDateTime形式のstruct構造型の特別な定義済みデータ型を使用します。さっそく次のような形で、現在の終了時刻で初期化します。

   MqlDateTime local, start_day, start_week, start_month;               // create structure to filter dates
   TimeLocal(local);                                                    // fill in initially
   TimeLocal(start_day);                                                // fill in initially
   TimeLocal(start_week);                                               // fill in initially
   TimeLocal(start_month);                                              // fill in initially

現在の時刻を最初に初期化するには、定義済み関数TimeCurrent()ではなくTimeLocal()を使用することに注意してください。これは、後者はローカル時間を使用する一方、前者はブローカーから受信した最後のティックから時間を取得するため、異なるブローカー間のタイムゾーンの違いにより制限が不正確に計算される可能性があるためです。次に、各期間の開始時刻をリセットして、それぞれの開始日の値を取得する必要があります。そのためには、構造体のpublicフィールドに以下のようにアクセスします。

//--- reset to have the report from the beginning of the period
   start_day.sec     = 0;                                               // from the day beginning
   start_day.min     = 0;                                               // from the day beginning
   start_day.hour    = 0;                                               // from the day beginning

   start_week.sec    = 0;                                               // from the week beginning
   start_week.min    = 0;                                               // from the week beginning
   start_week.hour   = 0;                                               // from the week beginning

   start_month.sec   = 0;                                               // from the month beginning
   start_month.min   = 0;                                               // from the month beginning
   start_month.hour  = 0;                                               // from the month beginning

週と月のデータを正しく取得するには、週と月の始まりを見つけるロジックを定義する必要があります。月の場合、月はすべて1日から始まることがわかっているので、すべてが単純です。1週間を扱うのは少し複雑です。特定の報告ポイントがなく、日付も毎回変わるからです。ここでは、MqlDateTime構造体の特別なday_of_weekフィールドを使用することができます。現在の日付から、ゼロから始まる週番号を取得できるので、次のように簡単に現在の週の開始日を知ることができます。

//--- determining the beginning of the week
   int dif;                                                             // day of week difference variable
   if(start_week.day_of_week==0)                                        // if this is the first day of the week
     {
      dif = 0;                                                          // then reset
     }
   else
     {
      dif = start_week.day_of_week-1;                                   // if not the first, then calculate the difference
      start_week.day -= dif;                                            // subtract the difference at the beginning of the week from the number of the day
     }

//---month
   start_month.day         = 1;                                         // everything is simple with the month

現在時点を基準とした各期間の正確な開始日がわかったので、口座で実行された取引の履歴データの要求に進むことができます。最初に、クローズされた注文を考慮するために必要な変数を宣言し、選択された各期間について取引の財務結果が収集される変数の値をリセットする必要があります。

//---
   uint     total  = 0;                                                 // number of selected trades
   ulong    ticket = 0;                                                 // order number
   long     type;                                                       // order type
   double   profit = 0,                                                 // order profit
            commis = 0,                                                 // order commission
            swap   = 0;                                                 // order swap

   DayorderLoss      = 0;                                               // daily loss without commission
   DayorderProfit    = 0;                                               // daily profit
   WeekorderLoss     = 0;                                               // weekly loss without commission
   WeekorderProfit   = 0;                                               // weekly profit
   MonthorderLoss    = 0;                                               // monthly loss without commission
   MonthorderProfit  = 0;                                               // monthly profit
   MonthOrderCommis  = 0;                                               // monthly commission
   MonthOrderSwap    = 0;                                               // monthly swap

定義済みの端末関数HistorySelect()を使用して、決済済み注文の履歴データを要求します。この関数のパラメータには、各期間について、先ほど受け取った日付が使用されます。そのためには、MqlDateTime変数の型を、パラメータHistorySelect()関数で要求される型(datetime)に変更する必要があります。そのために、定義済みの端末関数StructToTime() を使用します。取引データについても同様に、必要な期間の始期と終期を必要な値に置き換えて要求します。

HistorySelect()関数を呼び出すたびに、定義済みの端末関数HistoryDealsTotal()を使用して選択された注文の数を取得し、この値をローカル変数totalに入れる必要があります。クローズ済み取引の数を取得した後、for演算子でループを構成し、定義済みの端末関数HistoryDealGetTicket()で各注文の件数を要求します。これにより、各注文のデータにアクセスできるようになります。事前定義された端末関数HistoryDealGetDouble()およびHistoryDealGetInteger()を使用して各注文のデータにアクセスし、以前に受信した注文番号を渡します。ENUM_DEAL_PROPERTY_INTEGER列挙体およびENUM_DEAL_PROPERTY_DOUBLE列挙体から、対応する取引プロパティ識別子を指定する必要があります。また、ENUM_DEAL_TYPE列挙体からDEAL_TYPE_BUYDEAL_TYPE_SELLの値を確認し、残高取引やボーナス発生などの他の口座操作をフィルタリングすることにより、取引操作からの取引のみを考慮する場合はブール選択演算子によるフィルタを追加する必要があります。というわけで、データを選択するコードは次のようになります。

//--- now select data by --==DAY==--
   HistorySelect(StructToTime(start_day),StructToTime(local));          // select required history 
//--- check
   total  = HistoryDealsTotal();                                        // number number of selected deals
   ticket = 0;                                                          // order number
   profit = 0;                                                          // order profit
   commis = 0;                                                          // order commission
   swap   = 0;                                                          // order swap

//--- for all deals
   for(uint i=0; i<total; i++)                                          // loop through all selected orders
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // get the number of each in order
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // get data on financial results
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // get data on commission
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // get swap data
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // get data on operation type

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // if the deal is form a trading operatoin
           {
            if(profit>0)                                                // if financial result of current order is greater than 0,
              {
               DayorderProfit += profit;                                // add to profit
              }
            else
              {
               DayorderLoss += MathAbs(profit);                         // if loss, add up
              }
           }
        }
     }

//--- now select data by --==WEEK==--
   HistorySelect(StructToTime(start_week),StructToTime(local));         // select the required history
//--- check
   total  = HistoryDealsTotal();                                        // number number of selected deals
   ticket = 0;                                                          // order number
   profit = 0;                                                          // order profit
   commis = 0;                                                          // order commission
   swap   = 0;                                                          // order swap

//--- for all deals
   for(uint i=0; i<total; i++)                                          // loop through all selected orders
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // get the number of each in order
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // get data on financial results
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // get data on commission
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // get swap data
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // get data on operation type

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // if the deal is form a trading operatoin
           {
            if(profit>0)                                                // if financial result of current order is greater than 0,
              {
               WeekorderProfit += profit;                               // add to profit
              }
            else
              {
               WeekorderLoss += MathAbs(profit);                        // if loss, add up
              }
           }
        }
     }

//--- now select data by --==MONTH==--
   HistorySelect(StructToTime(start_month),StructToTime(local));        // select the required history
//--- check
   total  = HistoryDealsTotal();                                        // number number of selected deals
   ticket = 0;                                                          // order number
   profit = 0;                                                          // order profit
   commis = 0;                                                          // order commission
   swap   = 0;                                                          // order swap

//--- for all deals
   for(uint i=0; i<total; i++)                                          // loop through all selected orders
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // get the number of each in order
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // get data on financial results
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // get data on commission
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // get swap data
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // get data on operation type

         MonthOrderSwap    += swap;                                     // sum up swaps
         MonthOrderCommis  += commis;                                   // sum up commissions

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // if the deal is form a trading operatoin
           {
            if(profit>0)                                                // if financial result of current order is greater than 0,
              {
               MonthorderProfit += profit;                              // add to profit
              }
            else
              {
               MonthorderLoss += MathAbs(profit);                       // if loss, sum up
              }
           }
        }
     }

上記のメソッドは、現在の制限使用値を更新する必要があるたびに呼び出すことができます。この関数を呼び出すだけでなく、実際の制限の値を更新することもできます。このメソッドのポイントは制限を更新することなので、TradeTradeTransactionのような現在の注文の変更に関連するイベントが発生したときや、NewTickイベントで新しいティックが出現したときに実行することができます。これらのメソッドは非常にリソース効率が良いので、ティックごとに実際の制限を更新します。次に、動的キャンセルと取引解決に関連するイベントを処理するために必要なイベントハンドラを実装してみましょう。


クラスイベントハンドラ

イベントを処理するために、ContoEvents()クラスの内部メソッドをprotectedアクセスレベルで定義します。そのために、同じアクセスレベルを持つ補助フィールドを追加宣言します。取引許可フラグを変更するために必要な、新しい取引期間の開始時刻を即座に追跡できるようにするには、最後に記録した期間と現在の期間の値を保存する必要があります。このような目的のために、datetimeデータ型で宣言された単純な配列を使用して、対応する期間の値を格納することができます。

   //--- additional auxiliary arrays
   datetime          Periods_old[3];         // 0-day,1-week,2-mn
   datetime          Periods_new[3];         // 0-day,1-week,2-mn

1次元目は日、2次元目には週、3次元目には月の値を格納します。管理期間をさらに拡張する必要がある場合は、これらの配列を静的にではなく、動的に宣言することができますが、ここでは3つの期間しか扱いません。では、クラスのコンストラクタに、これらの配列変数の一次初期化を以下のように追加してみましょう。

   Periods_new[0] = iTime(_Symbol, PERIOD_D1, 1);                       // initialize the current day with the previous period
   Periods_new[1] = iTime(_Symbol, PERIOD_W1, 1);                       // initialize the current week with the previous period
   Periods_new[2] = iTime(_Symbol, PERIOD_MN1, 1);                      // initialize the current month with the previous period

対応する各期間を、定義済みの終端関数iTime()を使用して初期化します。パラメータには、現在の期間の前の期間からENUM_TIMEFRAMESの対応する期間を渡します。Periods_old[]配列はあえて初期化していません。この場合、コンストラクタとContoEvents()メソッドを呼び出した後、新しい取引期間の開始イベントがトリガーされ、取引開始のためのすべてのフラグがオープンされ、制限が残っていない場合にのみコードによってクローズされるようにします。そうしないと、再初期化したときにクラスが正しく動作しない可能性があります。もし現在の期間が前の期間と等しくなければ、それは新しい対応する期間が始まったことを意味し、フラグの値を変更することで限度をリセットし、取引を許可することができます。また、各期間ごとに、先に説明したRefreshLimits()メソッドを呼び出して、入力限度を再計算します。

//+------------------------------------------------------------------+
//|                     ContoEvents                                  |
//+------------------------------------------------------------------+
void RiskManagerBase::ContoEvents()
  {
// check the start of a new trading day
   NewTradeDay    = false;                                              // variable for new trading day set to false
   Periods_old[0] = Periods_new[0];                                     // copy to old, new
   Periods_new[0] = iTime(_Symbol, PERIOD_D1, 0);                       // update new for day
   if(Periods_new[0]!=Periods_old[0])                                   // if do not match, it's a new day
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade day!");  // inform
      NewTradeDay = true;                                               // variable to true

      DayProfitArrive     = false;                                      // reset flag of reaching target profit after a new day started
      RiskDayPermission = true;                                         // allow opening new positions

      RefreshLimits();                                                  // update limits

      DayorderLoss = 0;                                                 // reset daily financial result
      DayorderProfit = 0;                                               // reset daily financial result
     }

// check the start of a new trading week
   Periods_old[1]    = Periods_new[1];                                  // copy data to old period
   Periods_new[1]    = iTime(_Symbol, PERIOD_W1, 0);                    // fill new period for week
   if(Periods_new[1]!= Periods_old[1])                                  // if periods do not match, it's a new week
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade week!"); // inform

      RiskWeekPermission = true;                                        // allow opening new positions

      RefreshLimits();                                                  // update limits

      WeekorderLoss = 0;                                                // reset weekly losses
      WeekorderProfit = 0;                                              // reset weekly profits
     }

// check the start of a new trading month
   Periods_old[2]    = Periods_new[2];                                  // copy the period to the old one
   Periods_new[2]    = iTime(_Symbol, PERIOD_MN1, 0);                   // update new period for month
   if(Periods_new[2]!= Periods_old[2])                                  // if do not match, it's a new month
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade Month!");   // inform

      RiskMonthPermission = true;                                       // allow opening new positions

      RefreshLimits();                                                  // update limits

      MonthorderLoss = 0;                                               // reset the month's loss
      MonthorderProfit = 0;                                             // reset the month's profit
     }

// set the permission to open new positions true only if everything is true
// set to true
   if(RiskDayPermission    == true &&                                   // if there is a daily limit available
      RiskWeekPermission   == true &&                                   // if there is a weekly limit available
      RiskMonthPermission  == true                                      // if there is a monthly limit available
     )                                                                  //
     {
      RiskTradePermission=true;                                         // if all are allowed, trading is allowed
     }

// set to false if at least one of them is false
   if(RiskDayPermission    == false ||                                  // no daily limit available
      RiskWeekPermission   == false ||                                  // or no weekly limit available
      RiskMonthPermission  == false ||                                  // or no monthly limit available
      DayProfitArrive      == true                                      // or target profit is reached
     )                                                                  // then
     {
      RiskTradePermission=false;                                        // prohibit trading
     }
   }

また、このメソッドでは、新規ポジションを建てる可能性を示すフラグのメイン変数であるRiskTradePermissionのデータの状態に対する制御を追加しました。論理的な選択演算子によって、すべての権限がtrueの場合のみ、この変数を通して権限を有効にし、少なくとも1つのフラグが取引を許可していない場合は無効にします。この変数は、すでに作成されているアルゴリズムEAにこのクラスを統合する場合に非常に便利です。ゲッターを介してこの変数を受け取り、注文を出すための条件をコードに挿入するだけです。私たちの場合、これは単に自由な取引制限がないことをユーザーに知らせ始めるためのフラグとして機能します。さて、このクラスでは、指定した損失が達成されたときにリスクを制御する方法を「学んだ」ので、次は目標利益の達成を制御する機能の実装に移りましょう。


1日の目標利益を制御する仕組み

これまでの記事で、目標利益に対する制御を開始するためのフラグと、口座の預金額に対するその値を決定するための入力変数を宣言しました。目標利益の達成を制御する当クラスのロジックによれば、すべてのポジションの合計利益が目標値に達した場合、すべての未決済ポジションはクローズされます。口座のすべてのポジションをクローズするには、クラスで内部メソッド AllOrdersClose() をpublicアクセスレベルで宣言します。このメソッドを機能させるには、ポジションのデータを受け取り、自動的に決済注文を送信する必要があります。 

この機能を独自に実装して時間を浪費しないために、端末の既製の内部クラスを使用することにします。ポジションを扱うには内部標準の端末クラスCPositionInfoを使用し、ポジションをクローズするにはCTradeクラスを使用します。この2つのクラスの変数も、デフォルトのコンストラクタでポインタを使わずに、protectedアクセスレベルで次のように宣言してみましょう。

   CTrade            r_trade;                // instance
   CPositionInfo     r_position;             // instance

これらのオブジェクトを今必要な機能の枠組みの中で扱う場合、追加で設定する必要はないので、クラスのコンストラクタに書くことはありません。以下は、宣言されたクラスを使用したこのメソッドの実装です。

//+------------------------------------------------------------------+
//|                       AllOrdersClose                             |
//+------------------------------------------------------------------+
bool RiskManagerBase::AllOrdersClose()                                  // closing market positions
  {
   ulong ticket = 0;                                                    // order ticket
   string symb;

   for(int i = PositionsTotal(); i>=0; i--)                             // loop through open positoins
     {
      if(r_position.SelectByIndex(i))                                   // if a position selected
        {
         ticket = r_position.Ticket();                                  // remember position ticket

         if(!r_trade.PositionClose(ticket))                             // close by ticket
           {
            Print(__FUNCTION__+". Error close order. "+IntegerToString(ticket)); // if not, inform
            return(false);                                              // return false
           }
         else
           {
            Print(__FUNCTION__+". Order close success. "+IntegerToString(ticket)); // if not, inform
            continue;                                                   // if everything is ok, continue
           }
        }
     }
   return(true);                                                        // return true
  }

目標利益が達成されたときと、制限に達したときの両方で、説明したメソッドを呼び出します。また、注文決済を送信する際にエラーを処理する必要がある場合に備えて、bool値も返します。目標利益が達成されたかどうかを制御する機能を提供するために、イベント処理メソッドContoEvents()を、すでに上で説明したコードの直後に、以下のコードで補足します。

//--- daily
   if(dayProfitControl)							// check if functionality is enabled by the user
     {
      if(CurrentEquity >= (StartEquity+PlanDayProfit))                  // if equity exceeds or equals start + target profit,
        {
         DayProfitArrive = true;                                        // set flag that target profit is reached
         Print(__FUNCTION__+", PlanDayProfit has been arrived.");       // inform about the event
         Print(__FUNCTION__+", CurrentEquity = "+DoubleToString(CurrentEquity)+
               ", StartEquity = "+DoubleToString(StartEquity)+
               ", PlanDayProfit = "+DoubleToString(PlanDayProfit));
         AllOrdersClose();                                              // close all open orders

         StartEquity = CurrentEquity;                                   // rewrite starting equity value

         //--- send a push notification
         ResetLastError();                                              // reset the last error
         if(!SendNotification("The planned profitability for the day has been achieved. Equity: "+DoubleToString(CurrentEquity)))// notification
           {
            Print(__FUNCTION__+IntegerToString(__LINE__)+", Error of sending notification: "+IntegerToString(GetLastError()));// if not, print
           }
        }
     }

このメソッドには、このイベントが発生したことを通知するためにユーザーにプッシュ通知を送信することが含まれます。そのために、定義済みの端末関数SendNotificationを使用します。このクラスの最低限必要な機能を完成させるには、publicアクセスを持つクラスメソッドをもうひとつ組み立てるだけです。このメソッドは、リスクマネージャーがEAの構造に接続されたときに呼び出されます。


EA構造における監視開始メソッドの定義

リスクマネージャークラスのインスタンスからEA構造に監視機能を追加するために、ContoMonitor() publicメソッドを宣言します。このメソッドでは、以前に宣言されたすべてのイベント処理メソッドを収集し、また、実際に使用された制限値を、入力パラメータでユーザーが承認した値と比較する機能で補足します。このメソッドをpublicアクセスレベルで宣言し、クラスの外部に次のように記述してみましょう。

//+------------------------------------------------------------------+
//|                       ContoMonitor                               |
//+------------------------------------------------------------------+
void RiskManagerBase::ContoMonitor()                                    // monitoring
  {
   ForOnTrade();                                                        // update at each tick

   ContoEvents();                                                       // event block

//---
   double currentProfit = AccountInfoDouble(ACCOUNT_PROFIT);
   
   if((MathAbs(DayorderLoss)+MathAbs(currentProfit) >= RiskPerDay &&    // if equity is less than or equal to the start balance minus the daily risk
       currentProfit<0                                            &&    // profit below zero
       RiskDayPermission==true)                                         // day trading is allowed
      ||                                                                // OR
      (RiskDayPermission==true &&                                       // day trading is allowed
       MathAbs(DayorderLoss) >= RiskPerDay)                             // loss exceed daily risk
   )                                                                    

     {
      Print(__FUNCTION__+", EquityControl, "+"ACCOUNT_PROFIT = "  +DoubleToString(currentProfit));// notify
      Print(__FUNCTION__+", EquityControl, "+"RiskPerDay = "      +DoubleToString(RiskPerDay));   // notify
      Print(__FUNCTION__+", EquityControl, "+"DayorderLoss = "    +DoubleToString(DayorderLoss)); // notify
      RiskDayPermission=false;                                          // prohibit opening new orders during the day
      AllOrdersClose();                                                 // close all open positions
     }

// check if there is a WEEK limit available for opening a new position if there are no open ones
   if(
      MathAbs(WeekorderLoss)>=RiskPerWeek &&                            // if weekly loss is greater than or equal to the weekly risk
      RiskWeekPermission==true)                                         // and we traded
     {
      RiskWeekPermission=false;                                         // prohibit opening of new orders during the day
      AllOrdersClose();                                                 // close all open positions

      Print(__FUNCTION__+", EquityControl, "+"WeekorderLoss = "+DoubleToString(WeekorderLoss));  // notify
      Print(__FUNCTION__+", EquityControl, "+"RiskPerWeek = "+DoubleToString(RiskPerWeek));      // notify
     }

// check if there is a MONTH limit available for opening a new position if there are no open ones
   if(
      MathAbs(MonthorderLoss)>=RiskPerMonth &&                          // if monthly loss is greater than or equal to the monthly risk
      RiskMonthPermission==true)                                        // we traded
     {
      RiskMonthPermission=false;                                        // prohibit opening of new orders during the day
      AllOrdersClose();                                                 // close all open positions

      Print(__FUNCTION__+", EquityControl, "+"MonthorderLoss = "+DoubleToString(MonthorderLoss));  // notify
      Print(__FUNCTION__+", EquityControl, "+"RiskPerMonth = "+DoubleToString(RiskPerMonth));      // notify
     }
  }

このメソッドの動作ロジックは非常にシンプルです。月または週の実際の損失限度がユーザーによって設定されたものを超えた場合、指定された期間の取引フラグが禁止に設定され、それに応じて取引が禁止されます。唯一の違いは日次の限度で、そこではポジションの存在も制御する必要があります。このため、論理演算子ORを通じて、ポジションからの現在の利益の制御も追加します。リスクの限度に達したら、ポジションをクローズするメソッドを呼び出し、このイベントに関するログを表示します。

この段階で、クラスを完全に完成させるには、ユーザーが現在の制限を制御するためのメソッドを追加するだけです。最もシンプルで便利な方法は、標準の定義済み端末関数Comment()を使用して必要な情報を表示することです。この関数を使用するには、チャートに表示する情報を含む文字列型のパラメータを渡す必要があります。私たちのクラスからこれらの値を取得するために、publicアクセスレベルでMessage()メソッドを宣言します。これは、ユーザーが必要とするすべての変数に関する収集されたデータを含む文字列 データを返します。

//+------------------------------------------------------------------+
//|                        Message                                   |
//+------------------------------------------------------------------+
string RiskManagerBase::Message(void)
  {
   string msg;                                                          // message

   msg += "\n"+" ----------Risk-Manager---------- ";                    // common
//---
   msg += "\n"+"RiskTradePer = "+(string)RiskTradePermission;           // final trade permission
   msg += "\n"+"RiskDayPer   = "+(string)RiskDayPermission;             // daily risk available
   msg += "\n"+"RiskWeekPer  = "+(string)RiskWeekPermission;            // weekly risk available
   msg += "\n"+"RiskMonthPer = "+(string)RiskMonthPermission;           // monthly risk available

//---limits and inputs
   msg += "\n"+" -------------------------------- ";                    //
   msg += "\n"+"RiskPerDay   = "+DoubleToString(RiskPerDay,2);          // daily risk in usd
   msg += "\n"+"RiskPerWeek  = "+DoubleToString(RiskPerWeek,2);         // weekly risk in usd
   msg += "\n"+"RiskPerMonth = "+DoubleToString(RiskPerMonth,2);        // monthly risk usd
//--- current profits and losses for periods
   msg += "\n"+" -------------------------------- ";                    //
   msg += "\n"+"DayLoss     = "+DoubleToString(DayorderLoss,2);         // daily loss
   msg += "\n"+"DayProfit   = "+DoubleToString(DayorderProfit,2);       // daily profit
   msg += "\n"+"WeekLoss    = "+DoubleToString(WeekorderLoss,2);        // weekly loss
   msg += "\n"+"WeekProfit  = "+DoubleToString(WeekorderProfit,2);      // weekly profit
   msg += "\n"+"MonthLoss   = "+DoubleToString(MonthorderLoss,2);       // monthly loss
   msg += "\n"+"MonthProfit = "+DoubleToString(MonthorderProfit,2);     // monthly profit
   msg += "\n"+"MonthCommis = "+DoubleToString(MonthOrderCommis,2);     // monthly commissions
   msg += "\n"+"MonthSwap   = "+DoubleToString(MonthOrderSwap,2);       // monthly swaps
//--- for current monitoring

   if(dayProfitControl)                                                 // if control daily profit
     {
      msg += "\n"+" ---------dayProfitControl-------- ";                //
      msg += "\n"+"DayProfitArrive = "+(string)DayProfitArrive;         // daily profit achieved
      msg += "\n"+"StartBallance   = "+DoubleToString(StartBalance,2);  // starting balance
      msg += "\n"+"PlanDayProfit   = "+DoubleToString(PlanDayProfit,2); // target profit
      msg += "\n"+"PlanDayEquity   = "+DoubleToString(PlanDayEquity,2); // target equity
     }
   return(msg);                                                         // return value
  }

このメソッドで作成されたユーザーのメッセージは次のようになります。

図2:データ出力形式

図2:データ出力形式

このメソッドは、端末でグラフィックを扱うための要素を追加することによって、変更または補足することができます。しかし、このように使用することで、ユーザーにクラスから判断を下すのに十分なデータを提供することができるからです。必要であれば、将来この形式を改良し、グラフィックの面でより美しくすることもできます。ここで、個別取引戦略を使用する際に、このクラスを拡張する可能性について説明しましょう。


最終的な実装とクラス拡張の可能性

先に述べたように、ここで説明した機能は必要最低限であり、ほとんどすべての取引戦略にとって最も普遍的なものです。一日のリスクを制御し、預金の損失を防ぐことができます。この部分では、このクラスを拡大する可能性をさらにいくつか見ていきます。 

  • 売りストップロスで取引する場合、スプレッドのサイズを制御
  • ポジションのスリッページを制御
  • 目標月利益を制御

最初の点については、売りのストップロスでの取引を使用する取引システムのための追加機能を実装することができます。SpreadMonitor(int intSL)メソッドを宣言することができます。このメソッドは、テクニカルまたは計算されたストップロスをパラメータとして受け取り、現在のスプレッドレベルと比較します。このメソッドでは、スプレッドがストップロスに対してユーザーが決めた割合で大きく広がった場合、注文を出すことを禁止し、スプレッドによるストップロスでポジションを決済する高いリスクを回避します。

オープン時のスリッページを制御するには、2番目のポイントに従って、SlippageCheck()メソッドを宣言します。このメソッドは、ブローカーが、取引リスクが期待値を上回ったために、提示された価格と大きく異なる価格で取引を開始した場合、個々の取引をクローズします。これにより、ストップロスがトリガーされた場合でも、1回のエントリごとにハイリスク取引をして統計を台無しにすることがなくなります。また、ストップロスとテイクプロフィットの比率を固定して取引する場合、この比率はスリッページによって悪化するため、後で大きな損失を被るよりは、小さな損失でポジションを決済した方がよいです。

日次利益を制御するロジックと同様に、月次目標利益を制御するための対応するメソッドを実装することが可能です。このメソッドは、長期的な戦略を取引する際に利用できます。説明したクラスは、手動による日中取引で使用するために必要なすべての機能をすでに備えており、取引EAの最終的な実装に統合することができます。これは、手動取引の開始と同時に、商品チャート上で起動する必要があります。

プロジェクトの最終的な組み立てには、#includeプリプロセッサー指令を使用したクラスの接続が含まれます。

#include <RiskManagerBase.mqh>

次に、グローバルレベルでリスクマネージャーオブジェクトのポインタを宣言します。

RiskManagerBase *RMB;

EAを初期化する際、手動でオブジェクトのメモリを確保し、起動前に準備します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

   RMB = new RiskManagerBase();

//---
   return(INIT_SUCCEEDED);
  }

チャートからEAを取り除く際には、メモリリークを避けるためにオブジェクトからメモリをクリアする必要があります。そのために、EAのOnDeinit関数に次のように記述します。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

   delete RMB;

  }

また、必要であれば、同じイベントでComment(" ")メソッドを呼び出し、そこに空の文字列を渡すことで、EAが銘柄チャートから削除されたときにチャートのコメントが消去されるようにすることもできます。

銘柄の新しいティックを受け取ったら、クラスのメイン監視メソッドを呼び出します。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   RMB.ContoMonitor();

   Comment(RMB.Message());
  }
//+------------------------------------------------------------------+

これでリスクマネジャーを組み込んだEAの組み立ては完了し、使用する準備が完全に整いました(ファイルManualRiskManager.mq5)。いくつかの使用例をテストするために、現在のコードにちょっとした追加をおこない、手動取引のプロセスをシミュレートします。


使用例

リスクマネージャーを使用した場合と使用しなかった場合の手動取引のプロセスを視覚化するためには、手動取引をモデル化する追加コードが必要になります。この記事では、取引戦略の選択については触れないので、コードで完全な取引機能は実装しません。その代わりに、日足チャートから視覚的にエントリを取得し、EAに既製のデータを追加します。非常に単純な戦略を使用して取引判断をおこない、この戦略の最終的な財務結果を、リスク制御の有無という唯一の違いで見ていきます。

エントリの例として、USDJPYの商品について、2ヶ月間にわたるフラクタルレベルのブレイクアウトを使用したシンプルな戦略を使用します。この戦略が、リスク制御の有無にかかわらず、どのように機能するか見てみましょう。手動エントリのシグナルは以下のようになります。

図3:テスト戦略を使用したエントリ

図3:テスト戦略によるエントリ

この戦略をモデル化するために、あらゆる手動戦略のための普遍的なユニットテストとして、小さな追加項目を書いてみましょう。このテストでは、エントリのロジックを実装せずに、準備完了信号を戦略に読み込みます。そのためにはまず、フラクタルベースのエントリを格納する追加の構造体structを宣言する必要があります。

//+------------------------------------------------------------------+
//|                         TradeInputs                              |
//+------------------------------------------------------------------+
struct TradeInputs
  {
   string             symbol;                                           // symbol
   ENUM_POSITION_TYPE direction;                                        // direction
   double             price;                                            // price
   datetime           tradedate;                                        // date
   bool               done;                                             // trigger flag
  };

取引シグナルのモデリングを担当する主要クラスは TradeModel です。このクラスのコンストラクタは、シグナル入力パラメータを持つコンテナを受け取り、メインのProcessing()メソッドは、入力値に基づいてエントリポイントの時刻が来たかどうかを毎ティック監視します。日中取引をシミュレートしているので、一日の終わりには、リスクマネージャークラスで先に宣言したAllOrdersClose()メソッドを使用してすべてのポジションを削除します。これが補助クラスです。

//+------------------------------------------------------------------+
//|                        TradeModel                                |
//+------------------------------------------------------------------+
class TradeModel
  {
protected:

   CTrade               *cTrade;                                        // to trade
   TradeInputs       container[];                                       // container of entries

   int               size;                                              // container size

public:
                     TradeModel(const TradeInputs &inputs[]);
                    ~TradeModel(void);

   void              Processing();                                      // main modeling method
  };

便利な発注を可能にするため、必要な機能をすべて備えた標準端末クラスCTradeを使用します。これで補助クラスの開発にかかる時間を短縮できます。クラスインスタンスを生成するときに入力パラメータを渡すために、エントリコンテナの入力パラメータを1つ持つコンストラクタを定義します。

//+------------------------------------------------------------------+
//|                          TradeModel                              |
//+------------------------------------------------------------------+
TradeModel::TradeModel(const TradeInputs &inputs[])
  {
   size = ArraySize(inputs);                                            // get container size
   ArrayResize(container, size);                                        // resize

   for(int i=0; i<size; i++)                                            // loop through inputs
     {
      container[i] = inputs[i];                                         // copy to internal
     }

//--- trade class
   cTrade=new CTrade();                                                 // create trade instance
   if(CheckPointer(cTrade)==POINTER_INVALID)                            // if instance not created,
     {
      Print(__FUNCTION__+IntegerToString(__LINE__)+" Error creating object!");   // notify
     }
   cTrade.SetTypeFillingBySymbol(Symbol());                             // fill type for the symbol
   cTrade.SetDeviationInPoints(1000);                                   // deviation
   cTrade.SetExpertMagicNumber(123);                                    // magic number
   cTrade.SetAsyncMode(false);                                          // asynchronous method
  }

コンストラクタでは、入力パラメータのコンテナーを希望する値で初期化し、そのサイズを記憶させ、必要な設定でCTradeクラスのオブジェクトを作成します。ここでのパラメータのほとんどは、ユニットテストを作成する目的には影響しないので、ユーザーが設定することはありません。 

TradeModel クラスのデストラクタはCTradeオブジェクトを削除するだけです。

//+------------------------------------------------------------------+
//|                         ~TradeModel                              |
//+------------------------------------------------------------------+
TradeModel::~TradeModel(void)
  {
   if(CheckPointer(cTrade)!=POINTER_INVALID)                            // if there is an instance,
     {
      delete cTrade;                                                    // delete
     }
  }

それでは、プロジェクト全体の構造の中で、クラスの操作のための主要な処理メソッドを実装していきましょう。図3に従って注文を出すロジックを実装してみましょう。

//+------------------------------------------------------------------+
//|                         Processing                               |
//+------------------------------------------------------------------+
void TradeModel::Processing(void)
  {
   datetime timeCurr = TimeCurrent();                                   // request current time

   double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);                  // take bid
   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // take ask

   for(int i=0; i<size; i++)                                            // loop through inputs
     {
      if(container[i].done==false &&                                    // if we haven't traded yet AND
         container[i].tradedate <= timeCurr)                            // date is correct
        {
         switch(container[i].direction)                                 // check trade direction
           {
            //---
            case  POSITION_TYPE_BUY:                                    // if Buy,
               if(container[i].price >= ask)                            // check if price has reached and
                 {
                  if(cTrade.Buy(0.1))                                   // by the same lot
                    {
                     container[i].done = true;                          // if time has passed, put a flag
                     Print("Buy has been done");                        // notify
                    }
                  else                                                  // if hasn't passed,
                    {
                     Print("Error: buy");                               // notify
                    }
                 }
               break;                                                   // complete the case
            //---
            case  POSITION_TYPE_SELL:                                   // if Sell
               if(container[i].price <= bid)                            // check if price has reached and
                 {
                  if(cTrade.Sell(0.1))                                  // sell the same lot
                    {
                     container[i].done = true;                          // if time has passed, put a flag
                     Print("Sell has been done");                       // notify
                    }
                  else                                                  // if hasn't passed,
                    {
                     Print("Error: sell");                              // notify
                    }
                 }
               break;                                                   // complete the case

            //---
            default:
               Print("Wrong inputs");                                   // notify
               return;
               break;
           }
        }
     }
  }

この方法の理屈はいたってシンプルです。モデリング時間が到来したコンテナ内に未処理のエントリがあれば、図3でマークされたフラクタル方向と価格に従って、これらの注文を発注します。この機能はリスクマネージャーをテストするのに十分なので、メインプロジェクトに統合することができます。

まず、テストクラスとESコードを次のように接続してみましょう。

#include <TradeModel.mqh>

OnInit()関数の中で、TradeInputsデータ配列構造体のインスタンスを作成し、この配列をTradeModelクラスのコンストラクタに渡して初期化します。

//---
   TradeInputs modelInputs[] =
     {
        {"USDJPYz", POSITION_TYPE_SELL, 146.636, D'2024-01-31',false},
        {"USDJPYz", POSITION_TYPE_BUY,  148.794, D'2024-02-05',false},
        {"USDJPYz", POSITION_TYPE_BUY,  148.882, D'2024-02-08',false},
        {"USDJPYz", POSITION_TYPE_SELL, 149.672, D'2024-02-08',false}
     };

//---
   tModel = new TradeModel(modelInputs);

DeInit()関数でtModelオブジェクトのメモリをクリアすることを忘れないでください。主な機能はOnTick()関数の中で実行され、以下のコードで補足されます。

   tModel.Processing();                                                 // place orders

   MqlDateTime time_curr;                                               // current time structure
   TimeCurrent(time_curr);                                              // request current time

   if(time_curr.hour >= 23)                                             // if end of day
     {
      RMB.AllOrdersClose();                                             // close all positions
     }

同じ戦略でリスク制御クラスを使用した場合と使わなかった場合の結果を比較してみましょう。ユニットテストファイルManualRiskManager(UniTest1)をリスク制御メソッドなしで実行してみましょう。期間2024年1月~3月の戦略の結果は次のようになります。

図4:リスクマネージャーを使用しない場合のテストデータ

図4:リスクマネージャーを使わずにデータをテストする

その結果、次のようなパラメータを持つこの戦略に対して、数学的に正の期待値が得られます。

# パラメータ名 パラメータ値
 1  EA  ManualRiskManager(UniTest1)
 2  銘柄  USDJPY
 3  チャートの時間枠  М15
 4  時間  2024.01.01 - 2024.03.18
 5  フォワードテスト  いいえ 
 6  遅延   遅延なし、完璧なパフォーマンス
 7  シミュレーション  全ティック 
 8  初期投資額  USD 10,000 
 9  レバレッジ  1:100 

表1:ストラテジーテスターの入力パラメータ


では、ユニットテストManualRiskManager(UniTest2)を実行しましょう。ここでは、リスクマネージャークラスを以下の入力パラメータで使用します。

入力パラメータ名 変数値
inp_riskperday 0.25
inp_riskperweek 0.75
inp_riskpermonth 2.25
inp_plandayprofit  0.78 
dayProfitControl  true

表2:リスクマネジャーの入力パラメータ

入力パラメータを生成するためのロジックは、第3回で入力パラメータの構造を設計する際に上述したロジックと同様です。利益曲線はこのようになります。

図5:リスクマネージャーを使用したテストデータ

図5:リスクマネージャーを使用したデータのテスト


2つのケースのテスト結果の要約を以下の表に示します。

# リスクマネージャーなし リスクマネージャーあり 変化
 1  総収益  41.1 144.48  +103.38
 2  残高ドローダウン最大値  0.74% 0.25%  3倍削減
 3  エクイティドローダウン最大値  1.13% 0.58%  2倍に減少
 4  期待損益  10.28 36.12  3倍以上の成長
 5  シャープレシオ  0.12 0.67  5倍の成長
 6  勝ち取引(%)  75% 75%  -
 7  平均収益取引  38.52 56.65  50%の成長
 8  平均損失  -74.47 -25.46  3倍削減
 9  平均リスクリターン  0.52  2.23  4倍の成長

表3:リスクマネージャーを使用した場合と使用しなかった場合の財務結果の比較

単体テストの結果から、リスクマネージャークラスによるリスク制御の利用は、リスクを制限し、固定リスクに対する各取引の利益を確定することで、同じ単純な戦略を使用した取引の効率を大幅に高めたと結論づけることができます。これにより、残高のドローダウンを3倍、エクイティ残高を2倍に減らすことができました。同戦略の期待ペイオフは3倍以上、シャープレシオは5倍以上増加しました。平均利益取引は50%増加し、平均不採算取引は3倍減少したため、口座の平均リスクリターンをほぼ目標値である1対3にすることができました。下の表は、プールから個々の取引について財務結果を詳細に比較したものです。


日付 銘柄 方向 ロット リスクマネージャーなし リスクマネージャーあり 変化
2024.01.31 USDJPY 0.1 25.75 78 + 52.25
2024.02.05
USDJPY 0.1
13.19 13.19 -
2024.02.08
USDJPY 0.1
76.63 78.75 + 2.12
2024.02.08
USDJPY 0.1
-74.47 -25.46 + 49.01
合計 - - - 41.10 144.48 + 103.38

表4:リスクマネージャーの有無による約定比較


結論

本稿で提示されたテーゼに基づき、以下の結論が導き出されます。手動取引でもリスクマネージャーを使用することで、収益性の高い戦略を含め、戦略の有効性を大幅に高めることができます。負ける戦略の場合、リスクマネージャーを利用することで、預託金を確保し、損失を抑えることができます。冒頭で述べたように、私たちは心理的な要因を軽減しようとしています。すぐに損失を取り戻そうとして、リスクマネージャーをオフにすべきではありません。限度が完了するのを待ち、感情的にならずに取引を再開した方がいいです。リスクマネジャーが取引を禁止している時間を利用して、取引戦略を分析し、損失の原因と今後の回避方法を理解します。

この記事を最後まで読んでくださった皆さん、ありがとうございました。この記事によって、少なくとも1人の預金が完全に失われることから救われることを心から願っています。この場合、私の努力は無駄ではなかったと考えることにします。特にこのクラスを純粋にアルゴリズム的な EA に適応できる新しい記事を開始すべきかどうかについて、皆さんのコメントやDMをお待ちしています。ご意見、ご感想は大歓迎です。よろしくお願いします。


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

母集団アルゴリズムのハイブリダイゼーション:逐次構造と並列構造 母集団アルゴリズムのハイブリダイゼーション:逐次構造と並列構造
ここでは、最適化アルゴリズムのハイブリダイゼーションの世界に飛び込み、3つの主要なタイプ、すなわち戦略混合、逐次ハイブリダイゼーション、並列ハイブリダイゼーションについて見ていきます。関連する最適化アルゴリズムを組み合わせ、テストする一連の実験をおこないます。
多通貨エキスパートアドバイザーの開発(第5回):可変ポジションサイズ 多通貨エキスパートアドバイザーの開発(第5回):可変ポジションサイズ
前回開発中のエキスパートアドバイザー(EA)は、固定されたポジションサイズのみを使用して取引をおこなうことができました。これはテスト用には許容できますが、実際の口座で取引する場合にはお勧めできません。可変のポジションサイズで取引できるようにしましょう。
最適化アルゴリズムの効率における乱数生成器の品質の役割 最適化アルゴリズムの効率における乱数生成器の品質の役割
この記事では、メルセンヌ・ツイスタ乱数生成器を取り上げ、MQL5の標準的な乱数生成器と比較します。また、乱数生成器の品質が最適化アルゴリズムの結果に与える影響についても調べます。
GIT:それは何か? GIT:それは何か?
今回は、開発者にとって非常に重要なツールを紹介しましょう。GITに馴染みのない方は、この記事を読んでGITとは何か、MQL5でどのように使用するかをご覧ください。