Guia Prático do MQL5: Teste de estresse de uma estratégia de negociação utilizando os símbolos personalizados

7 janeiro 2020, 13:58
Denis Kirichenko
0
1 461

Introdução

Algum tempo atrás, um novo recurso foi adicionado a plataforma MetaTrader 5: a possibilidade de criação dos símbolos personalizados. Agora, os traders algorítmicos podem, em alguns casos, atuar como corretoras, já que não há a necessidade de servidores de negociação e podendo controlar totalmente o ambiente de negociação. No entanto, "alguns casos" aqui referem-se apenas ao modo de teste. Obviamente, as ordens de símbolos personalizados não podem ser executadas por corretoras. No entanto, essa funcionalidade permite que os traders algorítmicos testem suas estratégias de negociação com mais cuidado.

Neste artigo, nós criaremos as condições para esses testes. Vamos começar com a classe de símbolo personalizado.


1. A classe de símbolo personalizado CiCustomSymbol

A Biblioteca padrão fornece uma classe para acesso simplificado às propriedades do símbolo. É a classe CSymbolInfo. De fato, a classe realiza uma função intermediária: mediante solicitação do usuário, a classe se comunica com o servidor e recebe dele uma resposta na forma de um valor da propriedade solicitada.

Nosso objetivo é criar uma classe análoga para lidar com o símbolo personalizado. Além disso, essa funcionalidade da classe será ainda mais ampla, pois nós precisamos adicionar a criação, exclusão e alguns outros métodos. Por outro lado, nós não usaremos os métodos associados a conexão com o servidor. Isso inclui as funções Refresh(), IsSynchronized() etc.

Para criar um ambiente de negociação, esta classe encapsula as funções padrão para trabalhar com os símbolos personalizados.

A estrutura da declaração de classe é exibida abaixo.

//+------------------------------------------------------------------+
//| Class CiCustomSymbol.                                            |
//| Purpose: Base class for a custom symbol.                         |
//+------------------------------------------------------------------+
class CiCustomSymbol : public CObject
  {
   //--- === Data members === ---
private:
   string            m_name;
   string            m_path;
   MqlTick           m_tick;
   ulong             m_from_msc;
   ulong             m_to_msc;
   uint              m_batch_size;
   bool              m_is_selected;
   //--- === Methods === ---
public:
   //--- constructor/destructor
   void              CiCustomSymbol(void);
   void             ~CiCustomSymbol(void) {};
   //--- create/delete
   int               Create(const string _name,const string _path="",const string _origin_name=NULL,
                            const uint _batch_size=1e6,const bool _is_selected=false);
   bool              Delete(void);
   //--- methods of access to protected data
   string            Name(void) const { return(m_name); }
   bool              RefreshRates(void);
   //--- fast access methods to the integer symbol properties
   bool              Select(void) const;
   bool              Select(const bool select);
   //--- service methods
   bool              Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0);
   bool              LoadTicks(const string _src_file_name);
   bool              ChangeSpread(const uint _spread_size,const uint _spread_markup=0,
                                  const ENUM_SPREAD_BASE _spread_base=SPREAD_BASE_BID);
   //--- API
   bool              SetProperty(ENUM_SYMBOL_INFO_DOUBLE _property,double _val) const;
   bool              SetProperty(ENUM_SYMBOL_INFO_INTEGER _property,long _val) const;
   bool              SetProperty(ENUM_SYMBOL_INFO_STRING _property,string _val) const;
   double            GetProperty(ENUM_SYMBOL_INFO_DOUBLE _property) const;
   long              GetProperty(ENUM_SYMBOL_INFO_INTEGER _property) const;
   string            GetProperty(ENUM_SYMBOL_INFO_STRING _property) const;
   bool              SetSessionQuote(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index,
                                     const datetime _from,const datetime _to);
   bool              SetSessionTrade(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index,
                                     const datetime _from,const datetime _to);
   int               RatesDelete(const datetime _from,const datetime _to);
   int               RatesReplace(const datetime _from,const datetime _to,const MqlRates &_rates[]);
   int               RatesUpdate(const MqlRates &_rates[]) const;
   int               TicksAdd(const MqlTick &_ticks[]) const;
   int               TicksDelete(const long _from_msc,long _to_msc) const;
   int               TicksReplace(const MqlTick &_ticks[]) const;
   //---
private:
   template<typename PT>
   bool              CloneProperty(const string _origin_symbol,const PT _prop_type) const;
   int               CloneTicks(const MqlTick &_ticks[]) const;
   int               CloneTicks(const string _origin_symbol) const;
  };
//+------------------------------------------------------------------+

Vamos começar com os métodos que criarão um símbolo personalizado com todos os recursos a partir do símbolo selecionado.


1.1 O método CiCustomSymbol::Create()

Para usar todas as possibilidades de símbolos personalizados, nós precisamos criá-lo ou garantir que ele tenha sido criado. 

//+------------------------------------------------------------------+
//| Create a custom symbol                                           |
//| Codes:                                                           |
//|       -1 - failed to create;                                     |
//|        0 - a symbol exists, no need to create;                   |
//|        1 - successfully created.                                 |
//+------------------------------------------------------------------+
int CiCustomSymbol::Create(const string _name,const string _path="",const string _origin_name=NULL,
                           const uint _batch_size=1e6,const bool _is_selected=false)
  {
   int res_code=-1;
   m_name=m_path=NULL;
   if(_batch_size<1e2)
     {
      ::Print(__FUNCTION__+": a batch size must be greater than 100!");
     }
   else
     {
      ::ResetLastError();
      //--- attempt to create a custom symbol
      if(!::CustomSymbolCreate(_name,_path,_origin_name))
        {
         if(::SymbolInfoInteger(_name,SYMBOL_CUSTOM))
           {
            ::PrintFormat(__FUNCTION__+": a custom symbol \"%s\" already exists!",_name);
            res_code=0;
           }
         else
           {
            ::PrintFormat(__FUNCTION__+": failed to create a custom symbol. Error code: %d",::GetLastError());
           }
        }
      else
         res_code=1;
      if(res_code>=0)
        {
         m_name=_name;
         m_path=_path;
         m_batch_size=_batch_size;
         //--- if the custom symbol must be selected in the "Market Watch"
         if(_is_selected)
           {
            if(!this.Select())
               if(!this.Select(true))
                 {
                  ::PrintFormat(__FUNCTION__+": failed to set the \"Market Watch\" symbol flag. Error code: %d",::GetLastError());
                  return false;
                 }
           }
         else
           {
            if(this.Select())
               if(!this.Select(false))
                 {
                  ::PrintFormat(__FUNCTION__+": failed to unset the \"Market Watch\" symbol flag. Error code: %d",::GetLastError());
                  return false;
                 }
           }
         m_is_selected=_is_selected;
        }
     }
//---
   return res_code;
  }
//+------------------------------------------------------------------+

O método retornará um valor em formato de código numérico:

  • -1 — erro ao criar um símbolo;
  • 0  — o símbolo foi criado anteriormente;
  • 1  — o símbolo foi criado com sucesso durante a chamada do método atual.

Aqui estão algumas palavras sobre os parâmetros _batch_size e _is_selected.

O primeiro parâmetro (_batch_size) define o tamanho do lote, que será usado para carregar os ticks. Os ticks serão carregados em lotes: os dados são lidos primeiro para um array auxiliar; depois que o array é preenchido, os dados são carregados no banco de dados de ticks do símbolo personalizado (histórico de ticks). Por um lado, com essa abordagem, você não precisa criar um array enorme; por outro lado, não há necessidade de atualizar o banco de dados de ticks com muita frequência. O tamanho padrão do array auxiliar de ticks é de 1 milhão.

O segundo parâmetro (_is_selected) determina se nós iremos escrever os ticks diretamente no banco de dados ou se eles serão adicionados primeiro à janela Observação do Mercado.

Como exemplo, vamos executar o script TestCreation.mql5, que cria um símbolo personalizado.

No Diário nós podemos ver o código de retorno da chamada para este método.

2019.08.11 12:34:08.055 TestCreation (EURUSD,M1) A custom symbol "EURUSD_1" creation has returned the code: 1

Para mais detalhes relacionados à criação de símbolos personalizados, leia a Documentação.


1.2 O método CiCustomSymbol::Delete()

Este método tentará excluir um símbolo personalizado. Antes de excluir o símbolo, o método tentará removê-lo da janela Observação de Mercado. Em caso de falha, o procedimento de exclusão será interrompido.

//+------------------------------------------------------------------+
//| Delete                                                           |
//+------------------------------------------------------------------+
bool CiCustomSymbol::Delete(void)
  {
   ::ResetLastError();
   if(this.Select())
      if(!this.Select(false))
        {
         ::PrintFormat(__FUNCTION__+": failed to set the \"Market Watch\" symbol flag. Error code: %d",::GetLastError());
         return false;
        }
   if(!::CustomSymbolDelete(m_name))
     {
      ::PrintFormat(__FUNCTION__+": failed to delete the custom symbol \"%s\". Error code: %d",m_name,::GetLastError());
      return false;
     }
//---
   return true;
  }
//+------------------------------------------------------------------+

Como exemplo, vamos começar um script simples TestDeletion.mql5, que exclui um símbolo personalizado. Se for bem-sucedido, o log correspondente será adicionado ao Diário.

2019.08.11 19:13:59.276 TestDeletion (EURUSD,M1) A custom symbol "EURUSD_1" has been successfully deleted.



1.3 O método CiCustomSymbol::Clone()

Este método executa a clonagem: com base no símbolo selecionado, ele determina as propriedades para o símbolo personalizado atual. Simplificando, nós recebemos o valor da propriedade do símbolo original e o copiamos para outro símbolo. O usuário também pode definir a clonagem do histórico de ticks. Para fazer isso, você precisa definir o intervalo de tempo.

//+------------------------------------------------------------------+
//| Clone a symbol                                                   |
//+------------------------------------------------------------------+
bool CiCustomSymbol::Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0)
  {
   if(!::StringCompare(m_name,_origin_symbol))
     {
      ::Print(__FUNCTION__+": the origin symbol name must be different!");
      return false;
     }
   ::ResetLastError();
//--- if to load history
   if(_to_msc>0)
     {
      if(_to_msc<_from_msc)
        {
         ::Print(__FUNCTION__+": wrong settings for a time interval!");
         return false;
        }
      m_from_msc=_from_msc;
      m_to_msc=_to_msc;
     }
   else
      m_from_msc=m_to_msc=0;
//--- double
   ENUM_SYMBOL_INFO_DOUBLE dbl_props[]=
     {
      SYMBOL_MARGIN_HEDGED,
      SYMBOL_MARGIN_INITIAL,
      SYMBOL_MARGIN_MAINTENANCE,
      SYMBOL_OPTION_STRIKE,
      SYMBOL_POINT,
      SYMBOL_SESSION_PRICE_LIMIT_MAX,
      SYMBOL_SESSION_PRICE_LIMIT_MIN,
      SYMBOL_SESSION_PRICE_SETTLEMENT,
      SYMBOL_SWAP_LONG,
      SYMBOL_SWAP_SHORT,
      SYMBOL_TRADE_ACCRUED_INTEREST,
      SYMBOL_TRADE_CONTRACT_SIZE,
      SYMBOL_TRADE_FACE_VALUE,
      SYMBOL_TRADE_LIQUIDITY_RATE,
      SYMBOL_TRADE_TICK_SIZE,
      SYMBOL_TRADE_TICK_VALUE,
      SYMBOL_VOLUME_LIMIT,
      SYMBOL_VOLUME_MAX,
      SYMBOL_VOLUME_MIN,
      SYMBOL_VOLUME_STEP
     };
   for(int prop_idx=0; prop_idx<::ArraySize(dbl_props); prop_idx++)
     {
      ENUM_SYMBOL_INFO_DOUBLE curr_property=dbl_props[prop_idx];
      if(!this.CloneProperty(_origin_symbol,curr_property))
         return false;
     }
//--- integer
   ENUM_SYMBOL_INFO_INTEGER int_props[]=
     {
      SYMBOL_BACKGROUND_COLOR,
      SYMBOL_CHART_MODE,
      SYMBOL_DIGITS,
      SYMBOL_EXPIRATION_MODE,
      SYMBOL_EXPIRATION_TIME,
      SYMBOL_FILLING_MODE,
      SYMBOL_MARGIN_HEDGED_USE_LEG,
      SYMBOL_OPTION_MODE,
      SYMBOL_OPTION_RIGHT,
      SYMBOL_ORDER_GTC_MODE,
      SYMBOL_ORDER_MODE,
      SYMBOL_SPREAD,
      SYMBOL_SPREAD_FLOAT,
      SYMBOL_START_TIME,
      SYMBOL_SWAP_MODE,
      SYMBOL_SWAP_ROLLOVER3DAYS,
      SYMBOL_TICKS_BOOKDEPTH,
      SYMBOL_TRADE_CALC_MODE,
      SYMBOL_TRADE_EXEMODE,
      SYMBOL_TRADE_FREEZE_LEVEL,
      SYMBOL_TRADE_MODE,
      SYMBOL_TRADE_STOPS_LEVEL
     };
   for(int prop_idx=0; prop_idx<::ArraySize(int_props); prop_idx++)
     {
      ENUM_SYMBOL_INFO_INTEGER curr_property=int_props[prop_idx];
      if(!this.CloneProperty(_origin_symbol,curr_property))
         return false;
     }
//--- string
   ENUM_SYMBOL_INFO_STRING str_props[]=
     {
      SYMBOL_BASIS,
      SYMBOL_CURRENCY_BASE,
      SYMBOL_CURRENCY_MARGIN,
      SYMBOL_CURRENCY_PROFIT,
      SYMBOL_DESCRIPTION,
      SYMBOL_FORMULA,
      SYMBOL_ISIN,
      SYMBOL_PAGE,
      SYMBOL_PATH
     };
   for(int prop_idx=0; prop_idx<::ArraySize(str_props); prop_idx++)
     {
      ENUM_SYMBOL_INFO_STRING curr_property=str_props[prop_idx];
      if(!this.CloneProperty(_origin_symbol,curr_property))
         return false;
     }
//--- history
   if(_to_msc>0)
     {
      if(this.CloneTicks(_origin_symbol)==-1)
         return false;
     }
//---
   return true;
  }
//+------------------------------------------------------------------+


Observe que nem todas as propriedades podem ser copiadas, porque algumas delas estão definidas no nível da plataforma. Seus valores podem ser recuperados, mas não podem ser controlados (propriedade get).

Uma tentativa de definir a propriedade get para o símbolo personalizado retornará o erro 5307 (ERR_CUSTOM_SYMBOL_PROPERTY_WRONG). Além disso, existe um grupo separado de erros para os símbolos personalizados na seção "Erros em tempo de execução".

Como exemplo, vamos executar o script simples TestClone.mql5, que clona um símbolo simples. Se a tentativa de clonagem for bem-sucedida, o seguinte log aparecerá no Diário.

2019.08.11 19:21:06.402 TestClone (EURUSD,M1) A base symbol "EURUSD" has been successfully cloned.



1.4 O método CiCustomSymbol::LoadTicks()

Este método lê os ticks do arquivo e os carrega-o para uso posterior. Devemos notar que o método exclui anteriormente o banco de dados de ticks existente para esse símbolo personalizado. 

//+------------------------------------------------------------------+
//| Load ticks                                                       |
//+------------------------------------------------------------------+
bool CiCustomSymbol::LoadTicks(const string _src_file_name)
  {
   int symbol_digs=(int)this.GetProperty(SYMBOL_DIGITS);;
//--- delete ticks
   if(this.TicksDelete(0,LONG_MAX)<0)
      return false;
//--- open a file
   CFile curr_file;
   ::ResetLastError();
   int file_ha=curr_file.Open(_src_file_name,FILE_READ|FILE_CSV,',');
   if(file_ha==INVALID_HANDLE)
     {
      ::PrintFormat(__FUNCTION__+": failed to open a %s file!",_src_file_name);
      return false;
     }
   curr_file.Seek(0,SEEK_SET);
//--- read data from a file
   MqlTick batch_arr[];
   if(::ArrayResize(batch_arr,m_batch_size)!=m_batch_size)
     {
      ::Print(__FUNCTION__+": failed to allocate memory for a batch array!");
      return false;
     }
   ::ZeroMemory(batch_arr);
   uint tick_idx=0;
   bool is_file_ending=false;
   uint tick_cnt=0;
   do
     {
      is_file_ending=curr_file.IsEnding();
      string dates_str[2];
      if(!is_file_ending)
        {
         //--- time
         string time_str=::FileReadString(file_ha);
         if(::StringLen(time_str)<1)
           {
            ::Print(__FUNCTION__+": no datetime string - the current tick skipped!");
            ::PrintFormat("The unprocessed string: %s",time_str);
            continue;
           }
         string sep=".";
         ushort u_sep;
         string result[];
         u_sep=::StringGetCharacter(sep,0);
         int str_num=::StringSplit(time_str,u_sep,result);
         if(str_num!=4)
           {
            ::Print(__FUNCTION__+": no substrings - the current tick skipped!");
            ::PrintFormat("The unprocessed string: %s",time_str);
            continue;
           }
         //--- datetime
         datetime date_time=::StringToTime(result[0]+"."+result[1]+"."+result[2]);
         long time_msc=(long)(1e3*date_time+::StringToInteger(result[3]));
         //--- bid
         double bid_val=::FileReadNumber(file_ha);
         if(bid_val<.0)
           {
            ::Print(__FUNCTION__+": no bid price - the current tick skipped!");
            continue;
           }
         //--- ask
         double ask_val=::FileReadNumber(file_ha);
         if(ask_val<.0)
           {
            ::Print(__FUNCTION__+": no ask price - the current tick skipped!");
            continue;
           }
         //--- volumes
         for(int jtx=0; jtx<2; jtx++)
            ::FileReadNumber(file_ha);
         //--- fill in the current tick
         MqlTick curr_tick= {0};
         curr_tick.time=date_time;
         curr_tick.time_msc=(long)(1e3*date_time+::StringToInteger(result[3]));
         curr_tick.bid=::NormalizeDouble(bid_val,symbol_digs);
         curr_tick.ask=::NormalizeDouble(ask_val,symbol_digs);
         //--- flags
         if(m_tick.bid!=curr_tick.bid)
            curr_tick.flags|=TICK_FLAG_BID;
         if(m_tick.ask!=curr_tick.ask)
            curr_tick.flags|=TICK_FLAG_ASK;
         if(curr_tick.flags==0)
            curr_tick.flags=TICK_FLAG_BID|TICK_FLAG_ASK;;
         if(tick_idx==m_batch_size)
           {
            //--- add ticks to the custom symbol
            if(m_is_selected)
              {
               if(this.TicksAdd(batch_arr)!=m_batch_size)
                  return false;
              }
            else
              {
               if(this.TicksReplace(batch_arr)!=m_batch_size)
                  return false;
              }
            tick_cnt+=m_batch_size;
            //--- log
            for(uint idx=0,batch_idx=0; idx<::ArraySize(dates_str); idx++,batch_idx+=(m_batch_size-1))
               dates_str[idx]=::TimeToString(batch_arr[batch_idx].time,TIME_DATE|TIME_SECONDS);
            ::PrintFormat("\nTicks loaded from %s to %s.",dates_str[0],dates_str[1]);
            //--- reset
            ::ZeroMemory(batch_arr);
            tick_idx=0;
           }
         batch_arr[tick_idx]=curr_tick;
         m_tick=curr_tick;
         tick_idx++;
        }
      //--- end of file
      else
        {
         uint new_size=tick_idx;
         if(new_size>0)
           {
            MqlTick last_batch_arr[];
            if(::ArrayCopy(last_batch_arr,batch_arr,0,0,new_size)!=new_size)
              {
               ::Print(__FUNCTION__+": failed to copy a batch array!");
               return false;
              }
            //--- add ticks to the custom symbol
            if(m_is_selected)
              {
               if(this.TicksAdd(last_batch_arr)!=new_size)
                  return false;
              }
            else
              {
               if(this.TicksReplace(last_batch_arr)!=new_size)
                  return false;
              }
            tick_cnt+=new_size;
            //--- log
            for(uint idx=0,batch_idx=0; idx<::ArraySize(dates_str); idx++,batch_idx+=(tick_idx-1))
               dates_str[idx]=::TimeToString(batch_arr[batch_idx].time,TIME_DATE|TIME_SECONDS);
            ::PrintFormat("\nTicks loaded from %s to %s.",dates_str[0],dates_str[1]);
           }
        }
     }
   while(!is_file_ending && !::IsStopped());
   ::PrintFormat("\nLoaded ticks number: %I32u",tick_cnt);
   curr_file.Close();
//---
   MqlTick ticks_arr[];
   if(::CopyTicks(m_name,ticks_arr,COPY_TICKS_INFO,1,1)!=1)
     {
      ::Print(__FUNCTION__+": failed to copy the first tick!");
      return false;
     }
   m_from_msc=ticks_arr[0].time_msc;
   if(::CopyTicks(m_name,ticks_arr,COPY_TICKS_INFO,0,1)!=1)
     {
      ::Print(__FUNCTION__+": failed to copy the last tick!");
      return false;
     }
   m_to_msc=ticks_arr[0].time_msc;
//---
   return true;
  }
//+------------------------------------------------------------------+

Nesta variante, os seguintes campos da estrutura tick são preenchidos:

struct MqlTick 
  { 
   datetime     time;          // Last price update time 
   double       bid;           // Current Bid price 
   double       ask;           // Current Ask price 
   double       last;          // Current price of the last trade (Last)
   ulong        volume;        // Volume for the current Last price
   long         time_msc;      // Last price update time in milliseconds 
   uint         flags;         // Tick flags 
   double       volume_real;   // Volume for the current Last price
  };

O valor TICK_FLAG_BID|TICK_FLAG_ASK é definido para o primeiro flag do tick. Um valor adicional depende de qual preço (bid ou ask) que foi alterado. Se nenhum dos preços tiverem sido alterados, eles serão processados como sendo o primeiro tick.

Começando com a build 2085, o histórico de barras pode ser formado simplesmente pelo carregamento do histórico de ticks. Se o histórico foi carregado, nós podemos solicitar programaticamente o histórico da barra.

Como exemplo, vamos executar o script simples TestLoad.mql5, que carrega os ticks de um arquivo. O arquivo de dados deve estar localizado na pasta % MQL5/Files. Neste exemplo, o arquivo é o EURUSD1_tick.csv. Ele contém os ticks EURUSD de 1 e 2 de agosto de 2019. Além disso, nós consideraremos as fontes de dados do tick.

Depois de executar o script, o número de ticks carregados serão exibidos no diário. Além disso, nós verificaremos novamente o número de ticks disponíveis solicitando os dados do banco de dados de ticks da plataforma. 354,400 ticks foram copiados. Assim, os números são iguais. Nós também recebemos 2.697 barras de 1 minuto. 

NO      0       15:52:50.149    TestLoad (EURUSD,H1)    
LN      0       15:52:50.150    TestLoad (EURUSD,H1)    Ticks loaded from 2019.08.01 00:00:00 to 2019.08.02 20:59:56.
FM      0       15:52:50.152    TestLoad (EURUSD,H1)    
RM      0       15:52:50.152    TestLoad (EURUSD,H1)    Loaded ticks number: 354400
EJ      0       15:52:50.160    TestLoad (EURUSD,H1)    Ticks from the file "EURUSD1_tick.csv" have been successfully loaded.
DD      0       15:52:50.170    TestLoad (EURUSD,H1)    Copied 1-minute rates number: 2697
GL      0       15:52:50.170    TestLoad (EURUSD,H1)    The 1st rate time: 2019.08.01 00:00
EQ      0       15:52:50.170    TestLoad (EURUSD,H1)    The last rate time: 2019.08.02 20:56
DJ      0       15:52:50.351    TestLoad (EURUSD,H1)    Copied ticks number: 354400


Outros métodos pertencem ao grupo de métodos da API.


2. Dados de ticks. Fontes

Os dados de ticks formam uma série de preços, com uma vida muito ativa, na qual a oferta e a demanda estão lutando entre si.

A natureza da série de preços tem sido objeto de discussões de traders e especialistas. Esta série é objeto de análise, a base subjacente para a tomada de decisões, etc.

As ideias a seguir são aplicáveis no contexto do artigo.

Como você sabe, o mercado de Forex é um mercado de balcão. Portanto, não há cotações de referência. Como resultado, não há arquivos de ticks (histórico de ticks), que podem ser usados como referência. Mas temos os futuros das moedas, a maioria dos quais é negociada na Chicago Mercantile Exchange. Provavelmente, essas cotações podem servir como algum tipo de referência. No entanto, os dados históricos não podem ser obtidos gratuitamente. Em alguns casos, você pode ter um período de teste gratuito. Mas, mesmo nesse caso, você terá que se registrar no site da bolsa e se comunicar com o gerente de vendas. Por outro lado, existe concorrência entre as corretoras. Portanto, as cotações não devem diferir muito entre as corretoras. Mas geralmente as corretoras não armazenam os arquivos de ticks e não os disponibilizam para download.

Uma das fontes gratuitas é website do Banco Dukascopy.

Após um registro simples, é permitido o download dos dados históricos, incluindo os ticks.

As linhas no arquivo consistem em 5 colunas:

  • Hora
  • Preço de Ask
  • Preço de Bid
  • Volume comprado
  • Volume vendido
O inconveniente do download manual de ticks é que você pode selecionar apenas 1 dia. Se você precisar de um histórico de vários anos de ticks, o download levará muito tempo e esforço.


Quant Data Manager

Fig.1 Símbolos para download na guia Data do aplicativo Quant Data Manager


Existem aplicativos auxiliares que baixam os arquivos de ticks do site do Banco Dukascopy. Um deles é o aplicativo Quant Data Manager. A Figura 1 mostra uma das guias da janela do aplicativo. 


O método CiCustomSymbol::LoadTicks() é ajustado para o formato de arquivo csv usado no aplicativo acima.


3 Teste de estresse das estratégias de negociação

O teste da estratégia de negociação é um processo de vários aspectos. Na maioria das vezes, o "teste" refere-se à execução de um algoritmo de negociação usando as cotações históricas (back-testing). Mas existem outros métodos para testar uma estratégia de negociação. 

Uma delas é Teste de Estresse.

O teste de Estresse é uma forma de teste deliberadamente intenso ou completo usado para determinar a estabilidade de um determinado sistema ou entidade.

A ideia é simples: nós criamos condições específicas para a operação da estratégia de negociação, o que fornecerá um estresse acelerado para a estratégia. O objetivo final de tais condições é verificar o grau de confiabilidade do sistema de negociação e sua resistência a condições que se tornaram ou podem piorar.


3.1 Alteração do Spread

O fator de spread é extremamente importante para uma estratégia de negociação, pois ela determina a quantidade de custos adicionais. As estratégias que visam negócios de curto prazo são particularmente sensíveis ao tamanho do spread. Em alguns casos, a proporção do spread/retorno pode exceder 100%. 

Vamos tentar criar um símbolo personalizado, que será diferente do tamanho padrão do spread. Para esse fim, vamos criar um novo método chamado de CiCustomSymbol::ChangeSpread().

//+------------------------------------------------------------------+
//| Change the initial spread                                        |
//| Input parameters:                                                |
//|     1) _spread_size - the new fixed value of the spread, pips.   |
//|        If the value > 0 then the spread value is fixed.          |
//|     2) _spread_markup - a markup for the floating value of the   |
//|        spread, pips. The value is added to the current spread if |
//|        _spread_size=0.                                           |
//|     3) _spread_base - a type of the price to which a markup is   |
//|        added in case of the floating value.                      |
//+------------------------------------------------------------------+
bool CiCustomSymbol::ChangeSpread(const uint _spread_size,const uint _spread_markup=0,
                                  const ENUM_SPREAD_BASE _spread_base=SPREAD_BASE_BID)
  {
   if(_spread_size==0)
      if(_spread_markup==0)
        {
         ::PrintFormat(__FUNCTION__+":  neither the spread size nor the spread markup are set!",
                       m_name,::GetLastError());
         return false;
        }
   int symbol_digs=(int)this.GetProperty(SYMBOL_DIGITS);
   ::ZeroMemory(m_tick);
//--- copy ticks
   int tick_idx=0;
   uint tick_cnt=0;
   ulong from=1;
   double curr_point=this.GetProperty(SYMBOL_POINT);
   int ticks_copied=0;
   MqlDateTime t1_time;
   TimeToStruct((int)(m_from_msc/1e3),t1_time);
   t1_time.hour=t1_time.min=t1_time.sec=0;
   datetime start_datetime,stop_datetime;
   start_datetime=::StructToTime(t1_time);
   stop_datetime=(int)(m_to_msc/1e3);
   do
     {
      MqlTick custom_symbol_ticks[];
      ulong t1,t2;
      t1=(ulong)1e3*start_datetime;
      t2=(ulong)1e3*(start_datetime+PeriodSeconds(PERIOD_D1))-1;
      ::ResetLastError();
      ticks_copied=::CopyTicksRange(m_name,custom_symbol_ticks,COPY_TICKS_INFO,t1,t2);
      if(ticks_copied<0)
        {
         ::PrintFormat(__FUNCTION__+": failed to copy ticks for a %s symbol! Error code: %d",
                       m_name,::GetLastError());
         return false;
        }
      //--- there are some ticks for the current day
      else
         if(ticks_copied>0)
           {
            for(int t_idx=0; t_idx<ticks_copied; t_idx++)
              {
               MqlTick curr_tick=custom_symbol_ticks[t_idx];
               double curr_bid_pr=::NormalizeDouble(curr_tick.bid,symbol_digs);
               double curr_ask_pr=::NormalizeDouble(curr_tick.ask,symbol_digs);
               double curr_spread_pnt=0.;
               //--- if the spread is fixed
               if(_spread_size>0)
                 {
                  if(_spread_size>0)
                     curr_spread_pnt=curr_point*_spread_size;
                 }
               //--- if the spread is floating
               else
                 {
                  double spread_markup_pnt=0.;
                  if(_spread_markup>0)
                     spread_markup_pnt=curr_point*_spread_markup;
                  curr_spread_pnt=curr_ask_pr-curr_bid_pr+spread_markup_pnt;
                 }
               switch(_spread_base)
                 {
                  case SPREAD_BASE_BID:
                    {
                     curr_ask_pr=::NormalizeDouble(curr_bid_pr+curr_spread_pnt,symbol_digs);
                     break;
                    }
                  case SPREAD_BASE_ASK:
                    {
                     curr_bid_pr=::NormalizeDouble(curr_ask_pr-curr_spread_pnt,symbol_digs);
                     break;
                    }
                  case SPREAD_BASE_AVERAGE:
                    {
                     double curr_avg_pr=::NormalizeDouble((curr_bid_pr+curr_ask_pr)/2.,symbol_digs);
                     curr_bid_pr=::NormalizeDouble(curr_avg_pr-curr_spread_pnt/2.,symbol_digs);
                     curr_ask_pr=::NormalizeDouble(curr_bid_pr+curr_spread_pnt,symbol_digs);
                     break;
                    }
                 }
               //--- new ticks
               curr_tick.bid=curr_bid_pr;
               curr_tick.ask=curr_ask_pr;
               //--- flags
               curr_tick.flags=0;
               if(m_tick.bid!=curr_tick.bid)
                  curr_tick.flags|=TICK_FLAG_BID;
               if(m_tick.ask!=curr_tick.ask)
                  curr_tick.flags|=TICK_FLAG_ASK;
               if(curr_tick.flags==0)
                  curr_tick.flags=TICK_FLAG_BID|TICK_FLAG_ASK;
               custom_symbol_ticks[t_idx]=curr_tick;
               m_tick=curr_tick;
              }
            //--- replace ticks
            int ticks_replaced=0;
            for(int att=0; att<ATTEMTS; att++)
              {
               ticks_replaced=this.TicksReplace(custom_symbol_ticks);
               if(ticks_replaced==ticks_copied)
                  break;
               ::Sleep(PAUSE);
              }
            if(ticks_replaced!=ticks_copied)
              {
               ::Print(__FUNCTION__+": failed to replace the refreshed ticks!");
               return false;
              }
            tick_cnt+=ticks_replaced;
           }
      //--- next datetimes
      start_datetime=start_datetime+::PeriodSeconds(PERIOD_D1);
     }
   while(start_datetime<=stop_datetime && !::IsStopped());
   ::PrintFormat("\nReplaced ticks number: %I32u",tick_cnt);
//---
   return true;
  }
//+------------------------------------------------------------------+

Como alterar o tamanho do spread para um símbolo personalizado?

Primeiramente, o spread fixo pode ser definido. Isso pode ser feito através da especificação de um valor positivo no parâmetro _spread_size. Observe que esta parte funcionará no Testador, apesar das seguinte regra:

No Testador de Estratégia, o spread é sempre considerado flutuante. Ou seja, SymbolInfoInteger(symbol, SYMBOL_SPREAD_FLOAT) sempre retorna true.

Em segundo lugar, uma marcação pode ser adicionada ao valor do spread disponível. Isso pode ser feito através da definição do parâmetro _spread_markup. 

Além disso, o método permite especificar o preço, que servirá como valor de spread de referência. Isso é feito pela enumeração ENUM_SPREAD_BASE.

//+------------------------------------------------------------------+
//| Spread calculation base                                          |
//+------------------------------------------------------------------+
enum ENUM_SPREAD_BASE
  {
   SPREAD_BASE_BID=0,    // bid price
   SPREAD_BASE_ASK=1,    // ask price
   SPREAD_BASE_AVERAGE=2,// average price
  };
//+------------------------------------------------------------------+

Se nós usarmos o preço de bid (SPREAD_BASE_BID), então ask = bid + spread calculado. Se nós usarmos o preço de ask (SPREAD_BASE_ASK), então bid = ask - spread calculado. Se nós usarmos o preço médio (SPREAD_BASE_AVERAGE), então bid = preço médio - spread calculado/2.

O método CiCustomSymbol::ChangeSpread() não altera o valor de uma determinada propriedade do símbolo, mas altera o valor do spread em cada tick. Os ticks atualizados são armazenados na base de ticks.


Vamos verificar a operação do método usando o script TestChangeSpread.mq5. Se o script funcionar corretamente, o seguinte log será adicionado no Diário:

2019.08.30 12:49:59.678 TestChangeSpread (EURUSD,M1)    Replaced ticks number: 354400


Isso significa que o tamanho do tick foi alterado para todos os ticks carregados anteriormente. 


A tabela abaixo mostra meus resultados de testes da estratégia com diferentes valores de spread, usando os dados do EURUSD (Tabela 1).

Valor Spread 1 (12-17 pontos) Spread 2 (25 pontos) Spread 2 (50 pontos)
Total de negociações
172 156 145
Lucro Líquido, $
4 018.27 3 877.58 3 574.1
Rebaixamento máx. de capital, %
11.79 9.65 8.29
 Lucro por negociação, $
 23.36  24.86  24.65

Tabela 1. Resultados do teste com diferentes valores de spread

A coluna "Spread 1" reflete os resultados com um spread flutuante real (12 a 17 pontos com cinco casas decimais).

Com um spread mais alto, o número de negociações foi menor. Isso resultou em uma diminuição no rebaixamento. Além disso, a lucratividade da negociação aumentou neste caso.


3.2 Alterando os níveis de Stop e de Congelamento

Algumas estratégias podem depender do nível de stop e do nível de congelamento. Muitas corretoras fornecem o nível de stop igual ao spread e o nível de congelamento zero. No entanto, às vezes as corretoras podem aumentar esses níveis. Geralmente isso acontece durante os períodos de maior volatilidade ou baixa liquidez. Esta informação pode ser obtida nas Regras de Negociação da corretora. 

Aqui está um exemplo de uma extração dessas Regras de Negociação:

"A distância mínima em que as ordens pendentes ou ordens T/P e S/L podem ser colocadas é igual ao spread do símbolo. 10 minutos antes do lançamento de estatísticas macroeconômicas importantes ou de notícias econômicas, a distância mínima para as ordens S/L pode ser aumentada para 10 pontos de spread. 30 minutos antes do fechamento do pregão, esse nível para as ordens de C / L aumentam para 25 pontos de spread. "

Esses níveis (SYMBOL_TRADE_STOPS_LEVEL e SYMBOL_TRADE_FREEZE_LEVEL) podem ser configurados usando o método CiCustomSymbol::SetProperty(). 

Observe que as propriedades do símbolo não podem ser alteradas dinamicamente no testador. Na versão atual, o Testador opera com parâmetros de símbolos personalizados pré-configurados. Os desenvolvedores da plataforma estão apresentando um grande trabalho. Portanto, esse recurso pode ser adicionado em futuras compilações.


3.3 Alteração dos requisitos de margem

Os requisitos da margem individual também podem ser definidos para um símbolo personalizado. Os valores para os seguintes parâmetros podem ser definidos programaticamente: SYMBOL_MARGIN_INITIAL, SYMBOL_MARGIN_MAINTENANCE, SYMBOL_MARGIN_HEDGED. Assim, nós podemos definir o nível de margem para o instrumento de negociação. 

Também existem os identificadores de margem para os tipos separados de posições e volumes (SYMBOL_MARGIN_LONG, SYMBOL_MARGIN_SHORT etc.). Eles são definidos no modo manual

As variações de alavancagem permitem testar a estratégia de negociação em termos de resistência a rebaixamentos e a capacidade de evitar interrupções.


Conclusão

Este artigo destaca alguns aspectos do teste de estresse de uma estratégia de negociação.

As configurações personalizadas dos símbolos permitem configurar os parâmetros de seus próprios símbolos. Todo trader algorítmico pode selecionar um conjunto específico de parâmetros, necessários para uma estratégia de negociação específica. Uma das opções interessantes descobertas diz respeito à profundidade do mercado de um símbolo personalizado.

O arquivo contém um arquivo com ticks, que foram processados como parte dos exemplos, bem como os arquivos de origem do script e a classe de símbolo personalizado. 

Eu gostaria de expressar minha gratidão ao fxsaber, para os moderadores Artyom Trishkin e Slava pelas discussões interessantes no tópico "Símbolos personalizados. Erros, bugs, perguntas & sugestões".

Traduzido do russo pela MetaQuotes Software Corp.
Artigo original: https://www.mql5.com/ru/articles/7166

Arquivos anexados |
EURUSD1_tick.zip (3087.31 KB)
Custom.zip (10.37 KB)
Analisador Sintático HTML com o curl Analisador Sintático HTML com o curl

O artigo fornece a descrição de uma biblioteca simples para análise sintática (parser) de código HTML usando componentes de terceiros. Em particular, ela abrange as possibilidades de acessar dados que não podem ser recuperados usando os métodos HTTP GET e POST. Nós selecionaremos um site com páginas não muito extensas e tentaremos obter alguns dados interessantes dele.

Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XIX): classe de mensagens de biblioteca Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XIX): classe de mensagens de biblioteca

No artigo, veremos uma classe para exibir mensagens de texto. Agora, vamos supor que temos suficientes mensagens de texto e devemos pensar em comoarmazená-las, exibi-las, editá-las em outro idioma e adicionar novos idiomas à biblioteca e alterná-los rapidamente.

Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XX): criação e armazenamento de recursos de programas Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XX): criação e armazenamento de recursos de programas

No artigo, veremos como armazenar dados no código fonte de um programa e como criar arquivos de som e gráficos a partir dele. Muitas vezes, ao criar um programa, precisamos usar sons e imagens. Na linguagem MQL, existem várias maneiras de usar esse tipo de dados.

Construtor de estratégia baseado nos padrões de Merill Construtor de estratégia baseado nos padrões de Merill

No artigo anterior, nós consideramos a aplicação dos padrões de Merill a vários dados, como em valores de preço em um gráfico de par de moeda e de indicadores padrão do MetaTrader 5: ATR, WPR, CCI, RSI, entre outros. Agora, vamos tentar criar um conjunto para a construção de estratégias baseado nos padrões de Merill.