連続ウォークフォワード最適化(パート5):自動オプティマイザプロジェクトの概要とGUIの作成
イントロダクション
前の記事では、ターミナルに直接関連するプロジェクトパーツと、プロジェクトの一般的な適用を説明する部分を検討しました。 前回の記事は、シリーズの残りの部分に先んじていました。 これは2つの理由で行われました。 まず、アプリケーションを使用するための指示として機能します。 次に、コードの理解に役立つアプリの作成アイデアとロジックを示すことができます。
この記事は次のリンクから入手できます。
- 連続ウォークフォワード最適化(パート1):最適化レポートの操作
- 連続ウォークフォワード最適化(パート2):ロボットの最適化レポート作成機構
- 連続ウォークフォワード最適化(パート3):ロボットをオートオプティマイザに適応させる
- 連続ウォークフォワード最適化(パート4):最適化マネージャ(オートオプティマイザ)
以前の記事、アプリケーションでも使用するマテリアル:
今回の記事では、Visual Studio IDE とそのコンポーネントのプロジェクト構造について説明します。 このパートは、アプリの GUI の作成に専念します。 また、最適化が格納されているマネージ ディレクトリの構造と、前のプロジェクトから取得したクラスを管理する最適化プロセスの変更も考慮します。
プロジェクト構造の概要
この部分は C# にも当てはめているので、まずそのファイル構造を考慮してみましょう。
以下に添付されたソリューションには、2 つのプロジェクトがあります。 そのうちの1つは最初の記事で検討され、2番目は後の記事で分析されました。このプロジェクトは自動オプティマイザです。
このプロジェクトには GUI があるため、今回は MVVM(ModelViewViewModel)アプローチを再び使用します。 プロジェクト テンプレートは、適切なセクションに分割されます。 プロジェクト ロジックはモデル パーツに実装する必要があるため、プロジェクトのグラフィカルな部分に関連しないクラスはModelサブディレクトリに配置され、さらにディレクトリに分割されます。
- 管理されたディレクトリおよびターミナルディレクトリの向きを担当するクラスは、Model/DirectoryManagers フォルダにあります。
- ファイル管理クラスは、Model/FileReaders.の下にあります。
- 最適化ロジックと適切なインスタンス化クラスを持つオブジェクトは、Model/OptimisationManagersの下にあります
- ターミナルとその構成ファイルを管理するオブジェクトは、Model/Terminalです。
変更された前の記事シリーズのオブジェクトから始めましょう。 この説明は、前のパートを読んでいない人にとっても役立ちます。
アプリケーションのグラフィカルパートの作成
グラフィカルインターフェイスに移りましょう。 以前は、C# 言語で MetaTrader5 のアドオンを作成するメソッドと、その関数を.dll と OnTimer コールバックを使用してEAと組み合わせる方法について考察しました。 現在の実装では、自動オプティマイザはターミナルの外部に実装されます。 今では、外部の最適化マネージャーとして実行し、最適化を起動し、結果を処理するトレーダーのタスクを模倣します。 さらに、同じコンピュータ上で実行されている複数のターミナルで同時に最適化を行うのを避け、自動オプティマイザを別々のアプリケーションとして実装することで、オプティマイザが実行されているコンピュータを含め、コンピュータにインストールされているすべてのターミナルにアクセスできます。 このコンピュータは、前のプロジェクトで使用できませんでした。
そのため、現在のプロジェクトは完全に.dll として実装されていませんが、現在は.dll と自動オプティマイザ プロジェクトの実行可能ファイルに分割されています。
上のスクリーンショットからわかるように、プロジェクト ウィンドウはHeader、Footer、およびTabControlで構成されたSettingsとReportsがあります。 中央部で選択されているタブに関係なく、ウィンドウのヘッダ部分とフッターは変更されず、部分にあるすべてのコントロール要素にアクセスできます。
ウィンドウのヘッダ部分は、次の XAML マークアップによって作成されます。
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <WrapPanel Margin="2"> <Label Content="Optimisation:"/> <ComboBox Width="200" ItemsSource="{Binding SelectedOptimisationNames,UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding SelectedOptimisation}" SelectedIndex="0" IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/> <Button Content="Load" Margin="2,0,0,0" Width="34" Command="{Binding LoadResults}" IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/> </WrapPanel> <WrapPanel HorizontalAlignment="Right" Margin="2" Grid.Column="1"> <Label Content="Terminal:"/> <ComboBox Width="200" SelectedIndex="{Binding SelectedTerminalIndex}" ItemsSource="{Binding Terminals,UpdateSourceTrigger=LostFocus}" IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/> </WrapPanel> </Grid>
考慮された領域で使用できるすべてのコントロール要素を含む Grid コンテナは、2 つの列に分割されます。 次の要素は、最初の列に追加されます:パラメータ名(最適化)、使用可能な最適化のリストを含むコンボ ボックス、および最適化の読み込みボタン。 2 番目の列には、パラメータ名と、使用可能なターミナルの ID を含むドロップダウン リストがあります。
グラフィカルウィンドウのフッターを表す Grid コンテナ (ProgressBar を使用) は、同様の構造を持ちます。
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Content="{Binding Status, UpdateSourceTrigger=PropertyChanged}"/>
<ProgressBar Grid.Column="1"
Value="{Binding Progress, UpdateSourceTrigger=PropertyChanged}"
Minimum="0"
Maximum="100"/>
</Grid>
エリアを2つに分割し、最初の部品のサイズを制限します。 その結果、最大のコンテナ部品が ProgressBar によって使用されます。 さらに、ウィンドウ全体の幅が変更された場合に、ProgressBar の幅が調整されます。 XAML マークアップルールに従って、3 つのコンポーネントはすべて <Window/> コンテナに配置されます。
<Window x:Class="Metatrader_Auto_Optimiser.AutoOptimiser" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:Metatrader_Auto_Optimiser.View_Model" xmlns:v="clr-namespace:Metatrader_Auto_Optimiser.View" mc:Ignorable="d" Title="Auto Optimiser" Height="500" Width="1200" MinHeight="500" MinWidth="1200"> <Window.DataContext> <vm:AutoOptimiserVM/> </Window.DataContext> ... </Window>
このコンテナは、名前空間参照を定義します。
- Metatrader_Auto_Optimiser.View_Modelは、ユーザー アクション (ViewModel) に対するプログラムの反応を記述するコールバックを格納します。
- Metatrader_Auto_Optimiser.View: ListView オブジェクトのダブルクリック イベントをコマンドに変換するクラスを格納します。 ビューモデルからの ICommand インターフェイスとのイベントの組み合わせの使用を有効にする必要があります。
また、プログラムの起動時にウィンドウを開く最小サイズと初期サイズを設定します。 次に、グラフィカル インターフェイス用にDataContextがインストールされ、上記の別名を使用して ViewModel を含む名前空間に対して使用します。
パネルの中央部分は、2 つのタブを含む TabControl 要素で構成されます。 グラフィック要素の "Body" として、主要な部分として機能します。 "設定"タブの構造は次のとおりです。
このタブも 3 つの部分に分かれています。 タブの上部には、保存する自動オプティマイザレポートのパラメータを設定できるパネルがあります。 また、資産名の選択と*setファイルを更新するためのボタンも含まれます。 "Settings" タブの中央には、オートオプティマイザプロセス中にフィルタおよびソートパラメータを選択するためのオプティマイザ設定とオプションがあります。 直近の部分では、EAのパラメータを設定し、最適化と未来の日付を選択することができます。 より便利にするため、GridSplitter要素は最初の 2 つの部分の間にあります。 ドラッグすると、タブのサイズを変更できます。 ロングインプットリストを持つロボットの最適化パラメータを埋める必要がある場合に特に便利です。
「設定」タブの最初の部分のマークアップコードをそのまま表示してみましょう。
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="100"/> </Grid.ColumnDefinitions> <WrapPanel HorizontalAlignment="Left" VerticalAlignment="Bottom"> <Label Content="Select Optimiser:"/> <ComboBox Width="150" IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding Optimisers}" SelectedIndex="{Binding SelectedOptimiserIndex}"/> <Button Content="GUI" Command="{Binding ShowOptimiserGUI}" IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/> <Label Content="Directory prefix:"/> <TextBox Width="150" IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}" Text="{Binding DirPrefix}"/> <ComboBox Width="100" Margin="2,0,0,0" SelectedIndex="0" ItemsSource="{Binding FileFillingType}" IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding FileWritingMode}"/> <Label Content="Asset name:"/> <TextBox Width="100" IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}" Text="{Binding AssetName}"/> <Button Content="Update (*.set) file" Margin="2,0,0,0" IsEnabled="{Binding EnableMainTogles}" Command="{Binding UpdateSetFile}"/> </WrapPanel> <Button Content="Start/Stop" Grid.Column="2" Margin="2" Command="{Binding StartStopOptimisation}"/> </Grid>
この説明には、2 つの列の分割が含まれます。 最初の列の幅は動的に変更できます。2 番目の列幅は固定で、100 ピクセルです。 最初の列には、パネル内のすべてのコントロールが含まれます。 すべては、次々と要素を配置できるように、WrapPanelに提供されています。 まず、自動オプティマイザの選択とセットアップを担当するコントロールが表示されます。 後には、最適化レポートを使用したフォルダの命名に関連するパラメータと、レポートの生成方法 (Rewrite, Append)に関連するパラメータが続きます。 直近の部分は、最適化に使用するアセット名の指定と、*setファイルをロボットパラメータで更新するためのボタンです。 固定幅の列は、最適化を開始してストップするための開始点となる"Start/Stop"ボタンで占めます。
"設定"タブの2番目の部分は2つのパーツに分かれています。
最初のは、オプティマイザー設定パラメータのリストを含む ListView です。 ここで、パラメータの名前と値は、ターミナルのオプティマイザー設定フィールドに対応します。 2 番目には、データの並べ替えとフィルタ係数の指定があります。 この列には、説明された領域を区切るGridSplitter要素もあります。 この要素を作成するコードはシンプルなので、ここでは提供しません。 完全なコードは以下に添付されています。 タブの下部は上部と完全に類似しており、唯一の例外は、最適化日付を含む右側の部分が2つの部分に分かれているということです。 最初のものは、リストにデータを追加するためのコントロールを備えています。 2 つ目のリストは、作成されたリストを表示するために使用します。
グラフィカルインターフェイスの直近の要素は、最適化結果、および順方向およびヒストリーテストの結果を表示するために設計された "Results" タブです。
添付された図からわかるように、タブは前の図よりも興味深い構造があります。 2 つの部分に分割され、GridSplitter 要素で区切られており、パーツのサイズを変更して最適化結果をより詳細に調べることができます。 上部には、TabItem グループの 2 つの要素があります。 前方テストとヒストリーテストが配置されている"選択されたパス"(Selected pass)タブは、"Optimisations"タブほど興味深くはありません。
タブの下部には、垂直GridSplitterで区切られた 2 つのフィールドがあります。 最初のは、上部のテーブルの 1 つから選択したテストの日付とモードを指定するためのものであり、もう 1 つは表示と読み取りを容易にするためにテーブルにまとめられた変数の数を示します。 また、選択した最適化パス ("Bot Params" タブ) のパラメータのリストも表示されます。
マークアップ要素と"最適化"(Optimisations)タブでの結果の比較は、次の構造を持ちます。
"選択パス"と同様に、このタブには"Save to (*csv)"ボタンがあり、選択した日付に対して完了したすべての最適化の結果をファイルに保存します。 テーブル内のデータを並べ替えおよびフィルタ処理するためのボタンが 2 つ追加され、すべての最適化の結果が表示されます。 結果 table の構造は、「Selected pass.History」と「Selected pass.Forward」のタブにあるテーブルと似ています。 テーブル データを作成するマークアップの部分を次に示します。
<ListView ItemsSource="{Binding AllOptimisations}" SelectedIndex="{Binding SelecterReportItem}" v:ListViewExtention.DoubleClickCommand="{Binding StartTestReport}"> <ListView.View> <GridView> <GridViewColumn Header="Date From" DisplayMemberBinding="{Binding From}"/> <GridViewColumn Header="Date Till" DisplayMemberBinding="{Binding Till}"/> <GridViewColumn Header="Sort by" DisplayMemberBinding="{Binding SortBy}"/> <GridViewColumn Header="Payoff" DisplayMemberBinding="{Binding Payoff}"/> <GridViewColumn Header="Profit pactor" DisplayMemberBinding="{Binding ProfitFactor}"/> <GridViewColumn Header="Average Profit Factor" DisplayMemberBinding="{Binding AverageProfitFactor}"/> <GridViewColumn Header="Recovery factor" DisplayMemberBinding="{Binding RecoveryFactor}"/> <GridViewColumn Header="Average Recovery Factor" DisplayMemberBinding="{Binding AverageRecoveryFactor}"/> <GridViewColumn Header="PL" DisplayMemberBinding="{Binding PL}"/> <GridViewColumn Header="DD" DisplayMemberBinding="{Binding DD}"/> <GridViewColumn Header="Altman Z score" DisplayMemberBinding="{Binding AltmanZScore}"/> <GridViewColumn Header="Total trades" DisplayMemberBinding="{Binding TotalTrades}"/> <GridViewColumn Header="VaR 90" DisplayMemberBinding="{Binding VaR90}"/> <GridViewColumn Header="VaR 95" DisplayMemberBinding="{Binding VaR95}"/> <GridViewColumn Header="VaR 99" DisplayMemberBinding="{Binding VaR99}"/> <GridViewColumn Header="Mx" DisplayMemberBinding="{Binding Mx}"/> <GridViewColumn Header="Std" DisplayMemberBinding="{Binding Std}"/> </GridView> </ListView.View> </ListView>
最適化結果フィルタとソートパラメータを含むTabItemは、"設定"タブの同じ項目と完全に同じです。 マークアップで区切られていますが、ViewModel は、そのうちの 1 つの変更が他の変更で即座にレンダリングされるように配置されています。 変更のレンダリングメカニズムは、次の記事で検討されます。
このセクションからわかるように、グラフィカルインターフェイスマークアップは簡単です。 主なタスクは機能性だったので、プログラムで適切な視覚効果を提供しませんでした。 アプリケーションを美しくしたい場合は、集中プロジェクトストレージとして機能するApp.xamlファイルを編集します。
"最適化管理" シリーズの記事とその変更から借用されたクラス
このプロジェクトでは、"最適化管理"シリーズに以前に作成されたオブジェクトを使用しました。 上記の記事で利用できるため、各オブジェクトの詳細な説明は提供しません。 しかし、特にこのプロジェクト内で変更されたものについて、いくつかについて深く掘り下げましょう。 借りたオブジェクトの完全なリストは次のとおりです。
- RelayCommand — ユーザーが実行するすべての GUI アクションを ViewModel プロジェクトパーツに渡す ICommand インターフェイスを実装します。
- ListViewExtention — ICommand インターフェイスに、ListView 要素のダブルクリックのイベントを渡します。 したがって、ListView コンテナを使用して、使用する MVVM テンプレートに集中することができます。 このクラスは、ダブルクリック イベントのラッパとして機能します。
- DirectoryInfoExtention - DirectoryInfo クラスの拡張メソッドを含むクラス。
- Config — このクラスはターミナル構成ファイルのラッパとして機能します。 このクラスでは、ファイルを Config 型オブジェクトでタスクしているかのように操作できます。 これより、コード行数が減り、エラーが発生する可能性がなくなります。
- TerminalManager — このクラスは、サードパーティアプリケーションからターミナルを起動およびストップします。 MetaTrader5ターミナル専用に作成され、起動前にすべてのターミナル設定方法をサポートします。
- SetFileManager — テスト・セプター構成の *.set ファイルを操作するためのクラス。
- TerminalDirectory — このクラスはターミナルディレクトリのマネージャです。 キー ディレクトリへのアクセスを提供し、目的のフォルダへの完全パスを書き込む必要がなくなります。
リストの最後の 4 つのオブジェクトは、C# コードからターミナルを操作するための独自の API と見なすことができます。 この記事のこの部分で説明した変更は内部のみでしました。 つまり、クラス (パブリック メソッドとプロパティ) を操作するための外部インターフェイスは、シグネチャに変更されません。 したがって、前のプロジェクトでオブジェクトの以前の実装を新しいものに置き換えても、プロジェクトはコンパイルされ、動作します。
変更された構造を持つ最初のオブジェクトは Config クラスです。 このクラスは、ターミナルドキュメントの該当するセクションで説明されている表を示します。 このプロパティには、すべてのテーブル フィールドが含まれます。 プロパティーを変更すると、ターミナル初期設定ファイルの特定のセクションにある特定のキーの値を変更できます。 初期化ファイル *.ini は、一般的に使用する形式を表します。 Windows OS カーネルには、この形式を使用するための関数が用意されています。 そのうちの 2 つを C# コードにインポートしました。 このクラスの前の実装では、使用されたメソッドは Config クラスに直接インポートされました。 現在の実装では、メソッドは分離された IniFileManager クラスで実装されます。
class IniFileManager { private const int SIZE = 1024; //Maximum size (for reading the value from the file) public static string GetParam(string section, string key, string path) { //To get the value StringBuilder buffer = new StringBuilder(SIZE); //Get value to buffer if (GetPrivateProfileString(section, key, null, buffer, SIZE, path) == 0) ThrowCErrorMeneger("GetPrivateProfileStrin", Marshal.GetLastWin32Error(), path); //Return the received value return buffer.Length == 0 ? null : buffer.ToString(); } /// <summary> /// Return error /// </summary> /// <param name="methodName">Method name</param> /// <param name="er">Error code</param> private static void ThrowCErrorMeneger(string methodName, int er, string path) { if (er > 0) { if (er == 2) { if (!File.Exists(path)) throw new Exception($"{path} - File doesn1t exist"); } else { throw new Exception($"{methodName} error {er} " + $"See System Error Codes (https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes) for details"); } } } public static void WriteParam(string section, string key, string value, string path) { //Write value to the INI-file if (WritePrivateProfileString(section, key, value, path) == 0) ThrowCErrorMeneger("WritePrivateProfileString", Marshal.GetLastWin32Error(), path); } }
この結果の Config ファイルには、構成ファイルに含まれるフィールドのみが含まれます。 このオブジェクトの完全な説明は、最適化管理に関する以前の記事しています。
次に変更されるクラスはTerminalManagerです。 このクラスの内容は変更されません。 クラスのメソッドとコンポーネントは借りたクラスであるため、考慮しません。 ただし、このクラスは、ターミナル操作を開始およびストップする際に、アプリケーションで重要な役割を果たします。 このクラス実装の完全なコードを次に示します。
class TerminalManager { /// <summary> /// Constructor /// </summary> /// <param name="TerminalChangeableDirectory"> /// Path to the directory with mutable files (the one in AppData) /// </param> public TerminalManager(DirectoryInfo TerminalChangeableDirectory) : this(TerminalChangeableDirectory, new DirectoryInfo(File.ReadAllText(TerminalChangeableDirectory.GetFiles().First(x => x.Name == "origin.txt").FullName)), false) { } /// <summary> /// Constructor /// </summary> /// <param name="TerminalChangeableDirectory"> /// Path to the directory with mutable files /// </param> /// <param name="TerminalInstallationDirectory"> /// Path to the terminal folder /// </param> public TerminalManager(DirectoryInfo TerminalChangeableDirectory, DirectoryInfo TerminalInstallationDirectory, bool isPortable) { this.TerminalInstallationDirectory = TerminalInstallationDirectory; this.TerminalChangeableDirectory = TerminalChangeableDirectory; TerminalID = TerminalChangeableDirectory.Name; CheckDirectories(); Process.Exited += Process_Exited; Portable = isPortable; } /// <summary> /// Destructor /// </summary> ~TerminalManager() { Close(); Process.Exited -= Process_Exited; } /// <summary> /// Terminal startup process /// </summary> private readonly System.Diagnostics.Process Process = new System.Diagnostics.Process(); /// <summary> /// Running process completion event /// </summary> public event Action<TerminalManager> TerminalClosed; #region Terminal start Arguments /// <summary> /// Login for start - flag /Login /// </summary> public uint? Login { get; set; } = null; /// <summary> /// Platform launch under a certain profile. /// The profile must be created in advance and located in the /profiles/charts/ folder of the trading platform /// </summary> public string Profile { get; set; } = null; /// <summary> /// Config file as a /Config object /// </summary> public Config Config { get; set; } = null; /// <summary> /// Flag of terminal launch in /portable mode /// </summary> private bool _portable; public bool Portable { get => _portable; set { _portable = value; if (value && !TerminalInstallationDirectory.GetDirectories().Any(x => x.Name == "MQL5")) { WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized; if (Run()) { System.Threading.Thread.Sleep(1000); Close(); } WaitForStop(); WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal; } } } /// <summary> /// window style of the launched process /// </summary> public System.Diagnostics.ProcessWindowStyle WindowStyle { get; set; } = System.Diagnostics.ProcessWindowStyle.Normal; #endregion #region Terminal directories /// <summary> /// Path to terminal installation folder /// </summary> public DirectoryInfo TerminalInstallationDirectory { get; } /// <summary> /// Path to terminal folder with variable files /// </summary> public DirectoryInfo TerminalChangeableDirectory { get; } /// <summary> /// Path to the MQL5 folder /// </summary> public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5"); #endregion /// <summary> /// Terminal ID folder name in AppData directory /// </summary> public string TerminalID { get; } /// <summary> /// Flag of whether the terminal is currently running or not /// </summary> public bool IsActive => Process.StartInfo.FileName != "" && !Process.HasExited; #region .ex5 files relative paths /// <summary> /// List of full EA names /// </summary> public List<string> Experts => GetEX5FilesR(MQL5Directory.GetDirectory("Experts")); /// <summary> /// List of full indicator names /// </summary> public List<string> Indicators => GetEX5FilesR(MQL5Directory.GetDirectory("Indicators")); /// <summary> /// List of full script names /// </summary> public List<string> Scripts => GetEX5FilesR(MQL5Directory.GetDirectory("Scripts")); #endregion /// <summary> /// Terminal launch /// </summary> public bool Run() { if (IsActive) return false; // Set path to the terminal Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe"); Process.StartInfo.WindowStyle = WindowStyle; // Set data for terminal launch (if any data were set) if (Config != null && File.Exists(Config.Path)) Process.StartInfo.Arguments = $"/config:{Config.Path} "; if (Login.HasValue) Process.StartInfo.Arguments += $"/login:{Login.Value} "; if (Profile != null) Process.StartInfo.Arguments += $"/profile:{Profile} "; if (Portable) Process.StartInfo.Arguments += "/portable"; // Notify the process of the need to call an Exit event after closing the terminal Process.EnableRaisingEvents = true; // Run the process and save the launch status to the IsActive variable return Process.Start(); } /// <summary> /// Wait for the terminal operation to complete /// </summary> public void WaitForStop() { if (IsActive) Process.WaitForExit(); } /// <summary> /// Stop the process /// </summary> public void Close() { if (IsActive) Process.Kill(); } /// <summary> /// Wait for the terminal operation to complete for a certain time /// </summary> public bool WaitForStop(int miliseconds) { if (IsActive) return Process.WaitForExit(miliseconds); return true; } /// <summary> /// Search for files with the Ex5 extension /// Search is performed recursively - files are searched in the specified folder and in all subfolders /// </summary> /// <param name="path">Path to the folder where search begins</param> /// <param name="RelativeDirectory">Folder relative to which oath is returned</param> /// <returns>List of paths to the found files</returns> private List<string> GetEX5FilesR(DirectoryInfo path, string RelativeDirectory = null) { if (RelativeDirectory == null) RelativeDirectory = path.Name; string GetRelevantPath(string pathToFile) { string[] path_parts = pathToFile.Split('\\'); int i = path_parts.ToList().IndexOf(RelativeDirectory) + 1; string ans = path_parts[i]; for (i++; i < path_parts.Length; i++) { ans = Path.Combine(ans, path_parts[i]); } return ans; } List<string> files = new List<string>(); IEnumerable<DirectoryInfo> directories = path.GetDirectories(); files.AddRange(path.GetFiles("*.ex5").Select(x => GetRelevantPath(x.FullName))); foreach (var item in directories) files.AddRange(GetEX5FilesR(item, RelativeDirectory)); return files; } /// <summary> /// Terminal closing event /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Process_Exited(object sender, EventArgs e) { TerminalClosed?.Invoke(this); } /// <summary> /// Check the correctness of the passed terminal path /// </summary> private void CheckDirectories() { if (!TerminalInstallationDirectory.Exists) throw new ArgumentException("PathToTerminalInstallationDirectory doesn`t exists"); if (!TerminalChangeableDirectory.Exists) throw new ArgumentException("PathToTerminalChangeableDirectory doesn`t exists"); if (!TerminalInstallationDirectory.GetFiles().Any(x => x.Name == "terminal64.exe")) throw new ArgumentException($"Can`t find terminal (terminal64.exe) in the instalation folder {TerminalInstallationDirectory.FullName}"); } }
このクラスは ITerminalManager インターフェイスを実装していません (前回の時点)。 開発プロセスをスピードアップし、プロジェクトの数を最小限に抑えるために、記述されたアプリケーションを実装するときに単体テストを使用しないことにしました。 その結果、このオブジェクトにインターフェイスは必要ありません。
次の変更は、ターミナルが実行されているかどうかを判断する新しい方法に関するものです。 以前のバージョンでは、プロパティは Run メソッド (false 値が割り当てられたメソッド) と最適化完了コールバックから値を受け取りました。 しかし、あまり良い決断ではなく、時にはうまくいかないかもしれません。 したがって、IsActiveプロパティ取得を変更しました。 ここで、getter はProcessオブジェクトの HasExited プロパティに直接アクセスします。 ただし、最初の起動の前にプロパティにアクセスしようとすると、エラー メッセージが生成されます。 プロセスクラスの詳細を調べて、説明されたオブジェクトを介してプロセスを開始すると、そのStartInfo.FileNameプロパティが実行可能ファイルへのパスで満たされていることがわかります。 最初の起動前は空の値 ("") です。 これがIsActiveゲッターが奇妙に見える理由です。 最初に、getter は名前が存在するかどうかを確認し、次に Process.HasExited プロパティをチェックします。 つまり、ターミナルはデフォルトで閉じられ、TerminalManager クラスを通じてしか開始できないものとします。 したがって、StartInfo.FileName == """の場合は、false を返します(ターミナルが実行されていない)。 ターミナルが起動した場合は、HasExited プロパティの値を比較します。 このプロパティ値は、ターミナルが起動するたびに、オブジェクトから開始された場合、およびシャットダウンされる時点で変化します。 この関数により、自動オプティマイザを使用する場合は、常にターミナルを閉じるようにしてください。
この説明は、内部構造が変更された直近のオブジェクトによって完了します。 SetFileManagerクラスとそのUpdateParamsメソッドです。
/// <summary> /// Clear all recorded data in Params and load data from the required file /// </summary> public virtual void UpdateParams() { _params.Clear(); using (var file = FileInfo.OpenText()) { string line; while ((line = file.ReadLine()) != null) { if (line[0].CompareTo(';') != 0 && line[0].CompareTo('#') != 0) { string[] key_value = line.Replace(" ", "").Split('='); string[] value_data = key_value[1].Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); ParamsItem item = new ParamsItem { Variable = key_value[0], Value = (value_data.Length > 0 ? value_data[0] : null), Start = (value_data.Length > 1 ? value_data[1] : null), Step = (value_data.Length > 2 ? value_data[2] : null), Stop = (value_data.Length > 3 ? value_data[3] : null), IsOptimize = (value_data.Length > 4 ? value_data[4].CompareTo("Y") == 0 : false) }; _params.Add(item); } } } }
このクラスの変更は 1 つのメソッドにしか関係しないため、ここでは完全なクラス コードは提供しません。 アプリケーションテスト中に見つけたのは、時にはオプティマイザ用のターミナルによって生成されたロボットパラメータを含む*.setファイルが、パラメータで半分空になることがあるということです。 たとえば、ターミナルがValueフィールドにインプットし、最適化の初期値または最終値をインプットできない場合があります。 これはパラメータの型によって異なります。 たとえば、文字列パラメータは Value フィールドにのみインプットします。 以下のコードの変更を実装する目的は、上記の問題を回避するためでしました。
データディレクトリ構造
以前の記事では、自動オプティマイザによって作成された最適化レポートやその他のタスクファイルを格納するローカル "Data" ディレクトリについて既に説明しました。 次に、ディレクトリをより詳細に検討します。 Data ディレクトリは、ターミナルの起動時に実行可能ファイルの近くに作成されます。 このディレクトリは、自動オプティマイザの起動時に存在しない場合にのみ作成されます。 それ以外の場合、そのパスは、以下のクラスの適切なプロパティに保存されます。 このディレクトリは、タスクフォルダと記憶域として同時に機能します。 ファイルにアクセスして保存する必要がある場合は、このディレクトリ内で実行します。 次のオブジェクトは、ディレクトリを作成して格納します。
/// <summary> /// The object describing the Data directory with the auto optimizer's mutable files. /// </summary> class WorkingDirectory { /// <summary> /// Default constructor /// </summary> public WorkingDirectory() { // Create a root directory with mutable files WDRoot = new DirectoryInfo("Data"); if (!WDRoot.Exists) WDRoot.Create(); // Create a subdirectory with optimization reports Reports = WDRoot.GetDirectory("Reports", true); } /// <summary> /// Nested directory with optimization reports /// </summary> public DirectoryInfo Reports { get; } /// <summary> /// Root directory with mutable files and folders /// </summary> public DirectoryInfo WDRoot { get; } /// <summary> /// Get or create (if not previously created) a directory nested inside the Reports directory. /// The resulting directory stores the results of a particular optimization pass. /// </summary> /// <param name="Symbol">The symbol on which the optimization was performed</param> /// <param name="ExpertName">Robot name</param> /// <param name="DirectoryPrefix">Prefix added to the directory name</param> /// <param name="OptimiserName">The name of the use optimizer</param> /// <returns> /// Path to the directory with the optimization results. /// The name of the directory is formed as follows: public DirectoryInfo WDRoot { get; } /// {DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol} /// </returns> public DirectoryInfo GetOptimisationDirectory(string Symbol, string ExpertName, string DirectoryPrefix, string OptimiserName) { return Reports.GetDirectory($"{DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol}", true); } /// <summary> /// Path to Data/Tester /// Needed to temporarily move files from the terminal directory of the same name /// </summary> public DirectoryInfo Tester => WDRoot.GetDirectory("Tester", true); }
このクラスは、記述されたディレクトリのマネージャーとして機能します。 自動オプティマイザの実行可能ファイルの場所に関係なく、このオブジェクトのWDRootプロパティにアクセスすることで、目的のディレクトリへの正しいパスを常に取得できるため、便利です。 このコンストラクタでは、データディレクトリがまだ存在しない場合に作成します。 それ以外の場合は、上記のプロパティにアドレスを保存します。 また、"レポート" サブディレクトリへのパスを保存します。 渡された true パラメータは、ディレクトリが存在しない場合は作成する必要があります。
その結果、Data ディレクトリは最初の起動の直後に作成されます。 作成後、ディレクトリには"Reports" というサブディレクトリが 1 つだけ存在し、空になります。 最適化またはテストの最初の起動中に、Tester サブディレクトリは、記述されたオブジェクトの適切なプロパティへの呼び出しによって作成されます。 構成ファイル {Terminal ID}.ini は、デフォルトで選択した構成ファイルをコピーして作成されます。 したがって、ソース構成ファイルの上書きを回避できます。 Testerディレクトリは、以前に実行された最適化のキャッシュを一時的にコピーするために作成されます。 部分的に、変更可能なターミナルディレクトリの中で利用可能な関連するテスターディレクトリに似ています。
このディレクトリには cache" フォルダのみがあります。 選択したターミナルの同じディレクトリにあるすべてのファイルが、このキャッシュフォルダに移動されます。 最適化プロセスが終了すると、ファイルは前の場所に戻されます。 この操作により、最適化プロセスが確実に実行されます。 オプティマイザーのロジックによれば、ターミナルディレクトリに最適化プロセスを記述するファイルがある場合、オプティマイザは新しいプロセスを開始する代わりに、以前に実行された最適化をロードします。 これは多くの時間を節約する素晴らしいソリューションです。 しかし、我々の目的には全く適していません。 自動オプティマイザ用に調整された最適化レポートのコピーを独自に保存するため(現在のシリーズの記事1と記事1)、レポートを作成する必要があります。 レポートを生成するには、最適化プロセスを開始する必要があります。 これが、このファイルの存在をエミュレートする理由です。 これを行うには、一時的にファイルをローカルディレクトリに移動します。 最適化プロセスが正常に完了すると、GetOptimisationDirectoryメソッドを使用して、レポート ディレクトリにサブディレクトリが作成されます。
上のスクリーンショットでは、最適化の起動前に自動オプティマイザの設定で指定されたディレクトリプレフィックスが色で表示されます。 これで同じEAの異なる最適化を区別することができます。 各ディレクトリには、最適化の結果を含む 3 つのファイルが格納されます。
- Forward.xml.xml — フォワードテストのみ
- History.xml - ヒストリーテストのみ
- Report.xml — すべての過去の期間を通過した場合に実行されたすべての最適化。
ファイルの構造は、このシリーズの最初の記事で説明した構造が似ています。 GUI の Load ボタンをクリックすると、自動オプティマイザは選択したディレクトリから該当するテーブルに 3 つのファイルをすべてロードします。 3 つのファイルのいずれかが見つからない場合、またはファイルがすべて存在しない場合は、適切なメッセージが生成されます。 不足しているファイルに対応するテーブルは空白で表示されます。
あるコンピュータ上の自動オプティマイザプログラムから別のコンピュータにある自動オプティマイザプログラムに最適化結果を移動する必要がある場合は、Reportsディレクトリをコピーして、2台目のコンピュータの該当するディレクトリに移動します。 起動後、自動オプティマイザは目的のディレクトリにアクセスし、結果を得ます。 したがって、結果はダウンロードとさらなる分析に利用可能になります。
結論
このシリーズの最初の記事では、最適化レポートの作成とアップロードについて検討しました。 次に、オートオプティマイザプロジェクトの検討に進みます。 前回の記事では、準備完了プロジェクトを分析しました。 このシリーズの最終的な目的を提示しようとしました。 また、前の記事では、完成した自動オプティマイザを使用するステップを説明します。 この記事では、オートオプティマイザの実装における技術的側面を考察しました。 プロジェクトの論理部分の分析に移る前に、前の一連の記事から借りたファイルのグラフィカルインタフェースと変更を検討しました。 この記事の概要では、前の一連の記事へのリンクが提供されています。 次の記事では、プログラムの論理部分の実装について検討します。
添付ファイルには、第4の記事で分析されたトレーディングロボットを備えた自動オプティマイザプロジェクトがあります。 このプロジェクトを使用するには、自動オプティマイザプロジェクトファイルとテストロボットファイルをコンパイルしてください。 次に、(最初の記事で説明した) ReportManager.dllを MQL5/Libraries ディレクトリにコピーすると、EAのテストを開始できます。 オートオプティマイザをEAに接続する方法の詳細については、このシリーズの記事3と記事4を参照してください。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/7583
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索