English Русский 中文 Español Deutsch 日本語
preview
Desenvolvimento de EAs auto-otimizáveis em MQL5 (Parte 13): Introdução à teoria de controle com fatoração de matrizes

Desenvolvimento de EAs auto-otimizáveis em MQL5 (Parte 13): Introdução à teoria de controle com fatoração de matrizes

MetaTrader 5 — Exemplos |
20 2
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Muitas vezes, é difícil fazer planos para os mercados financeiros ou prever seu comportamento com antecedência. O sentimento dos investidores costuma ser instável e pode mudar rapidamente conforme o cenário global e as questões que dominam a pauta do momento. Por isso, estratégias de negociação que parecem lucrativas com base em dados históricos frequentemente apresentam resultados insatisfatórios quando aplicadas ao mercado em tempo real.

Há diversas razões que explicam esse comportamento nas aplicações de trading. No entanto, é importante compreender que, depois de desenvolvidas e implantadas, nossas aplicações mantêm o mesmo comportamento, que, em geral, não pode ser alterado sem intervenção humana. Isso significa que nossas estratégias correm o risco de repetir os mesmos erros indefinidamente, sem que possamos aproveitar as experiências malsucedidas ou aprender com os erros anteriores.

Figura 1: Arquitetura padrão utilizada para implantar aplicações de trading nos mercados financeiros

Diversas soluções já foram propostas para esse problema recorrente. No entanto, uma das soluções com enorme potencial vem da teoria de controle. Essa área se dedica principalmente a ajustar o comportamento de sistemas que operam em ambientes dinâmicos ou caóticos, buscando conduzi-los a um objetivo predefinido.

Ao enviar periodicamente ao controlador com realimentação dados sobre os resultados da nossa estratégia, o qual registra e monitora a interação da estratégia com o mercado, podemos estimar a relação entre o comportamento da estratégia e os resultados obtidos no mercado. Isso indica que o controlador com realimentação aprendeu a identificar os padrões predominantes que distinguem as operações lucrativas daquelas que geram prejuízo. Se essa estrutura realmente existir e pudermos aprender com ela, em teoria, o controlador com realimentação será capaz de ajustar a dinâmica do nosso sistema de trading e conduzi-lo à lucratividade, mesmo em meio ao caos e às constantes mudanças do mercado.

Na prática, isso mudará a estrutura de implantação da nossa estratégia em relação ao esquema apresentado na Figura 1. Na Figura 2, introduzimos uma notação simples, utilizada na literatura sobre teoria de controle, em que os sinais de entrada provenientes do mercado são representados por (M) e os sinais de saída da nossa estratégia, por (S).

Figura 2: Podemos redefinir nossa aplicação de trading utilizando uma notação abreviada para representar os dados de entrada do mercado (M) e os resultados da estratégia (S)

Na maioria das discussões anteriores sobre fatoração de matrizes, concentramos nossos esforços principalmente na construção de modelos de regressão e classificação para prever níveis futuros de preços ou alterações nos indicadores técnicos. Nesta discussão, porém, vamos explorar a teoria de controle, mais especificamente a área dos controladores com realimentação. Esse aspecto costuma receber pouca atenção nas discussões sobre aplicações de trading baseadas em métodos numéricos, embora seus fundamentos teóricos possam ser adaptados às nossas necessidades como traders.

Nosso objetivo é demonstrar que os controladores com realimentação permitem controlar sistemas de trading com elevado grau de precisão. Quando configurados adequadamente, esses controladores podem ajustar o funcionamento do sistema e mantê-lo na direção desejada, assegurando a lucratividade das operações.

Começamos com uma versão básica da nossa estratégia de negociação, que utilizava duas médias móveis: uma calculada com base nos preços máximos e outra nos preços mínimos. Ambas tinham o mesmo período, formando um canal delimitado pelas médias móveis. Quando o preço rompia o limite superior desse canal, abríamos posições compradas; quando rompia o limite inferior, abríamos posições vendidas. Essa versão inicial estabeleceu um nível de referência de rentabilidade que pretendíamos superar com o auxílio do nosso controlador com realimentação. Nossa estratégia de negociação está ilustrada na Figura 3, abaixo.

Figura 3: Visualização da nossa estratégia de negociação em funcionamento no gráfico diário do EURUSD

Inicialmente, nosso controlador com realimentação monitorou o desempenho da estratégia durante 90 dias antes de receber autorização para intervir. Ou seja, nos primeiros 90 dias, o controlador não enviava nenhum sinal ao sistema, limitando-se a coletar dados observacionais. É importante destacar que esse período de 90 dias é um parâmetro configurável, embora nem sempre seja possível determinar antecipadamente seu valor ideal. Escolhemos arbitrariamente esse intervalo por considerarmos que ele estaria alinhado aos ciclos de negócios das instituições financeiras que predominam no mercado de câmbio. Ainda assim, os leitores podem experimentar outros valores para esse parâmetro conforme julgarem conveniente.

Durante esses 90 dias, o controlador registrou diversas variáveis relevantes: as cotações Bid do mercado, o saldo da conta, o patrimônio líquido da conta, os valores dos indicadores e os tipos de posições que abrimos. Essas observações foram armazenadas em uma matriz denominada "snapshot". Em seguida, construímos um modelo linear para representar a evolução dos estados registrados nos snapshots ao longo do tempo. Em outras palavras, os primeiros 89 snapshots foram pareados com os últimos 89, permitindo que o sistema identificasse como o estado da estratégia evoluía ao longo do tempo.

Após os 90 dias, o sistema linear reuniu todos os dados sobre o estado do sistema e previu se a próxima operação resultaria em lucro ou prejuízo. Caso a previsão indicasse prejuízo, o sistema simplesmente suspendia as operações até que surgissem condições de mercado mais favoráveis.

Assim, a função do nosso controlador com realimentação é monitorar as saídas do sistema (S) e aprender uma nova função de controle (F), capaz de ajustar seu comportamento (FS) para recuperar a lucratividade, de modo que nossa estratégia não dependa exclusivamente da influência direta do mercado (M). Após os 90 dias de observação, nossa estratégia deixará de depender apenas do mercado (M), passando a ser orientada pela combinação dos dados de mercado com as recomendações do controlador com realimentação (FS + M).

Figura 4: Visualização de como o controlador com realimentação modifica o comportamento da nossa aplicação de trading após entrar em operação

Realizamos backtests dos dois sistemas utilizando cinco anos de dados diários do par EUR/USD. O controlador com realimentação aumentou a rentabilidade do sistema em 82%. Inicialmente, nossa estratégia básica gerou um lucro de US$ 134. Após a implementação do sistema de controle com realimentação, esse valor subiu para US$ 245. Além disso, o sistema aprimorado alcançou esse resultado com menos operações: o número de posições caiu de 180 para 152, representando uma redução de 15% na atividade de negociação. Isso significa que nosso novo sistema conseguiu gerar um lucro maior com menor risco, uma característica extremamente desejável em qualquer aplicação de trading.

O nível geral de risco também diminuiu. Na versão básica, o prejuízo total foi de US$ 1.092, enquanto no sistema com realimentação esse valor caiu para US$ 838, mantendo o lucro bruto praticamente inalterado. Esse é outro resultado bastante desejável.

O índice de Sharpe do sistema básico foi de 0,34. Após a implementação do controlador com realimentação, esse indicador subiu para 0,68, uma melhoria de 100%, um resultado excepcional em um mercado tão complexo quanto o EUR/USD. A proporção de operações lucrativas também aumentou cerca de 6%, passando de 53,89% para 57,24%. Consequentemente, a proporção de operações com prejuízo diminuiu na mesma medida, indicando que o controlador com realimentação conseguiu identificar os padrões predominantes que distinguem as operações lucrativas das operações com prejuízo. Por fim, o resultado esperado por operação aumentou de 0,75 para 1,61, representando uma melhoria de 114%.

Fica evidente que os controladores com realimentação podem desempenhar um papel fundamental em aplicações de trading baseadas em métodos numéricos. Quando configurados adequadamente, eles podem aprender a intervir de maneira confiável para evitar que a estratégia repita os mesmos erros. A construção de um controlador diretamente a partir de dados observacionais sobre o estado do sistema é uma técnica conhecida como identificação de sistemas.

Essa família de algoritmos foi desenvolvida originalmente por engenheiros especializados em dinâmica dos fluidos. Esses profissionais frequentemente precisavam projetar sistemas de controle para os componentes das asas das aeronaves, mas não existem fórmulas bem definidas para prever ou corrigir os efeitos da turbulência. Por isso, era necessário encontrar maneiras de calcular as ações de controle ideais com base nas observações do comportamento do sistema e das ações que provocam esse comportamento.

Ao longo desta discussão, construímos um modelo linear do sistema. Portanto, essa abordagem é mais precisamente denominada identificação de sistemas lineares. Os resultados apresentados aqui nos incentivam a, no futuro, dedicar mais tempo à definição de novas tarefas para o controlador com realimentação e ao estudo de métodos de identificação de sistemas não lineares. Vamos começar.


Primeiros passos com MQL5

Como na maioria das nossas aplicações de trading, começamos definindo os parâmetros importantes do sistema. Nossa estratégia básica precisa de apenas uma definição, que estabelece o período da média móvel.

//+------------------------------------------------------------------+
//|                                         Closed Loop Feedback.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System definitions                                               |
//+------------------------------------------------------------------+
#define MA_PERIOD 10

Outro aspecto importante do projeto do sistema é a definição das variáveis globais. Neste caso específico, precisaremos apenas de algumas variáveis globais relacionadas aos indicadores técnicos utilizados pela estratégia. Teremos, mais precisamente, handles para os dois indicadores de média móvel e outro para o indicador ATR, utilizado na definição dos níveis de Stop Loss. Cada um desses indicadores também precisará de seu próprio buffer.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
int    ma_h_handler,ma_l_handler,atr_handler;
double ma_h[],ma_l[],atr[];

Praticamente todas as aplicações de trading que desenvolvemos ao longo destas discussões possuem dependências, já que nem sempre escrevemos todo o código do zero. Nesta aplicação, utilizamos a biblioteca padrão e duas bibliotecas próprias: uma para detectar a formação de novas velas e outra para obter informações essenciais sobre os preços, como as cotações Bid e Ask.

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>

CTrade      Trade;
Time        *DailyTimeHandler;
TradeInfo   *TradeInfoHandler;

Na inicialização do sistema, começamos criando instâncias das nossas classes personalizadas. Também criamos novas instâncias dos indicadores e obtemos seus respectivos handles.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   DailyTimeHandler = new Time(Symbol(),PERIOD_D1);
   TradeInfoHandler = new TradeInfo(Symbol(),PERIOD_D1);
   ma_h_handler = iMA(Symbol(),PERIOD_D1,MA_PERIOD,0,MODE_EMA,PRICE_HIGH);
   ma_l_handler = iMA(Symbol(),PERIOD_D1,MA_PERIOD,0,MODE_EMA,PRICE_LOW);
   atr_handler      = iATR(Symbol(),PERIOD_D1,14);
//---
   return(INIT_SUCCEEDED);
  }

Quando a aplicação deixa de ser utilizada, para gerenciar a memória de maneira eficiente, excluímos os objetos dinâmicos que criamos anteriormente e liberamos os recursos dos indicadores que não são mais necessários, para uma gestão segura dos recursos do sistema.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   delete DailyTimeHandler;
   delete TradeInfoHandler;
   IndicatorRelease(ma_h_handler);
   IndicatorRelease(ma_l_handler);
  }

A cada nova cotação recebida, a função OnTick é chamada. Essa função utiliza nossa biblioteca própria para verificar se uma nova vela diária foi formada. Quando uma nova vela é detectada, atualizamos os valores dos indicadores armazenados nos buffers e guardamos uma cópia do preço de fechamento atual. Se não houver posições abertas, aplicamos nossas regras de negociação para decidir entre comprar e vender: se o preço de fechamento estiver acima do limite superior do canal de médias móveis, abrimos uma posição comprada; se estiver abaixo do limite inferior, abrimos uma posição vendida.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(DailyTimeHandler.NewCandle())
     {
      CopyBuffer(ma_h_handler,0,0,1,ma_h);
      CopyBuffer(ma_l_handler,0,0,1,ma_l);
      CopyBuffer(atr_handler,0,0,1,atr);

      double c = iClose(Symbol(),PERIOD_D1,0);

      if(PositionsTotal() == 0)
        {
         if(c > ma_h[0])
            Trade.Buy(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetAsk(),(TradeInfoHandler.GetBid()-(atr[0]*2)),(TradeInfoHandler.GetBid()+(atr[0]*2)),"");

         if(c < ma_l[0])
            Trade.Sell(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetBid(),(TradeInfoHandler.GetAsk()+(atr[0]*2)),(TradeInfoHandler.GetAsk()-(atr[0]*2)),"");
        }
     }
  }

Por fim, após a conclusão de todas as operações, fazemos a limpeza necessária, removendo as definições do sistema introduzidas no início da aplicação. Com isso, temos a versão básica do nosso sistema de trading.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_PERIOD
//+------------------------------------------------------------------+

Agora podemos testar nossa aplicação com dados históricos. Para esse teste, selecionamos cinco anos de dados do par EUR/USD e executamos nossa aplicação básica.

Figura 5: Backtest da nossa aplicação de trading com base em cinco anos de dados históricos do par EUR/USD

Para tornar o teste mais realista, também introduzimos atrasos aleatórios na execução, pois as condições reais de mercado são imprevisíveis, e esses atrasos ajudam a simular parte dessa incerteza.

Figura 6: Seleção de condições realistas para o backtest da nossa análise

Em seguida, podemos gerar um relatório detalhado sobre o desempenho da aplicação. Na introdução deste artigo, já apresentei uma visão geral dos resultados do backtest. Aqui, observamos que o índice de Sharpe do sistema é baixo, de apenas 0,34, e que o prejuízo médio por operação supera o lucro médio. É importante reconhecer que, mesmo na versão aprimorada da nossa aplicação, o prejuízo médio por operação continuou ligeiramente superior ao lucro médio. No entanto, nosso sistema de controle em malha fechada conseguiu reduzir essa diferença.

Figura 7: Estatísticas detalhadas do desempenho da nossa aplicação de trading

Além disso, ao analisar a curva de capital do sistema básico, percebemos que ela apresenta uma tendência de alta. No entanto, essa trajetória é pouco consistente: frequentemente, a curva oscila durante meses dentro de uma faixa estreita, alternando períodos de lucro e prejuízo, o que faz com que a estratégia permaneça praticamente estagnada por longos intervalos. É justamente essa instabilidade que pretendemos eliminar utilizando o controlador com realimentação para ajustar o comportamento do sistema.

Figura 8: A curva de capital gerada pela nossa aplicação de trading apresenta um comportamento insatisfatório devido à sua instabilidade


Aprimorando nossos resultados iniciais

Agora estamos prontos para otimizar nossos resultados iniciais com o auxílio do controlador com realimentação. Para evitar repetições desnecessárias, omiti os trechos de código que permaneceram inalterados e vou me concentrar principalmente nas modificações realizadas na versão original da aplicação.

Como podemos perceber de imediato, aumentou o número de definições do sistema necessárias para nossa aplicação. Enquanto a versão inicial exigia apenas uma definição, a versão atualizada depende de quatro. As três novas definições correspondem a: (1) o número total de observações que desejamos coletar antes que o controlador com realimentação comece a operar em tempo real; (2) o número de atributos que pretendemos monitorar, que neste exemplo são doze atributos relacionados ao desempenho da nossa estratégia de negociação; e (3) um vetor que representa o tipo de posição que mantemos aberta no momento.

//+------------------------------------------------------------------+
//|                                         Closed Loop Feedback.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System definitions                                               |
//+------------------------------------------------------------------+
#define MA_PERIOD    10
#define OBSERVATIONS 90
#define FEATURES     12
#define ACCOUNT_STATES 3

À medida que nossa aplicação se expande, suas variáveis globais também se tornam mais complexas. Agora precisaremos de matrizes para armazenar os snapshots, vetores para registrar as previsões geradas pelo sistema linear e flags booleanas para determinar quando o sistema está pronto para passar da fase de observação à execução efetiva de operações.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
int    ma_h_handler,ma_l_handler,atr_handler,scenes,b_matrix_scenes;
double ma_h[],ma_l[],atr[];
matrix snapshots,OB_SIGMA,OB_VT,OB_U,b_vector,b_matrix;
vector S,prediction;
vector account_state;
bool predict,permission;

Durante a inicialização, executamos uma série de etapas de pré-processamento. As primeiras já são conhecidas: configuramos os indicadores técnicos. Em seguida, inicializamos a matriz de snapshots, com 12 linhas e 90 colunas. Neste momento, todos os seus elementos são definidos como zero. O flag "permissão" é inicializado como true, indicando que o sistema começa com autorização para operar. No entanto, após o período de observação de 90 dias, esse flag passa a ser false, enquanto o flag "predict" é definido como true. A partir desse momento, o sistema deixa de executar operações sem restrições e precisa obter a autorização do modelo linear antes de realizar qualquer operação.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   DailyTimeHandler  = new Time(Symbol(),PERIOD_D1);
   TradeInfoHandler  = new TradeInfo(Symbol(),PERIOD_D1);
   ma_h_handler      = iMA(Symbol(),PERIOD_D1,MA_PERIOD,0,MODE_EMA,PRICE_HIGH);
   ma_l_handler      = iMA(Symbol(),PERIOD_D1,MA_PERIOD,0,MODE_EMA,PRICE_LOW);
   atr_handler       = iATR(Symbol(),PERIOD_D1,14);
   snapshots         = matrix::Ones(FEATURES,OBSERVATIONS);
   scenes            = 0;
   b_matrix_scenes   = 0;
   account_state     = vector::Zeros(3);
   b_matrix          = matrix::Zeros(1,1);
   prediction        = vector::Zeros(2);
   predict           = false;
   permission        = true;
//---
   return(INIT_SUCCEEDED);
  }

A função OnTick, responsável pelo tratamento dos eventos de tick, passou por mudanças significativas em relação à versão básica do sistema. Uma das principais alterações é que o estado da conta agora é armazenado em um vetor de três elementos. Esse vetor utiliza a codificação one-hot: se houver uma posição comprada aberta, o primeiro elemento recebe o valor 1; se houver uma posição vendida, o segundo elemento recebe 1; e, se não houver nenhuma posição aberta, o terceiro elemento recebe 1. Isso permite fornecer ao sistema linear as informações categóricas em um formato estruturado.

A lógica de negociação permanece a mesma: se o preço de fechamento estiver acima do limite superior do canal de médias móveis, consideramos uma compra; se estiver abaixo do limite inferior, consideramos uma venda. Durante os primeiros 90 dias, enquanto o flag de previsão estiver definido como false, o sistema terá autorização irrestrita para operar. Após esse período, todas as decisões de negociação precisarão ser confirmadas pelo sistema linear. Assim que o número necessário de observações for coletado, a matriz de snapshots será redimensionada para incorporar os novos dados, e o modelo linear identificado passará a filtrar nossas decisões de negociação antes da execução das operações no mercado.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(DailyTimeHandler.NewCandle())
     {
      CopyBuffer(ma_h_handler,0,0,1,ma_h);
      CopyBuffer(ma_l_handler,0,0,1,ma_l);
      CopyBuffer(atr_handler,0,0,1,atr);
      double c = iClose(Symbol(),PERIOD_D1,0);

      if(PositionsTotal() == 0)
        {
         account_state = vector::Zeros(ACCOUNT_STATES);

         if(c > ma_h[0])
           {
            if(!predict)
              {
               if(permission)
                  Trade.Buy(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetAsk(),(TradeInfoHandler.GetBid()-(atr[0]*2)),(TradeInfoHandler.GetBid()+(atr[0]*2)),"");
              }

            account_state[0] = 1;
           }

         else
            if(c < ma_l[0])
              {
               if(!predict)
                 {
                  if(permission)
                     Trade.Sell(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetBid(),(TradeInfoHandler.GetAsk()+(atr[0]*2)),(TradeInfoHandler.GetAsk()-(atr[0]*2)),"");
                 }

               account_state[1] = 1;
              }

            else
              {
               account_state[2] = 1;
              }
        }

      if(scenes < OBSERVATIONS)
        {
         take_snapshots();
        }

      else
        {
         matrix temp;
         temp.Assign(snapshots);
         snapshots = matrix::Ones(FEATURES,scenes+1);

         //--- The first row is the intercept and must be full of ones
         for(int i=0;i<FEATURES;i++)
            snapshots.Row(temp.Row(i),i);

         take_snapshots();
         fit_snapshots();

         predict = true;
         permission = false;
        }
      scenes++;
     }
  }

Nessa etapa, o sistema registra um conjunto completo de variáveis relevantes: os preços de abertura, máxima, mínima e fechamento do dia anterior, os valores dos indicadores técnicos, as informações da conta e o vetor de estado da conta codificado por one-hot. Com essas variáveis, o sistema pode aprender a modelar a relação entre nossa estratégia e o mercado. Nosso objetivo já não se limita a prever os níveis futuros de preços. Agora, queremos prever o saldo futuro da nossa conta de trading. Se essa relação realmente existir e puder ser identificada, esperamos que nosso controlador com realimentação aprenda a ajustar o comportamento da estratégia sem intervenção humana.

//+------------------------------------------------------------------+
//| Record the current state of our system                           |
//+------------------------------------------------------------------+
void take_snapshots(void)
  {
   snapshots[1,scenes] = iOpen(Symbol(),PERIOD_D1,1);
   snapshots[2,scenes] = iHigh(Symbol(),PERIOD_D1,1);
   snapshots[3,scenes] = iLow(Symbol(),PERIOD_D1,1);
   snapshots[4,scenes] = iClose(Symbol(),PERIOD_D1,1);
   snapshots[5,scenes] = AccountInfoDouble(ACCOUNT_BALANCE);
   snapshots[6,scenes] = AccountInfoDouble(ACCOUNT_EQUITY);
   snapshots[7,scenes] = ma_h[0];
   snapshots[8,scenes] = ma_l[0];
   snapshots[9,scenes] = account_state[0];
   snapshots[10,scenes] = account_state[1];
   snapshots[11,scenes] = account_state[2];
  }
//+------------------------------------------------------------------+

Em seguida, resolvemos o sistema linear utilizando duas matrizes: X, que contém os dados de entrada, e y, que contém os valores-alvo. Trata-se de um sistema com múltiplas saídas, capaz de prever vários resultados simultaneamente. A matriz y é deslocada em um passo de tempo, de modo que os dados de entrada correspondam aos 89 snapshots anteriores, enquanto as saídas correspondem aos 89 snapshots seguintes. À medida que novos dados são recebidos, esse laço se adapta naturalmente ao crescimento do conjunto de dados, acompanhando o número total de cenas transcorridas.

Uma cena corresponde ao intervalo de tempo entre dois snapshots consecutivos. Como nesta discussão coletamos snapshots diariamente, o número de cenas também pode ser ajustado conforme as preferências do leitor. Utilizando a pseudoinversa da matriz, calculada pela função PInv() do MQL5, encontramos os coeficientes ótimos que permitem mapear X em y. Essa função foi explicada detalhadamente em artigos anteriores da nossa série. Os leitores que ainda não conhecem a importância da PInv() podem consultar o material indicado aqui para compreender melhor seu funcionamento, pois se trata de uma ferramenta poderosa que utilizamos em nossa abordagem.

Após a configuração, o sistema exibe os snapshots, os dados de entrada e os valores-alvo, os coeficientes obtidos durante o treinamento e suas previsões. Em seguida, analisamos essas previsões em tempo real. Se o modelo prever um aumento no saldo da conta, a operação será autorizada. A autorização também será concedida se o modelo prever um impulso de alta na média móvel dos preços máximos quando estivermos considerando abrir uma posição comprada. Da mesma forma, se o modelo prever um movimento de baixa na média móvel dos preços mínimos e estivermos considerando abrir uma posição vendida, a operação será autorizada. Em todos os outros casos, o sistema negará a autorização.

Por fim, se a operação for autorizada e não houver posições abertas, o sistema executará a operação pretendida. Ao concluir esse procedimento, o programa exibe o saldo atual, o saldo previsto e a informação sobre a autorização da operação. Com isso, concluímos as modificações necessárias para observar uma melhoria nos resultados da nossa estratégia. O código que desenvolvemos não fornece instruções explícitas ao controlador sobre quando comprar ou vender. Em vez disso, definimos um conjunto de ações que ele pode executar com base nas conclusões extraídas dos dados acumulados.

//+------------------------------------------------------------------+
//| Fit our linear model to our collected snapshots                  |
//+------------------------------------------------------------------+
void fit_snapshots(void)
  {
   matrix X,y;
   X.Reshape(FEATURES,scenes);
   y.Reshape(FEATURES-1,scenes);

   for(int i=0;i<scenes;i++)
     {
      X[0,i] = snapshots[0,i];
      X[1,i] = snapshots[1,i];
      X[2,i] = snapshots[2,i];
      X[3,i] = snapshots[3,i];
      X[4,i] = snapshots[4,i];
      X[5,i] = snapshots[5,i];
      X[6,i] = snapshots[6,i];
      X[7,i] = snapshots[7,i];
      X[8,i] = snapshots[8,i];
      X[9,i] = snapshots[9,i];
      X[10,i] = snapshots[10,i];
      X[11,i] = snapshots[11,i];

      y[0,i] = snapshots[1,i+1];
      y[1,i] = snapshots[2,i+1];
      y[2,i] = snapshots[3,i+1];
      y[3,i] = snapshots[4,i+1];
      y[4,i] = snapshots[5,i+1];
      y[5,i] = snapshots[6,i+1];
      y[6,i] = snapshots[7,i+1];
      y[7,i] = snapshots[8,i+1];
      y[8,i] = snapshots[9,i+1];
      y[9,i] = snapshots[10,i+1];
      y[10,i] = snapshots[11,i+1];
     }

//--- Find optimal solutions
   b_vector = y.MatMul(X.PInv());
   Print("Day Number: ",scenes+1);
   Print("Snapshot");
   Print(snapshots);
   Print("Input");
   Print(X);
   Print("Target");
   Print(y);
   Print("Coefficients");
   Print(b_vector);
   Print("Prediciton");
   Print(y.Col(scenes-1));
   prediction = b_vector.MatMul(snapshots.Col(scenes-1));

   if(prediction[4] > AccountInfoDouble(ACCOUNT_BALANCE))
      permission = true;

   else
      if((account_state[0] == 1) && (prediction[6] > ma_h[0]))
         permission = true;

      else
         if((account_state[1] == 1) && (prediction[7] < ma_l[0]))
            permission = true;

         else
            permission = false;

   if(permission)
     {
      if(PositionsTotal() == 0)
        {
         if(account_state[0] == 1)
            Trade.Buy(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetAsk(),(TradeInfoHandler.GetBid()-(atr[0]*2)),(TradeInfoHandler.GetBid()+(atr[0]*2)),"");

         else
            if(account_state[1] == 1)
               Trade.Sell(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetBid(),(TradeInfoHandler.GetAsk()+(atr[0]*2)),(TradeInfoHandler.GetAsk()-(atr[0]*2)),"");
        }
     }

   Print("Current Balabnce: ",AccountInfoDouble(ACCOUNT_BALANCE)," Predicted Balance: ",prediction[4]," Permission: ",permission);
  }
//+------------------------------------------------------------------+

Com essa estrutura pronta, podemos realizar o backtest da nossa aplicação utilizando as mesmas condições históricas do teste anterior. Para evitar repetições, não apresentarei novamente os parâmetros do teste, mas vale destacar que são idênticos aos utilizados na Figura 6.

Figura 9: Preparação para testar a versão aprimorada da nossa estratégia de negociação

Uma análise detalhada revela diferenças expressivas entre o sistema básico e aquele que utiliza realimentação. O lucro líquido aumentou significativamente, enquanto o lucro bruto permaneceu praticamente inalterado. Isso indica que o controlador está conseguindo lidar de maneira eficaz com as operações que geram prejuízo. O índice de Sharpe também apresentou um aumento expressivo, e o lucro médio das operações vencedoras e o prejuízo médio das operações perdedoras agora apresentam magnitudes praticamente iguais, representando uma melhoria considerável em relação aos resultados iniciais.

Figura 10: Análise detalhada dos resultados da nossa aplicação de trading, demonstrando as melhorias alcançadas

A curva de capital apresenta menor volatilidade e uma tendência de alta mais acentuada e consistente. Era exatamente esse o resultado que esperávamos alcançar ao incorporar um sistema de controle com realimentação à nossa estratégia de negociação.

Figura 11: A curva de capital gerada pela nossa estratégia de negociação aprimorada apresenta menor volatilidade ao longo do tempo e uma rentabilidade mais estável.

Também incluí uma captura de tela dos snapshots registrados pelo sistema durante o backtest. Infelizmente, não foi possível mostrar os 12 atributos em uma única imagem. No entanto, o leitor pode observar que a última linha da matriz de snapshots contém o mesmo saldo inicial de US$ 500 com o qual nossa estratégia de negociação simulada começou a operar (veja a Figura 6). A partir daí, acompanhamos como as condições de mercado e as decisões tomadas pela nossa estratégia de negociação afetam o patrimônio líquido e o saldo da conta.

Figura 12: Indicadores de desempenho do nosso sistema que monitoramos para acompanhar e ajustar nossa estratégia

Uma captura do log de resultados confirma esse comportamento. Por exemplo, no 1645º dia, o flag de permissão foi definido como false porque o sistema linear previa um prejuízo e, por isso, bloqueou a operação.

Figura 13: Nosso controlador do sistema linear negou à estratégia de negociação a autorização para operar após identificar condições desfavoráveis de mercado

Já no dia seguinte, quando a previsão indicava lucro, o sistema autorizou e executou a operação. Em linhas gerais, esse é o funcionamento da aplicação atualizada, do início ao fim. Isso demonstra que os modelos preditivos têm diversas aplicações além da simples previsão de preços. Podemos utilizar esses modelos treinados para verificar se existe alguma estrutura nos dados que permita identificar os padrões que determinam quais operações geram lucro e quais resultam em prejuízo. Dessa forma, podemos aprender a evitar a repetição dos erros do passado.

Figura 14: Quando as condições de mercado parecem favoráveis à nossa estratégia, o controlador com realimentação autoriza a continuidade das operações



Conclusão

Em conclusão, os controladores com realimentação permitem desenvolver sistemas de trading que não apenas geram lucro, mas também são capazes de se adaptar às mudanças nas condições de mercado. Ao analisar sua própria atuação, esses sistemas podem nos ajudar a evitar a repetição de erros anteriores e a manter a eficiência mesmo diante das mudanças no cenário de mercado. Nossos resultados demonstram um aumento expressivo da rentabilidade e da eficiência, além da redução do risco, comprovando o valor prático da teoria de controle no trading. Olhando para o futuro, este estudo nos incentiva a ir além dos modelos lineares e explorar controladores não lineares com realimentação, capazes de representar padrões ainda mais complexos do comportamento do mercado. Com esses avanços, poderemos continuar melhorando a estabilidade e a rentabilidade dos nossos sistemas, mesmo em condições de incerteza.

No entanto, antes de avançarmos além do nosso sistema linear, ainda podemos implementar diversas melhorias importantes. Avaliar a eficácia de cada uma dessas possíveis melhorias em sistemas lineares simples nos fornecerá uma referência confiável para qualquer outro sistema não linear que venhamos a desenvolver.

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/19132

Arquivos anexados |
Últimos Comentários | Ir para discussão (2)
Majeed Odubela
Majeed Odubela | 20 set. 2025 em 14:12
Você não anexou os arquivos de inclusão volatilityDoctor/Time..Trade. Seu envio não pode ser testado sem esses dois arquivos de inclusão.
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 23 set. 2025 em 14:17
Majeed Odubela #:
Você não anexou os arquivos de inclusão volatilityDoctor/Time..Trade. Seu envio não pode ser testado sem esses dois arquivos de inclusão.
Majeed, lamento saber do que aconteceu com você.

No entanto, você também deve entender que este artigo faz parte de uma série maior de artigos relacionados que se complementam.

A classe que você está procurando foi criada e anexada do zero, em um artigo anterior.
Sincronização de gráficos para facilitar a análise técnica Sincronização de gráficos para facilitar a análise técnica
A sincronização de gráficos facilita a análise técnica ao permitir que objetos gráficos, como linhas de tendência, retângulos e indicadores, sejam exibidos de maneira uniforme em todos os timeframes de um mesmo símbolo. Ações como rolagem, zoom ou mudança de instrumento são reproduzidas em todos os gráficos sincronizados, facilitando a visualização e a comparação do mesmo contexto em diferentes timeframes.
Introdução à linguagem MQL5 (Parte 20): Introdução aos padrões harmônicos Introdução à linguagem MQL5 (Parte 20): Introdução aos padrões harmônicos
Neste artigo, exploraremos os fundamentos dos padrões harmônicos, sua estrutura e suas aplicações no trading. Você conhecerá as retrações e extensões de Fibonacci e aprenderá como implementar a detecção de padrões harmônicos em MQL5, estabelecendo a base para desenvolver ferramentas de trading e EAs mais avançados.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Arbitragem estatística com ações cointegradas (Parte 3): Configuração do banco de dados Arbitragem estatística com ações cointegradas (Parte 3): Configuração do banco de dados
Neste artigo, apresentamos um exemplo de implementação de um serviço em MQL5 para atualizar um banco de dados recém-criado, que será utilizado como fonte de dados para a análise e a negociação de uma cesta de ações cointegradas. Explicamos detalhadamente a lógica de modelagem do banco de dados e fornecemos uma descrição da estrutura dos dados (data dictionary) como material de referência. Também disponibilizamos scripts em MQL5 e Python para criar o banco de dados, inicializar seu esquema e carregar os dados de mercado.