English Русский 中文 Español Deutsch Português
preview
多通貨エキスパートアドバイザーの開発(第15回):実際の取引のためのEAの準備

多通貨エキスパートアドバイザーの開発(第15回):実際の取引のためのEAの準備

MetaTrader 5テスター |
153 2
Yuriy Bykov
Yuriy Bykov

はじめに

これまでの記事で一定の成果を上げてきましたが、まだやるべきことが多く残っています。私たちが目指している最終的な形は、実際の口座や、異なるブローカーの複数の実際の口座で動作できる多通貨EAです。これまでのところ、私たちの取り組みは、テスト段階で良好な取引結果を得ることに集中してきました。なぜなら、開発したEAを実際の口座で運用し、良い取引結果を得るためには、まずテストで成功を収めることが不可欠だからです。そして現在、ある程度満足のいくテスト結果が得られたため、実際の口座で正しく動作させるための準備にも目を向ける段階に入っています。

このEA開発における側面については、すでに一部取り上げています。特に、リスクマネージャーの開発は、実際の取引プロセスで発生する可能性のある要件を満たすための重要な一歩でした。リスクマネージャーは非常に重要なツールですが、補助的な役割を持つため、取引のアイデアをテストする段階では必須ではありません。

本記事では、実際の口座での取引を開始する前に備えておくべき、その他の重要なメカニズムについて取り上げます。これらのメカニズムは、ストラテジーテスター上でEAを動かしているだけでは発生しないような状況に対応するためのものです。そのため、それらの動作をデバッグし、正しく機能するかを確認するために、新たに補助的なEAを開発する必要が生じる可能性があります。


パスのマッピング 

実際の口座で取引をおこなう際には、考慮すべき細かな点が数多くあります。ここでは、特に重要ないくつかのポイントに焦点を当ててみましょう。

  • 銘柄の置き換え:私たちは最適化をおこない、EAの初期化文字列を、特定の取引銘柄の名称を用いて作成しました。しかし、実際の口座では、取引銘柄の名称が私たちが使用したものと異なる場合があります。たとえば、名称に接尾辞や接頭辞が付く(EURGBPの代わりにEURGBP.xやxEURGBP)、あるいは大文字・小文字の違いがある(EURGBPの代わりにeurgbp)といったケースです。将来的には、取引銘柄のリストが拡張され、さらに大きな名称の違いを持つ銘柄が含まれる可能性もあります。そのため、EAが特定のブローカーが使用する銘柄に適応できるよう、取引銘柄の名称を置き換えるルールを設定できる仕組みを導入する必要があります。

  • 取引完了モード:EA内部で同時に動作する取引ストラテジーの構成や設定を定期的に更新する予定があるため、すでに稼働中のEAを「決済専用モード」に切り替える機能を備えておくことが望ましいです。このモードでは、新規の取引はおこなわず、既存のポジションを決済することに専念します。理想的には、すべてのポジションを利益確定で決済することを目指しますが、未決済ポジションに損失が発生している場合は、取引の終了までに時間がかかる可能性があります。

  • 再起動後の復旧:EAがターミナルの再起動後も継続して正常に動作する能力を指します。ターミナルの再起動はさまざまな要因によって発生し、すべての原因を事前に防ぐことは不可能です。しかし、EAは単に再起動後に動作を続けるだけでなく、再起動がなかった場合とまったく同じ状態で取引を継続できる必要があります。そのため、EAの動作中に必要な情報を適切に保存し、再起動後に正確に復元できるようにする仕組みを整えることが重要です。

それでは、これらの実装に取り組んでいきましょう。


銘柄の置き換え

まずは最もシンプルな機能から取り組みましょう。EAの設定で取引銘柄の名称を置き換えるルールを設定できるようにすることです。一般的な違いとして、接頭辞や接尾辞の有無が挙げられます。そのため、一見すると、EAの入力パラメータとして接頭辞と接尾辞を指定するための2つの新しい項目を追加することで対応できそうです。

しかし、この方法では、初期化文字列から取得した元の銘柄名を特定のルールに従って変換する固定的なアルゴリズムしか使用できないため、柔軟性に欠けます。さらに、大文字・小文字の変換を考慮する場合、さらに別のパラメータが必要になります。そこで、別の方法を導入します。

EAに新しいパラメータを1つ追加し、次のような形式の文字列を設定できるようにします。

<Symbol1>=<TargetSymbol1>;<Symbol2>=<TargetSymbol2>;...<SymbolN>=<TargetSymbolN>

ここで、<Symbol[i]>は初期化文字列で使用される元の取引銘柄名を表し、<TargetSymbol[i]>は実際の取引で使用するターゲットの銘柄名を表します。たとえば、以下のように設定することができます。

EURGBP=EURGBPx;EURUSD=EURUSDx;GBPUSD=GBPUSDx

このパラメータの値は、EAオブジェクト(CVirtualAdvisorクラス)の専用メソッドに渡され、必要な処理がおこなわれます。このメソッドに空の文字列が渡された場合は、取引銘柄名の変更はおこないません。

このメソッドをSymbolsReplaceとし、EAの初期化関数内で呼び出すように実装していきます。

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input string   symbolsReplace_   = "";       // - Symbol replacement rules


datetime fromDate = TimeCurrent();


CVirtualAdvisor     *expert;             // EA object

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

// Create an EA handling virtual positions
   expert = NEW(expertParams);

// If the EA is not created, then return an error
   if(!expert) return INIT_FAILED;

// If an error occurred while replacing symbols, then return an error
   if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED;

// Successful initialization
   return(INIT_SUCCEEDED);
}

加えられた変更を、カレントディレクトリのSimpleVolumesExpert.mq5ファイルに保存します。


EAクラスに、銘柄名の置換をおこなうメソッドを追加し、その実装をおこないましょう。このメソッドでは、渡された置換ルールの文字列を解析し、適切な銘柄名の変換をおこないます。まず、受け取った文字列をセミコロン(;)で区切り、次に等号(=)で分割することで、各置換ルールを個別の要素に分解します。そこから、元の銘柄名(ソース銘柄)と変換後の銘柄名(ターゲット銘柄)を対応付けるマッピングリストを作成します。次に、このマッピングリストを取引戦略の各インスタンスに順番に渡し、それぞれの戦略が自分の銘柄がマッピングリストのキーとして存在する場合に、対応するターゲット銘柄へと置換できるようにします。

また、処理の各ステップでエラーが発生する可能性があるため、その都度結果変数を更新し、銘柄名の置換が失敗した場合に上流の関数が検知できるようにします。この場合、EAは初期化の失敗を報告し、適切に対応できるようにします。

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

public:
   ...

   bool SymbolsReplace(const string p_symbolsReplace); // Replace symbol names
};

...

//+------------------------------------------------------------------+
//| Replace symbol names                                             |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::SymbolsReplace(string p_symbolsReplace) {
   // If the replacement string is empty, then do nothing
   if(p_symbolsReplace == "") {
      return true;
   }

   // Variable for the result
   bool res = true;

   string symbolKeyValuePairs[]; // Array for individual replacements
   string symbolPair[];          // Array for two names in one replacement

   // Split the replacement string into parts representing one separate replacement
   StringSplit(p_symbolsReplace, ';', symbolKeyValuePairs);

   // Glossary for mapping target symbol to source symbol
   CHashMap<string, string> symbolsMap;

   // For all individual replacements
   FOREACH(symbolKeyValuePairs, {
      // Get the source and target symbols as two array elements 
      StringSplit(symbolKeyValuePairs[i], '=', symbolPair);

      // Check if the target symbol is in the list of available non-custom symbols
      bool custom = false;
      res &= SymbolExist(symbolPair[1], custom);

      // If the target symbol is not found, then report an error and exit
      if(!res) {
         PrintFormat(__FUNCTION__" | ERROR: Target symbol %s for mapping %s not found", symbolPair[1], symbolKeyValuePairs[i]);
         return res;
      }
      
      // Add a new element to the glossary: key is the source symbol, while value is the target symbol
      res &= symbolsMap.Add(symbolPair[0], symbolPair[1]);
   });

   // If no errors occurred, then for all strategies we call the corresponding replacement method
   if(res) {
      FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).SymbolsReplace(symbolsMap));
   }

   return res;
}
//+------------------------------------------------------------------+

変更をカレントディレクトリのVirtualAdvisor.mqhファイルに保存します。


取引戦略クラスにも、同じ名前のメソッドを追加します。ただし、このメソッドでは文字列として置換ルールを受け取るのではなく、置換マッピングリストを引数として受け取る仕様に変更します。しかしながら、CVirtualStrategyクラスにはこのメソッドの実装を直接追加することができません。なぜなら、このクラスのレベルでは使用される取引銘柄についての情報がまだ不明だからです。そのため、このメソッドを仮想関数として定義し、具体的な実装の責任を子クラスに委ねることにします。

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
   ...
public:
   ...
   // Replace symbol names
   virtual bool      SymbolsReplace(CHashMap<string, string> &p_symbolsMap) { 
      return true;
   }
};

変更をカレントディレクトリのVirtualStrategy.mqhファイルに保存します。


現在のところ、子クラスは1つだけであり、このクラスにはm_symbolプロパティがあり、取引銘柄名を保持しています。ここにSymbolsReplace()メソッドを追加しましょう。このメソッドでは、渡されたマッピングリストに、現在の取引銘柄名と一致するキーが含まれているかをチェックし、該当する場合は取引銘柄を適切に変更します。

//+------------------------------------------------------------------+
//| Trading strategy using tick volumes                              |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;         // Symbol (trading instrument)
   ...

public:
   ...

   // Replace symbol names
   virtual bool      SymbolsReplace(CHashMap<string, string> &p_symbolsMap);
};

...

//+------------------------------------------------------------------+
//| Replace symbol names                                             |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::SymbolsReplace(CHashMap<string, string> &p_symbolsMap) {
   // If there is a key in the glossary that matches the current symbol
   if(p_symbolsMap.ContainsKey(m_symbol)) {
      string targetSymbol; // Target symbol
      
      // If the target symbol for the current one is successfully retrieved from the glossary
      if(p_symbolsMap.TryGetValue(m_symbol, targetSymbol)) {
         // Update the current symbol
         m_symbol = targetSymbol;
      }
   }
   
   return true;
}

変更をSimpleVoumesStrategy.mqhファイルに保存します。

これでこのサブタスクに関する編集は完了しました。テスターによるチェックの結果、EAが置換ルールに従い、新しい銘柄で正常に取引を開始することが確認されました。また、CHashMap::Add()メソッドを使用して置換マッピングリストを構築しているため、既に存在するキー(ソース銘柄)に対して、新たにターゲット銘柄を追加しようとするとエラーが発生することに注意が必要です。

つまり、置換ルールの文字列内で同じ銘柄に対するルールを2回指定すると、EAは初期化に失敗します。そのため、重複する置換ルールを削除し、適切に調整する必要があります。


取引完了モード

次の作業は、EAに特別な動作モード「取引完了モード」を設定する機能を追加することです。まず、このモードの意味について明確にしておく必要があります。このモードは、現在稼働中のEAを新しいパラメータを持つ別のEAに置き換える際にのみ有効化する予定です。そのため、一方では、既存のEAによって建てられたすべてのポジションをできるだけ早く決済したいという意図があります。他方では、ポジションの含み損が発生している場合、すぐに決済するのは避けたいとも考えられます。この場合、EAがドローダウンから回復するまで待った方が良いかもしれません。

この問題を次のように定義します。取引完了モードが有効な場合、EAはすべてのポジションを決済し、新しいポジションを建てないようにする。ただし、決済は含み損が解消し、非負になった時点でおこなう。このモードがオンになった時点で含み損がなければ、待つ必要はないので、即座にすべてのポジションを決済し、新しいポジションを建てない。逆に、含み損がある場合は、回復を待つしかない。

次に問題となるのはどのくらい待つべきかという点です。履歴テストの結果からドローダウンが数か月続くケースがあることが分かっています。そのため単に待つだけでは取引完了モードの起動タイミングによっては、非常に長く待たされる可能性があります。むしろ現在の損失を受け入れて即座にポジションを決済し新バージョンのEAをすぐに稼働させた方が有利かもしれません。その場合新バージョンが利益を生み出し旧バージョン停止時の損失をカバーできる可能性があります。

しかし旧バージョンがドローダウンから抜けるタイミングや新バージョンがどれだけの利益を生むかを事前に予測することはできません。 

このような状況における妥協案として最大待機時間を導入しその時間を超えた場合は強制的にポジションを決済する方法が考えられます。より高度な方法として時間制限を線形関数や非線形関数を使い「決済に必要な資金量」を時間とともに変化させることもできます。最も単純なケースでは時間制限前は0を返し時間制限後は口座資金の一定割合を返す「閾値関数」として動作させます。これにより指定した時間が経過するとすべてのポジションが確実に決済されます。

実装を進めます。最初の選択肢は、EAファイルに2つの入力(決済モードの有効化と制限時間(日単位))を追加し、さらにその先の初期化関数で使用することでした。

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input bool     useOnlyCloseMode_ = false;    // - Enable close mode
input double   onlyCloseDays_    = 0;        // - Maximum time of closing mode (days)

...

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

// Prepare the initialization string for an EA with a group of several strategies
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],%f\n"
                            "    ),\n"
                            "    class CVirtualRiskManager(\n"
                            "       %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            "    ,%d,%.2f\n"
                            ")",
                            strategiesParams, scale_,
                            rmIsActive_, rmStartBaseBalance_,
                            rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_,
                            rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_,
                            rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_,
                            rmMaxRestoreTime_, rmLastVirtualProfitFactor_,
                            magic_, "SimpleVolumes", useOnlyNewBars_,
                            useOnlyCloseMode_, onlyCloseDays_
                         );

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   ...

// Successful initialization
   return(INIT_SUCCEEDED);
}

しかし、作業を進めるうちに、つい最近まで書いていたコードと非常によく似たものを再び書かなければならないことが明らかになりました。その結果、クロージングモードで求められる動作は、リスクマネージャーがクロージングモード開始時の現在残高と基準残高の差を目標利益として設定しているEAの動作とほぼ同じであることが分かりました。であれば、リスクマネージャーを少し変更し、必要なパラメーターを設定するだけでクロージングモードを実行できるようにしてはどうでしょうか。

クロージングモードを実装するにあたり、リスクマネージャーに何が不足しているのかを考えてみましょう。最も単純なケースでは、最大待機時間を考慮しない場合、リスクマネージャーに特別な修正は必要ありません。旧バージョンでは、目標利益パラメーターを現在の口座残高と基準残高の差と同じ値に設定し、その値に達するまで待つだけで済みました。さらに進めば、時間の経過に応じてこの値を定期的に調整することもできます。ただし、この仕組みが頻繁に使われることはないでしょう。一方で、指定された時間が経過したら自動的にポジションを決済する方が望ましいと考えられます。そこで、リスクマネージャーに目標利益だけでなく、最大許容待機時間を設定できる機能を追加しましょう。これにより、ポジションを決済するための最大時間を制御できるようになります。

この時間は、具体的な日時として指定する方が、指定したインターバルの開始時点を覚えておく必要がなく、より便利です。このパラメータをリスクマネージャーに関連する入力項目のセットに追加し、その値を初期化文字列に代入する処理も加えましょう。

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input group ":::  Risk manager"

...

input ENUM_RM_CALC_OVERALL_PROFIT
rmCalcOverallProfitLimit_                    = RM_CALC_OVERALL_PROFIT_MONEY_BB;  // - Method for calculating total profit
input double      rmMaxOverallProfitLimit_   = 1000000;                          // - Overall profit
input datetime    rmMaxOverallProfitDate_    = 0;                                // - Maximum time of waiting for the total profit (days)

...

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

// Prepare the initialization string for an EA with a group of several strategies
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],%f\n"
                            "    ),\n"
                            "    class CVirtualRiskManager(\n"
                            "       %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%d,%.2f,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            strategiesParams, scale_,
                            rmIsActive_, rmStartBaseBalance_,
                            rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_,
                            rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_,
                            rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_,rmMaxOverallProfitDate_,
                            rmMaxRestoreTime_, rmLastVirtualProfitFactor_,
                            magic_, "SimpleVolumes", useOnlyNewBars_
                         );
   
// Create an EA handling virtual positions
   expert = NEW(expertParams);

...

// Successful initialization
   return(INIT_SUCCEEDED);
}

変更をカレントディレクトリのSimpleVolumesExpert.mq5ファイルに保存します。


リスクマネージャークラスでは、まず与えられた利益を待つための最大待機時間を表す新しいプロパティを追加し、初期化文字列からコンストラクタを通じてその値を設定します。さらに、新しいメソッドOverallProfit()を追加し、クロージングに必要な利益値を返すようにします。

//+------------------------------------------------------------------+
//| Risk management class (risk manager)                             |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Main constructor parameters
   ...
   ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Method for calculating maximum overall profit
   double            m_maxOverallProfitLimit;            // Parameter for calculating the maximum overall profit
   datetime          m_maxOverallProfitDate;             // Maximum time of reaching the total profit

   ...

// Protected methods
   double            DailyLoss();            // Maximum daily loss
   double            OverallLoss();          // Maximum total loss
   double            OverallProfit();        // Maximum profit

   ...
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualRiskManager::CVirtualRiskManager(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string and set the property values
   m_isActive = (bool) ReadLong(p_params);
   m_baseBalance = ReadDouble(p_params);
   m_calcDailyLossLimit = (ENUM_RM_CALC_DAILY_LOSS) ReadLong(p_params);
   m_maxDailyLossLimit = ReadDouble(p_params);
   m_closeDailyPart = ReadDouble(p_params);
   m_calcOverallLossLimit = (ENUM_RM_CALC_OVERALL_LOSS) ReadLong(p_params);
   m_maxOverallLossLimit = ReadDouble(p_params);
   m_closeOverallPart = ReadDouble(p_params);
   m_calcOverallProfitLimit = (ENUM_RM_CALC_OVERALL_PROFIT) ReadLong(p_params);
   m_maxOverallProfitLimit = ReadDouble(p_params);
   m_maxOverallProfitDate  = (datetime) ReadLong(p_params);
   m_maxRestoreTime = ReadDouble(p_params);
   m_lastVirtualProfitFactor = ReadDouble(p_params);

   ...
}


OverallProfit()メソッドは、まず希望する利益を達成するまでの時間が設定されているかどうかをチェックします。時間が設定され、現在の時間がすでに設定された時間を超えている場合、現在の値はすでに望ましい値であるため、メソッドは現在の利益値を返します。その結果、最終的にすべてのポジションが決済され、取引が停止されます。時間がまだ到達していない場合、このメソッドは、入力から計算された希望の利益の値を返します。

//+------------------------------------------------------------------+
//| Maximum total profit                                             |
//+------------------------------------------------------------------+
double CVirtualRiskManager::OverallProfit() {
   // Current time
   datetime tc = TimeCurrent();
   
   // If the current time is greater than the specified maximum time,
   if(m_maxOverallProfitDate && tc > m_maxOverallProfitDate) {
      // Return the value that guarantees the positions are closed
      return m_overallProfit;
   } else if(m_calcOverallProfitLimit == RM_CALC_OVERALL_PROFIT_PERCENT_BB) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxOverallProfitLimit / 100;
   } else {
      // To get a fixed value, just return it 
      // RM_CALC_OVERALL_PROFIT_MONEY_BB
      return m_maxOverallProfitLimit;
   }
}


CheckOverallProfitLimit()メソッド内でクロージングの必要性をチェックする際に、このメソッドを使用します。

//+------------------------------------------------------------------+
//| Check if the specified profit has been achieved                  |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallProfitLimit() {
// If overall loss is reached and positions are still open
   if(m_overallProfit >= OverallProfit() && CMoney::DepoPart() > 0) {
      // Reduce the multiplier of the used part of the overall balance by the overall loss
      m_overallDepoPart = 0;

      // Set the risk manager to the achieved overall profit state
      m_state = RM_STATE_OVERALL_PROFIT;

      // Set the value of the used part of the overall balance
      SetDepoPart();

      ...

      return true;
   }

   return false;
}

変更をカレントディレクトリのVirtualRiskManager.mqhファイルに保存します。

シャットダウンモードに関する変更はほぼ完了しました。再起動後に機能を回復できるようにするため、残りの作業は後で追加する予定です。


再起動後の回復

そのような機能を提供する必要性は、本連載の最初の段階から想定されていました。作成したクラスの多くは、オブジェクトの状態を保存・ロードするためのSave()メソッドやLoad()メソッドを備えています。これらのメソッドの中にはすでに動作するコードを持っているものもありましたが、その後、他の作業に追われ、必要がなかったため正常に動作させることができていませんでした。今こそ、それらに集中し、再び正常に機能するよう修正すべき時です。

おそらく、最も大きな変更が必要になるのはリスクマネージャークラスです。なぜなら、これらのメソッドがまだ完全に空のままだからです。また、EAのロード/保存時にリスクマネージャーのSave()メソッドとLoad()メソッドが確実に呼び出されるようにする必要があります。リスクマネージャーは後から追加されたため、保存対象の情報に含まれていませんでした。

まず、EAに以前の状態を復元するかどうかを決定するための入力パラメータを追加しましょう。デフォルトではTrueに設定します。もしEAをゼロから開始したい場合、このパラメータをFalseに設定してEAを再起動します(この場合、以前に保存された情報はすべて新しい情報で上書きされます)。その後、このパラメータをTrueに戻せば、次回の起動時に以前の状態が復元されます。EAの初期化関数内で、前回の状態をロードする必要があるかどうかを確認し、必要であればロードするようにします。

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input bool     usePrevState_     = true;     // - Load the previous state

...

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

// Create an EA handling virtual positions
   expert = NEW(expertParams);

// If the EA is not created, then return an error
   if(!expert) return INIT_FAILED;

// If an error occurred while replacing symbols, then return an error
   if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED;


// If we need to restore the state,
   if(usePrevState_) {
      // Load the previous state if available
      expert.Load();
      expert.Tick();
   }

// Successful initialization
   return(INIT_SUCCEEDED);
}

変更をカレントディレクトリのSimpleVolumesExpert.mq5ファイルに保存します。


状態を保存/ロードするメソッドに移る前に、次の点に注意しましょう。以前のバージョンでは、ビジュアルテストモードで実行する場合、EAの名前、マジックナンバー、そして「.test」という単語を組み合わせて保存用のファイル名を作成していました。EA名は定数値であり、ソースコードに埋め込まれているため、EAの入力によって変更されることはありません。一方、マジックナンバーは入力から変更可能です。つまり、マジックナンバーを変更すると、EAは以前のマジックナンバーで生成されたファイルをロードしなくなります。しかしこれは、取引戦略の単一インスタンスの構成を変更せず、マジックナンバーをそのままにした場合、EAが以前のファイルを使用してステータスをロードしようとすることも意味します。

この状況はエラーを引き起こす可能性が高いため、対策を講じる必要があります。その方法の一つとして、ファイル名に取引戦略のインスタンスに依存する部分を含めることが考えられます。これにより、戦略の構成が変更されるとファイル名の該当部分も変わり、EAは古いファイルを使用しなくなります。

この変更を実現するには、EAの初期化文字列またはその一部からハッシュ関数を計算し、それをファイル名に組み込む方法が有効です。ただし、すべての設定変更で異なるファイルを使用する必要はなく、特に取引戦略の構成を変更した場合のみ新しいファイルを使用するのが適切です。例えば、リスクマネージャーの設定を変更しても、ファイル名は変更されるべきではありません。したがって、取引戦略の単一インスタンスに関する情報を含む部分のみを抽出し、そこからハッシュ値を計算するようにします。

そのためには、新しいメソッドHashParams()を追加し、EAのコンストラクタを変更します。

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   virtual string    HashParams(string p_name);   // Hash value of EA parameters 

public:
   ...
};

...

//+------------------------------------------------------------------+
//| Hash value of EA parameters                                      |
//+------------------------------------------------------------------+
string CVirtualAdvisor::HashParams(string p_params) {
   uchar hash[], key[], data[];

   // Calculate the hash from the initialization string  
   StringToCharArray(p_params, data);
   CryptEncode(CRYPT_HASH_MD5, data, key, hash);

   // Convert it from the array of numbers to a string with hexadecimal notation
   string res = "";
   FOREACH(hash, res += StringFormat("%X", hash[i]); if(i % 4 == 3 && i < 15) res += "-");
   
   return res;
}

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {


       ... 

// If there are no read errors,
   if(IsValid()) {

      
    ... 

      // Form the name of the file for saving the state from the EA name and parameters
      m_name = StringFormat("%s-%d-%s%s.csv",
                            (p_name != "" ? p_name : "Expert"),
                            p_magic,
                            HashParams(groupParams),
                            (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                           );;

      
    ... 
   }
}


対応するEAメソッドに、リスクマネージャーの保存とロードを追加します。 

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
   bool res = true;

// Save status if:
   if(true
// later changes appeared
         && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
// currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
// and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, '\t');

      if(f != INVALID_HANDLE) {  // If file is open, save
         FileWrite(f, CVirtualReceiver::s_lastChangeTime);  // Time of last changes

         // All strategies
         FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f));

         m_riskManager.Save(f);

         FileClose(f);

         // Update the last save time
         m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
         PrintFormat(__FUNCTION__" | OK at %s to %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d",
                     m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
   bool res = true;

// Load status if:
   if(true
// file exists
         && FileIsExist(m_name)
// currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
// and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_READ, '\t');

      if(f != INVALID_HANDLE) {  // If the file is open, then load
         m_lastSaveTime = FileReadDatetime(f);     // Last save time
         PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

         // Load all strategies
         FOREACH(m_strategies, {
            res &= ((CVirtualStrategy*) m_strategies[i]).Load(f);
            if(!res) break;
         });

         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name);
         }

         res &= m_riskManager.Load(f);
         
         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_name);
         }

         FileClose(f);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError());
         res = false;
      }
   }

   return res;
}

変更をカレントディレクトリのVirtualAdvisor.mq5ファイルに保存します。


あとは、リスクマネージャーをセーブ/ロードするメソッドの実装を書くのみです。まず、リスクマネージャーにおいて何を保存すべきかを整理しましょう。リスクマネージャーの入力値は保存する必要がありません。これらは常にEAの入力から取得され、次回の起動時には変更された値が自動的に適用されます。また、リスクマネージャー自身が更新するデータ(残高、エクイティ、日々の利益など)についても、保存の必要はありません。唯一保持すべきなのは1日のベースレベルです。これは1日1回のみ計算されるため、適切に復元する必要があります。

さらに、現在の状態やポジションサイズの管理に関連するプロパティはすべて保存されるべきです。ただし、全体残高の使用済み部分は保存対象から除外します。

// Current state
   ENUM_RM_STATE     m_state;                // State
   double            m_lastVirtualProfit;    // Profit of open virtual positions at the moment of loss limit 
   datetime          m_startRestoreTime;     // Start time of restoring the size of open positions
   datetime          m_startTime;

// Updated values
   
    ... 

// Managing the size of open positions
   double            m_baseDepoPart;         // Used (original) part of the total balance
   double            m_dailyDepoPart;        // Multiplier of the used part of the total balance by daily loss
   double            m_overallDepoPart;      // Multiplier of the used part of the total balance by overall loss


以上を考慮すると、これらのメソッドの実装は次のようになります。

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::Save(const int f) {
   FileWrite(f,
             m_state, m_lastVirtualProfit, m_startRestoreTime, m_startTime,
             m_dailyDepoPart, m_overallDepoPart);

   return true;
}

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::Load(const int f) {
   m_state = (ENUM_RM_STATE) FileReadNumber(f);
   m_lastVirtualProfit = FileReadNumber(f);
   m_startRestoreTime = FileReadDatetime(f);
   m_startTime = FileReadDatetime(f);
   m_dailyDepoPart = FileReadNumber(f);
   m_overallDepoPart = FileReadNumber(f);

   return true;
}

変更をカレントディレクトリのVirtualRiskManager.mq5ファイルに保存します。


検証

追加された機能を検証するために、2つの方法を試みます。まず、コンパイル済みEAをチャートに設置し、ステータスデータファイルが正しく作成されることを確認します。さらに検証を進めるには、EAがポジションを建てるするまで待つ必要がありますが、これはかなりの時間を要する可能性があります。さらに、リスクマネージャーが発動し、EAが介入後に正常に再開するかどうかを確認するには、より長い時間待つ必要があります。次に、ストラテジーテスターを用いて、EAが停止した後に動作を再開するケースをシミュレートします。

このシミュレーションのために、既存のEAを基に新しいEAを作成し、2つの新しい入力を追加します。それが「再スタート前の停止時間」と「再スタートの開始時間」です。処理の流れは以下のようになります。

  • 再スタート前の停止時間が指定されていない場合(ゼロまたは1970-01-01 00:00:00に等しい)、またはテスト間隔内にない場合、EAは元のまま動作する
  • テスト実行区間内に特定の停止時間が指定されている場合、その時間に達すると、EAは「再スタートの開始時間」に達するまでティックハンドラの実行を停止する

コードでは、これら2つのパラメータは次のようになります。

input datetime restartStopTime_  = 0;        // - Stop time before restart
input datetime restartStartTime_ = 0;        // - Restart launch time

EAのティック処理関数を変更してみましょう。ブレークが発生したことを記憶するために、グローバルなブール変数isRestartingを追加します。この変数がTrueの場合、EAは現在待機中という状態になります。そして、現在時刻が再開時刻を超えたら、前回のEAステータスをロードし、isRestartingフラグをリセットします。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
// If the stop time is specified,
   if(restartStopTime_ != 0) {
      // Define the current time
      datetime tc = TimeCurrent();

      // If we are in the interval between stopping and resuming,
      if(tc >= restartStopTime_ && tc <= restartStartTime_) {
         // Save the status and exit
         isRestarting = true;
         return;
      }

      // If we were in a state between stopping and resuming,
      // and it is time to resume work,
      if(isRestarting && tc > restartStartTime_) {
         // Load the EA status
         expert.Load();
         // Reset the status flag between stop and resume
         isRestarting = false;
      }
   }

// Perform tick handling
   expert.Tick();
}

変更をカレントディレクトリのSimpleVolumesTestRestartExpert.mq5ファイルに保存します。


2021年から2022年にかけての中断なしの結果を見てみましょう。

図1:取引を中断することなく検査結果を得る


では、ある時点でEAの動作を一時停止してみましょう。テスト実行後の結果は、休止なしのケースとまったく同じでした。つまり、EAは短期間の停止後も正常にステータスを回復し、動作を継続できていることが確認できました。

では、その違いをより明確にするために、 例えば4カ月間という長期間の休止を設定してみます。すると、次のような結果が得られます。

図2:2021.07.27から2021.11.29まで取引を休止した場合のテスト結果

チャート上では、休止期間のおおよその位置が黄色の枠で囲まれた長方形として示されています。この間、EAが建てたポジションはそのまま放置されていました。しかし、その後EAは再び動作を開始し、既存のポジションを引き継いで、最終的には良好な結果を得ることができました。これにより、EAの状態を保存・復元する機能が正しく実装されていることが確認できました。


結論

今回の結果を改めて振り返ってみましょう。私たちは、EAを実際の取引口座で稼働させるための準備を本格的に進めています。そのために、テスト環境では発生しないが、実際の取引では頻繁に遭遇するさまざまなシナリオを考慮しました。

EAが最適化された環境と異なる銘柄名を使用する口座でも正常に動作できるようにするため、銘柄名の置き換え機能を実装しました。また、異なる入力パラメータでEAを起動する必要がある場合に、取引をスムーズに終了できる仕組みも導入しました。さらに、ターミナルを再起動した後でも適切に動作を再開できるように、EAの状態を保存する機能を追加したことも大きな進展です。

しかし、実際の取引口座でEAを稼働させる際におこなうべき準備はまだ残っています。EAの現在の状態をチャート上に分かりやすく表示し、補助情報を整理して見やすくする必要があります。さらに重要なのは、ターミナルの作業フォルダに最適化結果のデータベースが存在しない場合でも、EAが正常に動作できるように修正を加えることです。現時点では、EAの初期化文字列を生成するための情報をこのデータベースから取得しているため、これが欠けると動作に支障が出ます。これらの改善については、今後の記事で詳しく検討していきます。

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

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

最後のコメント | ディスカッションに移動 (2)
Максим Курбатов
Максим Курбатов | 23 7月 2024 において 17:26
エキスパートアドバイザーがコンパイルできません。最初は、異なるmqhファイルをインクルードする必要がありました。どうやら間違ったバージョンのファイルを読み込んでいるようです。このEAコードに関連するインクルードファイルのバージョンを教えていただけませんか?ありがとうございました!
Yuriy Bykov
Yuriy Bykov | 24 7月 2024 において 07:43

こんにちは!

すぐに確認しようと思います。各記事では、変更されたすべてのファイルが添付されているかダブルチェックするようにしています。現在の記事に添付されていないファイルは、そのようなファイルが存在する最新の記事から取得する必要があります。

取引におけるカオス理論(第1回):金融市場における導入と応用、リアプノフ指数 取引におけるカオス理論(第1回):金融市場における導入と応用、リアプノフ指数
カオス理論は金融市場に適用できるでしょうか。この記事では、従来のカオス理論とカオスシステムがビル・ウィリアムズが提案した市場のカオスの概念とどのように異なるかについて考察します。
適応型社会行動最適化(ASBO):Schwefel、ボックス=ミュラー法 適応型社会行動最適化(ASBO):Schwefel、ボックス=ミュラー法
この記事は、生物の社会的行動の世界と、それが新たな数学モデルであるASBO(適応型社会的行動最適化、Adaptive Social Behavior Optimization)の構築に与える影響について、興味深い洞察を提供します。生物社会におけるリーダーシップ、近隣関係、協力の原則が、革新的な最適化アルゴリズムの開発にどのように着想を与えるのかを探ります。
適応型社会行動最適化(ASBO):二段階の進化 適応型社会行動最適化(ASBO):二段階の進化
生物の社会的行動と、それが新しい数学モデルであるASBO(適応型社会的行動最適化)の開発に与える影響について、引き続き考察していきます。今回は、二段階の進化プロセスを詳しく分析し、アルゴリズムをテストした上で結論を導き出します。自然界において生物の集団が生存のために協力するのと同様に、ASBOも集団行動の原理を活用し、複雑な最適化問題を解決します。
人工電界アルゴリズム(AEFA) 人工電界アルゴリズム(AEFA)
この記事では、クーロンの静電気力の法則に触発された人工電界アルゴリズム(AEFA: Artificial Electric Field Algorithm)を紹介します。このアルゴリズムは、荷電粒子とその相互作用を利用して複雑な最適化問題を解決するために電気現象をシミュレートします。AEFAは、自然法則に基づいた他のアルゴリズムと比較して、独自の特性を示します。