English Русский 中文 Español Deutsch Português
preview
リスク管理(第4回):主要クラスメソッドの完了

リスク管理(第4回):主要クラスメソッドの完了

MetaTrader 5 |
24 0
Niquel Mendoza
Niquel Mendoza


はじめに

前回の記事に続き、本記事ではリスク管理専用クラスにおける主要な変数および初期メソッドについて解説します。今回の主な目的は、事前に定義された最大損失および最大利益の閾値を超過したかどうかを判定する機能を完成させることです。あわせて、1取引あたりのリスクを動的に制御するための2つの新しいアプローチについても紹介します。


記事の要約

まずは、いくつかの関数の最適化に加え、クラスのコンストラクタおよびデストラクタの整備をおこないます。その後、新しい構造体、列挙型、主要定数を定義します。続いて、EAによってオープンされたポジションを管理するための機能を実装し、最大利益・最大損失の閾値超過を検出する仕組みを組み込みます。最後に、1取引あたりのリスクを動的に制御するための追加メソッドを実装します。


新しい列挙型と構造体

まず本記事で後ほど使用する、新しい構造体、列挙型、定数を定義していきます。

1. 1取引あたりの動的リスク

前述のとおり、新しい概念として「1取引あたりの動的リスク」を導入しています。

1取引あたりの動的リスクとは

1取引あたりの動的リスクとは固定値ではなく、初期残高に対する損益状況に応じて変化する可変的なリスク値です。これは特に口座保護の観点で有効です。たとえば、残高が初期値から一定割合減少した場合、自動的にリスクを縮小し、さらなる損失を抑制することができます。つまり、特定の閾値に到達するたびに、1取引あたりのリスクが段階的に調整されます。

以下の表は具体的な例です。 初期口座残高を10,000ドルとします。

リスク調整の閾値として、3%、5%、7%を設定します。

条件 新たなリスク%
 残高が3%減少(300ドル、9,700ドル未満)  リスク0.7%に調整
 残高が5%減少(500ドル、9,500ドル未満)  リスク0.5%に調整
  残高が7%減少(700ドル、9,300ドル未満)  リスク0.25%に調整

動的リスクパラメータと運用に関する重要な注意事項:

  • 本記事で扱うすべての数値は、ユーザーの目的や運用方針に応じて自由に調整可能なパラメータです。これには、リスク変更をトリガーする閾値や、基準となる初期残高も含まれます。

初期残高の決定方法には2種類あります。

  • PropFirmアカウント(例:FTMO)propfirm_ftmoが選択された場合、初期残高はEAの入力パラメータとして手動で設定します。この残高は取引期間中を通して固定され、時間経過によって変化しない静的な値です。

  • 個人アカウント:personal_accountが選択された場合、初期残高はEA起動時にAccountInfoDouble()を使用して自動取得されます。この場合、システム初期化時点の口座残高が基準となり、実際の資金状況を反映した動的な値として扱われます。
1取引あたりの動的リスクの長所:
  • 口座破綻や資金全損のリスクを大幅に低減できます。
  • 連敗やドローダウン局面でのエクスポージャーを自動的に縮小し、取引の安全性を向上させます。

    1取引あたりの動的リスクの短所:

    • リスク縮小が先行するため、大きなドローダウン後の回復速度は遅くなる可能性があります。ただし、これは回復速度と引き換えに安全性と制御性を優先する設計です。

    1取引あたりの動的リスク実装用の構造体

    1取引あたりの動的リスクを管理および設定するには、gmlpo変数内のassigned_percentageプロパティにアクセスし、それを適切に更新する必要があります。この自動的な残高ベースの調整を実装するために、2つの主要要素を含む構造体を作成します。

    • リスク調整が発動する特定の残高(例:初期10,000ドルに対して9,700ドルまで減少した場合)
    • そのレベルに到達した際に適用される新しいリスク割合(例:このケースでは0.7%へ調整)

      当初は、単純に2つのdouble変数を持つ構造体を作成することも考えられますが、この方法は不十分です。その代わりに、構造体内で2つの配列を使用します。

      個別の変数ではなく配列を使う理由

      配列を使用する理由は、複数の関連する値を効率的にソートする必要があることです。このソート処理は、動的リスク管理を効率的におこなうための設計上の要件です。具体的には、リスク調整が発動する残高(balance_to_activate_the_risk[])をArraySort()関数で並び替える必要があります。

      ArraySort()によるソートが必要な理由

      主な理由は、リスク調整のトリガーとなる特定の閾値を、現在の残高が超えたかどうかを継続的にチェックするために「ループを使わない設計」を採用している点にあります。この方式は、ティック処理や決済時など高頻度で判定が発生する場面でも、処理負荷を抑えられるよう設計されています。

      値が正しい順序(昇順)で並んでいない場合、重大な問題が発生する可能性があります。これをより明確にするために、具体的な例を考えてみましょう。

      次のような閾値を定義したとします。

      • 閾値1:3%(発動残高:$9,700)
      • 閾値2:7%(発動残高:$9,300)
      • 閾値3:5%(発動残高:$9,500)

        この例では、値の順序が正しく整理されていません(7%と5%が逆転しています)。この問題の本質は、現在の実装が「カウンタ(整数変数)」によってリスク状態を管理している点にあります。

        具体例で確認してみます。

        • 初期残高が3%減少して(9,700ドルになると)、カウンタが1増加し、リスクは0.7%に調整されます。
        • その後、残高が次のレベルである7%減少に達すると、口座残高は9,300ドルとなり、カウンターが再び増加します。このとき、本来存在する中間値(5%減少時の9,500ドル)はスキップされてしまいます。
        • その結果、中間値である9,500ドルは使用されないままとなり、動的リスクが適切に調整されないため、混乱や重大な計算上の問題を引き起こします。

          さらに、口座残高が最も低いレベル(このケースでは誤って5%レベルとして扱われている状態)から回復を始める場合、本来は9,300ドルを超えてはじめて前の値へ戻るべきです。しかし、元の方法では順序が正しく考慮されていないため、回復処理もうまく機能せず、想定外動作を招くことになります。

          これらの理由から、ループを使用しない方法を最適に機能させるためには、適切なソート処理が極めて重要になります。私たちが提案する最もシンプルな構造は、以下のようになります。

          struct Dynamic_gmlpo
          {
             double balance_to_activate_the_risk[];
             double risk_to_be_adjusted[];
          };

          しかし、この構造はシンプルである一方で、重要な制限が残っています。これら2つの配列はキーと値のペア(残高 - 調整後リスク率)を表しているため、ソート時にこの対応関係を維持することが極めて重要です。ここで登場するのがCHashMap構造体です。

          CHashMapを用いた実装では、それぞれの残高に対して対応するリスク率を直接関連付けることができます。メイン配列(残高)をソートしても、対応するリスクとのマッピングは自動的に保持されます。これにより、すべての操作および計算の完全な正確性が保証されます。

          したがって、これまで実装してきたものは、2つの独立した配列を使用したシンプルな初期ソリューションであり、ArraySort()メソッドの適用を一時的に簡略化するためのものです。しかし、より信頼性が高く正確な実装、特に複数のキー・バリューペアを効率的に処理したい場合には、CHashMapの使用を推奨します。この構造により、各残高と対応する調整済みリスクとの正しいマッピングが保証され、動的値のソートや検索時に発生し得るエラーを回避することができます。

          次の記事では、CHashMapを用いてこのソリューションを実装する方法について、実践的な例と段階的な解説を交えながら詳しく見ていきます。

          残高チェック

          動的リスクを適切に実装するために必要な列挙型を続けて定義するにあたり、残高チェックをいつ実行するかを明確に定義する2つの追加オプションを用意する必要があります。このチェックは非常に重要です。なぜなら、現在の残高が事前定義されたパーセンテージ(割合)閾値を下回ったかどうかを判定し、それによって動的リスクの変更を発動させる正確なタイミングを決定するからです。

          主なチェック方法は2種類あります。

          1. 毎ティックでチェックする方法:この方法では、価格が変動するたび(ティックごと)に継続的にチェックを実行します。このオプションは、エクイティが特定の閾値を下回ったかどうかを常時確認するため、高い精度を提供します。しかし、大きな欠点もあります。現在のエクイティと常に比較をおこなうことで、不自然または非効率な状況が発生する可能性があります。

            たとえば、初期残高が10,000ドルで、最初のリスク発動レベルが9,700ドルであると仮定します。エクイティが9,701ドルから9,699ドルの間で変動すると、動的リスクの再定義が何度も有効化または無効化されることになります。これは扱いづらいだけでなく、高頻度のチェックによってシステムリソースに過度な負荷を与える原因にもなります。

          2. 取引決済時にチェックする方法:2つ目の方法では、チェックは毎ティックではなく、取引を決済したタイミングでのみ実行されます。このオプションは、特定のタイミングでのみ残高を確認するため、リソース使用の観点ではより効率的です。ただし、チェックは取引終了時にしかおこなわれないため、途中で発生した大きな変動を考慮できず、動的リスクを適切なタイミングで変更できない可能性があります。そのため、精度の面では劣る場合があります。

          これら2つの方法を選択しやすくするため、以下のように列挙型を用いてコード内で明確に定義します。

          //---
          enum ENUM_REVISION_TYPE
           {
            REVISION_ON_CLOSE_POSITION, //Check GMLPO only when closing positions
            REVISION_ON_TICK //Check GMLPO on all ticks
           };
          

          これにより、ユーザーは残高チェックを「どのように」「いつ」実行するかを明確に選択できるようになり、システムのパフォーマンスや精度に対する個々のニーズや好みに応じて、動的リスクの動作をカスタマイズできます。

          1取引あたりの動的リスク(GMLPO)モード

          1取引あたりの動的リスク(GMLPO)が適用されるモードを管理するために、3つの明確に異なるオプションを提供する専用の列挙型を使用します。次に、それぞれのオプションの目的と、なぜ列挙型を用いて実装することにしたのかを説明します。

          当初は、リスク変更が発生する割合や、新たに適用するリスク値を設定するために、ユーザー入力の文字列が使用されていました。元の方法では、ユーザーはリスク変更を発動させる負の残高変化率を1行の文字列入力として手動で記述し、別の行に適用する新しいリスク値を入力する必要がありました。このアプローチは十分実用的ではあったものの、大きな欠点がありました。 それは、文字列入力ではエキスパートアドバイザー(EA)の最適化機能を用いた自動最適化ができなかったことです。そのため、さまざまなシナリオの設定やテスト作業が、遅く、実用性に欠け、非常に煩雑なものになっていました。

          これらの制限を克服するため、私は3つの明確に定義されたモードを簡単に選択できる列挙型を実装することにしました。これにより、柔軟性と最適化時の利便性を両立できます。

          1. DYNAMIC_GMLPO_FULL_CUSTOM:このモードは完全にカスタマイズ可能で、ユーザーは複数のリスク発動レベルと、それに対応する新しいリスク率を手動で設定できます。引き続き文字列入力を使用しますが、ユーザーは必要な変更回数を自由に指定できるため、最大限の柔軟性を実現できます。ただし、その代償として、自動最適化機能は利用できません。

          2. DYNAMIC_GMLPO_FIXED_PARAMETERS:このモードでは、許可される変更回数を最大4つに制限することで、動的リスク設定を大幅に簡素化します。ここでは、ユーザーが負の残高変化率と対応するリスク率を数値パラメータとして直接指定します。そのため、最適化が非常に容易になります。このオプションは、カスタマイズ性と自動テスト/最適化性能とのバランスを提供します。

          3. NO_DYNAMIC_GMLPO: このモードでは、動的リスク機能を完全に無効化します。残高連動型のリスク変更をおこなわず、取引全体を通して固定リスクを維持したいユーザーに最適です。

          これらのモードに対応する列挙型を以下に示します。

          //---
          enum ENUM_OF_DYNAMIC_MODES_OF_GMLPO
           {
            DYNAMIC_GMLPO_FULL_CUSTOM, //Customisable dynamic risk per operation
            DYNAMIC_GMLPO_FIXED_PARAMETERS,//Risk per operation with fixed parameters
            NO_DYNAMIC_GMLPO //No dynamic risk for risk per operation
           };

          このような列挙型に基づく実装により、明確性と利便性が向上し、さまざまな設定を容易に最適化できるようになります。その結果、ユーザーの好みや戦略に応じた最適なソリューションを迅速に特定することが可能になります。

          取引管理

          リスク管理システムをさらに改善するために、口座内のすべてのポジションを正確に追跡できる特別な機能を追加します。これには、ユーザーが手動で開いたポジションだけでなく、EAによって開かれたポジションも含まれます。

          EAによって開かれたポジションについては、特定のチケットがそのEAに割り当てられたマジックナンバーと一致するかどうかも明確に判定できます。これは、どれだけのポジションがそのEAによって開かれているかを正確に把握し、手動で開かれたポジションと簡単に区別したい場合に特に有用です。

          この目的を実現するために、各ポジションに関する重要な情報を保持する、シンプルかつ効果的な構造を定義します。

          struct Positions
           {
            ulong              ticket; //position ticket
            ENUM_POSITION_TYPE type; //position type
           };

          利益/損失上限の超過

          次に、事前に設定された最大利益または最大損失の制限を超えたかどうかを判定する際に、どの基準を考慮するかを示す専用の列挙型を追加します。

          このチェックを実行する関数は、選択された基準に従って指定条件が満たされた場合にtrueを返します。

          この列挙型は、3つの明確に異なるオプションで構成されます。

          //--- Mode to check if a maximum loss or gain has been exceeded
          enum MODE_SUPERATE 
           {
            EQUITY, //Only Equity
            CLOSE_POSITION, //Only for closed positions
            CLOSE_POSITION_AND_EQUITY//Closed positions and equity
           };
          1. EQUITY
            このオプションでは、現在の口座エクイティのみを評価します。つまり、オープンポジションおよび決済済みポジションを含めたリアルタイム残高のみを考慮します。当日すでに決済された取引による利益や損失は考慮されません。このモードでは、リアルタイムエクイティが設定された制限値を直接超えた場合にのみ、利益/損失上限を超過したと判定されます。

          2. CLOSED_POSITIONS
            この方法では、当日中に決済されたポジションの利益または損失のみを考慮します。オープンポジションや現在のエクイティは完全に無視されます。したがって、制限超過の判定は、決済済み取引の累積結果のみに基づいておこなわれます。

          3. CLOSED_POSITION_AND_EQUITY
            これは最も包括的かつ正確な方法です。なぜなら、前述した2つのアプローチを組み合わせているからです。この関数は、当日に決済されたポジションの利益・損失と、現在のリアルタイムエクイティの両方を同時に評価します。つまり、1日の総合的な結果を分析するため、設定された制限を超過したかどうかを、より正確かつ厳密に判定できます。

          この列挙型をリスク管理システムに導入することで、ユーザーは制限確認方法を柔軟に選択できるようになり、さまざまな戦略や必要なリスク管理精度に応じて簡単に適応させることが可能になります。

          定義

          新しい変数の追加に続いて、#defineディレクティブを使用していくつかの定数を定義することも重要です。

          まず、EAによって生成される取引やメッセージを簡単に識別できるように、接頭辞用のdefineを設定します。この接頭辞にはEA名を含めることができ、ログやシステム生成コメント内で簡単に識別できるようになります。たとえば、このケースでは次のように定義します。

          #define EA_NAME "CRiksManagement | " //Prefix

          さらに、オープンポジションと保留注文の両方を正確に制御するために使用する、いくつかの定数(フラグ)も定義する必要があります。これらのフラグにより、取引タイプ(Buy、Sell、指値注文、ストップ注文など)を素早く識別できるようになり、効果的なリスク管理、ポジション決済、および現在の市場状況や保有ポジションに関する特定処理を簡素化できます。

          以下に対応するdefine定数を示します。

          //--- positions
          #define  FLAG_POSITION_BUY 2
          #define  FLAG_POSITION_SELL 4
          
          //--- orders
          #define FLAG_ORDER_TYPE_BUY             1
          #define FLAG_ORDER_TYPE_SELL            2
          #define FLAG_ORDER_TYPE_BUY_LIMIT       4
          #define FLAG_ORDER_TYPE_SELL_LIMIT      8
          #define FLAG_ORDER_TYPE_BUY_STOP        16
          #define FLAG_ORDER_TYPE_SELL_STOP       32
          #define FLAG_ORDER_TYPE_BUY_STOP_LIMIT  64
          #define FLAG_ORDER_TYPE_SELL_STOP_LIMIT 128
          #define FLAG_ORDER_TYPE_CLOSE_BY        256

          これらの定数により、今後実装する関数をより明確かつ効率的に構築できるようになり、EAコードの可読性、保守性、拡張性が大幅に向上します。


          コンストラクタとメイン関数の改善

          リスク管理の最適化は、CRiskManagementクラスのコンストラクタから開始します。1取引あたりの動的リスク機能が追加されたことで、コードにはいくつかの重要な改善が加えられました。

          まず、使用するロットタイプ(type_get_lot)と、初期残高に関連するパラメータ(account_propfirm_balance)を明示的に定義しました。これは、PropFirmアカウントを使用する場合に特に有用です。さらに、EA_NAMEのdefineが、メインクラス関数によって生成されるコメント内に常に表示される点にも注目してください。これにより、ターミナルログ内で関連メッセージを迅速に識別できるようになります。

          改良されたコンストラクタの実装は以下のとおりです。

          //+------------------------------------------------------------------+
          //| Constructor                                                      |
          //+------------------------------------------------------------------+
          CRiskManagemet::CRiskManagemet(bool mdp_strict_, ENUM_GET_LOT type_get_lot_, ulong magic_number_ = NOT_MAGIC_NUMBER, ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, double account_propfirm_balance = 0)
           {
            if(magic_number_ == NOT_MAGIC_NUMBER)
             {
              Print(EA_NAME, " (Warning) No magic number has been chosen, taking into account all the magic numbers and the user's trades");
             }
          //---
            this.mdp_is_strict = mdp_strict_;
            this.type_get_lot = type_get_lot_;
          //---
            this.account_balance_propfirm = account_propfirm_balance ;
            trade = new CTrade();
            trade.SetExpertMagicNumber(this.magic_number);
            this.account_profit = GetNetProfitSince(true, this.magic_number, D'1972.01.01 00:00');
            this.magic_number = magic_number_;
            this.mode_risk_managemet = mode_risk_management_;
            this.ActivateDynamicRiskPerOperation = false;
          //---
            this.last_day_time = iTime(_Symbol, PERIOD_D1, 0);
            this.last_weekly_time = iTime(_Symbol, PERIOD_W1, 0);
            this.init_time = magic_number_ != NOT_MAGIC_NUMBER ? TimeCurrent() : D'1972.01.01 00:00';
          //---
            this.positions_open = false;
            this.curr_profit = 0;
            UpdateProfit();
            
          //---
             for(int i = PositionsTotal() - 1; i >= 0; i--)
             {
              ulong position_ticket = PositionGetTicket(i);
              if(!PositionSelectByTicket(position_ticket))
                continue;
                
              ulong position_magic = PositionGetInteger(POSITION_MAGIC);
              ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
          
              if(position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER)
               {
                this.positions_open = true;
                Positions new_pos;
                new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
                new_pos.ticket = position_ticket;
                ExtraFunctions::AddArrayNoVerification(open_positions, new_pos);
               }
             }  
          }

          新たに追加された重要な変数は以下の通りです。

          • curr_profit: 現在の利益を保存し、結果を継続的に監視できるようにします。
          • ActivateDynamicRiskPerOperation: EAの動作中に動的リスクを使用するかどうかを決定するブール変数です。
          • mdp_is_strict: リスク管理がmdpを厳密に監視するかどうかを決定する変数です。
          • type_get_lot: ロットタイプを保存する変数です。
          さらに、マジックナンバーに基づいてすべてのオープンポジションを収集するループも追加されました。マジックナンバーが指定されていない場合は、それらのポジションをリスク管理用ポジション配列へ追加します。

          デストラクタの改善

          デストラクタにも、特に動的メモリ管理に関して、いくつかの重要な改善が加えられました。現在では、CTradeクラスポインタがCheckPointer()関数を使用して検証されるようになっています。これにより、そのポインタが動的に確保されたものである場合にのみ削除処理を実行できるため、メモリ解放時の潜在的なエラーを防止できます。

          最適化されたデストラクタの実装は以下のとおりです。

          //+------------------------------------------------------------------+
          //| Destructor                                                       |
          //+------------------------------------------------------------------+
          CRiskManagemet::~CRiskManagemet()
           {
            if(CheckPointer(trade) == POINTER_DYNAMIC) delete trade;
            ArrayFree(this.open_positions);
            ArrayFree(this.dynamic_gmlpos.balance_to_activate_the_risk);
            ArrayFree(this.dynamic_gmlpos.risk_to_be_adjusted);
           }

          CheckPointer()を適切に使用することで、tradeポインタが必要な場合にのみ正しく解放されることが保証されます。さらに、ArrayFree()関数を使用することで、クラス配列によって使用されているメモリを効果的に解放できるため、適切なメモリ管理が実現され、このリスク管理を実装したEAの安定性と効率性が向上します。

          全般的な改善

          リスク管理システム全体を強化し、EAの動作における潜在的なエラーを防止することを目的として、いくつかの重要な変更をおこないました。これらについては、以下でさらに詳しく説明します。

          1. ロット数量およびストップロス(SL)取得時のチェック

          1取引あたりのリスク値(gmlpo.assigned_percentage)が無効またはゼロになっていないことを保証するために、重要なチェック処理が追加されました。これらのチェックにより、重大なエラーを迅速に検出し、ユーザーへメッセージを表示できるため、不正な設定をすぐに修正できるようになります。

          主な判定基準は、gmlpo.assigned_percentageの直接検証です。この値が0以下である場合、重大なメッセージがコンソールへ出力され、関数は終了します。その際、EAの誤動作を防ぐため、安全な値を返すようになっています。

            if(gmlpo.assigned_percentage <= 0)
             {
              PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value);
              return 0;
             }

          GetSL()関数の例

          //+----------------------------------------------------------------------------------+
          //| Get the ideal stop loss based on a specified lot and the maximum loss per trade  |
          //+----------------------------------------------------------------------------------+
          long CRiskManagemet::GetSL(const ENUM_ORDER_TYPE type, double DEVIATION = 100, double STOP_LIMIT = 50)
           {
             if(gmlpo.assigned_percentage <= 0)
             {
              PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value);
              return 0;
             }
           
            double lot;
            return CalculateSL(type, this.gmlpo.value, lot, DEVIATION, STOP_LIMIT);
           }
          

          GetLote()関数の例

          //+-----------------------------------------------------------------------------------------------+
          //| Function to obtain the ideal lot based on the maximum loss per operation and the stop loss    |
          //+-----------------------------------------------------------------------------------------------+
          double CRiskManagemet::GetLote(const ENUM_ORDER_TYPE order_type)
           {
            if(gmlpo.assigned_percentage <= 0)
             {
              PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value);
              this.lote = 0.00;
              return this.lote;
             }
             
          //---
            if(this.type_get_lot == GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION)
             {
              double MaxLote = GetMaxLote(order_type);
              SetNMPLO(this.lote, MaxLote);
              PrintFormat("%s Maximum loss in case the next operation fails %.2f ", EA_NAME, this.nmlpo);
             }
            else
             {
              this.lote = GetLotByRiskPerOperation(this.gmlpo.value, order_type);
             }
          
          //---
            return this.lote;
           }

          2.パラメータ割り当て関数(SetEnums())の改善

          この関数は非常に重要であり、OnInit()イベント内で実行する必要があります。現在では、ユーザーの選択に応じて、金額または割合が正しく割り当てられていることを保証する追加チェックが含まれています。この検証により、特に固定金額(money)を基準として使用する場合に、不正または負の値が割り当てられることを防止できます。

          改善後のSetEnums()実装は、以下のようになります。

          //+----------------------------------------------------------------------------------------+
          //| Function to set how losses or gains are calculated,                                    |
          //| by percentage applied to (balance, equity, free margin or net profit) or simply money. |
          //+----------------------------------------------------------------------------------------+
          //Note: This method is mandatory, it must be executed in the OnInit event.
          void CRiskManagemet::SetEnums(ENUM_RISK_CALCULATION_MODE mode_mdl_, ENUM_RISK_CALCULATION_MODE mode_mwl_, ENUM_RISK_CALCULATION_MODE mode_gmlpo_, ENUM_RISK_CALCULATION_MODE mode_ml_, ENUM_RISK_CALCULATION_MODE mode_mdp_)
           {
            this.gmlpo.mode_calculation_risk = mode_gmlpo_;
            this.mdl.mode_calculation_risk  = mode_mdl_;
            this.mdp.mode_calculation_risk  = mode_mdp_;
            this.ml.mode_calculation_risk  = mode_ml_;
            this.mwl.mode_calculation_risk  = mode_mwl_;
          //-- En caso se haya escojido el modo dinero, asignamos la variable que guarda el dinero o porcentage alas varialbes correspondientes
            if(this.gmlpo.mode_calculation_risk  == money)
             {
              this.gmlpo.value = this.gmlpo.percentage_applied_to;
              this.ActivateDynamicRiskPerOperation = false;
             }
            else
              this.gmlpo.value = 0;
              
            this.mdp.value  = this.mdp.mode_calculation_risk  == money ? (this.mdp.percentage_applied_to > 0 ? this.mdp.percentage_applied_to : 0) : 0;
            this.mdl.value  = this.mdl.mode_calculation_risk  == money ? (this.mdl.percentage_applied_to > 0 ? this.mdl.percentage_applied_to : 0) : 0;
            this.ml.value  = this.ml.mode_calculation_risk  == money   ? (this.ml.percentage_applied_to  > 0 ? this.ml.percentage_applied_to  : 0) : 0;
            this.mwl.value  = this.mwl.mode_calculation_risk  == money ? (this.mwl.percentage_applied_to > 0 ? this.mwl.percentage_applied_to : 0) : 0;
           }


          新しいクラス変数

          改善をさらに進めるために、オープンポジションをより正確かつ効率的に制御できる変数を追加します。

          まず、口座内のすべてのオープンポジションに関する情報を保存するための特別な配列を定義します。これには、ユーザーが手動で開いたポジションと、EAによって自動的に開かれたポジションの両方が含まれます。

            //---
            Positions          open_positions[];    
          

          さらに、2つの重要な変数を追加します。

          • ブール変数positions_open: この変数は、市場にオープンポジションが存在するかどうかを示します。パフォーマンス最適化において重要な役割を果たし、アクティブなポジションが存在しない場合に不要なチェック処理を回避するのに役立ちます。

            //--- Boolean variable to check if there are any open operations by the EA or user
            bool               positions_open;

          • double型変数curr_profitを追加します。この変数は、現在の累積利益または損失をリアルタイムで保持するために使用されます。この変数により、現在の取引状態を素早く計算および評価することが可能になります。

            //--- Variable to store the current profit of the EA or user
            double             curr_profit;

          1取引あたりの動的リスク

          1取引あたりの動的リスク(dynamic GMLPO)を効果的に管理するためには、クラス内にいくつかの専用変数を追加し、明確に定義する必要があります。これらの変数は、ユーザーが設定したパラメータに応じてリスクを自動調整するための、効率的な制御を可能にします。以下にそれぞれを詳しく説明します。

          1. 残高と動的リスクを保存する構造体

          Dynamic_gmlpoというカスタム構造体を使用し、2つの動的配列を保持します。

            Dynamic_gmlpo      dynamic_gmlpos;

          この構造体により、複数の特定の残高レベルとそれぞれに対応するリスク割合を保存できるため、動的リスク管理が可能になります。

          2.動的リスクのチェック方式を決定する変数

          動的リスクのチェック方法(ティックごと、またはポジション決済時)を選択できるようにするため、revision_typeというenum変数を定義します。

            ENUM_REVISION_TYPE revision_type;

          3. 動的リスクの有効・無効を管理する変数

          1取引あたりの動的リスクを使用するかどうかを保持するブール変数です。

            bool               ActivateDynamicRiskPerOperation;

          4. 現在の動的リスクインデックス

          動的リスクの段階を正確に管理するために、現在参照している配列インデックスを示すインデックス変数を使用します。

            int                index_gmlpo;

          5. 基準(初期)残高を保存する変数

          ユーザーが選択した初期残高または基準残高を保存し、後に指定された割合計算の基準として使用します。

            double             chosen_balance;

          6. 次のリスク変更対象となる残高レベル

          この変数は、動的リスクを変更するために次に到達すべき残高レベルを保持します。

            double             NewBalanceToOvercome;

          7. インデックス範囲超過を防ぐためのフラグ

          ユーザーが設定した最小許容値(最大の負の割合)に到達したかどうかを示します。trueの場合、インデックスの増加を停止し、範囲外アクセスを防ぎます。

            bool               TheMinimumValueIsExceeded;

          8.初期の1取引あたりのリスク割合

          1取引あたりの初期リスク割合を保存する変数です。残高が低いレベルに到達した後、回復した際に元の値へ戻すために使用されます。

            double             gmlpo_percentage;

          これらの変数は、さまざまな運用シナリオにおいて、1取引あたりの動的リスクを明確かつ正確に制御するための、信頼性が高く安全で管理しやすい仕組みを効果的に実装することを可能にします。

          最大日次利益を制御する変数

          日次利益をより厳密に管理するために、ブール変数mdp_is_strictを導入します。この変数は、最大利益の計算において日次損失を考慮するかどうかをリスク管理システムに判断させる役割を持ちます。

          • mdp_is_strictがtrue の場合、最大日次利益は、まず過去の損失をすべて回復し、そのうえで元の利益目標に到達したときにのみ達成されたとみなされます。たとえば、最大日次利益が50ドルで、その日に20ドルの損失が発生している場合、損失の回復(20ドル)と純利益50ドルを合わせた合計70ドルの利益を達成する必要があります。
          • 一方で、mdp_is_strictがfalseの場合は、日次損失は最大利益の計算に影響しません。この場合、目標利益が50ドルで40ドルの損失があったとしても、損失を補填する40ドルと純利益10ドルを合わせた合計10ドルの追加利益を得ることで、最大日次利益に到達したとみなされます。

            bool               mdp_is_strict;

          ロットタイプ変数

          ロット割り当て機能を簡素化するために、GetBatch()関数を修正し、手動でロットタイプを指定する必要がないようにします。代わりに、ロットタイプはコンストラクタで初期化するか、今後開発する追加関数を通じて変更できるようにします。このアプローチにより、より直接的な設定が可能になり、手動入力に起因するエラーのリスクを軽減できます。

            ENUM_GET_LOT       type_get_lot;  
          

          これらの改善により、取引プラットフォームにおけるリスク管理とロット計算の両方において、効率性と精度の向上が期待されます。


          オープンポジションの管理

          このセクションでは、マジックナンバーまたはユーザーによって開かれたすべてのポジションを適切に管理する方法に焦点を当てます。 そのために、任意の時点でオープンポジションを追跡できる、いくつかの有用で明確な関数を作成します。

          1. 内部配列(open_positions)にチケットが存在するかどうかを確認する関数

          TheTicketExists()関数は、指定されたチケットが内部配列open_positionsに含まれているかを素早く確認するために設計されています。この処理は、特定のポジションがすでに管理対象になっているか、または追加の処理が必要かどうかを判断する上で非常に重要です。

          ロジックはシンプルで、配列を順に走査し、各要素と与えられたチケットを比較します。一致すればtrue、見つからなければfalseを返します。

          宣言

            bool               TheTicketExists(const ulong ticket); //Check if a ticket is in the operations array

          実装は以下の通りです。

          //+------------------------------------------------------------------+
          //| Function to check if the ticket is in the array                  |
          //+------------------------------------------------------------------+
          bool CRiskManagemet::TheTicketExists(const ulong ticket)
           {
            for(int i = 0; i < this.GetPositionsTotal() ; i++)
              if(this.open_positions[i].ticket == ticket)
                return true;
                
            return false;
           }

          2.オープンポジションの総数を取得する関数

          GetPositionsTotal()関数は、内部配列open_positionsに含まれる要素数を返します。これにより、現在リアルタイムで管理されているポジション数を簡単に取得できます。

          宣言と実装

            inline int         GetPositionsTotal() const { return (int)this.open_positions.Size(); } //Get the total number of open positions

          3. フラグ付きポジションの総数を取得する関数

          GetPositions()関数はフラグシステムを用いることで、Buy / Sellなど特定条件に基づいたポジション数を柔軟に取得できます。ここではENUM_POSITION_TYPEをビット演算に適したフラグ値(2、4、8などの2の累乗)に変換し、条件に応じてカウントします。

          ロジックは、すべてのオープンポジションを順に走査し、それぞれを指定されたフラグと照合します。そして一致が見つかるたびにカウンタをインクリメントしていきます。

          実装は以下の通りです。

          //+------------------------------------------------------------------+
          //| Function to obtain the number of open positions                  |
          //+------------------------------------------------------------------+
          int CRiskManagemet::GetPositions(int flags) const
          {
          int count = 0;
          
           for(int i = 0; i < ArraySize(this.open_positions) ; i++)
            {
              if(this.open_positions[i].type == POSITION_TYPE_BUY && (flags & FLAG_POSITION_BUY) != 0 )
              {
               count++;
              }
              else if(this.open_positions[i].type == POSITION_TYPE_SELL && (flags & FLAG_POSITION_SELL) != 0 )
              {
               count++;
              }
            } 
          
          return count;
          }

          4. 現在オープンポジションがあるかを確認する関数

          ThereAreOpenOperations()関数は、EAが現在ポジションを保有しているかを簡単に確認できます。内部のpositions_openブール変数を返すだけの軽量な処理です。

          宣言と実装

            inline bool        ThereAreOpenOperations() const { return this.positions_open; } //Check if there are any open operations

          5. ヘルパー関数

          クラスの主要機能に加えて、特定の処理を容易にする外部ヘルパー関数も追加されます。たとえば、フラグに基づいた注文のクローズ処理などです。

          5.1 フラグによる未決注文の決済機能

          特定条件に基づいて注文を閉じる前に、ENUM_ORDER_TYPEをビット演算可能なフラグへ変換する必要があります。これは「&」演算のエラー防止に重要です。

          以下は、特定の注文タイプを対応するフラグに変換するシンプルな関数です。

          無効な値または一般化された値(WRONG_VALUE)が渡された場合、関数はすべてのフラグの組み合わせを返します。

          // Converts an order type to its corresponding flag
          int OrderTypeToFlag(ENUM_ORDER_TYPE type)
           {
            if(type == ORDER_TYPE_BUY)
              return FLAG_ORDER_TYPE_BUY;
            else
              if(type == ORDER_TYPE_SELL)
                return FLAG_ORDER_TYPE_SELL;
              else
                if(type == ORDER_TYPE_BUY_LIMIT)
                  return FLAG_ORDER_TYPE_BUY_LIMIT;
                else
                  if(type == ORDER_TYPE_SELL_LIMIT)
                    return FLAG_ORDER_TYPE_SELL_LIMIT;
                  else
                    if(type == ORDER_TYPE_BUY_STOP)
                      return FLAG_ORDER_TYPE_BUY_STOP;
                    else
                      if(type == ORDER_TYPE_SELL_STOP)
                        return FLAG_ORDER_TYPE_SELL_STOP;
                      else
                        if(type == ORDER_TYPE_BUY_STOP_LIMIT)
                          return FLAG_ORDER_TYPE_BUY_STOP_LIMIT;
                        else
                          if(type == ORDER_TYPE_SELL_STOP_LIMIT)
                            return FLAG_ORDER_TYPE_SELL_STOP_LIMIT;
                          else
                            if(type == ORDER_TYPE_CLOSE_BY)
                              return FLAG_ORDER_TYPE_CLOSE_BY;
                              
            return (FLAG_ORDER_TYPE_BUY | FLAG_ORDER_TYPE_SELL | FLAG_ORDER_TYPE_BUY_LIMIT |
                    FLAG_ORDER_TYPE_SELL_LIMIT | FLAG_ORDER_TYPE_BUY_STOP | FLAG_ORDER_TYPE_SELL_STOP |
                    FLAG_ORDER_TYPE_BUY_STOP_LIMIT | FLAG_ORDER_TYPE_SELL_STOP_LIMIT | FLAG_ORDER_TYPE_CLOSE_BY);
           }
          

          メイン関数は、既存のすべての注文を順に処理し、指定されたフラグに一致する注文を閉じます。

          // Close all orders that match the flags in `flags`
          void CloseAllOrders(int flags, CTrade &obj_trade, ulong magic_number_ = NOT_MAGIC_NUMBER)
           {
            ResetLastError();
            for(int i = OrdersTotal() - 1; i >= 0; i--)
             {
              ulong ticket = OrderGetTicket(i);
              
              if(OrderSelect(ticket))
               {
                ENUM_ORDER_TYPE type_order = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
                ulong magic = OrderGetInteger(ORDER_MAGIC);
                int bandera = OrderTypeToFlag(type_order);
                if((bandera & flags) != 0 && (magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER))
                 {
                  if(type_order == ORDER_TYPE_BUY || type_order == ORDER_TYPE_SELL)
                    obj_trade.PositionClose(ticket);
                  else
                    obj_trade.OrderDelete(ticket);
                 }
               }
              else
               {
                PrintFormat("Error selecting order %d, last error %d", ticket, GetLastError());
               }
             }
           }

          5.2 オープンポジションの総数を取得する関数

          CRiskManagementクラスだけに依存せず、オープンポジション総数をカウントできるシンプルな外部関数も追加します。まず最初に、ポジションタイプ(ENUM_POSITION_TYPE)を互換性のあるフラグへ変換する必要があります。

          5.2.1 ENUM_POSITION_TYPEを有効なフラグに変換する関数

          int PositionTypeToFlag(ENUM_POSITION_TYPE type)
           {
            if(type == POSITION_TYPE_BUY)
              return FLAG_POSITION_BUY;
            else
              if(type == POSITION_TYPE_SELL)
                return FLAG_POSITION_SELL;
                
            return FLAG_POSITION_BUY | FLAG_POSITION_SELL;
           }

          5.2.2 フラグによるオープンポジションの総数を取得する関数

          この関数は、既存のすべてのポジションを順に調べ、指定されたフラグに一致する位置のみをカウントします。

          //---
          int GetPositions(int flags = FLAG_POSITION_BUY | FLAG_POSITION_SELL, ulong magic_number_ = NOT_MAGIC_NUMBER)
           {
            int counter = 0;
            for(int i = PositionsTotal() - 1; i >= 0; i--)
             {
              ulong position_ticket = PositionGetTicket(i);
              if(!PositionSelectByTicket(position_ticket))
                continue; // Si la selección falla, pasa a la siguiente posición
              ulong position_magic = PositionGetInteger(POSITION_MAGIC);
              ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
              // Check if the position type matches the flags
              if((flags & PositionTypeToFlag(type)) != 0 &&
                 (position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER))
               {
                counter++;
               }
             }
            return counter;
           }


          制限超過の処理

          ここでは、最大損失または最大日次利益を超過したかどうかを判定するための、いくつかの関数を定義します。

          まず、2つの主要関数を作成します。 1つは損失超過をチェックするための関数、もう1つは設定された最大利益目標に到達したかを確認するための関数です。

          これらの各モードは関数としてカプセル化されており、選択したオプションに応じて、設定した目標に到達したかどうかを通知します。目標に到達した場合はtrueを返し、それ以外の場合はfalseを返します。たとえば、CLOSED_POSITIONSモードでは、すでにどれだけの利益が得られているかを把握することが非常に重要です。なぜなら、その値を設定された最大損失または利益制限(日次または週次)と比較する必要があるからです。

          それでは、これをコードでどのように実装できるかを見ていきましょう。

          //+------------------------------------------------------------------+
          //| Boolean function to check if a loss was overcome                 |
          //+------------------------------------------------------------------+
          bool CRiskManagemet::IsSuperated(double profit_, double loss_, const MODE_SUPERATE mode) const
           {
            if(loss_ <= 0 || !this.positions_open)
              return false; //if loss is zero return false (the loss is not being used)
          //---
            if(mode == EQUITY) //---
             {
              if(this.curr_profit * -1 > loss_)
                return true;
             }
            else
              if(mode == CLOSE_POSITION)
               {
                if(profit_ * -1 > loss_)
                  return true;
               }
              else
                if(mode == CLOSE_POSITION_AND_EQUITY)
                 {
                  double new_loss = profit_ < 0 ? loss_ - MathAbs(profit_) : loss_;
                  if(this.curr_profit * -1 > new_loss)
                    return true;
                 }
          
            return false;
           }

          モード別の解説

          1. EQUITY:このモードでは、現在の利益(エクイティ − 口座残高)を直接比較します。この負の値が指定された損失レベルを超えた場合、設定された制限を超過したと判定されます。

          2. CLOSE_POSITION:このモードでは、すでに決済されたポジションの利益を分析し、適切に比較するためにその値へ-1を掛けます。

          3. CLOSE_POSITION_AND_EQUITY:このモードはより複雑です。ここでは、現在の利益状況を考慮して最大損失値を調整します。もしその日が損失状態であり、現在利益がマイナスである場合、その値を許容損失限度から差し引きます。そして、現在の負の利益が調整後の値を超えた場合、制限値も超過したと判定されます。

          利益最大化のための専用関数

          損失の場合と同様に、最大期待利益を超過したかどうかを確認する方法も必要です。そのために、最大日次利益(mdp)の値が意図せず変更されないようにするための専用関数を作成します。

          以下にこの関数のコードを示します。

          //+------------------------------------------------------------------+
          //| Function to check if the maximum profit per day was exceeded     |
          //+------------------------------------------------------------------+
          bool CRiskManagemet::MDP_IsSuperated(const MODE_SUPERATE mode) const
           {
            if(this.mdp.value  <= 0 || !this.positions_open)
              return false; //if loss is zero return false (the loss is not being used)
          //---
            if(mode == EQUITY) //---
             {
              if(this.curr_profit > this.mdp.value)
                return true;
             }
            else
              if(mode == CLOSE_POSITION)
               {
                if(this.daily_profit > this.mdp.value)
                  return true;
               }
              else
                if(mode == CLOSE_POSITION_AND_EQUITY)
                 {
                  double new_mdp = this.daily_profit > 0 ? this.mdp.value - this.daily_profit : (this.mdp_is_strict == false ? this.mdp.value : this.mdp.value + (this.daily_profit * -1));
                  if(this.curr_profit > new_mdp)
                    return true;
                 }
          //---
            return false;
           }

          関数の動作

          1. EQUITY:このモードでは、現在の利益(curr_profit)を最大日次利益(mdp)と直接比較します。現在の利益がmdpを上回った場合、利益目標を超過したと判定されます。

          2. CLOSE_POSITION:このモードでは、その日に決済されたポジションから得られた利益(daily_profit)のみを対象として、mdpの値を超えているかを確認します。

          3. CLOSE_POSITION_AND_EQUITY:このケースは最も複雑で、2つの状況を考慮します。

            • 日次利益がプラスの場合、 その値をmdpから差し引き、新しい調整済みターゲットを設定します。
            • 日次利益がマイナスで、利益管理ポリシーが厳格(mdpisstrict)な場合 その損失の絶対値をmdpに加算し、まず損失を回復しなければ利益目標を達成したと見なされないようにします。ポリシーが厳格でない場合 初期のmdp値をそのまま使用し、日次損失は無視されます。

          この機能により、利益目標に対する正確な制御が可能となり、ボラティリティの高い取引日であっても、利益目標を達成または超過したかどうかを正確に判断できます。

          PropFirmにおける最大損失

          損失管理についてさらに説明すると、PropFirmアカウント、特にFTMOスタイルのアカウントには重要な特徴があります。これらのアカウントでは、最大損失制限は固定されています。つまり、この値は変動せず、チャレンジ開始時点から常に一定です。たとえば、初期口座残高が10,000USDの場合、最大損失限度は9,000USDに設定されます。これは、口座エクイティが一度でもこの閾値を下回った場合、資金提供資格を自動的に失うことを意味します。

          この閾値の監視を簡素化し、不要な複雑さを避けるために、最大損失制限に到達または超過したかを確認する専用メソッドを実装します。

            //--- Function to check if the maximum loss has been exceeded in a PropFirm account of the FTMO type
            inline bool        IsSuperatedMLPropFirm() const { return (this.ml.value == 0 || !this.positions_open) ? false : AccountInfoDouble(ACCOUNT_EQUITY) < (account_balance_propfirm - (this.ml.value)); }

          ロジックはシンプルです。オープンポジションが存在しない場合、または最大損失を制御する変数(ml)が0の場合、チェックはスキップされ、falseが返されます。一方、オープンポジションが存在し、mlに値が設定されている場合、関数は現在のエクイティと「初期テスト残高 − 最大許容損失」を比較します。

          検証関数

          上記のメソッドに加えて、事前に設定された利益または損失レベルに到達、または超過したかを迅速に判定するための実用的な関数も作成します。これらの関数は、汎用メソッドIsSuperatedを簡潔に呼び出すラッパー関数として実装されています。

            //--- functions to verify if the established losses were exceeded
            inline bool        ML_IsSuperated(const MODE_SUPERATE mode)   const  {return this.mode_risk_managemet == personal_account ? IsSuperated(this.gross_profit, this.ml.value, mode) : IsSuperatedMLPropFirm();  }
            inline bool        MWL_IsSuperated(const MODE_SUPERATE mode)  const  {return IsSuperated(this.weekly_profit, this.mwl.value, mode);    }
            inline bool        MDL_IsSuperated(const MODE_SUPERATE mode)  const  {return IsSuperated(this.daily_profit, this.mdl.value, mode);     }
            inline bool        GMLPO_IsSuperated()                        const  {return IsSuperated(0, this.gmlpo.value, EQUITY);                 }
            inline bool        NMLPO_IsSuperated()                        const  {return IsSuperated(0, this.nmlpo, EQUITY);                       }
            bool               MDP_IsSuperated(const MODE_SUPERATE mode)  const;

          これらの関数により、日次、週次などの損失制限条件を、効率的かつ分かりやすく評価できるようになります。

          FTMOの動的最大日次損失

          FTMOにおけるリスク管理で重要なのは、「最大日次損失」が固定値ではなく動的であるという点です。この値は、その日に蓄積された利益に応じて変化します。つまり、その日に得た利益が大きいほど、許容される損失余地も大きくなるため、この制限値は固定ではなく変動します。

          以下は、実現利益に基づいて最大日次損失を更新する関数です。

            //--- Update Loss (only if ftmo propfirm FTMO is selected)
            inline void        UpdateDailyLossFTMO() {  this.mdl.value += this.daily_profit > 0 ? this.daily_profit : 0; 
                                                        PrintFormat("%s The maximum loss per operation has been modified, its new value: %.2f",EA_NAME,this.mdl.value); }

          この関数は、EAの取引トランザクション処理をおこなうメソッド、つまりOnTradeTransactionから呼び出す必要があります。

          //+------------------------------------------------------------------+
          //| TradeTransaction function                                        |
          //+------------------------------------------------------------------+
          void OnTradeTransaction(const MqlTradeTransaction& trans,
                                  const MqlTradeRequest& request,
                                  const MqlTradeResult& result)


          イベント処理

          次に、EAのイベント管理メソッド内で実行される新しいイベントについて説明します。

          OnTradeTransaction

          OnTradeTransactionメソッドは、動的リスク管理において中心的な役割を果たします。なぜなら、このメソッドは、ポジションのオープンやクローズ、既存注文の変更、口座への入金など、口座の取引操作に関連するあらゆるイベントでトリガーされるからです。

          この関数には3つの重要なパラメータがあります。

          • trans: 現在発生している取引トランザクションに関する情報
          • request: 保留中、または最近完了した取引リクエストに関する情報
          • result: それらのリクエスト処理後に返される結果

            リスク管理において特に重要なのはtransの情報です。なぜなら、取引トランザクションタイプや関連する操作について正確な詳細を提供してくれるからです。

            列挙型には、いくつかの取引トランザクションタイプが定義されています。 

            ENUM_TRADE_TRANSACTION_TYPE

            以下が、取引トランザクションタイプです。

            ID
            説明
             TRADE_TRANSACTION_ORDER_ADD
            新規注文を追加します。
             TRADE_TRANSACTION_ORDER_UPDATE 未処理の注文を更新します。クライアントターミナルやサーバー上での変更、または注文状態の変更(例:ORDER_STATE_STARTEDからORDER_STATE_PLACED)などを含みます。
             TRADE_TRANSACTION_ORDER_DELETE 未処理注文一覧から注文を削除します。注文は、対応するリクエストが送信された後、または約定(執行)されて履歴に追加された後のいずれかのタイミングで、未処理注文リストから削除されます。
             TRADE_TRANSACTION_DEAL_ADD 約定を履歴に追加します。これは、注文が約定された後、または口座残高に関する操作が実行された後に発生します。
             TRADE_TRANSACTION_DEAL_UPDATE 履歴内の約定を更新します。以前に完了した取引は、サーバー側で修正される場合があります。たとえば、ブローカーが取引を送信した外部取引システム(取引所)側で調整された場合などです。
             TRADE_TRANSACTION_DEAL_DELETE 履歴から約定を削除します。以前に完了した取引がサーバー側で削除される場合があります。たとえば、ブローカーが送信した外部取引システム(取引所)側でその取引が削除された場合などです。
             TRADE_TRANSACTION_HISTORY_ADD 実行後またはキャンセル後に注文を履歴に追加します。
               TRADE_TRANSACTION_HISTORY_UPDATE 注文履歴で注文を更新します。このタイプは、サーバーの機能を拡張することも目的としています。
               TRADE_TRANSACTION_HISTORY_DELETE 注文履歴から注文を削除します。このタイプは、サーバーの機能を拡張することも目的としています。
             TRADE_TRANSACTION_POSITION 取引の約定によるものとは関係のないポジション変更。この取引トランザクションタイプは、取引サーバー上でポジション自体が変更されたことを意味します。ポジションは、出来高、始値、ストップロスおよびテイクプロフィットのレベルによって変化する可能性があります。これらの変更に関する情報は、OnTradeTransactionハンドラを介してMqlTradeTransaction構造体で渡されます。約定によって生じたポジションの変更(追加、更新、または削除)は、後からTRADE_TRANSACTION_POSITIONトランザクションをトリガーしません。
             TRADE_TRANSACTION_REQUEST サーバーが取引リクエストを処理し、結果を返したことを通知します。この取引トランザクションタイプの場合、MqlTradeTransactionのフィールドのうち、分析する必要があるのはtype(取引トランザクションタイプ)1つだけです。詳細については、OnTradeTransactionの2番目と3番目のパラメータ(リクエストと結果)を分析してください。

            ただし、ここでは以下の取引トランザクションタイプに焦点を当てます。

            • TRADE_TRANSACTION_DEAL_ADDは、新しい取引トランザクションが履歴に追加されたことを示します(決済済みの取引、または確認されたポジションの新規オープン)。

            OnTradeTransactionEvent関数の作成

            次に、このイベントを明確かつ効率的に処理するための関数を定義します。

              void               OnTradeTransactionEvent(const MqlTradeTransaction& trans);

            この関数は、現在の取引トランザクションに関するすべての情報を含むtransパラメータのみを必要とします。

            この関数の基本的な構造は、まず取引トランザクションタイプがTRADE_TRANSACTION_DEAL_ADDであることを確認し、次に履歴から約定を事前に選択することから始まります。

            //+------------------------------------------------------------------+
            //| OnTradeTransaction Event                                         |
            //+------------------------------------------------------------------+
            void CRiskManagemet::OnTradeTransactionEvent(const MqlTradeTransaction &trans)
             {
              HistoryDealSelect(trans.deal);
              
              if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
               {
                ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
                ulong position_magic = (ulong)HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
                bool is_select = PositionSelectByTicket(trans.position);
            
                if(entry == DEAL_ENTRY_IN && is_select && (this.magic_number == position_magic || this.magic_number == NOT_MAGIC_NUMBER))
                 {
                  Print(EA_NAME, " New position opened with ticket: ", trans.position);
                  this.positions_open = true;   
                  
                  Positions new_pos;
                  new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
                  new_pos.ticket = trans.position;
                  
                  AddArrayNoVerification(open_positions, new_pos);
                  return;
                 }
            
                if(entry == DEAL_ENTRY_OUT && TheTicketExists(trans.position) == true  && !is_select)
                 {
                  Print(EA_NAME, " Position with ticket ", trans.position, " has been closed");
                  DeleteTicket(trans.position);
            
                  //---
                  if(this.revision_type == REVISION_ON_CLOSE_POSITION)
                    CheckAndModifyThePercentageOfGmlpo();
            
                  //---
                  if(GetPositionsTotal() == 0)
                    this.positions_open = false;
            
                  //---
                  UpdateProfit();
            
                  //---
                  if(this.mode_risk_managemet == propfirm_ftmo)
                    UpdateDailyLossFTMO();
            
                  SetGMLPO();
                  Print(StringFormat("%-6s| %.2f", "GMLPO", this.gmlpo.value));
                 }
               }
               
             }

            この関数内では、2つの重要なシナリオを処理します。

            • ポジションの新規建て:まず、ポジションが有効に選択できる状態(is_select)であること、そしてそのマジックナンバーが指定された値と一致していることを確認します。これらの条件を満たした場合、そのポジションは内部リストに追加されます。

            • ポジションの決済:次に、ポジションがすでにクローズされており(選択できない状態)、かつ管理対象リストに存在しているかを確認します。この場合、チケットを登録簿から削除し、累積利益や未決済ポジションの全体的な状態などの主要な変数を更新し、FTMOモードが有効になっている場合は、1日の最大損失を動的に調整します。

            このようにすることで、このフレームワークは登録された各取引トランザクションに積極的に対応し、明確で効率的かつ動的なリスク管理を提供することができます。

            OnTickEvent

            OnTickイベントは、EAの動作における重要なイベントの一つであり、新しいティックが発生するたびに呼び出されます。

            本ケースでは、このイベントは、マジックナンバーが指定されている場合にはそれに基づいてフィルタリングをおこない、指定されていない場合にはマジック値に関係なくすべてのオープンポジションを対象として、全体の累積利益を更新するために使用されます。

            基本的なメソッド定義は以下のとおりです。

              void               OnTickEvent();

            この関数は、不要な計算を避けるため、オープンポジションが存在する場合にのみ実行されます。

            構成は次の通りです。

            //+------------------------------------------------------------------+
            //| Function to execute in OnTick                                    |
            //+------------------------------------------------------------------+
            void CRiskManagemet::OnTickEvent(void)
             {
              if(!positions_open)
                return;
            //---
              GetPositionsProfit();
            
            //---
              if(this.revision_type == REVISION_ON_TICK)
                CheckAndModifyThePercentageOfGmlpo();
             }
            

            処理の詳細説明
            1. 総利益の更新

              • GetPositionsProfit()メソッドは、管理対象ポジションにおける現在の利益または損失に関する情報を取得し更新します。これにより、オープンポジション全体のパフォーマンスについて常に最新かつ正確なデータを保持することができます。

            2. GMLPOの検証と動的更新

              • REVIEW_ON_TICKオプションを選択している場合、EAは新しいティックが発生するたびに、事前に定義された利益または損失の閾値が超過されたかどうかを継続的にチェックします。これにより、取引ごとの許容リスクが動的に調整されます。この仕組みによって、オープンポジションのボリューム(市場エクスポージャー水準)をリアルタイムで制御でき、リスク管理の精度とパフォーマンスが向上します。


            動的リスクおよびポジション配列操作用関数

            このセクションでは、動的リスクを効果的に管理し、ポジション配列に関連する実用的なタスクを処理するための重要な関数を実装します。これらのツールは、リスク管理戦略のさまざまな段階で役立ちます。

            文字列をデータ型に変換する関数

            この関数はテンプレートを使用しており、複雑なクラスや構造体を除くさまざまな単純データ型に柔軟に対応できます。その主な目的は、文字列を必要なデータ型へ変換し、リアルタイムでの動的情報処理を簡素化することです。

            実装は以下の通りです。

            template <typename S>
            void StringToType(string token, S &value, ENUM_DATATYPE type)
             {
              if(StringLen(token) == 0)
               {
                Print("Error: String is empty.");
                return;
               }
              switch(type)
               {
                case TYPE_BOOL:
                  value = (S)(StringToInteger(token) != 0);  // Convertir a bool
                  break;
                case TYPE_CHAR:
                  value = (S)((char)StringToInteger(token));  // Convertir a char
                  break;
                case TYPE_UCHAR:
                  value = (S)((uchar)StringToInteger(token));  // Convertir a uchar
                  break;
                case TYPE_SHORT:
                  value = (S)((short)StringToInteger(token));  // Convertir a short
                  break;
                case TYPE_USHORT:
                  value = (S)((ushort)StringToInteger(token));  // Convertir a ushort
                  break;
                case TYPE_COLOR:
                  value = (S)((color)StringToInteger(token));  // Convertir a color
                  break;
                case TYPE_INT:
                  value = (S)(StringToColor(token));  // Convertir a int
                  break;
                case TYPE_UINT:
                  value = (S)((uint)StringToInteger(token));  // Convertir a uint
                  break;
                case TYPE_DATETIME:
                  value = (S)(StringToTime(token));  // Convertir a datetime
                  break;
                case TYPE_LONG:
                  value = (S)((long)StringToInteger(token));  // Convertir a long
                  break;
                case TYPE_ULONG:
                  value = (S)((ulong)StringToInteger(token));  // Convertir a ulong
                  break;
                case TYPE_FLOAT:
                  value = (S)((float)StringToDouble(token));  // Convertir a float
                  break;
                case TYPE_DOUBLE:
                  value = (S)(StringToDouble(token));  // Convertir a double
                  break;
                case TYPE_STRING:
                  value = (S)(token);  // Mantener como string
                  break;
                default:
                  Print("Error: Unsupported data type in ConvertToType.");
                  break;
               }
             }   
            

            この関数は動的リスクを扱う際に特に重要です。なぜなら、パラメータはテキスト文字列として渡され、その後の処理のために数値変数へ変換する必要があるためです。

            文字列をプリミティブ型の配列へ変換する関数

            同様に、この関数もテンプレートを使用しており、必要な型の配列へ文字列を変換する処理を簡略化します。この関数は以下の処理を実行します。

            • 入力された文字列を指定された区切り文字(デフォルトはカンマ「,」)で分割する
            • 分割された各要素を一時配列に格納する
            • して、それぞれの要素を指定されたデータ型に変換し、対象の配列に代入する
              実装は以下の通りです。
              //---
              template <typename S>
              void StringToArray(S &array_receptor[], string cadena, ENUM_DATATYPE type_data, ushort separator = ',')
               {
                string result[];
                int num = StringSplit(cadena, separator, result);
                ArrayResize(array_receptor, ArraySize(result));
                for(int i = 0; i < ArraySize(array_receptor) ; i++)
                 {
                  S value;
                  StringToType(result[i], value, type_data);
                  array_receptor[i]  = value;
                 }
               }

              たとえば、実際の動的リスクのシナリオでは、「5.0,4.5,3.0」のようなデータを受け取ることがありますが、これらは自動的にdouble型の配列へ変換されます。これにより、システム内での動的パラメータ管理が大幅に簡素化されます。

              配列操作のための高度な関数

              以下では、特に動的戦略やリスク管理における複雑な構造を扱う際に役立つ、効率的な配列管理を可能にするいくつかの関数を紹介します。

              インデックス指定による複数要素削除関数

              この関数は、配列から複数の要素を一度に削除することを可能にし、パフォーマンスと可読性の向上に貢献します。主な処理の流れは、削除対象となるインデックスをソートし、その後、保持すべき要素のみを新しい構造として元の配列へコピーし直すというものです。

              実装は以下の通りです。

              //---
              template <typename T>
              void RemoveMultipleIndexes(T &arr[], int &indexes_to_remove[])
               {
                int oldSize = ArraySize(arr);
                int removeSize = ArraySize(indexes_to_remove);
                if(removeSize == 0 || oldSize == 0)
                  return;
              // Ordenamos los índices para garantizar eficiencia al recorrerlos
                ArraySort(indexes_to_remove);
                int writeIndex = 0, readIndex = 0, removeIndex = 0;
                while(readIndex < oldSize)
                 {
                  if(removeIndex < removeSize && readIndex == indexes_to_remove[removeIndex])
                   {
                    removeIndex++;
                   }
                  else
                   {
                    arr[writeIndex] = arr[readIndex];
                    writeIndex++;
                   }
                  readIndex++;
                 }
                ArrayResize(arr, writeIndex);
               }

              配列に要素を追加する関数

              次の関数は、任意の型の配列に新しい要素を簡単に追加できるようにするものであり、配列の型に自動的に適応します。

              実装は以下の通りです。

              //---
              template <typename X>
              void AddArrayNoVerification(X &array[], const X &value)
               {
                ArrayResize(array, array.Size() + 1);
                array[array.Size() - 1] = value;
               }

              仕組みはシンプルで、配列のサイズを1つ増やし、新しい値を最後の要素に代入します。

              チケットによる単一要素削除の専用関数

              この関数は、ticketフィールドを含む構造体の配列に対して使用するように設計されています。 一意の識別子であるこのticketに基づいて、特定の要素を削除します。

              実装は以下の通りです。

              //---
              template<typename T>
              bool RemoveIndexFromAnArrayOfPositions(T &array[], const ulong ticket)
               {
                int size = ArraySize(array);
                int index = -1;
              // Search index and move elements in a single loop
                for(int i = 0; i < size; i++)
                 {
                  if(array[i].ticket == ticket)
                   {
                    index = i;
                   }
                  if(index != -1 && i < size - 1)
                   {
                    array[i] = array[i + 1]; // Move the elements
                   }
                 }
                if(index == -1)
                  return false;
              // Reducir el tamaño del array
                if(size > 1)
                  ArrayResize(array, size - 1);
                else
                  if(size <= 1)
                    ArrayFree(array);
                    
                return true;
               }

              この関数は、構造体の中にticketという名前の要素が含まれていることを前提としている点に注意する必要があります。この要素を持たない構造体に対して使用しようとすると、エラーが発生します。

              'ticket' - undeclared identifier        
              in template 'bool RemoveIndexFromAnArrayOfPositions(T&[],const ulong)' specified with [T=Message]      
              see template instantiation 'ExtraFunctions::RemoveIndexFromAnArrayOfPositions<Message>' 
              1 errors, 0 warnings            
              

              この場合、Message構造体にはticket要素が含まれていません。

              文字列を繰り返すための追加関数

              最後に、必要な回数だけテキスト文字列を繰り返して生成する便利な関数を紹介します。これは、テーブルを出力したり、情報を視覚的に区切る際に特に役立ちます。

              実装は以下の通りです。

              string StringRepeat(string str, int count)
               {
                string result = "";
                for(int i = 0; i < count; i++)
                  result += str;
              
                return result;
               } 
              

              たとえば 

              StringRepeat("-", 10) мы получим "----------".

              を呼び出すと、これらの高度な関数は、配列操作を大幅に簡素化し、コードの可読性を向上させるとともに、トレーディングフレームワークにおいて動的かつ正確なリスク管理をおこなうための効率的で汎用性の高いツールを提供します。


              動的リスクの構築

              最後に、最も重要な部分である動的リスクの実装に到達しました。これをおこなうために、処理を大幅に簡素化し、適応性を向上させる2つの重要な関数を使用します。

              開始する前に、以下のライブラリをインクルードする必要があります。

              #include <Generic\HashMap.mqh>

              このライブラリは、動的リスクに関連するデータを正しく整理および処理するのに役立ちます。

              動的リスク初期化関数

              前回の記事で示した基本的な概念に従い、動的リスクシステムは2つのdouble配列を含む構造に基づいています。1つは適用する新しいリスク値を格納し、もう1つはその変更を発動させるための残高レベルまたは割合を格納します。

              MQL5言語の制約により、double配列をそのままEAにパラメータとして渡すことはできません。そのため、この制限を回避するために、カンマ区切りの文字列を使用し、以前に実装した関数によって数値配列へ変換します。

              動的リスク初期化の主要関数は、これらの文字列を数値配列に変換し、重複をチェックし、さらに値が正しくソートされていることを保証します。

              関数の宣言は以下のとおりです。

                void               SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_);

              内部処理の流れは次の通りです。

              1. 動的リスク使用の検証

              最初に、すべての操作をおこなう前にGMLPOの割合が正しく定義されているかを確認します。

               if(this.gmlpo.assigned_percentage == 0)
                  return;
                  
                if(this.gmlpo.mode_calculation_risk == money)
                {
                this.ActivateDynamicRiskPerOperation = false;
                Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' ");
                return;
                } 

              2. 選択したチェックタイプの目的

                this.revision_type = revision_type_;
              

              3.文字列から数値配列への変換

              以前作成した関数を使用して、文字列をdouble配列へ変換します。

              //---
                ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ',');
                ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ',');

              4. 変換結果の検証

              //---
                if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 && this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1)
                 {
                  Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1");
                  this.ActivateDynamicRiskPerOperation = false;
                  return;
                 }
                if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size())
                 {
                  Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal");
                  this.ActivateDynamicRiskPerOperation = false;
                  return;
                 }
              
                Print(EA_NAME, " Arrays before revision");
                PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance");
                PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");

              次に、両方の配列が同じ長さであり、かつ空でないことを確認します。条件を満たさない場合は、エラーを回避するため動的リスクを無効化します。

              5.クリーニングと最終構造の準備

              最後に、HashMapをクリアし、FTMOまたは個人口座の種類に応じて参照残高を選択し、重複や無効インデックス処理用の補助配列を準備します。

               balanceRiskMap.Clear();
                this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm  : AccountInfoDouble(ACCOUNT_BALANCE);
                int indexes_to_remove[];

              この関数により、各口座や戦略に適応した信頼性の高い動的リスク設定が可能になり、正確で安定したリスク管理が実現されます。

              6. HashMapへの有効要素追加ループ

              次に、動的リスク管理の中核となるループを実装します。ここでは有効な要素のみをHashMapに追加します。以下の条件に該当する場合、要素は無効とみなされます。

              • リスクを発動させる割合が0以下
              • 新しいリスク値が0以下
              • すでにHashMapに存在する(重複)

                無効な要素は一時的にindexes_to_removeに保存され、後で削除されます。両方の配列(balance_to_activate_the_riskとrisk_to_be_adjusted)を評価する際には、ペアのどちらか一方の値が無効であるだけで、そのペア全体を除外するのに十分であり、データの整合性と一貫性を維持することができます。

                以下がループの実装です。

                //---
                  for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++)
                   {
                
                    if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0)
                     {
                      Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)");
                      ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
                      continue;
                     }
                
                    if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0)
                     {
                      Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)");
                      ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
                      continue;
                     }
                
                    if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false)
                      balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]);
                    else
                      ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
                   }

                たとえば、次のような配列があるとします。

                • [1, 3, 5]
                • [0.1, 0.3, 0.0]

                  この場合、最後のペア(5 - 0.0)は調整後のリスク値が0であるため無効となります。その結果、5は該当する配列から削除されます。

                  7.重複および無効要素の削除

                  無効または重複している要素を特定した後、それらの削除処理をおこないます。さらに、メインのbalance_to_activate_the_risk配列を再構成し、正しい順序とデータの一貫性を確保します。

                  //---
                    ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove);
                    ArraySort(dynamic_gmlpos.balance_to_activate_the_risk);
                    ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk)); 
                   
                  

                  削除後に両方の配列の整合性を保つためには、risk_to_be_adjusted配列のリサイズが必要になります。

                  8. リスク値の調整と変換

                  次に、balance_to_activate_the_riskに格納されている割合を、選択された口座残高に基づいた金額へと変換します。そして、その結果をrisk_to_be_adjusted配列に反映させます。

                  //---
                    for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++)
                     {
                      double value;
                      balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value);
                      dynamic_gmlpos.risk_to_be_adjusted[i] = value;
                      dynamic_gmlpos.balance_to_activate_the_risk[i] =  this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0));
                     }

                  9. 動的リスクの最終初期化

                  最後に、動的リスクが正しく動作するように必要な変数を初期化します。。

                  //---
                    this.index_gmlpo = 0;
                    this.ActivateDynamicRiskPerOperation = true;
                    this.TheMinimumValueIsExceeded = false;
                    this.NewBalanceToOvercome = 0.00;
                    Print(EA_NAME, " Arrays ready: ");
                    PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance");
                    PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");

                  このようにして、動的リスク調整は正確かつ効率的になり、取引戦略の現在の指標に継続的に適応することができます。

                  以下が完全な関数です。

                  //+------------------------------------------------------------------+
                  //| Function to set dynamic risks per operation                      |
                  //+------------------------------------------------------------------+
                  void CRiskManagemet::SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_)
                   {
                    if(this.gmlpo.assigned_percentage <= 0)
                      return;
                    
                    if(this.gmlpo.mode_calculation_risk == money)
                    {
                    this.ActivateDynamicRiskPerOperation = false;
                    Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' ");
                    return;
                    }
                    
                  //---
                    this.revision_type = revision_type_;
                  
                  //---
                    ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ',');
                    ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ',');
                  
                  //---
                    if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 || this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1)
                     {
                      Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1");
                      this.ActivateDynamicRiskPerOperation = false;
                      return;
                     }
                    if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size())
                     {
                      Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal");
                      this.ActivateDynamicRiskPerOperation = false;
                      return;
                     }
                  
                    Print(EA_NAME, " Arrays before revision");
                    PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance");
                    PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");
                  
                  //---
                    balanceRiskMap.Clear();
                    this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm  : AccountInfoDouble(ACCOUNT_BALANCE);
                    int indexes_to_remove[];
                  
                  //---
                    for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++)
                     {
                  
                      if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0)
                       {
                        Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)");
                        ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
                        continue;
                       }
                  
                      if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0)
                       {
                        Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)");
                        ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
                        continue;
                       }
                  
                      if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false)
                        balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]);
                      else
                        ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
                     }
                  
                  //---
                    ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove);
                    ArraySort(dynamic_gmlpos.balance_to_activate_the_risk);
                    ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk));
                  
                  //---
                    for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++)
                     {
                      double value;
                      balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value);
                      dynamic_gmlpos.risk_to_be_adjusted[i] = value;
                      dynamic_gmlpos.balance_to_activate_the_risk[i] =  this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0));
                     }
                  
                  //---
                    this.index_gmlpo = 0;
                    this.ActivateDynamicRiskPerOperation = true;
                    this.TheMinimumValueIsExceeded = false;
                    this.NewBalanceToOvercome = 0.00;
                    Print(EA_NAME, " Arrays ready: ");
                    PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance");
                    PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");
                   }

                  GMLPOの動的リスク割合を変更する関数の詳細かつ明確な説明

                  次に、CheckAndModifyThePercentageOfGmlpo関数について段階的に説明します。この関数は取引ごとの動的リスク管理を可能にし、取引口座のエクイティ水準に応じて自動調整します。

                  1. 初期検証

                  関数はまず、動的リスク管理が有効になっているかを確認します。無効の場合は、その時点で処理を終了します。

                  if(!this.ActivateDynamicRiskPerOperation)
                     return;

                  2. 現在の残高の検証

                  次に、現在の口座エクイティ(含み損益を含む実際の口座価値)を取得し、事前に選択された基準残高(chosen_balance)と比較します。

                  現在のエクイティが選択された残高を上回っており、かつ次の目標残高レベルにまだ到達していない場合、この時点では変更を加えず関数を終了します。

                  double account_equity = AccountInfoDouble(ACCOUNT_EQUITY);
                  
                  if(account_equity > this.chosen_balance && this.NewBalanceToOvercome != this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo])
                     return;

                  3.エクイティ減少時のリスク変更

                  エクイティが指定されたレベルを下回った場合、フレームワークは次に到達すべき低い残高レベルを決定します。

                  • あるレベルを下回った場合には以下が実行されます。

                    1. そのレベルに対応する新しいリスク割合が決定される
                    2. 内部変数が更新され、新しいリスクレベルが反映される
                    if(this.TheMinimumValueIsExceeded == false)
                     {
                      if(account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo])
                       {
                        PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]);
                  
                        while(IsStopped() == false && index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size())
                         {
                          if(index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - 1)
                           {
                            if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]  > account_equity
                               && account_equity > this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo + 1])
                             {
                              this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo];
                              this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo];
                              index_gmlpo ++;
                              PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome);
                              break;
                             }
                           }
                          else
                            if(index_gmlpo == this.dynamic_gmlpos.balance_to_activate_the_risk.Size() -  1)
                             {
                              if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]  > account_equity)
                               {
                                this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo];
                                this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo];
                                this.TheMinimumValueIsExceeded = true;
                                PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome);
                                PrintFormat("%s The minimum value %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]);
                                break;
                               }
                             }
                  
                          index_gmlpo++;
                         }
                  
                        PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage);
                        SetGMLPO();
                       }
                     }

                  4. エクイティ回復時のリスク復元


                  その後エクイティが再び上昇し、以前に到達していた目標レベルを上回った場合、以下が実行されます。

                  • 1取引あたりのリスクを再調整し、配列で定義されたレベルに基づいて、以前のリスク水準へ段階的に復元する

                    if(this.NewBalanceToOvercome > 0.00)
                     {
                      if(account_equity > this.NewBalanceToOvercome)
                       {
                        PrintFormat("%s Equity %.2f exceeded balance to shift risk to %.2f", EA_NAME, account_equity, NewBalanceToOvercome);
                        while(!IsStopped() && index_gmlpo > 0)
                         {
                          if(index_gmlpo > 0)
                           {
                            if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]  < account_equity
                               && account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1])
                             {
                              break;
                             }
                  
                            this.index_gmlpo--;
                           }
                         }
                  
                        this.TheMinimumValueIsExceeded = false;
                        if(this.index_gmlpo == 0)
                         {
                          Print(EA_NAME, " Excellent, the balance has been positively exceeded");
                          PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]);
                          this.gmlpo.assigned_percentage = this.gmlpo_percentage;
                          this.NewBalanceToOvercome = 0.00;
                  
                          PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage);
                          SetGMLPO();
                         }
                        else
                          if(index_gmlpo > 0)
                           {
                            Print(EA_NAME, " Excellent, the balance has been positively exceeded");
                            PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]);
                            this.NewBalanceToOvercome  = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1];
                            this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo - 1];
                  
                            PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage);
                            PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome);
                            SetGMLPO();
                           }
                       }
                     }


                  結論

                  本連載を通して、信頼性の高い包括的なリスク管理システムをどのように段階的に構築していくかを解説しました。最終回では、その詳細を整理し、1取引あたりの動的リスクという高度で非常に有用な概念を導入しました。この仕組みは、実際の口座結果に基づいてリスクレベルを自動的に調整するものであり、資金保全と取引パフォーマンスの向上において重要な役割を果たします。

                  特にMQL5プログラミングを始めたばかりの方にとって、本資料が少しでも役に立てば幸いです。

                  次のリスク管理セクションでは、このアプローチをさらに拡張し、これまで学んだ内容を実際の自動売買ロボットへ統合することで、実運用への応用方法を学びます。

                  そのために、以前開発したオーダーブロックインジケーターを使用します。

                  さらに、適切なリスク管理をおこなった場合とおこなわなかった場合の具体的な違いについても明確に確認できます。最終的には、これらの高度なツールを日常的に自動売買戦略へ組み込むための設定もおこない、運用を大幅に簡略化します。


                  以下は、この記事で使用または更新されたファイルです。

                  ファイル名 種類 説明 
                   Risk_Management.mqh   .mqh(インクルードファイル) システム全体のリスク管理を担当するCRiskManagementクラスおよび共通関数を含む主要ファイル。このファイルでは利損管理に関するすべての機能を定義および拡張していますl。


                  MetaQuotes Ltdによりスペイン語から翻訳されました。
                  元の記事: https://www.mql5.com/es/articles/17508

                  添付されたファイル |
                  Risk_Management.mqh (126.71 KB)
                  取引におけるニューラルネットワーク:市場異常の適応型検出(最終回) 取引におけるニューラルネットワーク:市場異常の適応型検出(最終回)
                  時系列データにおける異常検知のための高度なツールであるDADAフレームワークの基盤となるアルゴリズムの構築を続けます。このアプローチにより、ランダムな変動と重要な逸脱を効果的に区別することができます。従来の手法とは異なり、DADAはさまざまなデータタイプに動的に適応し、それぞれのケースにおいて最適な圧縮レベルを選択します。
                  市場シミュレーション(第13回):ソケット(VII) 市場シミュレーション(第13回):ソケット(VII)
                  xlwingsなど、Excelへの直接的な読み書きを可能にするパッケージを用いて何かを開発する場合には、すべてのプログラム、関数、または手続きは実行され、その処理を完了すると同時に終了するという点に注意する必要があります。どれだけ工夫をしても、それらを継続的なループ処理として動作させ続けることはできません。
                  エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
                  この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
                  取引におけるニューラルネットワーク:市場異常の適応型検出(DADA) 取引におけるニューラルネットワーク:市場異常の適応型検出(DADA)
                  時系列データにおける異常検知のための革新的手法であるDADAフレームワークについてご紹介します。本手法は、ランダムな変動と疑わしい逸脱を区別することを可能にします。従来の方法とは異なり、DADAは柔軟性を持っており、さまざまな種類のデータに適応します。固定された圧縮レベルを用いるのではなく、複数の選択肢の中から各ケースに最も適したものを選択する点が特徴です。