- Generar ticks en el probador
- Gestión del tiempo en el comprobador: temporizador, Sleep, GMT
- Pruebas de visualización: gráfico, objetos, indicadores
- Pruebas multidivisa
- Criterios de optimización
- Obtener estadísticas financieras de prueba: TesterStatistics
- Evento OnTester
- Sintonización automática: ParameterGetRange y ParameterSetRange
- Grupo de eventos OnTester para el control de la optimización
- Enviar marcos de datos de los agentes al terminal
- Obtener marcos de datos en terminal
- Directivas del preprocesador para el probador
- Gestionar la visibilidad de los indicadores: TesterHideIndicators
- Emulación de operaciones de depósito y retirada
- Parada forzada de la prueba: TesterStop
- Ejemplo de Gran Asesor Experto
- Cálculos matemáticos
- Depuración y creación de perfiles
- Limitaciones de las funciones del probador
Ejemplo de Gran Asesor Experto
Para generalizar y consolidar los conocimientos sobre las capacidades del probador, vamos a considerar paso a paso un amplio ejemplo de un Asesor Experto. En este ejemplo, resumiremos los siguientes aspectos:
- Utilización de varios símbolos, incluida la sincronización de barras
- Utilización de un indicador de un Asesor Experto
- Utilización de eventos
- Cálculo independiente de las principales estadísticas de trading
- Cálculo del criterio de optimización personalizado R2 ajustado a lotes variables
- Envío y tratamiento de frames con datos de aplicación (informes de trading desglosados por símbolos)
Utilizaremos MultiMartingale.mq5 como base técnica del Asesor Experto, pero lo haremos menos arriesgado pasando a operar con señales de sobrecompra/sobreventa multidivisa y aumentando los lotes sólo como añadido opcional. Anteriormente, en BandOsMA.mq5 vimos ya cómo operar basándose en señales de trading de indicadores. Esta vez utilizaremos UseUnityPercentPro.mq5 como indicador de la señal. Sin embargo, primero tenemos que modificarlo. Llamemos a la nueva versión UnityPercentEvent.mq5.
UnityPercentEvent.mq5
Recordemos la esencia del indicador Unity. Calcula la fuerza relativa de las divisas o tickers incluidos en un conjunto de instrumentos dados (se supone que todos los instrumentos tienen una divisa común a través de la cual es posible la conversión). En cada barra se forman lecturas para todas las divisas: algunas serán más caras, otras más baratas, y los dos elementos extremos se encuentran en un estado límite. Más adelante se pueden considerar dos estrategias esencialmente opuestas para ellas:
- Más desglose (confirmación y continuación de un fuerte movimiento lateral)
- Retroceso (inversión del movimiento hacia el centro debido a sobrecompra y sobreventa)
Para operar cualquiera de estas señales, debemos hacer un símbolo de trabajo de dos divisas (o tickers en general), si hay algo adecuado para esta combinación en la Observación de Mercado. Por ejemplo, si la línea superior del indicador pertenece a EUR y la inferior a USD, corresponden al par EURUSD, y según la estrategia de desglose deberíamos comprarlo pero según la estrategia de rebote, deberíamos venderlo.
En un caso más general, por ejemplo, cuando se indican CFD o materias primas con una divisa de cotización común en la cesta de instrumentos de trabajo del indicador, no siempre es posible crear un instrumento real. Para estos casos sería necesario complicar más el Asesor Experto introduciendo sintéticos de trading (posiciones compuestas), pero no lo haremos aquí y nos limitaremos al mercado Forex, donde casi todos los tipos cruzados suelen estar disponibles.
Por lo tanto, el Asesor Experto no sólo debe leer todos los búferes de los indicadores, sino también averiguar los nombres de las divisas, que corresponden a los valores máximos y mínimos. Y aquí tenemos un pequeño obstáculo.
MQL5 no permite leer los nombres de los búferes indicadores de terceros y, en general, cualquier propiedad de línea que no sea entera. Existen tres funciones para configurar las propiedades: PlotIndexSetInteger, PlotIndexSetDouble y PlotIndexSetString, pero sólo existe una función para leerlos, PlotIndexGetInteger.
En teoría, cuando los programas MQL compilados en un único complejo de trading son creados por el mismo desarrollador, esto no representa un gran problema. En concreto, podríamos separar una parte del código fuente del indicador en un archivo de encabezado e incluirlo no sólo en el indicador, sino también en el Asesor Experto. A continuación, en el Asesor Experto, sería posible repetir el análisis de los parámetros de entrada del indicador y restaurar la lista de divisas, completamente similar a la creada por el indicador. Duplicar los cálculos no es muy bonito, pero funcionaría. No obstante, también se requiere una solución más universal cuando el indicador tiene un desarrollador diferente, y éste no quiere revelar el algoritmo ni planea cambiarlo en el futuro (entonces las versiones compiladas del indicador y del Asesor Experto serán incompatibles). Este tipo de «acoplamiento» de indicadores ajenos con los propios, o de un Asesor Experto encargado a un servicio independiente, es una práctica muy común. Por lo tanto, el desarrollador del indicador debe facilitar al máximo su integración.
Una de las posibles soluciones es que el indicador envíe mensajes con los números y nombres de los búferes después de la inicialización.
Así es como se hace en el manejador OnInit del indicador UnityPercentEvent.mq5 (el código de abajo se muestra de forma abreviada ya que casi nada ha cambiado).
int OnInit()
|
En comparación con la versión original, aquí sólo se ha añadido una línea. Contiene la llamada EventChartCustom. La variable de entrada BarLimit se utiliza como identificador de la copia del indicador (de las que potencialmente puede haber varias). Como el indicador se llamará desde el Asesor Experto y no se mostrará al usuario, basta con indicar un número positivo pequeño, al menos 1, pero tendremos, por ejemplo, 10.
Ahora el indicador está listo y sus señales se pueden utilizar en Asesores Expertos de terceros. Empecemos a desarrollar el Asesor Experto UnityMartingale.mq5. Para simplificar la presentación, la dividiremos en 4 etapas, añadiendo gradualmente nuevos bloques. Tendremos tres versiones preliminares y una versión final.
UnityMartingaleDraft1.mq5
En la primera etapa, para la versión UnityMartingaleDraft1.mq5, utilicemos MultiMartingale.mq5 como base y modifiquémosla.
Cambiaremos el nombre de la antigua variable de entrada StartType, que determinaba la dirección de la primera transacción de la serie, por SignalType. Se utilizará para elegir entre las estrategias consideradas BREAKOUT y PULLBACK.
enum SIGNAL_TYPE
|
Para configurar el indicador, necesitamos un grupo separado de variables de entrada.
input group "U N I T Y S E T T I N G S"
|
Tenga en cuenta que el parámetro UnitySymbols contiene una lista de instrumentos de clúster para construir un indicador, y normalmente difiere de la lista de instrumentos de trabajo con los que queremos operar. Los instrumentos negociados se siguen configurando en el parámetro WorkSymbols.
Por ejemplo, por defecto, pasamos un conjunto de los principales pares de divisas Forex al indicador, y por lo tanto podemos indicar como trading no sólo los pares principales, sino también cualquier cruce. Suele tener sentido limitar este conjunto a los instrumentos con las mejores condiciones de trading (en concreto, diferenciales pequeños o moderados). Además, es deseable evitar distorsiones, es decir, mantener una cantidad igual de cada divisa en todos los pares, neutralizando así estadísticamente los riesgos potenciales de elegir una dirección poco acertada para una de las divisas.
A continuación, envolvemos el control del indicador en la clase UnityController. Además del indicador handle, los campos de clase almacenan los siguientes datos:
- El número de buffers del indicador, que se recibirán de los mensajes del indicador después de su inicialización.
- El número de bar del que se están leyendo los datos (normalmente el incompleto actual es 0, o el último completado es 1)
- El array data con los valores leídos de los búferes de los indicadores en la barra especificada
- La última hora de lectura lastRead
- Bandera de funcionamiento por ticks o barras tickwise
Además, la clase utiliza el objeto MultiSymbolMonitor para sincronizar las barras de todos los símbolos implicados.
class UnityController
|
En el constructor, que acepta todos los parámetros del indicador como argumentos, creamos el indicador y configuramos el objeto sync.
public:
|
El número de búferes se establece mediante el método attached. Lo llamaremos al recibir un mensaje del indicador.
void attached(const int b)
|
Un método especial isReady devuelve true cuando las últimas barras de todos los símbolos tienen la misma hora. Sólo en el estado de dicha sincronización obtendremos los valores correctos del indicador. Cabe señalar que aquí se supone el mismo calendario de sesiones de trading para todos los instrumentos. Si no es así, habrá que modificar el análisis de los tiempos.
bool isReady()
|
Definimos la hora actual de diferentes maneras dependiendo del modo de funcionamiento del indicador: cuando se recalcula en cada tick (tickwise es igual a true), utilizamos la hora del servidor, y cuando se recalcula una vez por barra, utilizamos la hora de apertura de la última barra.
datetime lastTime() const
|
La presencia de este método nos permitirá excluir la lectura del indicador si la hora actual no ha cambiado y, en consecuencia, los últimos datos leídos almacenados en el búfer data siguen siendo relevantes. Y así es como se organiza la lectura de los búferes indicadores en el método read. Sólo necesitamos un valor de cada búfer para la barra con el índice bar.
bool read()
|
Al final, sólo guardamos el tiempo de lectura en la variable lastRead. Si está vacía o no es igual a la nueva hora actual, el acceso a los datos del controlador en los siguientes métodos hará que se lean los búferes indicadores utilizando read.
Los principales métodos externos del controlador son getOuterIndices para obtener los índices de los valores máximo y mínimo y el operador '[]' para leer los valores.
bool isNewTime() const
|
Anteriormente, el Asesor Experto BandOsMA.mq5 introdujo el concepto de interfaz TradingSignal.
interface TradingSignal
|
Basándonos en ella, describiremos la implementación de la señal utilizando el indicador UnityPercentEvent. El objeto controlador UnityController se pasa al constructor. También indica los índices de divisas (búferes), de cuyas señales queremos hacer un seguimiento. Podremos crear un conjunto arbitrario de señales diferentes para los símbolos de trabajo seleccionados.
class UnitySignal: public TradingSignal
|
El método signal devuelve 0 en situación de incertidumbre y +1 o -1 en estados de sobrecompra y sobreventa de dos divisas concretas.
Para formalizar las estrategias de trading utilizamos la interfaz TradingStrategy.
interface TradingStrategy
|
En este caso, se crea sobre su base la clase UnityMartingale, que coincide en gran medida con SimpleMartingale de MultiMartingale.mq5. Sólo mostraremos las diferencias.
class UnityMartingale: public TradingStrategy
|
La parte de trading está lista; queda por considerar la inicialización. En el nivel global se describen un puntero automático al objeto UnityController y el array con los nombres de las divisas. El conjunto de sistemas de trading es completamente similar a los desarrollos anteriores.
AutoPtr<TradingStrategyPool> pool;
|
En el manejador OnInit, creamos el objeto UnityController y esperamos a que el indicador envíe la distribución de divisas por índices del búfer.
int OnInit()
|
Si en los parámetros de entrada del indicador se selecciona el tipo de precio PRICE_CLOSE y un período único, el cálculo en el controlador se realizará una vez por barra. En todos los demás casos, las señales se actualizarán por ticks, pero no más a menudo que una vez por segundo (recuerde la implementación del método lastTime en el controlador).
El método de ayuda StartUp generalmente hace lo mismo que el antiguo manejador OnInit en el Asesor Experto MultiMartingale. Rellena la estructura Settings con opciones de configuración, comprobando que sean correctas y creando un conjunto de sistemas de trading TradingStrategyPool consistente en objetos de la clase UnityMartingale para diferentes símbolos de trading WorkSymbols. Sin embargo, ahora este proceso se divide en dos etapas debido a que tenemos que esperar información sobre la distribución de divisas entre los búferes. Por lo tanto, la función StartUp tiene un parámetro de entrada que denota una llamada de OnInit y posteriormente de OnChartEvent.
Al analizar el código fuente de StartUp, es importante recordar que la inicialización es diferente para los casos en que operamos con un solo instrumento que coincide con el gráfico actual y cuando se especifica una cesta de instrumentos. El primer modo está activo cuando WorkSymbols es una línea vacía. Es conveniente para optimizar un Asesor Experto para un instrumento específico. Una vez encontrados los ajustes para varios instrumentos, podemos combinarlos en WorkSymbols.
bool StartUp(const bool init = false)
|
La función StartUp de OnInit se llama con el parámetro true, lo que significa que sólo se comprueba la corrección de los ajustes. La creación de un objeto del sistema de trading se retrasa hasta que se recibe un mensaje del indicador en OnChartEvent.
void OnChartEvent(const int id,
|
Aquí recordamos el número de divisas en la variable global currenciesCount y las almacenamos en el array currencies, tras lo cual llamamos a StartUp con el parámetro false (valor por defecto, por tanto omitido). Los mensajes llegan desde la cola en el orden en que existen en los búferes del indicador. Así, obtenemos una coincidencia entre el índice y el nombre de la divisa.
Cuando se vuelve a llamar a StartUp, se ejecuta un código adicional:
bool StartUp(const bool init = false)
|
La función de ayuda SplitSymbolToCurrencyIndices selecciona la divisa base y la divisa de beneficio del símbolo pasado y encuentra sus índices en el array currencies. De este modo, obtenemos los datos de referencia para generar señales en los objetos de UnitySignal. Cada uno de ellos tendrá su propio par de índices de divisas.
bool SplitSymbolToCurrencyIndices(const string symbol, int &first, int &second)
|
En general, el Asesor Experto está listo.
Puede ver que en los últimos ejemplos de Asesores Expertos tenemos clases de estrategias y clases de señales de trading. Deliberadamente las hicimos descendientes de las interfaces genéricas TradingStrategy y TradingSignal para poder posteriormente recopilar colecciones de implementaciones compatibles pero diferentes que puedan combinarse en el desarrollo de futuros Asesores Expertos. Por lo general, estas clases concretas unificadas deben separarse en archivos de encabezado independientes. En nuestros ejemplos no lo hemos hecho para simplificar la modificación paso a paso.
No obstante, el enfoque descrito es estándar para la programación orientada a objetos (POO). En particular, como mencionamos en la sección sobre creación de borradores de Asesor Experto, junto con MetaTrader 5 viene un framework de archivos de encabezado con clases estándar de operaciones de trading, indicadores de señal, y gestión de dinero, que se utilizan en el Asistente MQL. En el sitio mql5.com se publican otras soluciones similares en los artículos y en la sección Code Base.
Puede utilizar las jerarquías de clases ya creadas como base para sus proyectos, siempre que sean adecuadas en términos de capacidades y facilidad de uso.
Para completar el cuadro, hemos querido introducir en el Asesor Experto nuestro propio criterio de optimización basado en R2. Para evitar la contradicción entre la regresión lineal en la fórmula de cálculo de R2 y los lotes variables que se incluyen en nuestra estrategia, calcularemos el coeficiente no para la línea de balance habitual, sino para sus incrementos acumulados normalizados por tamaños de lote en cada operación.
Para ello, en el manejador OnTester, seleccionamos transacciones con los tipos DEAL_TYPE_BUY y DEAL_TYPE_SELL y con la dirección OUT. Solicitaremos todas las propiedades de transacción que forman el resultado financiero (beneficio/pérdida), es decir, DEAL_PROFIT, DEAL_SWAP, DEAL_COMMISSION, DEAL_FEE, así como su volumen DEAL_VOLUME.
#defineSTAT_PROPS5// number of requested deal properties
doubleOnTester() { HistorySelect(0,LONG_MAX);
constENUM_DEAL_PROPERTY_DOUBLEprops[STAT_PROPS] = { DEAL_PROFIT,DEAL_SWAP,DEAL_COMMISSION,DEAL_FEE,DEAL_VOLUME }; doubleexpenses[][STAT_PROPS]; ulongtickets[];// needed because of 'select' method prototype, but useful for debugging
DealFilterfilter; filter.let(DEAL_TYPE, (1<<DEAL_TYPE_BUY) | (1<<DEAL_TYPE_SELL),IS::OR_BITWISE) .let(DEAL_ENTRY, (1<<DEAL_ENTRY_OUT) | (1<<DEAL_ENTRY_INOUT) | (1<<DEAL_ENTRY_OUT_BY), IS::OR_BITWISE) .select(props,tickets,expenses); ... |
A continuación, en el array balance, acumulamos los beneficios/pérdidas normalizados por los volúmenes de trading y calculamos el criterio R2 para ello.
const int n = ArraySize(tickets);
|
La primera versión del Asesor Experto está básicamente lista. No hemos incluido la comprobación del modelo de ticks mediante TickModel.mqh. Se supone que el Asesor Experto será probado cuando genere ticks en el modo OHLC M1 o mejor. Cuando se detecta el modelo «sólo precios abiertos», el Asesor Experto enviará un frame especial con un estado de error al terminal y se descargará del probador. Por desgracia, esto sólo detendrá este pase, pero la optimización continuará. Por lo tanto, la copia del Asesor Experto que se ejecuta en el terminal emite una «alerta» para que el usuario interrumpa la optimización manualmente.
void OnTesterPass()
|
Puede optimizar los parámetros de SYMBOL SETTINGS para cualquier símbolo y repetir la optimización para símbolos diferentes. Al mismo tiempo, los grupos COMMON SETTINGS y UNITY SETTINGS deben contener siempre los mismos ajustes, ya que se aplican a todos los símbolos e instancias de los sistemas de trading. Por ejemplo, Trailing debe estar activado o desactivado para todas las optimizaciones. Observe también que las variables de entrada de un solo símbolo (es decir, el grupo SYMBOL SETTINGS) sólo tienen efecto mientras WorkSymbols contenga una cadena vacía. Por lo tanto, en la fase de optimización, debe mantenerla vacía.
Por ejemplo, para diversificar los riesgos, puede optimizar constantemente un Asesor Experto en pares completamente independientes: EURUSD, AUDJPY, GBPCHF, NZDCAD, o en otras combinaciones. El código fuente incluye tres archivos con ejemplos de configuraciones privadas.
#property tester_set "UnityMartingale-eurusd.set"
|
Para operar con tres símbolos a la vez, estos ajustes deben «empaquetarse» en un parámetro común WorkSymbols:
EURUSD+0.01*1.6^5(200,200)[17,21];GBPCHF+0.01*1.2^8(600,800)[7,20];AUDJPY+0.01*1.2^8(600,800)[7,20] |
Esta configuración también se incluye en un archivo aparte.
#property tester_set "UnityMartingale-combo.set" |
Uno de los problemas con la versión actual del Asesor Experto es que el informe del probador proporcionará estadísticas generales para todos los símbolos (más precisamente, para todas las estrategias de trading, ya que podemos incluir diferentes clases en el conjunto), mientras que sería interesante para nosotros supervisar y evaluar cada componente del sistema por separado.
Para ello es necesario aprender a calcular de forma independiente los principales indicadores financieros de trading, por analogía con la forma en que el probador lo hace por nosotros. Nos ocuparemos de esto en la segunda etapa del desarrollo del Asesor Experto.
UnityMartingaleDraft2.mq5
El cálculo estadístico puede ser necesario con bastante frecuencia, por lo que lo implementaremos en un archivo de encabezado separado TradeReport.mqh, donde organizaremos el código fuente en las clases apropiadas.
Llamemos a la clase principal TradeReport. Muchas variables de trading dependen de las curvas de saldo y margen libre (fondos propios). Por lo tanto, la clase contiene variables para el seguimiento del saldo actual y el beneficio, así como un array constantemente actualizado con el historial de saldos. No almacenaremos el historial del capital, porque puede cambiar en cada tick, y es mejor calcularlo sobre la marcha. Veremos un poco más adelante la razón de tener la curva de equilibrio.
class TradeReport
|
La modificación y lectura de los campos de la clase se realiza mediante métodos, incluido el constructor, en el que el saldo se inicializa mediante la propiedad ACCOUNT_BALANCE.
TradeReport()
|
Estos métodos serán necesarios para calcular iterativamente (sobre la marcha) la reducción del capital. El array de saldos data será necesario para un cálculo puntual de la reducción del saldo (lo haremos al final de la prueba).
En función de las fluctuaciones de la curva (da igual que sea de saldo o capital), se debe calcular la reducción absoluta y relativa utilizando el mismo algoritmo. Por lo tanto, este algoritmo y las variables internas necesarias para él, que almacenan estados intermedios, se implementan en la estructura anidada DrawDown. En el siguiente código se muestran sus principales métodos y propiedades:
struct DrawDown
|
El primer método calcDrawdown calcula las reducciones cuando conocemos todo el array y esto se utilizará para el saldo. El segundo método calcDrawdown calcula la reducción de forma iterativa: cada vez que se le llama, se le indica el siguiente valor de la serie, y éste se utilizará para el capital.
Además de la reducción, como sabemos, existe un gran número de estadísticas estándar para los informes, pero para empezar sólo admitiremos algunas de ellas. Para ello, describimos los campos correspondientes en otra estructura anidada, GenericStats. Se hereda de DrawDown porque seguimos necesitando la reducción en el informe.
struct GenericStats: public DrawDown
|
Por los nombres de las variables es fácil adivinar a qué métrica estándar corresponden. Algunas métricas son redundantes y, por tanto, se omiten. Por ejemplo, dado el número total de operaciones (trades) y el número de operaciones de compra entre ellas (buy_trades), podemos encontrar fácilmente el número de operaciones de venta (trades - sell_trades). Lo mismo ocurre con las estadísticas complementarias de ganancias y pérdidas. Las rachas ganadoras y perdedoras no se tienen en cuenta. Quienes lo deseen pueden completar nuestro informe con estos indicadores.
Para la unificación con las estadísticas generales del probador, existe el método fillByTester que rellena todos los campos a través de la función TesterStatistics. Lo utilizaremos más adelante.
void fillByTester()
|
Por supuesto, tenemos que implementar nuestro propio cálculo para aquellos saldos y fondos propios de los sistemas de trading que el probador no puede calcular. Anteriormente se han presentado prototipos de métodos de calcDrawdown. Durante la operación, rellenan el último grupo de campos con el prefijo «serie_dd». Además, la clase TradeReport contiene un método para calcular el ratio de Sharpe. Como entrada, toma una serie de números y un tipo de financiación sin riesgo. El código fuente completo se encuentra en el archivo adjunto.
static double calcSharpe(const double &data[], const double riskFreeRate = 0); |
Como se puede adivinar, al llamar a este método, el array miembro relevante de la clase TradeReport con saldos se pasará en el parámetro data. El proceso de llenar este array y llamar a los métodos anteriores para indicadores específicos ocurre en el método calcStatistics (ver abajo). Se le pasa un objeto filtro de transacciones como entrada (filter), depósito inicial (start) y tiempo (origin). Se supone que el código de llamada configurará el filtro de tal manera que sólo las operaciones del sistema de trading que nos interesa caigan bajo él.
El método devuelve una estructura llena GenericStats, y además, llena dos arrays dentro del objeto TradeReport, data y moments, con valores de saldo y referencias temporales de cambios, respectivamente. Lo necesitaremos en la versión final del Asesor Experto.
GenericStats calcStatistics(DealFilter &filter,
|
Aquí puede ver cómo llamamos a calcSharpe y calcDrawdown para obtener los indicadores correspondientes en el array data. El resto de indicadores se calculan directamente en el bucle dentro de calcStatistics.
La clase TradeReport está lista, y podemos ampliar la funcionalidad del Asesor Experto a la versión UnityMartingaleDraft2.mq5.
Añadamos nuevos miembros a la clase UnityMartingale.
class UnityMartingale: public TradingStrategy
|
Necesitamos el objeto report para llamar a calcStatistics, donde se incluirá la reducción de saldos. El objeto equity es necesario para un cálculo independiente de la reducción de capital. El saldo y la fecha iniciales, así como el inicio del cálculo de la reducción del capital, se establecen en el constructor.
public:
|
La continuación del cálculo de la reducción por capital se realiza sobre la marcha, con cada llamada al método trade.
virtual bool trade() override
|
Esto no es todo lo que se necesita para un cálculo correcto. Hay que tener en cuenta la ganancia o pérdida flotante sobre el saldo. La parte de código anterior sólo muestra la llamada a addFloatingPL, pero la clase TradeReport también tiene un método para modificar el saldo: addBalance. No obstante, el saldo sólo cambia cuando se cierra la posición.
Gracias al concepto POO, cerrar una posición en nuestra situación corresponde a borrar el objeto position de la clase PositionState. Entonces, ¿por qué no podemos interceptarla?
La clase PositionState no proporciona ningún medio para ello, pero podemos declarar una clase derivada PositionStateWithEquity con un constructor y un destructor especiales.
Al crear un objeto, no sólo se pasa al constructor el identificador de posición, sino también un puntero al objeto de informe al que habrá que enviar información.
class PositionStateWithEquity: public PositionState
|
En el destructor encontramos todas las operaciones por el ID de la posición cerrada, calculamos el resultado financiero total (junto con las comisiones y otras deducciones), y luego llamamos a addBalance para relacionar el objeto report.
~PositionStateWithEquity()
|
Queda por aclarar un punto: cómo crear objetos de clase PositionStateWithEquity para las posiciones en lugar de PositionState. Para ello, basta con cambiar el operador new en un par de lugares en los que se llama en la clase TradingStrategy.
position=MQLInfoInteger(MQL_TESTER) ? newPositionStateWithEquity(tickets[0], &report) :newPositionState(tickets[0]); |
Así, hemos puesto en marcha la recogida de datos. Ahora necesitamos generar directamente un informe, es decir, llamar a calcStatistics. Aquí tenemos que ampliar nuestra interfaz TradingStrategy: le añadimos el método statement.
interface TradingStrategy
|
A continuación, en esta implementación actual, pensada para nuestra estrategia, podremos llevar el trabajo a su conclusión lógica.
class UnityMartingale: public TradingStrategy
|
El nuevo método simplemente imprimirá todos los indicadores calculados en el registro. Reenviando el mismo método a través del conjunto de sistemas de trading TradingStrategyPool, vamos a solicitar informes separados para todos los símbolos al manejador OnTester.
double OnTester()
|
Comprobemos si nuestro informe es correcto. Para ello, vamos a ejecutar el Asesor Experto en el probador, un símbolo cada vez, y a comparar el informe estándar con nuestros cálculos. Por ejemplo, para configurar UnityMartingale-eurusd.set, operando en EURUSD H1 obtendremos tales indicadores para 2021.
Informe del probador para 2021, EURUSD H1
En el registro, nuestra versión se muestra como dos estructuras: DrawDown con reducción del capital y GenericStats con indicadores de reducción del saldo y otras estadísticas.
Informe comercial separado para EURUSD Equity DD: [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10022.48 10017.03 10000.00 9998.20 6.23 0.06 » » [series_dd_relative_percent] [series_dd_relative] » 0.06 6.23
Trade Statistics (with Balance DD): [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10022.40 10017.63 10000.00 9998.51 5.73 0.06 » » [series_dd_relative_percent] [series_dd_relative] » » 0.06 5.73 » » [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] » » 194 97 43 42 19 23 57.97 -39.62 18.35 1.46 » » [average_trade] [recovery] [max_profit] [max_loss] [sharpe] » 0.19 3.20 2.00 -2.01 0.15 |
Es fácil comprobar que estos números coinciden con el informe del probador.
Ahora vamos a empezar a operar en el mismo periodo para tres símbolos a la vez (configurando UnityMartingale-combo.set).
Además de las entradas EURUSD, en el diario aparecerán estructuras para GBPCHF y AUDJPY.
Informe comercial separado para GBPCHF Equity DD: [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10029.50 10000.19 10000.00 9963.65 62.90 0.63 » » [series_dd_relative_percent] [series_dd_relative] » 0.63 62.90 Trade Statistics (with Balance DD): [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10023.68 9964.28 10000.00 9964.28 59.40 0.59 » » [series_dd_relative_percent] [series_dd_relative] » » 0.59 59.40 » » [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] » » 600 300 154 141 63 78 394.53 -389.33 5.20 1.01 » » [average_trade] [recovery] [max_profit] [max_loss] [sharpe] » 0.02 0.09 9.10 -6.73 0.01
Informe comercial separado para AUDJPY Equity DD: [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10047.14 10041.53 10000.00 9961.62 48.20 0.48 » » [series_dd_relative_percent] [series_dd_relative] » 0.48 48.20 Trade Statistics (with Balance DD): [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10045.21 10042.75 10000.00 9963.62 44.21 0.44 » » [series_dd_relative_percent] [series_dd_relative] » » 0.44 44.21 » » [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] » » 332 166 91 89 54 35 214.79 -170.20 44.59 1.26 » » [average_trade] [recovery] [max_profit] [max_loss] [sharpe] » 0.27 1.01 7.58 -5.17 0.09 |
En este caso, el informe del probador contendrá datos generalizados, por lo que, gracias a nuestras clases, hemos recibido detalles antes inaccesibles.
Sin embargo, consultar un pseudoinforme en un registro no resulta muy cómodo. Además, me gustaría ver una representación gráfica de la línea de balance como mínimo, ya que su aspecto suele decir más sobre la idoneidad del sistema que las meras estadísticas.
Vamos a mejorar el Asesor Experto dándole la capacidad de generar informes visuales en formato HTML: al fin y al cabo, los informes del probador también pueden exportarse a HTML, guardarse y compararse a lo largo del tiempo. Además, en el futuro, estos informes podrán transmitirse en frames al terminal justo durante la optimización, y el usuario podrá empezar a estudiar los informes de pasadas concretas incluso antes de que finalice todo el proceso.
Esta será la penúltima versión del ejemplo UnityMartingaleDraft3.mq5.
UnityMartingaleDraft3.mq5
La visualización del informe de trading incluye una línea de balance y una tabla con indicadores estadísticos. No generaremos un informe completo similar al del probador, sino que nos limitaremos a los valores seleccionados más importantes. Nuestro propósito es implantar un mecanismo de trabajo que luego pueda personalizarse en función de las necesidades personales.
Dispondremos la base del algoritmo en forma de la clase TradeReportWriter (TradeReportWriter.mqh). La clase podrá almacenar un número arbitrario de informes de diferentes sistemas de trading: cada uno en un objeto separado DataHolder, que incluye arrays de valores de saldo y marcas de tiempo (data y when, respectivamente), la estructura stats con estadísticas, así como el título, el color y la anchura de la línea que se desea mostrar.
class TradeReportWriter
|
Disponemos de un array de punteros automáticos curves asignados a los objetos de la clase DataHolder. Además, necesitaremos límites comunes en cuanto a importes y plazos para que coincidan con las líneas de todos los sistemas de trading del cuadro. Esto lo proporcionarán las variables lower, upper, start y stop.
AutoPtr<DataHolder> curves[];
|
El método addCurve añade una línea de balance.
virtual bool addCurve(double &data[], datetime &when[], const string name,
|
La segunda versión del método addCurve añade no sólo una línea de balance, sino también un conjunto de variables financieras en la estructura GenericStats.
virtual bool addCurve(TradeReport::GenericStats &stats,
|
El método más importante de la clase que visualiza el informe se hace abstracto.
virtual void render() = 0; |
Esto permite implementar muchas formas de visualizar los informes, por ejemplo, tanto registrando en archivos de distintos formatos, como dibujando directamente sobre el gráfico. A continuación nos limitaremos a la formación de archivos HTML, ya que es el método tecnológicamente más avanzado y extendido.
La nueva clase HTMLReportWriter tiene un constructor, cuyos parámetros especifican el nombre del archivo, así como el tamaño de la imagen con curvas de balance. Generaremos la imagen propiamente dicha en el conocido formato de gráficos vectoriales SVG: es ideal en este caso porque es un subconjunto del lenguaje XML, que es el propio HTML.
class HTMLReportWriter: public TradeReportWriter
|
Antes de pasar al principal método público render, es necesario presentar al lector una tecnología que se describirá en detalle en la Parte 7 y última del libro. Estamos hablando de recursos: archivos y arrays de datos arbitrarios conectados a un programa MQL para trabajar con multimedia (sonido e imágenes), incrustar indicadores compilados, o simplemente como repositorio de información de la aplicación. Es esta última opción la que utilizaremos ahora.
La cuestión es que es mejor generar una página HTML no completamente a partir de código MQL, sino basándose en una plantilla (plantilla de página), en la que el código MQL sólo insertará los valores de algunas variables. Se trata de una técnica muy conocida en programación que permite separar el algoritmo y la representación externa del programa (o el resultado de su trabajo). Gracias a ello, podemos experimentar por separado con la plantilla HTML y el código MQL, trabajando con cada uno de los componentes en un entorno familiar. En concreto, MetaEditor todavía no es muy adecuado para editar páginas web y visualizarlas, al igual que un navegador estándar no sabe nada de MQL5 (aunque esto se puede arreglar).
Almacenaremos las plantillas de informes HTML en archivos de texto conectados al código fuente MQL5 como recursos. La conexión se realiza mediante una directiva especial #resource. Por ejemplo, en el archivo TradeReportWriter.mqh aparece la siguiente línea:
#resource "TradeReportPage.htm" as string ReportPageTemplate |
Significa que junto al código fuente debe estar el archivo TradeReportPage.htm, que estará disponible en el código MQL como cadena ReportPageTemplate. Por extensión, se puede entender que el archivo es una página web. He aquí el contenido de este archivo con abreviaturas (no tenemos la tarea de formar al lector en desarrollo web, aunque, al parecer, tener conocimientos al respecto puede ser útil también para un operador de trading). Las sangrías se añaden para representar visualmente la jerarquía de anidamiento de las etiquetas HTML; no hay sangrías en el archivo.
<!DOCTYPE html>
|
Los fundamentos de las plantillas son elegidos por el desarrollador. Existe un gran número de sistemas de plantillas HTML ya preparadas, pero ofrecen muchas funciones redundantes y, por tanto, son demasiado complejas para nuestro ejemplo. Desarrollaremos nuestro propio concepto.
Para empezar, observemos que la mayoría de las páginas web tienen una parte inicial (encabezado), una parte final (pie de página) y entre ambas se sitúa la información útil. El proyecto de informe mencionado no es una excepción en este sentido. Utiliza el carácter de tilde '~' para indicar contenido útil. En lugar de ello, el código MQL tendrá que insertar una imagen de saldo y una tabla con indicadores. Pero la presencia de '~' no es necesaria, ya que la página puede ser un todo único, es decir, la parte central muy útil: después de todo, el código MQL puede, si es necesario, insertar el resultado del procesamiento de una plantilla en otra.
Para terminar la digresión sobre las plantillas HTML, vamos a prestar atención a una cosa más. En teoría, una página web se compone de etiquetas que realizan funciones esencialmente diferentes. Las etiquetas HTML estándar indican al navegador qué debe mostrar. Además de ellos, existen estilos en cascada (CSS), que describen cómo mostrarlo. Por último, la página puede tener un componente dinámico en forma de scripts de JavaScript que controlen interactivamente tanto la primera como la segunda.
Normalmente, estos tres componentes se planifican de forma independiente, es decir, por ejemplo, una plantilla HTML, en sentido estricto, debe contener sólo HTML, pero no CSS ni JavaScript. Esto permite «desvincular» el contenido, la apariencia y el comportamiento de la página web, lo que facilita el desarrollo (se recomienda encarecidamente seguir el mismo enfoque en MQL5).
Sin embargo, en nuestro ejemplo, hemos incluido todos los componentes en la plantilla. En particular, en la plantilla anterior, vemos la etiqueta <style> con estilos CSS y la etiqueta <script> con algunas funciones JavaScript, que se omiten. Esto se hace para simplificar el ejemplo, con hincapié en las características MQL5 en lugar de desarrollo web.
Teniendo una plantilla de página web en la variable ReportPageTemplate conectada como recurso, podemos escribir el método render.
virtual void render() override
|
En realidad, divide la página en mitad superior e inferior mediante el carácter '~', las muestra tal cual y llama a un método de ayuda renderContent entre ellas.
Ya hemos descrito que el informe consistirá en un cuadro general con curvas de balance y tablas con indicadores de sistemas de trading, por lo que la implementación renderContent es natural.
private:
|
La generación de imágenes dentro de renderSVG se basa en otro archivo de plantilla TradeReportSVG.htm, que se vincula a una variable de cadena SVGBoxTemplate:
#resource "TradeReportSVG.htm" as string SVGBoxTemplate |
El contenido de esta plantilla es el último que enumeramos aquí. Quienes lo deseen pueden consultar por sí mismos los códigos fuente del resto de plantillas.
<span id="params" style="display:block;width:%WIDTH%px;text-align:center;"></span>
|
En el código del método renderSVG, veremos el conocido truco de dividir el contenido en dos bloques «antes» y «después» de la tilde, pero aquí hay algo nuevo:
void renderSVG()
|
En la parte superior de la página, en la cadena headerAndFooter[0], buscamos subcadenas de la forma especial «%WIDTH%» y «%HEIGHT%», y las sustituimos por la anchura y la altura requeridas de la imagen. Este es el principio por el que funciona la sustitución de valores en nuestras plantillas. Por ejemplo, en esta plantilla, estas subcadenas aparecen en la etiqueta rect:
<rect x="0" y="0" width="%WIDTH%" height="%HEIGHT%" style="fill:none; stroke-width:1; stroke: black;"/> |
Así, si el informe se pide con un tamaño de 600 por 400, la línea se convertirá en la siguiente:
<rect x="0" y="0" width="600" height="400" style="fill:none; stroke-width:1; stroke: black;"/> |
Esto mostrará un borde negro de 1 píxel de grosor de las dimensiones especificadas en el navegador.
La generación de etiquetas para dibujar líneas de balance específicas se gestiona mediante el método renderCurve, al que pasamos todos los arrays necesarios y otros ajustes (nombre, color y grosor). Dejaremos este método y otros muy especializados (renderTables, renderTable) para un estudio independiente.
Volvamos al módulo principal del Asesor Experto UnityMartingaleDraft3.mq5. Ajuste el tamaño de la imagen de los gráficos de balance y conecte TradeReportWriter.mqh.
#define MINIWIDTH 400
|
Para «conectar» las estrategias con el generador de informes, tendrá que modificar el método statement en la interfaz TradingStrategy: pase un puntero al objeto TradeReportWriter, que el código de llamada puede crear y configurar.
interface TradingStrategy
|
Ahora vamos a añadir algunas líneas en la implementación específica de este método en nuestra clase de estrategia UnityMartingale.
class UnityMartingale: public TradingStrategy
|
Todo se reduce a obtener un array de balance y una estructura con indicadores del objeto report (clase TradeReport) y pasarlo al objeto TradeReportWriter, llamando a addCurve.
Por supuesto, el conjunto de estrategias de trading garantiza la transferencia del mismo objeto TradeReportWriter a todas las estrategias para generar un informe combinado.
class TradingStrategyPool: public TradingStrategy
|
Por último, el manejador OnTester ha sufrido la mayor modificación. Las siguientes líneas bastarían para generar un informe HTML de las estrategias de trading.
double OnTester()
|
Sin embargo, para mayor claridad y comodidad del usuario, sería estupendo añadir al informe una curva de balance general, así como un cuadro con indicadores generales. Tiene sentido emitirlos sólo cuando se especifican varios símbolos en la configuración del Asesor Experto porque, de lo contrario, el informe de una estrategia coincide con el general del archivo.
Esto requería un poco más de código.
double OnTester()
|
Veamos qué tenemos. Si ejecutamos el Asesor Experto con la configuración UnityMartingale-combo.set, tendremos el archivo temp.html en la carpeta MQL5/Files de uno de los agentes. Este es el aspecto que tiene en el registro:
Informe HTML para Asesor Experto con múltiples estrategias/símbolos de trading
Ahora que sabemos cómo generar informes en una pasada de prueba, podemos enviarlos al terminal durante la optimización, seleccionar los mejores sobre la marcha y presentarlos al usuario antes de que finalice todo el proceso. Todos los informes se colocarán en una carpeta separada dentro de MQL5/Files del terminal. La carpeta recibirá un nombre que contendrá el símbolo y el marco temporal de la configuración del probador, así como el nombre del Asesor Experto.
UnityMartingale.mq5
Como sabemos, para enviar un archivo al terminal, basta con llamar a la función FrameAdd. Ya hemos generado el archivo en el marco de la versión anterior.
double OnTester()
|
En la instancia receptora del Asesor Experto, realizaremos la preparación necesaria. Describamos la estructura Pass con los principales parámetros de cada pase de optimización.
struct Pass
|
En las cadenas parameters, los pares «name=value» se conectan con el símbolo '&'. Esto será útil para la interacción de páginas web de informes en el futuro (el símbolo '&' es el estándar para combinar parámetros en direcciones web). No hemos descrito el formato de los archivos de conjuntos, pero el siguiente código fuente que forma la cadena preset permite estudiar esta cuestión en la práctica.
A medida que lleguen frames, escribiremos mejoras según el criterio de optimización en el array TopPasses. La mejor pasada actual será siempre la última pasada del array y también está disponible en la variable BestPass.
Pass TopPasses[]; // stack of constantly improving passes (last one is best)
|
En el manejador OnTesterInit vamos a crear un nombre de carpeta.
void OnTesterInit()
|
En el manejador OnTesterPass seleccionaremos secuencialmente sólo aquellos frames en los que el indicador haya mejorado, encontraremos para ellos los valores de los parámetros optimizados y otros, y añadiremos toda esta información al array de estructuras Pass.
void OnTesterPass()
|
Los informes resultantes con las mejoras se guardan en archivos con nombres que incluyen el valor del criterio de optimización y el número de pasada.
Ahora viene lo más interesante: en el manejador OnTesterDeinit, podemos formar un archivo HTML común (overall.htm), que permite ver todos los informes a la vez (o, digamos, los 100 primeros). Utiliza el mismo esquema con plantillas que hemos visto antes.
#resource "OptReportPage.htm" as string OptReportPageTemplate
|
En la siguiente imagen se muestra el aspecto de la página web de resumen después de optimizar UnityMartingale.mq5 mediante el parámetro UnityPricePeriod en modo multidivisa.
Página web general con informes de trading de los mejores pases de optimización
Para cada informe, mostramos sólo la parte superior, donde cae el gráfico del balance. Esta parte es la más conveniente para obtener una estimación con sólo mirarla.
Encima de cada gráfico aparecen listas de parámetros optimizados («name=value&name=value...»). Al pulsar sobre una línea se abre un bloque con el texto del archivo de configuración de todas las opciones de configuración de este pase. Si hace clic dentro de un bloque, su contenido se copiará en el portapapeles. Se puede guardar en un editor de texto y obtener así un archivo de conjunto listo.
Si hace clic en el gráfico, accederá a la página específica del informe, junto con los scorecards (indicados anteriormente).
Al final de la sección abordamos una cuestión más. Antes prometimos demostrar el efecto de la función TesterHideIndicators. El Asesor Experto UnityMartingale.mq5 utiliza actualmente el indicador UnityPercentEvent.mq5. Después de cualquier prueba, el indicador se muestra en el gráfico de apertura. Supongamos que queremos ocultar al usuario el mecanismo de trabajo del Asesor Experto y de dónde toma las señales. A continuación, puede llamar a la función TesterHideIndicators (con el parámetro true) en el manejador OnInit, antes de crear el objeto UnityController, en el que se recibe el descriptor a través de iCustom.
int OnInit()
|
Esta versión del Asesor Experto ya no mostrará el indicador en el gráfico. Sin embargo, no está muy bien escondido. Si miramos en el registro del probador, veremos líneas sobre programas cargados entre un montón de información útil: primero, un mensaje sobre la carga del propio Asesor Experto, y un poco más tarde, sobre la carga del indicador.
...
|
Así, un usuario meticuloso puede averiguar el nombre del indicador. Esta posibilidad puede eliminarse mediante el mecanismo de recursos, que ya hemos mencionado de pasada en el contexto de los espacios en blanco de las páginas web. Resulta que el indicador compilado también se puede incrustar en un programa MQL (en un Asesor Experto u otro indicador) como un recurso. Y estos programas de recursos ya no se mencionan en el registro del probador. Estudiaremos los recursos en detalle en la 7ª Parte del libro, y ahora mostraremos las líneas asociadas a ellos en la versión final de nuestro Asesor Experto.
En primer lugar, vamos a describir el recurso con la directiva de indicador #resource. De hecho, contiene simplemente la ruta al archivo del indicador compilado (obviamente, ya debe estar compilado de antemano), y aquí es obligatorio utilizar barras invertidas dobles como delimitadores, ya que no se admiten barras diagonales simples en las rutas de recursos.
#resource "\\Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5" |
A continuación, en las líneas con la llamada iCustom, sustituimos el operador anterior:
UnityController(const string symbolList, const int offset, const int limit,
|
Exactamente igual, pero con un enlace al recurso (nótese la sintaxis con un par de dos puntos '::' al principio, necesaria para distinguir entre las rutas normales en el sistema de archivos y las rutas dentro de los recursos).
UnityController(const string symbolList, const int offset, const int limit,
|
Ahora la versión compilada del Asesor Experto puede entregarse a los usuarios por sí sola, sin un indicador separado, ya que está oculta dentro del Asesor Experto. Esto no afecta en absoluto a su rendimiento, pero teniendo en cuenta el reto TesterHideIndicators, el dispositivo interno queda oculto. Hay que tener en cuenta que si el indicador se actualiza, el Asesor Experto también tendrá que volver a compilarse.