Recetas MQL5 – Prueba de estrés de una estrategia comercial con ayuda de símbolos personalizados
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.
Fig.1 Símbolos para la descarga en la pestaña "Data" en el programa Quant Data Manager
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 Ltd.
Artículo original: https://www.mql5.com/ru/articles/7166
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso