Cómo visualizar la historia del comercio multidivisa en informes con formato HTML y CSV

17 julio 2019, 08:33
Stanislav Korotky
0
212

Como sabemos, MetaTrader 5 ofrece la posibilidad de realizar simulaciones multidivisa desde su aparición. Esta función tiene mucha demanda entre la mayoría de los tráders, pero, por desgracia, no es tan universal como querríamos. En concreto, después de realizar una simulación, el usuario puede abrir un gráfico con las operaciones comerciales realizadas, pero se tratará solo del gráfico del símbolo de trabajo elegido en los ajustes del simulador. No es posible ver la historia comercial de todos los símbolos utilizados después de la prueba, y no siempre resulta cómodo realizar una simulación visual. Además, podríamos necesitar un análisis adicional cierto tiempo después de la simulación, o puede que recibamos el informe de otra persona. Por eso, resulta deseable tener una herramienta para la visualización del comercio en multitud de símbolos de trabajo usando como base el informe HTML del simulador.

Esta tarea también se relaciona estrechamente con otra variante de uso de MetaTrader. Resulta que muchas señales comerciales disponibles en mql5.com también son multidivisa, y los archivos CSV con su historia resultarían cómodos de mostrar en los gráficos de forma análoga.

Vamos a escribir un indicador que ejecuta las funciones anteriormente mencionadas.

Puesto que hay varios símbolos de trabajo, implementaremos el análisis paralelo para ellos en varios ejemplares (uno para cada símbolo) en las subventanas del gráfico. La principal construcción gráfica del indicador serán las "cotizaciones" del símbolo seleccionado (normalmente, distinto del símbolo del gráfico), sincronizadas con las barras de la ventana principal. Sobre estas "cotizaciones" colocaremos las líneas de tendencia correspondientes a las órdenes (posiciones) comerciales.

Asimismo, existe un enfoque alternativo con la representación de las transacciones en la ventana principal, pero posibilita el análisis solo de un símbolo en el gráfico. Para ello, necesitaremos implementar otro indicador sin búferes, con la posibilidad de alternar cómodamente con cualquier símbolo entre los mencionados en el informe.

En el anterior artículo, describimos un parser HTML basado en selectores CSS[1]. Con su ayuda, obtendremos la lista de transacciones del informe HTML, y después formaremos sobre su base las transacciones (objetos gráficos). En el caso de los archivos CSV del apartado de señales, todo resultará más sencillo: su formato para las señales МТ4 (*.history.csv) y МТ5 (*.positions.csv) es soportado por la funciones MQL incorporadas.

Indicador SubChart

El primer paso en la implementación de nuestra idea es la creación de un indicador sencillo que represente las "cotizaciones" de símbolo de terceros en la subventana de cualquier gráfico. Lo llamaremos SubChart.

Para representar los datos con los valores OHLC (Open, High, Low, Close), el lenguaje MQL ofrece varios estilos de representación, concretamente, DRAW_CANDLES y DRAW_BARS. Resulta obvio que utilizan cuatro búferes de indicador. En lugar de elegir uno de los estilos, vamos a dar soporte a ambas variantes: para ello haremos dinámica nuestra selección usando como base los ajustes actuales de la ventana. Como ya sabemos, los ajustes del gráfico contienen en la pestaña "Generales" el grupo de botones de opción "Barras", "Velas japonesas" y "Línea", en este caso, además, para ofrecer acceso rápido, están duplicados por los botones correspondientes en la paleta de herramientas. Podemos obtener estos ajustes de MQL con la ayuda de la llamada:

(ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE)

La enumeración ENUM_CHART_MODE contiene elementos con tareas semejantes: CHART_CANDLES, CHART_BARS, CHART_LINE.

El último punto, CHART_LINE, también tiene sentido mantenerlo en nuestro indicador, para que el indicador cambie su aspecto externo de forma sincrónica con la ventana principal como respuesta a cualquier alternancia en el modo de la interfaz de usuario. Para el último modo, nos ayudará el estilo DRAW_LINE, que usa un único búfer.

Bien, vamos a comenzar a codificar. Declaramos el número de búferes de indicador y construcciones gráficas representadas:

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   1

Añadimos las variables de entrada:

input string SubSymbol = ""; // Symbol
input bool Exact = false;

Con la ayuda de SubSymbol, el usuario podrá elegir para el indicador cualquier símbolo diferentes al símbolo actual de laventana principal.

El parámetro Exact determina qué hacer en los casos en los que las barras en la ventana principal no tienen barras de otro símbolo cuyo tiempo coincida exactamente con el de ellas. Como podemos adivinar por el nombre, este parámetro se usará en la llamada de la función iBarShift y, de acuerdo con su lógica de trabajo, provocará el siguiente efecto visual:

  • cuando Exact sea igual a false, la función retornará el número de la barra apropiada más próxima para el tiempo especificado, y por ello, en ausencia de cotizaciones (la "ventana" en el horario de las transacciones, días festivos, etcétera), el indicador representará la barra anterior;
  • cuando Exact sea igual true, la función retornará -1, y en el gráfico del indicador, este espacio se encontrará vacío;

Por defecto, el parámetro SubSymbol es igual a una línea vacía, lo que supone la duplicación de las cotizaciones de la ventana principal. En este caso, es necesario editar los valores factuales de la variable a _Symbol, pero, como input es en MQL una variable solo de lectura, deberemos introducir la variable intermedia Symbol y rellenarla en el manejador OnInit.

string Symbol;

int OnInit()
{
  Symbol = SubSymbol;
  if(Symbol == "") Symbol = _Symbol;
  else SymbolSelect(Symbol, true);
  ...

Preste atención a que vamos a necesitar añadir el símbolo de terceros a la Observación del Mercado, ya que podría no estar en su lista.

Para controlar el modo de representación actual, usaremos la variable mode:

ENUM_CHART_MODE mode = 0;

Los cuatro búferes de indicador recibirán los nombres esperados:

// OHLC
double open[];
double high[];
double low[];
double close[];

Para iniciar los búferes de la forma acostumbrada (estableciendo la propiedad "seriality"), seleccionaremos una pequeña función:

void InitBuffer(int index, double &buffer[], ENUM_INDEXBUFFER_TYPE style)
{
  SetIndexBuffer(index, buffer, style);
  ArraySetAsSeries(buffer, true);
}

La inicialización de la construcción gráfica también se reduce a una función auxiliar (aquí tendremos una construcción, pero cuando sean varias, esta función nos ahorrará espacio):

void InitPlot(int index, string name, int style, int width = -1, int colorx = -1)
{
  PlotIndexSetInteger(index, PLOT_DRAW_TYPE, style);
  PlotIndexSetDouble(index, PLOT_EMPTY_VALUE, 0);
  PlotIndexSetString(index, PLOT_LABEL, name);
  if(width != -1) PlotIndexSetInteger(index, PLOT_LINE_WIDTH, width);
  if(colorx != -1) PlotIndexSetInteger(index, PLOT_LINE_COLOR, colorx);
}

Para pasar el modo de representación al estilo del búfer, vamos a escribir la función:

int Mode2Style(/*global ENUM_CHART_MODE mode*/)
{
  switch(mode)
  {
    case CHART_CANDLES: return DRAW_CANDLES;
    case CHART_BARS: return DRAW_BARS;
    case CHART_LINE: return DRAW_LINE;
  }
  return 0;
}

Esta utiliza la variable mode mencionada más arriba, que debemos rellenar con el valor correcto ya en OnInit, junto con las llamadas de todas las funciones auxiliares.

  InitBuffer(0, open, INDICATOR_DATA);
  string title = "# Open;# High;# Low;# Close";
  StringReplace(title, "#", Symbol);
  mode = (ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE);
  InitPlot(0, title, Mode2Style());

  InitBuffer(1, high, INDICATOR_DATA);
  InitBuffer(2, low, INDICATOR_DATA);
  InitBuffer(3, close, INDICATOR_DATA);

Pero, para que el indicador funcione correctamente, esto es aún insuficiente. Dependiendo del modo actual (variable mode), será necesario cambiar el color de las líneas: estas también se toman de los ajustes del gráfico.

void SetPlotColors()
{
  if(mode == CHART_CANDLES)
  {
    PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 3);
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 0, (int)ChartGetInteger(0, CHART_COLOR_CHART_LINE));  // rectangle
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 1, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BULL)); // up
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 2, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BEAR)); // down
  }
  else
  {
    PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 1);
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, (int)ChartGetInteger(0, CHART_COLOR_CHART_LINE));
  }
}

Añadiendo la llamada de SetPlotColors() a OnInit, además de indicando la precisión de los valores, garantizamos la representación correcta del indicador después del inicio.

  SetPlotColors();

  IndicatorSetString(INDICATOR_SHORTNAME, "SubChart (" + Symbol + ")");
  IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(Symbol, SYMBOL_DIGITS));
  
  return INIT_SUCCEEDED;
}

Sin embargo, si el usuario cambia el modo del gráfico mientras el indicador está en funcionamiento, deberá monitorear este evento y modificar las propiedades de los búferes. Para ello, crearemos el manejador OnChartEvent.

void OnChartEvent(const int id,
                  const long& lparam,
                  const double& dparam,
                  const string& sparam)
{
  if(id == CHARTEVENT_CHART_CHANGE)
  {
    mode = (ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE);
    PlotIndexSetInteger(0, PLOT_DRAW_TYPE, Mode2Style());
    SetPlotColors();
    ChartRedraw();
  }
}

Solo nos queda escribir la función más importante del indicador: el manejador OnCalculate. En nuestro caso, su principal capacidad consiste en que el indicador en realidad trabaja con las cotizaciones del símbolo de terceros, y no del símbolo del gáfico. Por eso, todos los métodos estándar de codificación basados en los valores rates_total y prev_calculated transmitidos desde el núcleo no funcionan, o no son adecuados. La carga de cotizaciones de un símbolo de terceros tiene lugar de forma asincrónica, y por eso, en cualquier momento puede llegar una nueva partida de barras que requiera de un recálculo completo. Por este motivo, vamos a crear variables que controlen el número de barras en el símbolo de terceros (lastAvailable) y el "clon" editado del argumento constante prev_calculated.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& op[],
                const double& hi[],
                const double& lo[],
                const double& cl[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
{
  static int lastAvailable = 0;
  static bool initialized = false;

  int _prev_calculated = prev_calculated;

  if(iBars(Symbol, _Period) - lastAvailable > 1) // bar gap filled
  {
    _prev_calculated = 0;
    lastAvailable = 0;
    initialized = false;
  }

  if(_prev_calculated == 0)
  {
    for(int i = 0; i < rates_total; ++i)
    {
      open[i] = 0;
      high[i] = 0;
      low[i] = 0;
      close[i] = 0;
    }
  }

Si el símbolo del indicador se diferencia del símbolo de la ventana, usaremos la función iBarShift para encontrar las barras sincrónicas y copiar sus valores OHLC.

  if(_Symbol != Symbol)
  {
    for(int i = 0; i < MathMax(rates_total - _prev_calculated, 1); ++i)
    {
      datetime dt = iTime(_Symbol, _Period, i);
      int x = iBarShift(Symbol, _Period, dt, Exact);
      if(x != -1)
      {
        open[i] = iOpen(Symbol, _Period, x);
        high[i] = iHigh(Symbol, _Period, x);
        low[i] = iLow(Symbol, _Period, x);
        close[i] = iClose(Symbol, _Period, x);
      }
    }
  }

Si el símbolo del indicador coincide con el símbolo de la ventana, todo será más sencillo, pues usaremos las matrices-argumentos que se nos han transmitido:

  else
  {
    ArraySetAsSeries(op, true);
    ArraySetAsSeries(hi, true);
    ArraySetAsSeries(lo, true);
    ArraySetAsSeries(cl, true);
    for(int i = 0; i < MathMax(rates_total - _prev_calculated, 1); ++i)
    {
      open[i] = op[i];
      high[i] = hi[i];
      low[i] = lo[i];
      close[i] = cl[i];
    }
  }

Finalmente, vamos a posibilitar la carga de los datos utilizando la implementación de la función RefreshHistory del popular ejemplo de la compañía MetaQuotes (incluiremos este código como archivo de encabezado Refresh.mqh).

La variable estadística initialized contiene el signo de finalización de la actualización. Lo estableceremos en true, si RefreshHistory retorna el signo de éxito o si el número de barras del símbolo de terceros permanece constante y distinto a cero (en el caso de que no exista historia del número de barras necesario del símbolo de terceros).

  if(lastAvailable == iBars(Symbol, _Period) && lastAvailable != 0)
  {
    if(!initialized)
    {
      Print("Updated ", Symbol, " ", iBars(Symbol, _Period), " bars");
      initialized = true;
    }
    return rates_total;
  }

  if(!initialized)
  {
    if(_Symbol != Symbol)
    {
      Print("Updating ", Symbol, " ", lastAvailable, " -> ", iBars(Symbol, _Period), " bars up to ", (string)time[0], "... Please wait");
      int result = RefreshHistory(Symbol, time[0]);
      if(result >= 0 && result <= 2)
      {
        _prev_calculated = rates_total;
      }
      if(result >= 0)
      {
        initialized = true;
        ChartSetSymbolPeriod(0, _Symbol, _Period);
      }
    }
    else
    {
      initialized = true;
    }
  }
  
  lastAvailable = iBars(Symbol, _Period);
  
  return _Symbol != Symbol ? _prev_calculated : rates_total;
}

Si el proceso de carga de datos se prolonga demasiado, podríamos necesitar actualizar el gráfico manualmente.

Después de realizar la inicialización una vez, las nuevas barras se calcularán en el modo económico, es decir, teniendo en cuenta _prev_calculated y rates_total. Si en el futuro el número de barras lastAvailable cambia más que en 1, realizaremos un redibujado completo, redefiniendo initialized en false.

Colocamos el indicador en un gráfico, por ejemplo, EURUSD, e introducimos otro símbolo en los parámetros, por ejemplo, UKBrent (es un CFD de Brent, nos interesa porque en los marcos temporales intradía, al establecer Exact en true, se nota la ausencia de barras nocturnas). Pulsamos los botones de cambio de modo y nos aseguramos de que el indicador dibuje correctamente.

Alternando el modo de representación del indicador

Alternando el modo de representación del indicador

Hay que prestar atención a que, en el modo de representación de una línea, el indicador usa el primer búfer (índice 0), es decir, open. Esto lo diferencia del gráfico principal, que se muestra según los precios close. Se ha hecho de esta forma, para no redibujar el indicador por completo al cambiar al estilo de línea y desde el mismo, y es que, para mostrar los precios de cierre, es necesario copiarlos en el primer (y único) búfer, mientras que para los modos de velas y barras (cuando hay 4 búferes), en el primero se guardan los precios de apertura. La implementación actual aprovecha que el búfer open es el primero entre los cuatro de OHLC, y por eso, el cambio de estilo provoca de inmediato el cambio de la presentación externa sin recálculo. Al analizar la historia, el modo de las líneas difícilmente será necesario, por eso, este recurso no es de importancia crítica.

Iniciamos el indicador en el simulador visual.

Indicador SubChart en el simulador visual.

Indicador SubChart en el simulador visual.

Ahora, usando como base el indicador, podemos proceder a visualizar la historia comercial.

Indicador SubChartReporter

Llamaremos al nuevo indicador SubChartReporter Vamos a complementar el código ya creado con la lectura de informes en los formatos HTML y CSV. El nombre del archivo analizado lo estableceremos en la variable de entrada ReportFile. Asimsimo, se han previsto parámetros de entrada para indicar el desplazamiento temporal, así como los prefijos y sufijos de los símbolos para los casos en los que el informe se ha obtenido de otro usuario (de otro entorno comercial).

input string ReportFile = ""; // · ReportFile
input string Prefix = ""; // · Prefix
input string Suffix = ""; // · Suffix
input int  TimeShift = 0; // · TimeShift

En el indicador SubChartReporter, habrá clases especiales que se encargarán del procesamiento de los datos obtenidos y la generación de objetos gráficos (líneas de tendencia).

Dado que se supone que vamos a analizar no solo archivos HTML, sino también CSV, se ha proyectado una clase básica para todos los tipos de informes, Processor, y ya de esta se heredan las implementaciones concretas para los formatos HTML y CSV: ReportProcessor y HistoryProcessor, respectivamente.

En la clase Processor se describen las siguientes variables:

class Processor
{
  protected:
    string symbol;
    string realsymbol;
    IndexMap *data;
    ulong timestamp;
    string prefix;
  • symbol — nombre del símbolo de trabajo tomado del informe;
  • realsymbol — nombre del símbolo de trabajo verdaderamente disponible: puede distinguirse al trabajar con informes de terceros, debido a los prefijos y sufijos;
  • data — matriz de operaciones comerciales (la clase IndexMap ya la conocemos del artículo [1] y se incluye desde el archivo de encabezado homónimo);
  • timestamp y prefix — variables auxiliares para la denominación única de los objetos gráficos que el indicador generará en el gráfico;

Asimismo, para alternar entre los diferentes símbolos del informe, hemos contemplado botones de interfaz; el identificador del botón pulsado (correspondiente el símbolo elegido) lo guardaremos en la variable:

    string pressed;

En la clase Processor se han implementado los siguientes métodos auxiliares de generación de objetos:

    void createTrend(const long dealIn, const long dealOut, const int type, const datetime time1, const double price1, const datetime time2, const double price2, const string &description)
    void createButton(const int x, const int y, const int dx, const int dy, const string text, const bool selected)
    void controlPanel(const IndexMap &symbols)

Como podemos entener por el nombre, createTrend crea una representación visual para una transacción individual (una línea de tendencia y dos flechas), createButton es el botón para el símbolo de trabajo, y controlPanel supone un paquete de botones completo para todos los símbolos del informe. Los botones se muestran en la esquina inferior izquierda de la subventana.

La interfaz pública de la clase Processor incluye dos grupos de métodos:

  • los virtuales, que están sujetos a una implementación física en las clases herederas;
  • los no virtuales, que ofrecen una funcionalidad estándar única;

    virtual IndexMap *load(const string file) = 0;
    virtual int getColumnCount() = 0;
    virtual int getSymbolColumn() = 0;
    virtual datetime getStart() = 0;
    virtual bool applyInit() { return true; }
    virtual void makeTrade(IndexMap *row) = 0;
    virtual int render() = 0;

    bool attach(const string file)
    bool apply(const string _s = NULL)
    bool isEmpty() const
    string findrealsymbol()
    string _symbol() const
    string _realsymbol() const
    void onChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)

podrá familiarizarse con el código fuente en los archivos adjuntos, aquí solo encontrará una breve descripción.

  • load — carga el archivo indicado con datos;
  • getColumnCount — retorna el número de columnas en el recuadro de datos;
  • getSymbolColumn — retorna el número de la columna con el nombre de los símbolos de las operaciones comerciales;
  • getStart — retorna la fecha de la primera operación comercial;
  • applyInit — método para la inicialización opcional de las estructuras internas antes de comenzar el procesamiento;
  • makeTrade — método para registrar una entrada del informe en las estructuras de datos internas;
  • render — método para generar los objetos gráficos basados en las entradas en las estructuras de datos internas;

Los dos últimos métodos existen por separado, debido a que algunos formatos de informe, en concreto los informes HTML de MT5, contienen entradas sobre transacciones, mientras que nosotros necesitamos representar posiciones. Por consiguiente, será necesario una transformación adicional de una entidad en otra, con la necesidad de analizar la matriz de transacciones.

Los métodos no virtuales se presentan a continuación de forma simplificada. El método attach usa el nombre del archivo analizado, lo carga con la ayuda del métod virtual load, forma una lista de símbolos únicos y crea para ellos un "panel" de botones.

    bool attach(const string file)
    {
      data = load(file);
      
      IndexMap symbols;
      
      for(int i = 0; i < data.getSize(); ++i)
      {
        IndexMap *row = data[i];
        // collect all unique symbols
        string s = row[getSymbolColumn()].get<string>();
        StringTrimLeft(s);
        if(StringLen(s) > 0) symbols.set(s);
      }

      if(symbols.getSize() > 0)
      {
        controlPanel(symbols);
      }
      return true;
    }

El método apply activa en el indicador el símbolo de trabajo elegido entre aquellos que se encuentran en el informe. Primero, los objetos antiguos son eliminados del gráfico (si existen), después se busca un símbolo real apropiado (por ejemplo, si su bróker tiene EURUSD en lugar de el EURUSD.m mencionado en el informe), dando a sus clases la posibilidad de redefinir matrices antiguas utilizando applyInit. Luego, para las entradas en el recuadro de datos en las que el nombre del símbolo se corresponda con el seleccionado, se generan transacciones (llamada de makeTrade), se crean objetos gráficos (llamada de render) basados ​​en estas transacciones y se actualiza la interfaz (selección del botón activo, sustitución del nombre de pantalla, llamada de ChartRedraw).

                                                                                                                                          
    bool apply(const string _s = NULL)
    {
      ObjectsDeleteAll(0, "SCR", ChartWindowFind(), OBJ_TREND);

      if(_s != NULL && _s != "") symbol = _s;
      if(symbol == NULL)
      {
        Print("No symbol selected");
        return false;
      }
      
      string real = findrealsymbol();
      if(real == NULL)
      {
        Print("No suitable symbol found");
        return false;
      }
      
      SymbolSelect(real, true);

      if(!applyInit()) return false;
      
      int selected = 0;
  
      for(int i = 0; i < data.getSize(); ++i)
      {
        IndexMap *row = data[i];
        
        string s = row[getSymbolColumn()].get<string>();
        StringTrimLeft(s);
        
        if(s == symbol)
        {
          selected++;
          makeTrade(row);
        }
      }
      
      pressed = prefix + "#" + symbol;
      ObjectSetInteger(0, pressed, OBJPROP_BGCOLOR, clrGreen);
      
      int trends = render();
      Print(data.getSize(), " records in total");
      Print(selected, " trades for ", symbol);
      
      string title = CHART_REPORTER_TITLE + " (" + symbol + ", " + (string)selected + " records, " + (string)trends + " trades)";
      IndicatorSetString(INDICATOR_SHORTNAME, title);
      
      ChartRedraw();
      return true;
    }

El método findrealsymbol usa varios enfoques para buscar los símbolos adecuados. Se encarga de comprobar la información de mercado (precios bid) para el símbolo. Si la hay, se considerará que el símbolo es real. Si no la hay, el programa intentará aplicar los parámetros Suffix y/o Prefix (si han sido establecidos, se entiende), es decir, los eliminará o los añadirá al nombre del símbolo. Si la modificación logra que se obtenga el precio, esto significará, probablemente, que hemos encontrado un alias para trabar con el símbolo.

Los métodos _symbol y _realsymbol retornan el nombre del símbolo actual desde el informe, así como a él o a su "doble" de trabajo para su cuenta. Al analizar sus propias cuentas, usted obtendrá el mismo nombre del instrumento, si el bróker no lo excluye transcurrido un tiempo.

El método onChartEvent ha sido pensado para procesar los eventos OnChartEvent, para ser más exactos, las pulsaciones a los botones. El botón destacado anteriormente (si lo hay) se vuelve gris, como todos los demás y, lo más importante, se llama el metodo virtual apply, al que se transmite el nombre del nuevo símbolo destacado del identificador del botón.

    void onChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
    {
      if(id == CHARTEVENT_OBJECT_CLICK)
      {
        int x = StringFind(sparam, "_#");
        if(x != -1)
        {
          string s = StringSubstr(sparam, x + 2);
          Print(s, " ", sparam, " ", pressed);
          
          ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
          ObjectSetInteger(0, pressed, OBJPROP_STATE, false);
          ObjectSetInteger(0, pressed, OBJPROP_BGCOLOR, clrGray);
          pressed = "";
          
          if(apply(s)) // will set pressed and other properties
          {
            ChartSetSymbolPeriod(0, _Symbol, _Period);
          }
        }
      }
    }

Ha llegado el momento de pasar a la implementación de la funcionalidad principal en los métodos virtuales de las clases herederas. Comenzaremos con ReportProcessor.

Para el parseo de las páginas HTML, vamos a usar WebDataExtractor del artículo[1], dándole la forma de archivo de inclusión WebDataExtractor.mqh. En comparación con los códigos fuente originales, en este archivo todos los métodos de trabajo están envueltos en la clase HTMLConverter, para no ensuciar el contexto global. En primer lugar, nos interesa el método HTMLConverter::convertReport2Map, que coincide casi totalmente con la función process analizada en [1]. a la entrada de convertReport2Map se suministra el nombre del informe (desde ReportFile), y en la salida, obtenemos el mapa IndexMap con las líneas correspondienets a las operaciones comerciales en el recuadro del informe.

Todos los ajustes necesarios para el parseo de los informes HTML, tales como RowSelector, ColumnSettingsFile, ya se han indicado en el archivo de encabezado, pero pueden ser editados, puesto que han sido descritos como parámetros de entrada. Por defecto, en los parámetros se indican los ajustes para los informes MT5; en este caso, además, de estos se extrae el recuadro de transacciones (deal), y ya sobre su base, como veremos más tarde, se calculan las posiciones representadas. Cada transacción se describe con un ejemplar de la clase especial Deal. El constructor Deal toma una entrada del recuadro del informe, compactada en IndexMap. Dentro de Deal, claro está, guarda el campo con la hora y el precio de la transacción, su tipo y dirección, su volumen y las demás propiedades.

class ReportProcessor: public Processor
{
  private:
    class Deal   // if MQL5 could respect private access specifier for classes,
    {            // Trades will be unreachable from outer world, so it would be fine to have
      public:    // fields made public for direct access from Processor only
        datetime time;
        double price;
        int type;      // +1 - buy, -1 - sell
        int direction; // +1 - in, -1 - out, 0 - in/out
        double volume;
        double profit;
        long deal;
        long order;
        string comment;
        
      public:
        Deal(const IndexMap *row) // this is MT5 deal
        {
          time = StringToTime(row[COLUMN_TIME].get<string>()) + TimeShift;
          price = StringToDouble(row[COLUMN_PRICE].get<string>());
          string t = row[COLUMN_TYPE].get<string>();
          type = t == "buy" ? +1 : (t == "sell" ? -1 : 0);
          t = row[COLUMN_DIRECTION].get<string>();
          direction = 0;
          if(StringFind(t, "in") > -1) ++direction;
          if(StringFind(t, "out") > -1) --direction;
          volume = StringToDouble(row[COLUMN_VOLUME].get<string>());
          t = row[COLUMN_PROFIT].get<string>();
          StringReplace(t, " ", "");
          profit = StringToDouble(t);
          deal = StringToInteger(row[COLUMN_DEAL].get<string>());
          order = StringToInteger(row[COLUMN_ORDER].get<string>());
          comment = row[COLUMN_COMMENT].get<string>();
        }
    
        bool isIn() const
        {
          return direction >= 0;
        }
        
        bool isOut() const
        {
          return direction <= 0;
        }
        
        bool isOpposite(const Deal *t) const
        {
          return type * t.type < 0;
        }
        
        bool isActive() const
        {
          return volume > 0;
        }
    };

Todas las transacciones llegan a la matriz array. Durante el análisis posterior de la historia, ubicaremos las transacciones de entrada en el mercado en la cola (queue) y las quitaremos de allí a medida que encontremos una transacción opuesta de salida. Si la cola no está vacía después de pasar la historia completa, significará que hay una posición abierta.

    RubbArray<Deal *> array;
    RubbArray<Deal *> queue;

La clase RubbArray es un envoltorio para la matriz dinámica, que se expande automáticamente conforme van llegándole datos.

Aquí tenemos la implementación de algunos métodos virtuales:

    virtual IndexMap *load(const string file) override
    {
      return HTMLConverter::convertReport2Map(file, true);
    }

    virtual int getColumnCount() override
    {
      return COLUMNS_COUNT;
    }

    virtual int getSymbolColumn() override
    {
      return COLUMN_SYMBOL;
    }

Los dos últimos métodos usan macros para el recuadro de transacciones estándar del informe HTML de MT5.

#define COLUMNS_COUNT 13
#define COLUMN_TIME 0
#define COLUMN_DEAL 1
#define COLUMN_SYMBOL 2
...

El método applyInit limpia las matrices array y queue.

    virtual bool applyInit() override
    {
      ((BaseArray<Deal *> *)&queue).clear();
      array.clear();
      return true;
    }

Los objetos de las transacciones son creados y colocados en la matriz array, en el método makeTrade.

    virtual void makeTrade(IndexMap *row) override
    {
      array << new Deal(row);
    }

Y finalmente, lo más interesante, a la vez que complicado: el análisis de la lista de transacciones y la generación de objetos de transacción sobre su base.

    virtual int render() override
    {
      int count = 0;
      
      for(int i = 0; i < array.size(); ++i)
      {
        Deal *current = array[i];
        
        if(!current.isActive()) continue;
        
        if(current.isOut())
        {
          // first try to find exact match
          for(int j = 0; j < queue.size(); ++j)
          {
            if(queue[j].isIn() && queue[j].isOpposite(current) && queue[j].volume == current.volume)
            {
              string description;
              StringConcatenate(description, (float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
              createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);
              current.volume = 0;
              queue >> j; // remove from queue
              ++count;
              break;
            }
          }

          if(!current.isActive()) continue;
          
          // second try to perform partial close
          for(int j = 0; j < queue.size(); ++j)
          {
            if(queue[j].isIn() && queue[j].isOpposite(current))
            {
              string description;
              if(current.volume >= queue[j].volume)
              {
                StringConcatenate(description, (float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
                createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);

                current.volume -= queue[j].volume;
                queue[j].volume = 0;
                ++count;
              }
              else
              {
                StringConcatenate(description, (float)current.volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
                createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);

                queue[j].volume -= current.volume;
                current.volume = 0;
                ++count;
                break;
              }
            }
          }
          
          // purge all inactive from queue
          for(int j = queue.size() - 1; j >= 0; --j)
          {
            if(!queue[j].isActive())
            {
              queue >> j;
            }
          }
        }
        
        if(current.isActive()) // is _still_ active
        {
          if(current.isIn())
          {
            queue << current;
          }
        }
      }
      
      if(!isQueueEmpty())
      {
        Print("Warning: not all deals are processed (probably, open positions left).");
      }
      
      return count;
    }

El algoritmo se deslaza por la lista de todas las transacciones y coloca en la cola una transacción de entrada en el mercado. Al detectarse una transacción de salida en la cola, primero se busca una transacción opuesta con el tamaño adecuado. Si no hay ninguna adecuada, se seleccionarán de forma consecutiva - según el principio FIFO - los volúmenes de las transacciones de entrada que cubran el volumen de salida. Las transacciones que hayan perdido el volumen hasta alcanzar cero, son eliminadas de la cola. En cada combinación de volumen de entrada y salida se crea su propia línea de tendencia (createTrend).

El modo FIFO se ha seleccionado como el más fundamentado desde un punto de vista algorítmico, pero esto no lo convierte en el único válido. Un robot concreto puede cerrar transacciones, no solo según FIFO, sino también según LIFO, e incluso en orden aleatorio. Lo mismo se puede decir del comercio manual. Por eso, para estabecer la correspondencia entre una transacción de apertura y una de cierre, en el modo de cobertura es necesario encontrar cierto enfoque alternativo, por ejemplo, el análisis de beneficio o los comentarios. Podrá encontrar un ejemplo de cálculo del beneficio entre dos puntos de precio en el blog del autor, sin embargo, no existe registro alguno de cambio en las cotizaciones. En general, esta tarea no es tan trivial como parece a primera vista, si bien queda fuera del marco del presente artículo. 

De esta forma, hemos analizado en líneas generales cómo tiene lugar el procesamiento del informe HTML en las clases descritas. Para un archivo CSV, la implementación de la clase HistoryProcessor es notablemente más sencilla. En caso necesario, es posible estudiarla fácilmente con los códigos fuente adjuntos. Tendrá que prestar atención, eso sí, a que los archivos CSV con la historia de señales mql5.com tienen un número diferente de columnas para MT4 y MT5; el formato se elige automáticamente, partiendo de la extensión doble: ".history.csv" para MT4 y ".positions.csv" para MT5. El único ajuste para los archivos CSV es el símbolo separador (en los archivos de las señales mql5.com, por defecto - ';').

Recordemos que SubChartReporter hereda gran parte del código fuente de SubChart, en el que comprobamos el enfoque con la representación de cotizaciones de terceros en subventanas, por lo que aquí tiene sentido prestar atención solo a los nuevos fragmentos.

Los objetos de las clases se crean y se usan en los manejadores de eventos. Concretamente, el procesador se crea en OnInit y se destruye en OnDeinit:

Processor *processor = NULL;

int OnInit()
{
  if(StringFind(ReportFile, ".htm") > 0)
  {
    processor = new ReportProcessor();
  }
  else if(StringFind(ReportFile, ".csv") > 0)
  {
    processor = new HistoryProcessor();
  }
  string Symbol = SubSymbol;
  if(Symbol == "") Symbol = _Symbol;
  else SymbolSelect(Symbol, true);
  processor.apply(Symbol);
  ...
}

void OnDeinit(const int reason)
{
  if(processor != NULL) delete processor;
}

El procesamiento de los eventos de los objetos gráficos se añade al manejador OnChartEvent:

void OnChartEvent(const int id,
                  const long& lparam,
                  const double& dparam,
                  const string& sparam)
{
  if(id == CHARTEVENT_CHART_CHANGE)
  {
    ... // same code
  }
  else
  {
    processor.onChartEvent(id, lparam, dparam, sparam);
  }
}

En el manejador OnCalculate, será necesario monitorear la situación en la que, como respuesta a las acciones del usuario, cambia el símbolo actualmente analizado, en relación con lo cual se inicia el recálculo completo:

  string Symbol = processor._realsymbol();
  if(Symbol == NULL) Symbol = _Symbol;
  if(lastSymbol != Symbol)
  {
    _prev_calculated = 0;
    lastAvailable = 0;
    initialized = false;
    IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(Symbol, SYMBOL_DIGITS));
  }

Cuando el dibujado inicial ha sido finalizado y el número de barras disponibles en el símbolo de trabajo se ha estabilizado, iniciamos el temporizador para la carga de los datos del informe.

  if(lastAvailable == iBars(Symbol, _Period) && lastAvailable != 0)
  {
    if(!initialized)
    {
      Print("Updated ", Symbol, " ", iBars(Symbol, _Period), " bars");
      initialized = true;
      if(ReportFile != "") //
      {                    //
        EventSetTimer(1);  //
      }                    //
    }
    
    return rates_total;
  }

Cargamos el informe (si todavía no ha sido cargado) en el manejador del temporizador y activamos el símbolo elegido (se establece desde los parámetros en OnInit o pulsando un botón en OnChartEvent).

void OnTimer()
{
  EventKillTimer();
  
  if(processor.isEmpty()) // load file only once
  {
    if(processor.attach(ReportFile))
    {
      processor.apply(/*keep already selected symbol*/);
    }
    else
    {
      Print("File loading failed: ", ReportFile);
    }
  }
}

Vamos a probar el indicador en acción. Para ello, abrimos el gráfico EURUSD, colocamos el indicador sobre él e indicamos en el parámetro ReportFile el archivo ReportTester-example.html (está adjunto). Después de realizar la inicialización, veremos en la subventana el gráfico EURUSD (porque el parámetro Symbol ha sido definido como vacío) y una serie de botones con los nombres de todos los símbolos mencionados en el informe.

Indicador SubChartReporter sin símbolo de trabajo seleccionado

Indicador SubChartReporter sin símbolo de trabajo seleccionado

Puesto que en el informe no hay EURUSD, todos los botones están grises. La pulsación de cualquier botón, por ejemplo, EURGBP, provocará la carga de esta divisa en la subventana de cotizaciones, y también representará las transacciones sobre ella. El botón mismo se pondrá en verde.

Indicador SubChartReporter con símbolo de trabajo seleccionado

Indicador SubChartReporter con símbolo de trabajo seleccionado

En la implementación actual, el orden de los botones se determina teniendo en cuenta la cronología de la frecuencia de aparición de los símbolos en el informe. En caso necesario, se puede clasificar en cualquier orden requerido, por ejemplo, por orden alfabético o según el orden de las transacciones.

Alternando con la ayuda de los botones, podemos ver por turno todos los símbolos del informe, pero esto no resulta muy cómodo. Para algunos informes, sería deseable ver todos los símbolos al mismo tiempo, cada uno en su subventana. Para lograrlo, vamos a escribir el script SubChartsBuilder, que creará subventanas y cargará en ellas ejemplares del indicador SubChartReporter para diferentes símbolos.

Script SubChartsBuilder

El script posee un conjunto de parámetros análogo al del indicador SubChartReporter iniciado. Para posibilitar un inicio único, los parámetros se colocan en la matriz MqlParam, a continuación, se llama IndicatorCreate de MQL API. Todo ello se combina en una función createIndicator aplicada.

bool createIndicator(const string symbol)
{
  MqlParam params[18] =
  {
    {TYPE_STRING, 0, 0.0, "::Indicators\\SubChartReporter.ex5"},
    
    {TYPE_INT, 0, 0.0, NULL}, // chart settings
    {TYPE_STRING, 0, 0.0, "XYZ"},
    {TYPE_BOOL, 1, 0.0, NULL},
    
    {TYPE_INT, 0, 0.0, NULL}, // common settings
    {TYPE_STRING, 0, 0.0, "HTMLCSV"},
    {TYPE_STRING, 0, 0.0, "PREFIX"},
    {TYPE_STRING, 0, 0.0, "SUFFIX"},
    {TYPE_INT, 0, 0.0, NULL}, // time shift

    {TYPE_INT, 0, 0.0, NULL}, // html settings
    {TYPE_STRING, 0, 0.0, "ROW"},
    {TYPE_STRING, 0, 0.0, "COLUMNS"},
    {TYPE_STRING, 0, 0.0, "SUBST"},
    {TYPE_BOOL, 0, 0.0, NULL},
    {TYPE_BOOL, 0, 0.0, NULL},
    {TYPE_BOOL, 0, 0.0, NULL},

    {TYPE_INT, 0, 0.0, NULL}, // csv settings
    {TYPE_STRING, 0, 0.0, ";"}
  };
  
  params[2].string_value = symbol;
  params[5].string_value = ReportFile;
  params[6].string_value = Prefix;
  params[7].string_value = Suffix;
  params[8].integer_value = TimeShift;
  params[10].string_value = RowSelector;
  params[11].string_value = ColumnSettingsFile;
  params[12].string_value = SubstitutionSettingsFile;
  params[17].string_value = CSVDelimiter;
  
  int handle = IndicatorCreate(_Symbol, _Period, IND_CUSTOM, 18, params);
  if(handle == INVALID_HANDLE)
  {
    Print("Can't create SubChartReporter for ", symbol, ": ", GetLastError());
    return false;
  }
  else
  {
    if(!ChartIndicatorAdd(0, (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL), handle))
    {
      Print("Can't attach SubChartReporter for ", symbol, ": ", GetLastError());
      return false;
    }
  }
  return true;
}

Preste atención a que el indicador se toma del recurso para el cual ha sido registrado anteriormente en el código fuente, en las instrucciones:

#resource "\\Indicators\\SubChartReporter.ex5"

De esta forma, el script constituye un programa autosuficiente, y no depende de la presencia de un indicador para un usuario concreto.

Además, este enfoque se usa con otro objetivo. La cosa es que, al generar los ejemplares de los indicadores para todos los símbolos del informe, ya no es necesario tener botones de control. Sin embargo, MQL, por desgracia, no ofrece la posibilidad de determinar si un indicador ha sido iniciado por el usuario o por IndicatorCreate, es decir, si funciona por sí mismo o es un componente (dependiente) de un programa mayor. Una vez colocado el indicador en el recurso, obtenemos la posibilidad de cambiar la visibilidad de los botones, dependiendo de la ruta del indicador: la ruta al recurso (es decir, la presencia de la línea "::Indicators\\") indica la necesidad de añadir la muestra de los botones.

Para llamar la función createIndicator para cada símbolo del informe, debemos analizar el informe en el script.

int OnStart()
{
  IndexMap *data = NULL;
  int columnsCount = 0, symbolColumn = 0;
  
  if(ReportFile == "")
  {
    Print("cleanUpChart");
    return cleanUpChart();
  }
  else if(StringFind(ReportFile, ".htm") > 0)
  {
    data = HTMLConverter::convertReport2Map(ReportFile, true);
    columnsCount = COLUMNS_COUNT;
    symbolColumn = COLUMN_SYMBOL;
  }
  else if(StringFind(ReportFile, ".csv") > 0)
  {
    data = CSVConverter::ReadCSV(ReportFile);
    if(data != NULL && data.getSize() > 0)
    {
      IndexMap *row = data[0];
      columnsCount = row.getSize();
      symbolColumn = CSV_COLUMN_SYMBOL;
    }
  }
  
  if(data != NULL)
  {
    IndexMap symbols;
    
    for(int i = 0; i < data.getSize(); ++i)
    {
      IndexMap *row = data[i];
      if(CheckPointer(row) == POINTER_INVALID || row.getSize() != columnsCount) break;
      
      string s = row[symbolColumn].get<string>();
      StringTrimLeft(s);
      if(StringLen(s) > 0) symbols.set(s);
    }
    
    for(int i = 0; i < symbols.getSize(); ++i)
    {
      createIndicator(symbols.getKey(i));
    }
    delete data;
  }

  return 0;
}

Preste atención: si iniciamos el script con el nombre del informe vacío, eliminará de la ventana todas las subventanas con ejemplares del indicador (si han sido creadas anteriormente). De ello se encarga la función cleanUpChart.

bool cleanUpChart()
{
  bool result = true;
  int n = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL);
  for(int i = n - 1; i > 0; --i)
  {
    string name = ChartIndicatorName(0, i, 0);
    if(StringFind(name, "SubChartReporter") == 0)
    {
      Print("Deleting ", name);
      result &= ChartIndicatorDelete(0, i, name);
    }
  }
  return result;
}

Esto puede resultar cómodo para limpiar el gráfico al finalizar el análisis del informe.

Para poner el script a prueba, hemos descargado varios archivos CSV desde la historia de las señales. Este es el aspecto que puede tener (el gráfico principal ha sido minimizado):

Varios ejemplares de SubChartReporter al realizar el análisis del comercio multidivisa

Varios ejemplares de SubChartReporter al realizar el análisis del comercio multidivisa

Recordemos que los objetos generados tienen descripciones con los detalles del informe (número de transacciones, volumen, beneficio, comentarios), y que para representarlos, podemos activar la opción "Mostrar las descripciones de los objetos" en los ajustes del gráfico.

Si hay un número elevado de símbolos, las subventanas reducirán su tamaño. Aunque esto nos permite hacernos una imagen general, dificulta el estudio de los detalles. Cuando debemos analizar cada transacción, tiene sentido usar cuanto más espacio, mejor, activando la propia ventana principal. Para ello, podemos crear una nueva versión del indicador SubChartReporter que, en lugar de usar una subventana, representará las transacciones en el gráfico principal. La llamaremos MainChartReporter.

Indicador MainChartReporter

Dado que este indicador se muestra en el gráfico de cotizaciones, ya no es necesario calcular los búferes o dibujar cualquier cosa que no sean los objetos de tendencia. En otras palabras, se trata de un indicador sin búferes que, para cambiar el símbolo analizado, cambia el símbolo de trabajo actual. Desde el punto de vista de la implementación, todo está ya listo: el nuevo código fuente representa un SubChartReporter sustancialmente simplificado. Sus peculiaridades son las siguientes.

El código fuente de las tres clases principales, es decir, Processor, ReportProcessor, HistoryProcessor, se saca al archivo de encabezado y se conecta a ambos indicadores. Las diferencias específicas para cada una de las versiones se encuadrarán con instrucciones de preprocesador para la compilación condicional. Para el código del indicador SubChartReporter, vamos a definir la macro CHART_REPORTER_SUB, y para el código del indicadorMainChartReporter, CHART_REPORTER_MAIN.

En el casod e MainChartReporter, necesitaremos añadir solo 2 líneas. En el método apply, será necesario alternar el gráfico al nuevo símbolo real:

#ifdef CHART_REPORTER_MAIN
      ChartSetSymbolPeriod(0, real, _Period);
#endif

así como mostrar los comentarios con el texto que se estableció como su nombre en la primera versión del indicador (INDICATOR_SHORTNAME).

#ifdef CHART_REPORTER_MAIN
      Comment(title);
#endif

En el método de procesamiento del evento onChartEvent, hemos actualizado el gráfico actual, ya que la primera versión del indicador, normalmente, dibuja los datos según un símbolo distinto al de la ventana principal. En la nueva versión, las cotizaciones del símbolo principal se usan "como son", y no es necesario actualizar la ventana; por eso, la línea correspondiente va en la compilación condicional solo del indicador SubChartReporter.

#ifdef CHART_REPORTER_SUB
            ChartSetSymbolPeriod(0, _Symbol, _Period);
#endif

Eliminamos los búferes directamente del código fuente del indicador MainChartReporter (concretamente, registramos su cantidad como 0, para evitar las advertencias del compilador).

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

Asimismo, eliminamos por innecesarios una serie de ajustes de la subventana:

input GroupSettings Chart_Settings; // S U B C H A R T    S E T T I N G S
input string SubSymbol = ""; // · Symbol
input bool Exact = true; // · Exact

La función OnCalculate queda vacía (pero deberá estar presente en el indicador). El temporizador para la obtención de datos del informe se inicia en OnInit.

void OnTimer()
{
  EventKillTimer();
  
  if(processor.isEmpty()) // load file only once
  {
    if(processor.attach(ReportFile))
    {
      processor.apply();
      datetime start = processor.getStart();
      if(start != 0)
      {
        ChartSetInteger(ChartID(), CHART_AUTOSCROLL, false);
        // FIXME: this does not work as expected
        ChartNavigate(ChartID(), CHART_END, -1 * (iBarShift(_Symbol, _Period, start)));
      }
    }
  }
}

Aquí hemos intentado desplazar el gráfico hacia la primera transacción con la ayuda de la llamada a ChartNavigate. Por desgracia, no hemos conseguido que dicho fragmento funcione, simplemente, el desplazamiento no tiene lugar. Una posible solución sería determinar la posición actual y navegar con respecto a la misma usando CHART_CURRENT_POS, pero no parece la solución óptima.

Este es el aspecto del indicador MainChartReporter en el gráfico.

Indicador MainChartReporter

Indicador MainChartReporter

Archivos adjuntos

  • SubChart.mq5 — indicador SubChart;
  • SubChartReporter.mq5 — indicador SubChartReporter;
  • MainChartReporter.mq5 — indicador MainChartReporter;
  • SubChartsBuilder.mq5 — script para crear el grupo de ejemplares del indicador SubChartReporter de todos los símbolos del informe;
  • ChartReporterCore.mqh — clases generales principales para los indicadores;
  • WebDataExtractor.mqh — parser HTML;
  • CSVReader.mqh — parser CSV;
  • HTMLcolumns.mqh — definir las columnas de los informes HTML;
  • CSVcolumns.mqh — definir las columnas de los archivos CSV;
  • IndexMap.mqh — clase auxiliar del mapa;
  • RubbArray.mqh — clase auxiliar de la matriz de goma;
  • StringUtils.mqh — funciones auxiliares para trabajar con líneas;
  • empty_strings.h — lista de tags vacíos para el parser HTML;
  • GroupSettings.mqh — componer grupos de parámetros de entrada;
  • Refresh.mqh — solicitar cotizaciones de los símbolos;
  • ReportTester-example.html — ejemplo de informe HTML del simulador;
  • ReportHistoryDeals.cfg.csv — ajustes de los selectores CSS para destacar las columnas del recuadro con el parser HTML.

Conclusión

Hemos analizado varios indicadores que permiten visualizar la cotizaciones y las transacciones comerciales de varios símbolos. Como fuente de los datos de entrada actúan varios informes en formato HTML; en este caso, además, gracias al uso de un parser universal es posible activar no solo informes estándar (cuyos ajustes ya están activados en el código fuente), sino también otros distintos. Asimismo, se ofrece soporte a archivos de formato CSV, en los que se proporciona la historia comercial de las señales de mql5.com. El código fuente abierto permite adaptar el programa a las propias necesidades.

Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/5913

Archivos adjuntos |
report2chart.zip (34.55 KB)
Aplicando OLAP en el trading (parte 1): Fundamentos del análisis corriente de datos multidimensionales Aplicando OLAP en el trading (parte 1): Fundamentos del análisis corriente de datos multidimensionales

En este artículo, se describen los principios básicos de la construcción del framework para el procesamiento analítico en línea (OLAP en inglés), su implementación en MQL en el ambiente de MetaTrader, usando el procesamiento del historial de trading de la cuenta como ejemplo.

Web scraping de datos sobre la rentabilidad de los bonos Web scraping de datos sobre la rentabilidad de los bonos

Cuando diseñamos los sistemas del trading automático, casi siempre utilizamos los datos de los indicadores técnicos que analizan el pasado con el fin de predecir el futuro comportamiento del precio. Pero si no tomamos en cuenta las fuerzas fundamentales que mueven el mercado, evidentemente estaremos en una situación menos ventajosa en comparación con los traders que consideran adicionalmente los datos fundamentales en sus decisiones comerciales. Recopilando automáticamente los datos sobre los tipos de interés, Usted podrá mejorar el funcionamiento de su Asesor Experto.

Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte V): Clases y colección de eventos comerciales, envío de eventos al programa Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte V): Clases y colección de eventos comerciales, envío de eventos al programa

En anteriores artículos comenzamos a crear una gran biblioteca multiplataforma cuyo objetivo es simplificar la escritura de programas para las plataformas MetaTrader 5 y MetaTrader 4. En la cuarta parte, hemos puesto a prueba el seguimiento de eventos comerciales en la cuenta. En esta parte, vamos a crear las clases de los eventos comerciales y a colocarlas en la colección de eventos desde la que serán enviadas al objeto básico de la biblioteca Engine y al gráfico del programa de control.

Implementando OLAP en la negociación (Parte 2): Visualización de los resultados del análisis interactivo de los datos multidimensionales Implementando OLAP en la negociación (Parte 2): Visualización de los resultados del análisis interactivo de los datos multidimensionales

En este artículo, se consideran diversos aspectos del desarrollo de la interfaz gráfica interactiva de un programa MQL diseñado para el procesamiento analítico en línea (OLAP) del historial de la cuenta y de los informes comerciales. Para obtener un resultado visual, se usan las ventanas maximizadas y de escala, una disposición adaptable de los controles «de goma» y un nuevo control para mostrar diagramas. A base de eso, fue implementado GUI con una selección de indicadores a lo largo de los ejes de coordenadas, funciones agregadas, tipos de los gráficos y ordenaciones.