English Русский 中文 Español Deutsch Português
preview
連続的なウォークフォワード最適化(その6):オートオプティマイザの論理部分と構造

連続的なウォークフォワード最適化(その6):オートオプティマイザの論理部分と構造

MetaTrader 5テスター | 3 8月 2020, 09:01
1 194 0
Andrey Azatskiy
Andrey Azatskiy

イントロダクション

引き続き、連続的なウォークフォワード最適化を実装したオートオプティマイザの作成について説明します。 前回の記事では、出来上がったアプリケーションのグラフィカルインターフェースを解析しましたが、論理的な部分や内部構造については考慮していませんでした。 この記事ではこのようなことが書かれています。 このシリーズの中の過去の記事です。

  1. 連続的なウォークフォワード最適化(その1)。最適化レポートを使ったタスク
  2. 連続的なウォークフォワード最適化(その2)。任意のロボットの最適化レポートを作成する仕組み
  3. 連続的なウォークフォワード最適化(その3)。ロボットの自動最適化への適応
  4. 連続的なウォークフォワード最適化(その4)。最適化マネージャ(オートオプティマイザ)
  5. 連続的なウォークフォワード最適化(その5)。オートオプティマイザプロジェクトの概要とGUIの作成

アプリケーションの内部構造や、操作時にアプリケーションが行う呼び出しについて、UML図を使って説明します。 図の目的は、主要なオブジェクトとその間の関係を模式的に示すことであり、既存のすべてのオブジェクトを記述することではないことに注意してください。

内部アプリケーションの構造とその説明、キーオブジェクトの生成

前回の記事で述べたように、結果として得られるプログラムで使用する主なパターンはMVVMです。このパターンによると、プログラム全体のロジックはデータモデルクラスに実装されており、ViewModelオブジェクトの役割を実装する別のクラスを介してグラフィックスと接続されています。 プログラムのロジックは、さらに素体であるクラスに分かれています。 そのロジックとログインとUIの関係を記述した主要なプログラム・エンティティは、以下のクラスのUML図に示されています。    


図を見る前に、異なるオブジェクトタイプに使用されている色表示を見てみましょう。 はグラフィックレイヤーに使用します。 XAMLマークアップを表現するオブジェクトで、すべてのWPFメカニズムが隠されており、エンドユーザーからも開発者からも見えないようになっています。 は、アプリケーションのグラフィックとロジックをつなぐレイヤーに使用します。 つまり、使用されているMVVMモデルからのViewModel層です。 ピンクは、その背後に隠されたデータを抽象的に表現したインターフェースを示すために使用されています。

そのうちの最初の(IMainModel)は、データモデルの特定の実装を隠します。 MVVMパターンの主な考え方によると、データモデルは可能な限り独立していなければならず、一方でViewModelはこのモデルの特定の実装に依存してはならない。 2つ目(IOptimiser)は最適化ロジックのインターフェースで、プログラムのアイデアの1つによると、複数の最適化を実行してロジックを選択することができ、ユーザーはコンボボックスから適切なオプティマイザーを選択することで変更することができる、最適化ロジックのインターフェースです。

ブラウンは、グラフィカル・インターフェースでデータ・モデルを表現するレイヤーに使用します。 ご覧のように、図の中には2つのデータモデルがあります:1つ目は自動最適化装置そのものを指し、2つ目は最適化装置のグラフィカル・インターフェースを指します。 黄色は、現時点で存在する唯一の最適化マネージャに使用します。 しかし、複数の最適化マネージャが存在する場合もあります。 また、独自の最適化ロジックを実装することもできます(仕組みの実装方法については、次の記事で検討します)。 は、ファクトリとして機能し、現時点で必要とされるオブジェクトの作成を実装する補助オブジェクトに使用します。

さらに、アプリケーション起動時のオブジェクトの関係や作成プロセスを考えてみましょう。 これに先立ち、グラフィックレイヤーとその構成要素を考える必要があります。

  • AutoOptimiser (main window),
  • AutoOptimiserVM (view model),
  • IMainModel (model interface),
  • MainModel (Model),
  • MainModelCreator (static factory creating the data model).


図示した最初の5つのオブジェクトです。 AutoOptimiserクラスは、アプリケーションの起動時に最初にインスタンス化されます。 このクラスは、グラフィカル・インターフェースを作成します。 グラフィカルインターフェースの XAML マークアップには、ViewModel として動作する AutoOptimiserVM オブジェクトへの参照があります。 したがって、グラフィカルレイヤーの作成中に、AutoOptimiserVMクラスも作成されますが、グラフィカルレイヤーは完全に所有します。 このオブジェクトは、グラフィカル・インターフェースをデストロイした後、デストロイされるまで存在します。 "Composition "を介してAutoOptimiserクラス(ウィンドウ)と接続されており、オブジェクトの完全な所有権と制御を意味します。  

ViewModel クラスは Model クラスへのアクセス権を持たなければなりませんが、データモデルクラスは ViewModel から独立していなければなりません。 つまり、どのクラスがデータモデルを提供しているかを知る必要はありません。 その代わりに ViewModel クラスは、メディエーターが使用できるパブリックメソッド、イベント、プロパティのセットを含むモデルインターフェースを認識します。 そのため、このクラスは MainModel クラスに直接接続されているのではなく、分析されたクラスがを使用するクラスに属するという「集約」の関係を介して、そのインターフェースに接続されています。

しかし、アグリゲーションとコンポジションの差の一つとして、解析されたクラスは一度に複数のオブジェクトに属することができ、そのライフタイムプロセスはコンテナオブジェクトによって制御されないことが挙げられます。 このステートメントは MainModel クラスの静的コンストラクタ (MainModelCreator クラス) で作成され、MainModel クラスと AutoOptimiserVM クラスの両方に同時に格納されるため、MainModel クラスについては完全に真です。 アプリケーションがタスクを完了すると、オブジェクトは破棄されます。 元々は静的プロパティで実装されていたもので、アプリケーションが完了した時にのみクリアされるためです。   

3つのキーオブジェクトの関係を考えてみました。モデル - ビュー - ViewModelです。 図の残りの部分は、アプリケーションのメイン・ビジネス・ロジックに割かれています。 最適化処理を担当するオブジェクトとデータモデルオブジェクトの関係を提示します。 最適化制御プロセスを担当するオブジェクトは、必要なプロセスを起動し、その実行を個々のプログラムオブジェクトに委譲する一種のコントローラとして機能します。 その一つがオプティマイザーです。 また、オプティマイザは、ターミナルの起動やターミナルの起動に必要な設定ファイルの生成など、タスクの実行をタスク指向のオブジェクトに委譲するマネージャでもあります。 


MainModel クラスのインスタンス化の間に、既にお馴染みのスタティック コンストラクタのメカニズムを使用してオプティマイザ クラスのインスタンス化も行います。 図からわかるように、オプティマイザクラスはIOptimiserインターフェースを実装し、OptimiserCreatorから派生したコンストラクタクラスを持つ必要があります - オプティマイザの特定のインスタンスを作成します。 これはプログラム実行モードでオプティマイザの動的置換を実装するために必要です。

各オプティマイザは個別の最適化ロジックを持つことができます。 現在のオプティマイザのロジックとオプティマイザの実装については、次の記事で詳しく考察していきます。 さて、構築に戻りましょう。 データモデルクラスは、アソシエーション関係を介してすべてのモデルコンストラクタの基底クラスと接続されており、つまり、データモデルクラスは、基底クラスにキャストされたオプティマイザのコンストラクタを使用して、特定のオプティマイザインスタンスを作成します。

作成されたオプティマイザはインターフェイス型にキャストされ、MainModel クラスの適切なフィールドに保存されます。 このように、オブジェクト生成時(オブジェクト・コンストラクタ)とインスタンス生成時(オプティマイザ)を抽象化することで、プログラム実行時にオプティマイザを動的に置換することができるようにします。 使用されているアプローチは「抽象化ファクトリ」と呼ばれています。 その考え方は、プロダクト(最適化ロジックを実装するクラス)とそのファクトリ(プロダクトを作成するクラス)の両方が独自の抽象化があるというものです。 ユーザークラスは、両方のコンポーネントのロジックの特定の実装を知る必要はありませんが、異なる実装を使用することができなければなりません。

実際の生活からの例として、炭酸水やお茶、コーヒーなどの商品を製造するファクトリ(向上)などがあります。 人は、その飲み物を飲むために、その飲み物の具体的な製造方法を知る必要はありません。 また、飲料を製造しているファクトリや売りしている店舗の一定の内部構造を知る必要もありません。 この例では

  • 人は利用者です。
  • 飲み物を提供する店舗はファクトリです。
  • 飲み物は商品です。 
プログラムでは、ユーザーはMainModelクラスです。


デフォルトのオプティマイザーの実装を見てみると、設定のグラフィカルインターフェイス(すべてのオプティマイザーが列挙されているComboBoxの横にある「GUI」ボタンをクリックすることで呼び出されます)も持っていることがわかります。 クラス図(とコード内)では、オプティマイザ設定のグラフィカルな部分を「SimpleOptimiserSettings」と呼び、ViewModelとViewをそれぞれ「SimpleOptimiserVM」「SimpleOptimiserM」と呼んでいます。 クラス図を見てもわかるように、オプティマイザ設定のViewModelはグラフィカルな部分が完全に所有しているため、"Composition "の関係を介して接続されています。 ビュー部分はオプティマイザによって完全に所有され、「構成」関係を介してマネージャクラスと接続されています。 オプティマイザの設定データモデルの一部は、オプティマイザとViewModelの両方に属しているため、両者の間に「集約」の関係があります。 オプティマイザがオプティマイザ設定グラフィックデータモデルに保存されている設定にアクセスできるようにするために意図的に行われています。      

本章を完結させるために、ここでは、上記のように考えられたオブジェクトのインスタンス化プロセスを示すシーケンス図を提供します。


図は上から下まで読んでください。 表示される処理の開始点は、メインオプティマイザウィンドウのグラフィックレイヤーのインスタンス化でアプリケーションの開始の瞬間を示すインスタンスです。 インスタンス化の間、グラフィック・インターフェースは、メイン・ウィンドウのDataContextとして宣言されているので、SimpleOptimiserVMクラスのインスタンスを作成します。 インスタンス化の間、SimpleOptimiserVMMainModelCreator.Modelの静的プロパティを呼び出し、よりMainModelオブジェクトが生成され、IMainModelインターフェイス型にキャストされます。

MainModelクラスのインスタンス化時に、オプティマイザのコンストラクタのリストが作成されます。 ComboBoxに表示されるリストで、希望するオプティマイザを選択することができます。 データモデルのインスタンス化後、SimpleOptimiserVMクラスのコンストラクタが呼び出され、IMainModelインタフェース型で提示されたデータモデルからChangeOptimiserメソッドを呼び出します。 ChangeOptimiserメソッドは、オプティマイザーの選択されたコンストラクタ 上でCreate()メソッドを呼び出します。 アプリケーションの起動を見ているので、選択されたオプティマイザのコンストラクタは指定されたリストの最初のフォームになります。 目的のオプティマイザのコンストラクタでCreateメソッドを呼び出すことで、特定のオプティマイザタイプの作成をコンストラクタに委譲します。 オプティマイザを作成し、インターフェイス型にキャストされたオプティマイザオブジェクトを返し、データモデルに渡し、適切なプロパティに保存します。 その後、ChangeOptimiserメソッドの操作が完了し、SimpleOptimiserVMクラスのコンストラクタに戻ります。

モデルクラスと論理プログラム部分

結果として得られるアプリケーションの一般的な構造と、アプリケーション起動時のメインオブジェクトの作成プロセスについて考えてみました。 では、そのロジックの実装の詳細を考えてみましょう。 作成されたアプリケーションのロジックを記述するすべてのオブジェクトは、"Model "ディレクトリに配置されています。 ディレクトリルートには "MainModel.cs" ファイルがあり、アプリケーションのビジネスロジック全体を起動するための出発点となるデータモデルクラスがあります。 その実装には1000行以上のコードが含まれているので、ここではクラス全体のコードは提供しませんが、個々のメソッドの実装だけを提供します。 このクラスは IMainModel インターフェースを継承します。 ここでは、その構造を示すインターフェースコードを示します。

/// <summary>
/// Data model interface of the main optimizer window
/// </summary>    
interface IMainModel : INotifyPropertyChanged
{
    #region Getters
    /// <summary>
    /// Selected optimizer
    /// </summary>
    IOptimiser Optimiser { get; }
    /// <summary>
    /// The list of names of terminals installed on the computer
    /// </summary>
    IEnumerable<string> TerminalNames { get; }
    /// <summary>
    /// The list of names of optimizers available for usage
    /// </summary>
    IEnumerable<string> OptimisatorNames { get; }
    /// <summary>
    /// The list of names of directories with saved optimizations (Data/Reports/*)
    /// </summary>
    IEnumerable<string> SavedOptimisations { get; }
    /// <summary>
    /// Structure with all passes of optimization results
    /// </summary>
    ReportData AllOptimisationResults { get; }
    /// <summary>
    /// Forward tests
    /// </summary>
    List<OptimisationResult> ForwardOptimisations { get; }
    /// <summary>
    /// Historical tests
    /// </summary>
    List<OptimisationResult> HistoryOptimisations { get; }
    #endregion

    #region Events
    /// <summary>
    /// Event of exception throw form the data model
    /// </summary>
    event Action<string> ThrowException;
    /// <summary>
    /// Optimization stop error
    /// </summary>
    event Action OptimisationStoped;
    /// <summary>
    /// Event of progress bar update form the data model
    /// </summary>
    event Action<string, double> PBUpdate;
    #endregion

    #region Methods
    /// <summary>
    /// Method loading previously saved optimization results
    /// </summary>
    /// <param name="optimisationName">The name of the required report</param>
    void LoadSavedOptimisation(string optimisationName);
    /// <summary>
    /// Method changing the previously selected terminal
    /// </summary>
    /// <param name="terminalName">ID of the requested terminal</param>
    /// <returns></returns>
    bool ChangeTerminal(string terminalName);
    /// <summary>
    /// Optimizer change method
    /// </summary>
    /// <param name="optimiserName">Optimizer name</param>
    /// <param name="terminalName">Terminal name</param>
    /// <returns></returns>
    bool ChangeOptimiser(string optimiserName, string terminalName = null);
    /// <summary>
    /// Optimization start
    /// </summary>
    /// <param name="optimiserInputData">Input data to launch optimization</param>
    /// <param name="IsAppend">Flag showing whether to add to existing data (if any) or overwrite them</param>
    /// <param name="dirPrefix">Prefix of the directory with optimizations</param>
    void StartOptimisation(OptimiserInputData optimiserInputData, bool IsAppend, string dirPrefix);
    /// <summary>
    /// Optimization stop from outside (by user)
    /// </summary>
    void StopOptimisation();
    /// <summary>
    /// Get robot parameters
    /// </summary>
    /// <param name="botName">Expert name</param>
    /// <param name="isUpdate">Flag whether file needs to be updated before reading</param>
    /// <returns>List of parameters</returns>
    IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate);
    /// <summary>
    /// Saving selected optimizations to the (* .csv) file 
    /// </summary>
    /// <param name="pathToSavingFile">Path to the file to be saved</param>
    void SaveToCSVSelectedOptimisations(string pathToSavingFile);
    /// <summary>
    /// Saving optimizations for the transferred date to the (* csv) file 
    /// </summary>
    /// <param name="dateBorders">Date range borders</param>
    /// <param name="pathToSavingFile">Path to the file to be saved</param>
    void SaveToCSVOptimisations(DateBorders dateBorders, string pathToSavingFile);
    /// <summary>
    /// Start the testing process
    /// </summary>
    /// <param name="optimiserInputData">List of tester setup parameters</param>
    void StartTest(OptimiserInputData optimiserInputData);
    /// <summary>
    /// Start the sorting process
    /// </summary>
    /// <param name="borders">Date range borders</param>
    /// <param name="sortingFlags">Array of parameter names for sorting</param>
    void SortResults(DateBorders borders, IEnumerable<SortBy> sortingFlags);
    /// <summary>
    /// Filtering optimization results
    /// </summary>
    /// <param name="borders">Date range borders</param>
    /// <param name="compareData">Data filtering flags</param>
    void FilterResults(DateBorders borders, IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData);
    #endregion
}

インターフェースのコンポーネントは #region ディレクティブで区切られています。 このように、インタフェース部は、代表的な部材に分けられています。 ご覧のように、データモデルで規制されているフィールドからグラフィカルなインターフェースまで、様々な情報を提供するプロパティがあります。 しかし、データへのアクセスを制限するだけのゲッターであり、読まれているオブジェクトを上書きする関数はありません。 これは、データモデルのロジックが誤ってViewModelから破損するのを防ぐために行われます。 インターフェースのプロパティで面白いのは、最適化結果のリストです。

  • AllOptimisationResults
  • ForwardOptimisations
  • HistoryOptimisations

このフィールドには、最適化のリストが含まれており、GUIの結果タブの表に表示されます。 すべての最適化パスのリストは、特別に作成された構造体「ReportData」に格納されています。

/// <summary>
/// Structure describing optimization results
/// </summary>
struct ReportData
{
    /// <summary>
    /// Dictionary with optimization passes
    /// key - date range
    /// value - list of optimization passes for the given range
    /// </summary>
    public Dictionary<DateBorders, List<OptimisationResult>> AllOptimisationResults;
    /// <summary>
    /// Expert and Currency
    /// </summary>
    public string Expert, Currency;
    /// <summary>
    /// Deposits
    /// </summary>
    public double Deposit;
    /// <summary>
    /// Leverage
    /// </summary>
    public int Laverage;
}

最適化データに加えて、テストの起動(選択した最適化パスをダブルクリックすることで)や、以前に最適化されたものと新しいデータを追加したときの最適化結果の比較に必要な主なオプティマイザの設定を記述した構造になっています。

また、データモデルには、コンピュータにインストールされているターミナルのリスト、選択可能なオプティマイザの名前(オプティマイザのコンストラクタから作成される)、および以前に保存されたオプティマイザのリスト("Data/Reports "にあるディレクトリの名前)があります。 オプティマイザ自体へのアクセスも提供されています。

(モデルからビューモデルへの)情報の逆交換は、データモデルのインスタンス化後に ViewModel がサブスクライブするイベントを使用して行われます。 このようなイベントは4つあり、そのうちの3つはカスタムイベントで、1つはINotifyPropertyChangedインターフェイスから継承されています。 データモデルでは、INotifyPropertyChangedインタフェースからの継承は必須ではありません。 しかし、このプログラムでは継承が使われているのは、便利そうです。

イベントの一つにThrowExceptionがあります。 当初は、データモデルから直接グラフィックを制御してはいけないので、アプリケーションのグラフィック部分にエラーメッセージを送信して表示するために作成されました。 しかし、現在では、このイベントは、データモデルからグラフィックスに多数のテキストメッセージを渡すためにも使用されています。 これらはエラーではなく、テキストアラートです。 そのため、イベントはさらにエラーではないメッセージを通過することを心に留めておいてください。 

データモデルのメソッドを考えるために、このプログラム部分を実装したクラスを見てみましょう。 

新しいロボットが選択されたときにオプティマイザが最初に行うことは、そのパラメータをロードすることです。 2つの可能なロジックを実装した "GetBotParams" メソッドによって行われます。 ロボットのパラメータで設定ファイルを更新したり、読み込んだりすることができます。 また、再帰的な処理を行うこともできます。 

/// <summary>
/// Get parameters for the selected EA
/// </summary>
/// <param name="botName">Expert name</param>
/// <param name="terminalName">Terminal name</param>
/// <returns>Expert parameters</returns>
public IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate)
{
    if (botName == null)
        return null;

    FileInfo setFile = new FileInfo(Path.Combine(Optimiser
                                   .TerminalManager
                                   .TerminalChangeableDirectory
                                   .GetDirectory("MQL5")
                                   .GetDirectory("Profiles")
                                   .GetDirectory("Tester")
                                   .FullName, $"{Path.GetFileNameWithoutExtension(botName)}.set"));


    try
    {
        if (isUpdate)
        {
            if (Optimiser.TerminalManager.IsActive)
            {
                ThrowException("Wating for closing terminal");
                Optimiser.TerminalManager.WaitForStop();
            }
            if (setFile.Exists)
                setFile.Delete();

            FileInfo iniFile = terminalDirectory.Terminals
                                                .First(x => x.Name == Optimiser.TerminalManager.TerminalID)
                                                .GetDirectory("config")
                                                .GetFiles("common.ini").First();

            Config config = new Config(iniFile.FullName);

            config = config.DublicateFile(Path.Combine(workingDirectory.WDRoot.FullName, $"{Optimiser.TerminalManager.TerminalID}.ini"));

            config.Tester.Expert = botName;
            config.Tester.FromDate = DateTime.Now;
            config.Tester.ToDate = config.Tester.FromDate.Value.AddDays(-1);
            config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
            config.Tester.Model = ENUM_Model.OHLC_1_minute;
            config.Tester.Period = ENUM_Timeframes.D1;
            config.Tester.ShutdownTerminal = true;
            config.Tester.UseCloud = false;
            config.Tester.Visual = false;

            Optimiser.TerminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;
            Optimiser.TerminalManager.Config = config;

            if (Optimiser.TerminalManager.Run())
                Optimiser.TerminalManager.WaitForStop();

            if (!File.Exists(setFile.FullName))
                return null;

            SetFileManager setFileManager = new SetFileManager(setFile.FullName, false);
            return setFileManager.Params;
        }
        else
        {
            if (!setFile.Exists)
                return GetBotParams(botName, true);

            SetFileManager setFileManager = new SetFileManager(setFile.FullName, false);
            if (setFileManager.Params.Count == 0)
                return GetBotParams(botName, true);

            return setFileManager.Params;
        }
    }
    catch (Exception e)
    {
        ThrowException(e.Message);
        return null;
    }
}

メソッドの冒頭では、C#標準ライブラリで利用可能なFileInfoクラスを使用して、ロボットパラメータを持つファイルのオブジェクト指向表現を作成します。 標準のターミナル設定では、MQL5/Profiles/Tester/{選択したロボット名}.setディレクトリに保存されます。 これはオブジェクト指向ファイル表現の作成時に設定されているパスです。 ファイル操作中にエラーが発生するリスクがあるため、さらなるアクションはtry-catch構文にラップされています。 これで、渡された isUpdate パラメータに応じて、可能性のあるロジック分岐の一つが実行されます。 isUpdate = true の場合、ファイルの値がデフォルトにリセットされている間に設定を更新し、パラメータを読み込まなければなりません。 このロジックブランチは、アプリケーションのグラフィカル部分の「Update (*.set) file」をクリックしたときに実行されます。 エキスパート設定で更新する際に一番便利なのが再生成です。

テスターでロボットが選択された時に存在しなかった場合は、ストラテジーテスターによってファイルが生成されます。 そのため、ファイルを削除した後にテスターを再起動し、ファイルが生成されるまで待ち、テスターを閉じてデフォルト値に戻すだけです。 まず、ターミナルが起動しているかどうかを確認します。 実行中であれば、対応するメッセージを表示して完了を待ちます。 そして、パラメータを持つファイルが存在するかどうかを確認します。 そのようなファイルがあれば削除してください。

そして、以前の記事で検討したお馴染みのConfigを使って、ターミナル起動用の設定ファイルを埋めます。 設定ファイルに書き込まれた日付に注意してください。 ターミナルでテストを起動しますが、テスト開始日が終了日より1日早く指定されています。 このため、テスターが起動し、必要な設定のファイルが生成されます。 その後、テストの起動に失敗して動作を決済し、その後、ファイルを読み込むことができます。 設定ファイルが作成・準備されたら、TerminalManagerクラスを使って設定ファイル生成の処理を起動します(処理は先ほど考えました)。 ファイルの生成が完了したら、SetFileManagerクラスを使用して、設定したファイルを読み込み、その内容を返します。

設定ファイルの明示的な生成を必要としない別のロジック分岐が必要な場合は、条件の2番目の部分を使用します。 このメソッドはEAの設定ファイルを読み込んでその内容を返すか、パラメータisUpdate = trueでメソッドを再帰的に起動し、以前に検討したロジック部分を実行します。

もう一つ面白いのが「StartOptimisation」というメソッドです。

/// <summary>
/// Start optimizations
/// </summary>
/// <param name="optimiserInputData">Input data for the optimizer</param>
/// <param name="isAppend">Flag whether data should be added to a file?</param>
/// <param name="dirPrefix">Directory prefix</param>
public async void StartOptimisation(OptimiserInputData optimiserInputData, bool isAppend, string dirPrefix)
{
    if (string.IsNullOrEmpty(optimiserInputData.Symb) ||
        string.IsNullOrWhiteSpace(optimiserInputData.Symb) ||
        (optimiserInputData.HistoryBorders.Count == 0 && optimiserInputData.ForwardBorders.Count == 0))
    {
        ThrowException("Fill in asset name and date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

    if (Optimiser.TerminalManager.IsActive)
    {
        ThrowException("Terminal already running");
        return;
    }

    if (optimiserInputData.OptimisationMode == ENUM_OptimisationMode.Disabled)
    {
        StartTest(optimiserInputData);
        return;
    }

    if (!isAppend)
    {
        var dir = workingDirectory.GetOptimisationDirectory(optimiserInputData.Symb,
                                                  Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot),
                                                  dirPrefix, Optimiser.Name);
        List<FileInfo> data = dir.GetFiles().ToList();
        data.ForEach(x => x.Delete());
        List<DirectoryInfo> dirData = dir.GetDirectories().ToList();
        dirData.ForEach(x => x.Delete());
    }

    await Task.Run(() =>
    {
        try
        {
            DirectoryInfo cachDir = Optimiser.TerminalManager.TerminalChangeableDirectory
                                                     .GetDirectory("Tester")
                                                     .GetDirectory("cache", true);
            DirectoryInfo cacheCopy = workingDirectory.Tester.GetDirectory("cache", true);
            cacheCopy.GetFiles().ToList().ForEach(x => x.Delete());
            cachDir.GetFiles().ToList()
                   .ForEach(x => x.MoveTo(Path.Combine(cacheCopy.FullName, x.Name)));

            Optimiser.ClearOptimiser();
            Optimiser.Start(optimiserInputData,
                Path.Combine(terminalDirectory.Common.FullName,
                $"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}_Report.xml"), dirPrefix);
        }
        catch (Exception e)
        {
            Optimiser.Stop();
            ThrowException(e.Message);
        }
    });
}

このメソッドは非同期であり、async await技術を使って書かれているため、非同期メソッドの宣言がよりよくなります。 まず、渡されたシンボル名と最適化範囲をチェックします。どれか一つでも欠けている場合は、ブロックされているGUIをアンロックし(最適化開始時に一部のGUIボタンがブロックされている)、エラーメッセージを表示してから関数の実行を完了させます。 すでにターミナルが起動している場合は全く同じことをしてください。 最適化ではなくテストモードが選択されていた場合、プロセスの実行をテストを開始するメソッドにリダイレクトする

Append modeが選択されている場合、最適化されたディレクトリ内のすべてのファイルとサブディレクトリを削除します。 その後、最適化の実行に進みます。 最適化プロセスは非同期に開始されるため、このタスクが実行されている間はGUIをブロックしません。 また、エラーが発生した場合に備えて、トライキャッチ構文にラップされています。 プロセス開始前に、以前に実行された最適化のすべてのキャッシュファイルを自動最適化装置のDataタスクディレクトリに作成された一時ディレクトリにコピーします。 これにより、最適化が先に起動されていた場合でも確実に起動されるようになります。 そして、以前にオプティマイザのローカル変数に書き込まれたすべてのデータをクリアし、最適化処理を起動します。 最適化起動パラメータの1つは、ロボットが生成したレポートファイルへのパスです。 記事3で述べたように、レポートは{robot name}_Report.xmlという名前で生成されます。 オートオプティマイザーでは、この名前を次の行で指定します。

$"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}_Report.xml")

最適化ファイルのパラメータの1つとして指定されたロボットへのパスからロボット名が形成される文字列連結によって行われます。 最適化ストップ処理は、完全にオプティマイザクラスに転送されます。 それをを実装したメソッドは、オプティマイザクラスのインスタンスでStopOptimisationメソッドを呼び出すだけです。

/// <summary>
/// Complete optimization from outside the optimizer
/// </summary>
public void StopOptimisation()
{
    Optimiser.Stop();
}

テストはオプティマイザではなく、データモデルクラスに実装されているメソッドを使って開始されます。

/// <summary>
/// Run tests
/// </summary>
/// <param name="optimiserInputData">Input data for the tester</param>
public async void StartTest(OptimiserInputData optimiserInputData)
{
    // Check if the terminal is running
    if (Optimiser.TerminalManager.IsActive)
    {
        ThrowException("Terminal already running");
        return;
    }

    // Set the date range
    #region From/Forward/To
    DateTime Forward = new DateTime();
    DateTime ToDate = Forward;
    DateTime FromDate = Forward;

    // Check the number of passed dates. Maximum one historical and one forward
    if (optimiserInputData.HistoryBorders.Count > 1 ||
        optimiserInputData.ForwardBorders.Count > 1)
    {
        ThrowException("For test there must be from 1 to 2 date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

    // If both historical and forward dates are passed
    if (optimiserInputData.HistoryBorders.Count == 1 &&
        optimiserInputData.ForwardBorders.Count == 1)
    {
        // Test the correctness of the specified interval
        DateBorders _Forward = optimiserInputData.ForwardBorders[0];
        DateBorders _History = optimiserInputData.HistoryBorders[0];

        if (_History > _Forward)
        {
            ThrowException("History optimization must be less than Forward");
            OnPropertyChanged("ResumeEnablingTogle");
            return;
        }

        // Remember the dates
        Forward = _Forward.From;
        FromDate = _History.From;
        ToDate = (_History.Till < _Forward.Till ? _Forward.Till : _History.Till);
    }
    else // If only forward or only historical data is passed
    {
        // Save and consider it a historical date (even if forward was passed)
        if (optimiserInputData.HistoryBorders.Count > 0)
        {
            FromDate = optimiserInputData.HistoryBorders[0].From;
            ToDate = optimiserInputData.HistoryBorders[0].Till;
        }
        else
        {
            FromDate = optimiserInputData.ForwardBorders[0].From;
            ToDate = optimiserInputData.ForwardBorders[0].Till;
        }
    }
    #endregion

    PBUpdate("Start test", 100);

    // Run test in the secondary thread
    await Task.Run(() =>
    {
        try
        {
            // Create a file with EA settings
            #region Create (*.set) file
            FileInfo file = new FileInfo(Path.Combine(Optimiser
                                             .TerminalManager
                                             .TerminalChangeableDirectory
                                             .GetDirectory("MQL5")
                                             .GetDirectory("Profiles")
                                             .GetDirectory("Tester")
                                             .FullName, $"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}.set"));

            List<ParamsItem> botParams = new List<ParamsItem>(GetBotParams(optimiserInputData.RelativePathToBot, false));

            // Fill the expert settings with those that were specified in the graphical interface
            for (int i = 0; i < optimiserInputData.BotParams.Count; i++)
            {
                var item = optimiserInputData.BotParams[i];

                int ind = botParams.FindIndex(x => x.Variable == item.Variable);
                if (ind != -1)
                {
                    var param = botParams[ind];
                    param.Value = item.Value;
                    botParams[ind] = param;
                }
            }

            // Save settings to a file
            SetFileManager setFile = new SetFileManager(file.FullName, false)
            {
                Params = botParams
            };
            setFile.SaveParams();
            #endregion

            // Create terminal config
            #region Create config file
            Config config = new Config(Optimiser.TerminalManager
                                                .TerminalChangeableDirectory
                                                .GetDirectory("config")
                                                .GetFiles("common.ini")
                                                .First().FullName);
            config = config.DublicateFile(Path.Combine(workingDirectory.WDRoot.FullName, $"{Optimiser.TerminalManager.TerminalID}.ini"));

            config.Tester.Currency = optimiserInputData.Currency;
            config.Tester.Deposit = optimiserInputData.Balance;
            config.Tester.ExecutionMode = optimiserInputData.ExecutionDelay;
            config.Tester.Expert = optimiserInputData.RelativePathToBot;
            config.Tester.ExpertParameters = setFile.FileInfo.Name;
            config.Tester.ForwardMode = (Forward == new DateTime() ? ENUM_ForvardMode.Disabled : ENUM_ForvardMode.Custom);
            if (config.Tester.ForwardMode == ENUM_ForvardMode.Custom)
                config.Tester.ForwardDate = Forward;OnPropertyChanged("StopTest");
            else
                config.DeleteKey(ENUM_SectionType.Tester, "ForwardDate");
            config.Tester.FromDate = FromDate;
            config.Tester.ToDate = ToDate;
            config.Tester.Leverage = $"1:{optimiserInputData.Laverage}";
            config.Tester.Model = optimiserInputData.Model;
            config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
            config.Tester.Period = optimiserInputData.TF;
            config.Tester.ShutdownTerminal = false;
            config.Tester.Symbol = optimiserInputData.Symb;
            config.Tester.Visual = false;
            #endregion

            // Configure the terminal and launch it
            Optimiser.TerminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            Optimiser.TerminalManager.Config = config;
            Optimiser.TerminalManager.Run();

            // Wait for the terminal to close
            Optimiser.TerminalManager.WaitForStop();
        }
        catch (Exception e)
        {
            ThrowException(e.Message);
        }

        OnPropertyChanged("StopTest");
    });
}

ターミナルが動作しているかどうかのお馴染みのチェックが終わったら、ヒストリカルテストとフォワードテストの日付設定に進みます。 ヒストリー的な範囲を片方に設定したり、ヒストリー的な範囲と前方の範囲の両方を設定したりすることができます。 設定で順方向の間隔のみを指定した場合は、ヒストリーとして扱われます。 まず、テスト日を格納する変数(フォワード、テスト終了日、テスト開始日)を宣言します。 そして、メソッドをチェックしてください - 複数のヒストリー範囲のボーダーや複数の前方テストボーダーが渡された場合は、エラーメッセージを表示します。 そして、set borders - この条件のアイデアは、3つの宣言された変数の間に4つの渡された日付を設定することです(ヒストリー的な期間だけを設定する場合は2つ)。

  • FromDate - 渡された日付の中で最も小さいもの。
  • ToDate - パスした日付のうち、最も高い日付。
  • Forward - フォワードの中で最も低い日付。

テスト開始もトライキャッチ構文でラップされています。 まず、ロボットパラメータの入ったファイルを生成し、渡されたロボットパラメータを埋めます。 先に検討した SetFileManager オブジェクトを使用して行います。 そして、オーダーに従ってコンフィグファイルを作成し、テスト処理を開始します。 そして、ターミナルが閉じるのを待ちます。 メソッドの操作が完了したら、テストが完了したことをグラフィックに通知する。 このメソッドは非同期であり、呼び出されたメソッドが完了するのを待つことなく、その呼び出し後もプログラムの動作が継続されるため、イベントを介して行われなければなりません。

最適化プロセスに関しても、オプティマイザは最適化プロセス完了イベントを介して最適化プロセスが終了したことをデータモデルに通知します。 これについては、最終回の記事で詳しく考察していきます。

結論

以前の記事では、作成したオートオプティマイザとアルゴリズムを組み合わせる過程とその一部を詳しく分析しました。 最適化レポートのロジックについてはすでに検討し、トレードアルゴリズムへの応用を見てきました。 前回の記事では、グラフィカルインターフェース(プログラムのView部分)とプロジェクトファイルの構造について考察しました。

また、プログラムの観点から、プロジェクトの内部構造、クラス間の相互作用、最適化プロセスの立ち上げなどを分析しました。 プログラムは複数の最適化ロジックをサポートしているので、実装されているロジックについては詳細に検討していません - オプティマイザの実装例として別の記事で記述した方が良いでしょう。 あと2回の記事では、論理部分とグラフィックスのつながりを分析するとともに、オプティマイザの実装アルゴリズムについて考察し、オプティマイザの実装例を考えていきます。

添付ファイルには、記事4で分析したトレーディングロボットを使ったオートオプティマイザープロジェクトがあります。 このプロジェクトを利用するには、オートオプティマイザのプロジェクトファイルとテスト用ロボットファイルをコンパイルしてください。 その後、ReportManager.dll(最初の記事で説明しました)をMQL5/Librariesディレクトリにコピーして、EAのテストを開始します。 オートオプティマイザーと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/7718

添付されたファイル |
Auto_Optimiser.zip (125.7 KB)
クロスプラットフォームグリッドEAの開発:マルチカレンシーEAのテスト クロスプラットフォームグリッドEAの開発:マルチカレンシーEAのテスト
この1か月で相場は30%以上も下落しました。(コロナショック後です。) グリッド系とマーチンゲール系のEAのテストには最適な時期のようです。 本記事は、「クロスプラットフォームのグリッドEAを作る」シリーズの無計画な続編です。 現在の相場では、グリッドEAのストレスレストを整えるチャンスとなっています。 ということで、この機会にEAのテストをしてみましょう。
MQLプログラムのグラフィカルインターフェイスのマークアップツールとしてのMQL 第2部 MQLプログラムのグラフィカルインターフェイスのマークアップツールとしてのMQL 第2部
本論文では、MQLプログラムのウィンドウインタフェースを記述するための新しい概念をMQLの構造体を用いて確認します。 MQLマークアップに基づいてGUIを自動的に作成することで、要素をキャッシュして動的に生成したり、イベントを処理するためのスタイルや新しいスキームを制御したりする関数が追加されます。 標準のコントロールライブラリの強化版が添付されています。
連続的なウォークフォワード最適化(その7)。オートオプティマイザの論理部分をグラフィックスでバインドし、プログラムからグラフィックスを制御する 連続的なウォークフォワード最適化(その7)。オートオプティマイザの論理部分をグラフィックスでバインドし、プログラムからグラフィックスを制御する
この記事では、オートオプティマイザプログラムのグラフィカルな部分と論理的な部分の接続について説明します。 ボタンクリックから最適化マネージャへのタスクリダイレクトまで、最適化の起動プロセスを考慮します。
DoEasyライブラリの時系列(第38部): 時系列コレクション-リアルタイムの更新とプログラムからのデータへのアクセス DoEasyライブラリの時系列(第38部): 時系列コレクション-リアルタイムの更新とプログラムからのデータへのアクセス
本稿では、時系列データのリアルタイム更新と、すべての銘柄のすべての時系列から「新しいバー」イベントに関するメッセージを制御プログラムチャートに送信し、カスタムプログラムでこれらのイベントを処理する機能について検討します。「新しいティック」クラスは、現在以外のチャート銘柄と期間の時系列を更新する必要性を判断するために使用されます。