Implementando OLAP en la negociación (Parte 4): Análisis cuantitativo y visual de los informes del Simulador de estrategias
En este artículo, seguiremos familiarizándonos con la tecnología OLAP, que proviene de On-Line Analytical Processing (Procesamiento Analítico en Línea), aplicándola al trading.
En los artículos anteriores, fueron descritos los métodos generales de la construcción de las clases para la acumulación y el análisis de datos multidimensionales, así como, la visualización de los resultados del análisis en la interfaz gráfica. Desde el punto de vista práctico, en los dos primeros artículos se trataba de los informes comerciales obtenidos de maneras diferentes: desde el Simulador de estrategias, desde el historial del trading en línea, desde los archivos en el formato HTML y CSV (incluyendo las señales comerciales MQL5). En el tercer artículo, tras una pequeña refactorización del código, OLAP fue usado para analizar las cotizaciones y elaborar las estrategias comerciales. Para comprender mejor el material nuevo, es necesario estudiar estos artículos (entre paréntesis se indica en lo que debe fijarse en primer lugar):
- Parte 1: Fundamentos del análisis corriente de datos multidimensionales (selectores, agregadores, cálculo del hipercubo, adaptadores de lectura de los registros sobre las operaciones comerciales desde el historial de la cuenta, archivos HTML, archivos CSV),
- Parte 2: Visualización de los resultados del análisis interactivo de los datos multidimensionales (ventanas extensibles y controles, diseño y principio del comportamiento de la interfaz gráfica de OLAP),
- Parte 3: Analizando las cotizaciones con el fin de desarrollar las estrategias comerciales (clase actualizada del motor de OLAP, selectores y agregador nuevos, ejemplo de implementación del adaptador y registros para otra área aplicada (cotizaciones) guardando el enfoque unificado que será usado en este artículo).
Hoy vamos a ampliar el área de la aplicación de OLAP gracias al análisis de los resultados de la optimización de MetaTrader 5.
Para realizar este proyecto, primero, necesitaremos perfeccionar un poco la interfaz gráfica de usuario considerada en el artículo 2. La cosa es que las mejoras del código realizadas en el artículo 3 se redujeron únicamente al motor de OLAP, mientras que la visualización no fue sometida a la actualización. Vamos a corregir esta negligencia usando el programa OLAPGUI para el análisis de informes comerciales como tarea de prueba. Lo conocimos en el segundo artículo. Al mismo tiempo, vamos a unificar esta parte gráfica de tal manera que se pueda combinarla fácilmente con cualquier nueva orientación aplicable, en particular, con el analizador planeado de los resultados de la optimización.
Gráfica comercial en el punto de mira
Vamos a recordar que el centro de GUI para OLAP es el componente visual CGraphicInPlot desarrollado especialmente. Su primera implementación presentada en el artículo 2 adolecía de algunos defectos. En primer lugar, eso se refería a la visualización de las etiquetas en los ejes. Aprendimos, si era necesario, a visualizar los nombres de las células de los selectores (como nombres de los días de la semana o divisas) en el eje horizontal X. Pero en todos los demás casos, el gráfico supone la visualización de un número «tal como es», lo cual no siempre es legible (o «amigable para el usuario»). A parte de eso, el eje Y, donde habitualmente se muestran los valores agregados (pero dependiendo de los ajustes también se puede mostrar las células de los selectores), también requiere una personalización. La solicitud del mantenimiento medio de la posición para el símbolo nos conviene muy bien como ejemplo de una visualización inconveniente.
Duración media de la posición para los símbolos (segundos)
Puesto que aquí, en el eje Y, se muestra un valor agregado de la duración en segundos, en vez del selector que redondea los valores hasta el tamaño de las células del cubo, los números grandes se perciben mal. Para solucionar este problema, intentaremos dividir segundos por la duración de la barra del marco temporal (timeframe) actual. En este caso, los valores visualizados mostrarán el número de las barras. Para ello, es necesario transferir una bandera a la clase CGraphicInPlot, y luego, a la clase incorporada del procesamiento de ejes CAxis. Las banderas que cambian el modo de trabajo pueden ser muchas, por tanto, vamos a reservar la nueva clase especial AxisCustomizer para ellas en el archivo Plot.mqh.
class AxisCustomizer { public: const CGraphicInPlot *parent; const bool y; // true for Y, false for X const bool periodDivider; const bool hide; AxisCustomizer(const CGraphicInPlot *p, const bool axisY, const bool pd = false, const bool h = false): parent(p), y(axisY), periodDivider(pd), hide(h) {} };
Potencialmente, se podrá incluir varios detalles de visualización de etiquetas en ella, pero por ahora incluye solamente el indicio del tipo del eje (X o Y) y algunas operaciones lógicas, como periodDivider y hide. La primera de ellas significa que se requiere dividir los valores por PeriodSeconds(), la segunda será considerada más tarde.
Los objetos de esta clase se incluyen en CGraphicInPlot a través de los siguientes métodos especiales:
class CGraphicInPlot: public CGraphic { ... void InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL); void InitXAxis(const AxisCustomizer *custom = NULL); void InitYAxis(const AxisCustomizer *custom = NULL); }; void CGraphicInPlot::InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL) { if(custom) { axe.Type(AXIS_TYPE_CUSTOM); axe.ValuesFunctionFormat(CustomDoubleToStringFunction); axe.ValuesFunctionFormatCBData((AxisCustomizer *)custom); } else { axe.Type(AXIS_TYPE_DOUBLE); } } void CGraphicInPlot::InitXAxis(const AxisCustomizer *custom = NULL) { InitAxes(m_x, custom); } void CGraphicInPlot::InitYAxis(const AxisCustomizer *custom = NULL) { InitAxes(m_y, custom); }
Cuando semejante objeto no se crea y no se traspasa a las clases gráficas, la biblioteca estándar muestra los valores de manera habitual, es decir, como un número (AXIS_TYPE_DOUBLE).
Aquí, usamos el enfoque de la biblioteca estándar para la personalización de las etiquetas en los ejes: el tipo del eje se establece igual a AXIS_TYPE_CUSTOM, el puntero al objeto AxisCustomizer se traspasa a través de ValuesFunctionFormatCBData. Posteriormente, la clase base CGraphic lo traspasa en nuestra función del dibujado de la etiqueta CustomDoubleToStringFunction (se empieza con la llamada a ValuesFunctionFormat en el fragmento del código de arriba). Está claro que necesitaremos la propia función CustomDoubleToStringFunction. Ya fue implementada antes, pero de una forma simplificada, sin usar los objetos de la clase seleccionada AxisCustomizer (es que el propio gráfico CGraphicInPlot nos servía de este objeto configurado).
string CustomDoubleToStringFunction(double value, void *ptr) { AxisCustomizer *custom = dynamic_cast<AxisCustomizer *>(ptr); if(custom == NULL) return NULL; // check options if(!custom.y && custom.hide) return NULL; // case of X axis and "no marks" mode // in simple cases return a string if(custom.y) return (string)(float)value; const CGraphicInPlot *self = custom.parent; // obtain actual object with cache if(self != NULL) { ... // retrieve selector mark for value } }
Los objetos de la personalización AxisCustomizer se almacenan en la clase CPlot, que representa un control de la interfaz (derivado de CWndClient) y un contenedor para CGraphicInPlot:
class CPlot: public CWndClient { private: CGraphicInPlot *m_graphic; ENUM_CURVE_TYPE type; AxisCustomizer *m_customX; AxisCustomizer *m_customY; ... public: void InitXAxis(const AxisCustomizer *custom = NULL) { if(CheckPointer(m_graphic) != POINTER_INVALID) { if(CheckPointer(m_customX) != POINTER_INVALID) delete m_customX; m_customX = (AxisCustomizer *)custom; m_graphic.InitXAxis(custom); } } ... };
Así, es posible usar las configuraciones de los ejes en los objetos m_customX y m_customY no sólo en la fase avanzada del formateo de los valores en la función CustomDoubleToStringFunction, sino también mucho antes: cuando los arrays con datos sólo se traspasan a CPlot a través de uno de los métodos de CurveAdd, por ejemplo, así:
CCurve *CPlot::CurveAdd(const PairArray *data, const string name = NULL) { if(CheckPointer(m_customY) != POINTER_INVALID) && m_customY.periodDivider) { for(int i = 0; i < ArraySize(data.array); i++) { data.array[i].value /= PeriodSeconds(); } } return m_graphic.CurveAdd(data, type, name); }
Precisamente aquí vemos la aplicación de la opción periodDivider para dividir todos los valores por PeriodSeconds(). Esta operación se ejecuta antes de que la biblioteca estándar reciba los datos y calcule el tamaño del paso de la cuadrícula para ellos. Es importante, ya que una vez calculada la cuadrícula, ya es tarde realizar la personalización relacionada con el escalado en la función CustomDoubleToStringFunction.
El código de la llamada en el diálogo debe crear e inicializar los objetos AxisCustomizer, según la necesidad, en el momento de la construcción del cubo, por ejemplo, así:
AGGREGATORS at = ... // get aggregator type from GUI ENUM_FIELDS af = ... // get aggregator field from GUI SORT_BY sb = ... // get sorting mode from GUI int dimension = 0; // calculate cube dimensions from GUI for(int i = 0; i < AXES_NUMBER; i++) { if(Selectors[i] != SELECTOR_NONE) dimension++; } bool hideMarksOnX = (dimension > 1 && SORT_VALUE(sb)); AxisCustomizer *customX = NULL; AxisCustomizer *customY = NULL; customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, hideMarksOnX); if(af == FIELD_DURATION) { customY = new AxisCustomizer(m_plot.getGraphic(), true, true); } m_plot.InitXAxis(customX); m_plot.InitYAxis(customY);
Aquí, m_plot es una variable del diálogo en la que se almacena el control CPlot. Mas tarde, se mostrará el código completo del método OLAPDialog::process, donde se verá cómo funciona eso en realidad. Es el ejemplo del gráfico arriba mencionado con el modo periodDivider activado automáticamente:
Duración media de la posición para los símbolos (barras del timeframe actual, D1)
Otra variable hide en AxisCustomizer supone la posibilidad de ocultar completamente las etiquetas en el eje X. Este modo es necesario para la situación cuando el usuario selecciona la ordenación por el valor en un cubo multidimensional. En este caso, las etiquetas de cada serie de los números van a tener su propio orden y no hay nada que mostrar a lo largo del eje X. La ordenación en un cubo multidimensional está disponible porque tiene sentido en otros modos, en particular, por las etiquetas.
El funcionamiento de la opción hide se realiza dentro de CustomDoubleToStringFunction. El comportamiento estándar de esta función supone la presencia de los selectores: sus etiquetas se almacenan en caché para el eje X en la clase especial CurveSubtitles y se retornan al gráfico según el índice de la división de la cuadrícula. Sin embargo, la bandera activada hide interrumpe este proceso al principio para cualquier abscisa, y la función devuelve NULL (valor no visible).
El segundo problema que hay que resolver en la gráfica comercial está relacionado con el dibujado del histograma. Cuando en el gráfico se muestran varias series (vectores con datos), las columnas del histograma en cada medición se sobreponen completamente una sobre otra, y la más grande de ellas puede solapar las demás.
La clase base CGraphic contiene el método virtual HistogramPlot. Es necesario redefinirlo y separar visualmente de alguna manera las columnas de una medición. Para este propósito, sería deseable tener un campo personalizado dentro del objeto de la curva CCurve, que almacene datos aleatorios (estos datos se interpreten por el código de usuario tal como le sea necesario). Lamentablemente, no tenemos este campo, y por tanto, tendremos que usar una de las propiedades estándar que no se utiliza en el presente proyecto. La elección recayó en LinesSmoothStep. A través del método «setter» CCurve::LinesSmoothStep, nuestro código de la llamada va a escribir ahí el número de orden de la serie, y a través del método «getter» CCurve::LinesSmoothStep, es fácil de obtenerlo en la nueva implementación de HistogramPlot. A continuación, tiene un ejemplo de la escritura del número de la serie en LinesSmoothStep:
CCurve *CGraphicInPlot::CurveAdd(const double &x[], const double &y[], ENUM_CURVE_TYPE type, const string name = NULL) { CCurve *c = CGraphic::CurveAdd(x, y, type, name); c.LinesSmoothStep((int)CGraphic::CurvesTotal()); // + ... return CacheIt(c); }
Sabiendo la cantidad total de las series y el número de la serie actual, podemos desplazar cada su punto un poco a la izquierda o a la derecha desde el centro de la medición durante el dibujado. Aquí tiene una versión adaptada de HistogramPlot. Las líneas modificadas están marcadas con los comentarios con el símbolo "*", las líneas añadidas, con "+".
void CGraphicInPlot::HistogramPlot(CCurve *curve) override { const int size = curve.Size(); const double offset = curve.LinesSmoothStep() - 1; // + double x[], y[]; int histogram_width = curve.HistogramWidth(); if(histogram_width <= 0) return; curve.GetX(x); curve.GetY(y); if(ArraySize(x) == 0 || ArraySize(y) == 0) return; const int w = m_width / size / 2 / CGraphic::CurvesTotal(); // + const int t = CGraphic::CurvesTotal() / 2; // + const int half = ((CGraphic::CurvesTotal() + 1) % 2) * (w / 2); // + int originalY = m_height - m_down; int yc0 = ScaleY(0.0); uint clr = curve.Color(); for(int i = 0; i < size; i++) { if(!MathIsValidNumber(x[i]) || !MathIsValidNumber(y[i])) continue; int xc = ScaleX(x[i]); int yc = ScaleY(y[i]); int xc1 = xc - histogram_width / 2 + (int)(offset - t) * w + half; // * int xc2 = xc + histogram_width / 2 + (int)(offset - t) * w + half; // * int yc1 = yc; int yc2 = (originalY > yc0 && yc0 > 0) ? yc0 : originalY; if(yc1 > yc2) yc2++; else yc2--; m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr); } }
Pronto comprobaremos qué aspecto tiene eso.
Otro momento desagradable está relacionado con la implementación estándar de la visualización de líneas. Si en los datos se encuentra algo diferente al número, CGraphic interrumpe la línea. Eso está mal para nuestra tarea, ya que es posible que en realidad no haya datos en algunas células del cubo, y en este caso, los agregadores escriben NaN ahí. Por ejemplo, algunos cubos, como el balance por el total cumulativo por varias secciones transversales, están «descargadas» por su naturaleza, porque en cada transacción se cambia el importe sólo en una sección. Para comprender el efecto negativo de la ruptura de las líneas en la percepción basta con echar un vistazo a la imagen «Curvas del balance para cada símbolo por separado» en el artículo 2.
Para resolver este problema, el método LinesPlot fue redefinido adicionalmente (ver códigos fuente, archivo Plot.mqh). El resultado del trabajo se muestra un poco más abajo, en el apartado del procesamiento de los archivos estándar del Simulador.
Finalmente, el último problema con la gráfica concierne a la definición de los ejes cero en la Biblioteca estándar. En el método CGraphic::CreateGrid, la búsqueda de los ceros se realiza de la siguiente manera trivial (se muestra el caso con el eje Y, el eje X se procesa de la misma manera):
if(StringToDouble(m_yvalues[i]) == 0.0) ...
Nótese que aquí m_yvalues representa las etiquetas tipo string. Obviamente, cualquier etiqueta que no contiene un número va a proporcionar un 0. Eso ocurre incluso cuando para el gráfico se establece el tipo de visualización AXIS_TYPE_CUSTOM, como en nuestro caso. Como resultado, al visualizar el gráfico por divisas, días de la semana, tipos de transacciones y otros selectores, todas las mediciones se interpretan sucesivamente como nulos a la medida de que se comprueban en el ciclo a lo largo de la cuadrícula completa, pero la «última palabra» se reserva para la última medición, la que se marca con una recta más gruesa en el gráfico (aunque no es un cero). Es más, cuando cada medición se convierte, aunque sea temporalmente, en un candidato a 0, omite el dibujado de la línea común de la cuadrícula, por lo cual la cuadrícula entera simplemente desaparece.
Dado que el método CreateGrid también es virtual, lo redefinimos con una verificación más intelectual de 0 que se pasará a la función auxiliar isZero.
bool CGraphicInPlot::isZero(const string &value) { if(value == NULL) return false; double y = StringToDouble(value); if(y != 0.0) return false; string temp = value; StringReplace(temp, "0", ""); ushort c = StringGetCharacter(temp, 0); return c == 0 || c == '.'; } void CGraphicInPlot::CreateGrid(void) override { int xc0 = -1.0; int yc0 = -1.0; for(int i = 1; i < m_ysize - 1; i++) { m_canvas.LineHorizontal(m_left + 1, m_width - m_right, m_yc[i], m_grid.clr_line); // * if(isZero(m_yvalues[i])) yc0 = m_yc[i]; // * for(int j = 1; j < m_xsize - 1; j++) { if(i == 1) { m_canvas.LineVertical(m_xc[j], m_height - m_down - 1, m_up + 1, m_grid.clr_line); // * if(isZero(m_xvalues[j])) xc0 = m_xc[j]; // * } if(m_grid.has_circle) { m_canvas.FillCircle(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle); m_canvas.CircleWu(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle); } } } if(yc0 > 0) m_canvas.LineHorizontal(m_left + 1, m_width - m_right, yc0, m_grid.clr_axis_line); if(xc0 > 0) m_canvas.LineVertical(xc0, m_height - m_down - 1, m_up + 1, m_grid.clr_axis_line); }
Interfaz gráfica de OLAP
Pues, hemos terminado la corrección de los defectos en el gráfico, pero la interfaz de ventanas todavía requiere una revisión en cuanto a la universalización. En el Asesor Experto no negociable OLAPGUI del segundo artículo, todo el trabajo con el diálogo se concentraba en el archivo de cabecera OLAPGUI.mqh. Ahí se almacenaban muchas particularidades aplicadas de la tarea anterior (análisis de informes comerciales). Puesto que vamos a usar el mismo diálogo para los datos aleatorios, hay que dividir este archivo en 2 partes: en una de ellas, se reúne todo el comportamiento general de la interfaz, en otra, las configuraciones de un determinado proyecto.
La clase anterior OLAPDialog recibe el nombre OLAPDialogBase. Los arrays selectors, settings, defaults con la codificación rígida, que en realidad describen los controles del diálogo, se convierten en unas piezas dinámicas en blanco (las clases derivadas se encargarán de su relleno). Las variables:
OLAPWrapper *olapcore; // <-- template <typename S,typename T> class OLAPEngine, since part 3
OLAPDisplay *olapdisplay;
también irán a los herederos, porque habrá que plantillarlas usando los tipos de los selectores y de los campos de registros que se determinan en la parte aplicada de cada motor OLAP. Recordemos que la clase antigua OLAPWrapper fue transformada en la clase-plantilla OLAPEngine<S,T> durante la refactorización en el artículo 3.
Para ejecutar el trabajo principal, tenemos reservados 2 nuevos métodos abstractos:
virtual void setup() = 0; virtual int process() = 0;
El primer método (setup) configura la interfaz, el segundo (process) inicia el análisis. La configuración se invoca desde OLAPDialogBase::Create
bool OLAPDialogBase::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2) { setup(); // + ... }
El análisis se inicia por el usuario pulsando el botón. Por tanto, las modificaciones más importantes han afectado al método OLAPDialogBase::OnClickButton. Ahí ha sido eliminada la mayor parte del código y la funcionalidad correspondiente (lectura de las propiedades de los controles e inicio del motor OLAP a su base) ha sido delegada al método process.
void OLAPDialogBase::OnClickButton(void) { if(processing) return; // prevent re-entrancy if(browsing) // 3D-cube browsing support { currentZ = (currentZ + 1) % maxZ; validateZ(); } processing = true; const int n = process(); if(n == 0 && processing) { finalize(); } }
Nótese que la clase OLAPDialogBase implementa toda la lógica del trabajo de la interfaz, empezando de la creación de los controles y terminando con el procesamiento de los eventos que influyen en su estado. Sin embargo, ella no sabe nada del contenido de los controles.
La clase OLAPDisplay implementa la interfaz virtual Display desde OLAPCommon.mqh (fue considerada en el artículo 3). Recordemos que la interfaz Display es un punto de la llamada inversa desde el núcleo de OLAP para la representación de los resultados del análisis (se transmiten en el primer parámetro, en el objeto de la clase MetaCube). Gracias al puntero a la ventana padre parent, en la clase OLAPDisplay ha sido implementada una cadena de la transmisión posterior de los datos del cubo al diálogo (este «traspaso» ha sido requerido porque en MQL5 no hay herencia múltiple).
class OLAPDisplay: public Display { private: OLAPDialogBase *parent; public: OLAPDisplay(OLAPDialogBase *ptr,): parent(ptr) {} virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override; };
Aquí, merece la pena matizar el detalle relacionado con la obtención de los nombres de los campos personalizados «reales» de parte de las clases derivadas de los adaptadores. La cosa es que aunque hasta ahora añadíamos nuestros propios campos personalizados (como, por ejemplo, MFE y MAE en el artículo 2) a los campos estándar objetuales, en realidad, se conocían de antemano y se incorporaban en el código. No obstante, cuando vamos a trabajar con los informes de la optimización, habrá que analizarlos en contexto de los parámetros de entrada de los robots, pudiendo obtener estos parámetros (sus nombres) solamente desde los datos analizados.
El adaptador traspasa los nombres de los campos personalizados en el agregador (metacubo) a través del método nuevo assignCustomFields. Además, todo eso ocurre «al fondo», es decir, automáticamente en el método Analyst::acquireData. Gracias a eso, cuando dentro de OLAPDisplay::display se invoca el método metaData.getDimensionTitle para obtener la designación de las secciones por los ejes, y el número de orden del campo n supera la potencia de la enumeración incorporada de los campos, entonces sabemos que se trata de un campo extendido y podemos solicitar la descripción al cubo. La estructura general del método OLAPDisplay::display no se ha cambiado. Podemos cerciorarnos fácilmente de ello si comparamos los códigos fuente adjuntos con la versión del artículo 2.
Además, es necesario saber de antemano los nombres de los campos personalizados en el diálogo para rellenar los elementos de la interfaz con ellos. Para este propósito, a la clase OLAPDialogBase le ha sido añadido el método nuevo setCustomFields que sirve para configurar los campos personalizados.
int customFieldCount; string customFields[]; virtual void setCustomFields(const DataAdapter &adapter) { string names[]; if(adapter.getCustomFields(names) > 0) { customFieldCount = ArrayCopy(customFields, names); } }
Está claro que tendremos que vincular el diálogo con el adaptador en el EA de prueba usando este método (véase a continuación). Después de eso, se podrá ver los nombres de los campos más razonables dentro de los controles del diálogo (en vez de los números custom 1, etc.). Es una solución temporal. Este y algunos otros aspectos requieren una optimización subsiguiente del código, pero se consideran insignificativos en el contexto de este artículo.
La parte aplicada de la configuración de la interfaz para el programa modificado OLAPGUI ha mudado de OLAPGUI.mqh al archivo de cabecera OLAPGUI_Trades.mqh. El nombre de la clase del diálogo ha quedado intacto, OLAPDialog, pero depende de los parámetros de plantilla que se utilizan luego durante la especialización del objeto OLAPEngine:
template<typename S, typename F> class OLAPDialog: public OLAPDialogBase { private: OLAPEngine<S,F> *olapcore; OLAPDisplay *olapdisplay; public: OLAPDialog(OLAPEngine<S,F> &olapimpl); ~OLAPDialog(void); virtual int process() override; virtual void setup() override; }; template<typename S, typename F> OLAPDialog::OLAPDialog(OLAPEngine<S,F> &olapimpl) { curveType = CURVE_POINTS; olapcore = &olapimpl; olapdisplay = new OLAPDisplay(&this); } template<typename S, typename F> OLAPDialog::~OLAPDialog(void) { delete olapdisplay; }
Todo el trabajo se realiza en los métodos setup y process. El método setup rellena los arrays settings, selectors, defaults con los mismos valores que conocemos del artículo 2 (puesto que la apariencia de la interfaz no se cambia). El método process inicia el análisis en la sección especificada y casi repite por completo el manejador anterior OnClickButton.
template<typename S, typename F> int OLAPDialog::process() override { SELECTORS Selectors[4]; ENUM_FIELDS Fields[4]; AGGREGATORS at = (AGGREGATORS)m_algo[0].Value(); ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value(); SORT_BY sb = (SORT_BY)m_algo[2].Value(); ArrayInitialize(Selectors, SELECTOR_NONE); ArrayInitialize(Fields, FIELD_NONE); int matches[10] = // selectors in combo-boxes (specific record fields are bound internally) { SELECTOR_NONE, SELECTOR_SERIAL, SELECTOR_SYMBOL, SELECTOR_TYPE, SELECTOR_MAGIC, SELECTOR_WEEKDAY, SELECTOR_WEEKDAY, SELECTOR_DAYHOUR, SELECTOR_DAYHOUR, SELECTOR_DURATION }; int subfields[] = // record fields listed in combo-boxes after selectors and accessible directly { FIELD_LOT, FIELD_PROFIT_AMOUNT, FIELD_PROFIT_PERCENT, FIELD_PROFIT_POINT, FIELD_COMMISSION, FIELD_SWAP, FIELD_CUSTOM_1, FIELD_CUSTOM_2 }; for(int i = 0; i < AXES_NUMBER; i++) // up to 3 orthogonal axes are supported { if(!m_axis[i].IsVisible()) continue; int v = (int)m_axis[i].Value(); if(v < 10) // selectors (every one is specialized for a field already) { Selectors[i] = (SELECTORS)matches[v]; if(v == 5 || v == 7) Fields[i] = FIELD_OPEN_DATETIME; else if(v == 6 || v == 8) Fields[i] = FIELD_CLOSE_DATETIME; } else // pure fields { Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS; Fields[i] = (TRADE_RECORD_FIELDS)subfields[v - 10]; } } m_plot.CurvesRemoveAll(); AxisCustomizer *customX = NULL; AxisCustomizer *customY = NULL; if(at == AGGREGATOR_IDENTITY || at == AGGREGATOR_COUNT) af = FIELD_NONE; if(at != AGGREGATOR_PROGRESSIVE) { customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, (dimension > 1 && SORT_VALUE(sb))); } if((af == FIELD_DURATION) || (at == AGGREGATOR_IDENTITY && Selectors[1] == SELECTOR_DURATION)) { customY = new AxisCustomizer(m_plot.getGraphic(), true, true); } m_plot.InitXAxis(customX); m_plot.InitYAxis(customY); m_button_ok.Text("Processing..."); return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb); }
Por el fin del método vemos la creación de los objetos de la configuración de los ejes AxisCustomizer que han sido descritos antes. Para ambos ejes X y Y, la división de los valores por PeriodSeconds() se activa al trabajar con el campo de la duración (bien en el agregador, bien en el selector, si el tipo del agregador es AGGREGATOR_IDENTITY —en este caso, los selectores no colocan el contenido de los campos en las células nombradas, sino entra en el cubo directamente). El eje X se desactiva cuando la dimensionalidad del cubo es más de 1 y tenemos seleccionada la ordenación de los valores.
Nos queda echar un vistazo al archivo OLAPGUI.mq5 del programa. Entre las diferencias de la versión antigua, podemos ver un orden de inclusión de archivos de cabecera ligeramente cambiado: si antes los adaptadores para los informes estaban incluidos en el núcleo (ya que todavía no había otras fuentes de datos), ahora tienen que escribirse en HTMLcube.mqh y CSVcube.mqh de forma explícita. Luego, en el código OnInit, se realiza la preparación del tipo correspondiente del adaptador en función de los datos de entrada y su traspaso en el motor a través de la llamada a _defaultEngine.setAdapter. Este fragmento ya aparecía en el programa OLAPRPRT.mq5 del artículo 3, donde se testeaba por primera vez un enfoque correcto con la descomposición en una parte universal y aplicada. La verdad es que en aquel caso OLAPRPRT se quedó sin interfaz gráfica, y ahora vamos a intentar acercarnos paso a paso a la liquidación de esta injusticia.
Con el fin de demostrar una separación estricta de los campos de registros tipo estándar y personalizados, la clase CustomTradeRecord con el cálculo de los campos MFE y MAE fue pasado de OLAPTrades.mqh en OLAPTradesCustom.mqh (no lo mostramos aquí, los códigos fuente se adjuntan). Así, se simplifica la escritura de otros campos personalizados a base de las transacciones si eso es necesario. Basta con cambiar el algoritmo en OLAPTradesCustom.mqh, dejando el núcleo de OLAP absolutamente intacto. Todas las cosas estándar: campos de registros comerciales, selectores relacionados con ellos, clase base TradeRecord, motor OLAPEngineTrade y adaptador para el historial de la cuenta permanecen en OLAPTrades.mqh. Claro que para incluir todo eso en el proyecto, en OLAPTradesCustom.mqh hay una referencia a OLAPTrades.mqh.
#include <OLAP/OLAPTradesCustom.mqh> // internally includes OLAPTrades.mqh #include <OLAP/HTMLcube.mqh> #include <OLAP/CSVcube.mqh> #include <OLAP/GUI/OLAPGUI_trades.mqh> OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine); int OnInit() { if(ReportFile == "") { Print("Analyzing account history"); _defaultEngine.setAdapter(&_defaultHistoryAdapter); } else { if(StringFind(ReportFile, ".htm") > 0 && _defaultHTMLReportAdapter.load(ReportFile)) { _defaultEngine.setAdapter(&_defaultHTMLReportAdapter); } else if(StringFind(ReportFile, ".csv") > 0 && _defaultCSVReportAdapter.load(ReportFile)) { _defaultEngine.setAdapter(&_defaultCSVReportAdapter); } else { Print("Unknown file format: ", ReportFile); return INIT_PARAMETERS_INCORRECT; } } ... if(!dialog.Create(0, "OLAPGUI" + (ReportFile != "" ? " : " + ReportFile : ""), 0, 0, 0, 750, 560)) return INIT_FAILED; if(!dialog.Run()) return INIT_FAILED; return INIT_SUCCEEDED; }
Iniciamos el EA actualizado OLAPGUI.mq5 y construimos algunas secciones de datos para asegurarnos de que, primero, el nuevo principio de la integración dinámica de la dependencia del núcleo de los adaptadores aplicados y de los tipos de registros funcione como es debido y, segundo, las mejoras del código en la parte gráfica ofrezcan ventajas visuales esperadas.
Es más fácil ver los cambios en comparación con las capturas de pantalla similares del artículo 2. La «Dependencia de los campos beneficio y duración para cada transacción» ahora es así. Ahora la duración por el eje X se expresa en barras del timeframe actual (en este caso es D1), en vez de segundos.
Dependencia del beneficio de la duración (en barras del timeframe actual, D1)
La división de los beneficios por símbolos y días de la semana demuestra las columnas del histograma apartadas opuestamente y una cuadrícula correcta.
Beneficios según símbolos y días de la semana
El análisis de los beneficios según el tamaño del lote en las transacciones se muestra en la siguiente captura de pantalla. A diferencia del artículo 2, los valores de lotes se muestran directamente en el eje X y no en el log.
Beneficios según el tamaño de lotes
Finalmente, veamos cómo será el «Número de transacciones por símbolos y por tipos». En la versión anterior, teníamos que usar el estilo del dibujado con líneas porque los histogramas se solapaban. Aquí este problema no existe.
Número de transacciones por símbolos y por tipos (histograma)
Pues, aquí podríamos concluir la inmersión repetida en la tarea del análisis de informes comerciales, pero para completar el cuadro, es necesario mencionar otra fuente de datos que se ha hecho disponible para los programadores de MQL. Se trata de los archivos tst en el formato interno del Simulador de estrategias.
Conexión de archivos estándar de los informes del Simulador (*.tst)
Hace poco que los desarrolladores de MetaTrader 5 descubrieron los formatos de los archivos guardados por el Simulador. En particular, los datos sobre la pasada única que hasta este momento podíamos analizar sólo después de su exportación al informe HTML ahora están disponibles para la lectura directamente desde el archivo tst.
No vamos a profundizar en la estructura interna del archivo. Afortunadamente, ya existe la biblioteca para leer los archivos tst SingleTesterCache (su autor es fxsaber). Usándola según el principio de la «caja negra», podemos obtener fácilmente un array con entradas sobre transacciones comerciales. La transacción está representada en la biblioteca a través de la clase TradeDeal. Para obtener su lista, basta con incluir la biblioteca, crear el objeto de la clase principal SINGLETESTERCACHE y cargar el archivo necesario usando el método load.
#include <fxsaber/SingleTesterCache/SingleTesterCache.mqh> ... SINGLETESTERCACHE SingleTesterCache; if(SingleTesterCache.Load(file)) { Print("Tester cache import: ", ArraySize(SingleTesterCache.Deals), " deals"); }
El array SingleTesterCache.Deals contiene todas las transacciones para cada una de las cuales tenemos disponible toda la información del Simulador que se encuentra en los campos correspondientes.
El algoritmo de generación de posiciones comerciales a base de las transacción es absolutamente idéntico al que se usa durante la importación del informe HTML. Un estilo elegante de la POO supone que hace falta incluir las partes generales del código en una clase base, y luego heredar de ella HTMLReportAdapter y nuevo TesterReportAdapter.
Hagamos que la clase BaseReportAdapter (archivo ReportCubeBase.mqh) sea el padre común de los informes. Puede comparar este archivo con el archivo HTMLcube.mqh de la versión antigua para asegurarse de que contiene pocas diferencias (aparte del cambio de nombre de las clases). Lo principal que salta a la vista es el relleno mínimo del método load que se ha convertido en un tapón virtual.
virtual bool load(const string file) { reset(); TradeRecord::reset(); return false; }
Los herederos tienen que redefinir este método.
También se ha cambiado el método generate que en realidad transforma las transacciones en posiciones. Ahora, al principio de este método, se invoca la «muñeca» virtual fillDealsArray.
virtual bool fillDealsArray() = 0; int generate() { ... if(!fillDealsArray()) return 0; ... }
Veamos cómo una parte del código existente para trabajar con informes HTML ha sido traspasada en nuevos métodos virtuales en la clase HTMLReportAdapter. ¡Atención! A continuación, se muestra la clase HTMLReportAdapter por completo. Dado que la parte principal del código se ha quedado en la clase base, aquí sólo hay que definir 2 métodos virtuales.
template<typename T> class HTMLReportAdapter: public BaseReportAdapter<T> { protected: IndexMap *data; virtual bool fillDealsArray() override { for(int i = 0; i < data.getSize(); ++i) { IndexMap *row = data[i]; if(CheckPointer(row) == POINTER_INVALID || row.getSize() != COLUMNS_COUNT) return false; // something is broken string s = row[COLUMN_SYMBOL].get<string>(); StringTrimLeft(s); if(StringLen(s) > 0) // there is a symbol -> this is a deal { array << new Deal(row); } else if(row[COLUMN_TYPE].get<string>() == "balance") { string t = row[COLUMN_PROFIT].get<string>(); StringReplace(t, " ", ""); balance += StringToDouble(t); } } return true; } public: ~HTMLReportAdapter() { if(CheckPointer(data) == POINTER_DYNAMIC) delete data; } virtual bool load(const string file) override { BaseReportAdapter<T>::load(file); if(CheckPointer(data) == POINTER_DYNAMIC) delete data; data = NULL; if(StringFind(file, ".htm") > 0) { data = HTMLConverter::convertReport2Map(file, true); if(data != NULL) { size = generate(); Print(data.getSize(), " deals transferred to ", size, " trades"); } } return data != NULL; } };
Gracias a la versión anterior, ya conocemos el código de ambos métodos, por tanto, no hay que cambiar nada.
Ahora veamos la implementación del nuevo adaptador TesterReportAdapter. En primer lugar, aquí fue necesario añadir la clase TesterDeal, derivándola de la clase Deal, que fue definida en ReportCubeBase.mqh (Deal es una clase antigua que se encontraba antes en HTMLcube.mqh). TesterDeal tiene un constructor con el parámetro TradeDeal, lo cual representa ni más ni menos que una transacción de la biblioteca SingleTesterCache. Además, en TesterDeal tenemos definidos dos métodos auxiliares para convertir las enumeraciones del tipo y de la dirección de las transacciones en cadenas.
class TesterDeal: public Deal { public: TesterDeal(const TradeDeal &td) { time = (datetime)td.time_create + TimeShift; price = td.price_open; string t = dealType(td.action); type = t == "buy" ? +1 : (t == "sell" ? -1 : 0); t = dealDir(td.entry); direction = 0; if(StringFind(t, "in") > -1) ++direction; if(StringFind(t, "out") > -1) --direction; volume = (double)td.volume; profit = td.profit; deal = (long)td.deal; order = (long)td.order; comment = td.comment[]; symbol = td.symbol[]; commission = td.commission; swap = td.storage; } static string dealType(const ENUM_DEAL_TYPE type) { return type == DEAL_TYPE_BUY ? "buy" : (type == DEAL_TYPE_SELL ? "sell" : "balance"); } static string dealDir(const ENUM_DEAL_ENTRY entry) { string result = ""; if(entry == DEAL_ENTRY_IN) result += "in"; else if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_OUT_BY) result += "out"; else if(entry == DEAL_ENTRY_INOUT) result += "in out"; return result; } };
Aparte de los métodos load y fillDealsArray, la clase TesterReportAdapter contiene un puntero al objeto SINGLETESTERCACHE —la clase principal de la biblioteca SingleTesterCache. Precisamente este objeto carga el archivo tst a nuestra instancia, y en caso del éxito, rellena el array Deals a base del cual funciona nuestro método fillDealsArray.
template<typename T> class TesterReportAdapter: public BaseReportAdapter<T> { protected: SINGLETESTERCACHE *ptrSingleTesterCache; virtual bool fillDealsArray() override { for(int i = 0; i < ArraySize(ptrSingleTesterCache.Deals); i++) { if(TesterDeal::dealType(ptrSingleTesterCache.Deals[i].action) == "balance") { balance += ptrSingleTesterCache.Deals[i].profit; } else { array << new TesterDeal(ptrSingleTesterCache.Deals[i]); } } return true; } public: ~TesterReportAdapter() { if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache; } virtual bool load(const string file) override { if(StringFind(file, ".tst") > 0) { // default cleanup BaseReportAdapter<T>::load(file); // specific cleanup if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache; ptrSingleTesterCache = new SINGLETESTERCACHE(); if(!ptrSingleTesterCache.Load(file)) { delete ptrSingleTesterCache; ptrSingleTesterCache = NULL; return false; } size = generate(); Print("Tester cache import: ", size, " trades from ", ArraySize(ptrSingleTesterCache.Deals), " deals"); } return true; } }; TesterReportAdapter<RECORD_CLASS> _defaultTSTReportAdapter;
Al final se crea una instancia del adaptador por defecto para el tipo de plantilla RECORD_CLASS. Recordemos que tenemos incluido en nuestro proyecto el archivo OLAPTradesCustom.mqh, en el que tenemos definida la clase personalizada de escritura CustomTradeRecord, y ahí mismo ella está marcada con la directiva del preprocesador con la macro RECORD_CLASS. De esta manera, en cuanto el nuevo adaptador esté incluido en el proyecto y el usuario especifique un archivo tst en los parámetros de entrada, el adaptador empezará a generar los objetos de la clase CustomTradeRecord y para ellos se crearán automáticamente nuestros campos personalizados MFE y MAE.
Veamos como el nuevo adaptador cumple con sus tareas. Aquí tenemos un ejemplo de las curvas del balance para los símbolos del archivo tst.
Curvas del balance para los símbolos
Nótese que las líneas se trazan sin interrupciones, es decir, nuestra implementación de CGraphicInPlot::LinesPlot funciona correctamente. Al trabajar con un agregador «progresivo» (un total acumulativo), el primer selector siempre tiene que ser el número de orden (o índice) de los registros.
Informes de la optimización del Simulador como un área aplicada del análisis OLAP
Aparte de los archivos con pasadas únicas del Simulador, MetaQuotes también ha lanzado el formato de los archivos opt con la caché de optimización. Para leerlos, ha sido creada la biblioteca TesterCache (es lógico que el autor es el mismo, fxsaber). Es fácil crear a su base una capa aplicada para el análisis OLAP de los resultados de la optimización. Para ello necesitaremos lo siguiente: una clase de la escritura con los campos que guardan los datos de cada pasada de la optimización, un adaptador y selectores (opcional). Tenemos las implementaciones de estos componentes para otras áreas aplicadas, lo cual permite usarlas como instrucciones (un proyecto). Luego, añadiremos la interfaz gráfica (formalmente, todo está listo y sólo hay que modificar las configuraciones).
Creamos el archivo OLAPOpts.mqh similar al archivo OLAPTrades.mqh según la designación. Incluimos el archivo de cabecera TesterCache.mqh en él.
#include <fxsaber/TesterCache/TesterCache.mqh>
Definimos la enumeración con todos los campos del optimizador. Los campos se cogen de la estructura ExpTradeSummary (se ubica en el archivo fxsaber/TesterCache/ExpTradeSummary.mqh, el archivo se conecta automáticamente a la biblioteca).
enum OPT_CACHE_RECORD_FIELDS { FIELD_NONE, FIELD_INDEX, FIELD_PASS, FIELD_DEPOSIT, FIELD_WITHDRAWAL, FIELD_PROFIT, FIELD_GROSS_PROFIT, FIELD_GROSS_LOSS, FIELD_MAX_TRADE_PROFIT, FIELD_MAX_TRADE_LOSS, FIELD_LONGEST_SERIAL_PROFIT, FIELD_MAX_SERIAL_PROFIT, FIELD_LONGEST_SERIAL_LOSS, FIELD_MAX_SERIAL_LOSS, FIELD_MIN_BALANCE, FIELD_MAX_DRAWDOWN, FIELD_MAX_DRAWDOWN_PCT, FIELD_REL_DRAWDOWN, FIELD_REL_DRAWDOWN_PCT, FIELD_MIN_EQUITY, FIELD_MAX_DRAWDOWN_EQ, FIELD_MAX_DRAWDOWN_PCT_EQ, FIELD_REL_DRAWDOWN_EQ, FIELD_REL_DRAWDOWN_PCT_EQ, FIELD_EXPECTED_PAYOFF, FIELD_PROFIT_FACTOR, FIELD_RECOVERY_FACTOR, FIELD_SHARPE_RATIO, FIELD_MARGIN_LEVEL, FIELD_CUSTOM_FITNESS, FIELD_DEALS, FIELD_TRADES, FIELD_PROFIT_TRADES, FIELD_LOSS_TRADES, FIELD_LONG_TRADES, FIELD_SHORT_TRADES, FIELD_WIN_LONG_TRADES, FIELD_WIN_SHORT_TRADES, FIELD_LONGEST_WIN_CHAIN, FIELD_MAX_PROFIT_CHAIN, FIELD_LONGEST_LOSS_CHAIN, FIELD_MAX_LOSS_CHAIN, FIELD_AVERAGE_SERIAL_WIN_TRADES, FIELD_AVERAGE_SERIAL_LOSS_TRADES }; #define OPT_CACHE_RECORD_FIELDS_LAST (FIELD_AVERAGE_SERIAL_LOSS_TRADES + 1)
Aquí tenemos todos los indicadores habituales, como beneficio, reducción del balance (drawdown) y la equidad, número de operaciones comerciales, coeficiente de Sharpe, etc. El único campo que vamos a añadir será FIELD_INDEX: número de orden de la entrada. Los campos en la estructura serán de varios tipos: long, double, int. Todo eso entrará en la clase de la escritura OptCacheRecord heredada de Record y va a almacenarse en su array tipo double.
Usaremos la estructura especial OptCacheRecordInternal para comunicarnos con la biblioteca:
struct OptCacheRecordInternal { ExpTradeSummary summary; MqlParam params[][5]; // [][name, current, low, step, high] };
Es que cada pasada del Simulador se caracteriza no sólo con las indicaciones de la eficacia, sino también está relacionada con un determinado conjunto de parámetros de entrada. En esta estructura, los parámetros de entrada se añaden tras ExpTradeSummary como el array MqlParam. Teniendo esta estructura, es bastante fácil escribir la clase OptCacheRecord que se rellena con datos en el formato del optimizador.
class OptCacheRecord: public Record { protected: static int counter; // number of passes void fillByTesterPass(const OptCacheRecordInternal &internal) { const ExpTradeSummary record = internal.summary; set(FIELD_INDEX, counter++); set(FIELD_PASS, record.Pass); set(FIELD_DEPOSIT, record.initial_deposit); set(FIELD_WITHDRAWAL, record.withdrawal); set(FIELD_PROFIT, record.profit); set(FIELD_GROSS_PROFIT, record.grossprofit); set(FIELD_GROSS_LOSS, record.grossloss); set(FIELD_MAX_TRADE_PROFIT, record.maxprofit); set(FIELD_MAX_TRADE_LOSS, record.minprofit); set(FIELD_LONGEST_SERIAL_PROFIT, record.conprofitmax); set(FIELD_MAX_SERIAL_PROFIT, record.maxconprofit); set(FIELD_LONGEST_SERIAL_LOSS, record.conlossmax); set(FIELD_MAX_SERIAL_LOSS, record.maxconloss); set(FIELD_MIN_BALANCE, record.balance_min); set(FIELD_MAX_DRAWDOWN, record.maxdrawdown); set(FIELD_MAX_DRAWDOWN_PCT, record.drawdownpercent); set(FIELD_REL_DRAWDOWN, record.reldrawdown); set(FIELD_REL_DRAWDOWN_PCT, record.reldrawdownpercent); set(FIELD_MIN_EQUITY, record.equity_min); set(FIELD_MAX_DRAWDOWN_EQ, record.maxdrawdown_e); set(FIELD_MAX_DRAWDOWN_PCT_EQ, record.drawdownpercent_e); set(FIELD_REL_DRAWDOWN_EQ, record.reldrawdown_e); set(FIELD_REL_DRAWDOWN_PCT_EQ, record.reldrawdownpercnt_e); set(FIELD_EXPECTED_PAYOFF, record.expected_payoff); set(FIELD_PROFIT_FACTOR, record.profit_factor); set(FIELD_RECOVERY_FACTOR, record.recovery_factor); set(FIELD_SHARPE_RATIO, record.sharpe_ratio); set(FIELD_MARGIN_LEVEL, record.margin_level); set(FIELD_CUSTOM_FITNESS, record.custom_fitness); set(FIELD_DEALS, record.deals); set(FIELD_TRADES, record.trades); set(FIELD_PROFIT_TRADES, record.profittrades); set(FIELD_LOSS_TRADES, record.losstrades); set(FIELD_LONG_TRADES, record.longtrades); set(FIELD_SHORT_TRADES, record.shorttrades); set(FIELD_WIN_LONG_TRADES, record.winlongtrades); set(FIELD_WIN_SHORT_TRADES, record.winshorttrades); set(FIELD_LONGEST_WIN_CHAIN, record.conprofitmax_trades); set(FIELD_MAX_PROFIT_CHAIN, record.maxconprofit_trades); set(FIELD_LONGEST_LOSS_CHAIN, record.conlossmax_trades); set(FIELD_MAX_LOSS_CHAIN, record.maxconloss_trades); set(FIELD_AVERAGE_SERIAL_WIN_TRADES, record.avgconwinners); set(FIELD_AVERAGE_SERIAL_LOSS_TRADES, record.avgconloosers); const int n = ArrayRange(internal.params, 0); for(int i = 0; i < n; i++) { set(OPT_CACHE_RECORD_FIELDS_LAST + i, internal.params[i][PARAM_VALUE].double_value); } } public: OptCacheRecord(const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields) { } OptCacheRecord(const OptCacheRecordInternal &record, const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields) { fillByTesterPass(record); } static int getRecordCount() { return counter; } static void reset() { counter = 0; } }; static int OptCacheRecord::counter = 0;
La correspondencia entre los elementos de la enumeración y los campos ExpTradeSummary se ve perfectamente en el método fillByTesterPass. El constructor recibe la estructura rellenada OptCacheRecordInternal como parámetro.
Usaremos un adaptador de datos especializado como intermediario entre la biblioteca TesterCache y OLAP. Se encargará de generar los registros de la clase OptCacheRecord.
template<typename T> class OptCacheDataAdapter: public DataAdapter { private: int size; int cursor; int paramCount; string paramNames[]; TESTERCACHE<ExpTradeSummary> Cache;
El campo size es la cantidad total de los registros, cursor es el número del registro actual durante el repaso consecutivo de la caché, paramCount es la cantidad de los parámetros optimizados, sus nombres se almacenan en el array paramNames. La variable Cache tipo TESTERCACHE<ExpTradeSummary> es un objeto de trabajo de la biblioteca TesterCache.
La inicialización primaria y la lectura de la caché de la optimización se realiza en los métodos reset, load y customize.
void customize() { size = (int)Cache.Header.passes_passed; paramCount = (int)Cache.Header.opt_params_total; const int n = ArraySize(Cache.Inputs); ArrayResize(paramNames, n); int k = 0; for(int i = 0; i < n; i++) { if(Cache.Inputs[i].flag) { paramNames[k++] = Cache.Inputs[i].name[]; } } if(k > 0) { ArrayResize(paramNames, k); Print("Optimized Parameters (", paramCount, " of ", n, "):"); ArrayPrint(paramNames); } } public: OptCacheDataAdapter() { reset(); } void load(const string optName) { if(Cache.Load(optName)) { customize(); reset(); } else { cursor = -1; } } virtual void reset() override { cursor = 0; if(Cache.Header.version == 0) return; T::reset(); } virtual int getFieldCount() const override { return OPT_CACHE_RECORD_FIELDS_LAST; }
La carga del archivo opt se realiza en el método load donde se invoca el método Cache.Load de la biblioteca. Y en caso del éxito, los parámetros del EA se seleccionan del encabezado del optimizador (en el método auxiliar customize). El método reset simplemente resetea el número actual del registro que va a incrementarse durante el siguiente recorrido de todos los registros desde el núcleo de OLAP a través del método getNext. Precisamente en este último, se rellena la estructura OptCacheRecordInternal con datos desde la caché de la optimización, después de lo cual a su base se crea un nuevo registro de la clase-parámetro de la plantilla (T).
virtual Record *getNext() override { if(cursor < size) { OptCacheRecordInternal internal; internal.summary = Cache[cursor]; Cache.GetInputs(cursor, internal.params); cursor++; return new T(internal, paramCount); } return NULL; } ... };
Está claro que la clase descrita OptCacheRecord es un parámetro de la plantilla.
#ifndef RECORD_CLASS #define RECORD_CLASS OptCacheRecord #endif OptCacheDataAdapter<RECORD_CLASS> _defaultOptCacheAdapter;
Se determina por la macro RECORD_CLASS que se utiliza en otras partes del núcleo OLAP. Abajo se muestra el diagrama de las clases de acuerdo con los adaptadores de datos anteriores que se soportan y los adaptadores nuevos.
Diagrama de las clases de adaptadores de datos
Es importante comprender qué tipos de los selectores pueden ser útiles para analizar los resultados de la optimización. La siguiente enumeración se propone como la primera opción mínima.
enum OPT_CACHE_SELECTORS { SELECTOR_NONE, // none SELECTOR_INDEX, // ordinal number /* all the next require a field as parameter */ SELECTOR_SCALAR, // scalar(field) SELECTOR_QUANTS, // quants(field) SELECTOR_FILTER // filter(field) };
En realidad, todos los campos del registro pertenecen a uno de los dos tipos: indicadores del trading (estadísticas) y parámetros del EA. Tiene sentido organizar los parámetros en células que correspondan exactamente a los valores verificados. Por ejemplo, si entre los parámetros hay un período de la media móvil y se usan 10 valores para él, tiene que haber 10 células en el cubo OLAP para este parámetro. De eso se encarga el selector de cuantización (SELECTOR_QUANTS) con el tamaño cero de la «cesta».
Para los campos que representan los indicadores, tiene sentido hacer la división por células con un cierto paso. Por ejemplo, se puede ver la distribución de las pasadas según el beneficio con el paso de 100 «unidades convencionales». Para eso, nos valdrá de nuevo el selector de cuantización. Pero en este caso, hay que establecer el tamaño de la «cesta» igual al paso necesario. Otros selectores añadidos desempeñan las funciones utilitarias. Así, el selector del número de orden (SELECTOR_INDEX) se usa para calcular el total cumulativo, mientras que el escalar (SELECTOR_SCALAR) permite obtener un número como la característica de toda la sección.
Las propias clases de los selectores ya están preparadas y se encuentran en el archivo OLAPCommon.mqh.
Vamos a escribir el método createSelector para los tipos de selectores seleccionados en la especialización de plantilla de la clase del motor OLAPEngine para la optimización:
class OLAPEngineOptCache: public OLAPEngine<OPT_CACHE_SELECTORS,OPT_CACHE_RECORD_FIELDS> { protected: virtual Selector<OPT_CACHE_RECORD_FIELDS> *createSelector(const OPT_CACHE_SELECTORS selector, const OPT_CACHE_RECORD_FIELDS field) override { const int standard = adapter.getFieldCount(); switch(selector) { case SELECTOR_INDEX: return new SerialNumberSelector<OPT_CACHE_RECORD_FIELDS,OptCacheRecord>(FIELD_INDEX); case SELECTOR_SCALAR: return new OptCacheSelector(field); case SELECTOR_QUANTS: return field != FIELD_NONE ? new QuantizationSelector<OPT_CACHE_RECORD_FIELDS>(field, (int)field < standard ? quantGranularity : 0) : NULL; } return NULL; } public: OLAPEngineOptCache(): OLAPEngine() {} OLAPEngineOptCache(DataAdapter *ptr): OLAPEngine(ptr) {} }; OLAPEngineOptCache _defaultEngine;
Al crear el selector de cuantización, dependiendo de que si el campo es «estándar» (es decir, guarda la estadística estándar de la pasada del Simulador) o es personalizado (parámetro del EA), establecemos el tamaño de la «cesta» igual a la variable quantGranularity o igual a cero. El campo quantGranularity está descrito en la clase base OLAPEngine, y podemos establecerlo tanto en el constructor del motor, como posteriormente a través del método especial setQuant.
OptCacheSelector es un envoltorio simple para BaseSelector<OPT_CACHE_RECORD_FIELDS>.
Interfaz gráfica para analizar los informes de la optimización del Simulador
Para visualizar los resultados del análisis de la optimización, vamos a aplicar la misma interfaz que en el caso con los informes comerciales. En realidad, podemos copiar el archivo OLAPGUI_Trade.mqh usando el nombre nuevo OLAPGUI_Opts.mqh e introducir pequeñas modificaciones. Obviamente, afectarán los métodos virtuales setup y process.
template<typename S, typename F> void OLAPDialog::setup() override { static const string _settings[ALGO_NUMBER][MAX_ALGO_CHOICES] = { // enum AGGREGATORS 1:1, default - sum {"sum", "average", "max", "min", "count", "profit factor", "progressive total", "identity", "variance"}, // enum RECORD_FIELDS 1:1, default - profit amount {""}, // enum SORT_BY, default - none {"none", "value ascending", "value descending", "label ascending", "label descending"}, // enum ENUM_CURVE_TYPE partially, default - points {"points", "lines", "points/lines", "steps", "histogram"} }; static const int _defaults[ALGO_NUMBER] = {0, FIELD_PROFIT, 0, 0}; const int std = EnumSize<F,PackedEnum>(0); const int fields = std + customFieldCount; ArrayResize(settings, fields); ArrayResize(selectors, fields); selectors[0] = "(<selector>/field)"; // none selectors[1] = "<serial number>"; // the only selector, which can be chosen explicitly, it correspods to the 'index' field for(int i = 0; i < ALGO_NUMBER; i++) { if(i == 1) // pure fields { for(int j = 0; j < fields; j++) { settings[j][i] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std]; } } else { for(int j = 0; j < MAX_ALGO_CHOICES; j++) { settings[j][i] = _settings[i][j]; } } } for(int j = 2; j < fields; j++) // 0-th is none { selectors[j] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std]; } ArrayCopy(defaults, _defaults); }
Es importante mencionar que casi no hay diferencia entre los campos y los selectores, ya que cualquier campo supone un selector de cuantización por este mismo campo. En otras palabras, el selector de cuantización se encarga de todo. Antes, en los proyectos de los informes y cotizaciones había selectores especiales para los campos separados (como selector del beneficio, selector del día de la semana, selector del tipo de la vela, etc.).
Los nombres de todos los elementos de las listas desplegables con los campos (se trata de los selectores por los ejes X, Y, Z) se formas a base de los nombres de los elementos de la enumeración OPT_CACHE_RECORD_FIELDS para una estadística estándar y a base del array customFields para los parámetros del EA. Antes, hemos analizado el método setCustomFields en la clase base OLAPDialogBase, que permite rellenar el array customFields con los nombres desde el adaptador. Podemos vincularlos en el código del EA analítico OLAPGUI_Opts.mq5 (véase a continuación).
Los campos estándar se muestran según el orden de los elementos de la enumeración, les siguen los campos relacionados con los parámetros del EA a optimizar (en el orden según el cual están escritos en el archivo opt).
La lectura del estado de los controles y el inicio del proceso del análisis se realiza en el método process.
template<typename S, typename F> int OLAPDialog::process() override { SELECTORS Selectors[4]; ENUM_FIELDS Fields[4]; AGGREGATORS at = (AGGREGATORS)m_algo[0].Value(); ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value(); SORT_BY sb = (SORT_BY)m_algo[2].Value(); if(at == AGGREGATOR_IDENTITY) { Print("Sorting is disabled for Identity"); sb = SORT_BY_NONE; } ArrayInitialize(Selectors, SELECTOR_NONE); ArrayInitialize(Fields, FIELD_NONE); int matches[2] = { SELECTOR_NONE, SELECTOR_INDEX }; for(int i = 0; i < AXES_NUMBER; i++) { if(!m_axis[i].IsVisible()) continue; int v = (int)m_axis[i].Value(); if(v < 2) // selectors (which is specialized for a field already) { Selectors[i] = (SELECTORS)matches[v]; } else // pure fields { Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS; Fields[i] = (ENUM_FIELDS)(v); } } m_plot.CurvesRemoveAll(); if(at == AGGREGATOR_IDENTITY) af = FIELD_NONE; m_plot.InitXAxis(at != AGGREGATOR_PROGRESSIVE ? new AxisCustomizer(m_plot.getGraphic(), false) : NULL); m_plot.InitYAxis(at == AGGREGATOR_IDENTITY ? new AxisCustomizer(m_plot.getGraphic(), true) : NULL); m_button_ok.Text("Processing..."); return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb); }
Análisis OLAP y visualización de los informes de optimización
El Simulador reglamentario de MetaTrader permite analizar los resultados de la optimización de maneras distintas, pero aun así están limitadas con un conjunto estándar. El motor creado de OLAP permite completar esta herramienta. Por ejemplo, la visualización built-in en el modo 2D siempre muestra el valor máximo del beneficio para la combinación de dos parámetros del EA, normalmente hay más parámetros. Eso significa que en cada punto de la superficie vemos los resultados para diferentes combinaciones de otros parámetros que no han entrado en los ejes. Como resultado, es posible que se forme una estimación demasiado optimista de la rentabilidad para determinados valores de parámetros visualizados. Para una estimación más precisa, tendría sentido evaluar un valor medio del beneficio y su dispersión. Podemos hacer todo eso e incluso más a través de OLAP.
La ejecución del análisis OLAP de los informes de la optimización será encargada al EA nuevo no negociable OLAPGUI_Opts.mq5. Su estructura repite por completo OLAPGUI.mq5. Incluso es más sencillo porque no hay que conectar diferentes adaptadores en función del tipo del archivo especificado. Para los resultados de la optimización siempre será un archivo opt.
En los parámetros de entrada, especificamos el nombre del archivo para el análisis y el tamaño del paso de cuantización para los parámetros estadísticos.
input string OptFileName = "Integrity.opt"; input uint QuantGranularity = 0;
Cabe mencionar que es deseable tener su propio paso de cuantización para cada campo, pero por el momento se establece sólo uno y no se modifica desde la interfaz gráfica. Es una de las tareas para el siguiente desarrollo del proyecto. Por ahora hay que recordar que el tamaño del paso puede valer para un campo y no convenir para otro (puede ser demasiado grande o pequeño). Por tanto, si hace falta, es necesario llamar al diálogo de propiedades del EA para cambiar un cuanto antes de seleccionar un campo en la lista desplegable de la interfaz OLAP.
Después de incluir los archivos de cabecera con todas las clases, creamos una instancia del diálogo y la vinculamos con el motor OLAP.
#include <OLAP/OLAPOpts.mqh> #include <OLAP/GUI/OLAPGUI_Opts.mqh> OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine);
En el manejador OnInit, incluimos el nuevo adaptador en el motor e iniciamos la carga de los datos desde el archivo.
int OnInit() { _defaultEngine.setAdapter(&_defaultOptCacheAdapter); _defaultEngine.setShortTitles(true); _defaultEngine.setQuant(QuantGranularity); _defaultOptCacheAdapter.load(OptFileName); dialog.setCustomFields(_defaultOptCacheAdapter); if(!dialog.Create(0, "OLAPGUI" + (OptFileName != "" ? " : " + OptFileName : ""), 0, 0, 0, 750, 560)) return INIT_FAILED; if(!dialog.Run()) return INIT_FAILED; return INIT_SUCCEEDED; }
Intentaremos construir algunas secciones analíticas para el archivo Integrity.opt y QuantGranularity = 100. Durante la optimización, han sido seleccionados tres parámetros PricePeriod, Momentum, Sigma.
Es el beneficio medio en división por los valores del parámetro PricePeriod.
Beneficio medio dependiendo del valor del parámetro del EA
Está claro que eso nos dice poco sin la dispersión.
Dispersión del beneficio dependiendo del valor del parámetro del EA
Al comparar estos dos histogramas, se puede estimar con qué valores del parámetro la dispersión no supera la media, significando así el punto muerto (break even). Es deseable hacerlo automáticamente en un gráfico, pero esta tarea excede el marco del presente artículo.
Como una opción, podemos hacer una «jugada con el caballo» y ver la rentabilidad usando el mismo parámetro (relación entre ganancias y pérdidas para todas las pasadas).
Rentabilidad de la estrategia (factor de beneficio) dependiendo del valor del parámetro del EA
Otro enfoque más capcioso ofrece un tamaño medio del período en división por los niveles con el paso de 100 (que hemos establecido en el parámetro de entrada QuantGranularity).
Valor medio del parámetro para obtener la ganancia en rangos diferentes (con el paso de 100)
Es la distribución de ganancias dependiendo del período, por decirlo así «a vista de pájaro» (se muestran todas las pasadas gracias al agregador identity).
Beneficio vs valor del parámetro para todas las posiciones
La división del beneficio por dos parámetros Momentum y Sigma es la siguiente.
Beneficio medio según dos parámetros
Para ver la distribución general de ganancias por los niveles con el paso de 100, seleccionamos el campo profit de la estadística por el eje X y el agregador count.
Distribución de ganancias de todas las pasadas por los rangos con el paso 100
Finalmente, el agregador identity nos da la posibilidad de estimar la influencia de la cantidad de transacciones en el beneficio. En realidad, este agregador permite ver visualmente muchas otras regularidades.
Beneficio vs número de trades
Conclusión
En este artículo, hemos ampliado el área de la aplicación de MQL OLAP para los informes del Simulador sobre las pasadas únicas y optimización. La estructura de las clases actualizada permite en el futuro ampliar el abanico de los medios de OLAP. Desde luego, la implementación propuesta no es completa y puede ser mejorada (en particular, en cuanto a la visualización 3D, inclusión de los ajustes de filtración y cuantización por ejes diferentes en GUI interactiva), pero al mismo tiempo nos sirve de un conjunto inicial mínimo a base del cual es mucho más fácil entrar en el mundo de OLAP. Usándolo, el trader es capaz de procesar grandes volúmenes de datos crudos y extraer nuevos conocimientos para tomar decisiones.
Archivos adjuntos:
Experts
- OLAPRPRT.mq5 — Asesor Experto para analizar el historial de trading de la cuenta, informes en HTML y CSV (actualizado desde el artículo N3, sin GUI);
- OLAPQTS.mq5 — Asesor Experto para analizar cotizaciones (actualizado desde el artículo N3, sin GUI);
- OLAPGUI.mq5 — Asesor Experto para analizar el historial de trading de la cuenta, informes en HTML y CSV, así como los archivos estándar TST del Simulador (actualizado desde el artículo N2, GUI);
- OLAPGUI_Opts.mq5 — Asesor Experto para analizar los resultados de la optimización desde los archivos OPT del Simulador (nuevo, GUI);
Include
Núcleo
- OLAP/OLAPCommon.mqh — archivo de cabecera principal con las clases OLAP;
- OLAP/OLAPTrades.mqh — clases estándar para el análisis OLAP del historial de trading;
- OLAP/OLAPTradesCustom.mqh — clases personalizadas para el análisis OLAP del historial de trading;
- OLAP/OLAPQuotes.mqh — clases para el análisis OLAP de las cotizaciones;
- OLAP/OLAPOpts.mqh — clases para el análisis OLAP de los resultados de la optimización de los EAs;
- OLAP/ReportCubeBase.mqh — clases básicas para el análisis OLAP del historial de trading;
- OLAP/HTMLcube.mqh — clases para el análisis OLAP del historial de trading en el formato HTML;
- OLAP/CSVcube.mqh — clases para el análisis OLAP del historial de trading en el formato CSV;
- OLAP/TSTcube.mqh — clases para el análisis OLAP del historial de trading en el formato TST;
- OLAP/PairArray.mqh — clase del array de pares [valor;nombre] con soporte de todas las opciones de ordenación;
- OLAP/GroupReportInputs.mqh — grupo de parámetros de entrada para analizar informes comerciales;
- MT4Bridge/MT4Orders.mqh — biblioteca MT4orders para trabajar con las órdenes usando el mismo estilo en МТ4 y en МТ5;
- MT4Bridge/MT4Time.mqh — archivo de cabecera auxiliar con implementación de las funciones de trabajo con fechas en estilo MT4;
- Marketeer/IndexMap.mqh — archivo de cabecera auxiliar con implementación del array con acceso combinado por la clave e índice;
- Marketeer/Converter.mqh — archivo de cabecera auxiliar con unión para convertir los tipos de datos;
- Marketeer/GroupSettings.mqh — archivo de cabecera auxiliar para configurar el grupo de parámetros de entrada;
- Marketeer/WebDataExtractor.mqh — analizador sintáctico HTML;
- Marketeer/empty_strings.h — lista de etiquetas HTML vacías;
- Marketeer/HTMLcolumns.mqh — definición de los índices de columnas en los informes HTML;
- Marketeer/RubbArray.mqh — archivo de cabecera auxiliar con el array de «goma»;
- Marketeer/CSVReader.mqh — analizador sintáctico CSV;
- Marketeer/CSVcolumns.mqh — definición de los índices de columnas en los informes CSV;
Interfaz gráfica
- OLAP/GUI/OLAPGUI.mqh — implementación general de la interfaz gráfica de ventana;
- OLAP/GUI/OLAPGUI_Trades.mqh — especializaciones de la interfaz gráfica para analizar informes comerciales;
- OLAP/GUI/OLAPGUI_Opts.mqh — especializaciones de la interfaz gráfica para analizar los resultados de la optimización;
- Layouts/Box.mqh — contenedor de controles;
- Layouts/ComboBoxResizable.mqh — control de la lista desplegable con posibilidad del redimencionamiento dinámico;
- Layouts/MaximizableAppDialog.mqh — ventana de diálogo con posibilidad del redimencionamiento dinámico;
- PairPlot/Plot.mqh — control con gráfica comercial que soporta redimencionamiento dinámico;
- Layouts/res/expand2.bmp — botón para maximizar la ventana;
- Layouts/res/size6.bmp — botón para cambiar el tamaño;
- Layouts/res/size10.bmp — botón para cambiar el tamaño;
TypeToBytes
- TypeToBytes.mqh
SingleTesterCache
- fxsaber/SingleTesterCache/SingleTesterCache.mqh
- fxsaber/SingleTesterCache/SingleTestCacheHeader.mqh
- fxsaber/SingleTesterCache/String.mqh
- fxsaber/SingleTesterCache/ExpTradeSummaryExt.mqh
- fxsaber/SingleTesterCache/ExpTradeSummarySingle.mqh
- fxsaber/SingleTesterCache/TradeDeal.mqh
- fxsaber/SingleTesterCache/TradeOrder.mqh
- fxsaber/SingleTesterCache/TesterPositionProfit.mqh
- fxsaber/SingleTesterCache/TesterTradeState.mqh
TesterCache
- fxsaber/TesterCache/TesterCache.mqh
- fxsaber/TesterCache/TestCacheHeader.mqh
- fxsaber/TesterCache/String.mqh
- fxsaber/TesterCache/ExpTradeSummary.mqh
- fxsaber/TesterCache/TestCacheInput.mqh
- fxsaber/TesterCache/TestInputRange.mqh
- fxsaber/TesterCache/Mathematics.mqh
- fxsaber/TesterCache/TestCacheRecord.mqh
- fxsaber/TesterCache/TestCacheSymbolRecord.mqh
Patch de la biblioteca estándar
- Controls/Dialog.mqh
- Controls/ComboBox.mqh
Files
- 518562.history.csv
- Integrity.tst
- Integrity.opt
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/7656
- 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