
Recetas MQL5 - Señales comerciales de pivotes
Introducción
El presente artículo continúa la serie en la que se describen los indicadores y configuraciones que generan las señales comerciales. Esta vez, hablaremos de los pivotes, niveles o puntos de reversa. Usaremos de nuevo la Librería estándar. Primero, trabajaremos con el indicador de los niveles de reversa, y luego basándose en él, crearemos una estrategia base que seguiremos modificando.
Es deseable que el lector conozca la clase base para la creación de los generadores de señales comerciales CExpertSignal.
1. Indicador de pivotes — niveles de reversa
Para la estrategia va a usarse el indicador que dibuja los niveles de una posible reversa. En este caso, el dibujado va a realizarse sólo a través de los medios de la construcción gráfica, sin usar los objetos gráficos. La ventaja principal de este enfoque es la posibilidad de acceder al indicador en el modo de optimización. Su desventaja consiste en el hecho de que las construcciones gráficas no pueden salir fuera de los límites de los búferes indicadores, lo que significa que en las barras no habrá líneas en el futuro.
Los niveles se puede calcular de varias maneras diferentes. Para aclarar mejor este asunto, lea el artículo «Estrategia de trading basadas en el análisis de los puntos pivote (Pivot Points)».
Por ahora, nos centraremos en el enfoque clásico cuando los niveles se definen de acuerdo con las fórmulas siguientes:
RES es el nivel i de resistencia, y SUP es el nivel i de soporte. En total habrá: 1 nivel base de reversa (PP), 6 niveles de resistencia (RES) y 6 niveles de soporte (SUP).
Pues, el indicador tiene aspecto de un conjunto de niveles horizontales construidos a base de diferentes precios. La primera ejecución del indicador en el gráfico construirá los niveles solamente para el día actual (Fig. 1).
Fig. 1 Indicador de pivotes: dibujado para el día actual
Examinaremos el código del indicador bloque por bloque, empezando de la parte de cálculo.
Entonces, cuando empieza un día nuevo, es necesario volver a calcular todos los niveles de reversa.
//--- si hay un día nuevo if(gNewDay.isNewBar(today)) { PrintFormat("Nuevo día: %s",TimeToString(today)); //--- normalización de precios double d_high=NormalizeDouble(daily_rates[0].high,_Digits); double d_low=NormalizeDouble(daily_rates[0].low,_Digits); double d_close=NormalizeDouble(daily_rates[0].close,_Digits); //--- memorizar los precios gYesterdayHigh=d_high; gYesterdayLow=d_low; gYesterdayClose=d_close; //--- 1) pivote: PP = (HIGH + LOW + CLOSE) / 3 gPivotVal=NormalizeDouble((gYesterdayHigh+gYesterdayLow+gYesterdayClose)/3.,_Digits); //--- 4) RES1.0 = 2*PP - LOW gResVal_1_0=NormalizeDouble(2.*gPivotVal-gYesterdayLow,_Digits); //--- 5) SUP1.0 = 2*PP – HIGH gSupVal_1_0=NormalizeDouble(2.*gPivotVal-gYesterdayHigh,_Digits); //--- 8) RES2.0 = PP + (HIGH -LOW) gResVal_2_0=NormalizeDouble(gPivotVal+(gYesterdayHigh-gYesterdayLow),_Digits); //--- 9) SUP2.0 = PP - (HIGH – LOW) gSupVal_2_0=NormalizeDouble(gPivotVal-(gYesterdayHigh-gYesterdayLow),_Digits); //--- 12) RES3.0 = 2*PP + (HIGH – 2*LOW) gResVal_3_0=NormalizeDouble(2.*gPivotVal+(gYesterdayHigh-2.*gYesterdayLow),_Digits); //--- 13) SUP3.0 = 2*PP - (2*HIGH – LOW) gSupVal_3_0=NormalizeDouble(2.*gPivotVal-(2.*gYesterdayHigh-gYesterdayLow),_Digits); //--- 2) RES0.5 = (PP + RES1.0) / 2 gResVal_0_5=NormalizeDouble((gPivotVal+gResVal_1_0)/2.,_Digits); //--- 3) SUP0.5 = (PP + SUP1.0) / 2 gSupVal_0_5=NormalizeDouble((gPivotVal+gSupVal_1_0)/2.,_Digits); //--- 6) RES1.5 = (RES1.0 + RES2.0) / 2 gResVal_1_5=NormalizeDouble((gResVal_1_0+gResVal_2_0)/2.,_Digits); //--- 7) SUP1.5 = (SUP1.0 + SUP2.0) / 2 gSupVal_1_5=NormalizeDouble((gSupVal_1_0+gSupVal_2_0)/2.,_Digits); //--- 10) RES2.5 = (RES2.0 + RES3.0) / 2 gResVal_2_5=NormalizeDouble((gResVal_2_0+gResVal_3_0)/2.,_Digits); //--- 11) SUP2.5 = (SUP2.0 + SUP3.0) / 2 gSupVal_2_5=NormalizeDouble((gSupVal_2_0+gSupVal_3_0)/2.,_Digits); //--- barra del inicio del día actual gDayStart=today; //--- encontrar la barra inicial del timeframe activo //--- como serie temporal for(int bar=0;bar<rates_total;bar++) { //--- hora de la barra seleccionada datetime curr_bar_time=time[bar]; user_date.DateTime(curr_bar_time); //--- día de la barra seleccionada datetime curr_bar_time_of_day=user_date.DateOfDay(); //--- si la barra actual fue el día anterior if(curr_bar_time_of_day<gDayStart) { //--- fijar la barra inicial gBarStart=bar-1; break; } } //--- resetear el contador local prev_calc=0; }
He utilizado el color rojo para destacar las líneas donde exactamente se recalculan los niveles. Luego hay que encontrar el número de la barra para el timeframe actual, que servirá de inicio para el dibujado de los niveles. La variable gBarStart se encarga de su valor. Durante la búsqueda, se activa la estructura SUserDateTimepara trabajar con fechas y horas, siendo el descendiente de la estructura CDateTime.
Ahora diré algunas palabras sobre el bloque donde se realiza el llenado de los valores de búfer para las barras del timeframe actual.
//--- si hay nueva barra en el timeframe actual if(gNewMinute.isNewBar(time[0])) { //--- hasta que barra contamos int bar_limit=gBarStart; //--- si no es la primer ejecución if(prev_calc>0) bar_limit=rates_total-prev_calc; //--- cálculo de búferes for(int bar=0;bar<=bar_limit;bar++) { //--- 1) пивот gBuffers[0].data[bar]=gPivotVal; //--- 2) RES0.5 if(gToPlotBuffer[1]) gBuffers[1].data[bar]=gResVal_0_5; //--- 3) SUP0.5 if(gToPlotBuffer[2]) gBuffers[2].data[bar]=gSupVal_0_5; //--- 4) RES1.0 if(gToPlotBuffer[3]) gBuffers[3].data[bar]=gResVal_1_0; //--- 5) SUP1.0 if(gToPlotBuffer[4]) gBuffers[4].data[bar]=gSupVal_1_0; //--- 6) RES1.5 if(gToPlotBuffer[5]) gBuffers[5].data[bar]=gResVal_1_5; //--- 7) SUP1.5 if(gToPlotBuffer[6]) gBuffers[6].data[bar]=gSupVal_1_5; //--- 8) RES2.0 if(gToPlotBuffer[7]) gBuffers[7].data[bar]=gResVal_2_0; //--- 9) SUP2.0 if(gToPlotBuffer[8]) gBuffers[8].data[bar]=gSupVal_2_0; //--- 10) RES2.5 if(gToPlotBuffer[9]) gBuffers[9].data[bar]=gResVal_2_5; //--- 11) SUP2.5 if(gToPlotBuffer[10]) gBuffers[10].data[bar]=gSupVal_2_5; //--- 12) RES3.0 if(gToPlotBuffer[11]) gBuffers[11].data[bar]=gResVal_3_0; //--- 13) SUP3.0 if(gToPlotBuffer[12]) gBuffers[12].data[bar]=gSupVal_3_0; } }
El cálculo de los búferes empieza cuando aparece nueva barra en el gráfico en el que ha sido iniciado el indicador. Con el color amarillo se marca la definición del número de la barra hasta que vamos a calcular los búferes. Para eso se utiliza el contador local de barras contadas. Lo necesitamos porque el inicio del nuevo día no reseteará el valor de la constante prev_calculated a cero, mientras que este reseteo es necesario.
El código completo del indicador de pivotes se encuentra en el archivo Pivots.mq5.
2. Estrategia básica
Intentaremos crear una estrategia básica simple basándose en el indicador descrito. Supongamos que la señal de apertura depende de la posición del precio de apertura del día respecto al pivote central. Cuando el precio toque el nivel del pivote, la señal se quedará confirmada.
Así, en el gráfico EURUSD, M15 ( Рис.2 ) se muestra la situación cuando la sesión de 14 de enero de 2015 fue abierta por debajo del pivote central, y luego el precio intradía tocó su nivel de abajo arriba. De esta manera, apareció una señal de venta. Si no se ha activado el Stop Loss ni el Take Profit, vamos a cerrar al comenzar el nuevo día.
Fig. 2. Estrategia básica: señal de venta
Los niveles de Stop Loss y Take Profit los vamos a vincular a los niveles de reversa del indicador de pivote. Del Stop para la venta nos servirá el nivel intermedio de resistencia Res0.5 ubicado en $1.18153. Como el Stop para el beneficio, usaremos el nivel base de soporte Sup1.0 en $1.17301 . Más tarde volveremos a la sesión de 14 de enero. Mientras tanto hablaremos del código que será la base algorítmica de la estrategia básica.
2.1 Clase de señal CSignalPivots
Crearemos la clase de señal que va a generar las señales de parte de varios modelos formados a base de la dinámica de precios y el indicador de los niveles de reversa.
//+------------------------------------------------------------------+ //| Class CSignalPivots | //| Purpose: Clase de señales comerciales a base de pivotes. | //| Descendiente de la clase CExpertSignal. | //+------------------------------------------------------------------+ class CSignalPivots : public CExpertSignal { //--- === Data members === --- protected: CiCustom m_pivots; // objeto-indicador "Pivots" //--- parámetros personalizados bool m_to_plot_minor; // dibujado de niveles secundarios double m_pnt_near; // tolerancia //--- de cálculo double m_pivot_val; // valor del pivote double m_daily_open_pr; // precio de apertura del día actual CisNewBar m_day_new_bar; // nueva barra del timeframe diario //--- modelos del mercado //--- 1) Modelo 0 «primer toque del nivel PP» (encima - buy, abajo - sell) int m_pattern_0; // peso bool m_pattern_0_done; // indicio del modelo modificado //--- === Methods === --- public: //--- constructor/destructor void CSignalPivots(void); void ~CSignalPivots(void){}; //--- métodos del establecimiento de parámetros personalizados void ToPlotMinor(const bool _to_plot) {m_to_plot_minor=_to_plot;} void PointsNear(const uint _near_pips); //--- métodos de configuración de los «pesos» de los modelos de mercado void Pattern_0(int _val) {m_pattern_0=_val;m_pattern_0_done=false;} //--- método de verificación de configuraciones virtual bool ValidationSettings(void); //--- método de la creación del indicador y serie temporal virtual bool InitIndicators(CIndicators *indicators); //--- métodos de verificación, si los modelos del mercado están formados virtual int LongCondition(void); virtual int ShortCondition(void); virtual double Direction(void); //--- métodos de definición de los niveles de la entrada en el mercado virtual bool OpenLongParams(double &price,double &sl,double &tp,datetime &expiration); virtual bool OpenShortParams(double &price,double &sl,double &tp,datetime &expiration); //--- protected: //--- método de inicialización del indicador bool InitCustomIndicator(CIndicators *indicators); //--- obtención del valor del nivel del pivote double Pivot(void) {return(m_pivots.GetData(0,0));} //--- obtención del valor del nivel principal de resistencia double MajorResistance(uint _ind); //--- obtención del valor del nivel secudario de resistencia double MinorResistance(uint _ind); //--- obtención del valor del nivel principal de soporte double MajorSupport(uint _ind); //--- obtención del valor del nivel secundario de soporte double MinorSupport(uint _ind); }; //+------------------------------------------------------------------+
En mi artículo «Recetas MQL5 - Señales comerciales de los canales móviles» ya utilizaba el enfoque cuando el contacto del precio con una línea se fija, si el precio entra en los alrededores de esta línea. El miembro de datos m_pnt_near definirá la tolerancia para el nivel de reversa.
Tal vez el papel más importante en la clase deba asignarse al modelo de señal que se atiende por esta clase. En la clase base habrá un modelo. Además del peso (m_pattern_0), también va a tener el indicio del procesamiento durante el día de trading (m_pattern_0_done).
La clase base de señal CExpertSignal abunda de métodos virtuales. Y esta riqueza permite implementar una configuración fina de la clase derivada.
En particular, para calcular los niveles de trading, he redefinido 2 métodos OpenLongParams() y OpenShortParams().
Examinaremos el código del primer método: determinación de los valores para los niveles comerciales durante la compra.
//+------------------------------------------------------------------+ //| Determinación de niveles comerciales para la compra | //+------------------------------------------------------------------+ bool CSignalPivots::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration) { bool params_set=false; sl=tp=WRONG_VALUE; //--- si el Modelo 0 se considera if(IS_PATTERN_USAGE(0)) //--- si el Modelo 0 no está procesado if(!m_pattern_0_done) { //--- precio de apertura - al mercado double base_price=m_symbol.Ask(); price=m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit()); //--- precio sl - nivel Sup0.5 sl=this.MinorSupport(0); if(sl==DBL_MAX) return false; //--- si se establece el precio sl sl=m_symbol.NormalizePrice(sl); //--- precio tp - nivel Res1.0 tp=this.MajorResistance(0); if(tp==DBL_MAX) return false; //--- si se establece el precio tp tp=m_symbol.NormalizePrice(tp); expiration+=m_expiration*PeriodSeconds(m_period); //--- si los precios están establecidos params_set=true; //--- modelo procesado m_pattern_0_done=true; } //--- return params_set; } //+------------------------------------------------------------------+
El precio de Stop Loss se calcula como el valor del primer nivel secundario de soporte usando el método MinorSupport(). El Profit se define según el precio del primer nivel base de resistencia usando a través del método MajorResistance(). Para la venta, estos métodos serán sustituidos por MinorResistance() y MajorSupport(), respectivamente.
Para que se activen los métodos de definición de niveles comerciales en la clase derivada, hay que hacer que la señal personalizada sea principal. El método de definición de los niveles comerciales de la clase principal tiene el siguiente aspecto:
//+------------------------------------------------------------------+ //| Detecting the levels for buying | //+------------------------------------------------------------------+ bool CExpertSignal::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration) { CExpertSignal *general=(m_general!=-1) ? m_filters.At(m_general) : NULL; //--- if(general==NULL) { //--- if a base price is not specified explicitly, take the current market price double base_price=(m_base_price==0.0) ? m_symbol.Ask() : m_base_price; price =m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit()); sl =(m_stop_level==0.0) ? 0.0 : m_symbol.NormalizePrice(price-m_stop_level*PriceLevelUnit()); tp =(m_take_level==0.0) ? 0.0 : m_symbol.NormalizePrice(price+m_take_level*PriceLevelUnit()); expiration+=m_expiration*PeriodSeconds(m_period); return(true); } //--- return(general.OpenLongParams(price,sl,tp,expiration)); } //+------------------------------------------------------------------+
Si el índice de la señal principal no está establecido, los niveles obtendrán los valores predefinidos. Para evitar esta situación, durante la inicialización de la señal se puede escribir lo siguiente en el código del Asesor Experto:
//--- filtro CSignalPivots CSignalPivots *filter0=new CSignalPivots; if(filter0==NULL) { //--- error PrintFormat(__FUNCTION__+": error creating filter0"); return INIT_FAILED; } signal.AddFilter(filter0); signal.General(0);
El método de comprobación de la condición de compra está presentado como sigue:
//+------------------------------------------------------------------+ //| Comprobación de la condición de compra | //+------------------------------------------------------------------+ int CSignalPivots::LongCondition(void) { int result=0; //--- si el Modelo 0 se considera if(IS_PATTERN_USAGE(0)) //--- si el Modelo 0 no está procesado if(!m_pattern_0_done) //--- si el día se ha abierto encima del pivote if(m_daily_open_pr>m_pivot_val) { //--- precio mínimo en la barra actual double last_low=m_low.GetData(1); //--- si el precio se ha obtenido if((last_low>WRONG_VALUE) && (last_low<DBL_MAX)) //--- si el toque ha sido arriba (teniendo en cuenta la tolerancia) if(last_low<=(m_pivot_val+m_pnt_near)) { result=m_pattern_0; //--- en el Diario Print("\n---== El precio toca el nivel del pivote arriba ==---"); PrintFormat("Precio: %0."+IntegerToString(m_symbol.Digits())+"f",last_low); PrintFormat("Pivote: %0."+IntegerToString(m_symbol.Digits())+"f",m_pivot_val); PrintFormat("Tolerancia: %0."+IntegerToString(m_symbol.Digits())+"f",m_pnt_near); } } //--- return result; } //+------------------------------------------------------------------+
Es fácil de notar que el toque arriba se comprueba tomando en cuenta la tolerancia last_low<=(m_pivot_val+m_pnt_near).
El método de definición de la dirección «ponderada» Direction(), entre otras cosas, comprueba el procesamiento del modelo base.
//+------------------------------------------------------------------+ //| Definición de la dirección «ponderada» | //+------------------------------------------------------------------+ double CSignalPivots::Direction(void) { double result=0.; //--- obtener datos históricos diarios MqlRates daily_rates[]; if(CopyRates(_Symbol,PERIOD_D1,0,1,daily_rates)<0) return 0.; //--- si el Modelo 0 ha sido procesado if(m_pattern_0_done) { //--- comprobar la aparición de nuevo día if(m_day_new_bar.isNewBar(daily_rates[0].time)) { //--- resetear la bandera del procesamiento del modelo m_pattern_0_done=false; return 0.; } } //--- si el Modelo 0 no ha sido procesado else { //--- precio de apertura del día if(m_daily_open_pr!=daily_rates[0].open) m_daily_open_pr=daily_rates[0].open; //--- pivote double curr_pivot_val=this.Pivot(); if(curr_pivot_val<DBL_MAX) if(m_pivot_val!=curr_pivot_val) m_pivot_val=curr_pivot_val; } //--- resultado result=m_weight*(this.LongCondition()-this.ShortCondition()); //--- return result; } //+------------------------------------------------------------------+
En cuanto a las señales para el cierre, vamos a redefinir los métodos de la clase principal CloseLongParams() y CloseShortParams(). Aquí tenemos el ejemplo del código para el bloque de la compra:
//+------------------------------------------------------------------+ //| Determinación del nivel comercial para la compra | //+------------------------------------------------------------------+ bool CSignalPivots::CloseLongParams(double &price) { price=0.; //--- si el Modelo 0 se considera if(IS_PATTERN_USAGE(0)) //--- si el Modelo 0 no está procesado if(!m_pattern_0_done) { price=m_symbol.Bid(); //--- en el Registro Print("\n---== Señal para cerrar la compra ==---"); PrintFormat("Precio del mercado: %0."+IntegerToString(m_symbol.Digits())+"f",price); return true; } //--- return the result return false; } //+------------------------------------------------------------------+
El límite de la señal para el cierre debe ser puesto a cero en el código del propio Asesor Experto.
Entonces, en la clase principal no habrá comprobación según la dirección.
//+------------------------------------------------------------------+ //| Generating a signal for closing of a long position | //+------------------------------------------------------------------+ bool CExpertSignal::CheckCloseLong(double &price) { bool result =false; //--- the "prohibition" signal if(m_direction==EMPTY_VALUE) return(false); //--- check of exceeding the threshold value if(-m_direction>=m_threshold_close) { //--- there's a signal result=true; //--- try to get the level of closing if(!CloseLongParams(price)) result=false; } //--- zeroize the base price m_base_price=0.0; //--- return the result return(result); } //+------------------------------------------------------------------+
Surge la pregunta, ¿a base de qué en este caso va a verificarse la señal del cierre? En primer lugar, va a verificarse gracias a la presencia de la posición (en el método Processing() ), y segundo, gracias a la característica m_pattern_0_done (en los métodos redifinidos CloseLongParams() y CloseShortParams() ). En total, en cuanto el EA detecte la presencia de una posición cuando el Modelo 0 no está procesado, enseguida intentará cerrar la posoción. Eso ocurre al iniciarse la sesión de trading.
Hemos considerado los principios de la clase personalizada de señal CSignalPivots, ahora hablaremos sobre la clase de la estrategia.
2.2 Clase de la estrategia comercial CPivotsExpert
La clase derivada de la estrategia parece a la clase análoga para los canales móviles. Las diferencias son las siguientes: en primer lugar, no se utiliza el modo de trading a base de ticks, sino el modo basado en minutos. Eso permitirá testear rápidamente la estrategia a base del historial bastante profundo. En segundo lugar, hay una verificación para cerrar la posición. Más arriba ya hemos determinado la razón por la que el Asesor Experto puede cerrar la posición.
El principal método procesador es el siguiente:
//+------------------------------------------------------------------+ //| Módulo principal | //+------------------------------------------------------------------+ bool CPivotsExpert::Processing(void) { //--- nueva barra en minutos if(!m_minute_new_bar.isNewBar()) return false; //--- cálculo de dirección m_signal.SetDirection(); //--- si no hay posición if(!this.SelectPosition()) { //--- módulo de apertura de posición if(this.CheckOpen()) return true; } //--- si hay posición else { //--- módulo del cierre de posición if(this.CheckClose()) return true; } //--- si no hay operaciones comerciales return false; } //+------------------------------------------------------------------+
Pues, eso es todo. Ahora podemos ejecutar la estrategia básica. Su código está disponible en el archivo BasePivotsTrader.mq5.
Fig. 3. Estrategia básica: venta
Volvamos al ejemplo de 14 de enero de 2015. En este caso, el modelo fue procesado perfectamente. Abrimos hacia abajo en el pivote, y cerramos en el nivel del soporte principal Sup1.0.
Fue realizado un repaso en el Probador de Estrategias de 07.01.2013 a 07.01.2017 en el período de tiempo de 15 minutos para el símbolo EURUSD con los siguientes valores de parámetros:
- Límite de señal para la apertura, [0...100] = 10;
- Peso, [0...1.0] = 1,0;
- Volumen fijo = 0,1;
- Tolerancia, pp = 15.
Al final, la estrategia tradeaba con un resultado estable. La verdad es que este resultado fue negativo (Fig. 4).
Fig. 4 EURUSD: resultados de la primera estrategia básica para 2013-2016.
A juzgar por el gráfico de la eficacia, lo hicimos todo erróneamente. Había que comprar cuando llegaba la señal de venta, y vender en caso de la señal de compra. ¿Es así? Intentaremos de comprobarlo. Para eso crearemos la segunda estrategia básica.
Introduciremos algunas modificaciones en la parte de las señales. Entonces, como ejemplo, la condición de compra será como sigue:
//+------------------------------------------------------------------+ //| Comprobación de la condición de venta | //+------------------------------------------------------------------+ int CSignalPivots::LongCondition(void) { int result=0; //--- si el Modelo 0 se considera if(IS_PATTERN_USAGE(0)) //--- si el Modelo 0 no está procesado if(!m_pattern_0_done) //--- si el día se ha abierto por debajo del pivote if(m_daily_open_pr<m_pivot_val) { //--- precio máximo en la barra actual double last_high=m_high.GetData(1); //--- si el precio se ha obtenido if((last_high>WRONG_VALUE) && (last_high<DBL_MAX)) //--- si el toque ha sido abajo (teniendo en cuenta la tolerancia) if(last_high>=(m_pivot_val-m_pnt_near)) { result=m_pattern_0; //--- en el Diario Print("\n---== El precio toca el nivel del pivote abajo ==---"); PrintFormat("Цена: %0."+IntegerToString(m_symbol.Digits())+"f",last_high); PrintFormat("Pivote: %0."+IntegerToString(m_symbol.Digits())+"f",m_pivot_val); PrintFormat("Tolerancia: %0."+IntegerToString(m_symbol.Digits())+"f",m_pnt_near); } } //--- return result; } //+------------------------------------------------------------------+
Volvemos a repasar la estrategia en el Probador y obtenemos el siguiente resultado:
Fig. 5 EURUSD: resultados de la segunda estrategia básica para 2013-2016.
Es evidente que no hubo «efecto espejado» de la primera parte. Obviamente, eso se debe al tamaño del Stop Loss y Take Profit. Aparte de eso, con el inicio del nuevo día se cerraban las posiciones para las que no se activó el Stop ni Profit durante la sesión de trading.
Intentaremos de nuevo modificar la estrategia básica, en particular, su segunda versión. Hagamos que para la compra el nivel de Stop Loss se coloque más allá -hasta el soporte principal Sup1.0- y el tamaño del Take Profit se limite por el nivel intermedio de resistencia Res0.5. Para la venta, colocaremos el Stop en el nivel Res1.0, y el Profit, en Sup0.5.
De esta manera, por ejemplo, los niveles comerciales para la compra van a determinarse así:
//+------------------------------------------------------------------+ //| Determinación de niveles comerciales para la compra | //+------------------------------------------------------------------+ bool CSignalPivots::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration) { bool params_set=false; sl=tp=WRONG_VALUE; //--- si el Modelo 0 se considera if(IS_PATTERN_USAGE(0)) //--- si el Modelo 0 no está procesado if(!m_pattern_0_done) { //--- precio de apertura — al mercado double base_price=m_symbol.Ask(); price=m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit()); //--- precio sl - nivel Sup1.0 sl=this.MajorSupport(0); if(sl==DBL_MAX) return false; //--- si se establece el precio sl sl=m_symbol.NormalizePrice(sl); //--- precio tp - nivel Res0.5 tp=this.MinorResistance(0); if(tp==DBL_MAX) return false; //--- si se establece el precio tp tp=m_symbol.NormalizePrice(tp); expiration+=m_expiration*PeriodSeconds(m_period); //--- si los precios están establecidos params_set=true; //--- modelo procesado m_pattern_0_done=true; } //--- return params_set; } //+------------------------------------------------------------------+
En el Probador, se obtendrá el siguiente resultado para la tercera versión:
Fig. 6 EURUSD: resultados de la tercera estrategia básica para 2013-2016 гг.
Se ha obtenido una imagen más o menos parecida a la imagen espejada para la primera versión. A primera vista, parece que el Grial ha sido encontrado. Pero hay unos escollos que serán discutidos en la siguiente sección.
3. Sobre la cuestión de robustez
Si nos fijamos atentamente en la Fig. 6, es fácil de notar que la curva del balance iba creciendo de forma desigual. Había áreas donde el balance acumulaba el beneficio de manera constante. Había áreas de reducción. Además, también había áreas donde la curva del balance se movía directamente a la derecha.
En general, se puede decir que a la estrategia le falta la robustez. ¿Se puede aumentarla? Lo intentaremos.
3.1 Indicador de tendencia
En mi opinión, las reglas comerciales trabajan mejor cuando en el mercado hay un movimiento dirigido, es decir una tendencia. Así, la estrategia mostraba el mejor resultado para EURUSD en 2014 y el principio de 2015, cuando el par estaba disminuyendo progresivamente.
Resulta que hace falta algún tipo del filtro que permite evitar el movimiento lateral. Respecto al tema sobre la definición de una tendencia estable hay un montón de materiales, inclusive en la sección "Artículos" en mql5.com. Personalmente a mí, me ha gustado el artículo «Distintas maneras para averiguar la tendencia en MQL5». Su autor ofrece una manera cómoda y, lo más importante, universal para buscar la tendencia.
He creado el indicador semejante MaTrendCatcher. En él se usan dos medias móviles, una rápida y otra lenta. Si la diferencia entre los valores es positiva, consideramos que la tendencia alcista. Entonces, en el indicador, las columnas del histograma serán iguales a 1. Si esta diferencia será negativa, entonces en el mercado reina una tendencia bajista. Las columnas serán iguales a menos 1 (Fig. 7).
Fig. 7 Indicador de la tendencia MaTrendCatcher
Además de eso, si en relación a la barra anterior, la diferencia entre las MA se aumenta (la tendencia se fortalece), la columna será verde, si se reduce, roja.
Existe un recurso más añadido al indicador: ahí donde la diferencia entre las MA es pequeña, las columnas no van a visualizarse.El parámetro personalizado del indicador «Corte, pp» (Fig. 8) establece el grado de la diferencia necesaria para ocultar su visualización.
Fig. 8 Indicador de tendencia MaTrendCatcher, corte de diferencias pequeñas
Así, para los efectos de la filtración, usaremos el indicador MaTrendCatcher.
Para activar el indicador, es necesario introducir algunas modificaciones en el código de los archivos del proyecto. Nótese que la última versión del Asesor Experto va a guardarse en la carpeta Model..
Para los efectos de la estrategia era necesario obtener el valor calculado de la dirección «ponderada». Por eso, se ha tenido que crear una clase descendiente personalizada de la clase básica de señal.
Luego, en la clase actualizada de señal para los niveles de reversa, ha aparecido un nuevo modelo— Modelo 1 «tendencia-flat-contratendencia».
La verdad es que completa el Modelo 0. Por eso, puede llamarse el submodelo. Más tarde, reflejaremos este momento en el código.
Ahora, el método para comprobar la condición de compra es el siguiente:
//+------------------------------------------------------------------+ //| Comprobación de la condición de compra | //+------------------------------------------------------------------+ int CSignalPivots::LongCondition(void) { int result=0; //--- si el Modelo 0 se considera if(IS_PATTERN_USAGE(0)) //--- si el Modelo 0 no está procesado if(!m_pattern_0_done) { m_is_signal=false; //--- si el día se ha abierto por debajo del pivote if(m_daily_open_pr<m_pivot_val) { //--- precio máximo en la barra anterior double last_high=m_high.GetData(1); //--- si el precio se ha obtenido if(last_high>WRONG_VALUE && last_high<DBL_MAX) //--- si el toque ha sido abajo (teniendo en cuenta la tolerancia) if(last_high>=(m_pivot_val-m_pnt_near)) { result=m_pattern_0; m_is_signal=true; //--- en el Diario this.Print(last_high,ORDER_TYPE_BUY); } } //--- si el Modelo 1 se considera if(IS_PATTERN_USAGE(1)) { //--- si en la barra anterior había la tendencia alcista if(m_trend_val>0. && m_trend_val!=EMPTY_VALUE) { //--- si hay aceleración if(m_trend_color==0. && m_trend_color!=EMPTY_VALUE) result+=(m_pattern_1+m_speedup_allowance); //--- si no hay aceleración else result+=(m_pattern_1-m_speedup_allowance); } } } //--- return result; }
He marcado en verde el bloque donde el submodelo entra en el juego.
La finalidad del cálculo es la siguiente: si la entrada en el mercado ha sido realizada sin considerar el submodelo, el resultado de la señal será igual al peso del Modelo 0. Si el submodelo se toma en cuenta, las opciones son las siguientes:
- la entrada según la tendencia con aceleración (premio por la tendencia y premio por la aceleración);
- la entrada según la tendencia sin aceleración (premio por la tendencia y multa por la aceleración);
- la entrada contra la tendencia con aceleración (multa por la contratendencia y multa por la aceleración);
- la entrada contra la tendencia sin aceleración (multa por la contratendencia y premio por la aceleración);
Este enfoque permitirá no reaccionar a una señal débil. Si el peso de la señal supera el valor del límite, entonces influye en el tamaño del volumen comercial. En la clase del Asesor Experto para los pivotes existe el método CPivotsExpert::LotCoefficient():
//+------------------------------------------------------------------+ //| Coeficiente del lote | //+------------------------------------------------------------------+ double CPivotsExpert::LotCoefficient(void) { double lot_coeff=1.; //--- señal común CExpertUserSignal *ptr_signal=this.Signal(); if(CheckPointer(ptr_signal)==POINTER_DYNAMIC) { double dir_val=ptr_signal.GetDirection(); lot_coeff=NormalizeDouble(MathAbs(dir_val/100.),2); } //--- return lot_coeff; } //+------------------------------------------------------------------+
Si la señal ha reunido 120 puntos, el volumen inicial se corregirá a 1,2, si ha reunido 70, a 0,7.
Para aplicar el coeficiente, hay que redefinir los métodos OpenLong() y OpenShort(). En particular, el método para la compra se presenta así:
//+------------------------------------------------------------------+ //| Long position open or limit/stop order set | //+------------------------------------------------------------------+ bool CPivotsExpert::OpenLong(double price,double sl,double tp) { if(price==EMPTY_VALUE) return(false); //--- get lot for open double lot_coeff=this.LotCoefficient(); double lot=LotOpenLong(price,sl); lot=this.NormalLot(lot_coeff*lot); //--- check lot for open lot=LotCheck(lot,price,ORDER_TYPE_BUY); if(lot==0.0) return(false); //--- return(m_trade.Buy(lot,price,sl,tp)); } //+------------------------------------------------------------------+
La idea con la formación dinámica del tamaño del lote es bastante sencilla: cuanto más fuerte sea la señal, mayor será el riesgo.
3.2 Tamaño del intervalo
También es fácil de notar que cuando los niveles de reversa (pivotes) se encuentran cerca uno de otro, eso indica en una baja volatilidad en el mercado. Para abstenerse de operar durante estos días, ha sido introducido el parámetro «Límite del ancho, pp». El Modelo 0 (y también el submodelo) va a considerarse como procesado, si el umbral del límite no ha sido superado. Este límite se comprueba en el cuerpo del método Direction(). Aquí tenemos una parte del código:
//--- si el límite está establecido if(m_wid_limit>0.) { //--- límite superior de cálculo double norm_upper_limit=m_symbol.NormalizePrice(m_wid_limit+m_pivot_val); //--- límite superior de real double res1_val=this.MajorResistance(0); if(res1_val>WRONG_VALUE && res1_val<DBL_MAX) { //--- si el límite no está superado if(res1_val<norm_upper_limit) { //--- el Modelo 0 ha sido procesado m_pattern_0_done=true; //--- en el Diario Print("\n---== El límite superior no está superado ==---"); PrintFormat("De cálculo: %0."+IntegerToString(m_symbol.Digits())+"f",norm_upper_limit); PrintFormat("Real: %0."+IntegerToString(m_symbol.Digits())+"f",res1_val); //--- return 0.; } } //--- límite inferior de cálculo double norm_lower_limit=m_symbol.NormalizePrice(m_pivot_val-m_wid_limit); //--- límite inferior real double sup1_val=this.MajorSupport(0); if(sup1_val>WRONG_VALUE && sup1_val<DBL_MAX) { //--- si el límite no está superado if(norm_lower_limit<sup1_val) { //--- el Modelo 0 ha sido procesado m_pattern_0_done=true; //--- en el Diario Print("\n---== El límite inferior no está superado ==---"); PrintFormat("De cálculo: %0."+IntegerToString(m_symbol.Digits())+"f",norm_lower_limit); PrintFormat("Real: %0."+IntegerToString(m_symbol.Digits())+"f",sup1_val); //--- return 0.; } } }
Y si la verificación del ancho del intervalo no ha sido superada, en el diario aparecerá, por ejemplo, la siguiente entrada:
2015.08.19 00:01:00 ---== El límite superior no está superado ==--- 2015.08.19 00:01:00 De cálculo: 1.10745 2015.08.19 00:01:00 Real: 1.10719O sea, para ser completo, a la señal le ha faltado 26 pp.
Ejecutamos la estrategia en el Probador de Estrategias en el modo de optimización. He utilizado los siguientes parámetros de optimización:
- «Límite del ancho, pp»;
- «Tolerancia, pp»;
- «MA rápida»;
- «MA lenta»;
- «Corte, pp»;
En términos de rentabilidad, el repaso más acertado es el siguiente:
Fig. 9 EURUSD: resultados de la estrategia con uso de filtros para 2013-2016.
Como era de esperar, algunas señales han sido eliminadas. La curva del balance se ha hecho más suave.
Pero cabe mencionar lo que no ha salido. Como se puede observar en el gráfico, desde el año 2015 la estrategia genera las áreas en las que la curva del balance fluctúa en una franja estrecha, sin un crecimiento explícito del beneficio. Los resultados de la optimización se encuentran en el archivo EURUSD_model.xml.
Vamos a ver los resultados en otros símbolos.
Así, en el par USDJPY, el mejor repaso se muestra en la Fig.10.
Fig. 10 USDJPY: resultados de la estrategia con uso de filtros para 2013-2016.
Ahora, echamos un vistazo en el oro «spot». El mejor resultado se muestra en la Fig. 11.
Fig. 11 XAUUSD: resultados de la estrategia con uso de filtros para 2013-2016.
Durante este período, el metal precioso se negociaba en una franja estrecha, por eso la estrategia no ha tenido un resultado positivo.
Lo que se refiere a la libra esterlina, el mejor de los repasos se muestra en la Fig. 12.
Fig. 12 GBPUSD: resultados de la estrategia con uso de filtros para 2013-2016.
La libra se negociaba bastante bien de acuerdo con la tendencia. Pero la corrección al principio del 2015 ha estropeado el resultado final.
En un todo, la estrategia trabaja mejor cuando en el mercado hay una tendencia.
Conclusión
En conclusión, cabe mencionar que el proceso de la creación de una estrategia de trading se compone de varias etapas. En la fase inicial, se formula la idea de trading. Normalmente, ésta es una hipótesis que debe formalizarse como código, y luego comprobarse en el Probador. A menudo es necesario corregir y mejorar algo ya en el proceso del testeo. En eso consiste el trabajo del desarrollador. En este caso, para codificar la estrategia de pivotes, ha sido utilizado este enfoque. Según mi parecer, el uso de la POO facilita considerablemente la tarea del desarrollador.
Voy a mencionar que el testeo completo en modo de optimización ha sido realizado en MQL5 Cloud Network. Usando la tecnología basada en la nube, fue posible evaluar la eficiencia de las estrategias de manera rápida y barata.
Ubicación de archivos
Es más conveniente colocar los archivos con las estrategias en la carpeta Pivots. Después de la compilación, es necesario colocar los archivos de los indicadores (Pivots.ex5 y MaTrendCatcher.ex5) en la carpeta de indicadores %MQL5\Indicators.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/2853





- 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