English Русский 中文 Español Português
preview
多通貨エキスパートアドバイザーの開発(第24回):新しい戦略の追加(I)

多通貨エキスパートアドバイザーの開発(第24回):新しい戦略の追加(I)

MetaTrader 5テスター |
15 0
Yuriy Bykov
Yuriy Bykov

はじめに

前回の記事では、MetaTrader 5における取引戦略の自動最適化システムの開発を続けました。このシステムの中核は、最適化プロジェクトに関する情報を格納する最適化データベースです。プロジェクトを作成するためのスクリプトも作成しました。このスクリプトは特定の取引戦略(SimpleVolumes)の最適化用に書かれたものですが、他の取引戦略にも適応できるテンプレートとして使用できます。

プロジェクトの最終段階では、選択された取引戦略のグループを自動でエクスポートする機能も作成しました。エクスポートは「EAデータベース」と呼ばれる別のデータベースに対しておこなわれました。このデータベースは、最終EAによって取引システムの設定を再コンパイルせずに更新するために使用できます。これにより、新しいプロジェクトの最適化結果が複数回現れる時間帯にわたって、テスターでEAの動作をシミュレーションできるようになります。

さらに、プロジェクトファイルの構造を意味のある形に整理しました。すべてのファイルを2つの部分に分けています。1番目の部分は「Advisorライブラリ」と呼ばれ、MQL5/Includeフォルダに移動しました。残りはMQL5/Experts内の作業フォルダに残しています。ライブラリ部分には、自動最適化システムをサポートするファイルであり、最適化される取引戦略の種類に依存しないものをすべて移動しました。プロジェクト作業フォルダには、ステージEA、最終EA、そして最適化プロジェクトを作成するスクリプトが含まれています。 

しかし、SimpleVolumesのモデル取引戦略はライブラリ部分に残しました。当時は、最終EAで戦略パラメータを自動更新するメカニズムをテストすることがより重要だったためです。コンパイル時に取引戦略のソースコードファイルが正確にどこに接続されるかはそれほど問題ではありませんでした。

ここでは、新しい取引戦略を自動最適化システムに接続し、ステージEAと最終EAを作成したいとします。その場合、何が必要でしょうか。


道筋の整理

まず、簡単な戦略をいくつか取り、それをAdvisorライブラリで使えるようにコードに実装してみましょう。そのコードはプロジェクトの作業フォルダに置きます。戦略が作成されたら、第1ステージのエキスパートアドバイザー(EA)を作成できます。このEAは、この取引戦略の単一インスタンスのパラメータを最適化するために使用されます。ここで、ライブラリコードとプロジェクトコードを分離する必要があることに関連するいくつかの難しさに直面します。

前回作成した第2ステージおよび第3ステージのEAは、ほとんど同じコードを再利用できます。なぜなら、それらのライブラリ部分のコードには、使用する取引戦略のクラスに関する記述が含まれていないからです。ただし、新しい戦略ファイルをプロジェクト作業フォルダ内のコードに含めるためのコマンドを追加する必要があります。

新しい戦略の場合、最適化データベース内のプロジェクト作成EAスクリプトにいくつかの変更を加える必要があります。少なくとも、最初のステージEA用の入力パラメータのテンプレートに影響します。というのも、新しい取引戦略の入力パラメータの構成は、以前の戦略とは異なるからです。

最適化データベース内のプロジェクト作成EAを修正した後、それを実行できるようになります。最適化データベースが作成され、このプロジェクトに必要な最適化タスクが追加されます。次に、自動最適化コンベアを実行し、その作業が終了するのを待ちます。このプロセスはかなり時間がかかります。所要時間は、選択した最適化の時間間隔(長ければ長いほど時間がかかる)、取引戦略自体の複雑さ(複雑であればあるほど時間がかかる)、そしてもちろん、最適化に使用できるテストエージェントの数(多ければ多いほど早く終わる)によって決まります。

最後のステップは、最終EAを実行するか、ストラテジーテスターでテストして最適化結果を評価することです。

それでは始めましょう。


SimpleCandles戦略

まず、MQL5/Expertsフォルダ内にプロジェクト用の新しいフォルダを作成します。たとえばArticle.17277としましょう。今後の混乱を避けるために、最初にお断りしておくのがよいかもしれません。ここでは「プロジェクト」という用語を2つの意味で使用します。1つ目の意味では、単に特定の取引戦略を自動最適化するために使用されるEAのファイルが入ったフォルダを指します。これらのEAのコードは、Advisorライブラリのincludeファイルを使用します。この文脈では、プロジェクトは端末のExpertsフォルダ内の作業フォルダに過ぎません。2つ目の意味では、「プロジェクト」とは、最適化データベース内に作成されるデータ構造を指し、最終EAが取引口座上で動作するために使用される結果を得るために自動で実行される最適化タスクを記述します。この場合、プロジェクトは最適化自体が始まる前の最適化データベースの中身と言えます。

ここではまず、1つ目の意味でのプロジェクトについて話しています。プロジェクト作業フォルダ内にStrategiesというサブフォルダを作成します。ここに様々な取引戦略のファイルを配置します。今回は、新しい戦略を1つ作るだけです。

まず、第1回SimpleVolumes取引戦略を開発したときの手順を繰り返しましょう。まずは取引アイデアの策定から始めます。  

ある銘柄で、いくつか連続したローソク足が同じ方向に進む場合、次のローソク足が逆方向に動く確率がわずかに高くなると仮定します。この場合、その後に逆方向のポジションを取れば、利益を得られるかもしれません。

このアイデアを戦略に落とし込むためには、未知のパラメータを含まない形でポジションを建てたり決済したりするルールを定める必要があります。このルールセットにより、戦略が稼働している任意の時点で、ポジションを建てるべきか、建てる場合はどれを建てるかを判断できるようにします。

まず、ローソク足の方向の概念を明確にします。ローソク足の終値が始値より高ければ、そのローソク足を上向きと呼びます。終値が始値より低ければ、下向きと呼びます。連続した過去のローソク足の方向を評価したいため、このローソク足の方向の概念はすでに確定したローソク足にのみ適用します。したがって、ポジションを建てる可能性のあるタイミングは、新しいバー、すなわち新しいローソク足が出現した瞬間になります。

さて、ポジションを建てるタイミングは決まりましたが、ポジションを決済するタイミングはどうでしょうか。これには最も単純な方法を採用します。ポジションを建てる際に、StopLossとTakeProfitのレベルを設定し、これらのレベルに到達した場合にのみポジションを決済します。

この戦略の説明をまとめると次の通りです。

ポジションを建てるシグナルは、新しいバーの開始時点で、直前の複数本のローソク足がすべて同じ方向を向いている場合です。ローソク足が上向きであれば売りポジションを建て、下向きであれば買いポジションを建てます。 

各ポジションにはStopLossとTakeProfitが設定されており、これらのレベルに到達したときのみ決済されます。すでにポジションが建てられている場合でも、新たなシグナルが発生した場合は、追加でポジションを建てることができます。ただし、その数が上限を超えないよう制限します。

ここまでが詳細な説明ですが、まだ完全ではありません。そのため、もう一度読み直し、明確でない箇所をすべて強調します。それらについては、より詳細な説明が必要です。 

以下がその点です。

  • 直前の複数本のローソク足」とは何本か
  • 追加でポジションを建てることができる」:同時に何ポジションまで建てられるか
  • StopLossとTakeProfitが設定されている」:これらの値をどのように使い、計算するのか

直前の複数本のローソク足とは何本か: これは最も簡単な質問です。この数量は単純に戦略パラメータのひとつとして扱い、最適値を探すために変更可能とします。整数で指定し、大きくはせず、チャートを見る限り、同方向のローソク足が長く続くことは稀なので、最大でも10本程度にします。

同時に建てるポジションは何ポジションまでか: これも戦略パラメータとして設定可能で、最適化の過程で最適な値を選択します。 

StopLossとTakeProfitの値はどのように使用するか、どのように計算するか: これは少し複雑な質問ですが、最も単純な場合は前述の考え方と同じ方法で答えられます。StopLossとTakeProfitをポイント単位で戦略パラメータとして定義します。ポジションを建てる際には、建値からこのパラメータで指定されたポイント数だけ、希望する方向に価格を動かして設定します。しかし、もう少し複雑な方法も可能です。これらのパラメータをポイントではなく、取引対象のシンボル価格のボラティリティの平均値に対する割合として設定することもできます。この場合、次の疑問が生じます。

そのボラティリティの値はどのように求めるか:これにはいくつか方法があります。たとえば、既存のATR (Average True Range)ボラティリティ指標を使用することもできますし、自分でボラティリティ計算方法を考案して実装することも可能です。おそらく、この計算で使用するパラメータとして、価格変動の範囲を評価する期間数や1期間の長さが含まれるでしょう。これらを戦略パラメータに追加すれば、ボラティリティの計算に使用できます。 

初期ポジションを建てた後、後続のポジションも同じ方向に建てる必要があるという制約を課さないため、戦略によっては異なる方向のポジションが同時に存在する場合があります。通常の実装では、このような戦略は独立したポジション管理が可能な口座(いわゆるヘッジ口座)のみに適用する必要があります。しかし、仮想ポジションを使用すれば、この制限はありません。

ここまでで主要な点は明確になったので、これまでに挙げた戦略パラメータをすべてまとめておきます。ポジションを建てるためのシグナルを受けるためには、どの銘柄とどの時間軸の時間足を追跡するかを選択する必要があります。これを踏まえると、戦略の説明は次のようになります。

EAは特定の銘柄および時間軸で起動します。

入力パラメータを設定します。

  • Symbol
  • 同方向のローソク足をカウントする時間軸(Timeframe)
  • 同方向のローソク足の本数(signalSeqLen)
  • ATR期間(periodATR)
  • ストップロス(ポイント単位またはATR%)(stopLevel)
  • テイクプロフィット(ポイント単位またはATR%)(takeLevel)
  • 同時に建てられる最大ポジション数(maxCountOfOrders)
  • ポジションサイジング

新しいローソク足が出現したとき、直近で確定したsignalSeqLen本のローソク足の方向を確認します。

方向がすべて同じで、かつ建てられているポジション数がmaxCountOfOrders未満であれば、

  • StopLossとTakeProfitを計算します。periodATR = 0の場合は、現在価格にstopLevelとtakeLevelパラメータで指定されたポイント数を単純に加減します。periodATR > 0の場合は、日足におけるATR値をperiodATRパラメータを使って計算します。そして、現在価格からATR × stopLevelおよびATR × takeLevelの値だけ離してStopLossとTakeProfitを設定します。

  • ローソク足の方向が上向きであれば売りポジションを建て、下向きであれば買いポジションを建てます。ポジションを建てる際には、あらかじめ計算したStopLossおよびTakeProfitのレベルを設定します。

この説明だけでも、実装を開始するには十分です。実装の過程で発生する問題は、その都度解決していきます。

また、戦略を説明する際に、建てられるポジションのサイズには触れていない点に注意してください。形式的にはパラメータとしてリストに追加しましたが、開発した戦略を自動最適化システムで使用する場合、テスト時には単純に最小ロットを使用できます。自動最適化の過程で、テスト期間全体で指定されたドローダウン10%を達成できる適切なポジションサイズの倍率が選択されます。そのため、ポジションサイズを手動で設定する必要はどこにもありません。


戦略の実装

既存のCSimpleVolumesStrategyクラスを利用し、そこからCSimpleCandlesStrategyクラスを作成します。このクラスはCVirtualStrategyクラスの子クラスとして宣言する必要があります。必要な戦略パラメータをクラスのフィールドとして列挙し、新しいクラスが祖先クラスから追加のフィールドやメソッドを継承していることを念頭に置きます。

//+------------------------------------------------------------------+
//| Trading strategy using unidirectional candlesticks               |
//+------------------------------------------------------------------+
class CSimpleCandlesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;            // Symbol (trading instrument)
   ENUM_TIMEFRAMES   m_timeframe;         // Chart period (timeframe)

   //---  Open signal parameters
   int               m_signalSeqLen;      // Number of unidirectional candles
   int               m_periodATR;         // ATR period

   //---  Position parameters
   double            m_stopLevel;         // Stop Loss (in points or % ATR)
   double            m_takeLevel;         // Take Profit (in points or % ATR)

   //---  Money management parameters
   int               m_maxCountOfOrders;  // Max number of simultaneously open positions

   CSymbolInfo       *m_symbolInfo;       // Object for getting information about the symbol properties

  // ...   

public:
   // Constructor
                     CSimpleCandlesStrategy(string p_params);
   
   virtual string    operator~() override;   // Convert object to string
   virtual void      Tick() override;        // OnTick event handler
};

取引対象(銘柄)のプロパティ情報を一元的に取得するために、クラスフィールドにCSymbolInfoクラスのオブジェクトへのポインタを含めます。 

新しい取引戦略のクラスはCFactorableクラスの子クラスです。これにより、コンストラクタ内でCFactorableクラスに実装された読み取りメソッドを使用して、初期化文字列からパラメータの値を読み込むことができます。読み取り中にエラーがなければ、IsValid()メソッドはtrueを返します。

仮想ポジションで作業するため、CVirtualStrategyの祖先クラスにはm_orders配列が宣言されており、これはCVirtualOrderクラスのオブジェクト、つまり仮想ポジションを格納するために使用されます。したがって、コンストラクタ内で、m_maxCountOfOrdersパラメータで指定された数の仮想ポジションオブジェクトを作成し、m_orders配列に配置するように指示します。この処理はCVirtualReceiver::Get()静的メソッドによっておこなわれます。

戦略では、新しいバーが指定された時間軸で発生した場合にのみポジションを建てるため、指定された銘柄と時間軸の新しいバー発生イベントを確認するオブジェクトを作成します。

コンストラクタで最後におこなう作業は、銘柄監視オブジェクトにCSymbolInfoクラス用の情報オブジェクトを作成させることです。

完全なコンストラクタコードは次のようになります。

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSimpleCandlesStrategy::CSimpleCandlesStrategy(string p_params) {
   // Read parameters from the initialization string
   m_params = p_params;
   m_symbol = ReadString(p_params);
   m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params);
   m_signalSeqLen = (int) ReadLong(p_params);
   m_periodATR = (int) ReadLong(p_params);
   m_stopLevel = ReadDouble(p_params);
   m_takeLevel = ReadDouble(p_params);
   m_maxCountOfOrders = (int) ReadLong(p_params);

   if(IsValid()) {
      // Request the required number of objects for virtual positions
      CVirtualReceiver::Get(&this, m_orders, m_maxCountOfOrders);

      // Add tracking a new bar on the required timeframe
      IsNewBar(m_symbol, m_timeframe);
      
      // Create an information object for the desired symbol
      m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol];
   }
}

次に、抽象的な仮想ティルダ演算子(~)を実装する必要があります。この演算子は戦略オブジェクトの初期化文字列を返します。実装は標準的です。

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CSimpleCandlesStrategy::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}

もう1つ必要な仮想メソッドはTick()です。このメソッドでは、新しいバーが発生したかどうかと、建てられているポジションの数が最大値に達していないかを確認します。条件を満たす場合、ポジションを建てるシグナルの有無をチェックします。シグナルがあれば、対応する方向にポジションを建てます。その他のメソッドは、クラス内で補助的な役割を果たします。 

//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void CSimpleCandlesStrategy::Tick() override {
// If a new bar has arrived for a given symbol and timeframe
   if(IsNewBar(m_symbol, m_timeframe)) {
// If the number of open positions is less than the allowed number
      if(m_ordersTotal < m_maxCountOfOrders) {
         // Get an open signal
         int signal = SignalForOpen();

         if(signal == 1) {          // If there is a buy signal, then 
            OpenBuy();              // open a BUY position
         } else if(signal == -1) {  // If there is a sell signal, then
            OpenSell();             // open a SELL_STOP position
         }
      }
   }
}

ポジションを建てるシグナルの有無を確認する処理を、別のSignalForOpen()メソッドに移動しました。このメソッドでは、過去のローソク足から取得したクォートの配列を受け取り、それらがすべて下向きか、または上向きかを順番にチェックします。

//+------------------------------------------------------------------+
//| Signal for opening pending orders                                |
//+------------------------------------------------------------------+
int CSimpleCandlesStrategy::SignalForOpen() {
// By default, there is no signal
   int signal = 0;

   MqlRates rates[];
// Copy the quote values (candles) to the destination array
   int res = CopyRates(m_symbol, m_timeframe, 1, m_signalSeqLen, rates);

// If the required number of candles has been copied
   if(res == m_signalSeqLen) {
      signal = 1; // buy signal

      // Loop through all the candles
      for(int i = 0; i < m_signalSeqLen; i++) {
         // If at least one upward candle occurs, cancel the signal
         if(rates[i].open < rates[i].close ) {
            signal = 0;
            break;
         }
      }

      if(signal == 0) {
         signal = -1; // otherwise, sell signal

         // Loop through all the candles
         for(int i = 0; i < m_signalSeqLen; i++) {
            // If at least one downward candle occurs, cancel the signal
            if(rates[i].open > rates[i].close ) {
               signal = 0;
               break;
            }
         }
      }

   }

   return signal;
}

新しく作成したOpenBuy()およびOpenSell()メソッドは、ポジションを建てる処理を担当します。これらは非常によく似ているため、ここではそのうち一方のコードのみを示します。このメソッドにおける重要なポイントは、StopLossとTakeProfitレベルを更新するメソッドを呼び出すことです。この処理により、対応するクラスフィールドであるm_slm_tpの値が更新されます。また、m_orders配列から、まだ建てられていない最初の仮想ポジションを建てるメソッドを呼び出す点も重要です。

//+------------------------------------------------------------------+
//| Open BUY order                                                   |
//+------------------------------------------------------------------+
void CSimpleCandlesStrategy::OpenBuy() {
// Retrieve the necessary symbol and price data
   double point = m_symbolInfo.Point();
   int digits = m_symbolInfo.Digits();

// Opening price
   double price = m_symbolInfo.Ask();

// Update SL and TP levels by calculating ATR
   UpdateLevels();

// StopLoss and TakeProfit levels
   double sl = NormalizeDouble(price - m_sl * point, digits);
   double tp = NormalizeDouble(price + m_tp * point, digits);

   bool res = false;
   for(int i = 0; i < m_maxCountOfOrders; i++) {   // Iterate through all virtual positions
      if(!m_orders[i].IsOpen()) {                  // If we find one that is not open, then open it
         // Open a virtual SELL position
         res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                                0,
                                NormalizeDouble(sl, digits),
                                NormalizeDouble(tp, digits));

         break; // and exit
      }
   }

   if(!res) {
      PrintFormat(__FUNCTION__" | ERROR opening BUY virtual order", 0);
   }
}

レベル更新メソッドでは、まずATR計算期間に0以外の値が設定されているかを確認します。設定されている場合は、ATR計算関数が呼び出され、その結果がchannelWidth変数に代入されます。期間の値が0の場合は、この変数に1が代入されます。この場合、m_stopLevelおよびm_takeLevel入力の値はポイント値として解釈され、変更されることなくm_slm_tpに反映されます。一方、それ以外の場合は、これらの値はATRの値に対する比率として解釈され、計算されたATR値を掛け合わせた結果が使用されます。

//+------------------------------------------------------------------+
//| Update SL and TP levels based on calculated ATR                  |
//+------------------------------------------------------------------+
void CSimpleCandlesStrategy::UpdateLevels() {
// Calculate ATR
   double channelWidth = (m_periodATR > 0 ? ChannelWidth() : 1);

// Update SL and TP levels
   m_sl = m_stopLevel * channelWidth;
   m_tp = m_takeLevel * channelWidth;
}

新しい取引戦略で最後に必要となるメソッドは、ATR計算メソッドです。すでに述べたように、このメソッドはさまざまな方法で実装可能で、既存のライブラリや関数を利用することもできます。ここでは簡単のため、手元にある実装例の中から1つの方法を使用します。

//+------------------------------------------------------------------+
//| Calculate the ATR value (non-standard implementation)            |
//+------------------------------------------------------------------+
double CSimpleCandlesStrategy::ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1) {
   int n = m_periodATR; // Number of bars for calculation
   MqlRates rates[];    // Array for quotes

   // Copy quotes from the daily (default) timeframe
   int res = CopyRates(m_symbol, p_tf, 1, n, rates);

   // If the required amount has been copied
   if(res == n) {
      double tr[];         // Array for price ranges
      ArrayResize(tr, n);  // Change its size
   
      double s = 0;        // Sum for calculating the average
      FOREACH(rates, {
         tr[i] = rates[i].high - rates[i].low; // Remember the bar size
      });
      
      ArraySort(tr); // Sort the sizes

      // Sum the inner two quarters of the bar sizes
      for(int i = n / 4; i < n * 3 / 4; i++) {
         s += tr[i];
      }
      
      // Return the average size in points
      return 2 * s / n / m_symbolInfo.Point();
   }

   return 0.0;
}

プロジェクト作業フォルダ内のStrategies/SimpleCandlesStrategy.mqhファイルに加えた変更を保存します。


戦略の接続

戦略全体の準備が整ったので、次はEAファイルに接続する必要があります。まずは第1ステージのEAから始めましょう。現在、このEAのコードは2つのファイルに分かれています。

  • MQL5/Experts/Article.17277/Stage1.mq5:SimpleCandles戦略を検証するための現在のプロジェクトファイル
  • MQL5/Include/antekov/Advisor/Experts/Stage1.mqh:すべてのプロジェクトで共通に使うライブラリファイル

現在のプロジェクトファイルでは、次の操作をおこなう必要があります。

  1. __NAME__定数を定義する:他のプロジェクトの名前と重複しないユニークな値を代入する
  2. 開発した取引戦略クラスのファイルを添付する
  3. Advisorライブラリから第1ステージEAの共通部分を接続する
  4. 取引戦略の入力パラメータを列挙する
  5. 入力パラメータの値を戦略オブジェクトの初期化文字列に変換するGetStrategyParams()関数を作成する
コードでは次のようになります。

// 1. Define a constant with the EA name
#define  __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME)

// 2. Connect the required strategy
#include "Strategies/SimpleCandlesStrategy.mqh";

// 3. Connect the general part of the first stage EA from the Advisor library
#include <antekov/Advisor/Experts/Stage1.mqh>

//+------------------------------------------------------------------+
//| 4. Strategy inputs                                               |
//+------------------------------------------------------------------+
sinput string     symbol_              = "GBPUSD";
sinput ENUM_TIMEFRAMES period_         = PERIOD_H1;

input group "===  Opening signal parameters"
input int         signalSeqLen_        = 5;     // Number of unidirectional candles
input int         periodATR_           = 30;    // ATR period

input group "===  Pending order parameters"
input double      stopLevel_           = 3750;  // Stop Loss (in points)
input double      takeLevel_           = 50;    // Take Profit (in points)

input group "===  Money management parameters"
input int         maxCountOfOrders_    = 3;     // Maximum number of simultaneously open orders


//+------------------------------------------------------------------+
//| 5. Strategy initialization string generation function            |
//|    from the inputs                                               |
//+------------------------------------------------------------------+
string GetStrategyParams() {
   return StringFormat(
             "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d)",
             symbol_, period_,
             signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_
          );
}
//+------------------------------------------------------------------+

しかし、第1ステージEAファイルをコンパイルすると(コンパイル自体はエラーなく完了する)、実行時にOnInit()関数で次のようなエラーが発生し、EAが停止してしまいます。

2018.01.01 00:00:00   CVirtualFactory::Create | ERROR: Constructor not found for:
2018.01.01 00:00:00   class CSimpleCandlesStrategy("GBPUSD",16385,5,30,2.95,3.92,3)

その理由は、すべてのCFactorable派生クラスのオブジェクトを作成する際に、Virtual/VirtualFactory.mqhファイルにあるCVirtualFactory::Create()関数を使用しているためです。この関数は、Base/Factorable.mqh内で宣言されているNEW(C)およびCREATE(C, O, P)マクロから呼び出されます。

この関数は、初期化文字列からオブジェクトのクラス名をclassName変数に読み取ります。読み取った部分は初期化文字列から削除されます。その後、すべての可能なクラス名(CFactorableの派生クラス)を単純に順番に確認し、先ほど読み取った名前と一致するものが見つかるまで繰り返します。一致した場合、目的のクラスの新しいオブジェクトが作成され、そのポインタがオブジェクト変数を通じて作成関数の結果として返されます。

// Create an object from the initialization string
   static CFactorable* Create(string p_params) {
      // Read the object class name
      string className = CFactorable::ReadClassName(p_params);
      
      // Pointer to the object being created
      CFactorable* object = NULL;

      // Call the corresponding constructor  depending on the class name
      if(className == "CVirtualAdvisor") {
         object = new CVirtualAdvisor(p_params);
      } else if(className == "CVirtualRiskManager") {
         object = new CVirtualRiskManager(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      } else if(className == "CHistoryStrategy") {
         object = new CHistoryStrategy(p_params);
      } 
            
      // If the object is not created or is created in the invalid state, report an error
      if(!object) {
         ...
      }

      return object;
   }

すべてのコードが1つのフォルダにあったときは、使用する新しいCFactorable子クラスのために、ここに追加の条件分岐を単純に加えていました。たとえば、最初のSimpleVolumesモデル戦略のオブジェクトを生成する部分は次のようにして作られました。

} else if(className == "CSimpleVolumesStrategy") {
   object = new CSimpleVolumesStrategy(p_params);
}

以前の方法に従えば、今回のSimpleCandlesモデル戦略についても同様のブロックをここに追加する必要があります。

} else if(className == "CSimpleCandlesStrategy") {
   object = new CSimpleCandlesStrategy(p_params);
}

しかし、この方法はすでにライブラリ部分とプロジェクト部分のコード分離の原則に違反しています。ライブラリ側のコードは、使用時にどの新しい戦略が作られるかを知る必要はありません。現在では、この方法でCSimpleVolumesStrategyを生成すること自体が不適切に見えます。

ここで、必要なオブジェクトの生成を保証しつつ、コードの分離を明確に保つ方法を考えてみましょう。


CFactorableの改善

実際のところ、この課題はそれほど簡単ではありませんでした。解決策を考えるのに頭を使い、最終的に現在使われている方法に落ち着くまでに、複数の実装方法を試すことになりました。MQL5言語にコンパイル済みプログラム内で文字列からコードを実行する機能があれば、すべて非常に簡単に解決できたでしょう。しかし、セキュリティ上の理由で、他のプログラミング言語にあるeval()関数のような機能は存在しません。そのため、手元にある手段で対処する必要がありました

一般的な考え方としては、各CFactorableの派生クラスは自身のオブジェクトを生成する静的関数を持つべきです。これは一種の静的コンストラクタと考えることができます。これにより、通常のコンストラクタを非公開にして、オブジェクトの生成は静的コンストラクタのみを通じておこなうようにできます。次に、初期化文字列から得られたクラス名に基づき、どのコンストラクタ関数を呼ぶか分かるように、クラス名と関数を関連付ける必要があります。

この問題を解決するには関数ポインタが必要です。関数ポインタは、変数に関数のコードへのポインタを保持し、そのポインタを使って関数を呼び出す特殊な変数です。ご覧の通り、異なるCFactorable派生クラスのオブジェクトの静的コンストラクタは次のシグネチャで宣言できます。

static CFactorable* Create(string p_params)

これにより、すべての派生クラスの静的コンストラクタへのポインタを格納する静的配列を作成できます。Advisorライブラリの一部を構成するクラス(CVirtualAdvisor、CVirtualStrategyGroup、CVirtualRiskManager)は、ライブラリ内のコードでこの配列に追加されます。一方、取引戦略クラスはプロジェクトの作業フォルダ内のコードからこの配列に追加されます。こうすることで、必要なコード分離が実現できます。

次の課題は、この静的配列をどのクラスに宣言するか、配列をどうやって補充するか、クラス名と配列要素の関連付けをどう保持するかです。

最初は、CFactorableクラスにこの静的配列を作るのが最も適切に思えました。関連付けのために、クラス名の静的配列をもうひとつ作ります。配列を補充する際に、1つの配列にクラス名、もう1つの配列にそのクラスの静的コンストラクタのポインタを追加することで、2つの配列の要素間にインデックス関係ができます。言い換えると、1つの配列で必要なクラス名の要素のインデックスを見つければ、もう1つの配列からコンストラクタ関数のポインタを取得し、初期化文字列を渡して呼び出すことができます。

では、この配列をどうやって埋めるかです。OnInit()から呼ばなければならない関数を作りたくありませんでした。この方法でも動作は可能ですが、最終的に別の方法にしました。

基本アイデアは、OnInit()からではなく、CFactorable派生クラスを記述したファイルから直接コードを呼び出したいというものでした。しかし、単にクラス定義外にコードを書いても実行されません。そこで、クラス定義外にグローバル変数としてオブジェクトを宣言すると、その場所でコンストラクタが呼ばれることを利用します。

この目的のために、CFactorableCreatorという別クラスを作ります。このクラスのオブジェクトは、クラス名とそのクラスの静的コンストラクタへのポインタを保持します。また、このクラスは同じクラスのオブジェクトへの静的配列も持ち、CFactorableCreatorのコンストラクタは作成されたすべてのオブジェクトを自動的にこの配列に追加します。

// Preliminary class definition
class CFactorable;

// Type declaration - pointer to the function for creating objects of the CFactorable class
typedef CFactorable* (*TCreateFunc)(string);

//+------------------------------------------------------------------+
//| Class of creators that bind names and static                     |
//| constructors of CFactorable descendant classes                   |
//+------------------------------------------------------------------+
class CFactorableCreator {
public:
   string            m_className;   // Class name
   TCreateFunc       m_creator;     // Static constructor for the class

   // Creator constructor
                     CFactorableCreator(string p_className, TCreateFunc p_creator);

   // Static array of all created creator objects
   static CFactorableCreator* creators[];
};

// Static array of all created creator objects
CFactorableCreator* CFactorableCreator::creators[];

//+------------------------------------------------------------------+
//| Creator constructor                                              |
//+------------------------------------------------------------------+
CFactorableCreator::CFactorableCreator(string p_className, TCreateFunc p_creator) :
   m_className(p_className),
   m_creator(p_creator) {
// Add the current creator object to the static array
   APPEND(creators, &this);
}
//+------------------------------------------------------------------+

では、CVirtualAdvisorクラスを例に、CFactorableCreator::creators配列の補充をどのように整理するかを見てみましょう。 まず、CVirtualAdvisorのコンストラクタをprotectedセクションに移動し、静的コンストラクタ関数Create()追加します。クラスを記述した後に、CVirtualAdvisorCreatorクラスのグローバルオブジェクトCFactorableCreator作成します。まさにここで、CFactorableCreatorのコンストラクタが呼ばれるときに、CFactorableCreator::creators配列が補充されます。

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {

protected:
   //...
                     CVirtualAdvisor(string p_param);    // Private constructor
public:
                     static CFactorable* Create(string p_params) { return new CVirtualAdvisor(p_params) };
                    //...
};

CFactorableCreator CVirtualAdvisorCreator("CVirtualAdvisor", CVirtualAdvisor::Create);

CFactorableの派生オブジェクトのすべてのクラスに対して、同じ3つの修正をおこなう必要があります。作業を少し簡単にするために、CFactorableクラスを含むファイルに2つの補助マクロを宣言します。

// Declare a static constructor inside the class
#define STATIC_CONSTRUCTOR(C) static CFactorable* Create(string p) { return new C(p); }

// Add a static constructor for the new CFactorable descendant class
// to a special array by creating a global object of the CFactorableCreator class 
#define REGISTER_FACTORABLE_CLASS(C) CFactorableCreator C##Creator(#C, C::Create);

これらは、すでにCVirtualAdvisorクラス用に作成したコードテンプレートを単純に繰り返すだけのものです。これで、次のように修正をおこなうことができます。

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   // ...
                     CVirtualAdvisor(string p_param);    // Constructor
public:
                     STATIC_CONSTRUCTOR(CVirtualAdvisor);
                    // ...
};

REGISTER_FACTORABLE_CLASS(CVirtualAdvisor);

Advisorライブラリ内の3つのクラスファイル(CVirtualAdvisor、CVirtualStrategyGroup、CVirtualRiskManager)にも同様の変更を加える必要がありますが、これは一度だけおこなえば十分です。これらの変更がライブラリに組み込まれたので、以後は忘れて構いません。 

一方、プロジェクト作業フォルダ内にある取引戦略クラスのファイルでは、新しいクラスごとにこうした追加をおこなうことが必須です。新しい戦略クラスにもこれらを追加すると、クラス定義コードは次のようになります。

//+------------------------------------------------------------------+
//| Trading strategy using unidirectional candlesticks               |
//+------------------------------------------------------------------+
class CSimpleCandlesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;            // Symbol (trading instrument)
   ENUM_TIMEFRAMES   m_timeframe;         // Chart period (timeframe)

   //---  Open signal parameters
   int               m_signalSeqLen;      // Number of unidirectional candles
   int               m_periodATR;         // ATR period

   //---  Position parameters
   double            m_stopLevel;         // Stop Loss (in points or % ATR)
   double            m_takeLevel;         // Take Profit (in points or % ATR)

   //---  Money management parameters
   int               m_maxCountOfOrders;  // Max number of simultaneously open positions

   CSymbolInfo       *m_symbolInfo;       // Object for getting information about the symbol properties

   double            m_tp;                // Stop Loss in points
   double            m_sl;                // Take Profit in points

   //--- Methods
   int               SignalForOpen();     // Signal to open a position
   void              OpenBuy();           // Open a BUY position
   void              OpenSell();          // Open a SELL position

   double            ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1); // Calculate the ATR value
   void              UpdateLevels();      // Update SL and TP levels

   // Private constructor
                     CSimpleCandlesStrategy(string p_params);

public:
   // Static constructor
                     STATIC_CONSTRUCTOR(CSimpleCandlesStrategy);

   virtual string    operator~() override;   // Convert object to string
   virtual void      Tick() override;        // OnTick event handler
};

// Register the CFactorable descendant class
REGISTER_FACTORABLE_CLASS(CSimpleCandlesStrategy);

改めて強調しておきますが、ハイライトされた部分は、どの新しい取引戦略クラスにも必ず含まれている必要があります。

あとは、オブジェクト生成用の配列に格納されたクリエイターを、CVirtualFactory::Create()の初期化文字列から呼び出す一般的なオブジェクト生成関数に適用するだけです。ここで、少し変更を加えます。実は、この関数を別のクラスに置く必要はもうありません。以前は、形式上CFactorableクラスがすべての派生クラスの名前を知る義務がないため、このようにしていました。変更後は、すべての派生クラスの名前を知らなくても、CFactorableCreator::creators配列の要素を通じて静的コンストラクタにアクセスすることで任意の派生クラスを生成できるようになっています。そこで、この関数のコードをCFactorable::Create()の新しい静的メソッドに移します。

//+------------------------------------------------------------------+
//| Base class of objects created from a string                      |
//+------------------------------------------------------------------+
class CFactorable {
 // ...

public:
   // ...

   // Create an object from the initialization string
   static CFactorable* Create(string p_params);
};


//+------------------------------------------------------------------+
//| Create an object from the initialization string                  |
//+------------------------------------------------------------------+
CFactorable* CFactorable::Create(string p_params) {
// Pointer to the object being created
   CFactorable* object = NULL;

// Read the object class name
   string className = CFactorable::ReadClassName(p_params);

// Find and call the corresponding constructor depending on the class name
   int i;
   SEARCH(CFactorableCreator::creators, className == CFactorableCreator::creators[i].m_className, i);
   if(i != -1) {
      object = CFactorableCreator::creators[i].m_creator(p_params);
   }

// If the object is not created or is created in the invalid state, report an error
   if(!object) {
      PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\n%s",
                  p_params);
   } else if(!object.IsValid()) {
      PrintFormat(__FUNCTION__
                  " | ERROR: Created object is invalid for:\n%s",
                  p_params);
      delete object; // Remove the invalid object
      object = NULL;
   }

   return object;
}

ご覧の通り、まず初期化文字列からクラス名を取得し、その後、必要なクラス名と一致する要素のインデックスをcreators配列内で探します。見つかったインデックスはi変数に格納されます。インデックスが見つかれば、対応する関数ポインタを通じて必要なクラスのオブジェクトの静的コンストラクタが呼び出されます。このコード内には、もはやCFactorable派生クラスの名前に対する直接の参照は存在しません。結果として、CVirtualFactoryクラスを含むファイルは不要になり、ライブラリから除外されます。


第1ステージEAの確認

第1ステージEAをコンパイルし、現時点では手動で最適化を実行してみましょう。たとえば、最適化期間を2018年から2023年まで、通貨ペアはGBPUSD、時間足はH4とします。最適化は正常に開始され、しばらくすると結果を確認できます。

図1:Stage1.mq5 EAの最適化設定と最適化結果の可視化

いくつか、比較的良好と思われる単発のパス結果を見てみましょう。

図2:次のパラメータを使用したパスの結果:class CSimpleCandlesStrategy("GBPUSD",16388,4,23,2.380,4.950,19)

図2の結果では、同じ方向に4本のローソク足が形成された後にエントリーがおこなわれ、StopLossとTakeProfitの比率はおおよそ1:2となっています。 

図3:次のパラメータを使用したパスの結果:class CSimpleCandlesStrategy("GBPUSD",16388,7,9,0.090,3.840,1)

図3では、同じ方向に7本のローソク足が形成された後にエントリーがおこなわれています。この場合、StopLossが非常に短く、TakeProfitは大きく設定されています。チャートを見ると、ほとんどの取引は小さな損失で決済され、6年間でわずか十数回の取引だけが利益で終了していることが分かります。ただし、その利益は大きなものです。

ですから、この取引戦略は非常にシンプルではありますが、複数のインスタンスを統合して最終EAに組み込むことで、より良い結果を狙うことも可能です。


結論

新しい戦略を自動最適化システムに接続するプロセスはまだ完了していませんが、今後の作業の道筋を確保する重要なステップを踏むことができました。まず、新しい取引戦略はCVirtualStrategyの派生クラスとして、独立したクラスとして実装済みです。次に、第1ステージEAに接続することができ、このEAの最適化プロセスを実行可能であることも確認しました。

第1ステージでは、取引戦略の単一インスタンスの最適化は、最適化データベースにまだどのパスの結果も存在しない状態で開始されます。第2ステージおよび第3ステージでは、すでに第1ステージのパスの最適化結果がデータベースに存在する必要があります。したがって、現時点では第2ステージおよび第3ステージのEAに戦略を接続してテストすることはできません。まず最初に、最適化データベース内にプロジェクトを作成し、第1ステージの結果を蓄積する必要があります。次回では、作業を継続し、プロジェクト作成EAの修正について考察します。

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


重要な注意事項

この記事および連載のこれまでのすべての記事で提示された結果は、過去のテストデータのみに基づいており、将来の利益を保証するものではありません。このプロジェクトでの作業は研究的な性質のものであり、公開された結果はすべて、自己責任で使用されるべきです。


アーカイブ内容

#
 名前
バージョン  詳細  最近の変更
  MQL5/Experts/Article.17277   プロジェクト作業フォルダ  
1 CreateProject.mq5 1.01
ステージ、ジョブ、最適化タスクを含むプロジェクトを作成するためのEAスクリプト
第23回
2 Optimization.mq5
1.00 プロジェクト自動最適化用EA  第23回
3 SimpleCandles.mq5
1.00 複数のモデル戦略グループを並列操作するための最終EA(パラメータは組み込みのグループライブラリから取得)
第24回
4 Stage1.mq5 1.22  取引戦略単一インスタンス最適化EA(第1ステージ)
第24回
5 Stage2.mq5
1.00 取引戦略単一インスタンス最適化EA(第2ステージ)
第23回
Stage3.mq5
1.00 生成された標準化された戦略グループを、指定された名前のEAデータベースに保存するEA
第23回
  MQL5/Experts/Article.17277/Strategies   プロジェクト戦略フォルダ  
7 SimpleCandlesStrategy.mqh 1.01   第24回
  MQL5/Include/antekov/Advisor/Base
  他のプロジェクトクラスが継承する基本クラス    
8 Advisor.mqh 1.04 EA基本クラス 第10回
9 Factorable.mqh
1.05
文字列から作成されたオブジェクトの基本クラス
第24回
10 FactorableCreator.mqh
1.00   第24回
11 Interface.mqh 1.01
さまざまなオブジェクトを視覚化するための基本クラス
第4回
12 Receiver.mqh
1.04  オープンボリュームを市場ポジションに変換するための基本クラス
第12回
13 Strategy.mqh
1.04
取引戦略基本クラス
第10回
  MQL5/Include/antekov/Advisor/Database
  プロジェクトEAで使用されるすべての種類のデータベースを扱うファイル
 
14 Database.mqh 1.10 データベースを扱うクラス 第22回
15 db.adv.schema.sql 1.00
最終EAのデータベース構造 第22回
16 db.cut.schema.sql
1.00 簡略化された最適化データベースの構造
第22回
17 db.opt.schema.sql
1.05  最適化データベース構造
第22回
18 Storage.mqh   1.01
EAデータベース内の最終EAのキー値ストレージを扱うクラス
第23回
  MQL5/Include/antekov/Advisor/Experts
  異なるタイプのEAで使用される共通部分のファイル
 
19 Expert.mqh  1.22 最終EAのライブラリファイル(グループパラメータはEAデータベースから取得)
第23回
20 Optimization.mqh  1.04 最適化タスクの起動を管理するEAのライブラリファイル
第23回
21 Stage1.mqh
1.19 単一インスタンス取引戦略最適化EAのライブラリファイル(第1ステージ)
第23回
22 Stage2.mqh 1.04 取引戦略インスタンスのグループを最適化するEAのライブラリファイル(第2ステージ)   第23回
23 Stage3.mqh
1.04 生成された標準化された戦略グループを、指定された名前のEAデータベースに保存するEAのライブラリファイル 第23回
  MQL5/Include/antekov/Advisor/Optimization
  自動最適化を担当するクラス
 
24 Optimizer.mqh
1.03  プロジェクト自動最適化マネージャーのクラス
第22回
25 OptimizerTask.mqh
1.03
最適化タスククラス
第22回
  MQL5/Include/antekov/Advisor/Strategies    プロジェクトの動作を示すために使用される取引戦略の例
 
26 HistoryStrategy.mqh 
1.00 取引履歴を再生するための取引戦略のクラス
第16回
27 SimpleVolumesStrategy.mqh
1.11
ティックボリュームを使用した取引戦略のクラス
第22回
  MQL5/Include/antekov/Advisor/Utils
  補助ユーティリティ、コード削減用マクロ  
28 ExpertHistory.mqh 1.00 取引履歴をファイルにエクスポートするクラス 第16回
29 Macros.mqh 1.05 配列操作に便利なマクロ 第22回
30 NewBarEvent.mqh 1.00  特定の銘柄の新しいバーを定義するクラス  第8回
31 SymbolsMonitor.mqh  1.00 取引商品(銘柄)に関する情報を取得するためのクラス 第21回
  MQL5/Include/antekov/Advisor/Virtual
  仮想の取引注文やポジションのシステムを用いた各種オブジェクト作成用クラス
 
32 Money.mqh 1.01  基本的なお金の管理クラス
第12回
33 TesterHandler.mqh  1.07 最適化イベント処理クラス  第23回
34 VirtualAdvisor.mqh  1.10  仮想ポジション(注文)を扱うEAのクラス 第24回
35 VirtualChartOrder.mqh  1.01  グラフィカル仮想ポジションクラス 第18回
36 VirtualHistoryAdvisor.mqh 1.00  トレード履歴再生EAクラス  第16回
37 VirtualInterface.mqh  1.00  EAGUIクラス  第4回
38 VirtualOrder.mqh 1.09  仮想注文とポジションのクラス  第22回
39 VirtualReceiver.mqh 1.04 オープンボリュームを市場ポジションに変換するクラス(レシーバー) 第23回
40 VirtualRiskManager.mqh  1.05 リスクマネジメントクラス(リスクマネージャー) 第24回
41 VirtualStrategy.mqh 1.09  仮想ポジションを使った取引戦略のクラス 第23回
42 VirtualStrategyGroup.mqh  1.03  取引戦略グループのクラス 第24回
43 VirtualSymbolReceiver.mqh  1.00 銘柄レシーバークラス  第3回

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

添付されたファイル |
MQL5.zip (102.06 KB)
ビリヤード最適化アルゴリズム(BOA) ビリヤード最適化アルゴリズム(BOA)
BOA法は、古典的なビリヤードに着想を得ており、最適解を探すプロセスを、玉が穴に落ちることで最良の結果を表すゲームとしてシミュレーションします。本記事では、BOAの基本、数学モデル、およびさまざまな最適化問題を解く際の効率について考察します。
カオスゲーム最適化(CGO) カオスゲーム最適化(CGO)
本記事では、新しいメタヒューリスティックアルゴリズムであるカオスゲーム最適化(CGO)を紹介します。CGOは、高次元問題に対しても高い効率を維持できるという独自の特性を示しています。ほとんどの最適化アルゴリズムとは異なり、CGOは問題の規模が大きくなると性能が低下するどころか、場合によっては向上することさえあり、これがこのアルゴリズムの主要な特徴です。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
純粋なMQL5で実装した通貨ペア強度インジケーター 純粋なMQL5で実装した通貨ペア強度インジケーター
MetaTrader 5向けの通貨強度分析用のプロフェッショナルなインジケーターを開発します。このステップバイステップガイドでは、強力な取引ツールを作成する方法を解説します。視覚的なダッシュボードを搭載し、複数の時間足(H1、H4、D1)で通貨ペアの強さを計算し、動的なデータ更新を実装し、ユーザーフレンドリーなインターフェースを作成することができます。