Recetas MQL5 – Prueba de estrés de una estrategia comercial con ayuda de símbolos personalizados

25 octubre 2019, 10:08
Denis Kirichenko
0
876

Introducción

Hace relativamente poco, en el terminal MetaTrader 5  surgió la posibilidad de crear símbolos personalizados. Ahora, los tráders algorítmicos podrán en cierta forma convertirse en brókeres para sí mismos, ya que no serán necesarios los servidores comerciales, y el entorno comercial estará plenamente bajo su control. Eso sí, debemos aclarar de inmediato que estamos hablando del modo de simulación. Está claro que ningún bróker común ofrece la ejecución real de órdenes comerciales para símbolos de usuario. No obstante, el tráder algorítmico dispone de una herramienta que le permitirá comprobar su estrategia más a fondo.

En el presente artículo, nos ocuparemos de crear las condiciones para dicha comprobación. Vamos a comenzar por la clase del símbolo de usuario.


1. La clase del símbolo de usuario CiCustomSymbol

En la Biblioteca Estándar ya hace mucho que existe una clase para el acceso simplificado a las propiedades de un símbolo. Se trata de la clase CSymbolInfo. En esencia, esta clase realiza una función intermedia, dado que recurre al servidor según la solicitud del usuario y luego obtiene del mismo una respuestas en forma de valor de la propiedad solicitada.

Nuestra tarea será crear una clase análoga, pero ya para el símbolo de usuario. Imaginamos que una funcionalidad de esta clase será más amplia por una parte, puesto que debemos escribir los métodos de creación, eliminación, etcétera, si bien por la otra no existirán los métodos que posibilitan la conexión con el servidor. A estos pertenecen Refresh(), IsSynchronized() y otros.

Esta clase para la creación del entorno comercial engloba las funciones estándar para trabajar con los símbolos de usuario.

La estructura de la declaración de la clase se muestra más abajo.

//+------------------------------------------------------------------+
//| 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 a comenzar por los métodos que deben hacer de nuestro símbolo un símbolo de usuario completamente funcional.


1.1 Método CiCustomSymbol::Create()

Para usar las posibilidades de un símbolo de usuario, debemos crearlo, o bien comprobar que ha sido creado anteriormente. 

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Aquí tenemos que aclarar que el método retornará el valor en forma de código numérico:

  • «-1» — error al crear el símbolo;
  • «0»  — el símbolo se ha creado anteriormente;
  • «1»  — el símbolo se ha creado con éxito en la llamada actual del método.

Vamos a decir unas palabras sobre los parámetros _batch_size y _is_selected.

El primero (_batch_size), establece el parámetro del conjunto con el que se cargarán los datos. Los ticks se cargarán en paquetes: en primer lugar, los datos se leen en la matriz de ticks auxiliar, y después, cuando la matriz ha sido rellenada, son transferidos a la base de datos sobre ticks del símbolo de usuario (historia de ticks). Por una parte, este enfoque permite no crear una matriz de gran tamaño, y por otra, no tenemos que actualizar la base de ticks con frecuencia. Por defecto, el tamaño de la matriz de ticks auxiliar es igual a 1 millón.

El segundo parámetro (_is_selected), determina si vamos a registrar los ticks en la base de datosdirectamente o primero van a llegar a la ventana de "Observación de mercado".

Como ejemplo, vamos a iniciar el sencillo script TestCreation.mql5, que crea un símbolo de usuario.

En el diario podremos ver qué código retorna la llamada de este método.

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

Podrá encontrar información más detallada sobre la creación  de símbolos de usuario en la Documentación.


1.2 Método CiCustomSymbol::Delete()

Este método intentará eliminar el símbolo de usuario. De forma preliminar, tratará de quitar el símbolo de la «Observación de mercado». Si el procedimiento falla, la eliminación se interrumpirá antes de finalizar.

//+------------------------------------------------------------------+
//| 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 ejemplo, vamos a iniciar el sencillo script TestDeletion.mql5, que elimina un símbolo de usuario. Si tiene éxito, en el diario aparecerá una entrada.

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


1.3 Método CiCustomSymbol::Clone()

Este método se ocupa de realizar la clonación: usando como base el símbolo elegido, determina las propiedades para el actual símbolo de usuario. Dicho de forma más sencilla: obtenemos el valor de una propiedad del símbolo original y lo copiamos para el otro. Asimismo, el usuario puede establecer la posibilidad de clonar también la historia de ticks. Para ello, bastará con determinar el intervalo temporal.

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Hay que tener en cuenta que no todas las propiedades pueden ser copiadas, puesto que algunas se establecen solo al nivel del terminal. Sus valores solo se pueden obtener, no es posible controlarlos de forma independiente (propiedad get).

Si intentamos establecer una propiedad get para un símbolo de usuario, obtendremos el error 5307 (ERR_CUSTOM_SYMBOL_PROPERTY_WRONG). Aquí también conviene destacar que un desarrollador definió un grupo completo de errores para los símbolos de usuario en el apartado «Errores de tiempo de ejecución».

Como ejemplo, vamos a iniciar el sencillo script TestClone.mql5, que clona el símbolo básico. Si el intento de clonación ha tenido éxito, en el diario aparecerá un mensaje.

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


1.4 Método CiCustomSymbol::LoadTicks()

Este método lee los ticks del archivo y los carga para su posterior uso. Debemos notar que el método elimina preliminarmente la base de ticks existente para este 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;
  }
//+------------------------------------------------------------------+

En esta variante, se rellenan los siguientes campos de la estructura de ticks:

struct MqlTick 
  { 
   datetime     time;          // Hora de la última actualización de precios 
   double       bid;           // Precio Bid actual 
   double       ask;           // Precio Ask actual 
   double       last;          // Precio actual de la última transacción (Last) 
   ulong        volume;        // Volumen para el precio Last actual 
   long         time_msc;      // Hora de la última actualización de precios en milisegundos 
   uint         flags;         // Banderas de ticks 
   double       volume_real;   // Volumen para el precio Last actual con precisión aumentada 
  };

Establecemos el valor "TICK_FLAG_BID|TICK_FLAG_ASK" para la bandera del primer tick. El siguiente valor dependerá de qué precio (bid o ask) ha cambiado. Si ninguno de los precios ha cambiado, lo procesaremos como el primer tick.

A partir de la versión 2085, basta con cargar la historia de ticks para formar la historia de barras. En cuanto se ha cargado, ya podemos solicitar la historia de barras de forma programática.

Como ejemplo, vamos a iniciar el sencillo script TestLoad.mql5, que carga los ticks desde el archivo correspondiente. El propio archivo con los datos debe encontrarse en la carpeta % MQL5/Files. En el ejemplo, este archivo es EURUSD1_tick.csv. Este contiene los ticks del símbolo EURUSD del 1 y el 2 de agosto de 2019. Más tarde analizaremos las fuentes de los datos de ticks.

Después de iniciar el script, en el diario aparecerá un mensaje con el número de ticks cargados. Además, vamos a comprobar una vez más de cuántos ticks disponemos, solicitando para ello los datos de la base de ticks del terminal. Se han copiado 354 400 ticks. Todo coincide. Asimismo, hemos obtenido 2 697 barras de 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


Los demás métodos pertenecen al grupo de los métodos API.


2. Datos de ticks. Fuentes

Los datos de ticks forman una serie de precios que muestra una actividad muy agitada: aquí, como en un frente bélico, «luchan» entre sí la oferta y la demanda.

Sobre la naturaleza de la serie de precios se ha discutido, se discute y se discutirá. Esta serie supone, además de un objeto de análisis de todo tipo,  una base en la toma de decisiones comerciales, etcétera.

En el contexto de nuestro artículo, destacaremos un par de momentos importantes.

Como ya sabemos, el mercado Fórex es un mercado extrabursátil, por lo que no tiene cotizaciones modelo. Y como consecuencia, no existen archivos de ticks (historia de ticks) que podamos tomar como modelo. Sí es cierto que existen futuros de divisas, que en una inmensa mayoría se comercian en la Bolsa de Chicago. Y las cotizaciones de estos seguramente tengan cierto valor como referencia. No será posible obtener los datos históricos de forma gratuita. En el mejor de los casos, se nos ofrecerá un periodo de prueba. Pero para ello, también deberemos superar una etapa de registro en el sitio web de la bolsa y contactar con el gerente de ventas. Por otra parte, la competencia entre brókeres también juega un papel determinado. Por eso, nos atreveremos a suponer que las cotizaciones con brókeres distintos son más o menos iguales, con un cierto margen de varios puntos. Otro asunto es que los brókeres, normalmente, no guarden los archivos de ticks y no los ofrezcan para la descarga.

Entre las fuentes gratuitas, destacaremos el sitio web del bróker Dukascopy Bank.

Después de superar un sencillo registro, podremos descargar los datos históricos, incluyendo los ticks.

La líneas en el archivo constan de 5 columnas:

  • Hora;
  • Precio ask;
  • Precio bid;
  • Volumen comprado;
  • Volumen vendido.
La incomodidad de descargar los ticks manualmente reside que solo podemos elegir 1 día. Si necesitamos la historia de ticks de varios años, tendremos que dedicar bastante tiempo a la descarga.


Quant Data Manager

Fig.1 Símbolos para la descarga en la pestaña "Data" en el programa Quant Data Manager


No obstante, existen programas auxiliares que descargan los archivos de ticks del sitio web del bróker Dukascopy Bank. Entre ellos, podemos mencionar Quant Data Manager. En la fig.1 se muestra una de las pestañas de la ventana del programa. 


Precisamente al formato del archivo csv del programa indicado se ajusta el método CiCustomSymbol::LoadTicks().


3. Pruebas de estrés de las estrategias comerciales

El procedimiento de prueba de una estrategia comercial tiene un carácter multidimensional. Y, aunque normalmente se entiende por prueba la pasada de un algoritmo comercial por una historia de cotizaciones (backtesting), también existen otros métodos de comprobación de una estrategia comercial. 

Uno de ellos puede ser la prueba de estrés.

La prueba de estrés (stress testing) es uno de los tipos de prueba usados para determinar la estabilidad de un sistema o módulo en condiciones de superación de los límites de un funcionamiento normal.

La idea es muy simple: se crean unas condiciones para el funcionamiento de la estrategia comercial tales que empeoren las normales o típicas con iguales variables. El objetivo final de estas acciones es comprobar el nivel de fiabilidad del sistema comercial analizado, así como su estabilidad en condiciones de cambio o eventual cambio a peor.


3.1 Cambio del spread

El factor del spread tiene una gran importancia para la estrategia comercial, dado que este determina el volumen de los gastos añadidos. Se muestran especialmente sensibles al tamaño del spread las estrategias orientadas a las transacciones a corto plazo. En estos casos, la relación del tamaño del spread con respecto a los ingresos puede superar el 100%. 

Vamos a intentar crear un símbolo de usuario que se distinga del básico en el tamaño del spread. Para ello, escribiremos el nuevo método 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;
  }
//+------------------------------------------------------------------+

¿Cómo podemos cambiar la magnitud del spread para un símbolo de usuario?

En primer lugar, podemos hacer el spread fijo. Basta con establecer un cierto valor positivo para el parámetro _spread_size. Aquí, debemos notar que esta construcción también funcionará en el Simulador, a pesar de la siguente regla:

En el Simulador, el spread siempre se considera flotante. Es decir, SymbolInfoInteger(symbol, SYMBOL_SPREAD_FLOAT) siempre retorna true.

En segundo lugar, es posible realizar un añadido (markup) al valor de spread ya disponible. Para ello, definiremos el parámetro _spread_markup. 

Además, el método permite indicar el precio que representará el punto de partida para el cálculo del valor del spread. La enumeración ENUM_SPREAD_BASE se encarga de ello.

//+------------------------------------------------------------------+
//| 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
  };
//+------------------------------------------------------------------+

Si tomamos el precio bid (SPREAD_BASE_BID), entonces: precio ask = precio bid + spread calculado. Si tomamos el precio ask (SPREAD_BASE_ASK), entonces: precio bid = precio ask - spread calculado. Si tomamos el precio medio (SPREAD_BASE_AVERAGE), entonces: bid = precio medio - spread calculado/2.

El método CiCustomSymbol::ChangeSpread() no cambia el valor de una cierta propiedad del símbolo, sino el valor del spread en cada tick. Los ticks actualizados se guardan en la base de ticks.


Vamos a comprobar el funcionamiento del método con la ayuda del script TestChangeSpread.mq5. Si el script funciona con normalidad, en el Diario aparecerá la entrada:

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

Es decir, para todos los ticks cargados anteriormente, el tamaño del spread ha cambiado. 


Vamos a ver los resultados de la simulación de nuestra estrategia comercial para diferentes valores de spread con la pareja EURUSD (Recuadro 1).

Índice Spread 1 (12-17 pp) Spread 2 (25 pp) Spread 2 (50 pp)
Transacciones totales
172 156 145
Beneficio neto, $
4 018.27 3 877.58 3 574.1
Máx. reducción de fondos, %
11.79 9.65 8.29
 Beneficio por transacción, $
 23.36  24.86  24.65

Recuadro 1 Valores de los indicadores de la simulación con spread cambiante

La columna "Spread 1" muestra el resultado para un spread flotante real (12-17 pp en el quinto dígito).

Es totalmente natural que conforme aumente el valor del spread se realicen menos transacciones. Esto ha causado una disminución de la reducción. También resulta interesante que, en este caso, la rentabilidad de la transacción haya aumentado.


3.2 Cambio del nivel de stop y el nivel de "congelación"

Algunas estrategias pueden depender del nivel stop y el nivel de congelación (freeze level). Con muchos brókeres, el primero de ellos es igual a la magnitud del spread, y el segundo es igual a cero. Pero hay momentos en los que el bróker puede aumentar los datos de un nivel. Esto sucede en los periodos de volatilidad alta o liquidez baja. Para más información, consulte las Reglas Comerciales del bróker. 

Vamos a mostrar como ejemplo un extracto de las Reglas Comerciales de un bróker:

"La distancia mínima a la que se pueden colocar órdenes pendientes o colocar órdenes T/P y S/L, es igual al spread del instrumento. Hasta 10 minutos antes de la publicación de indicadores significativos de estadísticas macroeconómicas y noticias importantes del ámbito político o económico, la distancia mínima para las órdenes S/L puede incrementarse hasta 10 spreads. Hasta 30 minutos antes del cierre de transacciones, este nivel para las órdenes S/L aumenta hasta 25 spreads."

Se ajustan los niveles indicados (SYMBOL_TRADE_STOPS_LEVEL y SYMBOL_TRADE_FREEZE_LEVEL) con la ayuda del método CiCustomSymbol::SetProperty(). 

Eso sí, no será posible cambiar de forma dinámica las propiedades del símbolo en el Simulador. En la versión actual, el Simulador funciona con parámetros del símbolo de usuario establecidos de antemano. Los desarrolladores están realizando un gran trabajo en cuanto a la modernización del Simulador. Es posible que en un futuro muy próximo surja esta posibilidad.


3.3 Cambiando el nivel de los requerimientos de margen

Para un símbolo de usuario, también se pueden determinar los valores de los requerimientos de margen. A día de hoy, es posible establecer de forma programática los valores para los siguientes parámetros: SYMBOL_MARGIN_INITIAL, SYMBOL_MARGIN_MAINTENANCE, SYMBOL_MARGIN_HEDGED. En esencia, de esta forma determinamos el tamaño del apalancamiento marginal para el instrumento con el que comerciamos. 

Hay además indentificadores de margen para tipos de posiciones y órdenes aparte (SYMBOL_MARGIN_LONG,SYMBOL_MARGIN_SHORT, etcétera). Pero estos se establecen en el modo manual

La variación de los valores de apalancamiento permite poner a prueba una estrategia comercial en cuanto a su estabilidad respecto a las reducciones y las posibilidades de evitar el stop-out.


Conclusión

Dentro del presente artículo, se ha arrojado luz sobre ciertos aspectos de las pruebas de estrés de estrategias comerciales.

Debemos notar que los ajustes de los símbolos de usuario permiten establecer multitud de parámetros para los símbolos propios. Y cada tráder algorítmico puede elegir de la lista aquellos que le resulten interesantes en relación con su estrategia. Sería bastante interesante, por ejemplo, trabajar con la profundidad de mercado de un símbolo de usuario.

En el directorio se encuentra un archivo con los ticks que han sido procesados en los ejemplos, así como los archivos fuente de los scripts y la clase del símbolo de usuario. 

Querríamos expresar especial gratitud al usuario fxsaber, y también a los moderadores Artyom Trishkin y Slava por la interesante discusión desarrollada en el foro sobre los "Símbolos de Usuario. Errores, bugs, preguntas, propuestas".

Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/7166

Archivos adjuntos |
EURUSD1_tick.zip (3087.31 KB)
Custom.zip (10.37 KB)
Análisis sintáctico HTML con ayuda de curl Análisis sintáctico HTML con ayuda de curl

En el artículo se describe una sencilla biblioteca que usa componentes de terceros para parsear código HTML. El lector podrá saber cómo llegar hasta los datos que no se pueden obtener con solicitudes GET y POST. Vamos a elegir algún sitio web con páginas no demasiado voluminosas, e intentaremos obtener del mismo información interesante.

Creando un EA gradador multiplataforma (Parte III): cuadrícula de correcciones con martingale Creando un EA gradador multiplataforma (Parte III): cuadrícula de correcciones con martingale

En este artículo, intentaremos crear el mejor asesor posible de aquellos que funcionan según el principio del gradador. Como siempre, se tratará de un asesor multiplataforma, capaz de funcionar tanto en MetaTrader 4, como en MetaTrader 5. El primer asesor era bueno en todo, excepto en que no podía traer beneficios en un periodo de tiempo prolongado. El segundo asesor podía funcionar en intervalos superiores a varios años. Pero era incapaz de lograr más de un 50% de beneficio anual con una reducción máxima inferior al 50%.

Creando un EA gradador multiplataforma (última parte): la diversificación como método para aumentar la rentabilidad Creando un EA gradador multiplataforma (última parte): la diversificación como método para aumentar la rentabilidad

En los anteriores artículos de la serie, hemos intentado crear de formas distintas un asesor gradador más o menos rentable. En esta ocasión, vamos a tratar de aumentar la rentabilidad del asesor comercial con la ayuda de la diversificación. Nuestro objetivo es el 100% de beneficio anual anhelado por todos, con un 20% de reducción máxima del balance.

Constructor de estrategias basado en las figuras técnicas de Merrill Constructor de estrategias basado en las figuras técnicas de Merrill

En el artículo anterior, analizamos un modelo de aplicación de las figuras técnicas de Merrill a diferentes datos, tales como el valor del precio en el gráfico de un instrumento de divisa y los valores de los diferentes indicadores del paquete estándar del terminal MetaTrader 5: ATR, WPR, CCI, RSI y otros. Ahora, vamos a intentar crear un constructor de estrategias basado en las ideas sobre el uso de las figuras técnicas de Merrill.