
Desenvolvendo um EA multimoeda (Parte 24): Conectando uma nova estratégia (I)
Introdução
No artigo anterior, demos continuidade ao desenvolvimento do sistema de otimização automática de estratégias de trading no MetaTrader 5. O núcleo é o banco de dados de otimização, que contém informações sobre projetos de otimização. Para criar esses projetos, foi escrito um script de criação de projeto. Embora o script tenha sido escrito para criar o projeto de otimização de uma estratégia de trading específica (SimpleVolumes), ele pode ser usado como modelo para outras estratégias.
Criamos a possibilidade de exportação automática dos grupos de estratégias de trading selecionados na última etapa do projeto. A exportação é feita para um banco de dados separado, chamado banco de dados do expert. Esse banco de dados pode ser utilizado pelo EA final para atualizar as configurações das estratégias de trading, sem a necessidade de recompilação. Isso permite simular, no testador, o funcionamento do EA em um intervalo de tempo no qual podem aparecer vários resultados de otimização dos projetos.
Além disso, finalmente migramos para uma estrutura organizada de arquivos do projeto, dividindo todos os arquivos em duas partes. A primeira parte, chamada Biblioteca Advisor, foi movida para a pasta MQL5/Include, e as demais permaneceram na pasta de trabalho dentro de MQL5/Experts. Na biblioteca, estão todos os arquivos que garantem o funcionamento do sistema de otimização automática, independentemente dos tipos de estratégias de trading otimizadas. Na pasta de trabalho do projeto, ficaram os EAs das etapas, o EA final e o script de criação do projeto de otimização.
No entanto, por enquanto, a estratégia de trading modelo SimpleVolumes foi deixada na biblioteca, pois naquele momento era mais importante verificar o funcionamento do mecanismo de atualização automática dos parâmetros das estratégias no EA final. A origem do arquivo com o código-fonte da estratégia de trading no momento da compilação não era algo tão relevante.
Agora, vamos imaginar que queremos implementar uma nova estratégia de trading e conectá-la ao sistema de otimização automática, criando os EAs das etapas e o EA final para ela. O que precisaremos fazer para isso?
Traçando o caminho
Primeiro, vamos escolher uma estratégia simples e implementá-la em código, já pensando em usá-la com a nossa biblioteca Advisor. Colocaremos o código na pasta de trabalho do projeto. Quando a estratégia estiver pronta, geraremos o EA da primeira etapa, que será usado para otimizar os parâmetros das instâncias individuais dessa estratégia. Nesse ponto, enfrentaremos algumas dificuldades relacionadas à necessidade de separar o código da biblioteca do código do projeto.
Os EAs da segunda e da terceira etapas podemos usar praticamente os mesmos que foram escritos na parte anterior, já que o código da parte de biblioteca não contém referências às classes das estratégias de trading utilizadas. Já no código da pasta de trabalho do projeto será necessário apenas adicionar o comando de inclusão do arquivo da nova estratégia.
Para a nova estratégia, também será necessário fazer algumas modificações no script do EA para a criação do projeto no banco de dados de otimização. No mínimo, as mudanças afetarão o modelo dos parâmetros de entrada do EA na primeira etapa, pois a composição desses parâmetros será diferente na nova estratégia de trading.
Após modificarmos o EA de criação do projeto no banco de dados de otimização, será possível executá-lo. Então, o banco de dados de otimização será criado e as tarefas de otimização necessárias para esse projeto serão adicionadas a ele. Então, será possível iniciar o pipeline de otimização automática e aguardar o término da execução. Esse é um processo relativamente longo. Sua duração dependerá do intervalo de tempo escolhido para a otimização (quanto maior o intervalo, mais demorado será o processo), da complexidade da estratégia de trading (quanto mais complexa, mais tempo levará) e, claro, da quantidade de agentes de teste disponíveis para realizar a otimização (quanto mais agentes, mais rápido o processo será).
O último passo será executar o EA final ou rodá-lo no testador de estratégias para avaliar os resultados da otimização.
Vamos começar!
A estratégia SimpleCandles
Criaremos na pasta MQL5/Experts uma nova pasta para o projeto. Vamos chamá-la, por exemplo, Article.17277. Vale a pena fazer uma ressalva desde já para evitar confusão no futuro. Usaremos o termo "projeto" em dois sentidos. Em um caso, estaremos nos referindo apenas à pasta com os arquivos dos EAs que serão utilizados para a otimização automática de uma estratégia de trading específica. No código desses EAs, serão usados arquivos incluídos da biblioteca Advisor. Nesse contexto, "projeto" se refere apenas à pasta de trabalho dentro da pasta de experts do terminal. No outro, a palavra "projeto" significará a estrutura de dados criada no banco de dados de otimização que descreve as tarefas que devem ser executadas automaticamente para gerar resultados a serem usados no EA final, destinado a operar em conta real. Nesse contexto, projeto é, essencialmente, o conteúdo do banco de dados de otimização antes mesmo do início da otimização.
Agora estamos falando de projeto no primeiro sentido. Então, na pasta de trabalho do projeto vamos criar uma subpasta chamada Strategies. Nela colocaremos os arquivos das diferentes estratégias de trading. Por enquanto, criaremos apenas uma nova estratégia.
Vamos repetir o caminho feito na Parte 1 ao desenvolver a estratégia de trading SimpleVolumes. Também começaremos com a formulação da ideia de trading.
Suponhamos que, quando um determinado símbolo apresenta várias velas consecutivas na mesma direção, a probabilidade de a próxima vela ter direção oposta aumenta um pouco. Nesse caso, se abrirmos uma posição no sentido contrário logo após essas velas, talvez consigamos obter lucro.
Tentemos transformar essa ideia em uma estratégia. Para isso, precisamos formular um conjunto de regras de abertura e fechamento de posições que não contenha parâmetros desconhecidos. Esse conjunto de regras deve permitir que, em qualquer momento da execução da estratégia, seja possível determinar se deve haver posições abertas e, em caso afirmativo, quais exatamente.
Primeiro, vamos definir claramente o conceito de direção da vela. Vamos chamar a vela de direcionada para cima quando o preço de fechamento for maior que o de abertura. A vela cujo preço de fechamento for menor que o de abertura será chamada de direcionada para baixo. Como queremos avaliar a direção de várias velas consecutivas passadas, o conceito de direção da vela será aplicado apenas às velas já fechadas. Daí concluímos que o momento possível de abertura de posição surgirá com o início de uma nova barra, ou seja, com o aparecimento de uma nova vela.
Certo, já definimos de alguma forma os momentos de abertura de posição, mas e quanto ao fechamento? Vamos usar a opção mais simples: no momento da abertura da posição serão definidos níveis de StopLoss e TakeProfit, nos quais a posição será encerrada.
Agora podemos descrever nossa estratégia da seguinte forma:
O sinal para abertura de posição será a situação em que, no início de uma nova barra (vela), todas as velas anteriores tenham se direcionado para o mesmo lado (para cima ou para baixo). Se as velas anteriores estavam direcionadas para cima, abriremos uma posição de venda. Caso contrário, abrimos uma posição de compra.
Cada posição terá níveis de StopLoss e TakeProfit e será encerrada apenas quando esses níveis forem atingidos. Se já houver uma posição aberta e surgir novamente um sinal para abertura de posição, poderão ser abertas posições adicionais, desde que a quantidade não seja excessiva.
Isso já é uma descrição mais detalhada, mas ainda não completa. Portanto, vamos reler e destacar todos os pontos em que algo não está claro. Nesses pontos será necessário dar explicações adicionais.
Eis as questões que surgiram:
- "...de várias velas anteriores..." — Várias velas — quantas exatamente?
- "...podem ser abertas posições adicionais..." — Quantas posições no total podem ser abertas?
- "...possui níveis de StopLoss e TakeProfit..." — Que valores usar para eles, como calculá-los?
Várias velas, quantas exatamente? Essa é a questão mais simples: essa quantidade será apenas um dos parâmetros da estratégia, que poderá ser ajustada para encontrar o valor mais adequado. Esse número provavelmente não será muito grande, já que, ao observar os gráficos, percebe-se que longas sequências de velas consecutivas na mesma direção são raras.
Quantas posições no total podem ser abertas? Isso também pode ser transformado em um parâmetro da estratégia e otimizado para encontrar os melhores valores.
Que valores usar para StopLoss e TakeProfit, e como calculá-los? Essa já é uma questão um pouco mais complexa, mas, no caso mais simples, também podemos resolvê-la da mesma forma que a anterior: tornando as grandezas StopLoss e TakeProfit em pontos como parâmetros da estratégia. Ao abrir a posição, recuaremos do preço de abertura o número de pontos definido nesses parâmetros, em cada direção necessária. No entanto, é possível aplicar uma abordagem um pouco mais sofisticada: definir esses parâmetros não em pontos fixos, mas em percentuais de um valor médio da volatilidade do preço do instrumento de trading (símbolo), expressa em pontos. Isso levanta a questão seguinte.
Como encontrar esse valor de volatilidade? Existem muitas maneiras de fazer isso. Podemos, por exemplo, usar o indicador de volatilidade pronto ATR (Average True Range) ou inventar e implementar nosso próprio método de cálculo da volatilidade. Provavelmente, alguns dos parâmetros nesses cálculos seriam o número de períodos em que as oscilações de preço do instrumento de negociação são analisadas e a duração de cada período. Ao adicionarmos essas grandezas como parâmetros da estratégia, podemos calcular a volatilidade com base nelas.
Como não impusemos a restrição de que as posições seguintes devam ser abertas na mesma direção da primeira, podem surgir situações em que a estratégia de trading mantenha posições em direções opostas. Em uma implementação convencional, seríamos obrigados a limitar o uso dessa estratégia apenas a contas que possuem contabilização independente de posições ("hedging"). No entanto, com o uso de posições virtuais, essa limitação deixa de existir.
Quando tudo estiver claro, listaremos todas as variáveis mencionadas como parâmetros da estratégia. Precisamos levar em conta que, para obter o sinal de abertura de posições, será necessário escolher o símbolo e o timeframe que observaremos. Assim, chegamos à seguinte descrição:
O EA é executado em um determinado símbolo e período (timeframe)
Definimos os parâmetros de entrada:
- Símbolo
- Timeframe para contagem das velas na mesma direção
- Quantidade de velas na mesma direção (signalSeqLen)
- Período ATR (periodATR)
- Stop Loss (em pontos ou % do ATR) (stopLevel)
- Take Profit (em pontos ou % do ATR) (takeLevel)
- Quantidade máxima de posições abertas simultaneamente (maxCountOfOrders)
- Tamanho das posições
Quando surge uma nova barra, verificamos as direções das últimas velas fechadas signalSeqLen.
Se as direções forem iguais e o número de posições abertas for menor que maxCountOfOrders, então:
- Calculamos o StopLoss e o TakeProfit. Se periodATR = 0, simplesmente recuamos a partir do preço atual pelo número de pontos definido nos parâmetros stopLevel e takeLevel. Se periodATR > 0, calculamos o valor do ATR usando o parâmetro periodATR no timeframe diário. A partir do preço atual, recuamos os valores ATR * stopLevel e ATR * takeLevel.
- Abrimos posição SELL se as velas estavam direcionadas para cima e posição BUY se as velas estavam direcionadas para baixo. No momento da abertura configuramos os níveis de StopLoss e TakeProfit calculados anteriormente.
Essa descrição já é suficiente para começarmos a implementação. Questões que surgirem ao longo do caminho serão resolvidas no processo.
Vale também destacar que, ao descrever a estratégia, em nenhum momento especificamos o tamanho das posições abertas. Sim, formalmente adicionamos esse parâmetro à lista, mas considerando que a estratégia será usada dentro do sistema de otimização automática, podemos simplesmente usar o lote mínimo para os testes. No processo de otimização automática serão selecionados os multiplicadores adequados do tamanho das posições, que garantirão a retração máxima de 10% em todo o intervalo de teste. Portanto, não precisaremos definir manualmente os tamanhos das posições em nenhum lugar.
Implementação da estratégia
Vamos usar a classe existente CSimpleVolumesStrategy e criar, com base nela, a classe CSimpleCandlesStrategy. Ela deve ser declarada como herdeira da classe CVirtualStrategy. Listaremos os parâmetros necessários da estratégia como campos da classe, lembrando que a nova classe também herdará alguns campos e métodos de suas classes superiores.
//+------------------------------------------------------------------+ //| Торговая стратегия c использованием однонаправленных свечей | //+------------------------------------------------------------------+ class CSimpleCandlesStrategy : public CVirtualStrategy { protected: string m_symbol; // Символ (торговый инструмент) ENUM_TIMEFRAMES m_timeframe; // Период графика (таймфрейм) //--- Параметры сигнала к открытию int m_signalSeqLen; // Количество однонаправленных свечей int m_periodATR; // Период ATR //--- Параметры позиций double m_stopLevel; // Stop Loss (в пунктах или % ATR) double m_takeLevel; // Take Profit (в пунктах или % ATR) //--- Параметры управление капиталом int m_maxCountOfOrders; // Макс. количество одновременно отрытых позиций CSymbolInfo *m_symbolInfo; // Объект для получения информации о свойствах символа // ... public: // Конструктор CSimpleCandlesStrategy(string p_params); virtual string operator~() override; // Преобразование объекта в строку virtual void Tick() override; // Обработчик события OnTick };
Para centralizar a obtenção de informações sobre as propriedades do instrumento de trading (símbolo), vamos adicionar ao conjunto de campos da classe um ponteiro para o objeto da classe CSymbolInfo.
A classe da nossa nova estratégia de trading também é descendente da classe CFactorable. Dessa forma, poderemos implementar no novo classe um construtor que leia os valores dos parâmetros a partir de uma string de inicialização, utilizando os métodos de leitura já implementados na classe CFactorable. Se durante a leitura não ocorrerem erros, o método IsValid() retornará verdadeiro.
Para trabalhar com posições virtuais, na classe-pai CVirtualStrategy foi declarado o array m_orders, destinado a armazenar ponteiros para objetos da classe CVirtualOrder, ou seja, posições virtuais. Portanto, no construtor pediremos a criação de tantos objetos de posições virtuais quanto especificado no parâmetro m_maxCountOfOrders, e esses objetos serão colocados em nosso array m_orders. Essa tarefa será realizada pelo método estático CVirtualReceiver::Get().
Como nossa estratégia abrirá posições apenas na abertura de uma nova barra no timeframe definido, criaremos um objeto para verificar o evento de chegada de uma nova barra para o respectivo símbolo e timeframe.
E, por fim, o que precisaremos fazer no construtor é solicitar ao monitor de símbolos que crie para o nosso símbolo um objeto de informações da classe CSymbolInfo.
O código completo do construtor ficará assim:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CSimpleCandlesStrategy::CSimpleCandlesStrategy(string p_params) { // Читаем параметры из строки инициализации m_params = p_params; m_symbol = ReadString(p_params); m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params); m_signalSeqLen = (int) ReadLong(p_params); m_periodATR = (int) ReadLong(p_params); m_stopLevel = ReadDouble(p_params); m_takeLevel = ReadDouble(p_params); m_maxCountOfOrders = (int) ReadLong(p_params); if(IsValid()) { // Запрашиваем нужное количество объектов для виртуальных позиций CVirtualReceiver::Get(&this, m_orders, m_maxCountOfOrders); // Добавляем отслеживание нового бара на нужном таймфрейме IsNewBar(m_symbol, m_timeframe); // Создаём информационный объект для нужного символа m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol]; } }
Em seguida, precisamos implementar o operador abstrato virtual til (~), que retorna a string de inicialização do objeto da estratégia. Sua implementação é padrão:
//+------------------------------------------------------------------+ //| Преобразование объекта в строку | //+------------------------------------------------------------------+ string CSimpleCandlesStrategy::operator~() { return StringFormat("%s(%s)", typename(this), m_params); }
Outro método virtual obrigatório a ser implementado é o de processamento de ticks Tick(). Com ele, verificamos a ocorrência de uma nova barra e também se a quantidade de posições abertas ainda não atingiu o valor máximo. Se essas condições forem atendidas, verificamos a presença de um sinal de abertura. Se houver, abriremos a posição correspondente à direção detectada. Os demais métodos que adicionaremos à classe desempenharão papéis auxiliares.
//+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void CSimpleCandlesStrategy::Tick() override { // Если наступил новый бар по заданному символу и таймфрейму if(IsNewBar(m_symbol, m_timeframe)) { // Если количество открытых позиций меньше допустимого if(m_ordersTotal < m_maxCountOfOrders) { // Получаем сигнал на открытие int signal = SignalForOpen(); if(signal == 1) { // Если сигнал на покупку, то OpenBuy(); // открываем позицию BUY } else if(signal == -1) { // Если сигнал на продажу, то OpenSell(); // открываем позицию SELL_STOP } } } }
A verificação da presença de sinal de abertura foi isolada em um método separado, SignalForOpen(). Nesse método obtemos o array de cotações das velas anteriores e verificamos em sequência se todas elas são direcionadas para baixo ou para cima:
//+------------------------------------------------------------------+ //| Сигнал для открытия отложенных ордеров | //+------------------------------------------------------------------+ int CSimpleCandlesStrategy::SignalForOpen() { // По-умолчанию сигнала на открытие нет int signal = 0; MqlRates rates[]; // Копируем значения котировок (свечей) в массив-приёмник int res = CopyRates(m_symbol, m_timeframe, 1, m_signalSeqLen, rates); // Если скопировалось нужное количество свечей if(res == m_signalSeqLen) { signal = 1; // сигнал на покупку // Перебираем все свечи for(int i = 0; i < m_signalSeqLen; i++) { // Если встречается хоть одна свеча вверх, то отменяем сигнал if(rates[i].open < rates[i].close ) { signal = 0; break; } } if(signal == 0) { signal = -1; // иначе - сигнал на продажу // Перебираем все свечи for(int i = 0; i < m_signalSeqLen; i++) { // Если встречается хоть одна свеча вниз, то отменяем сигнал if(rates[i].open > rates[i].close ) { signal = 0; break; } } } } return signal; }
Pela abertura das posições são responsáveis os métodos criados OpenBuy() e OpenSell(). Como eles são muito semelhantes, apresentaremos o código de apenas um deles. Os pontos-chave nesse método são a chamada do método de atualização dos níveis de StopLoss e TakeProfit, que atualiza os valores dos dois campos correspondentes da classe m_sl e m_tp, e também a chamada do método de abertura da primeira posição virtual ainda não utilizada do array m_orders.
//+------------------------------------------------------------------+ //| Открытие ордера BUY | //+------------------------------------------------------------------+ void CSimpleCandlesStrategy::OpenBuy() { // Берем необходимую нам информацию о символе и ценах double point = m_symbolInfo.Point(); int digits = m_symbolInfo.Digits(); // Цена открытия double price = m_symbolInfo.Ask(); // Обновим уровни SL и TP, рассчитав ATR UpdateLevels(); // Уровни StopLoss и TakeProfit double sl = NormalizeDouble(price - m_sl * point, digits); double tp = NormalizeDouble(price + m_tp * point, digits); bool res = false; for(int i = 0; i < m_maxCountOfOrders; i++) { // Перебираем все виртуальные позиции if(!m_orders[i].IsOpen()) { // Если нашли не открытую, то открываем // Открытие виртуальной позиции SELL res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot, 0, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // и выходим } } if(!res) { PrintFormat(__FUNCTION__" | ERROR opening BUY virtual order", 0); } }
O método de atualização dos níveis primeiro verifica se algum valor diferente de zero foi definido para o período de cálculo do ATR. Se sim, a função de cálculo do ATR é chamada. O resultado é armazenado na variável channelWidth. Quando o valor do período é igual a zero, a variável channelWidth recebe o valor um. Nesse caso, os valores dos parâmetros de entrada m_stopLevel e m_takeLevel são interpretados como pontos e transferidos diretamente para as variáveis m_sl e m_tp, sem alterações. Caso contrário, eles são interpretados como frações do valor do ATR e multiplicados pelo valor calculado do ATR:
//+------------------------------------------------------------------+ //| Обновление уровней SL и TP по рассчитанному ATR | //+------------------------------------------------------------------+ void CSimpleCandlesStrategy::UpdateLevels() { // Рассчитываем ATR double channelWidth = (m_periodATR > 0 ? ChannelWidth() : 1); // Обновляем уровни SL и TP m_sl = m_stopLevel * channelWidth; m_tp = m_takeLevel * channelWidth; }
O último método necessário para nossa nova estratégia de trading é o método de cálculo do ATR. Como já foi dito, ele pode ser implementado de várias formas, incluindo o uso de soluções prontas. Para simplificar, usaremos uma das implementações possíveis que tínhamos à disposição:
//+------------------------------------------------------------------+ //| Расчёт величины ATR (нестандартная реализация) | //+------------------------------------------------------------------+ double CSimpleCandlesStrategy::ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1) { int n = m_periodATR; // Количество баров для расчёта MqlRates rates[]; // Массив для котировок // Копируем котировки дневного (по умолчанию) таймфрейма int res = CopyRates(m_symbol, p_tf, 1, n, rates); // Если скопировалось нужное количество if(res == n) { double tr[]; // Массив для диапазонов цены ArrayResize(tr, n); // Изменяем его размер double s = 0; // Сумма для подсчёта среднего FOREACH(rates, { tr[i] = rates[i].high - rates[i].low; // Запоминаем размер бара }); ArraySort(tr); // Сортируем размеры // Суммируем внутренние две четверти размеров баров for(int i = n / 4; i < n * 3 / 4; i++) { s += tr[i]; } // Возвращаем средний размер в пунктах return 2 * s / n / m_symbolInfo.Point(); } return 0.0; }
Salvamos as alterações realizadas no arquivo Strategies/SimpleCandlesStrategy.mqh, localizado na pasta de trabalho do projeto.
Conexão da estratégia
Agora que a estratégia está praticamente pronta, precisamos conectá-la ao arquivo do EA. Começamos pelo EA da primeira etapa. Lembrando que seu código agora está dividido em dois arquivos:
- MQL5/Experts/Article.17277/Stage1.mq5 — arquivo do projeto atual para estudo da estratégia SimpleCandles;
- MQL5/Include/antekov/Advisor/Experts/Stage1.mqh — arquivo da biblioteca, comum a todos os projetos.
No arquivo do projeto atual, é necessário realizar as seguintes ações:
- Definir a constante __NAME__, atribuindo a ela um valor único, diferente dos nomes usados em outros projetos.
- Incluir o arquivo com a classe da estratégia de trading desenvolvida.
- Incluir a parte comum do EA da primeira etapa a partir da biblioteca Advisor.
- Listar os parâmetros de entrada da estratégia de trading.
- Criar a função com nome GetStrategyParams(), que converte os valores dos parâmetros de entrada em uma string de inicialização do objeto da estratégia.
// 1. Определяем константу с именем советника #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Подключаем нужную стратегию #include "Strategies/SimpleCandlesStrategy.mqh"; // 3. Подключаем общую часть советника первого этапа из библиотеки Advisor #include <antekov/Advisor/Experts/Stage1.mqh> //+------------------------------------------------------------------+ //| 4. Входные параметры для стратегии | //+------------------------------------------------------------------+ sinput string symbol_ = "GBPUSD"; sinput ENUM_TIMEFRAMES period_ = PERIOD_H1; input group "=== Параметры сигнала к открытию" input int signalSeqLen_ = 5; // Количество однонаправленных свечей input int periodATR_ = 30; // Период ATR input group "=== Параметры отложенных ордеров" input double stopLevel_ = 3750; // Stop Loss (в пунктах) input double takeLevel_ = 50; // Take Profit (в пунктах) input group "=== Параметры управление капиталом" input int maxCountOfOrders_ = 3; // Макс. количество одновременно отрытых ордеров //+------------------------------------------------------------------+ //| 5. Функция формирования строки инициализации стратегии | //| из входных параметров | //+------------------------------------------------------------------+ string GetStrategyParams() { return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d)", symbol_, period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_ ); } //+------------------------------------------------------------------+
No entanto, mesmo que compilarmos o arquivo do EA da primeira etapa (a compilação ocorre sem erros), ao executá-lo obteremos o seguinte erro na função OnInit(), que leva à parada do EA:
2018.01.01 00:00:00 CVirtualFactory::Create | ERROR: Constructor not found for: 2018.01.01 00:00:00 class CSimpleCandlesStrategy("GBPUSD",16385,5,30,2.95,3.92,3)
A causa está no fato de que, para criar objetos de todas as classes herdadas de CFactorable, é usada a função separada CVirtualFactory::Create() do arquivo Virtual/VirtualFactory.mqh. Ela é chamada nos macros NEW(C) e CREATE(C, O, P), declarados em Base/Factorable.mqh.
Nessa função, a partir da string de inicialização é lido o nome da classe do objeto e armazenado na variável className. Ao mesmo tempo, a parte lida é removida da string de inicialização. Depois disso, ocorre uma simples verificação sequencial de todos os possíveis nomes de classes (herdeiras de CFactorable), até encontrar uma correspondência com o nome recém-lido. Nesse caso, é criado um novo objeto da classe necessária, e o ponteiro para ele é retornado pela variável object como resultado da função de criação:
// Создание объекта из строки инициализации static CFactorable* Create(string p_params) { // Читаем имя класса объекта string className = CFactorable::ReadClassName(p_params); // Указатель на создаваемый объект CFactorable* object = NULL; // В зависимости от имени класса вызываем соответствующий конструктор if(className == "CVirtualAdvisor") { object = new CVirtualAdvisor(p_params); } else if(className == "CVirtualRiskManager") { object = new CVirtualRiskManager(p_params); } else if(className == "CVirtualStrategyGroup") { object = new CVirtualStrategyGroup(p_params); } else if(className == "CSimpleVolumesStrategy") { object = new CSimpleVolumesStrategy(p_params); } else if(className == "CHistoryStrategy") { object = new CHistoryStrategy(p_params); } // Если объект не создан или создан в неисправном состоянии, то сообщаем об ошибке if(!object) { ... } return object; }
Quando todo o nosso código ficava em uma única pasta, nós simplesmente adicionávamos aqui novos ramos ao operador condicional para cada nova classe-herdeira de CFactorable. Foi assim, por exemplo, que surgiu a parte responsável pela criação dos objetos da nossa primeira estratégia-modelo SimpleVolumes:
} else if(className == "CSimpleVolumesStrategy") { object = new CSimpleVolumesStrategy(p_params); }
Seguindo essa mesma abordagem, deveríamos adicionar aqui um bloco semelhante também para a nossa nova estratégia-modelo SimpleCandles:
} else if(className == "CSimpleCandlesStrategy") { object = new CSimpleCandlesStrategy(p_params); }
Mas agora isso já quebra o princípio da separação entre código da biblioteca e código do projeto. A parte da biblioteca não deve ter a obrigação de conhecer quais novas estratégias serão criadas durante seu uso. Inclusive, mesmo a criação da CSimpleVolumesStrategy dessa forma já não parece correta.
Vamos tentar pensar em um jeito de garantir, ao mesmo tempo, a criação de todos os objetos necessários e a separação clara entre código da biblioteca e do projeto.
Aprimorando o CFactorable
É preciso admitir que essa tarefa não é simples. Foi necessário refletir bastante sobre a solução e experimentar várias opções de implementação até chegar a uma que será mantida, pelo menos por enquanto. Se a linguagem MQL5 tivesse a capacidade de executar código a partir de uma string em um programa já compilado, tudo seria resolvido facilmente. No entanto, por motivos de segurança, não existe aqui uma função semelhante à eval() de outras linguagens. Portanto, foi necessário encontrar alternativas usando os recursos disponíveis.
Em resumo, cada herdeiro de CFactorable deve possuir uma função estática responsável por criar o objeto dessa classe. Ou seja, uma espécie de construtor estático. Dessa maneira, o construtor comum pode ser definido como não público e a criação de objetos seria feita exclusivamente pelo construtor estático. Em seguida, precisaríamos vincular os nomes das classes em formato de string a essas funções, de modo que, ao receber o nome da classe a partir da string de inicialização, soubéssemos qual função construtor deveria ser chamada.
Para essa solução, podemos recorrer aos ponteiros de função. Esse é um tipo especial de variável que permite armazenar nela o endereço de uma função e, posteriormente, chamar o código da função usando esse ponteiro. Podemos perceber que todos os construtores estáticos dos objetos de diferentes classes-herdeiras de CFactorable podem ser declarados com a mesma assinatura:
static CFactorable* Create(string p_params)
Portanto, podemos criar um array estático no qual serão armazenados os ponteiros para funções desse tipo, correspondentes a todas as classes-herdeiras. As classes que fazem parte da biblioteca Advisor (CVirtualAdvisor, CVirtualStrategyGroup, CVirtualRiskManager) seriam adicionadas a esse array dentro do código da própria biblioteca. Já as classes das estratégias de trading seriam adicionadas a esse array a partir do código localizado na pasta de trabalho do projeto. Dessa forma, alcançaríamos a separação desejada entre as partes do código.
Em seguida surge a questão: como exatamente faremos isso? Em qual classe declarar esse array estático e como garantir o seu preenchimento? Como manter o vínculo entre o nome da classe e o elemento correspondente nesse array?
A ideia mais adequada, a princípio, parecia ser criar esse array estático dentro da classe CFactorable. Para o vínculo, poderíamos ter ainda outro array estático de strings, contendo os nomes das classes. Se a operação de preenchimento adicionasse simultaneamente o nome da classe em um array e o ponteiro para o construtor estático de objetos dessa classe no outro, obteríamos uma associação por índice entre os elementos desses dois arrays. Ou seja, ao encontrar no primeiro array o índice do elemento com o nome de classe desejado, poderíamos usar esse índice para acessar no segundo array o ponteiro para a função-construtor e então chamá-la, passando a string de inicialização.
Mas como preencher esses arrays? Não parecia uma boa ideia criar funções que precisassem ser obrigatoriamente chamadas a partir do OnInit(). Embora, como acabou ficando claro, essa abordagem funcionasse, no final chegamos a outra solução.
A ideia principal era termos a possibilidade de executar determinado código não a partir do OnInit(), mas diretamente nos arquivos de definição das classes herdeiras de CFactorable. Contudo, se colocássemos o código simplesmente fora da definição da classe, ele não seria executado. Já se declararmos uma variável global, fora da definição da classe, e ela for um objeto de determinada classe, nesse ponto será chamado o construtor dessa variável!
Portanto, criaremos uma classe separada, chamada CFactorableCreator, especificamente para essa finalidade. Seus objetos armazenarão o nome da classe e o ponteiro para o construtor estático dos objetos da classe correspondente. Além disso, essa classe terá um array estático de ponteiros para objetos... dela mesma. E o construtor CFactorableCreator cuidará para que cada objeto criado seja automaticamente adicionado a esse array:
// Предварительное определение класса class CFactorable; // Объявление типа - указатель на функцию создания объектов класса CFactorable typedef CFactorable* (*TCreateFunc)(string); //+------------------------------------------------------------------+ //| Класс создателей, связывающих названия и статические | //| конструкторы классов-наследников CFactorable | //+------------------------------------------------------------------+ class CFactorableCreator { public: string m_className; // Название класса TCreateFunc m_creator; // Статический конструктор для этого класса // Конструктор создателя CFactorableCreator(string p_className, TCreateFunc p_creator); // Статический массив всех созданных объектов-создателей static CFactorableCreator* creators[]; }; // Статический массив всех созданных объектов-создателей CFactorableCreator* CFactorableCreator::creators[]; //+------------------------------------------------------------------+ //| Конструктор создателя | //+------------------------------------------------------------------+ CFactorableCreator::CFactorableCreator(string p_className, TCreateFunc p_creator) : m_className(p_className), m_creator(p_creator) { // Добавляем текущий объект создателя в статический массив APPEND(creators, &this); } //+------------------------------------------------------------------+
Vejamos como é possível organizar o preenchimento do array CFactorableCreator::creators usando como exemplo a classe CVirtualAdvisor. Vamos mover o construtor CVirtualAdvisor para a seção protected, adicionar a função de construtor estático Create() e, após a definição da classe, criar um objeto global da classe CFactorableCreator com o nome CVirtualAdvisorCreator. É nesse ponto que, ao chamar o construtor CFactorableCreator, acontecerá o preenchimento do array CFactorableCreator::creators.
//+------------------------------------------------------------------+ //| Класс эксперта, работающего с виртуальными позициями (ордерами) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: //... CVirtualAdvisor(string p_param); // Закрытый конструктор public: static CFactorable* Create(string p_params) { return new CVirtualAdvisor(p_params) }; //... }; CFactorableCreator CVirtualAdvisorCreator("CVirtualAdvisor", CVirtualAdvisor::Create);
Exatamente as mesmas três modificações precisarão ser feitas em todas as classes herdeiras de CFactorable. Para simplificar um pouco, declararemos dois macros auxiliares no arquivo da classe CFactorable:
// Объявление статического конструктора внутри класса #define STATIC_CONSTRUCTOR(C) static CFactorable* Create(string p) { return new C(p); } // Добавление статического конструктора для нового класса-наследника CFactorable // в специальный массив через создание глобального объекта класса CFactorableCreator #define REGISTER_FACTORABLE_CLASS(C) CFactorableCreator C##Creator(#C, C::Create);
Eles apenas repetem o padrão de código que já implementamos para a classe CVirtualAdvisor. Assim, poderemos realizar as alterações da seguinte forma:
//+------------------------------------------------------------------+ //| Класс эксперта, работающего с виртуальными позициями (ордерами) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: // ... CVirtualAdvisor(string p_param); // Конструктор public: STATIC_CONSTRUCTOR(CVirtualAdvisor); // ... }; REGISTER_FACTORABLE_CLASS(CVirtualAdvisor);
Essas mudanças devem ser aplicadas aos arquivos das três classes da biblioteca Advisor (CVirtualAdvisor, CVirtualStrategyGroup, CVirtualRiskManager), mas isso precisa ser feito apenas uma vez. Agora que essas alterações já estão na biblioteca, podemos esquecer delas.
Já nos arquivos das classes de estratégias de trading, localizados na pasta de trabalho do projeto, tais adições são obrigatórias para cada nova classe. Vamos adicioná-las à nossa nova estratégia e, depois disso, o código de definição da classe ficará assim:
//+------------------------------------------------------------------+ //| Торговая стратегия c использованием однонаправленных свечей | //+------------------------------------------------------------------+ class CSimpleCandlesStrategy : public CVirtualStrategy { protected: string m_symbol; // Символ (торговый инструмент) ENUM_TIMEFRAMES m_timeframe; // Период графика (таймфрейм) //--- Параметры сигнала к открытию int m_signalSeqLen; // Количество однонаправленных свечей int m_periodATR; // Период ATR //--- Параметры позиций double m_stopLevel; // Stop Loss (в пунктах или % ATR) double m_takeLevel; // Take Profit (в пунктах или % ATR) //--- Параметры управление капиталом int m_maxCountOfOrders; // Макс. количество одновременно отрытых позиций CSymbolInfo *m_symbolInfo; // Объект для получения информации о свойствах символа double m_tp; // Stop Loss в пунктах double m_sl; // Take Profit в пунктах //--- Методы int SignalForOpen(); // Сигнал для открытия позиции void OpenBuy(); // Открытие позиции BUY void OpenSell(); // Открытие позиции SELL double ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1); // Расчёт величины ATR void UpdateLevels(); // Обновление уровней SL и TP // Закрытый конструктор CSimpleCandlesStrategy(string p_params); public: // Статический конструктор STATIC_CONSTRUCTOR(CSimpleCandlesStrategy); virtual string operator~() override; // Преобразование объекта в строку virtual void Tick() override; // Обработчик события OnTick }; // Регистрация класса-наследника CFactorable REGISTER_FACTORABLE_CLASS(CSimpleCandlesStrategy);
Mais uma vez, é importante destacar que essas partes destacadas devem obrigatoriamente estar presentes em qualquer nova classe de estratégia de trading.
Resta apenas aplicar o array preenchido de criadores de objetos na função geral de criação de objetos a partir da string de inicialização CVirtualFactory::Create(). Aqui também faremos algumas mudanças. Como ficou claro, agora não há necessidade de manter essa função em uma classe separada. Antes, isso era feito porque formalmente a classe CFactorable não era obrigada a conhecer os nomes de todos os seus herdeiros. Mas, após as alterações já realizadas, não precisamos mais conhecer nominalmente todos os descendentes, podemos criar qualquer um deles chamando os construtores estáticos por meio dos elementos do array unificado CFactorableCreator::creators. Portanto, moveremos o código dessa função para um novo método estático da classe CFactorable::Create():
//+------------------------------------------------------------------+ //| Базовый класс объектов, создаваемых из строки | //+------------------------------------------------------------------+ class CFactorable { // ... public: // ... // Создание объекта из строки инициализации static CFactorable* Create(string p_params); }; //+------------------------------------------------------------------+ //| Создание объекта из строки инициализации | //+------------------------------------------------------------------+ CFactorable* CFactorable::Create(string p_params) { // Указатель на создаваемый объект CFactorable* object = NULL; // Читаем имя класса объекта string className = CFactorable::ReadClassName(p_params); // В зависимости от имени класса находим и вызываем соответствующий конструктор int i; SEARCH(CFactorableCreator::creators, className == CFactorableCreator::creators[i].m_className, i); if(i != -1) { object = CFactorableCreator::creators[i].m_creator(p_params); } // Если объект не создан или создан в неисправном состоянии, то сообщаем об ошибке if(!object) { PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\n%s", p_params); } else if(!object.IsValid()) { PrintFormat(__FUNCTION__ " | ERROR: Created object is invalid for:\n%s", p_params); delete object; // Удаляем неисправный объект object = NULL; } return object; }
Como se pode ver, primeiro obtemos o nome da classe a partir da string de inicialização e, em seguida, buscamos o índice do elemento no array de criadores cujo nome de classe corresponda ao necessário. O índice encontrado é armazenado na variável i. Se o índice for válido, o construtor estático do objeto da classe correspondente será chamado por meio do ponteiro para a função associado. Nesse código já não há nenhuma menção direta aos nomes das classes-herdeiras de CFactorable. O arquivo com a classe CVirtualFactory tornou-se desnecessário e será removido da biblioteca.
Verificação do EA da primeira etapa
Compilamos o EA da primeira etapa e iniciamos a otimização, por enquanto manualmente. O intervalo de otimização escolhido foi de 2018 até 2023, inclusive, no símbolo GBPUSD e timeframe H4. A otimização iniciou com sucesso e, após algum tempo, pudemos analisar os resultados obtidos:
Fig. 1. Configurações de otimização e visualização dos resultados de otimização para o EA Stage1.mq5
Vejamos alguns passes individuais que pareceram mais ou menos razoáveis.
Fig. 2. Resultados do passe com parâmetros: class CSimpleCandlesStrategy("GBPUSD",16388,4,23,2.380,4.950,19)
Nos resultados mostrados na fig. 2, a abertura ocorreu após quatro velas consecutivas na mesma direção, e a relação entre os níveis de StopLoss e TakeProfit foi de aproximadamente 1:2.
Fig. 3. Resultados do passe com parâmetros: class CSimpleCandlesStrategy("GBPUSD",16388,7,9,0.090,3.840,1)
Na fig. 3, são exibidos os resultados de um passe em que a abertura ocorreu após sete velas consecutivas na mesma direção. Nesse caso, foi usado um StopLoss muito curto e um TakeProfit bastante longo. Isso fica evidente no gráfico, onde a maioria das operações foi encerrada com pequenas perdas, e apenas cerca de dez operações, ao longo de seis anos, foram fechadas com lucro, mas um lucro expressivo.
Portanto, apesar de essa estratégia de trading ser muito simples, ainda é possível trabalhar com ela para obter resultados mais interessantes, após combinar várias instâncias em um EA final.
Considerações finais
Ainda não concluímos totalmente o processo de conexão de uma nova estratégia ao sistema de otimização automática, mas demos passos importantes que nos permitirão seguir adiante. Em primeiro lugar, já temos uma nova estratégia de trading implementada como uma classe separada, herdeira de CVirtualStrategy. Em segundo lugar, conseguimos conectá-la ao EA da primeira etapa e verificar que é possível iniciar o processo de otimização desse EA.
A otimização de uma instância única da estratégia de trading, realizada na primeira etapa, começa quando ainda não há resultados de nenhum passe no banco de dados de otimização. Já para a segunda e terceira etapas, é necessário que existam no banco de dados os resultados dos passes da primeira etapa. Portanto, ainda não podemos conectar e testar a estratégia nos EAs da segunda e terceira etapas. Antes, será preciso criar um projeto no banco de dados de otimização e executá-lo para acumular os resultados da primeira etapa. Na próxima parte, continuaremos esse trabalho analisando a modificação do EA de criação de projetos.
Obrigado pela atenção e até a próxima!
Aviso importante
Todos os resultados apresentados neste artigo e em todos os anteriores da série baseiam-se apenas em dados de testes históricos e não representam garantia de obtenção de qualquer lucro no futuro. O trabalho realizado neste projeto tem caráter exclusivamente de pesquisa. Todos os resultados publicados podem ser usados por qualquer pessoa por sua própria conta e risco.
Conteúdo do arquivo compactado
# | Nome | Versão | Descrição | Últimas alterações |
---|---|---|---|---|
MQL5/Experts/Article.17277 | Pasta de trabalho do projeto | |||
1 | CreateProject.mq5 | 1.01 | EA-script de criação de projeto com etapas, trabalhos e tarefas de otimização. | Parte 23 |
2 | Optimization.mq5 | 1.00 | EA para otimização automática de projetos | Parte 23 |
3 | SimpleCandles.mq5 | 1.00 | EA final para execução paralela de vários grupos de estratégias-modelo. Os parâmetros serão obtidos da biblioteca incorporada de grupos. | Parte 24 |
4 | Stage1.mq5 | 1.22 | EA de otimização de uma instância única da estratégia de trading (Etapa 1) | Parte 24 |
5 | Stage2.mq5 | 1.00 | EA de otimização de um grupo de instâncias de estratégias de trading (Etapa 2) | Parte 23 |
6 | Stage3.mq5 | 1.00 | EA que salva o grupo normalizado de estratégias formado no banco de dados do expert com o nome especificado. | Parte 23 |
MQL5/Experts/Article.17277/Strategies | Pasta de estratégias do projeto | |||
7 | SimpleCandlesStrategy.mqh | 1.01 | Parte 24 | |
MQL5/Include/antekov/Advisor/Base | Classes básicas das quais outras classes do projeto herdam | |||
8 | Advisor.mqh | 1.04 | Classe base do expert | Parte 10 |
9 | Factorable.mqh | 1.05 | Classe base de objetos criados a partir de string | Parte 24 |
10 | FactorableCreator.mqh | 1.00 | Parte 24 | |
11 | Interface.mqh | 1.01 | Classe base para visualização de diferentes objetos | Parte 4 |
12 | Receiver.mqh | 1.04 | Classe base para conversão de volumes abertos em posições de mercado | Parte 12 |
13 | Strategy.mqh | 1.04 | Classe base da estratégia de trading | Parte 10 |
MQL5/Include/antekov/Advisor/Database | Arquivos para trabalhar com todos os tipos de bancos de dados usados pelos EAs do projeto | |||
14 | Database.mqh | 1.10 | Classe para trabalhar com banco de dados | Parte 22 |
15 | db.adv.schema.sql | 1.00 | Esquema do banco de dados do EA final | Parte 22 |
16 | db.cut.schema.sql | 1.00 | Esquema do banco de dados reduzido de otimização | Parte 22 |
17 | db.opt.schema.sql | 1.05 | Esquema do banco de dados de otimização | Parte 22 |
18 | Storage.mqh | 1.01 | Classe para trabalhar com armazenamento Key-Value para o EA final no banco de dados do expert | Parte 23 |
MQL5/Include/antekov/Advisor/Experts | Arquivos com as partes comuns usadas pelos EAs de diferentes tipos | |||
19 | Expert.mqh | 1.22 | Arquivo da biblioteca para o EA final. Parâmetros dos grupos podem ser obtidos do banco de dados do expert | Parte 23 |
20 | Optimization.mqh | 1.04 | Arquivo da biblioteca para o EA que gerencia a execução de tarefas de otimização | Parte 23 |
21 | Stage1.mqh | 1.19 | Arquivo da biblioteca para o EA de otimização de uma instância única da estratégia de trading (Etapa 1) | Parte 23 |
22 | Stage2.mqh | 1.04 | Arquivo da biblioteca para o EA de otimização de um grupo de instâncias de estratégias de trading (Etapa 2) | Parte 23 |
23 | Stage3.mqh | 1.04 | Arquivo da biblioteca para o EA que salva o grupo normalizado de estratégias formado no banco de dados do expert com o nome especificado. | Parte 23 |
MQL5/Include/antekov/Advisor/Optimization | Classes responsáveis pelo funcionamento da otimização automática | |||
24 | Optimizer.mqh | 1.03 | Classe para o gerenciador de otimização automática de projetos | Parte 22 |
25 | OptimizerTask.mqh | 1.03 | Classe para tarefa de otimização | Parte 22 |
MQL5/Include/antekov/Advisor/Strategies | Exemplos de estratégias de trading usadas para demonstração do funcionamento do projeto | |||
26 | HistoryStrategy.mqh | 1.00 | Classe da estratégia de trading para reprodução do histórico de operações | Parte 16 |
27 | SimpleVolumesStrategy.mqh | 1.11 | Classe da estratégia de trading com uso de volumes de ticks | Parte 22 |
MQL5/Include/antekov/Advisor/Utils | Utilitários auxiliares, macros para redução de código | |||
28 | ExpertHistory.mqh | 1.00 | Classe para exportar o histórico de operações em arquivo | Parte 16 |
29 | Macros.mqh | 1.05 | Macros úteis para operações com arrays | Parte 22 |
30 | NewBarEvent.mqh | 1.00 | Classe para detecção de nova barra em um símbolo específico | Parte 8 |
31 | SymbolsMonitor.mqh | 1.00 | Classe para obtenção de informações sobre instrumentos de trading (símbolos) | Parte 21 |
MQL5/Include/antekov/Advisor/Virtual | Classes para criação de diferentes objetos, unificados pelo uso do sistema de ordens e posições virtuais | |||
32 | Money.mqh | 1.01 | Classe base de gestão de capital | Parte 12 |
33 | TesterHandler.mqh | 1.07 | Classe para processamento de eventos de otimização | Parte 23 |
34 | VirtualAdvisor.mqh | 1.10 | Classe de expert que trabalha com posições (ordens) virtuais | Parte 24 |
35 | VirtualChartOrder.mqh | 1.01 | Classe de posição virtual gráfica | Parte 18 |
36 | VirtualHistoryAdvisor.mqh | 1.00 | Classe de expert para reprodução do histórico de operações | Parte 16 |
37 | VirtualInterface.mqh | 1.00 | Classe de interface gráfica do EA | Parte 4 |
38 | VirtualOrder.mqh | 1.09 | Classe de ordens e posições virtuais | Parte 22 |
39 | VirtualReceiver.mqh | 1.04 | Classe para conversão de volumes abertos em posições de mercado (receptor) | Parte 23 |
40 | VirtualRiskManager.mqh | 1.05 | Classe de gerenciamento de risco (risk-manager) | Parte 24 |
41 | VirtualStrategy.mqh | 1.09 | Classe de estratégia de trading com posições virtuais | Parte 23 |
42 | VirtualStrategyGroup.mqh | 1.03 | Classe de grupo de estratégias de trading ou grupos de estratégias | Parte 24 |
43 | VirtualSymbolReceiver.mqh | 1.00 | Classe de receptor simbólico | Parte 3 |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/17277
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.





- 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