English Русский 中文 Español Deutsch Português
preview
アルゴリズム取引のリスクマネージャー

アルゴリズム取引のリスクマネージャー

MetaTrader 5トレーディング | 16 10月 2024, 10:37
212 0
Aleksandr Seredin
Aleksandr Seredin

内容


はじめに

この記事では、アルゴリズム取引におけるリスクを管理するためのリスクマネージャーを開発します。この記事の目的は、管理されたリスクの原則をアルゴリズム取引に適応させ、別クラスで実施することで、誰もがデイトレードや金融市場への投資におけるリスク標準化アプローチの有効性を検証できるようにすることです。ここで紹介する資料は、前回の記事「手動取引のリスクマネージャー」で要約した情報を使用し、補足するものです。前回の記事では、リスク管理によって、たとえ収益性の高い戦略であっても取引結果を大幅に改善し、短期間での大きなドローダウンから投資家を守ることができることを見ました。

前回の記事へのコメントを受けて、この記事では初心者の方にも理解しやすいように、実装方法の選択基準についても補足します。また、取引概念の定義については、コード上のコメントで説明します。また、経験豊富な開発者は、提示された資料を使用して、コードを自分のアーキテクチャに適合させることができます。

本稿で使用するその他の概念と定義

高/低は、バーまたはローソク足で示される一定期間の銘柄価格の高値または安値を示します。

ストップロスとは、ポジションを損切りするための指値のことです。価格が建てたポジションと反対方向に進んだ場合、損失が値を超える前に決済することで、建てたポジションの損失を抑えます。これらの値は、ポジションが建てられた時点で計算されます。

テイクプロフィットは、利益を得てポジションを終了するための指値です。この価格でポジションを終了し、利益を確定します。通常、入金時に予定された利益を捕捉するように設定されるか、または商品の1日のボラティリティが出尽くすと予想される領域で設定されます。つまり、その商品が一定期間内にこれ以上動く可能性がないことが明らかになった場合、反対方向への修正がおこなわれる可能性が高くなります。

テクニカルストップロスとは、テクニカル分析に基づいて設定されたストップロス値です。例えば、ローソク足の高値安値、ブレーク、フラクタルなど、使用する取引戦略に応じて設定されます。この手法の主な特徴は、一定のフォーメーションに基づいてチャートポイントでストップロスを設定することです。この場合、エントリポイントは変更できますが、ストップロスの値は変わりません。価格がストップロスの値に達すれば、テクニカル形成は破られたとみなされ、それに伴い、商品の方向性はもはや意味を持たなくなると想定します。 

計算されたストップロスは、一定期間の銘柄のボラティリティの計算値に基づいて設定されたストップロスです。チャート上のどのフォーメーションとも連動しない点が異なります。この手法では、ストップポジションがパターン上のどの位置にあるかよりも、取引のエントリポイントを見つけることが特に重要です。

ゲッターは、クラスや構造体のプロテクトフィールドの値にアクセスするためのクラスメソッドです。これは、クラス開発者によって実装されたロジック内にクラスの値をカプセル化するために使用され、その機能や、指定されたアクセスレベルでprotectedフィールドの値を変更する可能性はありません。

スリッページは、ブローカーが当初要求した価格と異なる価格で注文を出した場合に発生します。このような状況は、市場別に取引している場合に発生する可能性があります。例えば、注文を送信する際、ポジション量は、預託通貨で取引に設定されたリスクと、ポイントで計算/テクニカルストップロスに基づいて計算されます。そして、ブローカーがポジションを建てたとき、ポイントによるストップロスが計算された価格以外の価格でポジションが建てられることがあります。例えば、市場が不安定な間は、100ではなく150になるかもしれません。このようなポジションオープンのケースを監視し、オープン注文の結果としてのリスクが(リスクマネジャーのパラメータに対して)予想よりはるかに大きくなった場合は、損失拡大を避けるために、そのような取引を早期に決済すべきです。

デイトレードとは、1日のうちに取引操作をおこなう取引スタイルです。このアプローチによれば、ポジションは翌取引日に移動することはありません。この取引方法では、朝のギャップ、ポジション移動の追加手数料、翌日のトレンドの変化などに伴うリスクを考慮する必要はありません。原則として、未決済のポジションが翌日に持ち越される場合、この取引スタイルは中期的とみなすことができます。

ポジション取引は、取引量を増やしたり減らしたりすることなく、また追加のエントリ取引を実行することなく、ある商品のポジションを口座上に維持する取引スタイルです。この手法では、ある商品のシグナルを受信すると、トレーダーは即座にそのシグナルに基づいて全リスクを計算し、それを処理します。前のポジションが完全にクローズするまで、他のシグナルは考慮されません。 

取引銘柄のモメンタムとは、指定されたタイムフレームにおける、ロールバックのない一方向の銘柄の動きのことです。モメンタムの起点は、方向転換のない同じ方向への動きの起点となります。価格がモメンタムの開始点に戻る場合、これは通常、再テストと呼ばれます。商品のノーロールバックムーブメントのポイントの価値は、とりわけ、現在の市場のボラティリティ、重要なニュースリリース、または商品自体の重要な価格水準に左右されます。


アルゴリズム取引のサブクラス

前回の記事で書いたRiskManagerBase基本クラスには、活発なデイ取引中に、より安全な作業をおこなうためのリスク管理ロジックを提供するために必要な機能がすべて含まれています。この機能をすべて重複させないために、オブジェクト指向MQL5プログラミングの最も重要な原則の1つである継承を使用します。このアプローチにより、以前に書かれたコードを使用し、クラスをあらゆる取引アルゴリズムに組み込むために必要な機能で補うことができます。

プロジェクトアーキテクチャは以下の原則に基づいて構築されます。

  • 同じ機能の書き直しを避けることによる時間の節約
  • プログラミング原則の遵守
  • 複数の開発チームのためのアウトアーキテクチャによる作業の容易さ
  • プロジェクトをあらゆる取引戦略に拡大する可能性の提供

最初の点は、すでに述べたように、開発時間を大幅に短縮するためのものです。継承機能を使用することで、制限やイベントによる操作のロジックを以前作成したまま維持し、新しいコードのコピーやテストに時間を浪費しないようにします。

2つ目のポイントは、プログラミングにおけるクラス構築の基本原則に関するものです。まず、「オープンクローズド原則」を原則とし、リスク管理の基本原則とアプローチを失うことなく、クラスを拡大していきます。個々のメソッドを追加することで、単一責任原則を保証し、開発を容易にし、コードを論理的に理解できるようにします。これは、次のポイントで説明する原則につながります。

3つ目のポイントは、第三者がロジックを理解できるようにすることです。各クラスに別々のファイルを使うことで、開発者チームが同時に作業するのに便利になります。

またfinal指定子を使用して RiskManagerAlgo クラスからの継承を制限せず、さらなる継承の可能性を通じてさらに改善できるようにします。これによって、サブクラスをほとんどすべての取引システムに柔軟に適応させることができます。

以上の原則を踏まえると、クラスは次のようになります。

//+------------------------------------------------------------------+
//|                       RiskManagerAlgo                            |
//+------------------------------------------------------------------+
class RiskManagerAlgo : public RiskManagerBase
  {
protected:
   CSymbolInfo       r_symbol;                     // instance
   double            slippfits;                    // allowable slippage per trade
   double            spreadfits;                   // allowable spread relative to the opened stop level
   double            riskPerDeal;                  // risk per trade in the deposit currency

public:
                     RiskManagerAlgo(void);        // constructor
                    ~RiskManagerAlgo(void);        // destructor

   //---getters
   bool              GetRiskTradePermission() {return RiskTradePermission;};


   //---interface implementation
   virtual bool      SlippageCheck() override;  // checking the slippage for an open order
   virtual bool      SpreadMonitor(int intSL) override;           // spread control
  };
//+------------------------------------------------------------------+

既存のRiskManagerBase基本クラスのフィールドとメソッドに加えて、RiskManagerAlgoサブクラスでは、アルゴリズムによるエキスパートアドバイザー (EA) のための追加機能を提供するために、以下の要素を提供しています。まず、基本クラスRiskManagerBaseから派生クラスRiskTradePermissionのprotectedレベルフィールドのデータを取得するゲッターが必要です。このメソッドは、注文条件セクションでアルゴリズム的に新規ポジションを建てる許可をリスクマネジャーから得る主な方法となります。この変数にtrueが含まれていれば、EAは取引戦略のシグナルに従って注文を出し続けることができ、falseであれば、取引戦略が新しいエントリポイントを示していても注文を出すことはできません。

また、銘柄フィールドを操作するために、MetaTrader 5端末の標準CSymbolInfoクラスのインスタンスも提供します。CSymbolInfoクラスは、銘柄のプロパティへの簡単なアクセスを提供します。これにより、EAコードを視覚的に短縮し、クラス機能のさらなるメンテナンスをより便利に認識し、便利にすることができます。

ここでのクラスでは、スリップとスプレッドの管理条件に対する追加機能を提供します。slippfitsフィールドには、ユーザー定義のスリッページ条件の管理状態が格納され、スプレッドサイズの条件はspreadfits変数に格納されます。3つ目の必要な変数には、預託通貨での取引ごとのリスクサイズが含まれます。注文のスリッページを管理するために、特別に別の変数が宣言されたことに注意すべきです。原則として、デイトレードでは、取引システムは多くのシグナルを出すため、1日中、リスクサイズのある1つの取引に限定される必要はありません。つまり、トレーダーは取引前に、どの銘柄のどのシグナルを処理するかを事前に把握し、ポジションへの再エントリの回数を考慮した上で、1取引あたりのリスクと1日あたりのリスクを等しく考えます。

これによれば、すべてのエントリのリスクの合計は、その日のリスクを超えてはなりません。確かに、1日に1回しかエントリがないのであれば、これがすべてのリスクかもしれませんが、これはまれなケースです。次のコードをグローバルレベルで宣言してみましょう。便宜上、以前はgroupキーワードを使用して名前付きブロックに「ラップ」していました。

input group "RiskManagerAlgoClass"
input double inp_slippfits    = 2.0;  // inp_slippfits - allowable slippage per open deal
input double inp_spreadfits   = 2.0;  // inp_spreadfits - allowable spread relative to the stop level to open
input double inp_risk_per_deal   = 100;  // inp_risk_per_deal - risk per trade in the deposit currency

この項目は、ユーザーが指定した条件に従って、ポジションの監視を柔軟に設定することができます。

RiskManagerAlgoクラスのpublicセクションで、オーバーライドするインターフェイスの仮想関数を以下のように宣言します。

//--- implementation of the interface
   virtual bool      SlippageCheck() override;  // checking the slippage for an open order
   virtual bool      SpreadMonitor(int intSL) override;           // spread control

これは、RiskManagerBase基本クラスとRiskManagerAlgo派生クラスの関数の中から、実行時に適切なメンバーを動的に選択するメカニズムを提供する関数指定子として機能します。これらの共通の親は、純粋な仮想関数インターフェイスとなります。

RiskManagerAlgoサブクラスのコンストラクタで、ユーザーが入力パラメータを通して入力した値をクラスインスタンスの対応するフィールドにコピーすることにより、初期化をおこないます。

//+------------------------------------------------------------------+
//|                        RiskManagerAlgo                           |
//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(void)
  {
   slippfits   = inp_slippfits;           // copy slippage condition
   spreadfits  = inp_spreadfits;          // copy spread condition
   riskPerDeal  = inp_risk_per_deal;      // copy risk per trade condition
  }

ここで注意しなければならないのは、クラスフィールドを直接初期化した方が実用的な場合もあるということです。しかし、この場合、それほど大きな違いはないので、便宜上、コピーによる初期化を残すことにします。次に、以下のコードを使うことができます。

//+------------------------------------------------------------------+
//|                        RiskManagerAlgo                           |
//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(void):slippfits(inp_slippfits),
                                       spreadfits(inp_spreadfits),
                                       rispPerDeal(inp_risk_per_deal)
  {

  }

クラスのデストラクタでは、メモリを「手動で」クリーンアップする必要はないので、関数本体は空のままにしておきます。

//+------------------------------------------------------------------+
//|                         ~RiskManagerAlgo                         |
//+------------------------------------------------------------------+
RiskManagerAlgo::~RiskManagerAlgo(void)
  {

  }

必要な関数がすべてRiskManagerAlgoクラスで宣言されたので、ポジションのショートストップロスを扱うインターフェイスを実装するメソッドの選択に移りましょう。


ショートストップロスを扱うためのインターフェイス

mql5プログラミング言語は、柔軟な開発と最適な実装で必要な機能を使用することを可能にします。この機能の一部はC++から移植され、一部はより開発しやすくするために補足拡張されました。ショートストップロスで建てたポジションの管理に関連する機能を実装するために、リスクマネージャークラスで継承するだけでなく、他のEAアーキテクチャでも継承できるような、親として使用できる汎化オブジェクトが必要です。

特定の機能を実装し接続するために作られた汎用データ型を宣言するには、C++スタイルの抽象クラスと、インターフェイスのような別のデータ型の両方を使うことができます。 

抽象クラスは、インターフェイスと同様に、一般化された実体を作成することを目的としており、それを基に、より具体的な派生クラスが作成されます。ここでの場合は、ショートストップロスポジションを扱うためのクラスです。抽象クラスとは、あるサブクラスの基本クラスとしてのみ使用できるクラスのことで、抽象クラス型のオブジェクトを作成することはできません。この一般化されたエンティティを使う必要がある場合、クラスのコードは次のようになります。

//+------------------------------------------------------------------+
//|                         CShortStopLoss                           |
//+------------------------------------------------------------------+
class CShortStopLoss
  {
public:
                     CShortStopLoss(void) {};         // the class will be abstract event if at least one function in it is virtual
   virtual          ~CShortStopLoss(void) {};         // the same applies to the destructor

   virtual bool      SlippageCheck()         = NULL;  // checking slippage for the open order
   virtual bool      SpreadMonitor(int intSL)= NULL;  // spread control
  };

MQL5プログラミング言語は、エンティティを一般化するための特別なデータ型インターフェイスを提供します。その表記法ははるかにコンパクトかつシンプルで、機能に違いはないため、このタイプを使用します。実際、インターフェイスもまた、メンバーやフィールドを含むことができず、コンストラクタやデストラクタを持つことができないクラスです。インターフェイスで宣言されたメソッドはすべて、明示的な定義がなくても純粋に仮想的です。インターフェイスのような汎用エンティティを介した実装は次のようになります。

interface IShortStopLoss
  {
   virtual bool   SlippageCheck();           // checking the slippage for an open order
   virtual bool   SpreadMonitor(int intSL);  // spread control
  };

さて、使用する汎用エンティティのタイプが決まったので、サブクラスのインターフェイスですでに宣言されているメソッドの必要な機能をすべて実装する作業に移りましょう。


未決済注文のスリッページ管理

まず最初に、SlippageCheck(void)メソッドを実装するために、チャートの銘柄のデータを更新する必要があります。これはCSymbolInfoインスタンスのRefresh()メソッドを使っておこないます。銘柄を特徴づけるすべてのフィールドが更新され、さらなる操作が可能になります。

   r_symbol.Refresh();                                                  // update symbol data

Refresh()メソッドは、指定された銘柄の現在の価格のデータのみを更新する同じクラスの類似のメソッドRefreshRates(void)とは異なり、CsymbolInfoクラスのすべてのフィールドデータを更新します。この実装のRefresh()メソッドは、EAの各反復で最新の情報を確実に使用するために、毎ティック呼び出されます。

Refresh()メソッドの変数のスコープでは、すべてのポジションを反復処理してオープン時に発生する可能性のあるスリッページを計算するために、ポジションのプロパティのデータを格納する動的変数が必要になります。ポジションの情報は以下の形式で保存されます。

   double PriceClose = 0,                                               // close price for the order
          PriceStopLoss = 0,                                            // stop loss price for the order
          PriceOpen = 0,                                                // open price for the order
          LotsOrder = 0,                                                // order lot volume
          ProfitCur = 0;                                                // current order profit

   ulong  Ticket = 0;                                                   // order ticket
   string Symbl;                                                        // symbol

損失が発生した場合のティック値のデータを取得するには、RiskManagerAlgoクラスの内部で宣言されたCsymbolInfoクラスのインスタンスのTickValueLoss()メソッドを使用します。ここから得られる値は、標準ロットで価格が最低1ポイント変動した場合に、口座残高がいくら変動するかを示しています。この値は、ポジションの実際の建値における潜在的な損失を計算するために後で使用します。このメソッドは、すべてのティックとポジションを建てた直後に機能するため、ここでは「潜在的な」という用語を使用します。つまり、ストップロス価格よりも始値にまだ近いにもかかわらず、次のティックですぐに、取引でどれだけの損失が出るかを確認することができるのです。 

   double lot_cost = r_symbol.TickValueLoss();                          // get tick value
   bool ticket_sc = 0;                                                  // variable for successful closing

ここでは、スリッページのためにポジションを決済する必要があることが計算で示された場合に、未決済のポジションを決済する注文の実行をチェックするために必要な変数も宣言します。これはブール変数ticket_scです。

これで、スリッページ管理手法の枠組みの中で、すべてのポジションを反復処理できるようになりました。端末内のポジションの数によって制限されるforループを編成することによって、ポジションを反復します。ポジション数の値を取得するには、あらかじめ定義された端末関数PositionsTotal() を使用します。標準端末クラスCPositionInfoのSelectByIndex()メソッドを使用して、インデックスごとにポジションを選択します。

r_position.SelectByIndex(i)

一旦ポジションが選択されると、同じ標準的な端末クラスCPositionInfoを使用して、そのポジションのプロパティのクエリを開始することができます。しかしその前に、選択したポジションがこのEAインスタンスが稼働している銘柄に対応しているかどうかをチェックする必要があります。これは、ループの中で以下のコードを使っておこなうことができます。

         Symbl = r_position.Symbol();                                   // get the symbol
         if(Symbl==Symbol())                                            // check if it's the right symbol

インデックスによって選択されたポジションがチャートに属していることを確認してから、ポジションを確認するために他のプロパティを照会することができます。位置プロパティのさらなるクエリも、以下のように標準端末クラスCPositionInfoのインスタンスを使っておこなわれます。

            PriceStopLoss = r_position.StopLoss();                      // remember its stop loss
            PriceOpen = r_position.PriceOpen();                         // remember its open price
            ProfitCur = r_position.Profit();                            // remember financial result
            LotsOrder = r_position.Volume();                            // remember order lot volume
            Ticket = r_position.Ticket();

このチェックは、銘柄だけでなく、選択したポジションを建てるときに使用するMqlTradeRequest構造体のマジックナンバーでも実行できます。この方法は、1つの口座内で異なる戦略が実行する取引操作を分離するためによく使われますが、分析のしやすさ、コンピューティングリソースの利用という観点から、別々の戦略には別々の口座を使う方が良いと思うので、ここでは使用しません。もし、他のアプローチをお使いならば、この記事のコメント欄で共有してください。さて、メソッドの実装に移りましょう。

このメソッドにはポジションのクローズも含まれます。買いポジションはBid、売りポジションはAskで決済されるため、選択したポジションの種類に応じて、終値を取得するロジックを実装する必要があります。

            int dir = r_position.Type();                                // define order type

            if(dir == POSITION_TYPE_BUY)                                // if it is Buy
              {
               PriceClose = r_symbol.Bid();                             // close at Bid
              }
            if(dir == POSITION_TYPE_SELL)                               // if it is Sell
              {
               PriceClose = r_symbol.Ask();                             // close at Ask
              }

ここでは部分決済のロジックは考慮しませんが、ブローカーによって許可されていれば、端末は技術的にこれを実装することができますが、このタイプの決済はメソッドロジックには含まれていません。

選択したポジションが要件を満たし、必要な機能をすべて備えていることを確認したら、そのポジションに対する実際のリスクの計算に移ることができます。まず、実際の始値を考慮して、結果として得られるストップ レベルのサイズを最小ポイントで計算し、それを当初予想されていたものと比較する必要があります。

始値とストップロスの差の絶対値でサイズを計算します。そのために、定義済みの端末関数MathAbs()を使用します。端数の価格値からポイント単位の整数値を得るには、MathAbs()から受け取った値を端数の1ポイントの値で割ります。1点の値を求めるには、標準端末クラスCPositionInfoインスタンスのPoint()メソッドを使います。

int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // check the resulting stop

ここで、選択したポジションの実際の潜在的損失値を取得するには、取得したストップ値(ポイント単位)に、ロット単位のポジションサイズとポジション銘柄の値の1ティックを掛けるだけです。これは次のようにおこなわれます。

double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // calculate risk upon reaching the stop level

ここで、受け取った値がユーザーが入力した取引のリスク偏差の範囲内かどうかを整理してみましょう。この値はslippfits変数で指定されます。値がこの範囲を超えていれば、選択したポジションをクローズします。

             if(
                  potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) &&   // if the resulting stop exceeds risk per trade given the threshold value
                  ProfitCur<0                                                  &&   // and the order is at a loss
                  PriceStopLoss != 0                                                // if stop loss is not set, don't touch
               )

この条件セットには、さらに2つのチェックが追加されており、取引状況を処理するための以下のロジックが含まれています。

まず、条件「ProfitCur < 0」は、スリッページがポジションが損失ゾーンにある場合のみ処理されることを保証します。これは、取引戦略に基づくもので、スリッページが通常、市場のボラティリティが高い時に発生するからです。この条件では、ポジションがテイクプロフィットに向かっている間にスリッページが発生すると、ストップロスの設定が緩み、テイクプロフィット値が減少します。その結果、取引のリスクとリターンのバランスが悪化し、計画されたリスクに比べて潜在的な損失が増える可能性があります。ただし、ポジションがすでに「下落」しているモメンタムが続く可能性が高いため、逆にテイクプロフィットに達する可能性も上がります。この条件は、モメンタムが続いてスリッページが発生し、利益確定に達する前にストップロスにかかってしまう可能性がある場合、ポジションを決済することを意味します。

2つ目の条件「PriceStopLoss != 0」は、ストップロスが設定されていないポジションを保有し続けるリスクを避けるために必要なロジックを実装しています。つまり、ポジションを建てる際に、価格が不利に動いたとしても、そのポジションがその日のリスクをすべてカバーできる可能性があることをトレーダーが承知していることを意味します。これは、その日の取引予定のすべての銘柄に対して十分なリミットが設定されていない場合があり、そのため非常にリスクが高い状態となります。一方で、こうした銘柄がプラスに転じ、利益をもたらす可能性もあります。ストップロスがないポジションでは、こうした状況でエントリができない場合もあります。この条件を含めるかどうかは、個々の取引戦略によって判断する必要があります。ただし、私たちの実装では複数の商品を同時に取引することはないため、ストップロスのないポジションを削除することはありません。

ポジションのスリッページを特定するために必要なすべての条件が満たされた場合、基本クラスRiskManagerBaseで宣言された標準CTradeクラスのPositionClose()メソッドを使用してポジションをクローズします。入力パラメータとして、クローズするポジションの以前に保存したチケット番号を渡します。クローズ操作関数の呼び出し結果はticket_sc変数に保存され、注文の実行状況を管理します。

ticket_sc = r_trade.PositionClose(Ticket);                        // close order

メソッドのコード全体は以下の通りです。

//+------------------------------------------------------------------+
//|                         SlippageCheck                            |
//+------------------------------------------------------------------+
bool RiskManagerAlgo::SlippageCheck(void) override
  {
   r_symbol.Refresh();                                                  // update symbol data

   double PriceClose = 0,                                               // close price for the order
          PriceStopLoss = 0,                                            // stop loss price for the order
          PriceOpen = 0,                                                // open price for the order
          LotsOrder = 0,                                                // order lot volume
          ProfitCur = 0;                                                // current order profit

   ulong  Ticket = 0;                                                   // order ticket
   string Symbl;                                                        // symbol
   double lot_cost = r_symbol.TickValueLoss();                          // get tick value
   bool ticket_sc = 0;                                                  // variable for successful closing

   for(int i = PositionsTotal(); i>=0; i--)                             // start loop through orders
     {
      if(r_position.SelectByIndex(i))
        {
         Symbl = r_position.Symbol();                                   // get the symbol
         if(Symbl==Symbol())                                            // check if it's the right symbol
           {
            PriceStopLoss = r_position.StopLoss();                      // remember its stop loss
            PriceOpen = r_position.PriceOpen();                         // remember its open price
            ProfitCur = r_position.Profit();                            // remember financial result
            LotsOrder = r_position.Volume();                            // remember order lot volume
            Ticket = r_position.Ticket();

            int dir = r_position.Type();                                // define order type

            if(dir == POSITION_TYPE_BUY)                                // if it is Buy
              {
               PriceClose = r_symbol.Bid();                             // close at Bid
              }
            if(dir == POSITION_TYPE_SELL)                               // if it is Sell
              {
               PriceClose = r_symbol.Ask();                             // close at Ask
              }

            if(dir == POSITION_TYPE_BUY || dir == POSITION_TYPE_SELL)
              {
               int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // check the resulting stop

               double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // calculate risk upon reaching the stop level

               if(
                  potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) &&   // if the resulting stop exceeds risk per trade given the threshold value
                  ProfitCur<0                                                  &&   // and the order is at a loss
                  PriceStopLoss != 0                                                // if stop loss is not set, don't touch
               )
                 {
                  ticket_sc = r_trade.PositionClose(Ticket);                        // close order

                  Print(__FUNCTION__+", RISKPERDEAL: "+DoubleToString(riskPerDeal));                  //
                  Print(__FUNCTION__+", slippfits: "+DoubleToString(slippfits));                      //
                  Print(__FUNCTION__+", potentionLossOnDeal: "+DoubleToString(potentionLossOnDeal));  //
                  Print(__FUNCTION__+", LotsOrder: "+DoubleToString(LotsOrder));                      //
                  Print(__FUNCTION__+", curr_sl_ord: "+IntegerToString(curr_sl_ord));                 //

                  if(!ticket_sc)
                    {
                     Print(__FUNCTION__+", Error Closing Orders №"+IntegerToString(ticket_sc)+" on slippage. Error №"+IntegerToString(GetLastError())); // output to log
                    }
                  else
                    {
                     Print(__FUNCTION__+", Orders №"+IntegerToString(ticket_sc)+" closed by slippage."); // output to log
                    }
                  continue;
                 }
              }
           }
        }
     }
   return(ticket_sc);
  }
//+------------------------------------------------------------------+

これで、スリッページ管理方式のオーバーライドは完了しました。次に、新規ポジションを建てる前にスプレッドの大きさを管理するメソッドについて説明します。


ポジションを建てる際のスプレッド管理

SpreadMonitor()メソッドの実装におけるスプレッド管理は、取引を開始する直前の現在のスプレッドと、メソッドへの整数パラメータとして渡される計算/テクニカルストップロスとの予備的な比較で構成されます。この関数は、現在のスプレッドサイズがユーザーの許容範囲内であれば真を返します。そうでない場合、スプレッドサイズがこの範囲を超えると、このメソッドはfalseを返します。

関数の結果は、デフォルトでtrueに初期化された論理bool変数に格納されます。

   bool SpreadAllowed = true;

CSymbolInfoクラスの Spread() メソッドを使用して、銘柄の現在のスプレッドの値を取得します。

   int SpreadCurrent = r_symbol.Spread();

以下は、このチェックに使われた論理比較です。

if(SpreadCurrent>intSL*spreadfits)

つまり、現在の銘柄のスプレッドが、必要なストップロスとユーザーが指定した係数の積よりも大きい場合、このメソッドはfalseを返し、現在のスプレッドサイズのポジションが次のティックまで開かないようにする必要があります。以下はその説明です。

//+------------------------------------------------------------------+
//|                          SpreadMonitor                           |
//+------------------------------------------------------------------+
bool RiskManagerAlgo::SpreadMonitor(int intSL)
  {
//--- spread control
   bool SpreadAllowed = true;                                           // allow spread trading and check ratio further
   int SpreadCurrent = r_symbol.Spread();                               // current spread values

   if(SpreadCurrent>intSL*spreadfits)                                   // if the current spread is greater than the stop and the coefficient
     {
      SpreadAllowed = false;                                            // prohibit trading
      Print(__FUNCTION__+IntegerToString(__LINE__)+
            ". Spread is to high! Spread:"+
            IntegerToString(SpreadCurrent)+", SL:"+IntegerToString(intSL));// notify
     }
   return SpreadAllowed;                                                // return result
  }
//+------------------------------------------------------------------+

この方法を使用する際は、スプレッド条件が非常に厳しい場合、EAはポジションを建てず、関連情報がEAの操作ログに常に記録されることを考慮する必要があります。原則として、少なくとも 2 の係数値が使用されます。つまり、スプレッドがストップの半分である場合は、より小さなスプレッドを待つか、そのような短いストップ ロスでエントリーすることを拒否する必要があります。ストップ レベルがスプレッドのサイズに近いほど、そのようなポジションから損失が発生する可能性が高くなるためです。


インターフェイスの実装

mql5は多重継承をサポートしていないので、インターフェイスが基本クラスの最初の親になります。しかし、今回のプロジェクトでは、一貫した継承スキームを実装することができるので、この制限はありません。 

そのためには、基本クラスRiskManagerBaseに、先に説明したIShortStopLossインターフェイスを継承させる必要があります。

//+------------------------------------------------------------------+
//|                        RiskManagerBase                           |
//+------------------------------------------------------------------+
class RiskManagerBase:IShortStopLoss                        // the purpose of the class is to control risk in terminal

この記法により、必要な機能をサブクラスRiskManagerAlgoに移すことができます。この状況では、このインターフェイスは純粋な仮想関数を持ち、フィールド、コンストラクター、デストラクターを含まないので、継承アクセスレベルは重要ではありません。

カスタムRiskManagerAlgoクラスの最終的な継承構造を図1に示し、完全な機能を提供するためのpublicメソッドのカプセル化を示しています。

図 1.RiskManagerAlgo クラスの継承階層

図1:RiskManagerAlgoクラスの継承階層

さて、アルゴリズムを組み立てる前に、アルゴリズムによるリスク管理の機能をテストするために、意思決定ツールを実装する必要があります。


取引ブロックの実施

前回の「手動取引のリスクマネジャー」稿では、取引ブロックはフラクタルから受け取った入力を基本的に処理するための極めて単純なTradeModelエンティティで構成されていました。前回の記事とは異なり、今回の記事はアルゴリズム取引に関するものなので、フラクタルに基づいたアルゴリズムによる意思決定ツールも作ってみましょう。同じロジックに基づきますが、手動でシグナルを生成するのではなく、すべてをコードで実装するだけです。おまけに、必要な入力を手作業で生成する必要がなくなるので、より多くの期間の過去データで何が起こるかをテストできるようになります。

フラクタルからのシグナルを受け取るCFractalsSignalクラスを宣言しましょう。日足チャートの上側のフラクタルを上抜けたら、EAは買いを入れ、現在の価格が同じく日足チャートの下側のフラクタルを上抜けたら、売りシグナルを出します。取引は、取引が開始された日の取引終了時に、日中ベースで終了します。

CFractalsSignalクラスは、フラクタルブレークを分析するために使用される時間枠に関する情報を含むフィールドを持ちます。このように、フラクタルが分析される時間枠とEAが実行される時間枠は、単に使いやすいように区別することができます。列挙型変数ENUM_TIMEFRAMESを宣言しましょう。

ENUM_TIMEFRAMES   TF;                     // timeframe used

次に、テクニカル指標を扱うための標準的な端末クラスであるCiFractalsへの変数ポインタを宣言します。このクラスは必要なすべての関数を便利に実装しており、すべてを再度記述する必要はありません。

   CiFractals        *cFractals;             // fractals

また、シグナルに関するデータや、シグナルがEAによってどのように処理されるかというデータも保存する必要があります。前回の記事で使用したTradeInputsカスタム構造体を使用します。前回は手動で生成しましたが、今回はCFractalsSignalクラスが生成してくれます。

//+------------------------------------------------------------------+
//|                         TradeInputs                              |
//+------------------------------------------------------------------+

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

どちらの価格が最初にヒットするか事前に知ることはできないので、同時に考慮できるように、買いシグナルと売りシグナルのために、構造体の内部変数を別々に宣言します。

   TradeInputs       fract_Up, fract_Dn;     // current signal

CiFractalsクラスから受け取った現在の値を格納する変数を宣言するだけで、日足チャート上に新しく形成されたフラクタルのデータを得ることができます。

必要な機能を提供するために、CFractalsSignalクラスのpublicドメインにいくつかのメソッドが必要です。これらは最新の現在のフラクタル価格のブレークを監視し、ポジションをオープンするシグナルを与え、これらのシグナル処理の成功を監視する役割を担います。

クラスデータ状態の更新を管理するメソッドはProcess()です。このメソッドは何も返さず、パラメータも取らず、単に入力されるティックごとにデータ状態の更新を実行します。売買シグナルを受け取るメソッドはBuySignal()とSellSignal()と呼ばれます。これらはパラメータを取りませんが、対応する方向にポジションを建てるする必要がある場合、bool値を返します。BuyDone()メソッドとSellDone()メソッドは、対応するポジションのオープン成功に関するブローカーのサーバーレスポンスをチェックした後に呼び出す必要があります。クラスはこのようになります。

//+------------------------------------------------------------------+
//|                       CFractalsSignal                            |
//+------------------------------------------------------------------+
class CFractalsSignal
  {
protected:
   ENUM_TIMEFRAMES   TF;                     // timeframe used
   CiFractals        *cFractals;             // fractals

   TradeInputs       fract_Up, fract_Dn;     // current signal

   double            FrUp;                   // upper fractals
   double            FrDn;                   // lower fractals

public:
                     CFractalsSignal(void);  // constructor
                    ~CFractalsSignal(void);  // destructor

   void              Process();              // method to start updates

   bool              BuySignal();            // buy signal
   bool              SellSignal();           // sell signal

   void              BuyDone();              // buy done
   void              SellDone();             // sell done
  };

クラスコンストラクタでは、TF時間枠フィールドを日次間隔PERIOD_D1に初期化する必要があります。これは、日次チャートのレベルが、テイクプロフィットを達成するために必要なモメンタムを与えるのに十分強力であり、同時に週次および月次チャートのより強いレベルよりもはるかに頻繁に発生するためです。より小さな時間枠をテストする機会を読者に残し、ここでは日足に焦点を当てます。また、このクラスのフラクタル指標を操作するためのクラスオブジェクトのインスタンスを作成し、以下の順序で必要なすべてのフィールドをデフォルトで初期化します。

//+------------------------------------------------------------------+
//|                        CFractalsSignal                           |
//+------------------------------------------------------------------+
CFractalsSignal::CFractalsSignal(void)
  {
   TF  =  PERIOD_D1;                                                    // timeframe used

//--- fractal class
   cFractals=new CiFractals();                                          // created fractal instance
   if(CheckPointer(cFractals)==POINTER_INVALID ||                       // if instance not created OR
      !cFractals.Create(Symbol(),TF))                                   // variant not created
      Print("INIT_FAILED");                                             // don't proceed
   cFractals.BufferResize(4);                                           // resize fractal buffer
   cFractals.Refresh();                                                 // update

//---
   FrUp = EMPTY_VALUE;                                                  // leveled upper at start
   FrDn = EMPTY_VALUE;                                                  // leveled lower at start

   fract_Up.done  = true;                                               //
   fract_Up.price = EMPTY_VALUE;                                        //

   fract_Dn.done  = true;                                               //
   fract_Dn.price = EMPTY_VALUE;                                        //
  }

デストラクタで、コンストラクタで作成したフラクタル指標オブジェクトのメモリをクリアします。

//+------------------------------------------------------------------+
//|                        ~CFractalsSignal                          |
//+------------------------------------------------------------------+
CFractalsSignal::~CFractalsSignal(void)
  {
//---
   if(CheckPointer(cFractals)!=POINTER_INVALID)                         // if instance was created,
      delete cFractals;                                                 // delete
  }

データ更新メソッドでは、CiFractalsクラスインスタンスのRefresh()メソッドを呼び出して、親クラスのCIndicatorから継承したフラクタル価格のデータを更新するだけです。

//+------------------------------------------------------------------+
//|                         Process                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::Process(void)
  {
//---
   cFractals.Refresh();                                                 // update fractals
  }

ここで、フラクタルブレークの水準は日足チャートから得られるので、このデータを必ずしもティックごとに更新する必要はなく、この手法のさらなる最適化の可能性があることに注意したいと思います。さらに、日足チャートに新しいバーが出現したときのイベントメソッドを実装し、それがトリガーされたときだけこのデータを更新することもできます。しかし、この実装はシステムに大きな追加負担を強いるものではなく、追加機能の実装には、性能の向上はわずかでも追加コストが必要になるため、このままにしておくことにします。

買いシグナルを出すBuySignal(void)メソッドでは、まず最新の現在のAsk価格を要求します。

   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // Buy price

次に、CiFractalsクラスインスタンスのUpper()メソッドを通して、必要なバーのインデックスをパラメータとして渡し、上部フラクタルの現在値を要求します。

   FrUp=cFractals.Upper(3);                                             // request the current value

このメソッドに値3を渡すのは、完全に形成されたフラクタル断面のみを使用するためです。時系列では、バッファのカウントは最新のもの(0)から古いものへと上向きに進むので、日足チャート上の「3」は一昨日の前日を意味します。これは、ある瞬間に日足チャート上に形成されたフラクタルブレークを除外し、同じ日の価格が新高値/新安値に達し、フラクタルレベルが消滅することを意味します。

ここで、日足チャートで最後にブレークした価格が変化した場合に、現在のフラクタルブレークを更新するための論理的なチェックをおこなってみましょう。上記のFrUp変数で更新されたフラクタル指標の現在値と、TradeInputsカスタム構造体のpriceフィールドに格納された上位フラクタルの最新の現在値を比較します。指標によってデータが返されない場合 (ブレークが検出されない場合)、priceフィールドにリセットせずに常に最後の現在の価格の値を格納するには、空の指標値「FrUp != EMPTY_VALUE」の別のチェックを追加します。この2つの条件を組み合わせることで、最後のフラクタルの重要な価格値(指標のEMPTY_VALUEに対応するゼロ値を除く)のみを更新し、この変数を空の値で書き換えないことができます。チェック項目は以下の通りです。

   if(FrUp != fract_Up.price           &&                               // if the data has been updated
      FrUp != EMPTY_VALUE)                                              // skip empty value

メソッドの最後に、買いシグナルを受信したかどうかをチェックするための以下のロジックがあります。

   if(fract_Up.price != EMPTY_VALUE    &&                               // skip zero values
      ask            >= fract_Up.price &&                               // if the buy price is greater than or equal to the fractal
      fract_Up.done  == false)                                          // the signal has not been processed yet
     {
      return true;                                                      // generate a signal to process
     }

このブロックでは、最初に、最後に記録されたフラクタルのfract_Up変数がゼロかどうかを確認します。これは、この変数がクラスのコンストラクタで初期化された後、EAが初めて起動されたときに実行されるチェックです。次の条件「ask >= fract_Up.price」は、現在の市場の買値がフラクタルの最後の値を上回ったかどうかを確認します。これがこのメソッドの主要なロジックであり、重要な判断ポイントとなります。最後に、このフラクタルレベルがすでにこのシグナルに対して処理されたかどうかを確認します。ここでのポイントは、フラクタルブレークのシグナルが日足チャートから発生しているということです。そのため、たとえ市場の買値がフラクタルブレークの条件に達した場合でも、私たちの取引はポジションベースでおこなわれており、日中にポジションを増やしたり新たに追加のポジションを開いたりすることはありません。つまり、このシグナルは1日に1回だけ処理する必要があります。これら3つの条件がすべて満たされると、このメソッドはEAがこのシグナルを処理できることを示すために「true」を返します。

上記のロジックを持つメソッドの完全な実装を以下に示します。

//+------------------------------------------------------------------+
//|                         BuySignal                                |
//+------------------------------------------------------------------+
bool CFractalsSignal::BuySignal(void)
  {
   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // Buy price

//--- check fractals update
   FrUp=cFractals.Upper(3);                                             // request the current value

   if(FrUp != fract_Up.price           &&                               // if the data has been updated
      FrUp != EMPTY_VALUE)                                              // skip empty value
     {
      fract_Up.price = FrUp;                                            // process the new fractal
      fract_Up.done = false;                                            // not processed
     }

//--- check the signal
   if(fract_Up.price != EMPTY_VALUE    &&                               // skip zero values
      ask            >= fract_Up.price &&                               // if the buy price is greater than or equal to the fractal
      fract_Up.done  == false)                                          // the signal has not been processed yet
     {
      return true;                                                      // generate a signal to process
     }

   return false;                                                        // otherwise false
  }

上述したように、買いシグナルを取得するメソッドは、ブローカーのサーバーによるこのシグナルの処理を監視するメソッドと連動すべきです。買いシグナルが処理されたときに呼び出されるメソッドは非常にコンパクトです。

//+------------------------------------------------------------------+
//|                         BuyDone                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::BuyDone(void)
  {
   fract_Up.done = true;                                                // processed
  }

このpublicメソッドを呼び出すと、fract_Up構造体のインスタンスの対応する最後のシグナルのdoneフィールドに、シグナルのdoneフラグが設定されます。したがって、このメソッドは、ブローカーのサーバーによる注文オープンが成功したかどうかのチェックに合格したときのみ、EAのメインコードで呼び出されます。

売り方のロジックも似ています。唯一の違いは、AskではなくBid価格を要求することです。それに応じて、現在の価格の条件は、売りのために「フラクタルブレーク未満」であるかどうかをチェックします。

以下は売りシグナルのメソッドです。

//+------------------------------------------------------------------+
//|                         SellSignal                               |
//+------------------------------------------------------------------+
bool CFractalsSignal::SellSignal(void)
  {
   double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);                  // bid price

//--- check fractals update
   FrDn=cFractals.Lower(3);                                             // request the current value

   if(FrDn != EMPTY_VALUE        &&                                     // skip empty value
      FrDn != fract_Dn.price)                                           // if the data has been updated
     {
      fract_Dn.price = FrDn;                                            // process the new fractal
      fract_Dn.done = false;                                            // not processed
     }

//--- check the signal
   if(fract_Dn.price != EMPTY_VALUE    &&                               // skip empty value
      bid            <= fract_Dn.price &&                               // if the ask price is less than or equal to the fractal AND
      fract_Dn.done  == false)                                          // signal has not been processed
     {
      return true;                                                      // generate a signal to process
     }

   return false;                                                        // otherwise false
  }

売りシグナルの処理も同様のロジックです。この場合、doneフィールドは、売りの最後の現在のフラクタルを担当するfract_Dn構造体のインスタンスに従って入力されます。

//+------------------------------------------------------------------+
//|                        SellDone                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::SellDone(void)
  {
   fract_Dn.done = true;                                                // processed
  }
//+------------------------------------------------------------------+

これで、毎日のフラクタルブレークから入力を生成するメソッドの実装が完了したので、プロジェクトの一般的な組み立てに移ることができます。


プロジェクトの組み立てとテスト

プロジェクトのメインファイルの冒頭に必要なコードを含め、上記のすべてのファイルを接続して、プロジェクトの組み立てを開始します。そのために、#includeプリプロセッサコマンドを使用します。ファイル <RiskManagerAlgo.mqh>、<TradeModel.mqh>、<CFractalsSignal.mqh>は、前の章で説明したカスタムクラスです。残りの2つ<Indicators\BillWilliams.mqh>と<Trade\Trade.mqh>は、それぞれフラクタルと取引操作を扱うための標準的な端末クラスです。

#include <RiskManagerAlgo.mqh>
#include <Indicators\BillWilliams.mqh>
#include <Trade\Trade.mqh>
#include <TradeModel.mqh>
#include <CFractalsSignal.mqh>

スリッページ管理法を設定するために、inputメモリクラスのint型の整数変数をもうひとつ追加導入します。ユーザーは、商品の最小ポイント数で許容可能なストップ値を指定します。

input group "RiskManagerAlgoExpert"
input int inp_sl_in_int       = 2000;  // inp_sl_in_int - a stop loss level for a separate trade

より詳細な完全自動の統合または実装では、このスリッページ管理方法を使用する場合、ユーザー入力パラメータではなく、テクニカルストップレベルの設定を担当するクラスからストップ値を返すか、ボラティリティを扱うクラスから計算されたストップを返すことによって、このパラメータの転送を実装する方が良いでしょう。この実装では、ユーザーが特定の戦略に応じてこの設定を変更できるよう、追加の機会を残しておきます。

ここで、リスクマネージャー、ポジション、フラクタルクラスへの必要なポインタを宣言します。

RiskManagerAlgo *RMA;                                                   // risk manager
CTrade          *cTrade;                                                // trade
CFractalsSignal *cFract;                                                // fractal

EAの初期化OnInit()のイベントハンドラ関数内でポインタを初期化します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   RMA = new RiskManagerAlgo();                                         // algorithmic risk manager

//---
   cFract =new CFractalsSignal();                                       // fractal signal

//--- 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

//---
   return(INIT_SUCCEEDED);
  }

CTradeオブジェクトを設定する際、SetTypeFillingBySymbol()メソッドのパラメータにEAが稼働している現在の銘柄を指定します。EAが実行されている現在の銘柄は、定義済みの Symbol()メソッドを使用して返されます。SetDeviationInPoints()メソッドで要求価格から許容できる最大偏差については、もう少し大きな値を指定します。このパラメータはここでの研究にとってそれほど重要ではないので、入力としては実装せず、ハードコードしておきます。また、EAが建てたすべてのポジションのマジックナンバーを静的に登録します。

デストラクタでは、オブジェクトの削除を実装し、ポインタが有効であれば、ポインタによってメモリをクリアします。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(cTrade)!=POINTER_INVALID)                            // if there is an instance,
     {
      delete cTrade;                                                    // delete
     }

//---
   if(CheckPointer(cFract)!=POINTER_INVALID)                            // if an instance is found,
     {
      delete cFract;                                                    // delete
     }

//---
   if(CheckPointer(RMA)!=POINTER_INVALID)                               // if an instance is found,
     {
      delete RMA;                                                       // delete
     }
  }

それでは、新しいティック到着イベントOnTick()のエントリポイントで、EAの本体を記述しましょう。まず、サブクラスでオーバーライドしなかった基本クラスRiskManagerBaseのメインイベント監視メソッド ContoMonitor()を、次のようにサブクラスインスタンスから実行する必要があります。

   RMA.ContoMonitor();                                                  // run the risk manager

このメソッドは前述したように、新しいティックごとに処理し、ポジションの実際のリスクが(設定したストップロスに対して)計画したものに合致しているかどうかをチェックします。

   RMA.SlippageCheck();                                                 // check slippage

私たちのフラクタル意思決定ツールは、複雑な実装を意図したものではなく、リスク管理機能を強調するものです。そのため、このツールではストップロスを設定せず、取引日の終わりにポジションをクローズするだけです。渡されるすべての取引が許可されます。このメソッドを実装して完全に機能させるには、ブローカーのサーバーに対して、ゼロ以外の損切り値が設定された注文のみを送信できる必要があります。

次に、public Process()メソッドを使用して、カスタムCFractalsSignalクラスを通してフラクタルブレーク指標のデータを更新する必要があります。

   cFract.Process();                                                    // start the fractal process

すべてのクラスのすべてのイベントメソッドがコードに含まれたので、注文を出すシグナルの出現を監視するブロックに移ります。売買シグナルのチェックは、CFractalsSignal取引意思決定ツールクラスの対応するメソッドと同じ方法で分離されます。まず、次の2つの条件を使って買いのチェックを説明しましょう。

   if(cFract.BuySignal() &&
      RMA.SpreadMonitor(inp_sl_in_int))                                 // if there is a buy signal

まず最初に、CFractalsSignalクラスのインスタンスのBuySignal()メソッドで買いシグナルの有無をチェックします。このシグナルが出た場合、SpreadMonitor()メソッドを通じて、リスクマネジャーがスプレッドがユーザーによって許容された値と一致していることを確認するかどうかをチェックします。SpreadMonitor()メソッドの唯一のパラメータとして、ユーザー入力inp_sl_in_intを渡します。

両方の条件が満たされた場合、次のような簡略化された論理構造で注文を出します。

      if(cTrade.Buy(0.1))                                               // if Buy executed,
        {
         cFract.BuyDone();                                              // the signal has been processed
         Print("Buy has been done");                                    // notify
        }
      else                                                              // if Buy not executed,
        {
         Print("Error: buy");                                           // notify
        }

CTradeクラスインスタンスのBuy()メソッドを使用して、パラメータに0.1に等しいロット値を渡して注文を出します。リスクマネジャーの運用をより客観的に評価するため、出来高パラメータの統計値を「平滑化」するために、この値は変更しません。つまり、すべての入力がEAの統計において同じ重みを持つことになります。

Buy()メソッドが正しく実行された場合、つまり、ブローカーから肯定的な応答を受信し、取引が開始された場合、直ちにBuyDone()メソッドを呼び出し、シグナルが正常に処理され、この価格での他のシグナルは必要ないことをCFractalsSignalクラスに通知します。買い注文が発注できなかった場合は、操作ログでそのことをEAに通知し、シグナル処理に成功したメソッドを呼び出さず、新たな再開を試みます。

売り注文についても同様のロジックを実装し、一連のコードで売りに対応するメソッドを呼び出します。

前回の記事の取引終了時の決済注文のブロックをそのまま使用します。

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

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

夜間取引なしでデイトレードのロジックを実装しているため、このコードの主なタスクは後の既知のサーバー時間に従って23:00にすべてのポジションをクローズすることです。このコードのロジックについては、前回の記事で詳しく説明しています。ポジションを次の朝まで開いておきたい場合は、EAコードのこのブロックをコメントアウトするか、削除してください。

また、リスクマネージャークラスのMessage()メソッドを渡して、定義済みの端末関数Comment()を使って、リスクマネージャーデータの現在の状態を画面に表示する必要があります。

   Comment(RMA.Message());                                              // display the data state in a comment
以下は新しいティックイベントを処理するコードです。
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   RMA.ContoMonitor();                                                  // run the risk manager

   RMA.SlippageCheck();                                                 // check slippage

   cFract.Process();                                                    // start the fractal process

   if(cFract.BuySignal() &&
      RMA.SpreadMonitor(inp_sl_in_int))                                 // if there is a buy signal
     {
      if(cTrade.Buy(0.1))                                               // if Buy executed,
        {
         cFract.BuyDone();                                              // the signal has been processed
         Print("Buy has been done");                                    // notify
        }
      else                                                              // if Buy not executed,
        {
         Print("Error: buy");                                           // notify
        }
     }

   if(cFract.SellSignal())                                              // if there is a sell signal
     {
      if(cTrade.Sell(0.1))                                              // if sell executed,
        {
         cFract.SellDone();                                             // the signal has been processed
         Print("Sell has been done");                                   // notify
        }
      else                                                              // if sell failed,
        {
         Print("Error: sell");                                          // notify
        }
     }

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

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

   Comment(RMA.Message());                                              // display the data state in a comment
  }
//+------------------------------------------------------------------+

これでプロジェクトを構築し、過去のデータでテストすることができます。テスト例として、USDJPYペアを取り上げ、以下の入力で2023年にテストしてみましょう(表2参照)。

# 設定
 1  EA  RiskManagerAlgo.ex5
 2  銘柄  USDJPY
 3  チャートの時間枠  M15
 4  時間  2023.01.01 - 2023.12.31
 5  フォワードテスト  いいえ
 6  遅延  遅延なし、完璧なパフォーマンス
 7  シミュレーション  全ティック
 8  初期投資額  USD 10,000
 9  レバレッジ  1:100
 10  最適化  完全アルゴリズム(遅い) 

表1.RiskManagerAlgo EAのストラテジーテスター設定

ストラテジーテスターにおける最適化のために、最小ステップの原則に基づいてパラメータを設定し、訓練時間を短縮すると同時に、前回の記事で議論した依存性、つまりリスクマネージャーによって、利益を上げている戦略の取引結果さえも改善できるかどうかを追跡できるようにしました。最適化のための入力パラメータを表2に示します。

#
パラメータ名 開始値 ステップ 停止
 1  inp_riskperday  0.1  0.5  1
 2  inp_riskperweek  0.5  0.5  3
 3  inp_riskpermonth  2  1  8
 4  inp_plandayprofit  0.1  0.5  3
 5  dayProfitControl  false  -  true

表2:RiskManagerAlgo EAの戦略オプティマイザーのパラメータ

最適化パラメータには、戦略の有効性に直接依存せず、テスターでのモデリングに影響を与えないパラメータは含まれません。例えば、inp_slippfitsは主にブローカーの注文執行の質に依存し、エントリには依存しません。inp_spreadfitsパラメータはスプレッドサイズに直接依存し、スプレッドサイズはブローカー口座の種類、重要なニュースリリースのタイミングなどを含むがこれに限定されない多くの要因によって変化します。各自が取引する証券会社に応じて、これらのパラメータを独自に最適化することができます。

最適化の結果を図2と図3に示します。

図2.RiskManagerAlgo EAの最適化結果

図2:RiskManagerAlgo EAの最適化結果

最適化結果のグラフを見ると、ほとんどの結果が数学的期待値の正のゾーンにあることがわかります。これは、ストラテジーのロジックと密接に関連しており、多くの市場参加者が強いフラクタルブレークにポジションを集中させることで、価格がこれらのブレークを試す際に市場の動きが活発化し、結果的にモメンタムが形成されるためです。

フラクタルレベルの取引におけるモメンタムの存在を確認するために、上記のパラメータで得られたセットの中から、最も良かった反復と最も悪かった反復を比較することができます。私たちのリスクマネジャーは、ポジションに伴うリスクを、市場のモメンタムに応じて標準化する役割を担っています。市場モメンタムに対するリスクの標準化におけるリスクマネジャーの役割をさらに理解するためには、図3で示した「日次リスク」と「日次利益計画」のパラメータの関係を確認することが有効です。

図3:日次リスクと日次利益計画の対比図

図3:日次リスク対日次利益計画図

図3は、日次リスクパラメータのブレークポイントを示しており、この値が増加するにつれて、フラクタルブレークアウト戦略の有効性が一旦高まった後、やがて減少に転じることがわかります。これが、リスクと利益の関係におけるモデルの極点(関数の切れ目)を示しています。ある時点で、1日当たりのリスクパラメータの値が増加すると、期待される利益が増加するのではなく、逆に減少し始めます。この現象は、モデルに組み込まれたリスクに対して、市場のインパルスが小さくなることを示しており、リスクが過大であることを証明しています。この区切りは、グラフ上で明確に確認できるため、関数の導関数などの追加の数学的計算は必要ありません。

次に、市場にモメンタムが存在するかどうかを確認するため、リスクとリターンの比率を評価し、EAの最適化結果の最良と最悪の反復に基づくパラメータを個別に検討します。明らかに、エントリ時の予想利益が計画リスクの数倍であれば、その時点でモメンタムが発生します。これは、一方向に強い、反発のない動きです。一方、リスクのレベルがリターンと同等かそれ以上の場合、モメンタムは存在しません。

その日のリスクパラメータの区切りが、表3に示したパラメータに基づく最適化結果におけるベストパスの最適点であることが明らかであり、これはオプティマイザーが示してくれた結果です。

#
パラメータ名 パラメータ値
 1  inp_riskperday  0.6
 2  inp_riskperweek  3
 3  inp_riskpermonth  8
 4  inp_plandayprofit  3.1
 5  dayProfitControl  true
 6  inp_slippfits  2
 7  inp_spreadfits  2
 8  inp_risk_per_deal  100
 9  inp_sl_in_int  2000

表3:RiskManagerAlgo EAの戦略オプティマイザーのベストパスのパラメータ

計画利益が3.1であり、それを達成するために必要なリスクコストが0.6であることから、利益はリスクの5倍に相当します。つまり、預金の0.6%をリスクにさらすことで、3.1%の利益を得ることができるということです。この結果は、フラクタルブレイクの日足レベルにおいて価格のモメンタムが存在することを明確に示しており、数学的にポジティブな期待値を持つことを証明しています。

最良の反復結果を図4に示します。

図4.戦略オプティマイザの最適反復のグラフ

図4:戦略オプティマイザの最良の反復結果のグラフ

預金増加のグラフを見ると、リスク管理を用いた戦略が非常に安定した推移を示しており、各新しいロールバック(資産の減少)が前回のロールバックの最小値を下回ることはありません。これは、リスクの標準化とリスクマネージャーを活用して、投資の安全性を確保した結果です。次に、市場モメンタムの存在と、それに伴うリスク標準化の必要性を最終的に確認するため、最悪のオプティマイザー実行結果を見てみましょう。

表4のデータを用いて、以下のパラメータに基づく最悪の場合のリスクリターンを推定することができます。

#
パラメータ名
パラメータ値
 1  inp_riskperday  1.1
 2  inp_riskperweek  0.5
 3  inp_riskpermonth  2
 4  inp_plandayprofit  0.1
 5  dayProfitControl  true
 6  inp_slippfits  2
 7  inp_spreadfits  2
 8  inp_risk_per_deal  100
 9  inp_sl_in_int  2000

表4:RiskManagerAlgo EAの戦略オプティマイザーのワーストパスのパラメータ

表4のデータから、モメンタムに対するリスクを標準化しない場合、最悪の反復結果が図3のグラフ上の不利なゾーンに位置することがわかります。つまり、リスクを最大限に取っても必要なリターンが得られない領域にあるということです。この状況では、多額の保証金を費やしてエントリをおこなっても、そのリスクを十分に活かすことができず、期待される利益が得られません。

最悪の反復結果を図5に示します。

図5.戦略オプティマイザの最悪反復のグラフ

図5:戦略オプティマイザの最悪の反復結果のグラフ

図5によれば、リスクとリターンのバランスが悪い場合、口座の残高と資金に大きなドローダウンが発生することが明らかです。これまでに示した最適化の結果から、リスクを適切に管理するためには、リスクマネージャーの活用が不可欠であると結論づけられます。自身の取引戦略に基づき、論理的に正当化されたリスクを選択することが極めて重要です。それでは、この記事の総括に移りましょう。


結論

本稿で紹介したデータ、モデル、議論、計算に基づき、以下の結論に至ります。儲かる投資戦略やアルゴリズムを見つけるだけでは不十分です。仮に利益の見込める戦略であっても、資本に対する不適切なリスク設定によって損失を被る可能性があります。金融市場での効率的かつ安全な運営の鍵は、リスク管理の遵守にあります。長期的に安定した運用を実現するためには、使用する戦略の特性に応じてリスクを標準化することが前提条件となります。また、リスクマネージャーを無効にし、各ポジションにストップロスを設定せずに実際の口座で取引することは、絶対に避けるべきです。 

もちろん、すべてのリスクを完全に管理し、最小化できるわけではありません。しかし、期待されるリターンと比較しながら常にリスクを評価する必要があります。リスクマネージャーを活用すれば、標準化されたリスクは常に管理可能です。本稿を含むこれまでの記事でも繰り返し強調してきましたが、マネーマネジメントとリスクマネジメントの原則を遵守することを強く推奨します。

リスク管理が欠如した非システム的な取引では、どれほど優れた戦略でも失敗に終わる可能性があります。一方で、適切なリスク管理をおこなえば、負けていた戦略が勝つ戦略に変わることさえあります。本稿が少なくとも一人の方の資産を守る手助けになったのであれば、この仕事は無駄ではなかったと考えます。

この記事へのコメントやご意見をお待ちしています。皆様の取引が成功することを願っています。


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

添付されたファイル |
IShortStopLoss.mqh (1.21 KB)
RiskManagerAlgo.mq5 (10.53 KB)
RiskManagerAlgo.mqh (16.59 KB)
RiskManagerBase.mqh (61.49 KB)
TradeModel.mqh (12.99 KB)
CFractalsSignal.mqh (13.93 KB)
あらゆるタイプのトレーリングストップを開発してEAに接続する方法 あらゆるタイプのトレーリングストップを開発してEAに接続する方法
この記事では、様々なトレーリングストップを簡単に作成するためのクラスと、トレーリングストップを任意のEAに接続する方法について説明します。
彗尾アルゴリズム(CTA) 彗尾アルゴリズム(CTA)
この記事では、ユニークな宇宙物体である彗星と、太陽に接近する際に形成されるその印象的な尾にインスパイアされた「彗尾最適化アルゴリズム(CTA: Comet Tail Algorithm)」について考察します。このアルゴリズムは、彗星とその尾の運動の概念に基づき、最適化問題の最適解を見つけることを目的としています。
多通貨エキスパートアドバイザーの開発(第11回):最適化の自動化(最初のステップ) 多通貨エキスパートアドバイザーの開発(第11回):最適化の自動化(最初のステップ)
良いEAを得るためには、取引戦略の複数のインスタンスから優れたパラメータセットを選択する必要があります。これを実現するためには、さまざまな銘柄で最適化を行い、最良の結果を選ぶという手動のプロセスがあります。しかし、この作業をプログラムに任せ、より生産的な活動に専念したほうが効率的です。
亀甲進化アルゴリズム(TSEA) 亀甲進化アルゴリズム(TSEA)
これは、亀の甲羅の進化にインスパイアされたユニークな最適化アルゴリズムです。TSEAアルゴリズムは、問題に対する最適解を表す構造化された皮膚領域が徐々に形成される様子をエミュレートします。最良の解は「硬く」なり、外側に近い位置に配置され、成功しなかった解は「柔らかい」ままで内側に留まります。このアルゴリズムは、質と距離に基づく解のクラスタリングを利用し、成功率の低い選択肢を保持しながら、柔軟性と適応性を提供します。