Desarrollamos de un asesor multidivisa (Parte 1): Funcionamiento conjunto de varias estrategias comerciales
A lo largo de mi trabajo, he tenido que lidiar con diversas estrategias comerciales. Por regla general, los asesores para el trading automatizado implementan solo una idea comercial. Las dificultades para garantizar el trabajo conjunto estable de muchos asesores en un terminal obligaban a elegir solo un pequeño número de los mejores. Aun así, es una pena desechar estrategias perfectamente viables por esta causa. ¿Cómo conseguir varios asesores trabajen juntos?
Planteamiento del problema
En primer lugar, debemos definir lo que queremos y lo que tenemos.
Tenemos (bueno, o casi):
- algunas estrategias comerciales diferentes que funcionan en diferentes símbolos y marcos temporales en forma de código de asesor listo para usar o un simple conjunto de reglas formulado para realizar transacciones comerciales
- un depósito inicial
- una reducción máxima admisible
Queremos:
- el funcionamiento conjunto de todas las estrategias seleccionadas en una cuenta en varios símbolos y marcos temporales
- la distribución del depósito inicial entre todas por igual o según coeficientes establecidos
- el cálculo automático de los volúmenes de las posiciones abiertas para respetar la tasa máxima de reducción permitida
- el procesamiento correcto del reinicio del terminal
- la posibilidad de ejecutar el inicio en MT5 y MT4
Usaremos el enfoque orientado a objetos, MQL5, y el simulador estándar en MetaTrader 5.
El conjunto de tareas es bastante amplio, así que lo resolveremos paso a paso.
En el primer paso, tomaremos una idea comercial sencilla, y crearemos un asesor sencillo basado en ella. Luego lo optimizaremos y seleccionaremos los dos mejores conjuntos de parámetros. Después crearemos un asesor que contenga en su interior dos ejemplares del asesor simple original y observaremos los resultados.
De la idea a la estrategia
Como idea comercial de prueba, tomaremos una como la que sigue.
Supongamos que cuando un símbolo se comercia mucho, el precio puede cambiar con más fuerza por unidad de tiempo que cuando el símbolo se comercia poco. Entonces, si vemos que el comercio se ha intensificado y el precio ha cambiado en alguna dirección, es posible que siga cambiando en la misma dirección en un futuro próximo. Vamos a intentar sacar provecho de ello.
Una estrategia comercial es un conjunto de reglas para abrir y cerrar posiciones basadas en una idea comercial y no contiene ningún parámetro desconocido. Este conjunto de reglas debe permitir determinar, para un momento dado del funcionamiento de la estrategia, si debemos abrir alguna posición y, en caso afirmativo, cuáles.
Intentemos convertir una idea en una estrategia. En primer lugar, necesitaremos detectar de algún modo el aumento en la intensidad del comercio. Sin esto, no podremos determinar cuándo abrir posiciones. Para ello utilizaremos el volumen de ticks, es decir, el número de precios nuevos recibido por el terminal durante la vela actual. Un mayor volumen de ticks se considerará señal de una negociación más intensa. No obstante, la intensidad puede variar considerablemente según el símbolo. Por ello, no podremos establecer ningún nivel único para el volumen de ticks por encima del cual consideraremos que ha comenzado una negociación intensiva. Entonces podemos utilizar el volumen medio de varias velas para determinar dicho nivel. Tras reflexionar un poco, podemos ofrecer esta descripción:
Colocaremos una orden pendiente en el momento en que el volumen de ticks de la vela supere el volumen medio en la dirección de la vela actual. Cada orden tendrá un plazo de caducidad, transcurrido el cual será eliminada. Si una orden pendiente se ha convertido en una posición, solo se cerrará cuando se alcancen los niveles StopLoss y TakeProfit indicados. Si el volumen de ticks es incluso superior a la media, podremos colocar órdenes adicionales además de la orden pendiente ya abierta.
Esto ya es una descripción más detallada, pero aún incompleta. Así que volveremos a leerla y subrayamos todos los lugares en los que algo no esté claro. En estos lugares se requerirán explicaciones más detalladas.
Estas son las preguntas que me han surgido:
- "Colocamos una orden pendiente..." - ¿Qué órdenes pendientes vamos a colocar?
- "... volumen medio,..." - ¿Cómo calculamos el volumen medio de una vela?
- "...supera el volumen medio, ..." — ¿Cómo se define la superación del volumen medio?
- "... Si el volumen de ticks es aún mayor que la media, ... "- ¿Cómo se define dicho exceso aún mayor?
- "... pueden colocarse órdenes adicionales" — ¿Cuántas órdenes se pueden colocar?
¿Qué órdenes pendientes realizaremos? Basándonos en la idea, esperamos que el precio continúe moviéndose en la misma dirección que adoptó desde el principio de la vela. Si, por ejemplo, el precio en el momento actual es más alto que al principio del periodo de la vela, deberíamos abrir una orden de compra pendiente. Si abrimos BUY_LIMIT, para que funcione, primero el precio deberá volver (bajar) un poco, y luego para que la posición abierta obtenga beneficios, el precio deberá volver a subir. Si abrimos BUY_STOP, el precio deberá seguir moviéndose (subir) un poco más para abrir la posición, y luego subir aún más para obtener beneficios.
No está claro cuál de estas opciones sería mejor. Así que, para simplificar, elegiremos que siempre vamos a abrir órdenes stop (BUY_STOP y SELL_STOP). En el futuro, esto podría convertirse en un parámetro de estrategia cuyo valor determinará qué órdenes se abrirán.
¿Cómo calcular el volumen medio de una vela? Para calcular el volumen medio, tendremos que seleccionar las velas cuyos volúmenes se incluirán en el cálculo de la media. Tomaremos un cierto número de velas cerradas consecutivas. A continuación, si establecemos el número de velas, podremos calcular el volumen medio de ticks.
¿Cómo se determinará si hemos superado el volumen medio? Si tomamos una condición de la forma
V > V_avr ,
dónde
V es el volumen de ticks de la vela actual,
V_avr es el volumen medio por tick,
entonces el cumplimiento de esta condición se alcanzará en aproximadamente la mitad de las velas. Basándonos en esta idea, deberíamos colocar órdenes solo cuando el volumen se encuentre no solo por encima de la media, sino notablemente por encima de la media. De lo contrario, aún no podrá considerarse una señal de un comercio más intenso en esta vela en comparación con las velas anteriores. Podemos usar, por ejemplo, una fórmula como ésta:
V > V_avr + D * V_avr,
donde D será el coeficiente numérico. Si D = 1, la apertura se producirá cuando el volumen actual sea 2 veces el volumen medio, y si, por ejemplo, D = 2, la apertura se producirá cuando el volumen actual supere el volumen medio en 3 veces.
Sin embargo, esta condición solo podrá aplicarse para abrir una orden, ya que si se utiliza la misma condición para abrir la segunda orden y las siguientes, estas se abrirán inmediatamente después de la primera, que podrá sustituirse simplemente abriendo una orden de mayor volumen.
¿Cómo definir esta superación aún mayor? Para ello, añadiremos un parámetro más a la fórmula de la condición: el número de órdenes abiertas N:
V > V_avr + D * V_avr + N * D * V_avr.
Entonces, para que la segunda orden se abra después de la primera (es decir, N = 1), deberá cumplirse la condición:
V > V_avr + 2 * D * V_avr.
Para abrir la primera orden (N = 0), la fórmula adoptará la forma que ya conocemos:
V > V_avr + D * V_avr.
Una última corrección a la fórmula de apertura. Crearemos dos parámetros independientes D y D_add en lugar del mismo parámetro D para la primera orden y las siguientes:
V > V_avr + D * V_avr + N * D_add * V_avr,
V > V_avr * (1 + D + N * D_add)
Parece que esto nos dará más libertad a la hora de seleccionar los parámetros óptimos para la estrategia.
Si nuestra condición usa el valor N como número total de órdenes y posiciones, supondremos que cada orden pendiente se convertirá en una posición independiente, en lugar de aumentar el volumen de una posición ya abierta. Por ello, de momento, tendremos que limitar el alcance de dicha estrategia a trabajar en cuentas con registro de posición independiente ("cobertura").
Cuando todo esté claro, enumeraremos las magnitudes que pueden tomar distintos valores, no un único valor. Estos serán nuestros parámetros de entrada para la estrategia. Tendremos en cuenta que para abrir órdenes necesitamos conocer el volumen, la distancia desde el precio actual, la hora de expiración y los niveles StopLoss y TakeProfit. Luego tendremos esta descripción:
El asesor se iniciará en un determinado símbolo y periodo (timeframe) en la cuenta Hedge
Luego configuraremos los parámetros de entrada:
- Número de velas para promediar el volumen (K)
- Desviación relativa respecto a la media para la apertura de la primera orden (D)
- Desviación relativa respecto a la media en la apertura de la segunda orden y siguientes (D_add)
- Distancia desde el precio hasta la orden pendiente
- Stop Loss (en pips)
- Take Profit (en pips)
- Hora de expiración de las órdenes pendientes (en minutos)
- Número máximo de órdenes abiertas simultáneamente (N_max)
- Volumen de una orden
Encontramos el número de órdenes y posiciones abiertas (N).
Si es menor que N_max, entonces:
calcularemos el volumen medio por tick de las últimas K velas cerradas, obtendremos el valor V_avr.
Si se cumple la condición V > V_avr * (1 + D + N * D_add), entonces:
determinaremos la dirección del cambio de precio en la vela actual: si el precio ha subido, colocaremos una orden pendiente BUY_STOP, y en caso contrario, SELL_STOP
colocaremos una orden pendiente a la distancia, la hora de expiración y los niveles StopLoss y TakeProfit especificados en los parámetros.
Implementación de la estrategia comercial
Vamos a escribir el código. Para empezar, enumeraremos todos los parámetros, dividiéndolos en grupos para mayor claridad y proporcionando a cada parámetro un comentario. Estos comentarios (si los hay) se mostrarán en la ventana de diálogo de configuración de parámetros al iniciar el asesor y en la pestaña de parámetros en el simulador de estrategias en lugar de los nombres de las variables que hayamos elegido para ellos.
Estableceremos valores por defecto solo por ahora; buscaremos los mejores durante la optimización.
input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magicN_ = 27181; // Magic
Como el asesor realizará operaciones comerciales, vamos a crear un objeto global de la clase CTrade. Usando la llamada a los métodos de este objeto, colocaremos órdenes pendientes.
CTrade trade; // Object for performing trading operations
En cualquier caso, recordemos que las variables (u objetos) globales son variables (u objetos) declarados en el código del asesor, no dentro de alguna función. Por consiguiente, estarán disponibles en todas nuestras funciones del asesor. No deberán confundirse con las variables globales del terminal.
Para calcular los parámetros de las órdenes de apertura, necesitaremos obtener los precios actuales y otras propiedades del símbolo sobre el que se ejecutará el asesor. Para ello, crearemos un objeto global de la clase CSymbolInfo.
CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties
También tendremos que contar el número de órdenes y posiciones abiertas. Para ello, crearemos objetos globales de las clases COrderInfo y CPositionInfo, a través de los cuales obtendremos información sobre las órdenes y posiciones abiertas. La cantidad en sí se almacenará en dos variables globales countOrders y countPositions.
COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions
Para calcular el volumen medio de varias velas, podremos utilizar, por ejemplo, el indicador técnico iVolumes. Para obtener sus valores, necesitaremos una variable que almacene el manejador de este indicador (un número entero que almacene el número de serie de este indicador de entre todos los indicadores que se utilizarán en el asesor). Para hallar el volumen medio, primero deberemos copiar los valores del búfer del indicador en un array preparado previamente. Este array también lo haremos global.
int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves)
Ahora podemos pasar a la función de inicialización del asesor OnInit() y a la función de procesamiento de ticks OnTick().
Durante la inicialización podemos hacer lo siguiente:
- Descargaremos el indicador para obtener los volúmenes de ticks y memorizaremos su manejador
- Estableceremos el tamaño del array-receptor según el número de velas para calcular el volumen medio y estableceremos su direccionamiento como en las series temporales
- Estableceremos el número mágico para colocar órdenes a través del objeto comercial
Este será el aspecto que tendrá nuestra función de inicialización:
int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); }
En la función de procesamiento de ticks, según la descripción de la estrategia, deberíamos empezar por encontrar el número de órdenes y posiciones abiertas. Implementaremos esto como una función separada UpdateCounts(). En ella iteraremos todas las posiciones y órdenes abiertas y contaremos solo aquellos Magic Numbers que coincidan con el Magic Number de nuestro asesor.
void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } }
A continuación, deberemos comprobar que el número de posiciones y órdenes abiertas no supere el número establecido en la configuración. En este caso será necesario comprobar si se cumple la condición para abrir una nueva orden. Implementamos esta comprobación como una función aparte SignalForOpen() . Retornará uno de los tres valores posibles:
- +1 - señal para abrir una orden BUY_STOP
- 0 - sin señal de apertura
- -1 - señal para abrir una orden SELL_STOP
También escribiremos dos funciones aparte para colocar órdenes pendientes: OpenBuyOrder() y OpenSellOrder().
Ahora podremos escribir una implementación completa de la función OnTick().
void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } }
Después de eso, añadiremos la implementación de las funciones restantes y el código del asesor estará listo. Lo guardaremos en el archivo SimpleVolumes.mq5 en la carpeta actual.
#include <Trade\OrderInfo.mqh> #include <Trade\PositionInfo.mqh> #include <Trade\SymbolInfo.mqh> #include <Trade\Trade.mqh> input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magicN_ = 27181; // Magic CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) //+------------------------------------------------------------------+ //| Initialization function of the expert | //+------------------------------------------------------------------+ int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } } //+------------------------------------------------------------------+ //| Calculate the number of open orders and positions | //+------------------------------------------------------------------+ void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } } //+------------------------------------------------------------------+ //| Open the BUY_STOP order | //+------------------------------------------------------------------+ void OpenBuyOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = ask + distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price - stopLevel_ * point, digits); double tp = NormalizeDouble(price + (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.BuyStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Open the SELL_STOP order | //+------------------------------------------------------------------+ void OpenSellOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = bid - distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price + stopLevel_ * point, digits); double tp = NormalizeDouble(price - (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.SellStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int SignalForOpen() { // By default, there is no signal int signal = 0; // Copy volume values from the indicator buffer to the receiving array int res = CopyBuffer(iVolumesHandle, 0, 0, signalPeriod_, volumes); // If the required amount of numbers have been copied if(res == signalPeriod_) { // Calculate their average value double avrVolume = ArrayAverage(volumes); // If the current volume exceeds the specified level, then if(volumes[0] > avrVolume * (1 + signalDeviation_ + (countOrders + countPositions) * signaAddlDeviation_)) { // if the opening price of the candle is less than the current (closing) price, then if(iOpen(Symbol(), PERIOD_CURRENT, 0) < iClose(Symbol(), PERIOD_CURRENT, 0)) { signal = 1; // buy signal } else { signal = -1; // otherwise, sell signal } } } return signal; } //+------------------------------------------------------------------+ //| Number array average value | //+------------------------------------------------------------------+ double ArrayAverage(const double &array[]) { double s = 0; int total = ArraySize(array); for(int i = 0; i < total; i++) { s += array[i]; } return s / MathMax(1, total); } //+------------------------------------------------------------------+
Vamos a ejecutar la optimización de los parámetros del asesor para EURGBP en el periodo H1 con las cotizaciones de MetaQuotes para el periodo desde 2018-01-01 hasta 2023-01-01 con un depósito inicial de 100 000 dólares con un lote mínimo de 0,01. Deberemos considerar que el mismo asesor puede mostrar resultados ligeramente distintos cuando se prueba con cotizaciones de diferentes brókeres. Y a veces estos resultados pueden variar enormemente.
A partir de los resultados obtenidos en las pruebas, seleccionaremos dos buenos conjuntos de parámetros con estos resultados:
Figura 1. Resultados de las pruebas con los parámetros [130, 0,9, 1,4, 231, 3750, 50, 600, 3, 0,01].
Figura 2. Resultados de las pruebas con los parámetros [159, 1,7, 0,8, 248, 3600, 495, 39000, 3, 0,01].
Las pruebas se han realizado con un gran depósito inicial por un motivo: si el asesor abre posiciones de volumen fijo, la ejecución puede terminar prematuramente si la reducción es mayor que los fondos disponibles. En este caso, no sabremos si, con los mismos parámetros, podríamos haber reducido razonablemente el volumen de las posiciones abiertas (o, lo que es lo mismo, aumentado el depósito inicial) para evitar perder el depósito.
Aquí vemos un ejemplo. Supongamos que tenemos un depósito inicial de $1 000. Al ejecutarlo en el simulador hemos obtenido estas cifras:
- Depósito final $11 000 (1000% de beneficios, el asesor ha ganado +$10 000 que se suman a los $1 000 iniciales)
- Reducción máxima absoluta $2 000.
Obviamente, hemos tenido suerte de que esta reducción se produjera después de que el asesor aumentase el depósito a más de $2 000. Por eso la pasada en el simulador finalizó y pudimos ver estos resultados. Si dicha reducción se hubiera producido antes (por ejemplo, hubiéramos elegido un inicio diferente del periodo de prueba), lo único que habríamos conseguido es que el asesor perdiera todo el depósito.
Si realizamos las pasadas manualmente, podremos cambiar el volumen en los parámetros o aumentar el depósito inicial y volver a empezar la pasada. Pero si las pasadas se realizan durante el proceso de optimización, no existirá esa posibilidad. En este caso, un conjunto de parámetros potencialmente bueno puede ser rechazado debido a unos ajustes de gestión de capital incorrectamente elegidos. Para reducir la probabilidad de que se produzcan estos resultados, podemos ejecutar la optimización con un depósito inicial muy grande y un volumen mínimo.
Volviendo al ejemplo, si el depósito inicial fuera de $100 000, si se repitiera la reducción de $2 000, no se perdería la totalidad del depósito y el simulador obtendría estos resultados. Y podríamos calcular que si la reducción máxima permitida para nosotros es del 10%, entonces el depósito inicial debería ser de al menos $20 000 dólares. La rentabilidad en este caso será solo del 50% (el asesor ha sumado $10 000 a los $20 000 iniciales)
Vamos a realizar cálculos similares para nuestras dos combinaciones seleccionadas de parámetros para un tamaño del depósito inicial de $10 000 y una reducción permitida del 10% del depósito inicial.
Parámetros | Lote | Reducción | Beneficios | Reducción Permitida | Lote Permitido | Reducción Permitido |
---|---|---|---|---|---|---|
L key | D | P | Da | La = L * (Da / D) | Pa = P * (Da / D) | |
[130, 0.9, 1.4, 231, 3750, 50, 600, 3, 0.01] | 0.01 | 28.70 (0.04%) | 260.41 | 1000 (10%) | 0.34 | 9073 (91%) |
[159, 1.7, 0.8, 248, 3600, 495, 39000, 3, 0.01] | 0.01 | 92.72 (0.09%) | 666.23 | 1000 (10%) | 0.10 | 7185 (72%) |
Como podemos ver, ambas opciones de parámetros de entrada pueden producir rendimientos más o menos similares (~80%). La primera opción gana menos en términos absolutos, pero con menos reducciones. Por lo tanto, podemos aumentar el volumen de las posiciones abiertas para ella más que para la segunda opción, que gana más, pero permite una reducción mayor.
Por consiguiente, hemos encontrado varias combinaciones de parámetros de entrada que resultan prometedoras: vamos a empezar a combinarlas en un asesor.
Clase básica de estrategia
Crearemos una clase CStrategy en la que recogeremos todas las propiedades y métodos inherentes a todas las estrategias. Por ejemplo, cualquier estrategia tendrá algún símbolo y marco temporal independientemente de su relación con los indicadores. Además, cada estrategia tendrá su propio Magic Number para abrir las posiciones e indicar el tamaño de las mismas. Por simplicidad, por ahora no analizaremos el funcionamiento de la estrategia con un tamaño de posición variable, definitivamente añadiremos esto, pero más tarde.
De los métodos necesarios, solo podemos destacar el constructor que inicializa los parámetros de la estrategia, el método de inicialización y el manejador de eventos OnTick. Obtendremos el código siguiente:
class CStrategy : public CObject { protected: ulong m_magic; // Magic string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) double m_fixedLot; // Size of opened positions (fixed) public: // Constructor CStrategy(ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot); virtual int Init() = 0; // Strategy initialization - handling OnInit events virtual void Tick() = 0; // Main method - handling OnTick events };
Los métodos Init() y Tick() se declaran puramente virtuales (después del encabezado tenemos = 0). Esto significa que no escribiremos la implementación de estos métodos en la clase CStrategy. Partiendo de esta clase, crearemos las clases descendientes, en las que necesariamente deberán encontrarse los métodos Init() y Tick(), y que contienen la implementación de reglas comerciales específicas.
La descripción de la clase está lista, vamos a añadir tras ella la implementación del constructor necesario. Como se trata de una función de método que se llama automáticamente al crearse el objeto de estrategia, precisamente en ella deberemos asegurarnos de que se inicializan los parámetros de la estrategia. El constructor tomará cuatro parámetros y asignará sus valores a las variables de miembro correspondientes de la clase a través de la lista de inicialización.
CStrategy::CStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot) : // Initialization list m_magic(p_magic), m_symbol(p_symbol), m_timeframe(p_timeframe), m_fixedLot(p_fixedLot) {}
Guardaremos este código en el archivo Strategy.mqh de la carpeta actual.
Clase de estrategia comercial
Vamos a transferir la lógica del asesor simple original a la nueva clase descendiente CSimpleVolumesStrategy. Para ello, haremos que todas las variables de los parámetros de entrada y las variables globales sean miembros de la clase. Solo eliminaremos las variables fixedLot_ y magicN_, en cuyo lugar utilizaremos los miembros de la clase básica m_fixedLot y m_magic, heredados de la clase básica CStrategy.
#include "Strategy.mqh" class CSimpleVolumeStrategy : public CStrategy { //--- Open signal parameters int signalPeriod_; // Number of candles for volume averaging double signalDeviation_; // Relative deviation from the average to open the first order double signaAddlDeviation_; // Relative deviation from the average for opening the second and subsequent orders //--- Pending order parameters int openDistance_; // Distance from price to pending order double stopLevel_; // Stop Loss (in points) double takeLevel_; // Take Profit (in points) int ordersExpiration_; // Pending order expiration time (in minutes) //--- Money management parameters int maxCountOfOrders_; // Maximum number of simultaneously open orders CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) };
Las funciones OnInit() y OnTick() se convertirán en los métodos públicos Init() y Tick(), y todas las demás funciones se convertirán en nuevos métodos privados de la clase CSimpleVolumesStrategy. Los métodos públicos podrán ser llamados para estrategias desde código externo, por ejemplo desde los métodos de un objeto de asesor. Los métodos privados solo podrán llamarse desde métodos de una clase determinada. Vamos a añadir el encabezado de los métodos a la descripción de la clase.
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code double volumes[]; // Receiver array of indicator values (volumes themselves) //--- Methods void UpdateCounts(); // Calculate the number of open orders and positions int SignalForOpen(); // Signal for opening pending orders void OpenBuyOrder(); // Open the BUY_STOP order void OpenSellOrder(); // Open the SELL_STOP order double ArrayAverage( const double &array[]); // Average value of the number array public: //--- Public methods virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
En los lugares donde se encuentre la implementación de estas funciones, añadiremos el prefijo "CSimpleVolumesStrategy::" a sus nombres para que el compilador sepa que ya no son solo funciones, sino funciones de método de nuestra clase.
class CSimpleVolumeStrategy : public CStrategy { // Class description listing properties and methods... }; int CSimpleVolumeStrategy::Init() { // Function code ... } void CSimpleVolumeStrategy::Tick() { // Function code ... } void CSimpleVolumeStrategy::UpdateCounts() { // Function code ... } int CSimpleVolumeStrategy::SignalForOpen() { // Function code ... } void CSimpleVolumeStrategy::OpenBuyOrder() { // Function code ... } void CSimpleVolumeStrategy::OpenSellOrder() { // Function code ... } double CSimpleVolumeStrategy::ArrayAverage(const double &array[]) { // Function code ... }
En el asesor simple original, los valores de los parámetros de entrada se asignaban durante la declaración, y al iniciar el asesor compilado, se les asignaban los valores del diálogo de parámetros de entrada, no los especificados en el código. No podemos hacer eso en la descripción de la clase, así que aquí es donde el constructor entrará en escena.
Crearemos un constructor con la lista de parámetros necesaria. El constructor también debe ser público, de lo contrario no podremos crear objetos de estrategia desde código externo.
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code public: //--- Public methods CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders ); // Constructor virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
La descripción de la clase ya está lista. Todos sus métodos ya tienen una implementación, salvo el constructor. Vamos a añadirlo. En el caso más sencillo, el constructor de esta clase solo asignará los valores de los parámetros obtenidos a los miembros correspondientes de la clase. Y los cuatro primeros parámetros lo harán llamando al constructor de la clase básica.
CSimpleVolumeStrategy::CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders) : // Initialization list CStrategy(p_magic, p_symbol, p_timeframe, p_fixedLot), // Call the base class constructor signalPeriod_(p_signalPeriod), signalDeviation_(p_signalDeviation), signaAddlDeviation_(p_signaAddlDeviation), openDistance_(p_openDistance), stopLevel_(p_stopLevel), takeLevel_(p_takeLevel), ordersExpiration_(p_ordersExpiration), maxCountOfOrders_(p_maxCountOfOrders) {}
Ya no queda mucho por hacer. Ahora cambiaremos el nombre de fixedLot_ y magicN_, por m_fixedLot y m_magic en todos los lugares donde aparecen. Después sustituiremos el uso de la función de obtención del símbolo actual Symbol() por la variable de clase básica m_symbol y la constante PERIOD_CURRENT por m_timeframe. Guardaremos este código en el archivo SimpleVolumesStrategy.mqh en la carpeta actual.
Clase de experto
Vamos ahora a crear una clase básica CAdvisor, cuya tarea principal será almacenar la lista de objetos de estrategias comerciales específicas e iniciar sus manejadores de eventos. El nombre CExpert resultaría más apropiado para esta clase, pero ya se utiliza en la biblioteca estándar, así que vamos a utilizar un análogo.
#include "Strategy.mqh" class CAdvisor : public CObject { protected: CStrategy *m_strategies[]; // Array of trading strategies int m_strategiesCount;// Number of strategies public: virtual int Init(); // EA initialization method virtual void Tick(); // OnTick event handler virtual void Deinit(); // Deinitialization method void AddStrategy(CStrategy &strategy); // Strategy adding method };
Los métodos Init() y Tick() iteran todas las estrategias del array m_strategies[] y llaman a los métodos de procesamiento de los eventos correspondientes para ellas.
void CAdvisor::Tick(void) { // Call OnTick handling for all strategies for(int i = 0; i < m_strategiesCount; i++) { m_strategies[i].Tick(); } }
En el método de adición de estrategias ocurrirá exactamente esto.
void CAdvisor::AddStrategy(CStrategy &strategy) { // Increase the strategy number counter by 1 m_strategiesCount = ArraySize(m_strategies) + 1; // Increase the size of the strategies array ArrayResize(m_strategies, m_strategiesCount); // Write a pointer to the strategy object to the last element m_strategies[m_strategiesCount - 1] = GetPointer(strategy); }
Guardaremos este código en el archivo Advisor.mqh de la carpeta actual. Basándonos en esta clase, podremos crear descendientes que implementen cualquier forma específica para gestionar el funcionamiento de múltiples estrategias. Pero por ahora nos limitaremos a esta clase básica y no interferiremos en modo alguno con las estrategias individuales.
Asesor comercial con varias estrategias
Para escribir un asesor comercial, solo necesitaremos crear un objeto global de asesor (de la clase CAdvisor).
En el manejador OnInit() del evento de inicialización, al adjuntar al gráfico, crearemos los objetos de estrategia con los parámetros seleccionados y los añadiremos al objeto de asesor. Después de eso, llamaremos al método Init() del objeto asesor para inicializar todas las estrategias en él.
Los manejadores de eventos OnTick() y OnDeinit() simplemente llamarán a los métodos correspondientes del objeto de asesor.
#include "Advisor.mqh" #include "SimpleVolumesStartegy.mqh" input double depoPart_ = 0.8; // Part of the deposit for one strategy input ulong magic_ = 27182; // Magic CAdvisor expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert.AddStrategy(...); expert.AddStrategy(...); int res = expert.Init(); // Initialization of all EA strategies return(res); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { expert.Deinit(); } //+------------------------------------------------------------------+
Veamos ahora más de cerca la creación de objetos de estrategia. Como cada ejemplar de estrategia abre y contabiliza sus propias órdenes y posiciones, deberán tener un Magic diferente. El valor Magic es el primer parámetro del constructor de la estrategia. Por lo tanto, para garantizar diferentes Magic, añadiremos diferentes números al Magic original dado en el parámetro magic_.
expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 2, ...));
Los parámetros segundo y tercero del constructor serán el símbolo y el periodo. Como estamos realizando la optimización con EURGBP y el periodo H1, especificaremos estos valores concretos.
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, ...));
El siguiente parámetro es muy importante: el tamaño de las posiciones abiertas. Más arriba hemos calculado el tamaño correspondiente para las dos estrategias (0,34 y 0,10). Pero está dimensionado para funcionar con reducciones de hasta el 10% de 10 000 dólares cuando las estrategias funcionan por separado. Si las dos estrategias funcionan al mismo tiempo, la reducción de la primera puede sumarse a la de la segunda. En el peor de los casos, para mantenernos dentro del 10% establecido, tendríamos que reducir a la mitad el tamaño de las posiciones abiertas. Sin embargo, puede ocurrir que las reducciones de ambas estrategias no coincidan o incluso se compensen en cierta medida. En este caso, podríamos reducir el tamaño de las posiciones no tanto y seguir sin superar el 10%. Así que haremos que el multiplicador decreciente sea un parámetro del asesor (depoPart_), para el que luego encontraremos el valor óptimo.
El resto de parámetros del constructor de la estrategia serán los conjuntos de valores que hemos elegido tras optimizar el asesor simple. El resultado final será:
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, NormalizeDouble(0.34 * depoPart_, 2), 130, 0.9, 1.4, 231, 3750, 50, 600, 3) ); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, NormalizeDouble(0.10 * depoPart_, 2), 159, 1.7, 0.8, 248, 3600, 495, 39000, 3) );
Guardaremos el código obtenido en el archivo SimpleVolumesExpert.mq5 en la carpeta actual.
Resultados de las pruebas
Antes de probar el asesor combinado, recordemos que la estrategia con el primer conjunto de parámetros se suponía que debía dar un beneficio de alrededor del 91%, mientras que con el segundo conjunto de parámetros debía dar un 72% (para un depósito inicial de $10 000 y una reducción máxima del 10% ($ 1000) en el lote óptimo).
Ahora elegiremos el valor óptimo del parámetro depoPart_ según el criterio de sostenimiento de la reducción dado y obtendremos los siguientes resultados.
Figura 3. Resultado del funcionamiento del asesor conjunto
El balance al final del periodo de prueba ha sido de aproximadamente $22 400, lo que supone un rendimiento del 124%. Eso es más de lo que obtuvimos al ejecutar ejemplares individuales de esta estrategia. Hemos logrado mejorar los resultados comerciales trabajando únicamente con la estrategia comercial existente sin introducir ningún cambio en ella.
Conclusión
Solo hemos dado un pequeño paso hacia la consecución de nuestro objetivo. Dicho paso nos ha dado una mayor confianza en que este enfoque puede mejorar la calidad del comercio. Todavía no hemos abordado de forma alguna muchos aspectos importantes en el asesor escrito.
Por ejemplo, hemos considerado una estrategia muy simple que no controla de ninguna forma el cierre de posiciones, funciona sin necesidad de determinar el inicio exacto de la barra y no utiliza cálculos voluminosos. Para restablecer el estado después de reiniciar el terminal, no será necesario hacer ningún esfuerzo adicional, bastará con calcular las posiciones y órdenes abiertas, cosa que puede hacer el asesor. Pero no todas las estrategias serán tan sencillas. Además, el asesor no puede trabajar en cuentas de compensación y puede mantener posiciones opuestas abiertas al mismo tiempo. No hemos analizado la posibilidad de trabajar con distintos símbolos. Y etcétera, etcétera....
Estos aspectos deberán tenerse en cuenta antes de empezar a comerciar. Pero hablaremos más sobre ellos en los siguientes artículos.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/14026
- 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