最適化管理(パートII): キーオブジェクトとアドオンロジックの作成
目次
はじめに
本稿では、複数のターミナルで同時に最適化を管理ための便利なGUI作成プロセスについて詳しく説明します。前回の記事では、コンソールからターミナルを起動する方法を検討し、構成ファイルの説明が含まれていました。ここでは、ターミナルのC#ラッパーの作成に進みます。これにより、サードパーティプロセスとしての最適化管理が可能になります。以前に検討されたGUIにはロジックがなく、アクションを実行できませんでした。できるのは、押されたキーのテキストをコンソール(起動元)に出力することだけでした。本稿ではGUIイベントを処理するロジックが追加され、組み込みロジックが実装されます。ファイルを操作する多くのオブジェクトが作成されます。ファイルを操作する代わりに、これらのオブジェクトを介してプログラム操作のロジック部分を実装できるようになるので、操作が簡単になり、コードがわかりやすくなります。本稿では、アプリケーションは最終的にビデオに示されている形式を取ります。
外部ターミナルマネージャ(ITerminalManagerおよびConfig)
前に、アドオンのグラフィカルレイヤーの作成について検討しましたが、この部分では、論理コンポーネントの作成方法を検討します。 OOPの利点を使用して、論理部分はいくつかのクラスに分割され、それぞれが特定の領域を担当します。 ファイルとターミナルに関連する特定のアクションを実行するクラスから始めましょう。その後、結果のExtentionGUI_Mクラスに進みます。このクラスには、最終的なロジックが記述されます。ロジックの実装で使用されるクラスから始めましょう。
この章では、ターミナルでの操作について説明し、次のコンポーネントが考慮されます。
- 構成ファイルとの作業
- サードパーティのプロセスのようにターミナルで作業
構成ファイルから始めましょう。詳細はターミナルレフェレンスでご覧になれます。まず、必要なすべての変数を作成する必要があります。これを構成ファイルに適用します。これらの変数はConfig.csファイルで実装され、その数値はターミナルで表示されます。サーバアドレスを渡すための便利なメソッドの実装を見てみましょう。サーバアドレスは追加のポート番号を指定した特定の形式で渡す必要があることに注意してください。この問題は、コンストラクタを介して受信したサーバアドレスを格納するクラスを作成することで解決されます。インストールの前にその正確性を確認します。
/// <概要> /// IPv4 server address and port /// </概要> class ServerAddressKeeper { public ServerAddressKeeper(IPv4Adress ip, uint port) { IP = ip; Port = port; } public ServerAddressKeeper(string adress) { if (string.IsNullOrEmpty(adress) || string.IsNullOrWhiteSpace(adress)) throw new ArgumentException("adress is incorrect"); string[] data = adress.Split(':'); if (data.Length != 2) throw new ArgumentException("adress is incorrect"); IP = new IPv4Adress(data[0]); Port = Convert.ToUInt32(data[1]); } public IPv4Adress IP { get; } public uint Port { get; } public string Address => $"{IP.ToString()}:{Port}"; } /// <概要> /// IPv4 server address /// </概要> struct IPv4Adress { public IPv4Adress(string adress) { string[] ip = adress.Split('.'); if (ip.Length != 4) throw new ArgumentException("ip is incorrect"); part_1 = (char)Convert.ToInt32(ip[0]); part_2 = (char)Convert.ToInt32(ip[1]); part_3 = (char)Convert.ToInt32(ip[2]); part_4 = (char)Convert.ToInt32(ip[3]); } public char part_1; public char part_2; public char part_3; public char part_4; public new string ToString() { return $"{(int)part_1}.{(int)part_2}.{(int)part_3}.{(int)part_4}"; } }
このクラスには、サーバのIPアドレスを格納するIPv4Adress構造体が含まれます。私が本稿のデータを準備していた際に、IPv4形式とは異なるサーバアドレスは一度も見たことがなかったため、この形式が実装されています。コンストラクタでは、構造体はアドレスを含む文字列を受け取り、解析して適切なフィールドに保存します。アドレスの桁数が4未満の場合、エラーが返されます。メインクラスコンストラクタには2つのオーバーロードがあり、1つはサーバーアドレスの文字列を受け入れ、もう1つは形成されたIPアドレスとポート番号を受け入れます。さらに、IPv4Adress構造には、オーバーロードされたToStringメソッドがあります。このメソッドは、すべてのC#オブジェクトが暗黙的に継承されるObject基本クラスから派生したものです。 ServerAddressKeeperクラスにはAddressプロパティがあり、同じオブジェクトを実装します。結果的に、便利な形式でサーバアドレスを保存し、構成ファイルに必要な形式にアセンブルできるラッパークラスになります。
ここで、*.ini形式の構成ファイルで作業するためのメソッドを検討する必要があります。すでに述べたように、このファイル形式は時代遅れであると見なされ、現在ほとんど使用されていません。C#には、これらのファイルで作業するための組み込みインターフェイスがありません。これは、前の記事で検討したXMLマークアップで作業するためのインターフェイスに似ています。ただし、WinApiでは、このファイル形式で作業するためのWritePrivateProfileStringおよびGetPrivateProfileString関数が引き続きサポートされています。Microsoftからのメモは次のとおりです。
この関数は、16ビットWindowsベースのアプリケーションとの互換性のためにのみ提供されています。アプリケーションは初期化情報をレジストリに保存する必要があります。
これを使用して、独自のソリューションを開発する必要を回避できます。これを行うには、C関数のデータをC#コードにインポートする必要があります。C#では、これはMQL5と同様に実行できます。
[DllImport("kernel32.dll", SetLastError = true)] private extern static int GetPrivateProfileString(string AppName, string KeyName, string Default, StringBuilder ReturnedString, int Size, string FileName); [DllImport("kernel32.dll", SetLastError = true)] private extern static int WritePrivateProfileString(string AppName, string KeyName, string Str, string FileName);
#importとは対照的に、ここではDLLImport属性を指定し、それに関数のインポート元であるdllの名前と、その他のオプションのパラメータを渡す必要があります。特に、ここではインポート中にパラメータSetLastErro = trueを指定しました。 これにより、C#コードでGetLastError()を使用してC++コードからエラーを受け取り、これらのメソッドの正しい実行を制御できます。C#とCは文字列で作業するための異なるメソッドを備えているため、エクスポートされた関数を使用して作業を簡単にし、起こりうるエラーを処理できるラッパーメソッドを使用します。次の方法で実装しました。
/// <概要> /// Convenient wrapper for WinAPI function GetPrivateProfileString /// </概要> /// <param name="section">section name</param> /// <param name="key">key</param> /// <returns>the requested parameter or null if the key was not found</returns> protected virtual string GetParam(string section, string key) { //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()); //Return the received value return buffer.Length == 0 ? null : buffer.ToString(); } /// <概要> /// Convenient wrapper for WinAPI WritePrivateProfileString /// </概要> /// <param name="section">Section</param> /// <param name="key">Key</param> /// <param name="value">Value</param> protected virtual void WriteParam(string section, string key, string value) { //Write value to the INI-file if (WritePrivateProfileString(section, key, value, Path) == 0) ThrowCErrorMeneger("WritePrivateProfileString", Marshal.GetLastWin32Error()); } /// <概要> /// Return error /// </概要> /// <param name="methodName">Method name</param> /// <param name="er">Error code</param> private void ThrowCErrorMeneger(string methodName, int er) { 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/ru-ru/windows/desktop/Debug/system-error-codes) for detales"); } } }
これらのメソッドを使用しているときに、興味深い特徴を見付けたので、追加情報を検索して、これが私の問題だけではないことを確認しました。その特徴というのは、GetPrivateProfileStringメソッドがファイルが見つからないときだけでなく次の場合にも ERROR_FILE_NOT_FOUND(エラーコード= 2)を返すことです。
- セクションが読み取られたファイルに存在しない
- 要求されたキーが存在しない
したがって、エラーが発生した場合にThrowCErrorMenegerメソッドでファイルが存在するかどうかを確認することにします。最後のエラーを取得する(GetLastErrorメソッド)ために、C#にはMarshalクラスの静的メソッド(Marshal.GetLastWin32Error())があり、これを使用して各ファイルの読み取りまたは書き込みメソッドの呼び出し後にエラーを取得します。任意のデータ型を文字列にキャストできるため、便宜上、文字列のみを読み書きするメソッドだけがインポートされました。
もう1つの興味深い関数操作の側面は、ファイルからデータを削除する方法です。たとえば、セクション全体を削除するには、WriteParamメソッドにnullに等しいキー名を渡す必要があります。この可能性を使用して、適切なメソッドを作成しました。すべてのセクション名は以前にENUM_SectionTypeに追加されました。
/// <概要> /// Section deletion /// </概要> /// <param name="section">section selected for deletion</param> public void DeleteSection(ENUM_SectionType section) { WriteParam(section.ToString(), null, null); }
キー名を指定する必要があるがキー値はnullでいなければならない場合に、特定のキーを削除するメソッドもあります。各セクションがほとんど一意のキーを持っているため、このメソッドの実装では、渡されたキーの名前は文字列フィールドとして残されます。
/// <概要> /// Key deletion /// </概要> /// <param name="section">section from which key should be deleted</param> /// <param name="key">Key to delete</param> public void DeleteKey(ENUM_SectionType section, string key) { if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key)) throw new ArgumentException("Key is not vailed"); WriteParam(section.ToString(), key, null); }
セクションへのアクセスを便利にするために、プロパティを介して実装することにしました。下に示すように、Configクラスインスタンスはポイント演算子(.)を介して任意のセクションにアクセスし、このセクションの任意のキーにアクセスできるようになります。
Config myConfig = new Config("Path"); myConfig.Tester.Expert = MyExpert; string MyExpert = myConfig.Tester.Expert;
このアイデアを実装するには明らかに、セクションごとにクラスを作成する必要があります。すべてのセクションのクラスで、ファイル内のこの特定の文字列を読み書きするプロパティを指定する必要があります。セクションは実際にはこの特定の初期化ファイルのコンポーネントであり、Configはこのファイルのオブジェクト指向的表現であるので、合理的な解決策は、これらのセクションをConfigクラスのネストされたクラスとして記述するクラスを作成してConfigクラスで読み取り専用プロパティを設定することです、これらの特定のクラスによる型である必要があります。以下の例では、不必要なコードを省略して、上記の説明のみを示します。
class Config { public Config(string path) { Path = path; CreateFileIfNotExists(); Common = new CommonSection(this); Charts = new ChartsSection(this); Experts = new ExpertsSection(this); Objects = new ObjectsSection(this); Email = new EmailSection(this); StartUp = new StartUpSection(this); Tester = new TesterSection(this); } protected virtual void CreateFileIfNotExists() { if (!File.Exists(Path)) { File.Create(Path).Close(); } } public readonly string Path; // path to file public virtual Config DublicateFile(string path) { File.Copy(Path, path, true); return new Config(path); } #region Section managers internal class CommonSection { } internal class ChartsSection { } internal class ExpertsSection { } internal class ObjectsSection { } internal class EmailSection { } internal class StartUpSection { } internal class TesterSection { } #endregion public CommonSection Common { get; } public ChartsSection Charts { get; } public ExpertsSection Experts { get; } public ObjectsSection Objects { get; } public EmailSection Email { get; } public StartUpSection StartUp { get; } public TesterSection Tester { get; } }
特定のセクションを記述するネストされたクラスのそれぞれの実装は似ています。 Config.ChartsSectionクラスの例を使用して検討してください。
internal class ChartsSection { private readonly Converter converter; public ChartsSection(Config parent) { converter = new Converter(parent, "Charts"); } public string ProfileLast { get => converter.String("ProfileLast"); set => converter.String("ProfileLast", value); } public int? MaxBars { get => converter.Int("MaxBars"); set => converter.Int("MaxBars", value); } public bool? PrintColor { get => converter.Bool("PrintColor"); set => converter.Bool("PrintColor", value); } public bool? SaveDeleted { get => converter.Bool("SaveDeleted"); set => converter.Bool("SaveDeleted", value); } }
セクションを記述するクラスには、別の中間クラスを使用してファイルの読み書きを行うNullableセクションが含まれています。 クラスの実装は後で検討されます。 ここで、戻りデータに注意を向けたいと思います。クラスがファイルに書き込まれていない場合、ラッパークラスはキー値の代わりにnullを返します。キープロパティにnullを渡すと、この値は単に無視されます。フィールドを削除するには、上記で検討した特別なDeleteKeyメソッドを使用します。
次に、ファイルからデータを読み書きするConverterクラスについて検討してみましょう。このクラスはネストされたクラスであるため、「protected」アクセス修飾子でマークされているメインクラスのWriteParamメソッドとGetParamメソッドを使用できます。 このクラスには、次の型の読み取りおよび書き込みメソッドのオーバーロードがあります。
- Bool
- Int
- Double
- String
- DateTime
他のすべての型は、最も適切な型の1つにキャストされます。下記にクラスの実装を示します。
private class Converter { private readonly Config parent; private readonly string section; public Converter(Config parent, string section) { this.parent = parent; this.section = section; } public bool? Bool(string key) { string s = parent.GetParam(section, key); if (s == null) return null; int n = Convert.ToInt32(s); if (n < 0 || n > 1) throw new ArgumentException("string mast be 0 or 1"); return n == 1; } public void Bool(string key, bool? val) { if (val.HasValue) parent.WriteParam(section, key, val.Value ? "1" : "0"); } public int? Int(string key) { string s = parent.GetParam(section, key); return s == null ? null : (int?)Convert.ToInt32(s); } public void Int(string key, int? val) { if (val.HasValue) parent.WriteParam(section, key, val.Value.ToString()); } public double? Double(string key) { string s = parent.GetParam(section, key); return s == null ? null : (double?)Convert.ToDouble(s); } public void Double(string key, double? val) { if (val.HasValue) parent.WriteParam(section, key, val.Value.ToString()); } public string String(string key) => parent.GetParam(section, key); public void String(string key, string value) { if (value != null) parent.WriteParam(section, key, value); } public DateTime? DT(string key) { string s = parent.GetParam(section, key); return s == null ? null : (DateTime?)DateTime.ParseExact(s, "yyyy.MM.dd", null); } public void DT(string key, DateTime? val) { if (val.HasValue) parent.WriteParam(section, key, val.Value.ToString("yyyy.MM.dd")); } }
このクラスは、渡されたデータを期待される形式に変換してファイルに書き込みます。ファイルから読み取る場合、文字列を戻りファイルに変換し、結果を特定のセクションを記述するクラスに渡します。このクラスは値を期待される形式に変換します。プロパティにアクセスする場合、このクラスはファイルとの間でデータを直接読み書きすることにご注意ください。ファイルで作業するときに実際のデータが確保されますが、メモリアクセスに比べて時間がかかる場合があります。読み取りと書き込みにかかる時間はマイクロ秒単位なので、この遅延はプログラムの動作中に顕著ではありません。
次に、ターミナル操作マネージャについて考えてみましょう。このクラスの目的は、ターミナルの起動と停止、ターミナルが実行中かどうかに関するデータを取得する可能性、構成ファイルと起動フラグの設定です。言い換えると、このクラスは、ターミナルのマニュアルに記載されているすべてのターミナル起動方法を理解し、ターミナル操作プロセスの管理を可能にする必要があります。これらの要件に基づいて、必要なプロパティとメソッドのシグネチャを記述する次のインターフェイスが作成されました。ターミナルでのさらなる操作は、以下に示すインターフェイスを介して実装されます。
interface ITerminalManager { uint? Login { get; set; } string Profile { get; set; } Config Config { get; set; } bool Portable { get; set; } System.Diagnostics.ProcessWindowStyle WindowStyle { get; set; } DirectoryInfo TerminalInstallationDirectory { get; } DirectoryInfo TerminalChangeableDirectory { get; } DirectoryInfo MQL5Directory { get; } List<string> Experts { get; } List<string> Indicators { get; } List<string> Scripts { get; } string TerminalID { get; } bool IsActive { get; } bool Run(); void Close(); void WaitForStop(); bool WaitForStop(int miliseconds); event Action<ITerminalManager> TerminalClosed; }
インターフェイスからわかるように、最初の4つのプロパティは、レフェレンスやGUI作成の部分で説明されているフラグの値を受け入れます。5番目のフラグは、開始時にターミナルウィンドウのサイズを設定し、ターミナルを最小化したり、フルモードまたはスモールウィンドウモードで起動したりできます。ただし、(ウィンドウを非表示にするために)Hidden値が選択されている場合、予期される動作は実行されません。 ターミナルを非表示にするには、別の初期化ファイルを編集する必要があります。ただし、動作は重要なので、コードを複雑にしないことにしました。
このクラスはインターフェイスを継承し、2つのコンストラクタオーバーロードを持ちます。
public TerminalManager(DirectoryInfo TerminalChangeableDirectory) : this(TerminalChangeableDirectory, new DirectoryInfo(File.ReadAllText(TerminalChangeableDirectory.GetFiles().First(x => x.Name == "origin.txt").FullName)), false) { } public TerminalManager(DirectoryInfo TerminalChangeableDirectory, DirectoryInfo TerminalInstallationDirectory, bool isPortable) { this.TerminalInstallationDirectory = TerminalInstallationDirectory; this.TerminalChangeableDirectory = TerminalChangeableDirectory; TerminalID = TerminalChangeableDirectory.Name; CheckDirectories(); Process.Exited += Process_Exited; Portable = isPortable; }
Vladimir Karputovによる記事によると、可変ターミナルディレクトリにはファイル「origin.txt」があり、インストールディレクトリへのパスが保存されています。この事実は、最初のコンストラクタのオーバーロードで使用されます。このオーバーロードは ファイル「origin.txt」を検索し、ファイル全体を読み込んで DirectiryInfoクラスを作成します。このクラスはファイルから読み取ったデータを渡すことでこのディレクトリを記述します。 また、作業のためのクラスの準備に関連するすべてのアクションは、以下の3つのパラメータを受け入れる2番目のコンストラクタによって実行されることに注意してください。
- 変数ディレクトリへのパス(AppData内のパス)
- インストールディレクトリへのパス
- Portableモードでのターミナル起動のフラグ
このコンストラクタの最後のパラメータは、構成を簡単にするために追加されたので、コンストラクタの最後に意図的に割り当てる必要があります。ターミナルをPortableモードで起動すると、すべてのEAと指標が格納されるMQL5ディレクトリがターミナルインストールディレクトリに作成されます(以前に作成されていない場合)。ターミナルがPortableモードで実行されたことがない場合はこのディレクトリは存在しないため、このフラグが設定されている場合は、まずこのディレクトリが存在するかどうかを確認する必要があります。このフラグを設定して読み取るプロパティは、次のとおりです。
/// <概要> /// Flag of terminal launch in /portable mode /// </概要> 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(100); Close(); } WaitForStop(); WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal; } } }
渡された値を割り当てるとき、MQL5ディレクトリの存在が確認されます。 そのようなディレクトリがない場合は、ターミナルを起動し、起動が完了するまでスレッドを保持します。以前に設定されたターミナル起動フラグに従って、ターミナルはPortableモードで起動され、最初の起動時に目的のディレクトリが作成されます。 ターミナルが起動したら、ラッパーを閉じるコマンドを使用してターミナルを閉じ、完全に閉じられるまで待ちます。 その後、アクセスに問題がなければ、目的のMQL5ディレクトリが作成されます。ターミナルのMQL5フォルダへのパスを返すプロパティは、条件付きコンストラクトを介して機能します。上記のフラグに応じて、インストールディレクトリまたは可変ファイルがあるディレクトリから目的のディレクトリへのパスを返します。
/// <概要> /// Path to the MQL5 folder /// </概要> public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5");
コンストラクタのオーバーロードに関して別の注意が必要です。 可変ディレクトリの代わりに突然インストールディレクトリへのパスを渡した場合は、Portableモードで少なくとも1つの起動があった場合(またはisPortable =
trueフラグが設定されている場合)、このクラスは正しく動作するはずです。
ただし、ターミナルのインストールディレクトリのみが表示されます。この場合、TerminalIDはターミナルの可変ディレクトリに示されるような数字とラテン文字のセットではなく、ターミナルがインストールされているフォルダの名前(つまりインストールディレクトリ名)と同じになります。
また、ターミナルで利用可能な自動売買ロボット、指標、スクリプトに関する情報を提供するプロパティにも注意を払ってください。プロパティは、privateのGetEX5FilesRメソッドを介して実装されます。
#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 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; }
これらのプロパティでは、EAファイルへの可能なパスを取得するかわりに、Expertsフォルダ(指標の場合はIndicatorsフォルダ、スクリプトの場合はScriptsフォルダ)に相対したEAへのパスを取得します。選択中、クラスはファイル拡張子を確認します(EX5ファイルのみを検索します)。
見つかったEX5ファイルのリストを返すメソッドは、再帰を使用します。このメソッドを詳細に検討しましょう。まず、2番目のパラメータの値を確認します。このパラメータはオプションなので、パラメータが設定されていない場合、現在渡されているディレクトリの名前が割り当てられます。したがって、どのディレクトリファイルパスを生成する必要があるかを理解できます。使用される別のC#言語構造は、ネストされた関数です。これらの関数は、現在のメソッド内にのみ存在します。関数が使用されなくなるため、この構造を使用しました。その本体は大きすぎないため、このメソッド内に収めることができます。この関数は、EX5ファイルへのパスを入力として受け入れ、「\\」記号を区切り文字として使用して分割します。その結果、ディレクトリ名の配列を取得し、EX5ファイルの名前はこの配列の最後にあります。次のステップでは、ファイルへのパスが検索される相対ディレクトリのインデックスをi変数に割り当てます。 1増やして、ポインタを次のディレクトリまたはファイルに移動します。 「ans」変数は、見つかったアドレスを保存します。これを行うには、指定されたディレクトリの値をそれに割り当て、ループを終了するまで(つまり、目的のファイルの名前が追加されるまで)、ループに新しいディレクトリまたはファイルを追加します。 GetEX5FilesRメソッドは次のように動作します。
- ネストされたすべてのディレクトリへのパスを受け取る
- 現在のディレクトリでEX5ファイルを検索し、それらの相対パスを保存する
- ループ内で、ネストされた各ディレクトリに対して再帰的にパスを受け取る必要があるディレクトリの名前を渡し、 EX5ファイルの相対パスのリストに追加する
- 見つかったファイルパスを返す
したがって、このメソッドは完全なファイル検索を実行し、見つかったすべてのEAと他のMQL5実行可能ファイルを返します。
次に、C#言語でサードパーティのアプリケーションを起動する方法を考えてみましょう。この方法では外部プロセスのラッパーであるProcessクラスが起動され、他のアプリケーションの起動と操作に非常に便利な機能を備えています。たとえば、C#からメモ帳を起動するには、次の3行のコードを記述するだけです。
System.Diagnostics.Process Process = new System.Diagnostics.Process(); Process.StartInfo.FileName = "Notepad.exe"; Process.Start();
このクラスを使用して、アドオンからサードパーティのターミナルを管理するプロセスを実装します。ターミナルを起動するメソッドは次のとおりです。
public bool Run() { if (IsActive) return false; // Set path to terminal Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe"); Process.StartInfo.WindowStyle = WindowStyle; // Set data for terminal launch (if any were installed) 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 (IsActive = Process.Start()); }
起動前にターミナルを構成する場合、次のことを行う必要があります。
- 起動する実行可能ファイルへのパスを指定する
- プロセスのウィンドウタイプを設定する
- キーを設定する(コンソールアプリでは、これらはすべて起動するファイルの名前の後に指定された値)
- Process.EnableRaisingEvents= trueフラグを設定する(このフラグが設定されていない場合、プロセス終了イベントはトリガーされない)
- プロセスを開始し、IsActive変数に起動ステータスを保存する
IsActiveプロパティは、ターミナルが閉じられた後にトリガーされるコールバックで再びfalseになります。このコールバックではTerminalClosedイベントも呼び出されます。
/// <概要> /// Terminal closing event /// </概要> /// <param name="sender"></param> /// <param name="e"></param> private void Process_Exited(object sender, EventArgs e) { IsActive = false; TerminalClosed?.Invoke(this); }
他のターミナル管理メソッド(ターミナルの停止と閉鎖を待機)は、Processクラスの標準メソッドのラッパーです。
public void WaitForStop() { if (IsActive) Process.WaitForExit(); } /// <概要> /// Stop the process /// </概要> public void Close() { if (IsActive && !Process.HasExited) Process.Kill(); } /// <概要> /// Wait for terminal completion for specified time /// </概要> public bool WaitForStop(int miliseconds) { if (IsActive) return Process.WaitForExit(miliseconds); return true; }
したがって、標準のProcessクラスを使用して、MetaTrader 5ターミナルで動作する便利なラッパーを作成しました。これにより、Processクラスを直接使用するよりも、ターミナルでより便利な操作が可能になります。
ディレクトリ構造オブジェクト
ディレクトリの操作は既に記事の最初の部分で検討したので、次に、ファイルシステムにアクセスするメソッドについて説明します。ファイルとディレクトリのパスの作成に使用するメソッドから始めましょう。C#はこのために便利なPathクラスを備えています。ファイルやディレクトリへのパスを安全に作成できるため、発生する可能性のあるエラーも排除されます。ディレクトリはDirectoryInfoクラスを使用して示され、ネストされたディレクトリ、親ディレクトリ、ディレクトリ名およびそのフルパスに関するデータをすばやく取得できるだけでなく、他の多くの有用なプロパティにアクセスできます。たとえば、このクラスを使用すると、メソッドを1つだけ呼び出すことでディレクトリ内のすべてのファイルを取得できます。 FileInfoクラスは、任意のファイルのオブジェクト指向的表現に使用されます。このクラスは、機能に関してDirectoryInfoに類似しています。その結果、ファイルとディレクトリを使用した操作全体が、提示されたクラスを使用した操作として実装されます。これにより、主な問題に焦点を当てることができ、中間関数やメソッドを作成する必要はほとんどありません。
前述のTerminalManagerクラスでは、DirectoryInfoクラスインスタンスのGetDirectoryメソッドがよく使用されました。このメソッドは、標準のDirectoryInfoクラスレイアウトには含まれておらず、便宜上追加されました。 C#では、拡張メソッドを追加することによって標準およびカスタムクラスの機能を拡張する方法があります。ここでは、このC#言語の機能を使用して、GetDirectory拡張メソッドを追加しました。
static class DirectoryInfoExtention { public static DirectoryInfo GetDirectory(this DirectoryInfo directory, string Name, bool createIfNotExists = false) { DirectoryInfo ans = new DirectoryInfo(Path.Combine(directory.FullName, Name)); if (!ans.Exists) { if (!createIfNotExists) return null; ans.Create(); } return ans; } }
拡張メソッドを作成するには静的クラスを作成して、その中にpublicの静的メソッドを作成するする必要があります。最初のパラメータの型は、拡張機能を作成する対象の型を使用して設定し、キーワード「this」を前に付ける必要があります。 このパラメータは、自動拡張メソッドの呼び出し中に指定されます。明示的に関数に渡す必要はありませんが、これは拡張機能が作成されたクラスインスタンスそのものです。 拡張メソッドを格納するクラスのサンプルを作成する必要はありませんが、すべての拡張メソッドは、作成されたクラスのメソッドセットに自動的に追加されます。 特定のメソッドは、次のアルゴリズムに従って動作します。
- パラメータcreateIfNotExistsがfalse(または指定されていない)の場合、渡された名前を持つサブフォルダをDirectoryInfo型に渡したものを返す(このフォルダが存在する場合)、またはnullを返す
- createIfNotExistsがtrueの場合、フォルダが作成されていない場合にはフォルダを作成し、DirectoryInfo型にキャストされたフォルダを返す
可変ターミナルディレクトリのフォルダでの便利な操作のために、ディレクトリのオブジェクト指向的表示であるクラスを作成しました。
~\AppData\Roaming\MetaQuotes\Terminal
クラスは次のように実装されます。
class TerminalDirectory { public TerminalDirectory() : this(Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "MetaQuotes", "Terminal")) { } public TerminalDirectory(string path) { pathToTerminal = path; } private readonly string pathToTerminal; public List<DirectoryInfo> Terminals { get { List<DirectoryInfo> ans = new List<DirectoryInfo>(); string[] dir_array = Directory.GetDirectories(pathToTerminal); foreach (var item in dir_array) { string pathToOrigin = Path.Combine(pathToTerminal, item, "origin.txt"); if (!File.Exists(pathToOrigin)) continue; if (!File.Exists(Path.Combine(File.ReadAllText(pathToOrigin), "terminal64.exe"))) continue; ans.Add(new DirectoryInfo(Path.Combine(pathToTerminal, item))); } return ans; } } public DirectoryInfo Common => new DirectoryInfo(Path.Combine(pathToTerminal, "Common")); public DirectoryInfo Community => new DirectoryInfo(Path.Combine(pathToTerminal, "Community")); }
クラスには以下の3つのフィールドがあります。
- Terminals
- Common
- Community
これらのフィールドは、このディレクトリ内のサブディレクトリの名前に対応しています。Terminalsプロパティは、ターミナルファイルシステムに属するディレクトリのリストを返します。多くの場合、ターミナルの削除後、ターミナルフォルダはまだこのディレクトリに残っているため、ディレクトリの関連性の確認を追加することにしました。確認は、次の基準に従って実行されます。
- 分析対象ディレクトリのルートの「origin.txt」ファイルの存在(このファイルにより、ターミナルディレクトリパスにアクセスできます)
- 適切なディレクトリ内の実行可能なターミナルファイルの存在
拡張機能は64ビットターミナルバージョン用に設計されていることにご注意ください。32ビットバージョンで動作するには、プログラムのすべての場所(TerminalManagerおよび現在説明されているクラス)でterminal64.exeをterminal.exeに変更する必要があります。したがって、実行可能なターミナルファイルが見つからないディレクトリは無視されます。
最初のコンストラクタの検討に移ります。このコンストラクタは、ターミナルファイルディレクトリへのパスの自動生成を有効にします。
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData)
Environmentクラスでは「AppData」へのパスを自動的に取得できるため、ユーザ名を指定する必要はありません。この行のおかげで、拡張機能は標準のインストール方法を使用してPCにインストールされたすべてのターミナルのリストを見つけることができます。
ターミナルデータディレクトリを持つフォルダを記述するクラスに加えて、拡張機能には、一時ファイルと最適化レポートが保存される独自のディレクトリがあります。このディレクトリを記述するクラスは次のとおりです。
class OptimisationExtentionWorkingDirectory { public OptimisationExtentionWorkingDirectory(string DirectoryName) { DirectoryRoot = CreateIfNotExists(DirectoryName); Configs = CreateIfNotExists(Path.Combine(DirectoryName, "Configs")); Reports = CreateIfNotExists(Path.Combine(DirectoryName, "Reports")); } public DirectoryInfo DirectoryRoot { get; } public DirectoryInfo Configs { get; } public DirectoryInfo Reports { get; } protected DirectoryInfo CreateIfNotExists(string path) { DirectoryInfo ans = new DirectoryInfo(path); if (!ans.Exists) ans.Create(); return ans; } }
クラスコンストラクタからわかるように、作成中にルートディレクトリとネストされたディレクトリの存在を確認し、存在しない場合は作成されます。
- 「DirectoryRoot」は、アドオンがファイルとディレクトリを格納するメインディレクトリです。
- 「Configs」は、構成ファイルをコピーして変更し、ターミナルの起動時に入力パラメータとして設定するディレクトリです。
- 「Reports」ディレクトリには、各テスト後に読み込まれるレポートと最適化設定を含むファイルとフォルダの構造が保存されます。
Reportsディレクトリの内部構造は、「OptimisationManager」クラスで作成され、完了時に最適化ごとに形成されます。次で構成されています。
- ターミナルIDと同じ名前のディレクトリ
- ロボット名と同じ名前のディレクトリ(以下が含まれます)
- Settings.xml — 最適化設定を含むファイル(プログラム内で形成)
- History.xml — コピーされた最適化履歴ファイル(ターミナルにより形成)
- Forward.xml — コピーされたフォワード最適化ファイル(ターミナルにより形成)
したがって、ファイルシステムを操作するための出発点となる2つのクラスを作成しました。コード内のファイルシステムのさらなる作業は、標準のC#クラスを使用して実行されます。これにより、ファイルパスのエラーを回避し、プログラミングを大幅に高速化できます。
レポートおよびEA設定ファイル(OptimisatorSettingsManager、ReportReader、SetFileManager)で作業するオブジェクト
この章では、ファイルでの作業について説明します。アドオンでは以下のファイルとの作業が必要です。
- EA設定タブ
- 取引レポートファイル
- アドオンの「Reports」ディレクトリにレポートとともに保存される最適化設定ファイル
最適化のためのEAパラメーターを含むファイルから始めましょう。EA設定ファイルには拡張子(*.set)が付いています。ただし、チャートでの起動時の設定とテスターで実行するための設定を含むいくつかのセットアップファイルがあります。ここでは2番目のファイル形式に興味があります。これらのファイルは、ターミナルの以下の可変ディレクトリに保存されます。
~\MQL5\Profiles\Tester
インストール中にこのディレクトリが存在しない場合があるため、必要に応じて確認して作成する必要があることにご注意ください。このディレクトリが存在しない場合、ターミナルは最適化設定を保存できません。多くの場合、これは次の問題の理由となります。このようなターミナルインストールの場合、新しいテストまたは最適化が実行されるたびに、最適化設定タブにはデフォルト設定が引き続きあるままです。説明されているファイル構造は、INIファイルに多少似ており、次のようになります。
Variable_name=Value||Start||Step||Stop||(Y/N)
言い換えると、これらのファイルのキーはEAパラメータの名前であり、キー値はその値のリストを取ることができます。この例での名前はストラテジーテスターの適切な列と同じです。最後の変数は2つの値(Y/N)のいずれかを取ることができ、この特定のEAパラメータの最適化を有効/無効にします。この規則の例外は、INIファイルのような形式を持つ文字列パラメータの書き込みです。
Variable_name=Value
初期化ファイルと同様、SETファイルにもコメントがあります。コメント行は常に「;」(セミコロン)で始まります。 以下に簡単な例を示します。
; saved automatically on 2019.05.19 09:04:18
; this file contains last used input parameters for testing/optimizing 2MA_Martin expert advisor
;
Fast=12||12||1||120||N
Slow=50||50||1||500||N
maxLot=1||1||0.100000||10.000000||N
pathToDB=C:\Users\Administrator\Desktop\test_2MA_8
これらのファイルで作業するには、これらのファイルの読み取りを許可するラッパークラスと、読み取られた各文字列の値を格納するクラスを作成する必要があります。このクラスは、本稿の「表示」の説明部分で言及されているため、ここでは考慮しません。グラフィカルインターフェイス(SetFileManager)からパラメータセットを読み書きするメインクラスを考えてみましょう。 このクラスの実装は次のとおりです。
class SetFileManager { public SetFileManager(string filePath, bool createIfNotExists) { if ((FileInfo = new FileInfo(filePath)).Extension.CompareTo(".set") != 0) throw new ArgumentException("File mast have '.set' extention!"); if (createIfNotExists) File.Create(filePath).Close(); if (!File.Exists(filePath)) throw new ArgumentException("File doesn`t exists"); } public FileInfo FileInfo { get; } #region File data private List<ParamsItem> _params = new List<ParamsItem>(); public List<ParamsItem> Params { get { if (_params.Count == 0) UpdateParams(); return _params; } set { if (value != null && value.Count != 0) _params = value; } } #endregion public virtual void SaveParams() { if (_params.Count == 0) return; using (var file = new StreamWriter(FileInfo.FullName, false)) { file.WriteLine(@"; saved by OptimisationManagerExtention program"); file.WriteLine(";"); foreach (var item in _params) { file.WriteLine($"{item.Variable}={item.Value}||{item.Start}||{item.Step}||{item.Stop}||{(item.IsOptimize ? "Y" : "N")}"); } } } public virtual SetFileManager DublicateFile(string pathToFile) { if (new FileInfo(pathToFile).Extension.CompareTo(".set") != 0) throw new ArgumentException("File mast have '.set' extention!"); File.Copy(FileInfo.FullName, pathToFile, true); return new SetFileManager(pathToFile, false); } 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(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); } } } } }
最初に注意する必要があるのは、クラスコンストラクタで行われるファイル形式の確認です。ファイル形式がSETファイル形式と異なる場合、このクラスはエラーを返します。これは、ターミナルが認識していないファイルを処理しようとするためです。ファイルは、読み取り専用のpublicプロパティFileInfoに格納されます。ファイルの読み込みはUpdateParamsメソッドで実行されます。このメソッドは、「using construction」でコメントを無視しながら、ファイルを最初の行から最後の行まで読み込みます。また、読み取りファイルのパラメータの設定にも注意してください。読み取り行は最初に2つに分割されて等号("=")が区切り文字として使用されるので、変数名はその値から分離されます。次のステップでは、変数値は4つの要素[Value、Start、Step、Stop、IsOptimise]の配列に分割されます。文字列の場合、記号を分離する2行("||")が見つからないため、配列はこれらの文字列に分割されません。文字列のエラーを回避するために、文字列でこの文字を使用することはお勧めしません。新しい要素ごとに配列にデータがない場合、null値が割り当てられますが、そうでない場合は配列の値が使用されます。
値の保存は、SaveParamsメソッドで実行されます。ファイル内のデータ書き込み形式にご注意ください。これは以下のコード行で実行されます。
file.WriteLine($"{item.Variable}={item.Value}||{item.Start}||{item.Step}||{item.Stop}||{(item.IsOptimize ? "Y" : "N")}");
データの型に関係なく、データは文字列以外の型として書き込まれます。ターミナルは、それが文字列であるかどうかを理解できるため、唯一のデータ書き込み型が選択されました。このクラスの欠点の1つは、データ型を検出できないことです。ファイル構造はそのような情報を提供しないため、その形式を知ることはできません。ターミナルはこの情報をEAから直接受け取ります。
読み取りファイルへのアクセスとそのパラメーターの設定は、 Paramsプロパティを介して実装されます。 ファイルデータでの作業は説明されているプロパティを介して実装されているため、便宜上、ファイルが既に読み取られているかどうかを確認して、ファイルが読み取られていない場合は、 UpdateParamsメソッドが呼び出されます。一般に、このクラスで作業する手順は次のとおりです。
- インスタンス化して、ファイルのOOP表現を取得する
- 「Params」メソッド(または必要に応じて、たとえばファイルが外部から変更された場合はUpdateParams)を呼び出して読み取る
- 「セッター」を使用してカスタム値を設定するか、「ゲッター」を使用して受信した配列を使用してデータを変更する
- SaveParamsメソッドを使用して変更を保存する
INIファイルと比較した場合の主な欠点は、読み取りと書き込みの間、データがプログラムメモリに保存されることです。ただし、ファイルが誤ってまたは意図的に除外された場合、外部からファイルを変更できます。このクラスにはDublicateFileメソッドもあります。このメソッドは、渡されたパスにファイルをコピーする(このパスに同じ名前のファイルが存在する場合は上書き)ことを目的としています。
さらに、RepirtReaderクラスは、ターミナルによって生成された最適化レポートを読み取って解析し、データを表用に準備します。最適化履歴ファイルは、MS Excel用に作成されたXML形式で利用できます。そのルートノード(最初のタグ)「<Workbook/>」は本を説明します。 次の<DocumentProperties/>は、最適化が実行されたパラメーターを説明します。このノードには、次の有用な情報が含まれています。
- ロボット名、資産名、時間枠、最適化期間で構成されるヘッダ
- 作成日
- 最適化が実行されたサーバの名前
- 預金と預金通貨
- レバレッジ
<Styles/>はExcel用に作成されたもので、ここでは役に立ちません。次の<Worksheet/>は、最適化パスを持つワークシートを説明します。このノードには、ストラテジーテスターですべてのパラメータをテストした後の最適化結果のリストなどの列に分割された検索データを保存する<Table/>ノードが含まれます。表の最初の行には列ヘッダが含まれ、さらに行には値が含まれていることにご注意ください。各<Row/>ノードには、<Cell/>タグ内に表の値のリストが含まれています。また、各<Cell/>タグには、このセルの値のタイプを示すType属性が含まれています。このファイルは大きすぎるため、ここでは提示しません。ファイル全体は、任意のEAを最適化してアドオンの[Reports]フォルダから最適化結果を開くことによって確認できます。次に、説明したクラスの検討に移りましょう。最適化ファイルを説明するプロパティの確認から始めます。
#region DocumentProperties and column names /// <概要> /// Document column names /// </概要> protected List<string> columns = new List<string>(); /// <概要> /// Access to the collection of document columns from outside, collection copy is returned /// to protect the initial collection from modification /// </概要> public List<string> ColumnNames => new List<string>(columns); /// <概要> /// Document header /// </概要> public string Title { get; protected set; } /// <概要> /// Document author /// </概要> public string Author { get; protected set; } /// <概要> /// Document creation date /// </概要> public DateTime Created { get; protected set; } /// <概要> /// Server on which optimization was performed /// </概要> public string Server { get; protected set; } /// <概要> /// Initial deposit /// </概要> public Deposit InitialDeposit { get; protected set; } /// <概要> /// Leverage /// </概要> public int Leverage { get; protected set; } #endregion
これらのプロパティは<DocumentProperties/>に存在し、以下のメソッドで書き入れられます。
protected virtual void GetDocumentProperties(string path) { document.Load(path); Title = document["Workbook"]["DocumentProperties"]["Title"].InnerText; Author = document["Workbook"]["DocumentProperties"]["Author"].InnerText; string DT = document["Workbook"]["DocumentProperties"]["Created"].InnerText; Created = Convert.ToDateTime(DT.Replace("Z", "")); Server = document["Workbook"]["DocumentProperties"]["Server"].InnerText; string[] deposit = document["Workbook"]["DocumentProperties"]["Deposit"].InnerText.Split(' '); Deposit = new Deposit(Convert.ToDouble(deposit[0]), deposit[1]); Leverage = Convert.ToInt32(document["Workbook"]["DocumentProperties"]["Leverage"].InnerText); enumerator = document["Workbook"]["Worksheet"]["Table"].ChildNodes.GetEnumerator(); enumerator.MoveNext(); foreach (XmlElement item in (XmlElement)enumerator.Current) { columns.Add(item["Data"].InnerText); } }
したがって、C#ツールを使用した(*.xml)ファイルでの作業は、配列の操作とほとんど同じくらい簡単です。「document」オブジェクトはXmlDocumentクラスのインスタンスであり、読み取りファイルを保存し、それを使用して便利な作業を提供します。 enumeratorフィールドには値が割り当てられ、ドキュメントを1行ずつ読み取るReadメソッドで使用されます。クラス宣言中にはIDisposableインターフェイスが使用されます。
class ReportReader : IDisposable
このインターフェイスにはDispose()メソッドが1つしか含まれていないため、このクラスを「using」で使用できます。「using」は正しい動作を保証します。つまり、ファイルから読み取るたびにファイルを閉じる必要はありません。代わりに、ファイルはDispose()メソッドで閉じられます。このメソッドは、ファイルでの作業が実行される中括弧ブロックを終了した後に自動的に呼び出されます。この特定のケースでは、多くの不要な情報を読み取りファイルに保存しないように、「Dispose」メソッドでドキュメントフィールドをクリアします。下記はメソッドの実装です。
public void Dispose() { document.RemoveAll(); }
次に、標準のC#インターフェイスであるIEnumeratorインターフェイスは、以下の通りです。
// // Summary: // Supports a simple iteration over a non-generic collection. [ComVisible(true)] [Guid("496B0ABF-CDEE-11d3-88E8-00902754C43A")] public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }
このインターフェイスは2つのメソッドと1つのプロパティで構成されており、コレクションの一種のラッパーとして機能して一度に1つの値を反復処理します。MoveNextメソッドは、コレクションが終了するまでカーソルを1つの値だけ前方に移動します。コレクション全体を通過した後にこのメソッドを呼び出すと、反復の終了を意味するfalseが返されます。Resetメソッドを使用すると、カーソルをコレクションのゼロインデックスに移動して繰り返しを新たに再開できます。「Current」プロパティには、MoveNextのシフト後に受信したインデックスに対して現在選択されているコレクション要素が含まれています。このインターフェイスは、C#で広く使用されており、「foreach」ループはそれに基づいています。ただし、Readメソッドの実装にはこれが必要です。
/// <概要> /// Command to read a row from the optimizations table /// </概要> /// <param name="row"> /// Read row key - column header; value - cell value</param> /// <returns> /// true - if the row has been read /// false - if the row has not been read /// </returns> public virtual bool Read(out List<KeyValuePair<string, object>> row) { row = new List<KeyValuePair<string, object>>(); if (enumerator == null) return false; bool ans = enumerator.MoveNext(); if (ans) { XmlNodeList nodes = ((XmlElement)enumerator.Current).ChildNodes; for (int i = 0; i < columns.Count; i++) { string value = nodes[i]["Data"].InnerText; string type = nodes[i]["Data"].Attributes["ss:Type"].Value; KeyValuePair<string, object> item = new KeyValuePair<string, object>(columns[i], ConvertToType(value, type)); row.Add(item); } } return ans; }
「Read」メソッドの目的は、MoveNext()と似ています。さらに、渡されたパラメータを介して操作結果を返します。値を持つ行のみを返す必要があるため、列挙子変数に値を設定するとき、MoveNextメソッドを1回呼び出し、カーソルをゼロ位置(表の列ヘッダ)からインデックス1(値を持つ最初の行)に移動します。 データを読み取るときは ConvertToTypeも使用して、読み取った値を文字列形式から「Type」属性で設定された形式に変換します。これが、戻されるリストで「object」型が指定されている理由で、任意の型を戻り値の型に変換できます。 ConvertToTypeメソッドの実装を以下に示します。
private object ConvertToType(string value, string type) { object ans; switch (type) { case "Number": { System.Globalization.NumberFormatInfo provider = new System.Globalization.NumberFormatInfo() { NumberDecimalSeparator = "," }; ans = Convert.ToDouble(value.Replace('.', ','), provider); } break; case "DateTime": ans = Convert.ToDateTime(value); break; case "Boolean": { try { ans = Convert.ToBoolean(value.ToLower()); } catch (Exception) { ans = Convert.ToInt32(value) == 1; } } break; default: ans = value; break; // String } return ans; }
このメソッドでは、文字列が数値に変換されます。国によってデータと時刻の表示形式が異なるため、小数区切りを明示的に指定する必要があります。
リーダーの再起動は、IEnumerator.Resetメソッドのラッパーである「ResetReader」メソッドを介して有効になります。実装は以下の通りです。
public void ResetReader() { if (enumerator != null) { enumerator.Reset(); // Reset enumerator.MoveNext(); // Skip the headers } }
したがって、C#で使用可能なXMLファイルを解析するための便利なラッパーを使用して、レポートファイルを解析し、読み取り、追加データを取得するラッパークラスを簡単に作成できます。
次のクラスは、ターミナルではなくアドオン自体によって生成されるオプティマイザ設定ファイルを使用します。ターゲット機能の1つは、最適化パラメータをダブルクリックしてテスターでEAを起動する可能性です。しかし、テスターの設定(日付範囲、銘柄名、その他のパラメータ)はどこで取得できるでしょうか。これらのデータの一部は最適化レポートに保存されていますが、すべてではありません。明らかに、この問題を解決するには、これらの設定をファイルに保存する必要があります。便利なデータストレージ形式としてXMLマークアップが選択されました。XMLファイル読み取りの例は上記のクラスで示しましたが、読み取りに加えて、ファイルへの書き込みも行います。まず、設定ファイルに保存する情報を決定する必要があります。
保存する最初のオブジェクトは、オプティマイザ設定データ(メインの[設定]タブの下部領域の[設定]タブで使用可能)が保存される構造です。この構造体は次のように実装されます。
struct OptimisationInputData { public void Copy(OptimisationInputData data) { Login = data.Login; ForvardDate = data.ForvardDate; IsVisual = data.IsVisual; Deposit = data.Deposit; Laverage = data.Laverage; Currency = data.Currency; DepositIndex = data.DepositIndex; ExecutionDelayIndex = data.ExecutionDelayIndex; ModelIndex = data.ModelIndex; CurrencyIndex = data.CurrencyIndex; LaverageIndex = data.LaverageIndex; OptimisationCriteriaIndex = data.OptimisationCriteriaIndex; } public uint? Login; public DateTime ForvardDate; public bool IsVisual; public int Deposit; public string Laverage; public string Currency; public int DepositIndex, ExecutionDelayIndex, ModelIndex, CurrencyIndex, LaverageIndex, OptimisationCriteriaIndex; public ENUM_Model Model => GetEnum<ENUM_Model>(ModelIndex); public ENUM_OptimisationCriteria GetOptimisationCriteria => GetEnum<ENUM_OptimisationCriteria>(OptimisationCriteriaIndex); public ENUM_ExecutionDelay ExecutionDelay => GetEnum<ENUM_ExecutionDelay>(ExecutionDelayIndex); private T GetEnum<T>(int ind) { Type type = typeof(T); string[] arr = Enum.GetNames(type); return (T)Enum.Parse(type, arr[ind]); } }
この構造体は、最初はビューからモデルにデータを渡すためのコンテナとして作成されたため、データに加えてComboBoxのインデックスが含まれています。 モデルの構造体および他のクラスを効率的に使用するために、列挙(enum)の 値を変換するメソッドを作成しました。これは、目的の列挙型のインデックス番号によって構造体に格納されます。 列挙は次のように動作します。これらのリストの値をComboBoxに出力するには、便利な文字列形式で保存されます。 GetEnum<T>メソッドは逆変換に使用されます。これは、C++テンプレートに類似したジェネリックメソッドです。このメソッドで目的のEnumを見つけるには、型の値を格納するTypeクラスが使用される、渡された型の特定の値を見つけます。次に、この列挙型を行のリストに分解し、文字列から列挙への逆変換を使用して、文字列ビューではなく、目的の列挙として特定の列挙の値を取得します。
保存されたデータを含む次のオブジェクトはConfigCreator_inputDataです。この構造体には、選択したターミナルの表からのデータが含まれ、構成ファイルを作成するためにOptimisationManagerクラスで使用されます。構造体は次のとおりです。
struct ConfigCreator_inputData { public ENUM_Timeframes TF; public uint? Login; public string TerminalID, pathToBot, setFileName, Pass, CertPass, Server, Symbol, ReportName; public DateTime From, Till; public ENUM_OptimisationMode OptimisationMode; }
すべての保存データの3番目と最後のデータは、リスト要素ParamItem (List<ParamsItem>)によるEAパラメータタイプのリストです。ここで、クラスでの作業中に作成されたファイルを見てみましょう。
<Settings> <OptimisationInputData> <Item Name="Login" /> <Item Name="ForvardDate">2019.04.01</Item> <Item Name="IsVisual">False</Item> <Item Name="Deposit">10000</Item> <Item Name="Laverage">1:1</Item> <Item Name="Currency">USD</Item> <Item Name="DepositIndex">2</Item> <Item Name="ExecutionDelayIndex">0</Item> <Item Name="ModelIndex">1</Item> <Item Name="CurrencyIndex">1</Item> <Item Name="LaverageIndex">0</Item> <Item Name="OptimisationCriteriaIndex">0</Item> </OptimisationInputData> <ConfigCreator_inputData> <Item Name="TF">16386</Item> <Item Name="Login">18420888</Item> <Item Name="TerminalID">0CFEFA8410765D70FC53545BFEFB44F4</Item> <Item Name="pathToBot">Examples\MACD\MACD Sample.ex5</Item> <Item Name="setFileName">MACD Sample.set</Item> <Item Name="Pass" /> <Item Name="CertPass" /> <Item Name="Server" /> <Item Name="Symbol">EURUSD</Item> <Item Name="ReportName">MACD Sample</Item> <Item Name="From">2019.01.01</Item> <Item Name="Till">2019.06.18</Item> <Item Name="OptimisationMode">2</Item> </ConfigCreator_inputData> <SetFileParams> <Variable Name="InpLots"> <Value>0.1</Value> <Start>0.1</Start> <Step>0.010000</Step> <Stop>1.000000</Stop> <IsOptimize>False</IsOptimize> </Variable> <Variable Name="InpTakeProfit"> <Value>50</Value> <Start>50</Start> <Step>1</Step> <Stop>500</Stop> <IsOptimize>False</IsOptimize> </Variable> <Variable Name="InpTrailingStop"> <Value>30</Value> <Start>30</Start> <Step>1</Step> <Stop>300</Stop> <IsOptimize>False</IsOptimize> </Variable> <Variable Name="InpMACDOpenLevel"> <Value>3</Value> <Start>3</Start> <Step>1</Step> <Stop>30</Stop> <IsOptimize>True</IsOptimize> </Variable> <Variable Name="InpMACDCloseLevel"> <Value>2</Value> <Start>2</Start> <Step>1</Step> <Stop>20</Stop> <IsOptimize>True</IsOptimize> </Variable> <Variable Name="InpMATrendPeriod"> <Value>26</Value> <Start>26</Start> <Step>1</Step> <Stop>260</Stop> <IsOptimize>False</IsOptimize> </Variable> </SetFileParams> </Settings>
このファイルはビデオに示されているEA操作中に作成されました。その構造からわかるように、ルートファイルノードは<Settings/>で、中には<OptimisationInputData/>、<ConfigCreator_inputData/>、<SetFileParams/>の3つのノードがあります。これらのノードのデータ型は、名前に対応しています。テスター設定データを保存するノードの最後の要素は、「Name」属性を含む「Item」タグです。これを使用して、保存されたパラメータの名前を設定します。 <Variable/>タグはEAパラメーターリストに使用されます。「Name」属性にはパラメータの名前が格納され、対応する最適化パラメータ値はネストされたタグに保存されます。このファイルを作成するために、OptimisatorSettingsManagerクラスはIDisposableインターフェイスから継承され、指定された値はDisposeメソッドでファイルに保存されます。対応するプロパティのゲッター(取得メソッド)を使用して、ファイルからデータを読み取ります。
#region OptimisationInputData /// <概要> /// The OptimisationInputData structure for saving data /// </概要> private OptimisationInputData? _optimisationInputData = null; /// <概要> /// Get and save the OptimisationInputData structure /// </概要> public virtual OptimisationInputData OptimisationInputData { get { return new OptimisationInputData { Login = StrToUintNullable(GetItem(NodeType.OptimisationInputData, "Login")), ForvardDate = DateTime.ParseExact(GetItem(NodeType.OptimisationInputData, "ForvardDate"), DTFormat, null), IsVisual = Convert.ToBoolean(GetItem(NodeType.OptimisationInputData, "IsVisual")), Deposit = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "Deposit")), Laverage = GetItem(NodeType.OptimisationInputData, "Laverage"), Currency = GetItem(NodeType.OptimisationInputData, "Currency"), DepositIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "DepositIndex")), ExecutionDelayIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "ExecutionDelayIndex")), ModelIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "ModelIndex")), CurrencyIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "CurrencyIndex")), LaverageIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "LaverageIndex")), OptimisationCriteriaIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "OptimisationCriteriaIndex")) }; } set => _optimisationInputData = value; } #endregion
この特定の例では、ゲッターでOptimisationInputData構造体が取得されます。構造体の値は、上記のファイルから取得されます。ゲッターのGetItemメソッドは、ファイルからデータを受信するために使用されます。このメソッドには2つのパラメータがあります。
- データが使用されるノードのタイプ
- 「Name」属性で指定されているパラメータの名前
メソッドの実装は次のとおりです。
/// <概要> /// Get element from a settings file /// </概要> /// <param name="NodeName">Structure type</param> /// <param name="Name">Field name</param> /// <returns> /// Field value /// </returns> public string GetItem(NodeType NodeName, string Name) { if (!document.HasChildNodes) document.Load(Path.Combine(PathToReportDataDirectory, SettingsFileName)); return document.SelectSingleNode($"/Settings/{NodeName.ToString()}/Item[@Name='{Name}']").InnerText; }
このデータ取得メソッドでは、SQLに似ていますがXML形式に適用されるXpath言語を使用します。指定された属性値で目的のノードからデータを取得するには、このノードへのフルパスを指定し、最後のItemノードで次の条件を示す必要があります。Name属性は渡された名前と等しくなければなりません。したがって、すべての構造がファイルから読み取られます。このノード構造はより複雑であるため、パラメータのリストには別のメソッドが使用されます。
#region SetFileParams /// <概要> /// List of parameters to save /// </概要> private List<ParamsItem> _setFileParams = new List<ParamsItem>(); /// <概要> /// Get and set (.set) file parameters to save /// </概要> public List<ParamsItem> SetFileParams { get { if (!document.HasChildNodes) document.Load(Path.Combine(PathToReportDataDirectory, SettingsFileName)); var data = document["Settings"]["SetFileParams"]; List<ParamsItem> ans = new List<ParamsItem>(); foreach (XmlNode item in data.ChildNodes) { ans.Add(new ParamsItem(item.Attributes["Name"].Value) { Value = item["Value"].InnerText, Start = item["Start"].InnerText, Step = item["Step"].InnerText, Stop = item["Stop"].InnerText, IsOptimize = Convert.ToBoolean(item["IsOptimize"].InnerText) }); } return ans; } set { if (value.Count > 0) _setFileParams = value; } } #endregion
この場合、すべての<Variable/>ノードをループし、それぞれからName属性値を取得し、ParamItemクラスにこの特定のParamsItemノードに含まれるデータを入力します。
データは以下のDispose()メソッドでファイルに保存されます。
public virtual void Dispose() { // Nested method which assists in writing of structure elements void WriteItem(XmlTextWriter writer, string Name, string Value) { writer.WriteStartElement("Item"); writer.WriteStartAttribute("Name"); writer.WriteString(Name); writer.WriteEndAttribute(); writer.WriteString(Value); writer.WriteEndElement(); } void WriteElement(XmlTextWriter writer, string Node, string Value) { writer.WriteStartElement(Node); writer.WriteString(Value); writer.WriteEndElement(); } // firstly clean the file storing xml markup of the settings file if (document != null) document.RemoveAll(); // then check if the results can be saved if (!_configInputData.HasValue || !_optimisationInputData.HasValue || _setFileParams.Count == 0) { return; } using (var xmlWriter = new XmlTextWriter(Path.Combine(PathToReportDataDirectory, SettingsFileName), null)) { xmlWriter.Formatting = Formatting.Indented; xmlWriter.IndentChar = '\t'; xmlWriter.Indentation = 1; xmlWriter.WriteStartDocument(); xmlWriter.WriteStartElement("Settings"); xmlWriter.WriteStartElement("OptimisationInputData"); WriteItem(xmlWriter, "Login", _optimisationInputData.Value.Login.ToString()); WriteItem(xmlWriter, "ForvardDate", _optimisationInputData.Value.ForvardDate.ToString(DTFormat)); WriteItem(xmlWriter, "IsVisual", _optimisationInputData.Value.IsVisual.ToString()); WriteItem(xmlWriter, "Deposit", _optimisationInputData.Value.Deposit.ToString()); WriteItem(xmlWriter, "Laverage", _optimisationInputData.Value.Laverage); WriteItem(xmlWriter, "Currency", _optimisationInputData.Value.Currency); WriteItem(xmlWriter, "DepositIndex", _optimisationInputData.Value.DepositIndex.ToString()); WriteItem(xmlWriter, "ExecutionDelayIndex", _optimisationInputData.Value.ExecutionDelayIndex.ToString()); WriteItem(xmlWriter, "ModelIndex", _optimisationInputData.Value.ModelIndex.ToString()); WriteItem(xmlWriter, "CurrencyIndex", _optimisationInputData.Value.CurrencyIndex.ToString()); WriteItem(xmlWriter, "LaverageIndex", _optimisationInputData.Value.LaverageIndex.ToString()); WriteItem(xmlWriter, "OptimisationCriteriaIndex", _optimisationInputData.Value.OptimisationCriteriaIndex.ToString()); xmlWriter.WriteEndElement(); xmlWriter.WriteStartElement("ConfigCreator_inputData"); WriteItem(xmlWriter, "TF", ((int)_configInputData.Value.TF).ToString()); WriteItem(xmlWriter, "Login", _configInputData.Value.Login.ToString()); WriteItem(xmlWriter, "TerminalID", _configInputData.Value.TerminalID.ToString()); WriteItem(xmlWriter, "pathToBot", _configInputData.Value.pathToBot); WriteItem(xmlWriter, "setFileName", _configInputData.Value.setFileName); WriteItem(xmlWriter, "Pass", _configInputData.Value.Pass); WriteItem(xmlWriter, "CertPass", _configInputData.Value.CertPass); WriteItem(xmlWriter, "Server", _configInputData.Value.Server); WriteItem(xmlWriter, "Symbol", _configInputData.Value.Symbol); WriteItem(xmlWriter, "ReportName", _configInputData.Value.ReportName); WriteItem(xmlWriter, "From", _configInputData.Value.From.ToString(DTFormat)); WriteItem(xmlWriter, "Till", _configInputData.Value.Till.ToString(DTFormat)); WriteItem(xmlWriter, "OptimisationMode", ((int)_configInputData.Value.OptimisationMode).ToString()); xmlWriter.WriteEndElement(); xmlWriter.WriteStartElement("SetFileParams"); foreach (var item in _setFileParams) { xmlWriter.WriteStartElement("Variable"); xmlWriter.WriteStartAttribute("Name"); xmlWriter.WriteString(item.Variable); xmlWriter.WriteEndAttribute(); WriteElement(xmlWriter, "Value", item.Value); WriteElement(xmlWriter, "Start", item.Start); WriteElement(xmlWriter, "Step", item.Step); WriteElement(xmlWriter, "Stop", item.Stop); WriteElement(xmlWriter, "IsOptimize", item.IsOptimize.ToString()); xmlWriter.WriteEndElement(); } xmlWriter.WriteEndElement(); xmlWriter.WriteEndElement(); xmlWriter.WriteEndDocument(); xmlWriter.Close(); } }
このメソッドの最初に、2つのネストされた関数が作成されます。WriteItem関数は構造要素の書き込みに使用する繰り返しコードブロックを分離するのに使用され、WriteElement 関数はStart、Step、Stop、IsOptimizeなどの最適化パラメータの値を保存するために設計されています。3つのタグはすべて、設定ファイルで使用可能でなければなりません。したがって、書き込む前にチェックユニットを追加します。その目的は、必要なパラメータがすべて渡されていない場合にファイルの書き込みを防ぐことです。次に、データは、前述の「using」構文でファイルに書き込まれます。ネストされた関数の使用により、ファイルへのデータ書き込みに関連するコードを3分の1以下に減らすことができました。
キーオブジェクトのテスト
最後に、アプリケーションのテストに関するメモをいくつか追加します。アドオンはさらに拡張および変更されるため、キーオブジェクトを確認するテストを作成することにしました。後で変更が必要になった場合はそれらのパフォーマンスを簡単に確認できます。現在のテストは、部分的に次のクラスを対象としています。
- Config
- ReportReader
- OptimisationSettingsManager
- SetFileManager
- TerminalManager
最初の記事では、以降の章で説明するクラスが変更されます。これらの変更は、一部のメソッドのロジックと実行結果に関係するため、これらのクラスは単体テストの対象外となります。これらのクラスのテストは、次の記事で実装されます。また、テストは単体テストとして実装されていますが、外部オブジェクト(ターミナル、ファイルシステムなど)と対話するため、現在はすべて統合テストであることに注意してください。今後のオブジェクトは、上記のオブジェクトに依存せずに(つまり純粋な単体テストとして)テストする予定です。この目的のために、上記の各オブジェクトの前に作成の組みがあります。このような仕組みの例は、ReportReaderクラスを作成するためのものです。
#region ReportReaderFabric abstract class ReportReaderCreator { public abstract ReportReader Create(string path); } class MainReportReaderCreator : ReportReaderCreator { public override ReportReader Create(string path) { return new ReportReader(path); } } #endregion
そのコードは単純です。実際には、ReportReader型オブジェクトの作成を、ReportReaderFabricクラスから派生したMainReportReaderCreatorクラスにラップします。このアプローチにより、オブジェクトの型をReportReaderFabricとして主要なオブジェクトに渡すことができます(詳細については後の章で後述)。ここでは、特定の仕組みの実装が異なる場合があります。したがって、ファイルとターミナルを使用するクラスは、単体テストのキーオブジェクトに置き換えることができるので、クラスの相互依存も減少します。オブジェクトの形成に対するこのアプローチは、ファクトリメソッドと呼ばれます。
将来のテストの実装については、次の記事で詳しく検討します。 オブジェクトの作成にファブリックメソッドを使用する例については、以降の章で検討します。次に、構成ファイルを操作するクラスのテストを検討してみましょう。現在のプロジェクト内のすべてのテストは、個別の「Unit Test Project」プロジェクトに含める必要があります
テストは「OptimisationManagerExtention」プロジェクト用に作成されるため、「OptimisationManagerExtentionTests」というタイトルを付けましょう。次のステップでは、「OptimisationManagerExtention」プロジェクトへのリンクを追加します。つまり、グラフィックインターフェイスとロジックを使用してリンクをDLLに追加します。「public」アクセス修飾子でマークされていないオブジェクトをテストする必要があります。これらのオブジェクトをテストプロジェクトで使用できるようにする方法は2つあります。
- publicにする(プロジェクト内でのみ使用されるため、これは間違っています)
- 特定のプロジェクト内の内部クラスを表示する可能性を追加する(より望ましい方法)
2番目の方法を使用してこの問題を解決し、次の属性をメインプロジェクトコードに追加しました。
[assembly: InternalsVisibleTo("OptimisationManagerExtentionTests")]
次のステップは、選択したクラスのテストを作成することです。テストプロジェクトは補助的なものにすぎないため、各テストクラスは考慮せずに、例として1つのクラスを示します。便宜上、Configクラスをテストするための完全なクラスを以下に示します。このクラスをテストする最初の条件は、属性[TestClass]を追加することです。また、テスト対象のクラスはpublicである必要があり、テストメソッドは[TestMethod]属性を持つ必要がありますが、テストプロセス全体はそれらに実装されます。[TestInitialize]でマークされたメソッドは、テストが開始される前に毎回起動されます。同様の[ClassInitialize]属性はこのテストでは使用されませんが、他のテストで使用されて、[TestInitialize],でマークされたメソッドとは異なり、最初のテストが開始される前にのみ起動されます。各テストメソッドの最後に、Assertクラスメソッドの1つの呼び出しがあり、テスト値を必要なものと比較します。テストの合否が決定されます。
[TestClass] public class ConfigTests { private string ConfigName = $"{Environment.CurrentDirectory}\\MyTestConfig.ini"; private string first_excention = "first getters call mast be null because file doesn't contain this key"; Config config; [TestInitialize] public void TestInitialize() { if (File.Exists(ConfigName)) File.Delete(ConfigName); config = new Config(ConfigName); } [TestMethod] public void StringConverter_GetSetTest() { string expected = null; // first get string s = config.Common.Password; Assert.AreEqual(expected, s, first_excention); // set expected = "MyTestPassward"; config.Common.Password = expected; s = config.Common.Password; Assert.AreEqual(expected, s, "Login mast be equal to MyTestLogin"); // set null config.Common.Login = null; s = config.Common.Password; Assert.AreEqual(expected, s, "Login mast be equal to MyTestLogin"); } [TestMethod] public void ServerConverter_GetSetTest() { ServerAddressKeeper expected = null; // first get; ServerAddressKeeper server = config.Common.Server; Assert.AreEqual(expected, server); // set expected = new ServerAddressKeeper("193.219.127.76:4443"); // Open broker demo server config.Common.Server = expected; server = config.Common.Server; Assert.AreEqual(server.Address, expected.Address, $"Address must be {expected.Address}"); } [TestMethod] public void BoolConverter_GetSetTest() { bool? expected = null; // first get bool? b = config.Common.ProxyEnable; Assert.AreEqual(expected, b, first_excention); // set Random gen = new Random(); int prob = gen.Next(100); expected = prob <= 50; config.Common.ProxyEnable = expected; b = config.Common.ProxyEnable; Assert.AreEqual(expected.Value, b.Value, "ProxyEnables must be equal to true"); // set null config.Common.ProxyEnable = null; b = config.Common.ProxyEnable; Assert.AreEqual(expected.Value, b.Value, "ProxyEnables must be equal to true"); } [TestMethod] public void ENUMConverter_GetSetTest() { ENUM_ProxyType? expected = null; // first get ENUM_ProxyType? p = config.Common.ProxyType; Assert.AreEqual(expected, p, first_excention); // set Random gen = new Random(); int prob = gen.Next(300); int n = prob <= 100 ? 0 : (prob > 100 && prob <= 200 ? 1 : 2); expected = (ENUM_ProxyType)n; config.Common.ProxyType = expected; p = config.Common.ProxyType; Assert.AreEqual(expected.Value, p.Value, $"ProxyType must be equal to {expected.Value}"); // set null config.Common.ProxyEnable = null; p = config.Common.ProxyType; Assert.AreEqual(expected.Value, p.Value, $"ProxyType must be equal to {expected.Value}"); } [TestMethod] public void DTConverter_GetSetTest() { DateTime? expected = null; // first get DateTime? p = config.Tester.FromDate; Assert.AreEqual(expected, p, first_excention); // set expected = DateTime.Now; config.Tester.FromDate = expected; p = config.Tester.FromDate; Assert.AreEqual(expected.Value.Date, p.Value.Date, $"ProxyType must be equal to {expected.Value}"); // set null config.Common.ProxyEnable = null; p = config.Tester.FromDate; Assert.AreEqual(expected.Value.Date, p.Value.Date, $"ProxyType must be equal to {expected.Value}"); } [TestMethod] public void DoubleConverter_GetSetTest() { double? expected = null; // first get double? p = config.Tester.Deposit; Assert.AreEqual(expected, p, first_excention); // set Random rnd = new Random(); expected = rnd.NextDouble(); config.Tester.Deposit = expected; p = config.Tester.Deposit; Assert.AreEqual(Math.Round(expected.Value, 6), Math.Round(p.Value, 6), $"Deposit must be equal to {expected.Value}"); // set null config.Common.ProxyEnable = null; p = config.Tester.Deposit; Assert.AreEqual(Math.Round(expected.Value, 6), Math.Round(p.Value, 6), $"Deposit must be equal to {expected.Value}"); } [TestMethod] public void DeleteKeyTest() { config.Common.Login = 12345; config.DeleteKey(ENUM_SectionType.Common, "Login"); Assert.AreEqual(null, config.Common.Login, "Key must be deleted"); } [TestMethod] public void DeleteSectionTest() { config.Common.Login = 12345; config.DeleteSection(ENUM_SectionType.Common); Assert.AreEqual(null, config.Common.Login, "Key must be deleted"); } }
この特定のテストクラスを検討する場合、必要なすべてのメソッドを網羅するのではなく、Config.Converterクラスをテストすることに注意する必要があります。このクラスは、構成ファイルを使用して操作のロジック全体を実行しますが、privateクラスなので、クラス自体ではなく、このクラスを使用するプロパティのテストを記述する必要があります。たとえば、DoubleConverter_GetSetTest()テストはconfig.Tester.Depositプロパティを介して「string」から「double」への変換の正確性をテストするもので、3つの部分で構成されています。
- 作成されていないフィールドからのdouble型パラメータの要求し、nullを返す
- ファイルにランダムな値を書き込み、それを読み取る
- 無視されるべきNullエントリ
エラーはどの段階で検出されても簡単に検出して修正できるため、テストはアプリケーション開発に役立ちます。すべてのテストを作成したら、VisualStudioの[Test]=>[Run]=>[All Tests]で実行します。
これらは、異なるコンピュータ地域標準のテストにも役立ちます。これらのテストを実行することにより、可能性のあるエラー(たとえば、小数点区切り記号に関連する)を検出して修正できます。
最適化マネージャ(OptimisationManager)
アプリケーションの基準の1つは拡張性です。最適化プロセスは次の記事で変更されますが、メインのアドオンUIは大幅な変更を必要としません。そのため、最適化プロセスをモデルクラスとしてではなく、抽象クラスとして実装することにしました。その実装は、要求された最適化の方法に依存します。このクラスは、抽象クラスファクトリテンプレートに従って記述されます。ファクトリから始めましょう。
/// <概要> /// Factory for creating classes that manage the optimization process /// </概要> abstract class OptimisationManagerFabric { /// <概要> /// Constructor /// </概要> /// <param name="ManagerName">The name of the created optimization manager</param> public OptimisationManagerFabric(string ManagerName) { this.ManagerName = ManagerName; } /// <概要> /// Name reflecting the type of the created optimization manager (its features) /// </概要> public string ManagerName { get; } /// <概要> /// Method creating the optimization manager /// </概要> /// <returns>Optimization manager</returns> public abstract OptimisationManager Create(Dictionary<string, BotParamKeeper> botParamsKeeper, List<ViewModel.TerminalAndBotItem> selectedTerminals); }
お分かりのように抽象ファクトリクラスには実装されたクラスの名前が含まれています。これは、最適化マネージャを作成するメソッドと同様に、今後の記事で使用されます。最適化マネージャは、各最適化の前に作成され、その後ターミナルで操作を行うと想定されています。したがって、EAのリストとターミナルのリストを持つディクショナリなどのパラメータ(つまり、最適化の違いによって異なるパラメータ)は、 オブジェクト作成メソッドに渡されます。他のすべての必須パラメータは、コンストラクタから特定のファクトリのクラスに渡されます。次にOptimisationManagerクラスを検討しましょう。このクラスは、最適化を管理するように設計されていますが、テストの起動も担当します。テストの起動はほとんど常に同じアルゴリズムに従って実行されるため、この機能は考慮される抽象クラスに直接実装されます。以下のクラス実装を検討します。最適化の開始と停止に関しては、この機能は2つの抽象メソッドで実装され、子クラスでの実装を必要とします。クラスコンストラクタは、ファクトリの過剰な量を受け入れるため、上記のすべてのオブジェクトを操作できます。
public OptimisationManager(TerminalDirectory terminalDirectory, TerminalCreator terminalCreator, ConfigCreator configCreator, ReportReaderCreator reportReaderCreator, SetFileManagerCreator setFileManagerCreator, OptimisationExtentionWorkingDirectory currentWorkingDirectory, Dictionary<string, BotParamKeeper> botParamsKeeper, Action<double, string, bool> pbUpdate, List<ViewModel.TerminalAndBotItem> selectedTerminals, OptimisatorSettingsManagerCreator optimisatorSettingsManagerCreator)
AllOptimisationsFinishedイベントは、最適化の完了をモデルクラスに通知するために使用されます。次のプロパティを使用すると、この最適化マネージャに含まれるターミナルとロボットに関するデータにモデルクラスからアクセスできます。
/// <概要> /// Dictionary where: /// key - terminal ID /// value - full path to the robot /// </概要> public virtual Dictionary<string, string> TerminalAndBotPairs { get { Dictionary<string, string> ans = new Dictionary<string, string>(); foreach (var item in botParamsKeeper) { ans.Add(item.Key, item.Value.BotName); } return ans; } }
このプロパティは抽象クラスで実装されますが、「virtual」キーワードでマークされているために上書き可能です。モデルクラスに最適化/テストプロセスが開始されたかどうかを確認する機能を提供するために、適切なプロパティが作成されました。プロパティ値は、最適化/テストプロセスを起動するメソッドから設定されます。
public bool IsOptimisationOrTestInProcess { get; private set; } = false;
便宜上、最適化クラスとテスト起動クラスでほとんどの場合変更されない長いクラスは、抽象クラスに直接実装されます。以下は、構成ファイルを作成すメソッドです。
protected virtual Config CreateConfig(ConfigCreator_inputData data, OptimisationInputData optData) { DirectoryInfo termonalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == data.TerminalID); Config config = configCreator.Create(Path.Combine(termonalChangableFolder.GetDirectory("config").FullName, "common.ini")) .DublicateFile(Path.Combine(currentWorkingDirectory.Configs.FullName, $"{data.TerminalID}.ini")); // Fill the configuration file config.Common.Login = data.Login; config.Common.Password = data.Pass; config.Common.CertPassword = data.CertPass; if (!string.IsNullOrEmpty(data.Server) || !string.IsNullOrWhiteSpace(data.Server)) { try { config.Common.Server = new ServerAddressKeeper(data.Server); } catch (Exception e) { System.Windows.MessageBox.Show($"Server address was incorrect. Your adress is '{data.Server}' but mast have following type 'IPv4:Port'" + $"\nError message:\n{e.Message}\n\nStack Trace is {e.StackTrace}"); return null; } } bool IsOptimisation = (data.OptimisationMode == ENUM_OptimisationMode.Fast_genetic_based_algorithm || data.OptimisationMode == ENUM_OptimisationMode.Slow_complete_algorithm); config.Tester.Expert = data.pathToBot; config.Tester.ExpertParameters = data.setFileName; сonfig.Tester.Symbol = data.Symbol; config.Tester.Period = data.TF; config.Tester.Login = optData.Login; config.Tester.Model = optData.Model; config.Tester.ExecutionMode = optData.ExecutionDelay; config.Tester.Optimization = data.OptimisationMode; с data.From; config.Tester.ToDate = data.Till; config.Tester.ForwardMode = ENUM_ForvardMode.Custom; config.Tester.ForwardDate = optData.ForvardDate; config.Tester.ShutdownTerminal = IsOptimisation; config.Tester.Deposit = optData.Deposit; config.Tester.Currency = optData.Currency; config.Tester.Leverage = optData.Laverage; config.Tester.OptimizationCriterion = optData.GetOptimisationCriteria; config.Tester.Visual = optData.IsVisual; if (IsOptimisation) { config.Tester.Report = data.ReportName; config.Tester.ReplaceReport = true; } return config; }
まず、ターミナルの可変ディレクトリを記述するクラスと、Config型のオブジェクトを作成するファクトリを使用して構成ファイルオブジェクトを作成し、それをアドオンの適切なディレクトリにコピーします。元の構成ファイルが属していたターミナルのIDと同じ名前を設定します。 次に、コピーした構成ファイルの[テスター]セクションに書き込みます。このセクションに書き込むすべてのデータは、渡された構造から直接取得されます。これらの構造は、コード内で形成されるか(最適化の場合)、ファイルから取得されます(テスト開始の場合)。 サーバが正しく渡されていないと、適切なメッセージがMessageBoxとして出力されますが、構成ファイルの代わりにnullが返されます。繰り返しコードを分離する目的で、ターミナルマネージャを作成するメソッドを抽象クラスに実装します。 こちらです。
protected virtual ITerminalManager GetTerminal(Config config, string TerminalID) { DirectoryInfo TerminalChangebleFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID); ITerminalManager terminal = terminalCreator.Create(TerminalChangebleFolder); terminal.Config = config; if (MQL5Connector.MainTerminalID == terminal.TerminalID) terminal.Portable = true; return terminal; }
必要なターミナルのIDがアドオンを起動したターミナルのIDと一致する場合、ターミナルはPortableモードで起動するように構成されますが、アプリはターミナルが標準モードで起動されたときにのみ正常に動作できます。したがって、現在のターミナルを無視し、利用可能なターミナルのリストに追加しないフィルタがあります。
ダブルクリックイベントで選択したターミナルでテストを起動するメソッドは、抽象クラスでも実装されます。
/// <概要> /// Method for launching a test upon a double-click event /// </概要> /// <param name="TerminalID">ID of the selected terminal</param> /// <param name="pathToBot">Path to the robot relative to the experts tab</param> /// <param name="row">Row from the optimizations table</param> public virtual void StartTest(ConfigCreator_inputData data, OptimisationInputData optData) { pbUpdate(0, "Start Test", true); double pb_step = 100.0 / 3; IsOptimisationOrTestInProcess = true; pbUpdate(pb_step, "Create Config File", false); Config config = CreateConfig(data, optData); config.Tester.Optimization = ENUM_OptimisationMode.Disabled; config.Tester.ShutdownTerminal = false; config.DeleteKey(ENUM_SectionType.Tester, "ReplaceReport"); config.DeleteKey(ENUM_SectionType.Tester, "Report"); pbUpdate(pb_step, "Create TerminalManager", false); ITerminalManager terminal = GetTerminal(config, data.TerminalID); pbUpdate(pb_step, "Testing", false); terminal.Run(); terminal.WaitForStop(); IsOptimisationOrTestInProcess = false; pbUpdate(0, null, true); }
入力では、設定を保存したファイルから、モデルを記述するクラスで受信したデータを受け入れます。メソッド内では、渡されたデリゲートを介してプログレスバーの値と操作ステータスも設定されます。生成された構成ファイルは、テスターを実行するように調整されます。オプティマイザーレポートを記述するキーが削除され、テスター終了後のターミナルの自動シャットダウンがオフになります。 ターミナルの起動後、ターミナルを起動したスレッドがフリーズし、その操作が完了するのを待ちます。したがって、フォームにはテストの終了が通知されます。最適化/テストの起動時にフォームがフリーズするのを防ぐために、これらのプロセスはセカンダリスレッドのコンテキストで起動されます。最適化に関しては、すでに前述したように、このプロセスはprotectedの抽象メソッドで実装されます。ただし、抽象クラスには1つのpublicメソッドが実装されています。これは、クラスの正しい操作に必要であり、書き換えることはできません。
/// <概要> /// Launching optimization/testing for all planned terminals /// </概要> /// <param name="BotParamsKeeper">List of terminals, robots and robot parameters</param> /// <param name="PBUpdate">The delegate editing the values of the progress bar and the status</param> /// <param name="sturtup_status">Response from the function - only used if optimization/test could not be started /// reason for that is written here</param> /// <returns>true - if successful</returns> public void StartOptimisation() { pbUpdate(0, "Start Optimisation", true); IsOptimisationOrTestInProcess = true; DoOptimisation(); OnAllOptimisationsFinished(); IsOptimisationOrTestInProcess = false; pbUpdate(0, null, true); } protected abstract void DoOptimisation(); /// <概要> /// The method interrupting optimizations /// </概要> public abstract void BreakOptimisation();
このメソッドでは、プログレスバーの更新、最適化開始フラグと完了フラグの設定、および最適化パス完了イベントの呼び出しに関して、最適化プロセスがトリガーする順序を調整します。
抽象クラスに実装される最後のメソッドは、レポートをアドオンの作業ディレクトリに移動するメソッドです。レポートの移動に加えて、最適化設定を含むファイルを作成する必要があるため、これらのアクションは別のメソッドで実装されます。
protected virtual void MoveReportToWorkingDirectery(ITerminalManager terminalManager, string FileName, ConfigCreator_inputData ConfigCreator_inputData, OptimisationInputData OptimisationInputData) { FileInfo pathToFile_history = new FileInfo(Path.Combine(terminalManager.TerminalChangeableDirectory.FullName, $"{FileName}.xml")); FileInfo pathToFile_forward = new FileInfo(Path.Combine(terminalManager.TerminalChangeableDirectory.FullName, $"{FileName}.forward.xml")); int _i = 0; while (_i <= 100 && (!pathToFile_history.Exists && !pathToFile_forward.Exists)) { _i++; System.Threading.Thread.Sleep(500); } string botName = new FileInfo(terminalManager.Config.Tester.Expert).Name.Split('.')[0]; DirectoryInfo terminalReportDirectory = currentWorkingDirectory.Reports.GetDirectory(terminalManager.TerminalID, true); if (terminalReportDirectory == null) throw new Exception("Can`t create directory"); DirectoryInfo botReportDir = terminalReportDirectory.GetDirectory(botName, true); if (botReportDir == null) throw new Exception("Can`t create directory"); FileInfo _history = new FileInfo(Path.Combine(botReportDir.FullName, "History.xml")); FileInfo _forward = new FileInfo(Path.Combine(botReportDir.FullName, "Forward.xml")); if (_history.Exists) _history.Delete(); if (_forward.Exists) _forward.Delete(); if (pathToFile_history.Exists) { pathToFile_history.CopyTo(_history.FullName, true); pathToFile_history.Delete(); } if (pathToFile_forward.Exists) { pathToFile_forward.CopyTo(_forward.FullName, true); pathToFile_forward.Delete(); } string pathToSetFile = Path.Combine(terminalManager.TerminalChangeableDirectory .GetDirectory("MQL5") .GetDirectory("Profiles") .GetDirectory("Tester").FullName, ConfigCreator_inputData.setFileName); using (OptimisatorSettingsManager manager = optimisatorSettingsManagerCreator.Create(botReportDir.FullName)) { manager.OptimisationInputData = OptimisationInputData; manager.ConfigCreator_inputData = ConfigCreator_inputData; manager.SetFileParams = setFileManagerCreator.Create(pathToSetFile, false).Params; } }
まず、このメソッドでは、レポートを含むファイルへのパスを取得します。次に、目的のファイルの1つが作成されるまでループで待機します(フォワード期間のない履歴最適化中には常にはfileが2つ生成されない可能性があるため、1つだけ)。次に、レポートのあるファイルが保存されるディレクトリへのパスを作成します。実際、このコードスニペットには、Reportsディレクトリのサブフォルダのレイアウトが含まれています。次に、将来のファイルへのパスを作成し、古いファイルがあれば削除します。その後、アドオンディレクトリにレポートがコピーされます。最後に、最適化の起動中に使用された設定で*.xmlファイルを作成します。 このプロセスは段階的に実行される必要があって変更される可能性は低いため、抽象クラスに移動されています。開始するには、単にこのメソッドを子クラスから呼び出します。
次に、実装された最適化プロセスについて考えてみましょう。現在、標準テスターのように、選択された最適化パラメータを使用した通常のターミナル起動です。実装の最も興味深い側面は、起動プロセスと最適化完了イベントのハンドラです。
private readonly List<ITerminalManager> terminals = new List<ITerminalManager>(); /// <概要> /// The method interrupts the optimization process and forcibly closes the terminals /// </概要> public override void BreakOptimisation() { foreach (var item in terminals) { if (item.IsActive) item.Close(); } } private void UnsubscribeTerminals() { if (terminals.Count > 0) { foreach (var item in terminals) { item.TerminalClosed -= Terminal_TerminalClosed; } terminals.Clear(); } } protected override void DoOptimisation() { UnsubscribeTerminals(); double pb_step = 100.0 / (botParamsKeeper.Count + 1); foreach (var item in botParamsKeeper) { pbUpdate(pb_step, item.Key, false); ConfigCreator_inputData configInputData = GetConfigCreator_inputData(item.Key); OptimisationInputData optData = item.Value.OptimisationData; Config config = CreateConfig(configInputData, optData); ITerminalManager terminal = GetTerminal(config, item.Key); terminal.TerminalClosed += Terminal_TerminalClosed; terminal.Run(); terminals.Add(terminal); } pbUpdate(pb_step, "Waiting for Results", false); foreach (var item in terminals) { if (item.IsActive) item.WaitForStop(); } }
ターミナルマネージャのリストはフィールドで実装されており、さまざまなメソッドからアクセスできます。これにより、BreakOptimisationsメソッドの実装も可能になります。最適化プロセスの起動方法では、ターミナルを作成した後、ターミナルを閉じるイベントにサブスクライブするため、最適化の完了を追跡できます。最適化の起動後、起動されたすべてのターミナルが閉じられるまで、スレッドをループに保持します。UnsubscribeTerminalsメソッドは、最適化の再起動の場合に以前にサブスクライブされたすべてのイベントからサブスクライブを解除するために使用されます。 このメソッドはクラスデストラクタで呼び出されます。 最適化停止イベントハンドラは、次のように実装されます。
protected virtual void Terminal_TerminalClosed(ITerminalManager terminalManager) { string FileName = new FileInfo(terminalManager.Config.Tester.Expert).Name.Split('.')[0]; ConfigCreator_inputData ConfigCreator_inputDat = GetConfigCreator_inputData(terminalManager.TerminalID); OptimisationInputData optData = botParamsKeeper[terminalManager.TerminalID].OptimisationData; MoveReportToWorkingDirectery(terminalManager, FileName, ConfigCreator_inputDat, optData); } private ConfigCreator_inputData GetConfigCreator_inputData(string TerminalID) { ViewModel.TerminalAndBotItem settingsData = selectedTerminals.Find(x => x.TerminalID == TerminalID); BotParamKeeper ParamKeeper = botParamsKeeper[TerminalID]; ConfigCreator_inputData ConfigCreator_inputDat = new ConfigCreator_inputData { TerminalID = TerminalID, pathToBot = ParamKeeper.BotName, CertPass = settingsData.CertPass, From = settingsData.From, Till = settingsData.Till, Login = settingsData.Login, OptimisationMode = settingsData.GetOptimisationMode, Pass = settingsData.Pass, Server = settingsData.Server, setFileName = botParamsKeeper[TerminalID].BotParams.FileInfo.Name, Symbol = settingsData.AssetName, TF = settingsData.GetTF, ReportName = new FileInfo(ParamKeeper.BotName).Name.Split('.')[0] }; return ConfigCreator_inputDat; }
主な目的は、最適化レポートを含むファイルを適切なディレクトリに移動することです。したがって、最適化およびテスト起動ロジックが実装されます。次の記事で実行する操作の1つは、説明したサンプルに従って追加の最適化メソッドを実装することです。 作成したアプリケーションのほぼ全体を調べたので、次に、ViewModelから参照されるモデルを説明する主要な結果クラスを見てみましょう。
結果のモデルクラス(IExtentionGUI_Mおよびその実装)
説明したプロジェクトのこの部分は、IExtentionGUI_Mインターフェイスを実装し、説明したフォームのロジックを実装する開始点です。ラフィック部分とViewModelは、このクラスを参照してデータを受け取り、さまざまなコマンドの実行を委任します。次のように実装されたインターフェイスから始めましょう。
/// <概要> /// Model interface /// </概要> interface IExtentionGUI_M : INotifyPropertyChanged { #region Properties bool IsTerminalsLVEnabled { get; } List<FileReaders.ParamsItem> BotParams { get; } VarKeeper<string> Status { get; } VarKeeper<double> PB_Value { get; } ObservableCollection<string> TerminalsID { get; } DataTable HistoryOptimisationResults { get; } DataTable ForvardOptimisationResults { get; } ObservableCollection<ViewExtention.ColumnDescriptor> OptimisationResultsColumnHeadders { get; } ObservableCollection<string> TerminalsAfterOptimisation { get; } VarKeeper<int> TerminalsAfterOptimisation_Selected { get; set; } ObservableCollection<string> BotsAfterOptimisation { get; } VarKeeper<int> BotsAfterOptimisation_Selected { get; set; } #endregion void LoadOptimisations(); void LoadBotParams(string fullExpertName, string TerminalID, out OptimisationInputData? optimisationInputData); List<string> GetBotNamesList(int terminalIndex); uint? GetCurrentLogin(int terminalIndex); void StartOptimisationOrTest(List<ViewModel.TerminalAndBotItem> SelectedTerminals); void StartTest(ENUM_TableType TableType, int rowIndex); bool RemoveBotParams(string TerminalID); bool IsEnableToAddNewTerminal(); void SelectNewBotsAfterOptimisation_forNewTerminal(); void UpdateTerminalOptimisationsParams(OptimisationInputData optimisationInputData); } #region Accessory objects /// <概要> /// Enum characterizing the type of tables with optimization results /// </概要> enum ENUM_TableType { History, Forvard }
これは、ViewModelが動作するインターフェイスですが、必要に応じて、実装は置き換えることができます。この場合、プログラムのグラフィカル部分を変更する必要はありません。 一方、そのロジックを変更せずにグラフィカル部分を変更することもできます。このインターフェイスはINotifyPropertyChangedインターフェイスから継承されるため、このデータモデルに実装されているプロパティのいずれかが変更された場合、ViewModelとViewに通知する機会があります。便宜上、汎用ラッパークラスVarKeeperを追加しました。これは、型の値を格納することに加えて、格納された型に暗黙的にキャストでき、格納された値が変更された場合にViewModelに通知します。クラスの実装は次のとおりです。
/// <概要> /// Class storing the variable _Var of type T_keeper. /// We can implicitly cast to type T_keeper and also change the value of the stored variable /// At the time of changing the value it notifies all those which have subscribed /// </概要> /// <typeparam name="T_keeper">Type of stored variable</typeparam> class VarKeeper<T_keeper> { /// <概要> /// Constructor specifying the variable identification name /// </概要> /// <param name="propertyName">Variable identification name</param> public VarKeeper(string propertyName) { this.propertyName = propertyName; } /// <概要> /// Constructor specifying the variable identification name /// and the initial value of the variable /// </概要> /// <param name="PropertyName">Identification name of the variable</param> /// <param name="Var">initial value of the variable</param> public VarKeeper(string PropertyName, T_keeper Var) : this(PropertyName) { _Var = Var; } /// <概要> /// Overloading the implicit type conversion operator. /// Converts this type to T_keeper /// </概要> /// <param name="obj"></param> public static implicit operator T_keeper(VarKeeper<T_keeper> obj) { return obj._Var; } /// <概要> /// stored variable /// </概要> protected T_keeper _Var; /// <概要> /// Identification name of the variable /// </概要> public readonly string propertyName; #region Event /// <概要> /// Event notifying about the change of the stored variable /// </概要> public event Action<string> PropertyChanged; /// <概要> /// Method that calls the event notifying about the change of the stored variable /// </概要> protected void OnPropertyChanged() { PropertyChanged?.Invoke(propertyName); } #endregion /// <概要> /// Method which sets the value of a variable with the 'value' value /// </概要> /// <param name="value">new value of the variable</param> public void SetVar(T_keeper value) { SetVarSilently(value); OnPropertyChanged(); } public void SetVarSilently(T_keeper value) { _Var = value; } }
クラスコンストラクタでは、格納された変数の初期値と、値の変更を通知するために使用される変数の名前を渡します。変数は、このクラスのprotectedフィールドに保存されます。値の変更の通知に使用される変数の名前は、読み取り専用のpublicフィールドropertyNameに格納されます。変数値の設定方法は、値を設定してイベントを呼び出してすべてのサブスクライバーにこの変更を通知するメソッドと、変数値のみを設定するメソッドに分けられます。暗黙的にクラスをストアド値型に変換できるように、型キャスト演算子のオーバーロードが使用されます。このクラスで、変数値を保存し、明示的な型変換を使用せずにそれらを読み取り、変数値の変更を環境に通知できるようになります。IExtentionGUI_Mインターフェイスを実装するクラスのコンストラクタで、プロパティに値を設定し、これらのプロパティの更新が通知されるようにサブスクライブします。クラスデストラクタでは、プロパティイベントからサブスクライブ解除します。
public ExtentionGUI_M(TerminalCreator TerminalCreator, ConfigCreator ConfigCreator, ReportReaderCreator ReportReaderCreator, SetFileManagerCreator SetFileManagerCreator, OptimisationExtentionWorkingDirectory CurrentWorkingDirectory, OptimisatorSettingsManagerCreator SettingsManagerCreator, TerminalDirectory terminalDirectory) { // Assign the current working directory this.CurrentWorkingDirectory = CurrentWorkingDirectory; this.terminalDirectory = terminalDirectory; //Create factories this.TerminalCreator = TerminalCreator; this.ReportReaderCreator = ReportReaderCreator; this.ConfigCreator = ConfigCreator; this.SetFileManagerCreator = SetFileManagerCreator; this.SettingsManagerCreator = SettingsManagerCreator; CreateOptimisationManagerFabrics(); // subscribe to the event of a change in columns of the historic optimizations table HistoryOptimisationResults.Columns.CollectionChanged += Columns_CollectionChanged; // Assign initial status Status = new VarKeeper<string>("Status", "Wait for the operation"); Status.PropertyChanged += OnPropertyChanged; // Assign initial values for the progress bar PB_Value = new VarKeeper<double>("PB_Value", 0); PB_Value.PropertyChanged += OnPropertyChanged; // Create a variable storing the index of terminal selected from the list of available terminals for which optimization was done TerminalsAfterOptimisation_Selected = new VarKeeper<int>("TerminalsAfterOptimisation_Selected", 0); TerminalsAfterOptimisation_Selected.PropertyChanged += OnPropertyChanged; // Create a variable storing the index of robot selected from the list of available robots for which optimization was done BotsAfterOptimisation_Selected = new VarKeeper<int>("BotsAfterOptimisation_Selected", -1); BotsAfterOptimisation_Selected.PropertyChanged += OnPropertyChanged; _isTerminalsEnabled = new VarKeeper<bool>("IsTerminalsLVEnabled", true); _isTerminalsEnabled.PropertyChanged += OnPropertyChanged; // Load data on terminals installed on the computer FillInTerminalsID(); FillInTerminalsAfterOptimisation(); LoadOptimisations(); }
次のメソッドがコンストラクタで呼び出されます。
- CreateOptimisationManagerFabrics — 最適化マネージャを作成するファクトリです。最適化マネージャは配列に追加され、後に必要な最適化マネージャが特定の基準に従って選択されます。
- FillInTerminalsID — ターミナルIDのリストに書き入れます。このリストは、最適化の前に、ターミナルの選択可能なドロップダウンリストに表示され、現在のターミナル以外の見つかったすべてのターミナルが追加されています。
- FillInTerminalsAfterOptimisation — 最適化のいずれかがすでに実行され、最適化データに読み込むデータを持つターミナルのリストに書き入れます。
- LoadOptimiations — 選択したターミナルとロボットに応じて最適化の表に書き入れます(現在、両方のパラメータのインデックスはゼロです)。
したがって、操作のプログラムを準備し、すべての表と変数に初期値を設定するという、コンストラクのメインタスクを実装します。次の段階では、最適化のために選択されたターミナルの表で作業します。選択されたすべてのターミナルは、クラスフィールドのいずれかの語彙に保存されます。
/// <概要> /// Presenting the table of selected terminals at the start tab of the add-on /// key - Terminal ID /// value - bot params /// </概要> private readonly Dictionary<string, BotParamKeeper> BotParamsKeeper = new Dictionary<string, BotParamKeeper>(); /// <概要> /// Currently selected terminal /// </概要> private string selectedTerminalID = null; /// <概要> /// List of robot parameters to be edited /// </概要> List<ParamsItem> IExtentionGUI_M.BotParams { get { return (BotParamsKeeper.Count > 0 && selectedTerminalID != null) ? BotParamsKeeper[selectedTerminalID].BotParams.Params : new List<ParamsItem>(); } }
BotParamsはこの語彙からEAパラメータのリストを受け取り、選択したロボットが変更されると(このメカニズムについてはさらに説明します)、この語彙の新しいキーにアクセスします。語彙の内容は、このアドオンの最初のタブのドロップダウンリストから選択された新しいターミナルを追加するボタンがクリックされた直後に呼び出されるLoadBotParamメソッドによって制御されます。このメソッドは次のように実装されます。
void IExtentionGUI_M.LoadBotParams(string fullExpertName, string TerminalID, out OptimisationInputData? optimisationInputData) { PBUpdate(0, "Loading params", true); optimisationInputData = null; if (!IsTerminalsLVEnabled) return; _isTerminalsEnabled.SetVar(false); if (!BotParamsKeeper.Keys.Contains(TerminalID)) { PBUpdate(100, "Add New Terminal", false); AddNewTerminalIntoBotParamsKeeper(fullExpertName, TerminalID); } else { if (selectedTerminalID != null) BotParamsKeeper[selectedTerminalID].BotParams.SaveParams(); else { foreach (var item in BotParamsKeeper) { item.Value.BotParams.SaveParams(); } } } selectedTerminalID = TerminalID; optimisationInputData = BotParamsKeeper[selectedTerminalID].OptimisationData; if (BotParamsKeeper[selectedTerminalID].BotName != fullExpertName) { PBUpdate(100, "Load new params", false); BotParamKeeper param = BotParamsKeeper[selectedTerminalID]; param.BotName = fullExpertName; param.BotParams = GetSetFile(fullExpertName, TerminalID); BotParamsKeeper[selectedTerminalID] = param; } PBUpdate(0, null, true); _isTerminalsEnabled.SetVar(true); }
コードからわかるように、テストの最適化中にユーザーインターフェイスをブロックすることに加えて(ビデオに示されているように)、コードにはロボット(および場合によってはターミナル)パラメータのリストを更新できるかどうかの確認も含まれます。 ロボットまたはターミナルのパラメータを更新できる場合は、グラフィカルインターフェイスをブロックします。次に、新しいロボットが追加されるか、GUIを介して以前に入力されたパラメータが保存されます。その後、 選択したターミナルIDが保存され(語彙のキー)、新しく選択したロボットのパラメータがViewModelに返されます。選択したロボットを以前に選択したロボットと比較して変更した場合、GetSetFileメソッドを使用してロボットのパラメータをアップロードします。新しいターミナルを追加するメソッドは非常に単純で、考慮されたメソッドの最後の条件付き構築をほぼ完全に繰り返します。 主な作業は、GetSetFileメソッドによって実行されます。
private SetFileManager GetSetFile(string fullExpertName, string TerminalID) { DirectoryInfo terminalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID); // Creating a manager for working with the terminal ITerminalManager terminalManager = TerminalCreator.Create(terminalChangableFolder); // Creating path to the Tester folder (which is under ~/MQL5/Profiles) // If there is no such folder, create it yourself // Files with optimization parameter settings are stored in it DirectoryInfo pathToMqlTesterFolder = terminalManager.MQL5Directory.GetDirectory("Profiles").GetDirectory("Tester", true); if (pathToMqlTesterFolder == null) throw new Exception("Can`t find (or create) ~/MQL5/Profiles/Tester directory"); // Create a configuration file and copy it to the Configs folder of the current working add-on directory Config config = ConfigCreator.Create(Path.Combine(terminalChangableFolder.GetDirectory("config").FullName, "common.ini")) .DublicateFile(Path.Combine(CurrentWorkingDirectory.Configs.FullName, $"{TerminalID}.ini")); // Configure the terminal so that it launches the selected robot test and immediately shuts down // Thus the terminal will create a .set file with this Expert Advisor settings. // To have it immediately shut down, specify the test end one day lower than the start date. config.Tester.Expert = fullExpertName; config.Tester.Model = ENUM_Model.OHLC_1_minute; config.Tester.Optimization = ENUM_OptimisationMode.Disabled; config.Tester.Period = ENUM_Timeframes.D1; config.Tester.ShutdownTerminal = true; config.Tester.FromDate = DateTime.Now.Date; config.Tester.ToDate = config.Tester.FromDate.Value.AddDays(-1); // Set configuration file to the terminal manager, launch it and wait for he terminal to close // To enable automatic terminal shut down after testing completion, // assign the true value to field config.Tester.ShutdownTerminal terminalManager.Config = config; terminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized; string fileName = $"{new FileInfo(fullExpertName).Name.Split('.')[0]}.set"; while (!terminalManager.Run()) { System.Windows.MessageBoxResult mb_ans = System.Windows.MessageBox.Show(@"Can`t start terminal Close manually all MetaTrader terminals that are running now (except main terminal)", "Can`t start terminal", System.Windows.MessageBoxButton.OKCancel); if (mb_ans == System.Windows.MessageBoxResult.Cancel) break; } terminalManager.WaitForStop(); bool isSetFileWasCreated = pathToMqlTesterFolder.GetFiles().Any(x => x.Name == fileName); return SetFileManagerCreator.Create(Path.Combine(pathToMqlTesterFolder.FullName, fileName), !isSetFileWasCreated); }
このメソッドはよくコメントされています。 その主な目的を説明しましょう。メソッドは、選択されたロボットのパラメータ、つまりそのSETファイルを受け取ります。このファイルは、テスターでロボットが開始されるとターミナルによって作成されるため、ファイルを生成する唯一の方法は、選択したアルゴリズムをテスターで実行することです。これを明示的に実行しないために、実行中のテスターを備えたターミナルは最小化モードで起動されます。テスターが迅速に操作を完了してシャットダウンできるように、テストの開始日より1日前にテストの終了日を設定します。ターミナルがすでに実行されている場合、 ターミナルを開こうとする試みがループで実行され、適切なメッセージが表示されます。操作後、SETファイルのオブジェクト指向的表現を返します。
このクラスの次の興味深い点は、最適化開始プロセスです。これは、非同期のStartOptimisationOrTestメソッドによって実行されます。
async void IExtentionGUI_M.StartOptimisationOrTest(List<ViewModel.TerminalAndBotItem> SelectedTerminals) { if (BotParamsKeeper.Count == 0) return; foreach (var item in BotParamsKeeper) { item.Value.BotParams.SaveParams(); } SetOptimisationManager(SelectedTerminals); // Run the optimization and wait for it to finish _isTerminalsEnabled.SetVar(false); await System.Threading.Tasks.Task.Run(() => selectedOptimisationManager.StartOptimisation()); _isTerminalsEnabled.SetVar(true); } private void SetOptimisationManager(List<ViewModel.TerminalAndBotItem> SelectedTerminals) { // Select a factory to create an optimization manager from the list OptimisationManagerFabric OMFabric = optimisationManagerFabrics[0]; // Unsubscribe from a previously used optimization manager if (selectedOptimisationManager != null) { // Check if optimization is running at the moment if (selectedOptimisationManager.IsOptimisationOrTestInProcess) return; selectedOptimisationManager.AllOptimisationsFinished -= SelectedOptimisationManager_AllOptimisationsFinished; } // Create an optimization manager and subscribe it to the optimization completion event selectedOptimisationManager = OMFabric.Create(BotParamsKeeper, SelectedTerminals); selectedOptimisationManager.AllOptimisationsFinished += SelectedOptimisationManager_AllOptimisationsFinished; }
この実装は、最適化マネージャの使用方法を示しています。つまり、最適化マネージャは各最適化が開始される前に 再作成されます。この実装では、対応する配列からの最初のマネージャに対してのみ作成が実行されています。 より複雑なプロセスについては、次の記事で説明します。 テストの起動は、最適化の開始に似ていますが、ロボットパラメータがダブルクリックで選択されたパラメータに置き換えられます。
async void IExtentionGUI_M.StartTest(ENUM_TableType TableType, int rowIndex) { if (!IsTerminalsLVEnabled) return; string TerminalID = TerminalsAfterOptimisation[TerminalsAfterOptimisation_Selected]; string pathToBot = BotsAfterOptimisation[BotsAfterOptimisation_Selected]; DirectoryInfo terminalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID); DataRow row = (TableType == ENUM_TableType.History ? HistoryOptimisationResults : ForvardOptimisationResults).Rows[rowIndex]; ConfigCreator_inputData configInputData; OptimisationInputData OptimisatorSettings; DirectoryInfo BotReportDirectory = CurrentWorkingDirectory.Reports.GetDirectory(TerminalID).GetDirectory(pathToBot); using (OptimisatorSettingsManager settingsManager = SettingsManagerCreator.Create(BotReportDirectory.FullName)) { configInputData = settingsManager.ConfigCreator_inputData; OptimisatorSettings = settingsManager.OptimisationInputData; string setFilePath = Path.Combine(terminalChangableFolder .GetDirectory("MQL5") .GetDirectory("Profiles") .GetDirectory("Tester", true).FullName, configInputData.setFileName); SetFileManager setFile = SetFileManagerCreator.Create(setFilePath, true); setFile.Params = settingsManager.SetFileParams; foreach (var item in setFile.Params) { if (row.Table.Columns.Contains(item.Variable)) item.Value = row[item.Variable].ToString(); } setFile.SaveParams(); } _isTerminalsEnabled.SetVar(false); if (selectedOptimisationManager == null) SetOptimisationManager(new List<ViewModel.TerminalAndBotItem>()); await System.Threading.Tasks.Task.Run(() => { selectedOptimisationManager.StartTest(configInputData, OptimisatorSettings); }); _isTerminalsEnabled.SetVar(true); }
このメソッドも非同期です。また、以前に作成されていない場合、最適化マネージャの作成も含まれます。 テストの入力を取得するには、選択したロボットの最適化レポートの横にある 設定ファイルを呼び出します。ロボット設定ファイルが作成されたら、最適化レポートで指定されたパラメータを見つけ、「Value」パラメータで選択された最適化行から値を設定します。 パラメータを保存したら、 テストの起動に進みます。
最適化結果を適切な表にアップロードするには、ネストされたメソッドを含む次のメソッドが使用されます。
public void LoadOptimisations() { // Internal method filling the table with data void SetData(bool isForvard, DataTable tb) { // Clear the table from previously added data tb.Clear(); tb.Columns.Clear(); // Get data string TerminalID = TerminalsAfterOptimisation[TerminalsAfterOptimisation_Selected]; string botName = BotsAfterOptimisation[BotsAfterOptimisation_Selected]; string path = Path.Combine(CurrentWorkingDirectory.Reports .GetDirectory(TerminalID) .GetDirectory(botName) .FullName, $"{(isForvard ? "Forward" : "History")}.xml"); if (!File.Exists(path)) return; using (ReportReader reader = ReportReaderCreator.Create(path)) { if (reader.ColumnNames.Count == 0) return; // Fill the columns foreach (var item in reader.ColumnNames) { tb.Columns.Add(item); } // Fill the rows while (reader.Read(out List<KeyValuePair<string, object>> data)) { DataRow row = tb.NewRow(); foreach (var item in data) { row[item.Key] = item.Value; } tb.Rows.Add(row); } } } if (TerminalsAfterOptimisation.Count == 0 && BotsAfterOptimisation.Count == 0) { return; } // Fill historic optimization data first, then add forward test results SetData(false, HistoryOptimisationResults); SetData(true, ForvardOptimisationResults); }
メソッドは、操作が実行されるネストされた関数を2回呼び出します。ネストされた関数では、次が実行されます。
- 渡された表(およびその列)をクリアする
- レポートファイルへのパスを設定する
- ReportReaderクラスを使用してレポートを読み取んでデータを表に読み込む
次のコード行がコンストラクタに含まれています。
// subscribe to the event of a change in columns of the historic optimizations table
HistoryOptimisationResults.Columns.CollectionChanged += Columns_CollectionChanged;
これは、Columns_CollectionChangedメソッドを履歴最適化の表の列更新イベントにサブスクライブします。このメソッドを使用して、列の追加を追跡します。サブスクライブされたメソッド(添付ファイルのコードを参照してください)では、列名がOptimisationResultsColumnHeadersコレクションから自動的に追加または削除され、ViewModelおよびViewに配信され、その後、上記の自動カラムローディング拡張機能を使用してListViewに追加されます 。したがって、列のリストが履歴最適化表で編集されると、両方のテーブルの列がViewで自動的に編集されます。
この章では、最適化の起動、プログラムの読み込み、および履歴とフォワードの最適化パスを使用したファイルの読み込みの実装の詳細を検討し、ダブルクリックイベントでテストパスを起動するメソッドを分析しました。 したがって、ビデオに示されているアプリケーションはほとんど完成しており、ターミナルからの起動のみを実装する必要があります。これは、次のEAとして実装されたラッパーによって実行されます。
//+------------------------------------------------------------------+ //| OptimisationManagerExtention.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" #import "OptimisationManagerExtention.dll" //+------------------------------------------------------------------+ //| エキスパート初期化関数 | //+------------------------------------------------------------------+ int OnInit() { //--- string data[]; StringSplit(TerminalInfoString(TERMINAL_DATA_PATH),'\\',data); MQL5Connector::Instance(data[ArraySize(data)-1]); while(!MQL5Connector::IsWindowActive()) { Sleep(500); } EventSetMillisecondTimer(500); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnTimer() { if(!MQL5Connector::IsWindowActive()) ExpertRemove(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+
C#プロジェクト(リリースモード)をコンパイルした後、適切なディレクトリ(〜/ Libraries)に追加し、ロボットに接続します。現在のターミナルIDを取得するには、可変ディレクトリへのパスを見つけ、StringSplitメソッドを使用してトーケンに分割します。最後のディレクトリにはターミナルIDが含まれます。グラフィックの起動後、ウィンドウが読み込まれるまで、現在のスレッド遅延が有効になります。次に、タイマーを起動します。タイマーは、ウィンドウを閉じるイベントの追跡を有効にします。ウィンドウが閉じたら、チャートからEAを削除する必要があります。したがって、ビデオに示されている動作が実現されます。
結論と添付ファイル
冒頭で、GUIを使用してターミナルに柔軟に拡張可能なアドオンを作成し、最適化プロセスを管理するという目標を設定しました。実装には、グラフィックアプリケーションの開発に便利なインターフェイスを提供するだけでなく、プログラミングプロセスを大幅に簡素化する追加の素晴らしいオプションを提供するC#言語が使用されました。本稿では、コンソールプログラムの起動からC#テクノロジーを使用して別のターミナルからMetaTraderを起動するためのラッパーを作成するまでのアプリケーション作成プロセス全体を検討しました。この研究をが読者の皆さんにとって興味深く有用であることを願っています。私の意見では、本稿の最後の章で説明されているクラスは改善できるので、次の記事ではコードのリファクタリングを紹介するかもしれません。
添付アーカイブには2つのフォルダが含まれます。
- 「MQL5」は、アドオンが起動されるメインのMetaTrader 5ターミナル向けで、アドオンを実行するためのファイルが含まれています。
- 「Visual Studio」には、Visual Studio用に記述された3つのプロジェクトが含まれています。使用する前にコンパイルしてください。OptimisationManagerExtentionをコンパイルして取得された* .dllライブラリは、プロジェクトの起動元であるターミナルのLibrariesディレクトリに追加する必要があります。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/7059
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索