![Gerenciando otimizações (Parte 2): Cirando a lógica do aplicativo e objetos chave](https://c.mql5.com/i/articles/overlay.png)
Gerenciando otimizações (Parte 2): Cirando a lógica do aplicativo e objetos chave
Sumário
- Introdução
- Gerente de terminal externo (ITerminalManager e Config)
- Objetos que exibem a estrutura de diretórios
- Objetos que funcionam com arquivos de relatórios e de configurações de robô (OptimisatorSettingsManager, ReportReader, SetFileManager)
- Teste de objetos chave
- Gerente de otimização
- A classe de modelo resultante (IExtentionGUI_M e sua implementação)
- Conclusão e arquivos anexados
Introdução
Neste artigo, continuamos com a criação de uma interface gráfica amigável para gerenciar otimizações em vários terminais simultaneamente. No artigo anterior, examinamos um método que nos permite iniciar o terminal a partir do console, bem como a estrutura do arquivo de configuração. Neste artigo, consideraremos a criação de um wrapper para um terminal em C#, o que nos permitirá gerenciá-lo como um processo de terceiros. A interface gráfica discutida anteriormente não tinha lógica e não conseguia fazer nada além de responder ao pressionamento de tecla para exibição de texto no console (a partir do qual o iniciamos). Nesta parte, adicionaremos a lógica que processará eventos da GUI e executará a lógica incorporada. Também criaremos vários objetos para trabalhar com arquivos, o que nos permitirá aplicar a parte lógica do programa diretamente e tornará o trabalho muito mais fácil e o código mais informativo. De fato, neste artigo, o complemento descrita finalmente assumirá o formato que foi mostrado no vídeo.
Gerente de terminal externo (ITerminalManager e Config)
Antes, examinamos como criar uma camada gráfica para nosso complemento. Nesta parte, consideraremos a maneira para criar a parte lógica. Com ajuda das vantagens da POO, a parte lógica será dividida em várias classes, cada uma das quais será responsável por sua própria área. Consideraremos esta parte a partir das classes que executarão ações específicas nos arquivos e no terminal e, gradualmente, passaremos a considerar a classe ExtentionGUI_M resultante contendo a lógica final. Durante sua implementação, foram usadas classes com as quais iniciaremos nossa abordagem.
O capítulo atual é dedicado ao trabalho com terminais, além disso, é dividido assim:
- Trabalhando com arquivos de configuração
- Trabalhando com o terminal como um processo de terceiros
Vale a pena começar o estudo com uma discussão sobre o trabalho com arquivos de configuração. A partir das instruções do terminal e de processos de terceiros, podemos descobrir o componente exato. A primeira coisa a fazer é criar todas as enumerações usadas que implementar no arquivo de configuração: os valores numéricos destas enumerações podem ser visualizados no terminal (foi o que eu fiz), já sua implementação pode ser visualizada no arquivo Config.cs. A implementação conveniente de transferir o endereço do servidor é muito mais interessante, afinal, ele deve ser transmitido num determinado formato e, além do endereço do servidor, deve ser indicada a porta. Esse problema foi resolvido criando uma classe que armazena o endereço do servidor recebido por meio do construtor e que verifica se está correto antes de ser instanciado.
/// <summary> /// endereço e porta do servidor IPv4 /// </summary> 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}"; } /// <summary> /// endereço do servidor IPv4 /// </summary> 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}"; } }
Vale apena começar abordando a estrutura IPv4Adress, que armazena o endereço IP do servidor. Quando reunia informações, antes de escrever o artigo, nunca encontrei um endereço de servidor diferente do formato IPv4, por isso, essa estrutura implementa este formato. No seu construtor, ela pega uma string com um endereço, analisa-a e armazena-a nos campos correspondentes. Se o número de dígitos no endereço for menor que 4, será gerado um erro. O construtor da classe principal possui duas sobrecargas, uma delas assume uma representação de string do endereço do servidor, a outra, o endereço IP gerado e o número da porta. A estrutura IPv4Adress também possui o método sobrecarregado Tostring, herdado da classe base Object, da qual, por sua vez, são implicitamente herdados todos os objetos C#. A classe ServerAddressKeeper tem uma propriedade Address que toma conta do mesmo trabalho. Como resultado, obtemos uma classe-wrapper que armazena o endereço do servidor num formato conveniente e sabe como montá-lo no formato necessário para os arquivos de configuração.
Agora vale a pena considerar os recursos para trabalhar com os próprios arquivos de configuração em formato (* .ini). Como mencionado anteriormente, agora este formato está obsoleto e quase nunca é usado. O C# não possui interfaces internas para trabalhar com eles (é o caso do esquema XML que já vimos e que será abordado em mais detalhes nos capítulos seguintes deste artigo). No entanto, o WinApi ainda suporta as funções WritePrivateProfileString e GetPrivateProfileString para trabalhar com arquivos deste formato. Além disso, a Microsoft escreve:
Note: This function is provided only for compatibility with 16-bit Windows-based applications. Applications should store initialization information in the registry.
Estes métodos são armazenados no WinApi apenas para compatibilidade com versões anteriores de aplicativos de 16 bits para Windows. Os aplicativos devem armazenar informações de inicialização no registro. No entanto, podemos usá-los para não inventar nossa própria bicicleta. Para fazer isso, teremos de importar os dados das funções C para o nosso código C # (na verdade, o encaixe de duas linguagens de programação). Em C#, isso é feito da mesma maneira que em 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);
Só que, em vez do comando #import, devemos especificar o atributo DLLImport, passando para ele o nome da dll, da qual importamos a função e outros parâmetros opcionais. Em particular, ao fazer esta importação, eu especifico o parâmetro SetLastErro =true, o que nos dá a possibilidade de obter erros do código C++ usando GetLastError() em nosso código C# e, consequentemente, de processar a execução correta dos métodos em questão. Como a maneira de trabalhar com strings em C# e C é diferente, usaremos métodos-wrapper que permitem uma maneira conveniente de trabalhar com funções exportadas e de manipular possíveis erros. Eu os implemento da seguinte maneira:
/// <summary> /// Wrapper conveniente para a função WinAPI GetPrivateProfileString /// </summary> /// <param name="section">nome da seção</param> /// <param name="key">chave</param> /// <returns>parâmetro solicitado ou null se a chave não for encontrada</returns> protected virtual string GetParam(string section, string key) { //Para ter o valor StringBuilder buffer = new StringBuilder(SIZE); //Obter o valor no buffer if (GetPrivateProfileString(section, key, null, buffer, SIZE, Path) == 0) ThrowCErrorMeneger("GetPrivateProfileStrin", Marshal.GetLastWin32Error()); //Retornar o valor recebido return buffer.Length == 0 ? null : buffer.ToString(); } /// <summary> /// Wrapper conveniente para WinAPI WritePrivateProfileString /// </summary> /// <param name="section">Seção</param> /// <param name="key">Chave</param> /// <param name="value">Valor</param> protected virtual void WriteParam(string section, string key, string value) { //Registrar valor num arquivo .ini if (WritePrivateProfileString(section, key, value, Path) == 0) ThrowCErrorMeneger("WritePrivateProfileString", Marshal.GetLastWin32Error()); } /// <summary> /// Exibição de erro /// </summary> /// <param name="methodName">Nome de método</param> /// <param name="er">Código de erro</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/pt-br/windows/win32/debug/system-error-codes) for detales"); } } }
Ao trabalhar com estes métodos, descobri uma particularidade interessante. Após procurar informações, verifiquei que esse não era apenas o meu problema. A peculiaridade de trabalhar com estas funções é que o método GetPrivateProfileString retorna o erro ERROR_FILE_NOT_FOUND (código de erro = 2) não apenas quando o arquivo não é encontrado, mas também nas seguintes condições:
- A seção não existe num arquivo legível
- A chave solicitada não existe
É por causa desse peculiaridade que no métodoThrowCErrorMeneger verificamos se existe um arquivo legível, após esse erro ocorrer. Para obter o último erro (método GetLastError), em C# existe um método estático da classe Marshal ( Marshal.GetLastWin32Error ()) que usaremos para extrair o erro após cada chamada dos métodos de leitura ou de registro no arquivo. Por conveniência, importamos métodos que lêem e registram apenas strings, pois qualquer tipo de dados pode ser convertido numa cadeia de caracteres.
O próximo aspecto interessante ao trabalhar com essas funções é a maneira de excluir dados do arquivo. Por exemplo, para excluir a seção inteira, é necessário passar null, como nome da chave, para o método WriteParam. Baseando-me nesta possibilidade, criei o método correspondente, após retirados anteriormente todos os nomes de seção na enumeração ENUM_SectionType:
/// <summary> /// Remoção de seção /// </summary> /// <param name="section">seção selecionada para exclusão</param> public void DeleteSection(ENUM_SectionType section) { WriteParam(section.ToString(), null, null); }
Também há uma maneira de excluir uma chave específica, para isso, é preciso especificar o nome da chave, mas seu valor deve ser null. Na implementação deste método, deixei o nome da chave transmitida como um campo de string, porque as chaves de cada seção são na maioria exclusivas.
/// <summary> /// Remoção de chave /// </summary> /// <param name="section">seção da qual será removida a chave</param> /// <param name="key">Chave removível</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); }
Para facilitar o acesso às seções, decidi implementá-las por meio de propriedades, para que, na instância instanciada da classe Config, fosse possível acessar qualquer seção e, logo, qualquer chave desta seção através do Operador dot (.), conforme mostrado no exemplo abaixo:
Config myConfig = new Config("Path"); myConfig.Tester.Expert = MyExpert; string MyExpert = myConfig.Tester.Expert;
Obviamente, para começar com essa empresa, precisamos criar uma classe para cada uma das seções e, na classe de cada seção específica, escrever as propriedades que escrevem e lêem esta string específica no arquivo. Como as seções são essencialmente um componente deste arquivo de inicialização específico, e a classe Config é essencialmente uma representação desse arquivo orientada a objetos, faz sentido criar classes que descrevam essas seções como classes aninhadas na classe Config e, em seguida, na classe Config criar propriedades disponíveis somente para leitura, tipificadas por classes concretas. No exemplo abaixo, recortei todo o excesso de código, deixando apenas a parte que ilustra a ideia descrita nesta parte do texto:
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; // caminho para o arquivo 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; } }
A implementação de cada uma das classes aninhadas que descrevem uma seção específica é do mesmo tipo, vamos considerá-la com o exemplo da classe 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); } }
Como se pode ver, a classe que descreve a seção contém uma seção Nullable, que usa outra classe intermediária para ler e escrever no arquivo. Consideraremos a implementação desta classe posteriormente, mas agora vamos nos concentrar nos dados retornados, ou seja, se essa chave não for gravada no arquivo, nossa classe-wrapper retornará null em vez do valor da chave. Se passarmos null para a propriedade de qualquer chave, o valor em questão será simplesmente ignorado. Se quisermos excluir um campo, será necessário usar o método DeleteKey, discutido acima.
Agora consideremos a própria classe Converter que grava e lê dados de um arquivo, pois ela também é uma classe aninhada e, portanto, pode usar os métodos WriteParam e GetParam da classe principal, apesar de estarem marcados com o modificador de acesso protected. Esta classe possui sobrecargas de métodos de leitura e de gravação para os seguintes tipos:
- Bool
- Int
- Double
- String
- DateTime
Fazemso com que todos os outros tipos fiquem como um dos tipos de dados mais adequados nas classes que descrevem as seções. A implementação desta classe tem o seguinte formato:
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")); } }
Esta classe converte os valores transferidos no formato esperado no arquivo e os grava nele. Ao ler um arquivo, ele também converte as strings no formato de dados retornado e passa o resultado para uma classe que descreve uma seção específica, que por sua vez converte o valor no formato que esperamos. Vale ressaltar que, com cada chamada para uma das propriedades, esta classe escreve ou lê dados diretamente num arquivo; esse comportamento sempre fornece as informações mais recentes ao trabalhar com um arquivo, mas pode ser mais duradouro do que quando se trabalha com memória. Mas, se levarmos em conta que na escrita e na leitura não é gasto muto tempo, isso não é sentido ao trabalhar com o programa.
Na seguinte parte, vamos considerar o gerente do terminal. A tarefa desta classe é iniciar e parar o terminal, obter informações sobre se está em execução e também definir os sinalizadores de início do arquivo de configuração. Em outras palavras, essa classe deve entender todas as maneiras de iniciar o terminal descrito nas instruções e dar a capacidade de controlar o processo de operação do terminal. Para todos esses requisitos, primeiro foi escrita essa interface que descreve as assinaturas das propriedades e dos métodos desejados. Em seguida, no programa, através da interface abaixo será realizado o trabalho com o terminal.
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; }
Como pode ser visto na interface apresentada, as 4 primeiras propriedades assumem o valor dos sinalizadores apresentados nas instruções e discutidos anteriormente na parte em que foi descrita a criação da interface gráfica. O quinto sinalizador define o tamanho da janela do terminal ao ser iniciado. Ele pode minimizar o terminal, executar a janela no modo de tela cheia ou minimizada. No entanto, ao definir seu valor no parâmetro Hidden (que deve quebrar a janela), não é executado o comportamento esperado. Para ocultar a janela do terminal, é necessário editar outro arquivo de inicialização, mas como esse comportamento não é crítico, não compliquei ainda mais o código e programei o funcionamento com outro arquivo de inicialização.
A própria classe, que herda esta interface, possui duas sobrecargas de construtor que são apresentadas abaixo.
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; }
Conforme descrito num artigo de Vladímir Karputov, o diretório variável do terminal contém o arquivo origin.txt que armazena o caminho para o diretório de instalação que usamos na primeira sobrecarga do construtor. Esta sobrecarga procura arquivo origin.txt, lê-o todo e cria uma classe DirectiryInfo que descreve esse diretório, enquanto passa as informações lidas do arquivo para seu construtor. Também é importante notar que, de fato, toda a preparação da classe é realizada pelo segundo construtor, que utiliza três parâmetros:
- O caminho para o diretório a ser alterado (o do AppData).
- O caminho para o diretório de instalação.
- O sinalizador de inicialização do terminal no modo Portable.
O último parâmetro neste construtor foi adicionado para facilitar a configuração, e sua atribuição teve que ser feita no final do construtor intencionalmente. Acontece que, quando o terminal é iniciado no modo Portable, seu diretório MQL5 — que armazena todos os EAs e indicadores — é criado (se não tiver sido criado anteriormente) no diretório de instalação do terminal. Inicialmente, se o terminal nunca for executado no modo Portabletil, este diretório estará ausente, portanto, quando esse sinalizador é definido, é necessário verificar se diretório existe. A propriedade que define e lê este sinalizador é descrita a seguir.
/// <summary> /// Sinalizador para iniciar o terminal no modo /portable /// </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(100); Close(); } WaitForStop(); WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal; } } }
Como pode ser visto no Setter apresentado, ao atribuir o valor transferido, desde que seja true, é verificado se o diretório MQL5 existe. Se tal diretório não existir no diretório de instalação, iniciaremos o terminal e manteremos o fluxo até que o terminal seja iniciado. Ao iniciar o terminal, como primeiro definimos o sinalizador de inicialização do terminal, a inicialização em si será realizada no modo Portable. Após iniciado o terminal no modo Portable, será gerado o diretório desejado. Depois que o terminal é iniciado, fechamos o terminal com o comando Close do nosso wrapper para trabalhar com o terminal e aguardamos seu encerramento. Após este procedimento, se não houver problemas com os direitos de acesso, será criado o diretório MQL5 desejado. A nossa propriedade que retorna o caminho para o diretório MQL5 do terminal funciona através de uma construção condicional, retornando o caminho para o diretório desejado, a partir do diretório de instalação ou do diretório com arquivos mutáveis, dependendo do sinalizador descrito acima.
/// <summary> /// Caminho para a pasta MQL5 /// </summary> public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5");
Também é necessário prestar atenção ao uso da segunda sobrecarga de construtor. Se repentinamente você passa, em vez do diretório editável, o caminho para o diretório de instalação,
esta classe em princípio deverá funcionar corretamente caso acontecer uma inicialização no modo Portable pelo menos (ou caso seja definido o sinalizador isPortable = true). No entanto, ele verá apenas o diretório de instalação do terminal e nesse caso o TerminalID será igual ao nome da pasta na qual o terminal está instalado, em vez de ser um conjunto de números e símbolos latinos indicado no diretório de alteração do terminal.
As propriedades que fornecem informações sobre robôs, indicadores e scripts que estão no terminal são a próxima nuance da implementação desta classe. A implementação destas propriedades é feita pelo método private GetEX5FilesR.
#region .ex5 files relative paths /// <summary> /// Lista contendo os nomes completos dos EAs /// </summary> public List<string> Experts => GetEX5FilesR(MQL5Directory.GetDirectory("Experts")); /// <summary> /// Lista contendo os nomes completos dos indicadores /// </summary> public List<string> Indicators => GetEX5FilesR(MQL5Directory.GetDirectory("Indicators")); /// <summary> /// Lista contendo os nomes completos dos scripts /// </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; }
Antes de examinar este método em detalhe, é necessário focar no fato de que nestas propriedades não estamos tentando obter caminhos para arquivos de EAs, em vez disso, encontramos caminhos relativos a EAs em relação à pasta Experts, a indicadores - em relação a Indicators, a scripts - em relação a Scripts. Além disso, durante a seleção, nossa classe é guiada apenas pela extensão do arquivo (ela pesquisa apenas arquivos com a extensão EX5 nos diretórios transferidos).
O método que retorna uma lista de arquivos EX5 encontrados usa recursão. Vamos nos debruçar em mais detalhes. Primeiro ele verifica o valor do seu segundo parâmetro, que é opcional: se não estiver definido, a ele será atribuído o nome do diretório transferido atual. É assim que entendemos em relação a qual diretório é necessário emitir caminhos de arquivo. A seguir, há mais construção da linguagem C#, isto é, funções aninhadas. Estas funções existem apenas dentro do método atual. Recorremos ao uso desta construção, já que esta função não é mais necessária em nenhum lugar da classe e seu corpo não é muito grande, o que permite que ela se encaixe dentro do método considerado. Esta função pega o caminho para o arquivo EX5 como uma entrada e o separa usando o símbolo "\\", como resultado, obtemos um array de nomes de diretório e, no final deste array, o nome do arquivo EX5. O seguinte passo é atribuir à variável i o índice do diretório em relação ao qual procuramos o caminho para o arquivo, e aumentamos seu valor em 1, movendo o ponteiro para o próximo diretório ou o arquivo. A variável ans armazenará em si mesma o endereço encontrado, para isso, atribuímos o valor do diretório atribuído a ela e, em seguida, adicionamos no loop um novo diretório ou arquivo, e assim por diante, até sairmos do ciclo (ou seja, até adicionarmos o nome do arquivo desejado). O próprio método GetEX5FilesR funciona de acordo com o seguinte esquema:
- Obtemos os caminhos para todos os subdiretórios.
- Procuramos arquivos EX5 no diretório atual e salvamos seus caminhos relativos.
- Num loop, iniciamos uma recursão para cada subdiretório, enquanto passamos o nome do diretório (em relação ao qual desejamos obter o caminho para o EA) que retorna o valor. Adicionamos à lista de caminhos relativos aos arquivos EX5
- Retornamos os caminhos de arquivo encontrados.
Assim, este método executa uma pesquisa completa de arquivos e retorna todos os EA e arquivos executáveis encontrados escritos em MQL5.
Agora examinemos como executar aplicativos de terceiros em C#. Esta linguagem possui uma funcionalidade muito conveniente para inicialização e para trabalho com outros aplicativos, nomeadamente a classe Process, que é um wrapper para qualquer processo externo iniciado. Por exemplo, para iniciar o bloco de notas em C#, é preciso escrever apenas 3 linhas de código:
System.Diagnostics.Process Process = new System.Diagnostics.Process(); Process.StartInfo.FileName = "Notepad.exe"; Process.Start();
Com ajuda desta classe, implementamos o processo de gerenciamento de terminais de terceiros a partir do nosso complemento. O método que inicia o terminal fica assim:
public bool Run() { if (IsActive) return false; // Definimos o caminho para o terminal Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe"); Process.StartInfo.WindowStyle = WindowStyle; // Definimos os dados para iniciar o terminal (es houver alguns instalados) 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"; // Notificamos que o processo deve chamar o evento Exit após o encerramento do terminal Process.EnableRaisingEvents = true; //Iniciamos o processo e salvamos o status de inicialização na variável IsActive return (IsActive = Process.Start()); }
Após configurar o terminal antes de iniciá-lo, devemos primeiro fazer como com o bloco de notas:
- Especificar o caminho para o arquivo executável a ser iniciado.
- Definir o tipo de janela do processo de inicialização.
- Definir as chaves (no exemplo do aplicativo de console, elas eram todos os valores especificados com um espaço após o nome do arquivo executável).
- Definir o sinalizador Process.EnableRaisingEvents=true. Se isso não for feito, o evento de término de processo, que assinamos no construtor, não será acionado.
- Iniciar o processo, armazenando o status de inicialização na variável IsActive.
A propriedade IsActive novamente se torna false no retorno de chamada que é acionado após o fechamento do terminal. Nesse retorno de chamada também chamamos nosso evento TerminalClosed.
/// <summary> /// Evento de enceramento do terminal /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Process_Exited(object sender, EventArgs e) { IsActive = false; TerminalClosed?.Invoke(this); }
Os outros métodos que controlam o terminal (aguardando a parada e fechamento do terminal) são um wrapper dos métodos padrão da classe Process.
public void WaitForStop() { if (IsActive) Process.WaitForExit(); } /// <summary> /// Suspensão do processo /// </summary> public void Close() { if (IsActive && !Process.HasExited) Process.Kill(); } /// <summary> /// Aguardar que o terminal conclua seu funcionamento um certo tempo /// </summary> public bool WaitForStop(int miliseconds) { if (IsActive) return Process.WaitForExit(miliseconds); return true; }
Assim, usando a classe Process padrão, criamos um wrapper conveniente que funciona especificamente com o terminal MetaTrader 5 e, posteriormente, em todo o programa, podemos trabalhar com o terminal com mais conforto do que se usássemos a classe Process diretamente.
Objetos que exibem a estrutura de diretórios
Como na última parte deste artigo já abordamos o trabalho com diretórios, vale a pena considerar as maneiras de trabalhar com o sistema de arquivos usado neste complemento. A primeira coisa que gostaria de mencionar é a maneira pela qual são criados os caminhos para arquivos e para diretórios. Para fazer isso, existe a classe Path, que é muito conveniente. Graças a essa classe, podemos criar com segurança caminhos para arquivos e para diretórios, evitando possíveis erros nesse sentido. Para visualização do diretório, é usada a classe DirectoryInfo, ela permite que nós consigamos ter rapidamente informações sobre os subdiretórios, o diretório pai, o nome e o caminho completo para o diretório em questão, além de muitas propriedades úteis. Por exemplo, esta classe permite que nós tenhamos todos os arquivos neste diretório chamando apenas um método. Para a exibição orientada a objetos de qualquer um dos arquivos, é usada a classe Fileinfo, que pela sua funcionalidade é um análogo da classe DirectoryInfo. Como resultado, todo o trabalho com arquivos e diretórios se resume basicamente ao trabalho com as classes apresentadas, para que durante o processo de desenvolvimento nós possamos nos concentrar na tarefa em si, quase sem criar funções e métodos intermediários.
Também é importante observar que, na última classe TerminalManager descrita, o método GetDirectory era frequentemente usado numa instância da classe DirectoryInfo. Este método não faz parte da composição padrão da classe DirectoryInfo e foi adicionado por conveniência. No C#, existe uma maneira de estender a funcionalidade de classes padrão e nativas adicionando métodos de extensão a elas. Usamos esta funcionalidade da linguagem C# para adicionar o método de extensão GetDirectory, agora consideremos sua implementação.
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; } }
Como se pode ver, para criar um método de extensão, é necessário criar uma classe estática em que é criado um método estático público no qual, por sua vez, o primeiro parâmetro deve ser tipificado pelo tipo para o qual é criada a extensão e antes dela deve ser indicada a palavra-chave this. Este parâmetro é especificado automaticamente quando é chamado o método de extensão (ele não deve ser passado para a função explicitamente, uma vez que ele é a instância específica da classe para a qual foi escrita a extensão na qual foi chamado o método de extensão). Não é necessário criar uma instância de uma classe que armazene métodos de extensão, pois todos os métodos de extensão são incluídos automaticamente no conjunto de métodos da classe para a qual foram escritos. O método específico em consideração opera de acordo com o seguinte algoritmo:
- Se o parâmetro createIfNotExists=false (ou não especificado), ele retornará uma subpasta com o nome fornecido convertido no tipo DirectoryInfo (se ela existir), ou null.
- Se o parâmetro createIfNotExists=true, desde que a pasta não seja criada, ela será criada como resposta - essa pasta será retornada, convertida novamente no tipo DirectoryInfo.
Além disso, para facilitar a manipulação de pastas dos diretórios de terminal, foi criada uma classe, que é uma representação orientada a objetos do diretório.
~\AppData\Roaming\MetaQuotes\Terminal
Esta classe é implementada da seguinte maneira.
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")); }
Como se pode ver, ela contém três campos:
- Terminals
- Common
- Community
Estes campos correspondem aos nomes dos subdiretórios no diretório em questão. De maior interesse é a propriedade Terminais, que retorna uma lista de diretórios pertencentes ao sistema modificado de arquivos do terminal. Como, após excluir o terminal, eu encontrei repetidamente que sua pasta neste diretório ainda permanecia intocada, decidi verificar a relevância destes diretórios. Os critérios de verificação foram:
- A presença do arquivo "origin.txt" na raiz do diretório em questão, pois graças a este arquivo, obtemos o caminho para o diretório do terminal.
- A presença do arquivo executável do terminal no diretório correspondente.
Também é importante mencionar que este complemento foi projetado para funcionar com a versão de 64 bits do terminal. Para trabalhar com a versão de 32 bits, em todo o programa é necessário mudar o nome da classe (da classe TerminalManager e da classe em questão) "terminal64.exe" para "terminal.exe". Assim, são ignorados os diretórios para os quais os arquivos executáveis do terminal não podem ser encontrados.
O próximo recurso que se deveria considerar desta classe é o primeiro construtor. Este construtor permite gerar automaticamente o caminho para o diretório dos arquivos modificados do terminal desta maneira:
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData)
Como se pode ver, a classe Enviroment permite obter o caminho para o diretório "AppData" no computador em questão automaticamente. Por esse motivo, não precisamos digitar um nome de usuário, e é graças a essa linha que, nos nossos computadores, este complemento pode encontrar a lista de todos os terminais instalados de maneira padrão.
Além da classe que descreve a pasta com os diretórios de terminal modificáveis, nosso complemento possui seu próprio diretório no qual armazena arquivos temporários e relatórios de otimização. A classe que descreve este diretório é apresentada da seguinte maneira.
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; } }
Como se pode ver no construtor desta classe, ao ela ser criada, estamos verificamos a existência dos diretórios raiz e dos aninhados. Se eles não existirem, serão criados.
- "DirectoryRoot" é o diretório principal dentro do qual nosso complemento armazena seus próprios arquivos e diretórios.
- "Configs" é o diretório para o qual copiaremos os arquivos de configuração, modificá-los e depois defini-los como um parâmetro de entrada ao iniciar o terminal.
- "Reports" é o diretório no qual haverá uma estrutura de arquivos e de pastas com relatórios e configurações de otimização carregados após cada um dos testes.
Olhando para o futuro, vale dizer que a estrutura interna do diretório Reports é criada na classe "OptimisationManager" e é formada para cada otimização após ser concluída. Ela consiste nos seguintes itens:
- Diretório cujo nome é o ID do terminal.
- Diretório, segundo o nome do robô, que contém o seguinte:
- Settings.xml — arquivo com configurações de otimização (gerado dentro do programa)
- History.xml — arquivo de otimização histórica copiado (gerado pelo terminal)
- Forward.xml — arquivo de otimização para frente copiado (gerado pelo terminal)
Assim, criamos duas classes que são os pontos de partida para trabalhar com o sistema de arquivos, e já no código o trabalho com o sistema de arquivos é realizado usando classes de linguagem C# padrão, o que ajuda a evitar erros nos caminhos dos arquivos e acelera significativamente o desenvolvimento.
Objetos que funcionam com arquivos de relatórios e de configurações de robô (OptimisatorSettingsManager, ReportReader, SetFileManager)
No capítulo atual, consideraremos o trabalho com arquivos. No processo, nosso complemento deve funcionar com os seguintes arquivos:
- Arquivo de configurações do robô
- Arquivo de relatório de negociação
- Arquivo de configurações de otimização salvo juntamente com relatórios no diretório "Reports" do nosso complemento.
Iniciamos nossa discussão com o arquivo contendo os parâmetros de robô para otimização. Os arquivos de parâmetros do robô têm a extensão (*.set), no entanto, existem vários arquivos de configurações (quando ele é iniciado no gráfico e quando é iniciado o testador). Estamos interessados no segundo formato, e seus arquivos são armazenados no diretório variável do terminal segundo o caminho:
~\MQL5\Profiles\Tester
É importante notar que, às vezes, ao instalar o terminal, este diretório está ausente, portanto, antes de iniciar, é necessário verificar se existe e criá-lo manualmente, se necessário. Se este diretório não estiver presente, o terminal não poderá salvar as configurações de otimização. É exatamente por isso que às vezes surge um problema quando, após uma instalação limpa do terminal, após iniciar cada nova otimização de teste, depois de ir para a guia com configurações de otimização, elas são redefinidas para o padrão . A estrutura dos arquivos descritos é um pouco semelhante à dos arquivos .ini e tem a seguinte aparência:
Variable_name=Value||Start||Step||Stop||(Y/N)
Em outras palavras, nesses arquivos, a chave é o nome do parâmetro do robô e o valor da chave pode assumir sua lista de valores, cujos nomes no exemplo fornecido são idênticos às colunas no testador de estratégias. O último valor da variável pode assumir um de dois valores (Y/N) e indica a ativação/desativação da otimização deste parâmetro do robô. Uma exceção a esta regra é a maneira de escrever parâmetros de string, pois eles têm o formato como no arquivo .ini:
Variable_name=Value
Como os arquivos de inicialização, os arquivos SET têm comentários. A linha de comentário sempre começa com um ";" (ponto e vírgula). O exemplo mais simples de tal arquivo pode ser o seguinte:
; 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
Para trabalhar com estes arquivos, precisamos criar uma classe-wrapper que nos permita ler os arquivos em questão, bem como uma classe que armazene os valores de cada linha lida. Esta classe já foi considerada durante a descrição da parte View deste artigo, portanto não a consideraremos aqui. Consideremos a classe principal que lê arquivos e grava parâmetros especificados na interface gráfica — SetFileManager. A implementação desta classe é a seguinte:
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); } } } } }
Primeiro, é necessário notar a verificação de formato de arquivo definido no construtor desta classe. Se o formato do arquivo for diferente do formato do arquivo SET, esta classe gerará um erro, pois estamos tentando trabalhar com um arquivo que o terminal provavelmente não entenderá. O arquivo em si é armazenado numa propriedade pública somente de leitura — FileInfo. O arquivo é lido diretamente no método Updateparams, que na construção using lê o arquivo da primeira linha até a última, ignorando as linhas de comentário. Também é preciso observar como os parâmetros do arquivo legível são definidos. Primeiro, a linha de leitura é dividida em duas, o sinal de igual ("=") é usado como separador — separando assim o nome de variável dos seus valores. O próximo passo é separar os valores das variáveis num array de 4 elementos [Value, Start, Step, Stop, IsOptimise]. No caso das linhas, esta matriz não será dividida nestes elementos, pois não serão encontrados dois caracteres duplos ("||") que separam caracteres. Para evitar erros nas linhas, não é recomendável usar esse caractere nelas. Se para cada novo elemento de nossa linha não houver dados suficientes no array, será atribuído o valor null, caso contrário, será usado o valor do array.
Os valores são salvos no métodoSaveParams. É recomendável notar o formato de gravação de dados num arquivo, o que é realizado por esta linha de código:
file.WriteLine($"{item.Variable}={item.Value}||{item.Start}||{item.Step}||{item.Stop}||{(item.IsOptimize ? "Y" : "N")}");
Aqui se pode observar que, sejam essas cadeias de caracteres ou outros tipos de dados, registramo-las todas num formato de gravação de dados que não são de cadeia. O próprio terminal entende a sequência, seja ela ou não, portanto, foi escolhido um único tipo de registro de dados. Entre as desvantagens desta classe está a incapacidade de descobrir o tipo de dados. Não podemos ter certeza do formato, pois a estrutura do arquivo não fornece essas informações. O próprio terminal recebe estas informações diretamente do EA, mas na documentação não está descrito como isso é feito.
Os parâmetros do arquivo lido são acessados e definidos através da propriedadeParams. Como, de fato, todo o trabalho com os dados do arquivo é realizado através da propriedade descrita, por conveniência no getter, verificamos se o arquivo foi lido anteriormente ou não. Se o arquivo não foi lido, é chamado o método já considerado por nós Updateparams. Em geral, o procedimento para trabalhar com essa classe é assumido da seguinte forma:
- Instanciamos, obtendo assim a representação POO do arquivo
- Lemos chamando o método Params (ou UpdateParams, se necessário, por exemplo, se de repente o arquivo foi alterado de fora)
- Definimos valores próprios através do Setter ou simplesmente mudamos, trabalhando com o array recebido através do Getter.
- Salvamos as alterações através do métodoSaveParams
Como se pode ver, a principal desvantagem em comparação aos arquivos INI é o fato de que entre a leitura e a gravação de dados, eles estão na memória do programa, no entanto, se excluirmos este arquivo acidental ou intencionalmente, o arquivo poderá ser alterado de fora — essa suposição é bastante realista, além disso, a maioria dos programas de arquivos opera de acordo com um esquema semelhante. Também no arsenal desta classe existe um método Dublicatefile cuja tarefa é copiar o arquivo de acordo com o caminho transferido (a cópia é realizada com substituição, se houver um arquivo com o mesmo nome anteriormente no caminho proposto).
A seguir, a classe RepirtReader, que lê os relatórios de otimização gerados pelo terminal, os analisa para que as informações possam ser exibidas numa tabela. O arquivo com o histórico de otimização é apresentado em formato XML para MS Excel. Seu nó raiz (a primeira tag) é <Workbook/> e descreve o livro. Próximo nó <DocumentProperties/> descreve os parâmetros nos quais foi realizada a otimização. Este nó contém informações úteis, como:
- O título, gerado a partir do nome do robô, o nome do ativo, período gráfico e período de otimização.
- Data de criação
- Nome do servidor no qual foi executada a otimização
- Depósito e moeda de depósito
- Alavancagem
O próximo nó <Styles/> não é útil para nós (é criado principalmente para o Excel), ele é seguido por um nó <Worksheet /> que descreve uma planilha contendo o descarregamento de corridas de otimização. Neste nó, existe um nó <Table/> que armazena as informações que procuramos — uma lista de resultados de otimização dividida em colunas, como após iterados os parâmetros no testador. Observ que a primeira linha da tabela contém os cabeçalhos da tabela seguidos pelos valores. Cada nó <Row/> contém uma lista dos valores desta tabela listados na tag <Cell/> . Também cada tag <Cell/> contém o atributo Type que indica o tipo de valor nesta célula. Como este arquivo, mesmo num formato truncado, é bastante complicado, não darei um exemplo, mas você pode estudá-lo otimizando um robô e abrindo seus resultados de otimização na pasta Reports do nosso complemento. Agora consideremos a classe descrita. Começamos esta discussão com uma visão geral das propriedades que descrevem o arquivo de otimização.
#region DocumentProperties and column names /// <summary> /// Nome das colunas do documento /// </summary> protected List<string> columns = new List<string>(); /// <summary> /// Acesso externo à coleção de colunas do documento. É retornada uma cópia da coleção ///para evitar a modificação da coleção inicial /// </summary> public List<string> ColumnNames => new List<string>(columns); /// <summary> /// Cabeçalho do documento /// </summary> public string Title { get; protected set; } /// <summary> /// Autor do documento /// </summary> public string Author { get; protected set; } /// <summary> /// Data de criação do documento /// </summary> public DateTime Created { get; protected set; } /// <summary> /// Servidor no qual foi realizada a otimização /// </summary> public string Server { get; protected set; } /// <summary> /// Primeiro depósito /// </summary> public Deposit InitialDeposit { get; protected set; } /// <summary> /// Alavancagem /// </summary> public int Leverage { get; protected set; } #endregion
Como você pode ver, essas propriedades são obtidas de seus nós <DocumentProperties/>. As propriedades em questão são preenchidos no seguinte método:
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); } }
Como se pode ver, usar C# para trabalhar com arquivos (*.xml) é quase tão fácil quanto trabalhar com matrizes. O objeto document é uma instância da classe XmlDocument que armazena o arquivo de leitura e fornece um trabalho amigável. Precisaremos do campo enumerador, ao qual é atribuído um valor, no método Read, que lê o documento linha por linha, mais tarde voltaremos a abordá-lo. Agora, eis algumas palavras sobre a interface IDisposable usada ao declarar uma classe:
class ReportReader : IDisposable
Esta interface contém apenas um método Dispose() e é necessária para podermos usar essa classe no constructo using. O constructo using, que já encontramos ao gravar num arquivo na última classe considerada, garante a operação correta, o que significa que, a cada leitura do arquivo, não precisamos fechar o arquivo por conta própria, em vez disso, o encerramento acontece no método Dispise(), chamado automaticamente após sair do bloco de colchetes no qual trabalhamos com o arquivo. Neste caso específico, usaremos o método Dispise para limpar o campo document para não armazenar muitas informações no arquivo lido quando não precisarmos mais dele. A implementação deste método é a seguinte:
public void Dispose() { document.RemoveAll(); }
Agora olhemos para a interface IEnumerator mencionada acima, que novamente é uma interface da linguagem padrão C#. É assim:
// // 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(); }
Como se pode ver, ele consiste em dois métodos e uma propriedade. Esta interface serve como um tipo de wrapper para a coleção iterada por um valor de cada vez. O método MoveNext serve como um método que move o cursor um valor para frente até que a coleção termine. Se tentarmos chamar este método quando já percorrermos toda a coleção, ele retornará false, o que significará o fim da iteração. O método Reset é necessário para reiniciar a pesquisa, ou seja, move o cursor para o índice zero da coleção. A propriedade Current contém o item de coleção selecionado para o índice obtido usando o deslocamento via MoveNext. Esta interface é amplamente usada em C#, os ciclos foreach são baseados nela , no entanto, precisamos implementar o método Read.
/// <summary> /// Comando para ler uma linha de uma tabela com otimizações /// </summary> /// <param name="row"> /// Linha lida key - título de coluna; value - valor de célula</param> /// <returns> /// true - se a linha for lida /// false - se a linha não for lida /// </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; }
Como se pode ver, as tarefas para os métodos Read e MoveNext() são muito semelhantes, no entanto, Read também retorna seu resultado de trabalho através do parâmetro passado para ele. Como ele deve retornar apenas linhas com valores, quando definimos o valor da variável enumerator, chamamos o método MoveNext uma vez, deslocando o cursor da posição zero (cabeçalhos da tabela) para o índice 1 (primeira linha com valores). Ao ler dados, também usamos o método ConvertToType, que converte os valores de leitura do formato de string no formato especificado pelo atributo Type. É por isso que na lista retornada, o tipo de valor é especificado como o tipo object, assim, podemos converter qualquer um dos tipos no tipo de retorno. Implementação em si do método ConvertToType é apresentada abaixo.
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; }
Dos aspectos da implementação do método em questão, vale destacar a conversão de string em formato numérico. Como configurações diferentes são fornecidas para o tempo (de acordo com os princípios de exibição nos diferentes países) e, portanto, para o separador decimal, existem várias opções, assim sendo, depois de testar nos formatos de dados em russo e inglês, cheguei à necessidade de indicar explicitamente o separador decimal usado.
Para reiniciar o leitor, foi criado o método ResetReader, que é um wrapper para o método IEnumerator.Reset e é implementado da seguinte maneira:
public void ResetReader() { if (enumerator != null) { enumerator.Reset(); // Zeramos enumerator.MoveNext(); // Ignoramos os cabeçalhos } }
Assim, ao usar um wrapper conveniente para analisar os arquivos XML fornecidos pela linguagem C#, conseguimos escrever uma classe de wrapper para analisar arquivos de relatório sem nenhuma dificuldade, lendo e recebendo informações adicionais.
Neste capítulo, resta considerar outra classe que, diferentemente das duas discutidas acima, trabalha com os arquivos de configurações do otimizador gerados pelo complemento em questão, e não diretamente pelo terminal. Como uma das funcionalidades intituladas é a inicialização do robô no testador com um clique duplo no parâmetro de otimização selecionado, surge a pergunta: onde obter as configurações para o testador, isto é, intervalo de datas, nome do ativo e outras configurações? Afinal, o relatório do otimizador armazena apenas parte desses dados, mas não todos. Obviamente, para resolver esse problema, é preciso salvar estas configurações num arquivo. Como formato para armazenamento de dados, a marcação XML foi escolhida como a mais conveniente. Na classe considerada acima, já foi exemplificada a leitura de arquivos XML, nela além da leitura, também gravaremos num arquivo. Primeiro, lidaremos com as informações que serão salvas no arquivo de configurações.
O primeiro dos objetos salvos é a estrutura na qual são armazenados os dados das configurações do otimizador (apresentados na guia Settings, na área inferior da guia principal Settings). Esta estrutura é implementada da seguinte maneira.
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]); } }
Inicialmente, esta estrutura foi criada como um contêiner para a transferência de dados do View para o Model e, portanto, contém, além dos próprios dados, índices para ComboBox. Para facilitar o trabalho com essa estrutura no modelo, bem como em outras classes, foram escritos métodos de conversão de valores de enumerações (enum), que são armazenadas na estrutura pelo número do índice, para os tipos de enumerações solicitadas. Esta conversão funciona da seguinte maneira: no Modelo de Dados, para exibir os valores dos dados da lista no ComboBox, eles são armazenados em uma sequência legível; o método Getenum<T> é usado para a conversão reversa. Este método é genérico, e é análogo aos modelos do C++. Neste método, para obter o Enum desejado, primeiro precisamos descobrir o valor específico do tipo passado, para fazer isso, usamos a classe Type que armazena o valor do tipo. E, em seguida, primeiro decompomos desse tipo de enumeração numa lista de strings e, em seguida, usamos a transformação inversa de uma string para enum, para obter o valor de uma enumeração específica, não na forma de sequência, mas na forma da enumeração desejada.
O próximo objeto que contém os dados armazenados é ConfigCreator_inputData. Esta estrutura contém dados da tabela com o terminal selecionado e é usada apenas na classe OptimizationationManager considerada abaixo para criar um arquivo de configuração, daí seu nome. Esta estrutura é assim:
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; }
O último terço dos dados armazenados é a lista de parâmetros do robô digitados pela lista com o ParamItem <ParamsItem> ) Agora que já foram considerados os dados que é necessário salvar, é preciso olhar para o arquivo compilado durante o trabalho da classe em questão:
<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>
O arquivo apresentado foi criado durante o trabalho com o vídeo de exemplo para um dos robôs. Como se pode ver em sua estrutura, o nó do arquivo é <Settings/>, e dentro dele existem mais três nós: <OptimisationInputData/>, <ConfigCreator_inputData/>, <SetFileParams/>. Os tipos de dados em cada nó correspondem aos seus nomes. Nos nós que armazenam informações sobre as configurações do testador, o elemento final é a tag Item, que contém o atributo Name, através do qual definimos o nome do parâmetro salvo. Para a lista de parâmetros do robô existe a tag <Variable/>, que também possui o atributo Name, que armazena o nome do parâmetro, enquanto o valor dos parâmetros de otimização correspondente é armazenado nas tags aninhadas. Para criar esse arquivo, a classe OptimisatorSettingsManager é novamente herdada da interface IDisposable e, no método Dispose, os valores especificados serão salvos num arquivo. Os getters das propriedades correspondentes são usados para ler dados do arquivo.
#region OptimisationInputData /// <summary> /// Estrutura OptimisationInputData inserida para armazenamento /// </summary> private OptimisationInputData? _optimisationInputData = null; /// <summary> /// Obter e salvar a estrutura OptimisationInputData /// </summary> 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
Neste exemplo em particular, o getter obtém a estrutura OptimisationInputData, cujos valores são retirados do arquivo acima. Como se pode ver, o método GetItem é usado em qualquer lugar do getter para obter dados do arquivo. Este método usa 2 parâmetros:
- Tipo de nó do qual são obtidos os dados.
- O nome do parâmetro especificado no atributo de Name.
A implementação deste método é a seguinte:
/// <summary> /// Obter item do arquivo de configurações /// </summary> /// <param name="NodeName">Tipo de estrutura</param> /// <param name="Name">Nome do campo</param> /// <returns> /// Valor do campo /// </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; }
Este método de obtenção de dados é notável pelo uso da linguagemXpath, que é algo semelhante ao SQL, mas apenas para o formato XML. Para obter dados (do nó em que estamos interessados) com base no valor do atributo especificado, especificamos o caminho completo para este nó e, em seguida, no nó final item, indicamos a condição de que o atributo Name deve ser igual ao nome passado. Assim, ocorre a leitura do arquivo para todas as estruturas, já para a lista de parâmetros é usado um método diferente, pois a estrutura desse nó é mais complexa.
#region SetFileParams /// <summary> /// Lista de parâmetros para armazenamento /// </summary> private List<ParamsItem> _setFileParams = new List<ParamsItem>(); /// <summary> /// Obtemos e definimos os parâmetros (.set) do arquivo para armazenamento /// </summary> 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
Neste caso, passamos por todos os nós <Variable/> , de cada um, obtemos o valor do atributo Name e preenchemos a classe ParamItem com os dados contidos nesse nó ParamsItem específico.
O método Dispose() final, no qual concordamos em salvar os dados num arquivo, é representado pela seguinte implementação:
public virtual void Dispose() { // Método aninhado que ajuda a escrever elementos de estrutura 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(); } // primeiro limpamos a classe que preserva o esquema xml do arquivo de configurações if (document != null) document.RemoveAll(); // em seguida verificamos se os resultados podem ser salvos 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(); } }
No início deste método, são criadas duas funções aninhadas. A funçãoWriteItem é projetado para criar um bloco de código repetitivo para registrar elementos estruturais. A função WriteElement se destina para registrar os valores dos parâmetros de otimização, como Start, Step, Stop, IsOptimize. Como precisamos das três tags no arquivo de configurações, instalamos uma unidade de verificação cuja tarefa é impedir que o arquivo seja gravado se nem todos os parâmetros necessários tiverem sido passados. Além disso, no constructo using, cujo significado já consideramos anteriormente, acontece o registro de dados no arquivo. Graças às funções incorporadas, essa parte do método em questão, responsável pela gravação de dados num arquivo, foi reduzida em mais de três vezes.
Teste de objetos chave
Concluindo, gostaria de dizer algumas palavras sobre o teste do aplicativo. Como o complemento criado será desenvolvido e modificado, decidi escrever testes para os principais objetos, para que no futuro (se for necessário alterá-los), seja possível verificar rapidamente o desempenho deles. Atualmente, os testes cobrem parcialmente as seguintes classes:
- Config
- ReportReader
- OptimisationSettingsManager
- SetFileManager
- TerminalManager
No próximo artigo, planejamos alterar as classes que serão descritas nos próximos capítulos, e essas alterações afetarão a lógica e o resultado de alguns dos métodos, portanto, essas classes não são cobertas pelos testes Unit. Os testes para essas classes serão implementados no próximo artigo. Apesar de os testes serem escritos como testes Unit, na verdade todos eles são atualmente de integração, pois interagem com objetos externos (terminal/sistema de arquivos e outros). Os seguintes objetos planejados estão projetados para serem testados sem dependências dos objetos descritos acima, ou seja, como testes Unit. Para isso, na frente de cada um dos objetos descritos acima, existem fábricas para sua criação. Um exemplo é a fábrica para criar a classe 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
Seu código é simples, na verdade, envolvemos a criação de um objeto do tipo ReportReader na classe MainReportReaderCreator, que herda da classe ReportReaderFabric. Essa abordagem fornece a capacidade de transferir para objetos principais (discutido nos capítulos abaixo) um objeto tipificado como ReportReaderFabric, e a implementação de uma determinada fábrica já pode ser diferente. Assim, em testes Unit para objetos-chave podemos substituir classes que funcionam com arquivos e com o terminal, além de reduzir a dependência de classes entre si. Essa abordagem para formar de objetos é chamada de método de fábrica.
Vamos considerar a implementação de testes futuros em mais detalhes no próximo artigo. Um exemplo do uso do método de fábrica para criar objetos será considerado nos capítulos a seguir e, no capítulo atual, consideraremos para mais especificidade um teste escrito para uma classe que trabalha com arquivos de configuração. Para começar, todos os testes do projeto atual devem ser realizados num projeto separado — "Unit Test Project"
Vamos chamá-lo de "OptimisationManagerExtentionTests", pois os testes serão escritos para o projeto "OptimisationManagerExtention", o seguinte passo será adição de links para o projeto "OptimisationManagerExtention", ou seja, à nossa DLL com GUI e lógica. Como testaremos objetos que não estão marcados com um modificador de acesso público, há duas opções para disponibilizá-los em nosso projeto de teste:
- Torná-los públicos (o que está errado, porque os usamos apenas dentro do projeto)
- Adicionar a possibilidade de ver classes internas num projeto específico (o que é mais preferível em comparação ao primeiro método)
Para resolver este problema, usei o segundo método e, quanto à classe que testa apenas gráficos, adicionei o seguinte atributo ao código do projeto principal:
[assembly: InternalsVisibleTo("OptimisationManagerExtentionTests")]
O próximo passo é escrever os testes para as classes que escolhemos. Como o projeto de teste é apenas auxiliar, não consideraremos cada classe de teste, em vez disso, darei exemplificarei uma classe e tentarei descrever as principais nuances de como trabalhar com testes usando-a. Por conveniência, aqui está a classe completa que testa a classe Config. Como se pode ver, a primeira condição para essa classe se tornar de teste é a necessidade de ter um atributo [TestClass], também a classe testada deve ser pública e seus métodos de teste devem ter um atributo [TestMethod], pois é neles que será realizado o processo de teste. O método marcado com o atributo [TestInitialize] é iniciado sempre antes do início do teste seguinte, também existe um atributo semelhante [ClassInitialize], que não é usado neste teste, mas, sim, em outros, e ao contrário do método marcado com o atributo [TestInitialize], é executado apenas uma vez antes de iniciar o primeiro teste. Cada um dos métodos de teste, no final, contém a chamada de um dos métodos da classe Assert , que comparam o valor do teste com o desejado, portanto, o teste é confirmado ou refutado.
[TestClass] public class ConfigTests { private string ConfigName = $"{Environment.CurrentDirectory}\\MyTestConfig.ini"; private string first_excention = "first getters call mast be null becouse file doesn`t contains 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, $"Adress mast 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 mast be equal to true"); // set null config.Common.ProxyEnable = null; b = config.Common.ProxyEnable; Assert.AreEqual(expected.Value, b.Value, "ProxyEnables mast 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 mast be equal to {expected.Value}"); // set null config.Common.ProxyEnable = null; p = config.Common.ProxyType; Assert.AreEqual(expected.Value, p.Value, $"ProxyType mast 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 mast be equal to {expected.Value}"); // set null config.Common.ProxyEnable = null; p = config.Tester.FromDate; Assert.AreEqual(expected.Value.Date, p.Value.Date, $"ProxyType mast 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 mast 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 mast 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 mast be deleted"); } [TestMethod] public void DeleteSectionTest() { config.Common.Login = 12345; config.DeleteSection(ENUM_SectionType.Common); Assert.AreEqual(null, config.Common.Login, "Key mast be deleted"); } }
Se considerarmos esta classe de teste específica, ela não cobre todos os métodos necessários, em vez disso, testa a classe Config.Converter, que essencialmente executa toda a lógica de trabalho com o arquivo de configuração, no entanto, como é uma classe privada, precisamos escrever testes nas propriedades que usam essa classe. Por exemplo, o teste DoubleConverter_GetSetTest () verifica se a string é convertida em Double corretamente através da propriedade config.Tester.Deposit. Se você considerar este teste específico em mais detalhes, poderá ver que ele consiste em 3 partes:
- Consulta de um parâmetro do tipo double num campo que não criado, deve retornar null.
- Registro de valor aleatório num arquivo e sua leitura
- Registro de null que deve ser ignorado
Se um erro for detectado em qualquer etapa, será fácil corrigi-lo, para que os testes sejam bastante úteis no desenvolvimento de aplicativos. Após criar todos os testes, podemos executá-los diretamente do VisualStudio, no caminho Test => Run => AllTests
Eles também podem ser úteis para os leitores cujos padrões regionais de computador são diferentes dos meus — após executar estes testes, você pode detectar possíveis erros (por exemplo, o separador decimal de um número decimal) e corrigi-los você mesmo.
Gerente de otimização
Como mencionado anteriormente, ao escrever este aplicativo, um dos critérios era a extensibilidade. Considerando que o processo de otimização será alterado no próximo artigo e a interface principal do complemento não exigirá alterações significativas, decidi remover o processo de otimização da classe de modelo como uma classe abstrata, cuja implementação pode depender do método de otimização solicitado. Esta classe é escrita de acordo com o modelo de fábrica abstrata, vamos considerá-lo com a classe da própria fábrica:
/// <summary> /// Fábrica para criar classes que controlam o processo de otimização /// </summary> abstract class OptimisationManagerFabric { /// <summary> /// Construtor /// </summary> /// <param name="ManagerName">Nome do gerente de otimização criado</param> public OptimisationManagerFabric(string ManagerName) { this.ManagerName = ManagerName; } /// <summary> /// Nome refletindo o tipo de gerente de otimização criado (qual sua característica) /// </summary> public string ManagerName { get; } /// <summary> /// Método que cria o gerente de otimização /// </summary> /// <returns>Gerente de otimização</returns> public abstract OptimisationManager Create(Dictionary<string, BotParamKeeper> botParamsKeeper, List<ViewModel.TerminalAndBotItem> selectedTerminals); }
Como pode ser visto na classe da fábrica abstrata, ela contém o nome da classe, da qual precisaremos nos seguintes artigos, bem como o método que cria o gerente de otimização. Supõe-se que o gerenciador de otimização seja criado antes de cada otimização e, em seguida, todo o trabalho com o terminal seja delegado a ele, portanto, no método que gera o objeto passamos parâmetros como um dicionário com uma lista de robôs e uma lista de terminais (ou seja, parâmetros que variam de otimização a otimização). Todos os outros parâmetros necessários estão planejados para serem transferidos para a classe de uma fábrica específica do construtor. Agora abordemos a classe OptimisationManager. Esta classe foi projetada para gerenciar a otimização, mas, além da otimização, também é responsável pela execução de testes. Como a inicialização dos testes ocorre quase sempre de acordo com o mesmo algoritmo, esta funcionalidade é implementada diretamente na classe abstrata em consideração, e vamos nos debruçar sobre sua implementação, mais tarde. Quanto ao início e ao fim das otimizações, essa funcionalidade é transformada em dois métodos abstratos que requerem implementação na classe herdeira. O construtor desta classe aceita o volume excedente de fábricas, podendo assim operar com todos os objetos considerados acima.
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)
Para notificar a classe do modelo sobre conclusão do processo de otimização, ela contém o evento AllOptimisationsFinished. Para descobrir exatamente quais terminais e robôs estão contidos neste gerenciador de otimização da classe model, foi criada a propriedade a seguir.
/// <summary> /// Dicionário onde: /// key - ID do terminal /// value - caminho completo para o robô /// </summary> 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; } }
Esta propriedade é implementada numa classe abstrata, no entanto, pode ser reescrita, pois é marcada com a palavra-chave virtual. Para notificar classe de modelo se o processo de otimização/teste está em execução, foi criada uma propriedade cujos valores são definidos a partir dos métodos que iniciam o processo de otimização/teste.
public bool IsOptimisationOrTestInProcess { get; private set; } = false;
Por conveniência, um método longo que será essencialmente inalterado na maioria dos casos em classes com otimização e para a execução do teste também é implementado diretamente numa classe abstrata. Trata-se de um método para gerar um arquivo de configuração, que consideraremos em mais detalhes.
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")); // Preenchemos o arquivo de configuração 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 adress 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; config.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; com 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; }
Primeiro, nós, usando a classe que descreve o diretório do terminal a ser alterado, bem como a fábrica para criar objetos do tipo Config, criamos um objeto de arquivo de configuração e o copiamos para o diretório apropriado do nosso complemento, atribuindo como nome o ID do terminal ao qual o arquivo de configuração original pertencia. Em seguida, é preenchida a seção [Tester] do arquivo de configuração copiado. Todos os dados para preencher esta seção são obtidos diretamente das estruturas transmitidas que são geradas no código (no caso de otimização) ou no arquivo (no caso de iniciar o teste). Se o servidor for transferido incorretamente, a mensagem correspondente será exibida na forma de uma MessageBox e será retornado o valor nulk em vez do arquivo de configuração. Com o mesmo objetivo — para criar código repetido — numa classe abstrata foi implementado um método que cria um gerente de terminal. Vamos considerá-lo:
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; }
Dentre suas características de implementação, vale ressaltar o fato de que, se o ID do terminal necessário corresponder ao ID do terminal a partir do qual é iniciado nosso complemento, o terminal estará configurado para executar no modo Portable, no entanto, o aplicativo em si só funcionará corretamente com a inicialização do terminal no modo padrão. Assim, como veremos na próxima classe que descreve o modelo, há um filtro aqui que ignora o terminal atual e não o coloca na lista dos disponíveis.
O método que inicia o teste no terminal selecionado após o evento de clique duplo também é colocado numa classe abstrata e implementado da seguinte maneira:
/// <summary> /// Método de inicialização do teste após o evento de duplo clique /// </summary> /// <param name="TerminalID">ID do terminal selecionado</param> /// <param name="pathToBot">Caminho para o robô em relação à pasta de EAs</param> /// <param name="row">Linha da tabela de otimização</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); }
Em seus parâmetros de entrada, ele aceita os mesmos dados que na classe que descreve o modelo são obtidos a partir de um arquivo com as configurações salvas. Dentro do método, a barra de progresso e o status da operação também são definidos por meio do delegado passado. O arquivo de configuração gerado é ajustado para executar o testador: as chaves que descrevem o relatório do otimizador (pois são desnecessárias) são excluídas e o desligamento automático do terminal é desativado quando o testador termina seu trabalho. Após a inicialização do terminal, o thread que o inicia fica suspenso aguardando que o trabalho seja concluído, desta forma, o formulário é notificado da conclusão do teste. No futuro, para que o próprio formulário não seja interrompido quando a otimização/teste for iniciado, esses processos serão iniciados no contexto do thread secundário. Quanto à otimização, como já mencionado anteriormente, seu processo é transferido para o método abstrato protegido, no entanto, há também um método público implementado na classe abstrata que é necessário para que a classe funcione corretamente e não possa ser reescrita.
/// <summary> /// Inicialização do processo de otimização / teste de todos os terminais planejados /// </summary> /// <param name="BotParamsKeeper">Lista de terminais, robôs e parâmetros de robôs</param> /// <param name="PBUpdate">Delegado que edita o valor da faixa de carregamento e de status</param> /// <param name="sturtup_status">Resposta da função - usado se não for possível iniciar a otimização / teste, /// nesta linha é escrita a razão do início mal-sucedido</param> /// <returns>true - se for bem-sucedido</returns> public void StartOptimisation() { pbUpdate(0, "Start Optimisation", true); IsOptimisationOrTestInProcess = true; DoOptimisation(); OnAllOptimisationsFinished(); IsOptimisationOrTestInProcess = false; pbUpdate(0, null, true); } protected abstract void DoOptimisation(); /// <summary> /// Método que suspende a otimização /// </summary> public abstract void BreakOptimisation();
Basicamente, este método regula a ordem em que é acionado o processo de otimização em relação ao progresso da barra, aos sinalizadores do início e do fim da otimização e também ao chamando o evento de conclusão das corridas de otimização.
O último método implementado na classe abstrata é o de mover o relatório para o diretório de trabalho do nosso complemento. Como o arquivo de configurações de otimização deve ser criado juntamente com a movimentação do relatório, todas essas ações foram incluídas num só método.
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; } }
Primeiro, neste método, obtemos os caminhos para os arquivo de relatório. Depois, aguardamos o ciclo de criação de um dos arquivos desejados (um, já que os dois arquivos não necessariamente serão gerados, por exemplo, é iniciada apenas a otimização do histórico sem um período 'forward'). Em seguida, formamos o caminho para o diretório em que serão armazenados os arquivos de relatório. De fato, este trecho de código contém o layout das subpastas do diretório Reports. Depois, ocorre a criação de caminhos para arquivos futuros e a remoção de arquivos antigos, se houver. Logo, acontece a cópia relatórios para o diretório do nosso complemento. Para finalizar, nós criamos um arquivo (*.xml) com as configurações nas quais foi realizado o processo de otimização. Como este processo deve ser realizado em estágios e é improvável que seja alterado, nós o movemos para uma classe abstrata e, para iniciá-lo, agora bastará chamar o método em questão da classe herdeira.
Já considerada a classe abstrata, abordemos o processo de otimização implementado. Atualmente, trata-e de uma inicialização habitual do terminal com os parâmetros de otimização selecionados, como no testador padrão. Os seus aspectos mais interessantes de implementação são o processo de inicialização e o manipulador do evento de conclusão da otimização, que serão considerados em etapas, começando com a inicialização do processo
private readonly List<ITerminalManager> terminals = new List<ITerminalManager>(); /// <summary> //Método que interrompe o processo de otimização e que encerra o terminal forçosamente /// </summary> 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(); } }
Como se pode ver, a lista de gerentes de terminal é colocada no campo para acessá-la por diferentes métodos, graças a isso, podemos implementar o método BreakOptimisations. No método de inicialização do processo de otimização, após a criação do terminal, assinamos seu evento de encerramento, graças a isso, podemos cuidar da conclusão do processo de otimização. Após iniciar a otimização num loop, mantemos o thread até que todos os terminais em execução sejam encerrados. O método UnsubscribeTerminals é necessário para cancelar a assinatura de todos os eventos assinados anteriormente quando reiniciada a otimização. Este método também é chamado no destruidor da classe em questão. O manipulador de eventos de parada de otimização é implementado da seguinte maneira:
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; }
Como se pode ver, sua principal tarefa é mover os arquivos contendo o relatório de otimização para o diretório correspondente. Assim, são implementadas as lógicas de inicialização para a otimização e para o testador. Uma das operações que executaremos no próximo artigo será a implementação de métodos adicionais de otimização de acordo com a amostra já descrita. Examinamos quase todo o aplicativo criado, agora resta considerar a principal classe resultante que descreve o modelo ao qual nos referimos no ViewModel.
A classe de modelo resultante (IExtentionGUI_M e sua implementação)
Como mencionado anteriormente, esta parte do projeto implementa a interface IExtentionGUI_M e é o ponto de partida para realizar a lógica do formulário descrito. A parte gráfica e o ViewModel, como mencionado anteriormente, se referem a esta classe para receber dados e delegar a execução de vários comandos. Consideremos esta parte começando com a descrição de sua interface, que é implementada da seguinte maneira.
/// <summary> /// Interface do modelo /// </summary> 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 /// <summary> /// Enum que caracteriza o tipo de tabela com resultados de otimização /// </summary> enum ENUM_TableType { History, Forvard }
É com esta interface que funciona o VewModel, no entanto, se necessário, sua implementação (que consideraremos posteriormente) pode ser substituída por outra. Além disso, sem alterar a parte gráfica do programa, também é possível alterar os gráficos do programa e não afetar sua lógica. Como esta interface é herdada da interface INotifyPropertyChanged, conseguimos notificar o ViewModel (e, consequentemente, o View de nosso aplicativo) sobre uma alteração numa das propriedades implementadas em nosso modelo de dados. Para facilitar a escrita de código no modelo, adicionei um wrapper universal de classe Varkeeper, que, além de armazenar um valor de qualquer tipo, também sabe como converter implicitamente no tipo armazenado e, quando um valor armazenado é alterado, notifica o ViewModel sobre a alteração. A implementação desta classe é a seguinte:
/// <summary> /// Classe que armazena a variável _Var do tipo T_keeper. /// Podemos implicitamente converter no tipo T_keeper e também alterar o valor da variável armazenada /// Na hora de mudar o valor, notifica todos os signatários sobre isso /// </summary> /// <typeparam name="T_keeper">Tipo de variável armazenada</typeparam> class VarKeeper<T_keeper> { /// <summary> /// Construtor que especifica o nome da identificação da variável /// </summary> /// <param name="propertyName">nome de identificação a variável</param> public VarKeeper(string propertyName) { this.propertyName = propertyName; } /// <summary> /// Construtor que define o nome de identificação da variável /// e valor inicial da variável /// </summary> /// <param name="PropertyName">nome de identificação da variável</param> /// <param name="Var">valor inicial da variável</param> public VarKeeper(string PropertyName, T_keeper Var) : this(PropertyName) { _Var = Var; } /// <summary> /// Sobrecarga de operador de coerção de tipo implícito. /// Converte este tipo em T_keeper /// </summary> /// <param name="obj"></param> public static implicit operator T_keeper(VarKeeper<T_keeper> obj) { return obj._Var; } /// <summary> /// variável armazenada /// </summary> protected T_keeper _Var; /// <summary> /// Nome da identificação da variável /// </summary> public readonly string propertyName; #region Event /// <summary> /// Evento que notifica sobre a mudança da variável armazenada /// </summary> public event Action<string> PropertyChanged; /// <summary> /// Método que aciona um evento que notifica sobre uma mudança na variável armazenada /// </summary> protected void OnPropertyChanged() { PropertyChanged?.Invoke(propertyName); } #endregion /// <summary> /// Método que define o valor de uma variável com o valor value /// </summary> /// <param name="value">novo valor da variável</param> public void SetVar(T_keeper value) { SetVarSilently(value); OnPropertyChanged(); } public void SetVarSilently(T_keeper value) { _Var = value; } }
No construtor desta classe, acontece a transferência do valor inicial da variável armazenada , bem como do nome da variável que será usado quando notificada uma alteração em seu valor. A variável em si é armazenada no campo protegido desta classe. O nome da variável usada para notificar de uma alteração em seu valor é armazenado no campo público somente leitura ropertyName. Os métodos para definir o valor de uma variável são divididos em método que define seus valores e causa um evento notificando todos os signatários da alteração feita e em método que define apenas o valor da variável. Para habilitar a conversão implícita dessa classe no tipo de valor armazenado nela, é usada a sobrecarga do operador de conversão de tipos. Graças a esta classe, conseguimos armazenar os valores das variáveis, lê-las sem usar conversões de tipo explícitas e também notificar o ambiente de uma alteração no valor da variável. No construtor da classe que implementa a interface IExtentionGUI_M, definimos os valores das propriedades para o tipo tipificado que examinamos e também assinamos o evento de notificação quando essas propriedades são atualizadas. No destruidor dessa classe, pelo contrário, cancelamos a assinatura dos eventos destas propriedades.
public ExtentionGUI_M(TerminalCreator TerminalCreator, ConfigCreator ConfigCreator, ReportReaderCreator ReportReaderCreator, SetFileManagerCreator SetFileManagerCreator, OptimisationExtentionWorkingDirectory CurrentWorkingDirectory, OptimisatorSettingsManagerCreator SettingsManagerCreator, TerminalDirectory terminalDirectory) { // Atribuímos o diretório de trabalho atual this.CurrentWorkingDirectory = CurrentWorkingDirectory; this.terminalDirectory = terminalDirectory; //Criamos fábricas this.TerminalCreator = TerminalCreator; this.ReportReaderCreator = ReportReaderCreator; this.ConfigCreator = ConfigCreator; this.SetFileManagerCreator = SetFileManagerCreator; this.SettingsManagerCreator = SettingsManagerCreator; CreateOptimisationManagerFabrics(); // assinamos o evento de atualização da coleção de colunas na tabela com otimizações históricas HistoryOptimisationResults.Columns.CollectionChanged += Columns_CollectionChanged; // Atribuímos o status inicial Status = new VarKeeper<string>("Status", "Wait for the operation"); Status.PropertyChanged += OnPropertyChanged; // Atribuímos os valores iniciais para a barra de progresso PB_Value = new VarKeeper<double>("PB_Value", 0); PB_Value.PropertyChanged += OnPropertyChanged; // Criamos uma variável que armazena o índice do terminal selecionado a partir da lista de terminais disponíveis em que houve otimização TerminalsAfterOptimisation_Selected = new VarKeeper<int>("TerminalsAfterOptimisation_Selected", 0); TerminalsAfterOptimisation_Selected.PropertyChanged += OnPropertyChanged; // Criamos uma variável que armazena o índice do robô selecionado na lista de robôs disponíveis em que houve otimização BotsAfterOptimisation_Selected = new VarKeeper<int>("BotsAfterOptimisation_Selected", -1); BotsAfterOptimisation_Selected.PropertyChanged += OnPropertyChanged; _isTerminalsEnabled = new VarKeeper<bool>("IsTerminalsLVEnabled", true); _isTerminalsEnabled.PropertyChanged += OnPropertyChanged; // Carregamos informações sobre os terminais instalados no computador FillInTerminalsID(); FillInTerminalsAfterOptimisation(); LoadOptimisations(); }
Observe que, no construtor, chamamos vários métodos:
- CreateOptimisationManagerFabrics: fornece fábricas que criam gerentes de otimização, que são inseridos numa matriz e, a partir dela, selecionaremos o gerente de otimização necessário sob certas condições.
- FillInTerminalsID: preenche a lista de IDs de terminal que vemos no menu suspenso ao escolher um terminal para otimização. Todos os terminais encontrados são registrados, exceto o atual em que é iniciado nosso complemento.
- FillInTerminalsAfterOptimisation: preenche a lista de terminais nos quais foi realizada uma das otimizações e há dados para carregar na tabela de otimização.
- LoadOptimiations: a tabela é preenchida com otimizações de acordo com o terminal e o robô selecionados (ambos os parâmetros têm um índice zero no momento).
Assim sendo, realizamos a principal tarefa do designer: preparamos o programa para iniciar, preenchendo todas as tabelas e variáveis com os valores iniciais. Se seguirmos as etapas de trabalho com a interface gráfica de nosso artigo, o próximo passo será trabalhar com tabelas dos terminais selecionados para otimização. Todos os terminais selecionados são armazenados no dicionário num dos campos da classe.
/// <summary> /// Apresentação da tabela de terminais selecionados a partir da aba inicial deste complemento /// key - Termonal ID /// value - bot params /// </summary> private readonly Dictionary<string, BotParamKeeper> BotParamsKeeper = new Dictionary<string, BotParamKeeper>(); /// <summary> /// Terminal atualmente selecionado /// </summary> private string selectedTerminalID = null; /// <summary> /// Lista de parâmetros do robô para edição /// </summary> List<ParamsItem> IExtentionGUI_M.BotParams { get { return (BotParamsKeeper.Count > 0 && selectedTerminalID != null) ? BotParamsKeeper[selectedTerminalID].BotParams.Params : new List<ParamsItem>(); } }
A propriedade BotParams no seu getter recebe uma lista de parâmetros de robô deste dicionário e, ao alterar o robô selecionado (o mecanismo será descrito posteriormente), simplesmente passamos para a nova chave deste dicionário. A adição e modificação do conteúdo deste dicionário é controlada pelo método LoadBotParam, chamado imediatamente após clicar no botão Adicionar novo terminal selecionado na lista suspensa na primeira guia do nosso complemento. Este método é implementado da seguinte maneira:
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); }
Como pode ser visto no código, além de bloquear a interface do usuário no momento da otimização e teste (como foi visto no vídeo), o código também valida se podemos atualizar a lista de parâmetros do robô (e, possivelmente, terminais) ou não. Se pudermos atualizar os parâmetros do robô ou do terminal, bloqueamos a interface gráfica. Em seguida, vem a adição de um novo robô ou o salvamento dos parâmetros inseridos anteriormente a partir da interface gráfica. Em seguida, é salvo o ID do terminal selecionado (chave no nosso dicionário) eos parâmetros do robô recém-selecionado são passados de volta ao ViewModel. Se alteramos o robô selecionado em comparação com o selecionado anteriormente, carregamos parâmetros para ele através do método Getsetfile. O método que adiciona um novo terminal é bastante simples e repete quase completamente o último constructo condicional do método em questão. O principal trabalho nele é realizado pelo método GetSetFile.
private SetFileManager GetSetFile(string fullExpertName, string TerminalID) { DirectoryInfo terminalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID); // Criamos o gerente que trabalhará com o terminal ITerminalManager terminalManager = TerminalCreator.Create(terminalChangableFolder); // Criamos o caminho para a pasta Tester (que está localizada no diretório ~/MQL5/Profiles) // Se não ela existir (às vezes o MetaTrader não consegue criá-la durante a primeira instalação), vamos criá-la nós mesmos // Ela armazena arquivos com configurações de otimização DirectoryInfo pathToMqlTesterFolder = terminalManager.MQL5Directory.GetDirectory("Profiles").GetDirectory("Tester", true); if (pathToMqlTesterFolder == null) throw new Exception("Can`t find (or create) ~/MQL5/Profiles/Tester directory"); // Criamos um arquivo de configuração e o copiamos imediatamente para a pasta Configs do diretório de trabalho atual deste complemento Config config = ConfigCreator.Create(Path.Combine(terminalChangableFolder.GetDirectory("config").FullName, "common.ini")) .DublicateFile(Path.Combine(CurrentWorkingDirectory.Configs.FullName, $"{TerminalID}.ini")); // Configuramos o terminal para que ele inicie o teste do robô selecionado e se desligue imediatamente // Isso é necessário para o terminal criar um arquivo .set com as configurações deste EA. // Para que seja desativado imediatamente, indicamos a data final do teste um dia abaixo da data de início. 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); // Definimos o arquivo de configuração do gerente de terminal, executamo-lo e aguardamos até que o terminal seja encerrado // Para que o terminal seja encerrado automaticamente após o teste ser concluído, // atribuímos o valor true ao campo 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 by hands all Metatrader terminals that 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); }
Este método é bastante bem comentado por si só, mas vejamos seu principal objetivo. O método recebe os parâmetros do robô selecionado e, consequentemente, seu arquivo SET. Este arquivo é criado pelo terminal quando o robô é iniciado no testador, portanto, a única maneira de criá-lo é executar o algoritmo selecionado no testador. FPara que isso seja perceptível, o terminal com o testador ligado é iniciado no modo minimizado na bandeja e, para que o testador termine rapidamente seu trabalho e desligue, definimos a data final do teste um dia antes da sua data de início. Se o terminal que se planeja abrir já estiver em execução, nós tentamos executá-lo num loop dando o aviso correspondente. No final do trabalho, retornamos uma representação orientada a objeto do arquivo SET.
O próximo ponto interessante nesta classe é o processo de ativação de otimizações realizado pelo método assíncrono 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); // Executamos a otimização e esperamos que ela termine _isTerminalsEnabled.SetVar(false); await System.Threading.Tasks.Task.Run(() => selectedOptimisationManager.StartOptimisation()); _isTerminalsEnabled.SetVar(true); } private void SetOptimisationManager(List<ViewModel.TerminalAndBotItem> SelectedTerminals) { // Selecionamos uma fábrica para criar um gerente de otimização da lista OptimisationManagerFabric OMFabric = optimisationManagerFabrics[0]; // Cancelamos a assinatura do gerente de otimização usado anteriormente if (selectedOptimisationManager != null) { // Verificamos se a otimização está sendo executada no momento if (selectedOptimisationManager.IsOptimisationOrTestInProcess) return; selectedOptimisationManager.AllOptimisationsFinished -= SelectedOptimisationManager_AllOptimisationsFinished; } // Criamos um gerente de otimização e assinamos o evento de conclusão da otimização selectedOptimisationManager = OMFabric.Create(BotParamsKeeper, SelectedTerminals); selectedOptimisationManager.AllOptimisationsFinished += SelectedOptimisationManager_AllOptimisationsFinished; }
Sua implementação é interessante, pois mostra como usamos o gerente de otimização. Antes de cada início de otimização, sempre vamos recriá-lo. Nesta implementação, sua criação ocorre apenas somente para o primeiro gerente daqueles inseridos na matriz correspondente, no próximo artigo esse processo será complicado. A inicialização de teste é um pouco semelhante à de otimizações, no entanto, são alterados os parâmetros do robô para aqueles que são selecionados clicando duas vezes.
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); }
Este método também é assíncrono e também ocorre em seu processo também acontece a criação de um gerente de otimização, mas apenas se não tiver sido criado com antecedência. Para obter os parâmetros de entrada para executar o teste, nós recorremos ao arquivo de configurações, localizado ao lado do relatório de otimização para o robô selecionado. Após criar o arquivo contendo as configurações do robô, procuramos entre seus parâmetros aqueles indicados no relatório de otimização e definimos o valor Value como o valor da linha de otimização selecionada. Depois de salvar as configurações, vamos executar o teste.
Para carregar os resultados da otimização na tabela correspondente, é usado o método a seguir, que contém um método aninhado.
public void LoadOptimisations() { // Método interno que preenche com dados a tabela passada para ele void SetData(bool isForvard, DataTable tb) { // Limpamos a tabela de dados previamente preenchidos tb.Clear(); tb.Columns.Clear(); // Obtemos os dados 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; // Preenchemos as colunas foreach (var item in reader.ColumnNames) { tb.Columns.Add(item); } // Preenchemos as linhas 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; } // Preenchemos a otimização histórica e, em seguida, a otimização forward, uma de cada vez SetData(false, HistoryOptimisationResults); SetData(true, ForvardOptimisationResults); }
Este método apenasliga duas vezes a função aninhada na qual ocorre todo o trabalho. Na função anexada, acontece o seguinte:
- limpeza da tabela transferida (e de suas colunas)
- definição do caminho para o arquivo de relatório baixado
- leitura do relatório usando a classe ReportReader e carregando seus dados na tabela
Observe que o construtor contém a seguinte linha de código:
// assinamos o evento de atualização da coleção de colunas na tabela com otimizações históricas
HistoryOptimisationResults.Columns.CollectionChanged += Columns_CollectionChanged;
O que faz com que o métodoColumns_CollectionChanged assine o evento de atualização das colunas da tabela com otimizações históricas. Este evento ajuda a rastrear a adição de colunas. No método assinado, cuja implementação pode ser vista no código (é muito grande e fácil de implementar), são adicionados ou removidos automaticamente os nomes de colunas na coleçãoOptimizationResultsColumnHeadders, que é a fonte de dados para o ViewModel e o View, de onde, através da extensão descrita acima para o carregamento automático de colunas, são adicionados ao ListView. Assim, ao editar a lista de colunas na tabela de otimizações históricas, as colunas no View são editadas automaticamente nas duas tabelas.
Neste capítulo, examinamos os detalhes de implementação da lógica para iniciar otimizações, de carregamento de programas e de carregamento de arquivos contendo corridas de otimização históricas e 'forward', bem como o mecanismo para iniciar testes após o evento de clique duplo. De fato, o aplicativo, que foi mostrado no início do artigo em vídeo, está pronto, resta apenas implementar sua inicialização a partir do terminal. Para fazer isso, precisamos escrever um wrapper na forma de um EA que é o seguinte.
//+------------------------------------------------------------------+ //| 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" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ 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(); } //+------------------------------------------------------------------+
Após compilar nosso projeto em C# (modo Release), precisamos colocá-lo no diretório apropriado (~/Libraries) e incluir no nosso robô. Para obter o ID do terminal atual, precisamos obter o caminho para seu diretório mutável e, usando o método StringSplit, dividi-lo em seus caminhos componentes. O último diretório conterá o ID do terminal que procuramos. Após iniciado o gráfico, é definido o atraso atual do thread até a janela ser carregada. Depois, iniciamos o temporizador. Precisamos de um temporizador para rastrear o evento de fechamento de janela, ao fechar a janela, teremos que remover o EA do gráfico. De uma maneira tão simples, conseguimos o comportamento descrito no exemplo de vídeo.
Conclusão e arquivos anexados
No início do trabalho que realizamos, o objetivo era criar um complemento com boa extensibilidade para gerenciar processos de otimização com uma interface gráfica. Durante a implementação, escolhemos a linguagem C#, pois ela possui a interface mais conveniente para escrever aplicativos gráficos e muitos recursos adicionais, às vezes mágicos, que simplificam bastante o processo de programação. No artigo, examinamos todo o processo de criação do aplicativo em questão, começando com algo básico como a inicialização de programas através do console e terminando com a criação de um wrapper para iniciar o MetaTrader a partir de outro terminal usando tecnologias C#. O trabalho que realizamos foi bastante emocionante, e espero que o leitor se interesse por ele. Também gostaria de dizer que, na minha opinião, as classes descritas nos últimos capítulos deste artigo podem ser aprimoradas, portanto, no próximo artigo, provavelmente vou fazer uso da refatoração do código colocado nas classes em questão e tentarei estruturá-lo melhor.
O aplicativo contém um arquivo que tem duas pastas:
- MQL5: destina-se ao terminal principal MetaTrader 5, no qual será iniciado o complemento escrito, e contém o arquivo que inicia o complemento.
- Visual Studio: contém os três projetos descritos para o Visual Studio, que devem ser compilados antes de serem usados. A biblioteca (* .dll) obtida após a compilação do projeto OptimisationManagerExtention deve ser colocada no diretório Libraries do terminal onde será iniciado o projeto descrito.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/7059
![Biblioteca para desenvolvimento fácil e rápido de programas para a MetaTrader (parte XII): Implementação da classe de objeto "Conta" e da coleção de objetos da conta](https://c.mql5.com/2/36/MQL5-avatar-doeasy__7.png)
![Biblioteca para desenvolvimento fácil e rápido de programas para a MetaTrader (parte XI). Compatibilidade com a MQL4 - Eventos de encerramento de posição](https://c.mql5.com/2/36/MQL5-avatar-doeasy__6.png)
![Biblioteca para desenvolvimento fácil e rápido de programas para a MetaTrader (parte XIII): Eventos do objeto Conta](https://c.mql5.com/2/36/MQL5-avatar-doeasy__8.png)
![Gerenciando otimizações (Parte I): Criando uma interface gráfica do usuário](https://c.mql5.com/2/36/mql5-avatar-opt_control.png)
![MQL5 - Linguagem para estratégias de negociação inseridas no terminal do cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso