English Русский 中文 Español Deutsch Português
preview
継続的なウォークフォワード最適化(その8)。プログラムの改善と修正

継続的なウォークフォワード最適化(その8)。プログラムの改善と修正

MetaTrader 5テスター | 11 1月 2021, 07:46
518 0
Andrey Azatskiy
Andrey Azatskiy

イントロダクション

本連載では、ユーザーや読者の皆様からのご意見・ご要望をもとに、プログラムを修正しています。 この記事では、オートオプティマイザーの新バージョンを掲載しています。 このバージョンでは、要求された機能を実装し、他の改善点を提供しています。 これは、ミューテックスを使用せずに動作させることを決定した結果であり、レポート生成プロセスを遅くしていたいくつかの他のアクションを回避した結果です。 これで、最適化は資産のセットにも使えるようになりました。 また、最適化時にメモリが占有される問題も解消されました。  

このシリーズの中の過去の記事です。 

  1. 連続的なウォークフォワード最適化(その1)。最適化レポートを使った作業 
  2. 連続ウォークスルー最適化(その2)。任意のロボットの最適化レポートを作成する仕組み
  3. 連続的なウォークフォワード最適化(その3)。ロボットの自動最適化への適応
  4. 連続的なウォークフォワード最適化(その4)。最適化マネージャ(オートオプティマイザ)
  5. 連続的なウォークフォワード最適化(その5)。オートオプティマイザプロジェクトの概要とGUIの作成
  6. 連続ウォークフォワード最適化(その6)。オートオプティマイザの論理的な部分と構造 
  7. 連続的なウォークフォワード最適化(その7)。オートオプティマイザーの論理部分とグラフィックスを接続し、プログラムからグラフィックスを制御する


日付のオートコンプリートの追加

以前のプログラム版では、フォワード最適化とヒストリカル最適化のために日付を段階的に入力していたため、不便でした。 今回は、必要な時間帯の自動入力を実装しました。 機能の詳細については、以下のように説明することができます。 選択した時間間隔は、フォワード最適化とヒストリカル最適化に自動的に分割されます。 どちらの最適化タイプもステップが固定されており、間隔に分割する前に設定されます。 各新しいフォワード範囲は、前の範囲の翌日に開始しなければなりません。 ヒストリカルな間隔(重なり合う間隔)のシフトは、フォワードの窓のステップに等しい。 ヒストリカル最適化とは異なり、フォワードのは重複せず、連続的な取引履歴を実装しています。 

このタスクを実装するために、この機能を別のグラフィックウィンドウに移して、メインインターフェイスには直接関係のない独立したものにすることにしました。 その結果、以下のようなオブジェクトの階層ができました。

 


この機能がどのように接続されているかを考え、その実装例を見てみましょう。 私たちは、作成された拡張機能のグラフィカルなインターフェイスから始めましょう、すなわち、グラフィックウィンドウを表すAutoFillInDateBordersオブジェクトからチャート上のすべてのものです。 画像は、AutoFillInDateBordersVMクラスで表されるViewModel部分のGUI要素、XAMLマークアップ、フィールドを示しています。 

ご覧のように、GUIには主に3つのセクションがあります。 これらには、最適化期間の開始日と終了日を入力するための2つのカレンダー、フォワード間隔とヒストリカル間隔の境界を指定するためのテーブル、および指定した範囲をヒストリカルウィンドウとフォワードウィンドウに分割する「設定」ボタンが含まれています。 スクリーンショットのテーブルには3つの重複した行が含まれていますが、実際には2つの行しかありません:1つ目の行は過去の日付範囲を担当し、2つ目の行はフォワードの範囲を設定します。

表中の「値」は、対応する最適化タイプのステップを日数で表しています。 例えば、履歴間隔のValueを360日、順方向のValueを90とすると、カレンダーで指定された時間間隔は、ヒストリカル最適化間隔360日と順方向の間隔90日に分割されることになります。 次の各ヒストリカル最適化ウィンドウの開始位置は、フォワードの間隔ステップによってシフトされます。  

class AutoFillInDateBordersM : IAutoFillInDateBordersM {     private AutoFillInDateBordersM() { }     private static AutoFillInDateBordersM instance;     public static AutoFillInDateBordersM Instance()     {         if (instance == null)             instance = new AutoFillInDateBordersM();         return instance;     }     public event Action<List<KeyValuePair<OptimisationType, DateTime[]>>> DateBorders;     public void Calculate(DateTime From, DateTime Till, uint history, uint forward)     {         if (From >= Till)             throw new ArgumentException("Date From must be less then date Till");         List<KeyValuePair<OptimisationType, DateTime[]>> data = new List<KeyValuePair<OptimisationType, DateTime[]>>();         OptimisationType type = OptimisationType.History;         DateTime _history = From;         DateTime _forward = From.AddDays(history + 1);         DateTime CalcEndDate()         {             return type == OptimisationType.History ? _history.AddDays(history) : _forward.AddDays(forward);         }            while (CalcEndDate() <= Till)         {             DateTime from = type == OptimisationType.History ? _history : _forward;             data.Add(new KeyValuePair<OptimisationType, DateTime[]>(type, new DateTime[2] { from, CalcEndDate() }));             if (type == OptimisationType.History)                 _history = _history.AddDays(forward + 1);             else                 _forward = _forward.AddDays(forward + 1);             type = type == OptimisationType.History ? OptimisationType.Forward : OptimisationType.History;         }         if (data.Count == 0)             throw new ArgumentException("Can`t create any date borders with set In sample (History) step");         DateBorders?.Invoke(data);     } }

ウィンドウデータのモデルクラスは、オブジェクトシングルトンパターンで書かれたものです。 これにより、拡張機能のグラフィックスウィンドウをバイパスしながら、メインウィンドウの ViewModel 部分とデータモデルとのインタラクションが可能になります。 興味深いメソッドの中で、このオブジェクトには、日付範囲を計算する"Calculate"と、上記のプロシージャの完了後に呼び出されるイベントだけが含まれています。 イベントは、パラメータとして対になった値のコレクションを受け取ります。 最初の日付は選択された間隔の開始を示し、2番目の日付は終了を示します。

日付範囲を計算する方法は、計算されたウィンドウのタイプ(フォワードまたは履歴)を変更する代わりに、1つのループでそれらを計算します。 まず、すべての計算の起点として、ヒストリカルウィンドウ型が設定されています。 また、各ウィンドウタイプの初期日付値は、ループ開始前に設定されています。 ループの各反復では、選択されたウィンドウタイプの極端な境界線が計算される入れ子関数を使用して、この値は極端な範囲の日付で検証されます。 この日付を超えた場合はループ終了条件となります。 最適化ウィンドウの範囲がループで形成されます。 そして、次のウィンドウ開始日とウィンドウ種別切替器を更新します。

すべての操作の後、エラーが発生しなければ、渡された日付範囲でイベントが呼び出されます。 それ以上の行為はすべてクラスが行います。 上記のメソッドの実行は、"Set "ボタンのプレスコールバックによって開始されます。   

今回の拡張機能のデータモデルファクトリーは、最もシンプルな方法で実装されています。

class AutoFillInDateBordersCreator {     public static IAutoFillInDateBordersM Model => AutoFillInDateBordersM.Instance(); }

基本的には、'Model'スタティックプロパティを呼び出す際には、常にデータモデルオブジェクトの同じインスタンスを参照し、それをインターフェース型にキャストしています。 これをメインウィンドウの ViewModel 部分で使用してみましょう。

public AutoOptimiserVM() {     ...     AutoFillInDateBordersCreator.Model.DateBorders += Model_DateBorders;     .... } ~AutoOptimiserVM() {     ...     AutoFillInDateBordersCreator.Model.DateBorders -= Model_DateBorders;     .... }

メインウィンドウの ViewModel オブジェクトのコンストラクタとデストラクタの両方で、このクラスのインスタンスへのポインタを格納するのではなく、静的データモデルファクトリーを介して呼び出すことができます。 メインウィンドウのViewModel部分は、このクラスで動作することを知らなくても、考慮されたクラスで動作することに注意してください。 なぜなら、このオブジェクトへの参照は、クラスのコンストラクタとデストラクタ以外、どこにも言及されていないからです。 前述のイベントをサブスクライブするコールバックは、まず、以前に入力された日付範囲をすべて空にしてから、ループの中で、イベントを通して得られた新しい日付範囲を一つずつ追加していきます。 コレクションに日付範囲を追加するメソッドは、メインのグラフィカルインターフェイスの ViewModel 側にも実装されています。 次のような形です。 

void _AddDateBorder(DateTime From, DateTime Till, OptimisationType DateBorderType) {         try     {         DateBorders border = new DateBorders(From, Till);         if (!DateBorders.Where(x => x.BorderType == DateBorderType).Any(y => y.DateBorders == border))         {             DateBorders.Add(new DateBordersItem(border, _DeleteDateBorder, DateBorderType));         }     }     catch (Exception e)     {         System.Windows.MessageBox.Show(e.Message);     } }

DateBorderオブジェクトの作成は、'try - catch'構造でラップされています。 これは、オブジェクトのコンストラクタで例外が発生する可能性があり、何らかの方法で処理しなければならないためです。 ClearDateBordersメソッドも追加しました。 

ClearDateBorders = new RelayCommand((object o) => {     DateBorders.Clear(); });

入力されたすべての日付範囲を素早く削除することができます。 前バージョンでは、日付ごとに別々に削除する必要があり、日付が多いと不便でした。 記述された技術革新を呼び出すメインGUIウィンドウボタンは、以前に存在した日付範囲コントロールと同じ行に追加されています。 

Autoset をクリックすると、SubFormKeeper クラスの Open メソッドを呼び出すコールバックが発生します。 このクラスは、ネストされたウィンドウ作成プロセスをカプセル化するラッパーとして書かれています。 これにより、メインウィンドウのViewModel内の不要なプロパティやフィールドが削除されるだけでなく、作成された補助ウィンドウに直接アクセスしてはいけないため、作成された補助ウィンドウに直接アクセスすることができなくなります。 

class SubFormKeeper {     public SubFormKeeper(Func<Window> createWindow, Action<Window> subscribe_events = null, Action<Window> unSubscribe_events = null);     public void Open();     public void Close(); }

クラスのシグネチャを見てみると、パブリックメソッドから可能性のある可能性のセットを正確に提供していることがわかります。 さらに、すべての補助的なオートオプティマイザウィンドウは、この特定のクラスでラップされます。  


最適化結果を扱うためのライブラリの新機能とバグ修正

この記事の一部では、最適化レポートを操作するためのライブラリの変更点 - "ReportManager.dll "について説明します。 カスタム係数の導入に加えて、最適化レポートのターミナルからのアンロードを高速化する機能が追加されました。 また、データソートのエラーも修正されています。    

  • カスタム最適化係数の導入

以前の記事へのコメントにあった改善提案の1つは、最適化結果のフィルタリングにカスタム係数を使用できるようにすることでした。 このオプションを実装するには、既存のオブジェクトにいくつかの変更を加えなければなりませんでした。 それにもかかわらず、古いレポートをサポートするために、最適化データを読み込むクラスは、カスタム係数を持つレポートと、プログラムの以前のバージョンで生成されたレポートの両方で動作することができます。 そのため、報告書の形式に変更はありませんでした。 これには追加のパラメータがあり、カスタム係数を指定するためのフィールドがあります。

"SortBy "列挙に新しいパラメータ "Custom "が追加され、"Coefficients "構造体に適切なフィールドが追加されました。 これは、データの格納を担当するオブジェクトには係数を追加しますが、データをアンロードしたり読み込んだりするオブジェクトには追加しません。 データの書き込みは、MQL5からレポートを保存するために使用する静的メソッドを持つ2つのメソッドと1つのクラスで行います。

public static void AppendMainCoef(double customCoef,                                   double payoff,                                   double profitFactor,                                   double averageProfitFactor,                                   double recoveryFactor,                                   double averageRecoveryFactor,                                   int totalTrades,                                   double pl,                                   double dd,                                   double altmanZScore) {     ReportItem.OptimisationCoefficients.Custom = customCoef;     ... }

まず、AppendMainCoefメソッドにカスタム係数を識別する新しいパラメータが追加されました。 そして、他の渡された係数と同様に、ReportWriter.ReportItem構造体に追加されます。 さて、古いプロジェクトを新しい "ReportManager.dll "ライブラリでコンパイルしようとすると、AppendMainCoefメソッドのシグネチャが変更されているため、例外が発生します。 このエラーは、データをアンロードするオブジェクトを少し編集することで修正することができます。

現在のDLLバージョンで正しいコンパイルを可能にするために、この記事では、インクルードディレクトリ内の "History Manager "を以下に添付された新しいものに置き換えてください - これは、新旧のメソッドを使用してロボットをコンパイルするのに十分です。   

また、Writeメソッドのシグネチャを変更しました。 これは、プログラムが名前付きミューテックスを使用しなくなったために追加されたもので、データのアンロード処理が大幅に遅くなりましたが、古いバージョンのアンロードクラスではレポートの生成に必要だったためです。 ただし、以前に実装されていたデータ書き出し形式との互換性を保つために、ミューテックスを使ってデータを書き込むメソッドは削除していません。 

レポートファイルに新しいレコードを表示するためには、Name属性が "Custom "に等しい新しい<Item/>タグを作成する必要があります。 

WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.Custom.ToString(), new Dictionary<string, string> { { "Name", "Custom" } });

もう一つの修正されたメソッドは、OptimisationResultsExtentions.ReportWriterです:ここに同様の行が追加され、カスタム係数パラメータを持つ<Item/>タグが追加されました。 

では、データとMQLロボットのコードにカスタム係数を追加することを考えてみましょう。 まず、ReportWriterクラスで動作するコードがXmlHistoryWriter.mqhファイルのCXmlHistoryWriterクラスにある、古いバージョンのデータダウンロード機能を考えてみましょう。 カスタム係数をサポートするために、以下のシグネチャへの参照が作成されました。 

typedef double(*TCustomFilter)();

上記のクラスの'private'フィールドには、この関数が格納されています。

class CXmlHistoryWriter   { private:    const string      _path_to_file,_mutex_name;    CReportCreator    _report_manager;    TCustomFilter     custom_filter;    void              append_bot_params(const BotParams  &params[]);//    void              append_main_coef(PL_detales &pl_detales,                                       TotalResult &totalResult);//    //double            get_average_coef(CoefChartType type);    void              insert_day(PLDrawdown &day,ENUM_DAY_OF_WEEK day);//    void              append_days_pl();// public:                      CXmlHistoryWriter(string file_name,string mutex_name,                      CCCM *_comission_manager, TCustomFilter filter);//                      CXmlHistoryWriter(string mutex_name,CCCM *_comission_manager, TCustomFilter filter);                     ~CXmlHistoryWriter(void) {_report_manager.Clear();} //    void              Write(const BotParams &params[],datetime start_test,datetime end_test);//   };

この'private'フィールドの値は、クラスのコンストラクタから埋められます。 さらに、append_main_coefメソッドでは、DLLライブラリから "ReportWriter::AppendMainCoef "スタティック・メソッドを呼び出す際に、渡された関数をそのポインタで呼び出すことで、カスタム係数の値を受け取ることができます。

    3回目の記事で説明したラッパーがあるので、このクラスを直接使うことはありません - それはCAutoUploaderクラスです。

class CAutoUploader   { private:    datetime          From,Till; // Testing start and end dates    CCCM              *comission_manager; // Commission manager    BotParams         params[]; // List of parameters    string            mutexName; // Mutex name    TCustomFilter     custom_filter; public:                      CAutoUploader(CCCM *comission_manager, string mutexName, BotParams &params[],                                    TCustomFilter filter);                      CAutoUploader(CCCM *comission_manager, string mutexName, BotParams &params[]);    virtual          ~CAutoUploader(void);    virtual void      OnTick(); // Calculating testing start and end dates   };

このクラスでは、以前のコンストラクタに加えて、カスタム係数を返す関数へのポインタを取る新しいコンストラクタを追加しました。 これらのコンストラクタはまた、それが格納されている目的の関数への参照を変数に保存します。 
 

double EmptyCustomCoefCallback() {return 0;} //+------------------------------------------------------------------+ //| Constructor                                                       | //+------------------------------------------------------------------+ CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[], TCustomFilter filter) : comission_manager(_comission_manager),    mutexName(_mutexName),    From(0),    Till(0),    custom_filter(filter)   {    CopyBotParams(params,_params);   } //+------------------------------------------------------------------+ //| Constructor                                                       | //+------------------------------------------------------------------+ CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[]) : comission_manager(_comission_manager),    mutexName(_mutexName),    From(0),    Till(0),    custom_filter(EmptyCustomCoefCallback)   {    CopyBotParams(params,_params);   }

"EmptyCustomCoefCallback"関数は、古いバージョンのコンストラクタを保存するために作成されました。 この関数は、カスタム係数としてゼロを返します。 このクラスの前のコンストラクタが呼ばれた場合,正確にこの関数をCXmlHistoryWriterクラスに渡してください. 記事4で使用した例をとると、以下のようにロボットにカスタム係数を追加することができます。

//+------------------------------------------------------------------+ //|                                                     SimpleMA.mq5 | //|                        Copyright 2019, MetaQuotes Software Corp. | //|                                             https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link      "https://www.mql5.com" #property version   "1.00" #include <Trade/Trade.mqh> #include <History manager/AutoLoader.mqh> // Include CAutoUploader #define TESTER_ONLY input int ma_fast = 10; // MA fast input int ma_slow = 50; // MA slow input int _sl_ = 20; // SL input int _tp_ = 60; // TP input double _lot_ = 1; // Lot size // Comission and price shift (Article 2) input double _comission_ = 0; // Comission input int _shift_ = 0; // Shift int ma_fast_handle,ma_slow_handle; const double tick_size = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE); CTrade trade; CAutoUploader * auto_optimiser;// Pointer to CAutoUploader class (Article 3) CCCM _comission_manager_;// Comission manager (Article 2) double CulculateMyCustomCoef() {    return 0; } //+------------------------------------------------------------------+ //| Expert initialization function                                   | //+------------------------------------------------------------------+ int OnInit()   { //--- ...    // Add Instance CAutoUploader class (Article3)    auto_optimiser = new CAutoUploader(&_comission_manager_,"SimpleMAMutex",params,CulculateMyCustomCoef); //---    return(INIT_SUCCEEDED);   }   double OnTester()   {    return(CulculateMyCustomCoef());   } //+------------------------------------------------------------------+

ここでは、あまり多くのコードを提供しないために、カスタム係数の導入に関連する部分を除いて実装を削除しました。 フルコードは添付ファイルにあります。 まず、カスタム係数を計算する関数を作成する必要があります。 上記の例では、関数はゼロを返し、実装はありませんが、カスタム係数の計算はその中に記述する必要があります。 そのため、OnTesterコールバックでは計算は行われません - 代わりに記述された関数が呼び出されます。 CAutoUploaderクラスの作成中、新しいコンストラクタのオーバーロードを呼び出して、その中にカスタム係数を指定するだけです。 これでカスタム係数の追加は完了です。 

  • 新しいデータアップロード形式での最適化パスデータのアップロードの高速化

mutexを除外することを決定したことで、データの読み込み速度が向上しましたが、これだけでは十分ではありません。 以前のバージョンでは、ファイルにデータを追加するために、新しいレコードごとにいくつかの操作を行わなければなりませんでした。  

  1. ファイルの読み込み  
  2. 読み込んだデータをRAMに保存する 
  3. メモリに読み込んだデータに新しい最適化パスを追加します。 
  4. 古いファイルの削除 
  5. 古いファイルの代わりに新しいクリーンなファイルを作成します。 
  6. 作成されたファイルにデータ配列全体を保存します。 

これが、C#標準ライブラリからの使用済みXmlDocumentクラスの標準的な手順です。 この手続きには時間がかかります。 さらに、ファイルが大きくなるにつれて、この操作にかかる時間が増えていきます。 以前のバージョンでは、データを一括して蓄積することができなかったため、この機能を受け入れていたのですが、今回のバージョンでは、データを一括して蓄積することができるようになりました。 その代わりに、各最適化が完了した後のデータを保存しました。 本実施形態では、フレームを用いてデータを蓄積しているため、全てのデータを一度に必要な形式に変換することができます。 これは先に書いた "OptimisationResultsExtentions.ReportWriter "メソッドを使って実装しています。 これは、最適化パスの配列の拡張メソッドです。 ReportWriter.Writeとは異なり、このメソッドはファイルにデータを追加するのではなく、1つのファイルを作成し、そこにすべての最適化パスを1行ずつ書き込んでいきます。 このように、ReportWriter.Writeで書き込んだときに数分かかっていたデータ配列が、数秒で書き込めるようになりました。  

 ReportWriter クラスのラッパーが作成され、MQL5 の OptimisationResultsExtentions.ReportWriter メソッドが使用できるようになりました。 

public class ReportWriter
{
    private static ReportItem ReportItem;
    private static List<OptimisationResult> ReportData = new List<OptimisationResult>();
    public static void AppendToReportData(string symbol, int tf,
                                          ulong StartDT, ulong FinishDT)
    {
        ReportItem.Symbol = symbol;
        ReportItem.TF = tf;
        ReportItem.DateBorders = new DateBorders(StartDT.UnixDTToDT(), FinishDT.UnixDTToDT());

        ReportData.Add(ReportItem);
        ClearReportItem();
    }
    public static void ClearReportItem()
    {
        ReportItem = new ReportItem();
    }
    public static void ClearReportData() { ReportData.Clear(); }
    public static string WriteReportData(string pathToBot, string currency, double balance,
                                         int laverage, string pathToFile)
    {
        try
        {
            ReportData.ReportWriter(pathToBot, currency, balance, laverage, pathToFile);
            ClearReportData();
        }
        catch (Exception e)
        {
            return e.Message;
        }
        ClearReportData();
        return "";
    }
}

ReportWriterクラスでは、ReportDataフィールドを作成していますが、これはReportItem要素のコレクションを格納するので、これは最適化パスのコレクションになります。 最初の記事で紹介したメソッドを使って、MQL5から必要なデータをすべてReportItem構造体に書き込むというものです。 そして、AppendToReportDataメソッドを呼び出すことで、最適化パスのコレクションに追加します。 このようにして、必要なデータ収集はC#側で形成されます。 すべての最適化パスがコレクションに追加されたら、WriteReportDataメソッドを呼び出して、OptimisationResultsExtentions.ReportWriterメソッドを使用して最適化レポートを素早く作成します。

  • バグとり

残念ながら、前のプログラムのバージョンでエラーを起こしてしまい、気づくのがかなり遅くなってしまいました。 このエラーは、最初の記事で説明した最適化ソートの仕組みに関連しています。 データの並び替えは、いくつかの基準に応じて行うことができるので、これらの基準のうち、どれを最大化し、どれを最小化するかを決定する必要がある。 例えば、負けトレードの数を最大化する人はいないでしょう。 

混乱を避けるために、最適化ソートは少し違う方向の意味を持っています。 

  • 降順 - 最高のパラメータから最悪のパラメータまで
  • 昇順 - 最悪のパラメータから最高のパラメータへ

データソートメソッドが、どの基準を最大化すべきか、どの基準を最小化すべきかを判断できるようにするために、適切な変数を返す別のメソッドが作成されています。 前回のメソッドの実装は以下の通りです。 

private static SortMethod GetSortMethod(SortBy sortBy) {     switch (sortBy)     {         case SortBy.Payoff: return SortMethod.Increasing;         case SortBy.ProfitFactor: return SortMethod.Increasing;         case SortBy.AverageProfitFactor: return SortMethod.Increasing;         case SortBy.RecoveryFactor: return SortMethod.Increasing;         case SortBy.AverageRecoveryFactor: return SortMethod.Increasing;         case SortBy.PL: return SortMethod.Increasing;         case SortBy.DD: return SortMethod.Decreasing;         case SortBy.AltmanZScore: return SortMethod.Decreasing;         case SortBy.TotalTrades: return SortMethod.Increasing;         case SortBy.Q_90: return SortMethod.Decreasing;         case SortBy.Q_95: return SortMethod.Decreasing;         case SortBy.Q_99: return SortMethod.Decreasing;         case SortBy.Mx: return SortMethod.Increasing;         case SortBy.Std: return SortMethod.Decreasing;         case SortBy.MaxProfit: return SortMethod.Increasing;         case SortBy.MaxDD: return SortMethod.Decreasing;         case SortBy.MaxProfitTotalTrades: return SortMethod.Increasing;         case SortBy.MaxDDTotalTrades: return SortMethod.Decreasing;         case SortBy.MaxProfitConsecutivesTrades: return SortMethod.Increasing;         case SortBy.MaxDDConsecutivesTrades: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Mn: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Mn: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Mn: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Mn: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Tu: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Tu: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Tu: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Tu: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_We: return SortMethod.Increasing;         case SortBy.AverageDailyDD_We: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_We: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_We: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Th: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Th: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Th: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Th: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Fr: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Fr: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Fr: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Fr: return SortMethod.Decreasing;         default: throw new ArgumentException($"Unaxpected Sortby variable {sortBy}");     } }

現在の実施形態は以下の通りです。

private static OrderBy GetSortingDirection(SortBy sortBy) {     switch (sortBy)     {         case SortBy.Custom: return OrderBy.Ascending;         case SortBy.Payoff: return OrderBy.Ascending;         case SortBy.ProfitFactor: return OrderBy.Ascending;        case SortBy.AverageProfitFactor: return OrderBy.Ascending;         case SortBy.RecoveryFactor: return OrderBy.Ascending;         case SortBy.AverageRecoveryFactor: return Or-derBy.Ascending;         case SortBy.PL: return OrderBy.Ascending;         case SortBy.DD: return OrderBy.Ascending;         case SortBy.AltmanZScore: return OrderBy.Descending;         case SortBy.TotalTrades: return OrderBy.Ascending;         case SortBy.Q_90: return OrderBy.Ascending;         case SortBy.Q_95: return OrderBy.Ascending;         case SortBy.Q_99: return OrderBy.Ascending;         case SortBy.Mx: return OrderBy.Ascending;         case SortBy.Std: return OrderBy.Descending;         case SortBy.MaxProfit: return OrderBy.Ascending;         case SortBy.MaxDD: return OrderBy.Ascending;         case SortBy.MaxProfitTotalTrades: return OrderBy.Ascending;         case SortBy.MaxDDTotalTrades: return OrderBy.Descending;         case SortBy.MaxProfitConsecutivesTrades: return OrderBy.Ascending;         case SortBy.MaxDDConsecutivesTrades: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Mn: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Mn: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Mn: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Mn: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Tu: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Tu: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Tu: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Tu: return OrderBy.Descending;         case SortBy.AverageDailyProfit_We: return OrderBy.Ascending;         case SortBy.AverageDailyDD_We: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_We: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_We: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Th: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Th: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Th: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Th: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Fr: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Fr: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Fr: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Fr: return OrderBy.Descending;         default: throw new ArgumentException($"Unaxpected Sortby variable {sortBy}");     } }

コードを見るとわかるように、選択された係数のソート方向が変更されています。 以前の実装では、降順にソートされたものとしてマークされていましたが、今回の実装では、降順にソートされたものとしてマークされました。 しかし、これらのデータが負の値を持っていることや、降順ではなく昇順でソートすることを考慮していませんでした。 このロジックを理解するには、渡された値ごとにソートを実装している以下のコードを見てください。  

// 最小値がゼロ以下の場合、全てのデータを負の最小値だけシフトします。
if (mm.Min < 0) {     value += Math.Abs(mm.Min);     mm.Max += Math.Abs(mm.Min); } // If the maximum is greater than zero, calculate if (mm.Max > 0) {     // Calculate the coefficient according to the sorting method     if (GetSortingDirection(item.Key) == OrderBy.Descending)     {         // Calculate the coefficient to sort in descending order         data.SortBy += (1 - value / mm.Max) * coef;     }     else     {         // Calculate the coefficient to sort in ascending order         data.SortBy += value / mm.Max * coef;     } }

'Value'は、ある係数の数値です。 データをソートする前に、ソートに選択された係数の配列から最小値が負になっているかどうかを確認します。 そうであれば、これらの値を最小係数の値だけ上にずらして正面に変換する。 したがって、[0 ; (Max + |Min|)]の範囲の値の配列ができあがります。 最終的なソートを実行する結果の係数を計算する場合、各 i 番目の値をソートデータ配列の最大値で除算することで、データ配列を範囲 [0; 1] にシフトします。ソート方法が降順の場合は、結果の値を 1 から減算し、結果の重みの配列を反転します。 それが、以前のデータソートのバージョンが正しくない理由です:実装された多因子ソートロジックのために、我々は単に上記のコードでマークされた係数のために必要とされていない重みの配列を反転させました。 前記のソート手段は、最初の記事においてさらに詳細に記載されています。 便宜上、メソッド名と戻り値の型はより適切なものに変更されていますが、これはアプリケーションのロジックには何ら影響を与えません。  

2つ目のエラーは、ソート基準が1つしか選択されていない場合に、最適化結果の配列をソートするコードの部分でした。 前回の実施形態は以下の通りでした。

if (order == OrderBy.Ascending)     return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0))); else     return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));

現在のものは下記のような形です。

if (order == GetSortingDirection(sortingFlags.ElementAt(0)))     return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0))); else     return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));

以前のバージョンでは、GetSortingDirectionメソッドで指定された方向を考慮していませんでした。 新しいのは、この基準でソートします。 例えば、降順ソート(最高の結果が一番上に来る)を選択した場合、SortBy.PLでは、要求された降順ソートが実行され、最高の値が一番上に来るようになります。 ただし、SortBy.MaxDDTotalTradesパラメータ(不採算案件の総数)では、最小値がトップになり、配列は昇順にソートされます。 これは論理構造を保持します。 例えば、基準としてSortBy.MaxDDTotalTradesを選択した場合、以前のソートロジックでは最悪のパスを受けていたことになります。 

ロボットパラメータの自動ロードと新しいエキスパートアドバイザーによるルール作成

新しいパラメータロードロジックは、"AutoUploader2.mqh "ファイルで提供されています。 続いて、第4回で紹介したExpert Advisorをベースにした例を挙げて、メカニズムの説明を行います。 

class CAutoUploader2   { private:                      CAutoUploader2() {}    static CCCM       comission_manager;    static datetime   From,Till;    static TCustomFilter on_tester;    static TCallback on_tick,           on_tester_deinit;    static TOnTesterInit on_tester_init;    static string     frame_name;    static long       frame_id;    static string     file_name;    static bool       FillInData(Data &data);    static void       UploadData(const Data &data, double custom_coef, const BotParams &params[]); public:    static void       OnTick();    static double     OnTester();    static int        OnTesterInit();    static void       OnTesterDeinit();    static void       SetUploadingFileName(string name);    static void       SetCallback(TCallback callback, ENUM_CALLBACK_TYPE type);    static void       SetCustomCoefCallback(TCustomFilter custom_filter_callback);    static void       SetOnTesterInit(TOnTesterInit on_tester_init_callback);    static void       AddComission(string symbol,double comission,double shift);    static double     GetComission(string symbol,double price,double volume);    static void       RemoveComission(string symbol);   }; datetime CAutoUploader2::From = 0; datetime CAutoUploader2::Till = 0; TCustomFilter CAutoUploader2:: EmptyCustomCoefCallback; TCallback CAutoUploader2:: EmptyCallback; TOnTesterInit CAutoUploader2:: EmptyOnTesterInit; TCallback CAutoUploader2:: EmptyCallback; CCCM CAutoUploader2::comission_manager; string CAutoUploader2::frame_name = "AutoOptomiserFrame"; long CAutoUploader2::frame_id = 1; string CAutoUploader2::file_name = MQLInfoString(MQL_PROGRAM_NAME)+"_Report.xml";  

新しいクラスには静的メソッドしかありません。 これにより、インスタンス化する必要がなくなり、不要なコードを削除することでEAの開発プロセスを簡素化することができます。 このクラスには、以前使用したクラスと同様の日付範囲を含む多くの静的フィールドがあり、詳細は第3の記事を読んでください。完了コールバックのテストのための関数リファレンス、最適化フレームと新しいティック到着コールバック、コミッションマネージャクラス(詳細は#2記事を参照してください)、フレーム名とID、最適化結果をダウンロードするためのファイルの名前です。     

オートオプティマイザーを接続するには、いくつかのコールバックが既に定義されているファイルへのリンクをEAに追加します。 EAがこのファイルで定義されているコールバックを使用している場合、最も簡単な解決策は、使用されているコールバックのシグネチャとその実装を持つ関数を作成し、特別な静的関数を使用してコールバックの関数参照に追加します。

#ifndef CUSTOM_ON_TESTER double OnTester() { return CAutoUploader2::OnTester(); } #endif #ifndef CUSTOM_ON_TESTER_INIT int OnTesterInit() { return CAutoUploader2::OnTesterInit(); } #endif #ifndef CUSTOM_ON_TESTER_DEINIT void OnTesterDeinit() { CAutoUploader2::OnTesterDeinit(); } #endif #ifndef CUSTOM_ON_TICK void OnTick() { CAutoUploader2::OnTick(); } #endif

特定のコールバックのそれぞれは、プリプロセッサ条件でラップされており、適切なプリプロセッサ条件を定義することで、このファイルでの定義を避けることができます。 実装の詳細については、さらに例を挙げて説明します。 

これらのコールバックを自分で記述する場合は、CAutoUploader2クラスの静的メソッドを(このコードスニペットで行ったように)定義されたコールバックの最初にコールすることを忘れないでください。 これは、レポート生成機構を正しく動作させるために必要です。 

データダウンロードのカスタムコールバックを有効にするには(独自のコールバックを実装していない場合)、実装の説明を含む関数のポインタを CAutoUploader2::SetCustomCoefCallback スタティックメソッドに渡します。 手数料を管理するには、以下のいずれかの方法を使用してください。 

static void       AddComission(string symbol,double comission,double shift); static double     GetComission(string symbol,double price,double volume); static void       RemoveComission(string symbol);

これが全ての機能です。 では、その仕組みを見てみましょう。

int CAutoUploader2::OnTesterInit(void) { return on_tester_init(); }

Expert Advisor は OnTesterInit コールバックで CAutoUploader2::OnTesterInit メソッド(最適化が開始されている場合)を呼び出し、渡された関数ポインタを呼び出すか、デフォルトで置換されている場合は空の関数を呼び出します。

void CAutoUploader2::OnTick(void)   {    if(MQLInfoInteger(MQL_OPTIMIZATION)==1 ||       MQLInfoInteger(MQL_TESTER)==1)      {       if(From == 0)          From = iTime(_Symbol,PERIOD_M1,0);       Till=iTime(_Symbol,PERIOD_M1,0);      }    on_tick();   }

そして、ティックごとに、最適化開始の実時間が対応する変数に保存されます。 そして、EAは、新規ティック到着コールバックとして送られてきたon_tickメソッド、またはデフォルトの空のコールバックを呼び出します。 最適化時間の保存は、テスターでEAが動いている場合に限ります。 

double CAutoUploader2::OnTester(void)   {    double ret = on_tester();    Data data[1];    if(!FillInData(data[0]))       return ret;    if(MQLInfoInteger(MQL_OPTIMIZATION)==1)      {       if(!FrameAdd(frame_name, frame_id, ret, data))          Print(GetLastError());      }    else       if(MQLInfoInteger(MQL_TESTER)==1)         {          BotParams params[];          UploadData(data[0], ret, params, false);         }    return ret;   }

テスター操作が完了すると、CAutoUploader2::OnTester staticメソッドがOnTesterコールバックで呼び出され、ここではフレームが保存されます(最適化の場合)、またはフレームがファイルに書き込まれます (テストの場合)。 テストの場合は、現在のステップで処理を終了し、設定ファイルに渡されたコマンドでターミナルを閉じます。 ただし、最適化処理であれば、次のような最終段階を前もって行います。

input bool close_terminal_after_finishing_optimisation = false; // MetaTrader Auto Optimiser param (must be false if you run it  from terminal) void CAutoUploader2::OnTesterDeinit(void)   {    ResetLastError();    if(FrameFilter(frame_name,frame_id))      {       ulong pass;       string name;       long id;       double coef_value;       Data data[];       while(FrameNext(pass,name,id,coef_value,data))         {          string parameters_list[];          uint params_count;          BotParams params[];          if(FrameInputs(pass,parameters_list,params_count))            {             for(uint i=0; i<params_count; i++)               {                string arr[];                StringSplit(parameters_list[i],'=',arr);                BotParams item;                item.name = arr[0];                item.value = arr[1];                ADD_TO_ARR(params,item);               }            }          else             Print("Can`t get params");          UploadData(data[0], coef_value, params, true);         }       CheckRetMessage(ReportWriter::WriteReportData(get_path_to_expert(),                       CharArrayToString(data[0].currency),                       data[0].balance,                       data[0].laverage,                       TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name));      }    else      {       Print("Can`t select apropriate frames. Error code = " + IntegerToString(GetLastError()));       ResetLastError();      }    on_tester_deinit();    if(close_terminal_after_finishing_optimisation)      {       if(!TerminalClose(0))         {          Print("===================================");          Print("Can`t close terminal from OnTesterDeinit error number: " +                IntegerToString(GetLastError()) +                " Close it by hands");          Print("===================================");         }      }    ExpertRemove();   }

最後の最適化ステップは、CAutoUploader2::OnTesterDeinit()スタティックメソッドの呼び出しです。 この方法では、保存されたすべてのフレームが読み込まれ、最適化レポートを含む最終ファイルが形成されます。 まず、前回のエラーをリセットして、フレームを名前とidでフィルタリングします。 そして、各フレームをループで読み取り、その保存されたデータを取得し、ファイルに書き込まれます。

データを読み取った後、この最適化パスのEA の入力パラメーターを読み取り、受信した情報を C# 側の静的クラスのコレクションに追加します。ループを終了した後、フォームコレクションをファイルに書き込みます。 そして、渡されたカスタムコールバックまたはデフォルトの空の参照が呼び出されます。 このアプローチには 1 つの問題があります: 自動最適化装置が動作するためには、ターミナルを再起動できなければならず、そのためにはまずターミナルをシャットダウンしなければなりません。

以前は、この問題を解決するために、設定ファイルのフラグをtrueに設定していました。 ただし、フレームを操作する場合、最適化の停止後に最終処理が開始され、構成ファイルの必須フラグが true に設定されている場合、OnTerderDeinit メソッドが完了する前にターミナルがシャットダウンされるため、フレームを処理できません。 問題を解決するために、インクルードファイルと一緒にExpert Advisorに追加される入力変数を追加してみました。 この変数はオートオプティマイザーから変更され、手動でもコード内でも変更してはいけません。 trueに設定されている場合は、MQL5からターミナル終了メソッドが呼び出され、そうでない場合はターミナルは終了しません。 全ての状況を説明した後、フレームを処理するEAはチャートから削除されます。 

UploadData メソッドは、コレクションにデータを追加するメソッドとして、また、最適化ではなくテストである場合は、特定のテスターパスをファイルにダウンロードするメソッドとして動作します。 

void CAutoUploader2::UploadData(const Data &data, double custom_coef, const BotParams &params[], bool is_appent_to_collection)   {    int total = ArraySize(params);    for(int i=0; i<total; i++)       ReportWriter::AppendBotParam(params[i].name,params[i].value);    ReportWriter::AppendMainCoef(custom_coef,data.payoff,data.profitFactor,data.averageProfitFactor,                                 data.recoveryFactor,data.averageRecoveryFactor,data.totalTrades,                                 data.pl,data.dd,data.altmanZScore);    ReportWriter::AppendVaR(data.var_90,data.var_95,data.var_99,data.mx,data.std);    ReportWriter::AppendMaxPLDD(data.max_profit,data.max_dd,                                data.totalProfitTrades,data.totalLooseTrades,                                data.consecutiveWins,data.consequtiveLoose);    ReportWriter::AppendDay(MONDAY,data.averagePl_mn,data.averageDd_mn,                            data.numberProfitTrades_mn,data.numberLooseTrades_mn);    ReportWriter::AppendDay(TUESDAY,data.averagePl_tu,data.averageDd_tu,                            data.numberProfitTrades_tu,data.numberLooseTrades_tu);    ReportWriter::AppendDay(WEDNESDAY,data.averagePl_we,data.averageDd_we,                            data.numberProfitTrades_we,data.numberLooseTrades_we);    ReportWriter::AppendDay(THURSDAY,data.averagePl_th,data.averageDd_th,                            data.numberProfitTrades_th,data.numberLooseTrades_th);    ReportWriter::AppendDay(FRIDAY,data.averagePl_fr,data.averageDd_fr,                            data.numberProfitTrades_fr,data.numberLooseTrades_fr);    if(is_appent_to_collection)      {       ReportWriter::AppendToReportData(_Symbol,                                        data.tf,                                        data.startDT,                                        data.finishDT);       return;      }    CheckRetMessage(ReportWriter::Write(get_path_to_expert(),                                        CharArrayToString(data.currency),                                        data.balance,                                        data.laverage,                                        TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name,                                        _Symbol,                                        data.tf,                                        data.startDT,                                        data.finishDT));   }

is_appent_to_collection フラグが真の場合、パスは単にコレクションに追加されます。 上のコードからわかるように、フレームを読み込んでコレクションに追加してレポートを素早くダウンロードするときにのみ、フラグがtrueになるようになっています。 Expert Advisorをテストモードで実行した場合、このメソッドは'false'パラメータを指定して呼び出されます。  

では、新しいロジックを使って、最適化レポートをダウンロードするためのリンクを追加する方法を見てみましょう。 前回作成したファイルを、第4回の記事からテストエキスパートアドバイザーで考えてみましょう。 新しいメソッドの接続は(インクルードファイルへの参照を除いて)、記事4で使用されている例の16行の代わりに3行だけのコードを取ります。 データダウンロードに使用されるコールバックについては、現在のEAでは "OnTick "コールバックが実装されていますが、他のコールバック("OnTester"、"OnTesterInit"、"OnTesterDeinit")はインクルードファイルに実装されています。 

//+------------------------------------------------------------------+ //|                                                     SimpleMA.mq5 | //|                        Copyright 2019, MetaQuotes Software Corp. | //|                                             https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link      "https://www.mql5.com" #property version   "1.00" #include <Trade/Trade.mqh> #define CUSTOM_ON_TICK // Tell to uploading system that we implement OnTick callback ourself #include <History manager/AutoUpLoader2.mqh> // Include CAutoUploader #define TESTER_ONLY input int ma_fast = 10; // MA fast input int ma_slow = 50; // MA slow input int _sl_ = 20; // SL input int _tp_ = 60; // TP input double _lot_ = 1; // Lot size // Comission and price shift (Article 2) input double _comission_ = 0; // Comission input int _shift_ = 0; // Shift int ma_fast_handle,ma_slow_handle; const double tick_size = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE); CTrade trade; //+------------------------------------------------------------------+ //| Custom coeffifient`s creator                                     | //+------------------------------------------------------------------+ double CulculateMyCustomCoef()   {    return 0;   } //+------------------------------------------------------------------+ //| Expert initialization function                                   | //+------------------------------------------------------------------+ int OnInit()   { //--- ...    CAutoUploader2::SetCustomCoefCallback(CulculateMyCustomCoef);    CAutoUploader2::AddComission(_Symbol,_comission_,_shift_); //---    return(INIT_SUCCEEDED);   } //+------------------------------------------------------------------+ //| Expert tick function                                             | //+------------------------------------------------------------------+ void OnTick()   {    CAutoUploader2::OnTick(); // If CUSTOM_ON_TICK was defined    ...   } //+------------------------------------------------------------------+

は、新方式によるデータダウンロードインターフェースの追加を示しています。 例を見てもわかるように、OnTesterコールバックはデータダウンロードファイルに実装されたままです。 カスタム係数の計算を可能にするために、パスCulculateMyCustomCoefメソッドに渡しました。 OnTickコールバックの実装はロボットに残っています。 このため、データダウンロードの手順が記述されているファイルへの参照の前に、CUSTOM_ON_TICK変数が定義されています。 添付ファイルを使用して、EAの実装をより詳細に検討したり、デフォルトの実装と比較したり、以前のデータダウンロード方法の実装と比較したりすることができます。 

最適化起動方法の変更などの改善

新バージョンでは他にも多くの改良が加えられています。 その一つが、複数の資産に対して最適化をスケジュールする機能です。

  • 合格した資産リストの最適化スケジュール

 

この改善により、多くの資産に対して最適化を行うことで、時間を節約することができます。 スケジュールされたタスクは、指定されたリストが終了するまで昼夜を問わず実行されます。 この機能を有効にするには、以前の記事で紹介した最適化の起動処理を変更する必要がありました。 これまでは、「開始/停止」ボタンを押した後、ViewModel は即座にタスクをデータモデルメソッドにリダイレクトし、最適化の開始から結果の保存までをフルサイクルで呼び出すようになっていましたが、「開始/停止」ボタンを押した後は、タスクをデータモデルメソッドにリダイレクトしていました。 さて、最初にメソッドを呼び出して、渡されたパラメータのリストをループし、最適化を開始して適切なディレクトリに保存します。    

public async void StartOptimisation(OptimiserInputData optimiserInputData, bool isAppend, string dirPrefix, List<string> assets) {     if (assets.Count == 0)     {         ThrowException("Fill in asset name");         OnPropertyChanged("ResumeEnablingTogle");         return;     }     await Task.Run(() =>     {         try         {             if (optimiserInputData.OptimisationMode == ENUM_OptimisationMode.Disabled &&                assets.Count > 1)             {                 throw new Exception("For test there must be selected only one asset");             }             StopOptimisationTougle = false;             bool doWhile()             {                 if (assets.Count == 0 || StopOptimisationTougle)                     return false;                 optimiserInputData.Symb = assets.First();                 LoadingOptimisationTougle = assets.Count == 1;                 assets.Remove(assets.First());                 return true;             }             while (doWhile())             {                 var data = optimiserInputData; // Copy input data                 StartOptimisation(data, isAppend, dirPrefix);             }         }         catch (Exception e)         {             LoadingOptimisationTougle = true;             OnPropertyChanged("ResumeEnablingTogle");м             ThrowException?.Invoke(e.Message);         }     }); }

資産のリストを渡してその完全性を確認した後、このメソッドの非同期部分に移動します。 ループの中で、以前に検討した最適化の起動メソッドを呼び出します。 最適化パラメータに渡される構造体はオプティマイザクラスで変更される可能性があるため、新しい最適化を開始する前にコピーし、初期データを新しい最適化のたびにフィードします。

最適化が実行されるアセットの置換と同様に、継続の条件は、入れ子になった doWhile() 関数によって実行されます。 関数本体では、ループ終了条件を確認し、次のアセットの値を割り当て最後に割り当てられたアセットをリストから削除します。 このように、新しいループの繰り返しのたびに、まず、最適化を実行するアセットを示してから、リストが空になるまで、または最適化の完了信号が送信されるまで、最適化を実行します。 以前の実装では、実行中のプロセスを終了させるだけで、最適化プロセスを緊急に終了させることができました。 しかし、現在の実装では、処理が停止するのではなく、次の繰り返しに切り替わってしまいます。 そのため、最適化終了方法に適切な調整が行われました。 

/// <summary> /// Complete optimization from outside the optimizer /// </summary> public void StopOptimisation() {     StopOptimisationTougle = true;     LoadingOptimisationTougle = true;     Optimiser.Stop();     var processes = System.Diagnostics.Process.GetProcesses().Where(x => x.ProcessName == "metatester64");     foreach (var item in processes)         item.Kill(); } bool StopOptimisationTougle = false;

これで、最適化を停止するときは、単純にこのフラグをtrue切り替えます。 アセットループはフラグを見て、実行中の反復を終了します。 さらに、最適化停止プロセスをオプティマイザクラスにリダイレクトした後、実行中のテスタープロセスを終了させなければなりません。 

この目的のために、追加フラグ LoadingOptimisationTougle が使用されます。 このフラグは、以前に実装されていたように、現在実行されている最適化をグラフィカル・インターフェースにロードするかどうかを示します。 プロセスを高速化するために、このフラグは、プロセスが強制的に停止されるまで、または渡された資産のリストの最後の項目に到達するまで、常に "false "です。 そして、最適化プロセスを終了した後にのみ、データがグラフィカルなインターフェイスにロードされます。 

  • 最適化の起動パラメータで設定ファイルを保存し、読み込まれた最適化のメモリをクリアする

最適化を繰り返し実行する際に、新たなパスを以前のパスに追加する機能は、プログラムの最初のバージョンから提供されています。 しかし、スムーズな画像を確保するためには、同じパラメータで新しい最適化を起動する必要があります。 このために、以前に実行した最適化のパラメータを、最適化結果が保存されている同じディレクトリに保存するオプションを導入しました。 GUIに別のボタンが追加され、新しい設定のためにこれらの設定をアップロードできるようになりました。   

AutoOptimiserVMクラスの以下のメソッドは、ボタンをクリックした後にトリガーします。

private void SetBotParams()
{
    if (string.IsNullOrEmpty(SelectedOptimisation))
        return;

    try
    {
        Status = "Filling bot params";
        OnPropertyChanged("Status");
        Progress = 100;
        OnPropertyChanged("Progress");

        var botParams = model.GetBotParamsFromOptimisationPass(OptimiserSettings.First(x => x.Name == "Available experts").SelectedParam,
                                                                       SelectedOptimisation);
        for (int i = 0; i < BotParams.Count; i++)
        {
            if (!botParams.Any(x => x.Variable == BotParams[i].Vriable))
                continue;

            BotParams[i] = new BotParamsData(botParams.First(x => x.Variable == BotParams[i].Vriable));
        }
    }
    catch (Exception e)
    {
        MessageBox.Show(e.Message);
    }

    Status = null;
    OnPropertyChanged("Status");
    Progress = 0;
    OnPropertyChanged("Progress")
}

まず、データモデルからEAパラメータのリストを要求します。 そして、GUIに読み込まれたすべてのパラメータをループスルーし、受信したパラメータのリストにパラメータがあるかどうかをチェックします。 パラメータが見つかった場合、新しい値に置き換えられます。 設定ファイルの正しいパラメータを返すデータモデルmethodは、ComboBoxで選択されたディレクトリから、ファイルが "OptimisationSettings.set "という名前で保存されているディレクトリからそれを読み込みます。 このファイルは、このプロセスが完了すると、最適化を開始するメソッドによって生成されます。 

  • 以前にロードされた最適化パスをメモリから削除する

また、最適化パスをロードした後にクリアするオプションが追加されました。 RAMの占有容量が多すぎます。 コンピュータが低価格レートのRAMを搭載している場合、複数のフォワードテストやヒストリカルテストを行うと、顕著に遅くなることがあります。 リソースの使用量を最小限に抑えるために、フォワード最適化パスと過去の最適化パスのデータの重複を削除しました。 今ではデータモデルにのみ保存されています。 データモデルからClearResultsメソッドを参照する専用ボタン「Clear loaded results」がGUIに追加されました。 

void ClearOptimisationFields()
{
    if (HistoryOptimisations.Count > 0)
        dispatcher.Invoke(() => HistoryOptimisations.Clear());
    if (ForwardOptimisations.Count > 0)
        dispatcher.Invoke(() => ForwardOptimisations.Clear());
    if (AllOptimisationResults.AllOptimisationResults.Count > 0)
    {
        AllOptimisationResults.AllOptimisationResults.Clear();
        AllOptimisationResults = new ReportData
        {
            AllOptimisationResults = new Dictionary<DateBorders, List<OptimisationResult>>()
        };
    }

    GC.Collect();
}
public void ClearResults()
{
    ClearOptimisationFields();
    OnPropertyChanged("AllOptimisationResults");
    OnPropertyChanged("ClearResults");
}

前述のメソッドは、AutoOptimiserMクラスの最適化レポートのコレクションを空にするClearOptimisationFieldsプライベート・メソッドを参照しています。 しかし、メモリを手動ではなく自動で管理するC#を使用しているため、配列のクリアやメモリからのデータ削除を適用するためには、削除したオブジェクトからメモリをクリアする必要もあります。 これは、Garbage Collector (GC) クラスの静的 Collect メソッドを呼び出すことで行うことができます。 実行後、それまでに存在していたオブジェクトはRAMから消去されます。

  • 目的のパスの*.setファイルを生成します。

生成された最適化パスを表示した後、必要なパラメータをExpert Advisorに入力するための*setファイルを生成する必要があります。 以前は、見つかったパラメータを手動で入力するか、選択した最適化ラインをダブルクリックしてテストを起動し、テスターからファイルを作成しなければなりませんでした。

ボタンの上のテーブルには、最適化パラメータを格納するキー値リストがあります。 ボタンをクリックすることで、データモデルからメソッドを呼び出し、上の表のリストが渡されます。

public void SaveBotParams(IEnumerable<KeyValuePair<string, string>> data, string path)
{
    SetFileManager setFileManager = new SetFileManager(path, true)
    {
        Params = data.Select(x => new ParamsItem { Variable = x.Key, Value = x.Value }).ToList()
    };

    setFileManager.SaveParams();
}

このメソッドは、作成されたファイルのパスを受け取り、キー-バリュー配列をEAパラメータを持つ構造体に変換し指定されたパスに保存します。 ファイルへのパスは ViewModel の標準ファイル保存インターフェイスを介して設定されます。


結論

記事を出したのは予定よりもだいぶ遅れてしまいましたが、この企画をフォローしてくださった皆様には興味深く、お役に立てればと思っています。 今後も開発を続け、さらなる改善案を実行していきたいと思います。 その一つは、最適化の自動フィルタリングを追加し、ウォークフォワード最適化の遡及結果に基づく最適なパラメータの検索を実装し、実行された最適化のポートフォリオを収集できるようにすることです。 読者の皆様、このプロジェクトが興味を持っていただけたならば、今後も展開を続けていきたいと思います。 現在の状態のプロジェクトは使用可能な状態になっており、プロセッサに100%負荷をかけることができ、ウォークフォワードと通常のヒストリカル最適化の両方を使用して、複数の資産を一度に使用して最適化することができます。


添付ファイルには、記事4で考慮されたテストExpert Advisorを持つ完全な自動最適化プロジェクトが含まれています。 EAを使いたい場合は、自動最適化プロジェクトとテストロボットをコンパイルします。 その後、ReportManager.dll(最初の記事で説明しました)をMQL5/Librariesディレクトリにコピーして、EAのテストを開始します。 自動最適化機能をエキスパートアドバイザーに接続する方法の詳細については、このシリーズの中の記事3と記事4を参照してください。

ここでは、Visual Studioを使ったことがない方のために、コンパイルの手順を説明します。 プロジェクトはVisualStudioで様々な方法でコンパイルすることができますが、ここではそのうちの3つを紹介します。

  1. 一番簡単なのは、CTRL+SHIFT+Bを押すことです。
  2. エディタで緑の配列をクリックすると、コードデバッグモードでアプリケーションが起動し、コンパイルが実行されます(デバッグコンパイルモードが選択されている場合)。
  3. もう一つのオプションは、メニューからBuildコマンドを使用することです。

    コンパイルされたプログラムは、MetaTrader Auto Optimiser/bin/Debug(またはMetaTrader Auto Optimiser/bin/Release - 選択されたコンパイル方法に応じて)フォルダに依存します。



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

    添付されたファイル |
    Data.zip (142.39 KB)
    ニューラルネットワークが簡単に(第3回): コンボリューションネットワーク ニューラルネットワークが簡単に(第3回): コンボリューションネットワーク
    ニューラルネットワークの話題の続きとして、畳み込み型ニューラルネットワークの考察を提案します。 この種のニューラルネットワークは、通常、視覚的なイメージの分析に適用されます。 本稿では、これらのネットワークの金融市場への応用について考察します。
    外国為替取引の背後にある基本的な数学 外国為替取引の背後にある基本的な数学
    この記事は、外国為替取引の主な機能をできるだけ簡単かつ迅速に説明し、初心者といくつかの基本的なアイデアを共有することを目的としています。また、簡単なインディケータ―の開発を紹介するとともに、取引コミュニティで最も興味をそそる質問への回答を試みます。
    ニューラルネットワークが簡単に(第4回): リカレントネットワーク ニューラルネットワークが簡単に(第4回): リカレントネットワーク
    これまでニューラルネットワークの勉強を続けてきました。 この記事では、ニューラルネットワークのもう一つのタイプであるリカレントネットワークについて考えてみます。 このタイプは、MetaTrader 5の取引プラットフォームで価格チャートで表現される時系列を使用するために提案されています。
    PythonやRの知識が不要なYandexのCatBoost機械学習アルゴリズム PythonやRの知識が不要なYandexのCatBoost機械学習アルゴリズム
    この記事では、具体的な例を用いて、機械学習プロセスのコードと主要な段階の説明をします。 このモデルを取得するためには、PythonやRの知識は必要ありません。 さらに、MQL5の基本的な知識があれば十分です - まさに私のレベルです。 したがって、この記事が、機械学習の評価やプログラムへの実装に興味のある人たちの手助けとなり、幅広い人たちの良いチュートリアルとなることを期待しています。