
Desenvolvendo um EA multimoeda (Parte 1): várias estratégias de trading trabalhando juntas
No decorrer do meu trabalho, tive que lidar com várias estratégias de trading. Como regra geral, os EAs para trading automático implementam apenas uma ideia de trading. As dificuldades de garantir o funcionamento estável de vários EAs em um terminal forçavam a escolher apenas um pequeno número dos melhores. Mas ainda assim é uma pena descartar estratégias totalmente funcionais por essa razão. Como fazer com que elas trabalhem juntas?
Definição da tarefa
Precisamos definir o que queremos e o que temos.
Nós temos (ou quase temos):
- algumas estratégias de trading diferentes, funcionando em diferentes símbolos e timeframes como código de EA já pronto ou apenas um conjunto formulado de regras de operações de trading
- depósito inicial
- rebaixamento máximo permitido
Nós queremos:
- trabalho conjunto de todas as estratégias escolhidas em uma conta, em vários símbolos e timeframes
- distribuição do depósito inicial entre todos de forma igual ou de acordo com coeficientes estabelecidos
- cálculo automático dos volumes das posições abertas para respeitar o rebaixamento máximo permitido
- processamento correto do reinício do terminal
- possibilidade de execução no MT5 e MT4
Usaremos uma abordagem orientada a objetos, MQL5, o testador padrão no MetaTrader 5.
A tarefa definida é bastante grande, então vamos resolvê-la por etapas.
Na primeira etapa, pegaremos uma ideia de trading simples. Faremos um EA simples com base nela. Realizaremos sua otimização e escolheremos os dois melhores conjuntos de parâmetros. Criaremos um EA que conterá dois exemplares do EA simples original e veremos seus resultados.
Da ideia de trading à estratégia
Como uma ideia de trading experimental, pegaremos a seguinte.
Suponhamos que quando uma intensa atividade de trading começa em um símbolo, o preço pode mudar por unidade de tempo mais do que quando a atividade de trading é fraca. Então, se virmos que a atividade de trading aumentou e o preço mudou em alguma direção, é possível que ele mude ainda mais na mesma direção em um futuro próximo. Vamos tentar obter lucro com isso.
A estratégia de trading — é um conjunto de regras de abertura e fechamento de posições baseado na ideia de trading e não contém parâmetros desconhecidos. Esse conjunto de regras deve permitir, em qualquer momento da operação da estratégia, determinar se alguma posição deve ser aberta e, em caso afirmativo, quais exatamente.
Vamos tentar transformar a ideia em estratégia. Primeiro, precisamos detectar de alguma forma o aumento da intensidade do trading. Sem isso, não poderemos determinar os momentos de abertura de posições. Vamos usar para isso o volume de ticks, ou seja, a quantidade de novos preços recebidos pelo terminal durante a vela atual. Consideraremos um volume de ticks maior como um sinal de trading mais intenso. Mas a intensidade pode variar significativamente entre os diferentes símbolos. Por isso, não podemos estabelecer um nível único de volume de ticks que consideraremos o início de um trading intenso. Então, para determinar esse nível, podemos nos basear no volume médio de ticks de várias velas. Após alguma reflexão, podemos dar a seguinte descrição:
Colocamos uma ordem pendente no momento em que o volume de ticks da vela excede o volume médio, na direção da vela atual. Cada ordem terá um tempo de expiração, após o qual será removida. Se a ordem pendente se transformar em uma posição, ela será fechada apenas ao atingir os níveis definidos de StopLoss e TakeProfit. Se o volume de ticks exceder ainda mais o volume médio, além da ordem pendente já aberta, ordens adicionais poderão ser colocadas.
Esta é uma descrição mais detalhada, mas ainda não completa. Então, vamos reler e destacar todos os pontos que não estão claros. Nesses pontos, é necessário fornecer explicações mais detalhadas.
Aqui estão as questões que surgiram:
- "Colocamos uma ordem pendente..." — Quais ordens pendentes iremos colocar?
- "...volume médio, ..." — Como calcular o volume médio da vela?
- "... excede o volume médio, ..." — Como determinar o excedente do volume médio?
- "... Se o volume de ticks exceder ainda mais o volume médio, ..." — Como determinar esse excedente ainda maior?
- "... ordens adicionais poderão ser colocadas" — Quantas ordens podem ser colocadas no total?
Quais ordens pendentes iremos colocar? Com base nessa ideia, esperamos que o movimento do preço continue na mesma direção em que começou a partir do início da vela. Se, por exemplo, o preço no momento atual estiver acima do início do período da vela, devemos abrir uma ordem pendente de compra. Se abrirmos um BUY_LIMIT, para que ele seja acionado, o preço deve primeiro retornar um pouco (cair) e, depois, para que a posição aberta gere lucro, o preço deve subir novamente. Se abrirmos um BUY_STOP, para abrir a posição o preço deve continuar subindo um pouco mais e, em seguida, subir ainda mais para obter lucro.
Qual dessas opções será melhor não é imediatamente claro. Portanto, para simplificar, vamos escolher sempre abrir ordens de stop (BUY_STOP e SELL_STOP). Posteriormente, isso pode ser feito como um parâmetro da estratégia, cujo valor determinará quais ordens serão abertas.
Como calcular o volume médio da vela? Para calcular o volume médio, precisamos escolher as velas cujos volumes serão incluídos no cálculo da média. Vamos considerar uma quantidade de velas consecutivas fechadas recentemente. Então, se definirmos o número de velas, podemos calcular o volume médio de ticks.
Como determinar o excedente do volume médio? Se usarmos uma condição do tipo
V > V_avr,
onde
V é o volume de ticks da vela atual,
V_avr é o volume médio de ticks,
então essa condição será atendida em aproximadamente metade das velas. Com base na ideia, devemos colocar ordens apenas quando o volume não apenas exceder a média, mas exceder significativamente a média. Caso contrário, isso ainda não pode ser considerado um sinal de trading mais intenso nesta vela em comparação com as velas anteriores. Podemos usar, por exemplo, a seguinte fórmula:
V > V_avr + D * V_avr,
onde D é um coeficiente numérico. Se D = 1, então a abertura ocorrerá quando o volume atual exceder a média em 2 vezes, e se, por exemplo, D = 2, a abertura ocorrerá quando o volume atual exceder a média em 3 vezes.
No entanto, essa condição só pode ser aplicada para abrir uma ordem, pois se for usada para abrir a segunda e subsequentes, elas serão abertas imediatamente após a primeira, o que pode ser substituído simplesmente pela abertura de uma ordem de maior volume.
Como determinar esse excedente ainda maior? Para isso, adicionamos mais um parâmetro na fórmula da condição - o número de ordens abertas N:
V > V_avr + D * V_avr + N * D * V_avr.
Então, para abrir a segunda ordem após a primeira (ou seja, N = 1), a condição deve ser:
V > V_avr + 2 * D * V_avr.
Para abrir a primeira ordem (N = 0), a fórmula assume a forma já conhecida por nós:
V > V_avr + D * V_avr.
E a última correção na fórmula de abertura. Vamos fazer, em vez de um único parâmetro D para a primeira e as ordens subsequentes, dois parâmetros independentes D e D_add:
V > V_avr + D * V_avr + N * D_add * V_avr,
V > V_avr * (1 + D + N * D_add)
Parece que isso nos dará mais liberdade na escolha dos parâmetros ótimos para a estratégia.
Se em nossa condição usamos a magnitude N como a quantidade total de ordens e posições, isso significa que cada ordem pendente se transforma em uma posição separada, não aumentando o volume da posição já aberta. Portanto, por enquanto, teremos que limitar a aplicação dessa estratégia apenas ao trabalho em contas com hedge de posições.
Quando tudo estiver claro, enumeramos os valores que podem assumir diferentes valores, não apenas um único. Esses serão nossos parâmetros de entrada para a estratégia. Considere que, para abrir ordens, precisamos saber também o volume, a distância do preço atual, o tempo de expiração e os níveis de StopLoss e TakeProfit. Então, obtemos a seguinte descrição:
O EA é lançado em um determinado símbolo e período (timeframe) em uma conta Hedge
Definimos os parâmetros de entrada:
- Número de velas para calcular a média dos volumes (K)
- Desvio relativo da média para abrir a primeira ordem (D)
- Desvio relativo da média para abrir a segunda e subsequentes ordens (D_add)
- Distância do preço até a ordem pendente
- Stop Loss (em pontos)
- Take Profit (em pontos)
- Tempo de expiração das ordens pendentes (em minutos)
- Número máximo de ordens abertas simultaneamente (N_max)
- Volume de cada ordem
Encontramos o número de ordens e posições abertas (N).
Se for menor que N_max, então:
calculamos o volume médio de ticks das últimas K velas fechadas, obtendo a magnitude V_avr.
Se a condição V > V_avr * (1 + D + N * D_add) for atendida, então:
determinamos a direção da mudança de preço na vela atual: se o preço aumentou, colocamos uma ordem pendente BUY_STOP, caso contrário, SELL_STOP.
colocamos uma ordem pendente na distância, tempo de expiração e níveis de StopLoss e TakeProfit especificados nos parâmetros.
Implementação da estratégia de trading
Vamos começar a escrever o código. Primeiro, listamos todos os parâmetros, dividindo-os em grupos para maior clareza e adicionando um comentário a cada parâmetro. Esses comentários (se existirem) serão exibidos na caixa de diálogo de definição de parâmetros ao iniciar o EA e na aba de parâmetros no testador de estratégias, em vez dos nomes das variáveis que escolhemos para eles.
Os valores padrão serão atribuídos provisoriamente, e durante o processo de otimização, buscaremos os melhores.
input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magicN_ = 27181; // Magic
Como o EA realizará operações de trading, criaremos um objeto global da classe CTrade. Através da chamada dos métodos deste objeto, colocaremos ordens pendentes.
CTrade trade; // Object for performing trading operations
Por precaução, lembremos que variáveis (ou objetos) globais são variáveis (ou objetos) declaradas no código do EA não dentro de qualquer função. Portanto, estão disponíveis em todas as nossas funções no EA. Não devem ser confundidas com variáveis globais do terminal.
Para calcular os parâmetros de abertura das ordens, precisaremos obter os preços atuais e outras propriedades do símbolo no qual o EA será executado. Para isso, criaremos um objeto global da classe CSymbolInfo.
CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties
Também precisaremos contar o número de ordens e posições abertas. Para isso, criaremos objetos globais das classes COrderInfo e CPositionInfo, através deles obteremos informações sobre as ordens e posições abertas. O número será armazenado em duas variáveis globais countOrders e countPositions.
COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions
Para calcular o volume médio de ticks de várias velas, podemos usar, por exemplo, o indicador técnico iVolumes. Para obter seus valores, precisaremos de uma variável para armazenar o handle desse indicador (um número inteiro que armazena o número de série desse indicador entre todos os que serão usados no EA). Para encontrar o volume médio, primeiro teremos que copiar os valores do buffer do indicador para um array previamente preparado. Esse array também será global.
int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves)
Agora podemos começar a função de inicialização do EA OnInit() e a função de processamento de ticks OnTick().
Na inicialização, podemos fazer o seguinte:
- Carregar o indicador para obter volumes de ticks e armazenar seu handle.
- Definir o tamanho do array receptor de acordo com o número de velas para o cálculo do volume médio e configurar sua indexação como em séries temporais.
- Definir o Magic Number para colocação de ordens através do objeto trade.
Aqui está como nossa função de inicialização ficará:
int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); }
Na função de processamento de ticks, conforme a descrição da estratégia, devemos começar encontrando o número de ordens e posições abertas. Vamos implementar isso como uma função separada UpdateCounts(). Nela, vamos iterar por todas as posições e ordens abertas, contando apenas aquelas cujo Magic Number coincide com o Magic Number do nosso EA.
void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } }
Depois, devemos verificar se o número de posições e ordens abertas não excede o valor definido nas configurações. Neste caso, precisamos verificar se a condição para abrir uma nova ordem é atendida. Implementaremos essa verificação também como uma função separada SignalForOpen(). Ela retornará um dos três valores possíveis:
- +1 — sinal para abrir uma ordem BUY_STOP
- 0 — sem sinal para abertura
- -1 — sinal para abrir uma ordem SELL_STOP
Para colocar ordens pendentes, escreveremos duas funções separadas: OpenBuyOrder() e OpenSellOrder().
Agora podemos escrever a implementação completa da função OnTick().
void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } }
Depois, adicionamos a implementação das funções restantes e o código do EA estará pronto. Salvaremos em um arquivo chamado SimpleVolumes.mq5 na pasta atual.
#include <Trade\OrderInfo.mqh> #include <Trade\PositionInfo.mqh> #include <Trade\SymbolInfo.mqh> #include <Trade\Trade.mqh> input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magicN_ = 27181; // Magic CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) //+------------------------------------------------------------------+ //| Initialization function of the expert | //+------------------------------------------------------------------+ int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } } //+------------------------------------------------------------------+ //| Calculate the number of open orders and positions | //+------------------------------------------------------------------+ void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } } //+------------------------------------------------------------------+ //| Open the BUY_STOP order | //+------------------------------------------------------------------+ void OpenBuyOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = ask + distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price - stopLevel_ * point, digits); double tp = NormalizeDouble(price + (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.BuyStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Open the SELL_STOP order | //+------------------------------------------------------------------+ void OpenSellOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = bid - distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price + stopLevel_ * point, digits); double tp = NormalizeDouble(price - (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.SellStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int SignalForOpen() { // By default, there is no signal int signal = 0; // Copy volume values from the indicator buffer to the receiving array int res = CopyBuffer(iVolumesHandle, 0, 0, signalPeriod_, volumes); // If the required amount of numbers have been copied if(res == signalPeriod_) { // Calculate their average value double avrVolume = ArrayAverage(volumes); // If the current volume exceeds the specified level, then if(volumes[0] > avrVolume * (1 + signalDeviation_ + (countOrders + countPositions) * signaAddlDeviation_)) { // if the opening price of the candle is less than the current (closing) price, then if(iOpen(Symbol(), PERIOD_CURRENT, 0) < iClose(Symbol(), PERIOD_CURRENT, 0)) { signal = 1; // buy signal } else { signal = -1; // otherwise, sell signal } } } return signal; } //+------------------------------------------------------------------+ //| Number array average value | //+------------------------------------------------------------------+ double ArrayAverage(const double &array[]) { double s = 0; int total = ArraySize(array); for(int i = 0; i < total; i++) { s += array[i]; } return s / MathMax(1, total); } //+------------------------------------------------------------------+
Vamos otimizar os parâmetros do EA para EURGBP no período H1 usando cotações da MetaQuotes no período de 2018-01-01 a 2023-01-01 com um depósito inicial de $100,000 e lote mínimo de 0.01. Vale notar que o mesmo EA pode apresentar resultados ligeiramente diferentes quando testado com cotações de diferentes corretoras. Às vezes, esses resultados podem variar bastante.
Dos resultados obtidos nos testes, escolheremos dois conjuntos de parâmetros com resultados interessantes:
Fig. 1. Resultados do teste para os parâmetros [130, 0.9, 1.4, 231, 3750, 50, 600, 3, 0.01]
Fig. 2. Resultados do teste para os parâmetros [159, 1.7, 0.8, 248, 3600, 495, 39000, 3, 0.01]
Os testes foram realizados com um grande depósito inicial por uma razão. Se o EA abre posições com volume fixo, o teste pode terminar prematuramente se a perda for maior do que os fundos disponíveis. Nesse caso, não saberemos se, com os mesmos parâmetros, mas ajustando os volumes das posições (ou aumentando o depósito inicial), seria possível evitar a falência.
Vamos a um exemplo. Suponha que nosso depósito inicial era de $1,000. No teste, obtivemos os seguintes resultados:
- Depósito final de $11,000 (lucro de 1000%, o EA ganhou +$10,000 sobre os $1,000 iniciais)
- Rebaixamento máximo absoluto de $2,000
Obviamente, tivemos sorte de que esse rebaixamento ocorreu depois que o EA aumentou o depósito para mais de $2,000. Assim, o teste foi concluído, e pudemos ver esses resultados. Se esse rebaixamento tivesse ocorrido antes (por exemplo, se tivéssemos escolhido outro início de período de teste), teríamos visto apenas que o EA perde todo o depósito.
Se realizarmos os testes manualmente, podemos ajustar os volumes nos parâmetros ou aumentar o depósito inicial e reiniciar o teste. Mas se os testes forem realizados durante a otimização, essa possibilidade não existe. Nesse caso, um conjunto de parâmetros potencialmente bom pode ser rejeitado devido a configurações de gerenciamento de capital inadequadas. Para reduzir a probabilidade de tais resultados, podemos iniciar a otimização com um depósito inicial muito grande e volume mínimo.
Voltando ao exemplo, se o depósito inicial fosse de $100,000, ao ocorrer um rebaixamento de $2,000, a perda de todo o depósito não teria acontecido e o testador teria obtido esses resultados. Poderíamos então calcular que, se nossa máxima retração permitida for de 10%, o depósito inicial deve ser de pelo menos $20,000. A lucratividade nesse caso seria de apenas 50% (o EA teria ganho $10,000 sobre os $20,000 iniciais).
Vamos fazer cálculos semelhantes para as duas combinações de parâmetros escolhidas, com um depósito inicial de $10,000 e retração permitida de 10% do depósito inicial.
Parâmetros | Lote | Rebaixamento | Lucro | Rebaixamento permitido | Lote permitido | Lucro permitido |
---|---|---|---|---|---|---|
L | D | P | Da | La = L * (Da / D) | Pa = P * (Da / D) | |
[130, 0.9, 1.4, 231, 3750, 50, 600, 3, 0.01] | 0.01 | 28.70 (0.04%) | 260.41 | 1000 (10%) | 0.34 | 9073 (91%) |
[159, 1.7, 0.8, 248, 3600, 495, 39000, 3, 0.01] | 0.01 | 92.72 (0.09%) | 666.23 | 1000 (10%) | 0.10 | 7185 (72%) |
Como podemos ver, ambas as variantes de parâmetros de entrada podem gerar uma rentabilidade semelhante (~80%). A primeira variante ganha menos em termos absolutos, mas com menor rebaixamento. Portanto, podemos aumentar o volume das posições abertas mais significativamente para a primeira variante do que para a segunda, que, embora ganhe mais, permite um rebaixamento maior.
Assim, encontramos várias combinações de parâmetros de entrada promissoras e começamos a integrá-las em um único EA.
Classe básica da estratégia
Vamos criar uma classe CStrategy, que reunirá todas as propriedades e métodos comuns a todas as estratégias. Por exemplo, qualquer estratégia terá um símbolo e um timeframe, independentemente da relação com os indicadores. Também atribuímos a cada estratégia um Magic Number para abrir posições e o tamanho de uma posição. Para simplificar, não consideraremos por enquanto o trabalho da estratégia com tamanho de posição variável, isso será adicionado mais tarde.
Dos métodos necessários, podemos destacar apenas o construtor, que inicializa os parâmetros da estratégia, o método de inicialização e o manipulador de eventos OnTick. Obtivemos o seguinte código:
class CStrategy : public CObject { protected: ulong m_magic; // Magic string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) double m_fixedLot; // Size of opened positions (fixed) public: // Constructor CStrategy(ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot); virtual int Init() = 0; // Strategy initialization - handling OnInit events virtual void Tick() = 0; // Main method - handling OnTick events };
Os métodos Init() e Tick() são declarados como puramente virtuais (com = 0 após o cabeçalho do método). Isso significa que na classe CStrategy não implementaremos esses métodos. Baseando-nos nessa classe, criaremos classes derivadas, nas quais os métodos Init() e Tick() deverão ser implementados obrigatoriamente, contendo as regras específicas de trading.
A descrição da classe está pronta, adicionamos a implementação necessária do construtor em seguida. Como esta é uma função-método chamada automaticamente ao criar o objeto da estratégia, é nela que devemos garantir a inicialização dos parâmetros da estratégia. O construtor receberá quatro parâmetros e atribuirá seus valores às variáveis-membro correspondentes da classe através de uma lista de inicialização.
CStrategy::CStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot) : // Initialization list m_magic(p_magic), m_symbol(p_symbol), m_timeframe(p_timeframe), m_fixedLot(p_fixedLot) {}
Salvamos esse código no arquivo Strategy.mqh na pasta atual.
Classe da estratégia de trading
Transferimos a lógica do EA simples original para a nova classe derivada CSimpleVolumesStrategy. Para isso, todas as variáveis de entrada e variáveis globais se tornarão membros da classe. Vamos apenas remover as variáveis fixedLot_ e magicN_, substituindo-as pelos membros da classe base m_fixedLot e m_magic, herdados da classe base CStrategy.
#include "Strategy.mqh" class CSimpleVolumeStrategy : public CStrategy { //--- Open signal parameters int signalPeriod_; // Number of candles for volume averaging double signalDeviation_; // Relative deviation from the average to open the first order double signaAddlDeviation_; // Relative deviation from the average for opening the second and subsequent orders //--- Pending order parameters int openDistance_; // Distance from price to pending order double stopLevel_; // Stop Loss (in points) double takeLevel_; // Take Profit (in points) int ordersExpiration_; // Pending order expiration time (in minutes) //--- Money management parameters int maxCountOfOrders_; // Maximum number of simultaneously open orders CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) };
As funções OnInit() e OnTick() se transformarão em métodos públicos Init() e Tick(), e todas as outras funções se tornarão novos métodos privados da classe CSimpleVolumesStrategy. Os métodos públicos poderão ser chamados para estratégias a partir de código externo, por exemplo, a partir dos métodos do objeto EA. Os métodos privados podem ser chamados apenas a partir dos métodos desta classe. Adicionaremos os cabeçalhos dos métodos na descrição da classe.
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code double volumes[]; // Receiver array of indicator values (volumes themselves) //--- Methods void UpdateCounts(); // Calculate the number of open orders and positions int SignalForOpen(); // Signal for opening pending orders void OpenBuyOrder(); // Open the BUY_STOP order void OpenSellOrder(); // Open the SELL_STOP order double ArrayAverage( const double &array[]); // Average value of the number array public: //--- Public methods virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
Nos locais onde a implementação dessas funções está, adicionaremos o prefixo "CSimpleVolumesStrategy::" aos seus nomes para indicar ao compilador que não são apenas funções, mas métodos da nossa classe.
class CSimpleVolumeStrategy : public CStrategy { // Class description listing properties and methods... }; int CSimpleVolumeStrategy::Init() { // Function code ... } void CSimpleVolumeStrategy::Tick() { // Function code ... } void CSimpleVolumeStrategy::UpdateCounts() { // Function code ... } int CSimpleVolumeStrategy::SignalForOpen() { // Function code ... } void CSimpleVolumeStrategy::OpenBuyOrder() { // Function code ... } void CSimpleVolumeStrategy::OpenSellOrder() { // Function code ... } double CSimpleVolumeStrategy::ArrayAverage(const double &array[]) { // Function code ... }
No EA simples original, os valores dos parâmetros de entrada eram atribuídos na declaração, e ao iniciar o EA compilado, esses valores eram definidos pelo diálogo de parâmetros de entrada, e não pelos valores codificados. Na descrição da classe, isso não é possível, então é aqui que entra o construtor.
Vamos criar um construtor com a lista necessária de parâmetros. O construtor também deve ser público, caso contrário, não poderemos criar objetos de estratégias a partir de código externo.
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code public: //--- Public methods CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders ); // Constructor virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
A descrição da classe está pronta. Todos os seus métodos já têm implementação, exceto o construtor. Vamos adicioná-lo. No caso mais simples, o construtor desta classe apenas atribuirá os valores dos parâmetros recebidos aos membros correspondentes da classe. Os primeiros quatro parâmetros farão isso chamando o construtor da classe base.
CSimpleVolumeStrategy::CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders) : // Initialization list CStrategy(p_magic, p_symbol, p_timeframe, p_fixedLot), // Call the base class constructor signalPeriod_(p_signalPeriod), signalDeviation_(p_signalDeviation), signaAddlDeviation_(p_signaAddlDeviation), openDistance_(p_openDistance), stopLevel_(p_stopLevel), takeLevel_(p_takeLevel), ordersExpiration_(p_ordersExpiration), maxCountOfOrders_(p_maxCountOfOrders) {}
Falta muito pouco para finalizar. Renomearemos fixedLot_ e magicN_ para m_fixedLot e m_magic em todos os locais onde apareciam. Substituiremos o uso da função Symbol() para obter o símbolo atual pela variável da classe base m_symbol e a constante PERIOD_CURRENT por m_timeframe. Salvaremos este código no arquivo SimpleVolumesStrategy.mqh na pasta atual.
Classe do EA
Vamos criar uma classe base CAdvisor, cuja principal tarefa será armazenar a lista de objetos de estratégias de trading específicas e acionar seus manipuladores de eventos. Um nome mais adequado para essa classe seria CExpert, mas como ele já é usado na biblioteca padrão, usaremos um análogo.
#include "Strategy.mqh" class CAdvisor : public CObject { protected: CStrategy *m_strategies[]; // Array of trading strategies int m_strategiesCount;// Number of strategies public: virtual int Init(); // EA initialization method virtual void Tick(); // OnTick event handler virtual void Deinit(); // Deinitialization method void AddStrategy(CStrategy &strategy); // Strategy adding method };
Nos métodos Init() e Tick(), iteramos por todas as estratégias no array m_strategies[] e chamamos os métodos de processamento de eventos correspondentes.
void CAdvisor::Tick(void) { // Call OnTick handling for all strategies for(int i = 0; i < m_strategiesCount; i++) { m_strategies[i].Tick(); } }
No método de adição de estratégias, é exatamente isso que ocorre.
void CAdvisor::AddStrategy(CStrategy &strategy) { // Increase the strategy number counter by 1 m_strategiesCount = ArraySize(m_strategies) + 1; // Increase the size of the strategies array ArrayResize(m_strategies, m_strategiesCount); // Write a pointer to the strategy object to the last element m_strategies[m_strategiesCount - 1] = GetPointer(strategy); }
Salvaremos esse código no arquivo Advisor.mqh na pasta atual. Com base nessa classe, poderemos criar derivados que implementam maneiras específicas de gerenciar o trabalho de várias estratégias. Mas por enquanto, vamos nos limitar a essa classe base e não interferir no funcionamento das estratégias individuais.
EA com múltiplas estratégias
Para escrever um EA, basta criar um objeto global do advisor (classe CAdvisor).
No manipulador de eventos de inicialização ao anexar ao gráfico OnInit(), criaremos objetos de estratégias com os parâmetros escolhidos e os adicionaremos ao objeto advisor. Depois disso, chamamos o método Init() do objeto advisor para que todas as estratégias sejam inicializadas.
Os manipuladores de eventos OnTick() e OnDeinit() simplesmente chamam os métodos correspondentes do objeto advisor.
#include "Advisor.mqh" #include "SimpleVolumesStartegy.mqh" input double depoPart_ = 0.8; // Part of the deposit for one strategy input ulong magic_ = 27182; // Magic CAdvisor expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert.AddStrategy(...); expert.AddStrategy(...); int res = expert.Init(); // Initialization of all EA strategies return(res); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { expert.Deinit(); } //+------------------------------------------------------------------+
Agora vamos examinar mais detalhadamente a criação dos objetos das estratégias. Como cada instância da estratégia abre e gerencia suas próprias ordens e posições, elas devem ter diferentes Magic Numbers. O valor de Magic é o primeiro parâmetro do construtor das estratégias. Portanto, para garantir diferentes Magic Numbers, adicionaremos números distintos ao Magic inicial definido no parâmetro magic_.
expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 2, ...));
O segundo e o terceiro parâmetros do construtor são o símbolo e o período. Como realizamos a otimização no EURGBP e no período H1, usaremos esses valores específicos.
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, ...));
O próximo parâmetro é muito importante é o tamanho das posições abertas. Calculamos anteriormente o tamanho adequado para as duas estratégias (0.34 e 0.10). Mas esse tamanho é para trabalhar com uma retração de até 10% de $10,000 ao operar separadamente. Se duas estratégias operarem simultaneamente, a retração de uma pode se somar à retração da outra. No pior caso, para manter a retração dentro dos 10% declarados, teremos que reduzir pela metade o tamanho das posições abertas. No entanto, pode acontecer que as retrações das duas estratégias não coincidam ou até se compensem mutuamente. Nesse caso, podemos não reduzir tanto o tamanho das posições e ainda assim não exceder os 10%. Portanto, vamos fazer um multiplicador redutor como um parâmetro do EA (depoPart_), para o qual depois encontraremos o valor ideal.
Os parâmetros restantes do construtor da estratégia são os conjuntos de valores que escolhemos após a otimização do EA simples. Finalmente, obtemos:
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, NormalizeDouble(0.34 * depoPart_, 2), 130, 0.9, 1.4, 231, 3750, 50, 600, 3) ); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, NormalizeDouble(0.10 * depoPart_, 2), 159, 1.7, 0.8, 248, 3600, 495, 39000, 3) );
Salvamos o código resultante no arquivo SimpleVolumesExpert.mq5 na pasta atual.
Resultados dos testes
Antes de testar o EA combinado, lembremos que a estratégia com o primeiro conjunto de parâmetros deveria gerar um lucro de aproximadamente 91%, e com o segundo conjunto de parâmetros, 72% (para um depósito inicial de $10,000 e uma retração máxima de 10% ($1,000) com o lote ideal).
Vamos encontrar o valor ideal do parâmetro depoPart_ com base no critério de manter a retração especificada e obter os seguintes resultados.
Fig. 3. Resultado do EA combinado
O saldo ao final do período de teste foi de aproximadamente $22,400, ou seja, um lucro de 124%. Isso é mais do que obtivemos ao executar instâncias separadas dessa estratégia. Conseguimos melhorar os resultados de trading trabalhando apenas com a estratégia existente, sem fazer alterações nela.
Considerações finais
Demos apenas um pequeno passo em direção ao objetivo. Isso nos permitiu aumentar a confiança de que essa abordagem pode melhorar a qualidade do trading. No EA escrito, ainda não abordamos muitos aspectos importantes.
Por exemplo, consideramos uma estratégia muito simples que não controla o fechamento das posições, opera sem a necessidade de determinar precisamente o início da barra e não usa cálculos pesados. Para restaurar o estado após reiniciar o terminal, não é necessário tomar medidas adicionais, para isso basta contar as posições e ordens abertas, algo que o EA sabe fazer. Mas nem toda estratégia será tão simples. Além disso, o EA não pode operar em contas Netting e pode manter posições opostas abertas simultaneamente. Não consideramos a operação em diferentes símbolos. E assim por diante...
Esses aspectos devem ser considerados antes de iniciar a negociação real. Mas isso será abordado em artigos futuros.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/14026





- 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