English Русский 中文 Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Aplicación Práctica de Bases de Datos para Análisis de Mercados

Aplicación Práctica de Bases de Datos para Análisis de Mercados

MetaTrader 5Integración | 18 febrero 2014, 14:55
1 498 0
Alexander
Alexander

Introducción

Trabajar con datos se ha convertido en la principal tarea para el software moderno, tanto para aplicaciones independientes como para aplicaciones de red. Para resolver este problema se creó un software especializado. Se trata de los Sistemas de Gestión de Bases de Datos (Database Management Systems o DBMS), que pueden estructurar datos para su almacenamiento en el ordenador y su procesamiento. Este software es la base de las actividades de información en todos los sectores, desde la producción de bienes hasta las finanzas y telecomunicaciones. 

En lo que se refiere a trading, la mayoría de analistas no usan bases de datos en su trabajo. Pero hay tareas donde esta solución resultaría muy práctica. 

Este artículo trata una de estas tareas: el indicador de ticks, que guarda y carga datos de bases de datos

Algoritmo BuySellVolume 

BuySellVolume - este es el nombre que le he dado al indicador con un sencillo algoritmo: toma el tiempo (t) y el precio (p) de dos ticks secuenciales (tick1 y tick2). Calculemos las diferencias entre ellos:

Δt = t2 - t1     (segundos)
Δp = p2 - p1    (puntos)

El volumen de valor se calcula usando esta fórmula:

v2 = Δp / Δt

De modo que nuestro volumen es directamente proporcional al número de puntos por el que el precio se ha movido, e inversamente proporcional al tiempo invertido en ello. Si Δt = 0, entonces en lugar de ello se toma el valor 0,5. Por tanto, obtenemos un tipo de valor de actividad de compradores y vendedores en el mercado. 

1. Implementación del indicador sin usar base de datos

Creo que sería lógico primero considerar un indicador con una funcionalidad específica, pero sin interacción con una base de datos. En mi opinión, la mejor solución es crear una clase base que hará los cálculos pertinentes, y sus derivados para que realicen la interacción con la base de datos. Para implementar esto, necesitaremos la biblioteca AdoSuite. De modo que haga click en el enlace y descárguela.

Primero, cree el archivo BsvEngine.mqh y conecte las clases de datos de AdoSuite:

#include <Ado\Data.mqh>

A continuación cree una clase de indicador base que implementará todas las funciones necesarias, excepto el trabajo con la base de datos. Tendrá el siguiente aspecto:

Listado 1.1

//+------------------------------------------------------------------+
// BuySellVolume indicator class (without storing to database)       |
//+------------------------------------------------------------------+
class CBsvEngine
  {
private:
   MqlTick           TickBuffer[];     // ticks buffer
   double            VolumeBuffer[];   // volume buffer
   int               TicksInBuffer;    // number of ticks in the buffer

   bool              DbAvailable;      // indicates, whether it's possible to work with the database

   long              FindIndexByTime(const datetime &time[],datetime barTime,long left,long right);

protected:

   virtual string DbConnectionString() { return NULL; }
   virtual bool DbCheckAvailable() { return false; }
   virtual CAdoTable *DbLoadData(const datetime startTime,const datetime endTime) { return NULL; }
   virtual void DbSaveData(CAdoTable *table) { return; }

public:
                     CBsvEngine();

   void              Init();
   void              ProcessTick(double &buyBuffer[],double &sellBuffer[]);
   void              LoadData(const datetime startTime,const datetime &time[],double &buyBuffer[],double &sellBuffer[]);
   void              SaveData();
  };

Quiero señalar que para aumentar la productividad de la solución, los datos se ponen en buffers especiales (TickBuffer y VolumeBuffer), y después de un determinado período de tiempo se cargan a la base de datos. 

Consideremos el orden de la implementación de la clase. Empecemos con el constructor:

Listado 1.2

//+------------------------------------------------------------------+
// Constructor                                                       |
//+------------------------------------------------------------------+
CBsvEngine::CBsvEngine(void)
  {
// Initially, can be placed up to 500 ticks in a buffer
   ArrayResize(TickBuffer,500);
   ArrayResize(VolumeBuffer,500);
   TicksInBuffer=0;
   DbAvailable=false;
  } 

Aquí creo que todo debería estar claro: las variables se inicializan y se configuran los tamaños iniciales de los buffers.

Lo siguiente será la implementación de método Init():

 Listado 1.3

//+-------------------------------------------------------------------+
// Function, called in the OnInit event                               |
//+-------------------------------------------------------------------+
CBsvEngine::Init(void)
  {
   DbAvailable=DbCheckAvailable();
   if(!DbAvailable)
      Alert("Unable to work with database. Working offline.");
  }

Aquí comprobamos si es posible trabajar con la base de datos. La clase base DbCheckAvailable() siembre devuelve "false", porque solo se puede trabajar con bases de datos desde clases derivadas. Es posible que hayan notado que las funciones DbConnectionString(), DbCheckAvailable(), DbLoadData() y DbSaveData() no tienen todavía significado especial alguno. Estas son las funciones que ajustaremos en clases descendientes para vincularlas a una base de datos específica. 

El listado 1.4 muestra la implementación de la función ProcessTick() que se llama a la llegada del nuevo tick, lo inserta en el buffer y calcula los valores de nuestro indicador. Para esto, dos buffers de indicador pasan a la función: uno se usa para almacenar la actividad de los compradores, y el otro para almacenar la actividad de los vendedores. 

  Listado 1.4

//+------------------------------------------------------------------+
// Processing incoming tick and updating indicator data              |
//+------------------------------------------------------------------+
CBsvEngine::ProcessTick(double &buyBuffer[],double &sellBuffer[])
  {
// if it's not enough of allocated buffer for ticks, let's increase it
   int bufSize=ArraySize(TickBuffer);
   if(TicksInBuffer>=bufSize)
     {
      ArrayResize(TickBuffer,bufSize+500);
      ArrayResize(VolumeBuffer,bufSize+500);
     }

// getting the last tick and writing it to the buffer
   SymbolInfoTick(Symbol(),TickBuffer[TicksInBuffer]);

   if(TicksInBuffer>0)
     {
      // calculating the time difference
      int span=(int)(TickBuffer[TicksInBuffer].time-TickBuffer[TicksInBuffer-1].time);
      // calculating the price difference
      int diff=(int)MathRound((TickBuffer[TicksInBuffer].bid-TickBuffer[TicksInBuffer-1].bid)*MathPow(10,_Digits));

      // calculating the volume. If the tick came in the same second as the previous one, we consider the time equal to 0.5 seconds
      VolumeBuffer[TicksInBuffer]=span>0 ?(double)diff/(double)span :(double)diff/0.5;

      // filling the indicator buffers with data
      int index=ArraySize(buyBuffer)-1;
      if(diff>0) buyBuffer[index]+=VolumeBuffer[TicksInBuffer];
      else sellBuffer[index]+=VolumeBuffer[TicksInBuffer];
     }

   TicksInBuffer++;
  }

La función LoadData() carga datos de la base de datos para el intervalo actual para un período de tiempo especificado. 

  Listado 1.5

//+------------------------------------------------------------------+
// Loading historical data from the database                         |
//+------------------------------------------------------------------+
CBsvEngine::LoadData(const datetime startTime,const datetime &time[],double &buyBuffer[],double &sellBuffer[])
  {
// if the database is inaccessible, then does not load the data
   if(!DbAvailable) return;

// getting data from the database
   CAdoTable *table=DbLoadData(startTime,TimeCurrent());

   if(CheckPointer(table)==POINTER_INVALID) return;

// filling buffers with received data
   for(int i=0; i<table.Records().Total(); i++)
     {
      // get the record with data
      CAdoRecord *row=table.Records().GetRecord(i);

      // getting the index of corresponding bar
      MqlDateTime mdt;
      mdt=row.GetValue(0).ToDatetime();
      long index=FindIndexByTime(time,StructToTime(mdt));

      // filling buffers with data
      if(index!=-1)
        {
         buyBuffer[index]+=row.GetValue(1).ToDouble();
         sellBuffer[index]+=row.GetValue(2).ToDouble();
        }
     }
   delete table;
  }

LoadData() llama a la función DbLoadData(), que se debe ajustar en sus sucesores y devolver una tabla con tres columnas: la barra de tiempo, el valor del buffer de compradores y el valor del buffer de vendedores.

Otra función que se usa aquí es FindIndexByTime(). Hasta este el momento en el que escribo este artículo no he encontrado una función de búsqueda binaria para series de tiempo en la Biblioteca Estándar, así que la escribí yo mismo.

Y, finalmente, la función SaveData() almacena datos. 

Listado 1.6

//+---------------------------------------------------------------------+
// Saving data from the TickBuffer and VolumeBuffer buffers to database |
//+---------------------------------------------------------------------+
CBsvEngine::SaveData(void)
  {
   if(DbAvailable)
     {
      // creating a table for passing data to SaveDataToDb
      CAdoTable *table=new CAdoTable();
      table.Columns().AddColumn("Time", ADOTYPE_DATETIME);
      table.Columns().AddColumn("Price", ADOTYPE_DOUBLE);
      table.Columns().AddColumn("Volume", ADOTYPE_DOUBLE);

      // filling table with data from buffers
      for(int i=1; i<TicksInBuffer; i++)
        {
         CAdoRecord *row=table.CreateRecord();
         row.Values().GetValue(0).SetValue(TickBuffer[i].time);
         row.Values().GetValue(1).SetValue(TickBuffer[i].bid);
         row.Values().GetValue(2).SetValue(VolumeBuffer[i]);

         table.Records().Add(row);
        }

      // saving data to database
      DbSaveData(table);

      if(CheckPointer(table)!=POINTER_INVALID)
         delete table;
     }

// writing last tick to the beginning, to have something to compare
   TickBuffer[0] = TickBuffer[TicksInBuffer - 1];
   TicksInBuffer = 1;
  }

Como podemos ver, en el método se forma una tabla con la información necesaria para el indicador, y se pasa a la función DbSaveData(), que guarda los datos en la base de datos.Tras grabarlos, simplemente vaciamos el buffer.

De este modo, nuestro marco de trabajo está listo. Ahora veamos en el listado 1.7 el aspecto que tendría el indicador BuySellVolume.mq5: 

Listado 1.7

// including file with the indicator class
#include "BsvEngine.mqh"

//+------------------------------------------------------------------+
//| Indicator Properties                                             |
//+------------------------------------------------------------------+
#property indicator_separate_window

#property indicator_buffers 2
#property indicator_plots   2

#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  Red
#property indicator_width1  2

#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  SteelBlue
#property indicator_width2  2

//+------------------------------------------------------------------+
//| Data Buffers                                                     |
//+------------------------------------------------------------------+
double ExtBuyBuffer[];
double ExtSellBuffer[];

//+------------------------------------------------------------------+
//| Variables                                                        |
//+------------------------------------------------------------------+
// declaring indicator class
CBsvEngine bsv;
//+------------------------------------------------------------------+
//| OnInit
//+------------------------------------------------------------------+
int OnInit()
  {
// setting indicator properties
   IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume");
   IndicatorSetInteger(INDICATOR_DIGITS,2);
// buffer for 'buy'
   SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA);
   PlotIndexSetString(0,PLOT_LABEL,"Buy");
// buffer for 'sell'
   SetIndexBuffer(1,ExtSellBuffer,INDICATOR_DATA);
   PlotIndexSetString(1,PLOT_LABEL,"Sell");

// setting the timer to clear buffers with ticks
   EventSetTimer(60);

   return(0);
  }
//+------------------------------------------------------------------+
//| OnDeinit
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| OnCalculate
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
// processing incoming tick
   bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer);

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnTimer
//+------------------------------------------------------------------+
void OnTimer()
  {
// saving data
   bsv.SaveData();
  }

Muy sencillo, en mi opinión. En el indicador solo se llaman dos funciones de la clase: ProcessTick() y SaveData(). La función ProcessTick() se usa para hacer cálculos, y la función SaveData() es necesaria para restablecer el buffer con ticks, aunque no guarda dato alguno.

Tratemos de compilarlo y ya está: el indicador comienza a mostrar valores:

 

 Figura 1. Indicador BuySellVolume sin enlace a la base de datos GBPUSD M1

¡Excelente! Los ticks siguen llegando, y el indicador hace sus cálculos. La ventaja de esta solución es que solo necesitamos el indicador mismo (ex5) para su trabajo, y nada más. No obstante, al cambiar el intervalo o el instrumento, o cuando cierra su terminal, los datos se pierden para siempre. Para evitar esto, veamos cómo podemos añadir la opción de guardar y cargar datos en nuestro indicador.

2. Enlazar al Servidor SQL 2008

En estos momentos tengo dos DBMSd instalados en mi ordenador: el Servidor SQL 2008 y Db2 9.7. He elegido el Servidor SQL, puesto que asumo que la mayoría de lectores están más familiarizados con el Servidor SQL que con el Db2.

Para empezar, creemos una nueva base de datos BuySellVolume para el Servidor SQL 2008 (a través del Estudio de Gestión del Servidor SQL -SQL Server Management Studio- o cualquier otro medio) y un nuevo archivo BsvMsSql.mqh, en el que incluiremos el archivo con la clase básica CBsvEngine:

#include "BsvEngine.mqh"

El Servidor SQL está equipado con el driver OLE DB, que modo que podemos trabajar a través del proveedor OleDb, incluido en la biblioteca AdoSuite. Para ello, incluya las clases necesarias:

#include <Ado\Providers\OleDb.mqh>

Y cree una clase derivada:

Listado 2.1

//+------------------------------------------------------------------+
// Class of the BuySellVolume indicator, linked with MsSql database  |
//+------------------------------------------------------------------+
class CBsvSqlServer : public CBsvEngine
  {
protected:

   virtual string    DbConnectionString();
   virtual bool      DbCheckAvailable();
   virtual CAdoTable *DbLoadData(const datetime startTime,const datetime endTime);
   virtual void      DbSaveData(CAdoTable *table);

  };

Todo lo que necesitamos es ajustar cuatro funciones responsables del trabajo directo con la base de datos. Empecemos desde el principio. El método DbConnectionString() devuelve una cadena de caracteres para conectarse a la base de datos.

En mi caso, tiene el siguiente aspecto:

Listado 2.2

//+------------------------------------------------------------------+
// Returns the string for connection to database                     |
//+------------------------------------------------------------------+
string CBsvSqlServer::DbConnectionString(void)
  {
   return "Provider=SQLOLEDB;Server=.\SQLEXPRESS;Database=BuySellVolume;Trusted_Connection=yes;";
  }

En la cadena de caracteres de conexión podemos ver que trabajamos a través del driver MS SQL OLE-DB con el servidor SQLEXPRESS, localizado en el aparato local.Nos conectamos a la base de datos BuySellVolume usando una autenticación de Windows (otra opción sería introducir explícitamente nombre de usuario y contraseña).

El siguiente paso es implementar la función DbCheckAvailable(). Pero primero, veamos qué debería hacer realmente esta función.

Se dice que comprueba la posibilidad de trabajar con la base de datos. Hasta cierto punto, esto es cierto. De hecho, su principal propósito es comprobar si hay una tabla para almacenar datos para el instrumento actual, y si no, crearla.Si estas acciones terminan con un error, devolverá "false", y eso significaría que se ignorarán los datos de lectura y escritura del indicador de la tabla, y el indicador funcionará de forma similar a lo que ya hemos implementado (vea el listado 1.7).

Lo que yo sugiero es trabajar con datos a través de procedimientos almacenados (SP, por sus siglas en inglés) del Servidor SQL. ¿Por qué los usé? Porque quería hacerlo.Es una cuestión de gustos, por supuesto, pero creo que usar SPs es una solución más elegante para escribir comandos en el código. Además, requiere más tiempo para compilar, aunque no es algo aplicable a este caso, puesto que se usarán comandos dinámicos :)

El procedimiento almacenado de DbCheckAvailable() tiene el siguiente aspecto:

Listado 2.3

CREATE PROCEDURE [dbo].[CheckAvailable]
        @symbol NVARCHAR(30)
AS
BEGIN
        SET NOCOUNT ON;
        
        -- If there is no table for instrument, we create it
        IF OBJECT_ID(@symbol, N'U') IS NULL
        EXEC ('
                -- Creating table for the instrument
                CREATE TABLE ' + @symbol + ' (Time DATETIME NOT NULL,
                        Price REAL NOT NULL, 
                        Volume REAL NOT NULL);
                
                -- Creating index for the tick time
                CREATE INDEX Ind' + @symbol + '
                ON  ' + @symbol + '(Time);
        ');
END

Podemos ver que si la tabla deseada no está en la base de datos, se forma y ejecuta un comando dinámico (como una cadena de caracteres), que crea una tabla. Cuando se crea el procedimiento almacenado, es momento de gestionarlo con la función DbCheckAvailable(): 

  Listado 2.4

//+------------------------------------------------------------------+
// Checks whether it's possible to connect to database               |
//+------------------------------------------------------------------+
bool CBsvSqlServer::DbCheckAvailable(void)
  {
// working with ms sql via Oledb provider
   COleDbConnection *conn=new COleDbConnection();
   conn.ConnectionString(DbConnectionString());

// using stored procedure to create a table
   COleDbCommand *cmd=new COleDbCommand();
   cmd.CommandText("CheckAvailable");
   cmd.CommandType(CMDTYPE_STOREDPROCEDURE);
   cmd.Connection(conn);

// passing parameters to stored procedure
   CAdoValue *vSymbol=new CAdoValue();
   vSymbol.SetValue(Symbol());
   cmd.Parameters().Add("@symbol",vSymbol);

   conn.Open();

// executing stored procedure
   cmd.ExecuteNonQuery();

   conn.Close();

   delete cmd;
   delete conn;

   if(CheckAdoError())
     {
      ResetAdoError();
      return false;
     }

   return true;
  }

Ahora podremos trabajar con procedimientos almacenados del servidor: solo necesitamos configurar la propiedad CommandType como CMDTYPE_STOREDPROCEDURE, después pasar los parámetros necesarios y ejecutarla. Tal y como se diseñó desde el principio, en caso de error, la función DbCheckAvailable devolverá "false". 

Ahora escribamos un procedimiento almacenado para la función DbLoadData. Puesto que la base de datos almacena datos por cada tick, debemos crear datos a partir de ellos para cada barra del período requerido.He hecho el siguiente procedimiento:

  Listado 2.5

CREATE PROCEDURE [dbo].[LoadData]
        @symbol NVARCHAR(30),   -- instrument
        @startTime DATETIME,    -- beginning of calculation
        @endTime DATETIME,      -- end of calculation
        @period INT             -- chart period (in minutes)
AS
BEGIN
        SET NOCOUNT ON;
        
        -- converting inputs to strings for passing to a dynamic query
        DECLARE @sTime NVARCHAR(20) = CONVERT(NVARCHAR, @startTime, 112) + ' ' + CONVERT(NVARCHAR, @startTime, 114),
                @eTime NVARCHAR(20) = CONVERT(NVARCHAR, @endTime, 112) + ' ' + CONVERT(NVARCHAR, @endTime, 114),
                @p NVARCHAR(10) = CONVERT(NVARCHAR, @period);
                
        EXEC('        
                SELECT DATEADD(minute, Bar * ' + @p + ', ''' + @sTime + ''') AS BarTime, 
                        SUM(CASE WHEN Volume > 0 THEN Volume ELSE 0 END) as Buy,
                        SUM(CASE WHEN Volume < 0 THEN Volume ELSE 0 END) as Sell 
                FROM 
                (
                        SELECT DATEDIFF(minute, ''' + @sTime + ''', TIME) / ' + @p + ' AS Bar,
                                Volume 
                        FROM ' + @symbol + '
                        WHERE Time >= ''' + @sTime + ''' AND Time <= ''' + @eTime + '''
                ) x 
                GROUP BY Bar 
                ORDER BY 1;
        ');
END 

El único detalle a señalar es que la fecha de apertura de la barra completada se debería pasar como @startTime, si no obtendremos un offset.

Consideremos la implementación de DbLoadData() del siguiente listado:

Listado 2.6

//+------------------------------------------------------------------+
// Loading data from the database                                    |
//+------------------------------------------------------------------+
CAdoTable *CBsvSqlServer::DbLoadData(const datetime startTime,const datetime endTime)
  {
// working with ms sql via Oledb provider
   COleDbConnection *conn=new COleDbConnection();
   conn.ConnectionString(DbConnectionString());

// using stored procedure to calculate data
   COleDbCommand *cmd=new COleDbCommand();
   cmd.CommandText("LoadData");
   cmd.CommandType(CMDTYPE_STOREDPROCEDURE);
   cmd.Connection(conn);

// passing parameters to stored procedure
   CAdoValue *vSymbol=new CAdoValue();
   vSymbol.SetValue(Symbol());
   cmd.Parameters().Add("@symbol",vSymbol);

   CAdoValue *vStartTime=new CAdoValue();
   vStartTime.SetValue(startTime);
   cmd.Parameters().Add("@startTime",vStartTime);

   CAdoValue *vEndTime=new CAdoValue();
   vEndTime.SetValue(endTime);
   cmd.Parameters().Add("@endTime",vEndTime);

   CAdoValue *vPeriod=new CAdoValue();
   vPeriod.SetValue(PeriodSeconds()/60);
   cmd.Parameters().Add("@period",vPeriod);

   COleDbDataAdapter *adapter=new COleDbDataAdapter();
   adapter.SelectCommand(cmd);

// creating table and filling it with data, that were returned by stored procedure
   CAdoTable *table=new CAdoTable();
   adapter.Fill(table);

   delete adapter;
   delete conn;

   if(CheckAdoError())
     {
      delete table;
      ResetAdoError();
      return NULL;
     }

   return table;
  }

Aquí estamos llamando a un procedimiento almacenado, herramientas de pasaje, fecha de inicio de cálculos, fecha de final de cálculos y período del gráfico actual en minutos. Después, usando la clase COleDbDataAdapter, leeremos el resultado en una tabla de la que se llenarán los buffers de nuestro indicador.

Este es el último paso para implementar DbSaveData():

  Listado 2.7

CREATE PROCEDURE [dbo].[SaveData]
        @symbol NVARCHAR(30),
        @ticks NVARCHAR(MAX)
AS
BEGIN
        EXEC('
                DECLARE @xmlId INT,
                        @xmlTicks XML = ''' + @ticks + ''';

                EXEC sp_xml_preparedocument 
                        @xmlId OUTPUT, 
                        @xmlTicks;
                
                -- read data from xml to table
                INSERT INTO ' + @symbol + '
                SELECT *
                FROM OPENXML( @xmlId, N''*/*'', 0)
                WITH
                (
                        Time DATETIME N''Time'', 
                        Price REAL N''Price'',
                        Volume REAL N''Volume'' 
                );

                EXEC sp_xml_removedocument @xmlId;
        ');
END

Por favor, tenga en cuenta que los datos xml con ticks almacenados deben transferirse como parámetro @ticks al procedimiento.Esta decisión se tomó por motivos de productividad: es más fácil llamar al procedimiento una vez y enviar a él 20 ticks, que llamarlo 20 veces, transfiriendo un tick en cada ocasión. Veamos cómo la cadena de caracteres xml se debe formar en el siguiente listado: 

Listado 2.8

//+------------------------------------------------------------------+
// Saving data to database                                           |
//+------------------------------------------------------------------+
CBsvSqlServer::DbSaveData(CAdoTable *table)
  {
// if there is nothing to write, then return
   if(table.Records().Total()==0) return;

// forming the xml with data to pass into the stored procedure
   string xml;
   StringAdd(xml,"<Ticks>");

   for(int i=0; i<table.Records().Total(); i++)
     {
      CAdoRecord *row=table.Records().GetRecord(i);

      StringAdd(xml,"<Tick>");

      StringAdd(xml,"<Time>");
      MqlDateTime mdt;
      mdt=row.GetValue(0).ToDatetime();
      StringAdd(xml,StringFormat("%04u%02u%02u %02u:%02u:%02u",mdt.year,mdt.mon,mdt.day,mdt.hour,mdt.min,mdt.sec));
      StringAdd(xml,"</Time>");

      StringAdd(xml,"<Price>");
      StringAdd(xml,DoubleToString(row.GetValue(1).ToDouble()));
      StringAdd(xml,"</Price>");

      StringAdd(xml,"<Volume>");
      StringAdd(xml,DoubleToString(row.GetValue(2).ToDouble()));
      StringAdd(xml,"</Volume>");

      StringAdd(xml,"</Tick>");
     }

   StringAdd(xml,"</Ticks>");

// working with ms sql via Oledb provider
   COleDbConnection *conn=new COleDbConnection();
   conn.ConnectionString(DbConnectionString());

// using stored procedure to write data
   COleDbCommand *cmd=new COleDbCommand();
   cmd.CommandText("SaveData");
   cmd.CommandType(CMDTYPE_STOREDPROCEDURE);
   cmd.Connection(conn);

   CAdoValue *vSymbol=new CAdoValue();
   vSymbol.SetValue(Symbol());
   cmd.Parameters().Add("@symbol",vSymbol);

   CAdoValue *vTicks=new CAdoValue();
   vTicks.SetValue(xml);
   cmd.Parameters().Add("@ticks",vTicks);

   conn.Open();

// executing stored procedure
   cmd.ExecuteNonQuery();

   conn.Close();

   delete cmd;
   delete conn;

   ResetAdoError();
  }

La mitad de esta función toma la formación de esta misma cadena de caracteres con xml. Además, esta cadena de caracteres se transfiere al procedimiento almacenado, y allí se analiza.

De momento, la implementación de la interacción con el Servidor SQL 2008 está terminada, y podemos implementar el indicador BuySellVolume SqlServer.mq5.

Como podrá ver, la implementación de esta versión es similar a la implementación de la anterior, excepto por algunos cambios que trataremos más adelante.

  Listado 2.9

// including file with the indicator class
#include "BsvSqlServer.mqh"

//+------------------------------------------------------------------+
//| Indicator Properties                                             |
//+------------------------------------------------------------------+
#property indicator_separate_window

#property indicator_buffers 2
#property indicator_plots   2

#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  Red
#property indicator_width1  2

#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  SteelBlue
#property indicator_width2  2

//+------------------------------------------------------------------+
//| Input parameters of indicator                                    |
//+------------------------------------------------------------------+
input datetime StartTime=D'2010.04.04'; // start calculations from this date

//+------------------------------------------------------------------+
//| Data Buffers                                                     |
//+------------------------------------------------------------------+
double ExtBuyBuffer[];
double ExtSellBuffer[];

//+------------------------------------------------------------------+
//| Variables                                                        |
//+------------------------------------------------------------------+
// declaring indicator class
CBsvSqlServer bsv;
//+------------------------------------------------------------------+
//| OnInit
//+------------------------------------------------------------------+
int OnInit()
  {
// setting indicator properties
   IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume");
   IndicatorSetInteger(INDICATOR_DIGITS,2);
// buffer for 'buy'
   SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA);
   PlotIndexSetString(0,PLOT_LABEL,"Buy");
// buffer for 'sell'
   SetIndexBuffer(1,ExtSellBuffer,INDICATOR_DATA);
   PlotIndexSetString(1,PLOT_LABEL,"Sell");

// calling the Init function of indicator class
   bsv.Init();

// setting the timer to load ticks into database
   EventSetTimer(60);

   return(0);
  }
//+------------------------------------------------------------------+
//| OnDeinit
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
// if there are unsaved data left, then save them
   bsv.SaveData();
  }
//+------------------------------------------------------------------+
//| OnCalculate
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(prev_calculated==0)
     {
      // calculating the time of the nearest bar
      datetime st[];
      CopyTime(Symbol(),Period(),StartTime,1,st);
      // loading data
      bsv.LoadData(st[0],time,ExtBuyBuffer,ExtSellBuffer);
     }

// processing incoming tick
   bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer);

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnTimer
//+------------------------------------------------------------------+
void OnTimer()
  {
// saving data
   bsv.SaveData();
  }

La primera diferencia que notamos es la presencia del parámetro de entrada StartTime. Este parámetro está diseñado para limitar el intervalo de carga de datos para el indicador. Grandes cantidades de datos pueden conllevar un largo tiempo de cálculo, aunque en realidad los datos obsoletos no nos interesan.

La segunda diferencia es que el tipo de variable bsv ha cambiado.

La tercera diferencia es que se ha añadido la carga de datos en el primer cálculo de datos del indicador, así como la función Init() en OnInit(), y la función SaveData() en OnDeinit().

Ahora, intentemos compilar el indicador y veamos el resultado: 

 

Figura 2. El indicador BuySellVolume enlazado a la base de datos del Servidor SQL 2008 en EURUSD M15

¡Conseguido! Ahora, nuestros datos quedarán grabados, y podremos cambiar libremente entre intervalos.

3. Enlazar a SQLite 3.6

"Matar moscas a cañonazos": creo que ya entienden a qué me refiero. Para esta tarea, usar el Servidor SQL puede resultar ridículo. Por supuesto, si ya tiene este DBMS instalado y lo usa activamente, podría ser la opción preferida. ¿Pero qué pasa si desea dar un indicador a alguien que no está familiarizado con todas estas tecnologías y desea poner el mínimo esfuerzo para que las soluciones funcionen?

Aquí está la tercera versión del indicador que, a diferencia de la anterior, funciona con una base de datos que tiene una arquitectura de servidor de archivos.Con este enfoque, en la mayoría de los casos solo necesitará un par de DLLs con el código de la base de datos.

Aunque nunca antes había trabajado con SQLite, lo elegí por su sencillez, velocidad y mínimo peso. Al principio solo teníamos API para trabajar con programas escritos en C++ y TCL, pero ahora he descubierto el driver ODBC y el proveedor ADO.NET de desarrolladores externos.Puesto que AdoSuite nos permite trabajar con fuentes de datos a través de ODBC, lo mejor parece ser descargarse e instalar el driver ODBC. Pero si no lo he entendido mal, su financiación se cortó hace un año, y además, ADO.NET teóricamente es más rápido.

De modo que veamos qué debemos hacer para poder trabajar con SQLite a través del proveedor ADO.NET de nuestro indicador.

Dos acciones nos acercarán a nuestro objetivo:

  • Primero, debe descargarse e instalar el proveedor. Esta es la página web oficial, en la que podrá encontrar el enlace de descarga: http://sqlite.phxsoftware.com/. De todos estos archivos, a nosotros nos interesa la asamblea System.Data.SQLite.dll. Incluye el mismo código SQLite y el proveedor ADO.NET. Por motivos de conveniencia, he adjuntado esta biblioteca al artículo. Tras la descarga, abra la carpeta Windows\assembly en el Explorador de Windows (!). Debería ver una lista de asambleas, tal y como se muestra en la Figura 3:

 

Figura 3. El Explorer puede mostrar la Caché de Asamblea Global (GAC, por sus siglas en inglés) como una lista


Ahora, arrastre y suelte (!) System.Data.SQLite.dll en esta carpeta.

Así, la asamblea se situará en la GAC, y podremos trabajar con ella:


Figura 4.  System.Data.SQLite.dll instalado en la GAC

Por ahora, la configuración del proveedor está completa.

  • La segunda acción preparatoria que debemos llevar a cabo es escribir el proveedor AdoSuite para trabajar con SQLite. Se puede escribir rápida y fácilmente (a mí me llevó unos 15 minutos). No incluiré el código aquí para no hacer el artículo aún más grande. Puede ver el código en los archivos adjuntos a este artículo.

Ahora que ya está todo hecho, puede empezar a escribir un indicador. Creemos un nuevo archivo vacío para la base de datos SQLite en la carpeta MQL5\Files. SQLite no es excesivamente escrupuloso con las extensiones de archivo, de modo que llamémoslo simplemente BuySellVolume.sqlite.

De hecho, no es necesario crear el archivo: se creará automáticamente al hacer el primer comando en la base de datos especificada en la cadena de caracteres de conexión (vea el listado 3.2). Aquí lo crearemos explícitamente solo para aclarar de dónde proviene.

Cree un nuevo archivo llamado BsvSqlite.mqh, incluya nuestra clase base y proveedor para SQLite: 

#include "BsvEngine.mqh"
#include <Ado\Providers\SQLite.mqh>

 La clase derivada tiene la misma forma que la anterior, a excepción del nombre:

   Listing 3.1

//+------------------------------------------------------------------+
// Class of the BuySellVolume indicator, linked with SQLite database |
//+------------------------------------------------------------------+
class CBsvSqlite : public CBsvEngine
  {
protected:

   virtual string    DbConnectionString();
   virtual bool      DbCheckAvailable();
   virtual CAdoTable *DbLoadData(const datetime startTime,const datetime endTime);
   virtual void      DbSaveData(CAdoTable *table);

  };

Ahora procedamos con la implementación de métodos.

DbConnectionString() tendrá el siguiente aspecto:

    Listing 3.2

//+------------------------------------------------------------------+
// Returns the string for connection to database                     |
//+------------------------------------------------------------------+
string CBsvSqlite::DbConnectionString(void)
  {
   return "Data Source=MQL5\Files\BuySellVolume.sqlite";
  }

Como puede ver, la cadena de caracteres de conexión es mucho más sencilla y solo indica la localización de nuestra base.

Aquí se indica la ruta relativa, pero también se permite la ruta absoluta: "Data Source = c:\Program Files\Metatrader 5\MQL 5\Files\BuySellVolume.sqlite".

El listado 3.3 muestra el código DbCheckAvailable(). Puesto que SQLite no ofrece ninguna opción de procedimientos almacenados, ahora todos los comandos se escriben directamente en el código:

   Listado 3.3

//+------------------------------------------------------------------+
// Checks whether it's possible to connect to database               |
//+------------------------------------------------------------------+
bool CBsvSqlite::DbCheckAvailable(void)
  {
// working with SQLite via written SQLite provider
   CSQLiteConnection *conn=new CSQLiteConnection();
   conn.ConnectionString(DbConnectionString());

// command, that checks the availability of table for the instrument
   CSQLiteCommand *cmdCheck=new CSQLiteCommand();
   cmdCheck.Connection(conn);
   cmdCheck.CommandText(StringFormat("SELECT EXISTS(SELECT name FROM sqlite_master WHERE name = '%s')", Symbol()));

// command, that creates a table for the instrument
   CSQLiteCommand *cmdTable=new CSQLiteCommand();
   cmdTable.Connection(conn);
   cmdTable.CommandText(StringFormat("CREATE TABLE %s(Time DATETIME NOT NULL, " +
                        "Price DOUBLE NOT NULL, "+
                        "Volume DOUBLE NOT NULL)",Symbol()));

// command, that creates an index for the time
   CSQLiteCommand *cmdIndex=new CSQLiteCommand();
   cmdIndex.Connection(conn);
   cmdIndex.CommandText(StringFormat("CREATE INDEX Ind%s ON %s(Time)", Symbol(), Symbol()));

   conn.Open();

   if(CheckAdoError())
     {
      ResetAdoError();
      delete cmdCheck;
      delete cmdTable;
      delete cmdIndex;
      delete conn;
      return false;
     }

   CSQLiteTransaction *tran=conn.BeginTransaction();

   CAdoValue *vExists=cmdCheck.ExecuteScalar();

// if there is no table, we create it
   if(vExists.ToLong()==0)
     {
      cmdTable.ExecuteNonQuery();
      cmdIndex.ExecuteNonQuery();
     }

   if(!CheckAdoError()) tran.Commit();
   else tran.Rollback();

   conn.Close();

   delete vExists;
   delete cmdCheck;
   delete cmdTable;
   delete cmdIndex;
   delete tran;
   delete conn;

   if(CheckAdoError())
     {
      ResetAdoError();
      return false;
     }

   return true;
  }

El resultado de esta función es idéntico al equivalente del Servidor SQL. Un detalle que me gustaría señalar es los tipos de campos para la tabla. Lo curioso es que los tipos de campo apenas tienen significado en SQLite. Además, en él no existen los tipos de datos DOUBLE y DATETIME (al menos, no están incluidos en los estándar). Todos los valores se almacenan en forma de cadena de caracteres, y después se transforman dinámicamente en el tipo necesario.

Por tanto, ¿qué sentido tiene declarar columnas como DOUBLE y DATETIME? No conozco las particularidades de la operación, pero siguiendo un comando, ADO.NET los convierte a los tipos DOUBLE y DATETIME automáticamente. Pero esto no siempre sucede así. Se dan excepciones, como la que se muestra en el siguiente listado.

De modo que consideremos el listado de la siguiente función DbLoadData():

   Listado 3.4

//+------------------------------------------------------------------+
// Loading data from the database                                    |
//+------------------------------------------------------------------+
CAdoTable *CBsvSqlite::DbLoadData(const datetime startTime,const datetime endTime)
  {
// working with SQLite via written SQLite provider
   CSQLiteConnection *conn=new CSQLiteConnection();
   conn.ConnectionString(DbConnectionString());

   CSQLiteCommand *cmd=new CSQLiteCommand();
   cmd.Connection(conn);
   cmd.CommandText(StringFormat(
                   "SELECT DATETIME(@startTime, '+' || CAST(Bar*@period AS TEXT) || ' minutes') AS BarTime, "+
                   "  SUM(CASE WHEN Volume > 0 THEN Volume ELSE 0 END) as Buy, "+
                   "  SUM(CASE WHEN Volume < 0 THEN Volume ELSE 0 END) as Sell "+
                   "FROM "+
                   "("+
                   "  SELECT CAST(strftime('%%s', julianday(Time)) - strftime('%%s', julianday(@startTime)) AS INTEGER)/ (60*@period) AS Bar, "+
                   "     Volume "+
                   "  FROM %s "+
                   "  WHERE Time >= @startTime AND Time <= @endTime "+
                   ") x "+
                   "GROUP BY Bar "+
                   "ORDER BY 1",Symbol()));

// substituting parameters
   CAdoValue *vStartTime=new CAdoValue();
   vStartTime.SetValue(startTime);
   cmd.Parameters().Add("@startTime",vStartTime);

   CAdoValue *vEndTime=new CAdoValue();
   vEndTime.SetValue(endTime);
   cmd.Parameters().Add("@endTime",vEndTime);

   CAdoValue *vPeriod=new CAdoValue();
   vPeriod.SetValue(PeriodSeconds()/60);
   cmd.Parameters().Add("@period",vPeriod);

   CSQLiteDataAdapter *adapter=new CSQLiteDataAdapter();
   adapter.SelectCommand(cmd);

// creating table and filling it with data
   CAdoTable *table=new CAdoTable();
   adapter.Fill(table);

   delete adapter;
   delete conn;

   if(CheckAdoError())
     {
      delete table;
      ResetAdoError();
      return NULL;
     }

// as we get the string with date, but not the date itself, it is necessary to convert it
   for(int i=0; i<table.Records().Total(); i++)
     {
      CAdoRecord* row= table.Records().GetRecord(i);
      string strDate = row.GetValue(0).AnyToString();
      StringSetCharacter(strDate,4,'.');
      StringSetCharacter(strDate,7,'.');
      row.GetValue(0).SetValue(StringToTime(strDate));
     }

   return table;
  }

Esta función se desarrolla de la misma manera que su implementación para MS SQL. ¿Pero por qué hay un bucle al final de la función? Efectivamente, en este comando mágico, ninguno de mis intentos de devolución de DATETIME tuvo éxito. La ausencia del tipo DATETIME en SQLite es evidente: en lugar de la fecha, se devuelve la cadena de caracteres en el formato YYYY-MM-DD hh:mm:ss. Pero se puede convertir fácilmente a una forma comprensible para la función StringToTime, y nosotros lo podemos usar a nuestro favor.

Y finalmente, la función DbSaveData():

  Listado 3.5

//+------------------------------------------------------------------+
// Saving data to database                                           |
//+------------------------------------------------------------------+
CBsvSqlite::DbSaveData(CAdoTable *table)
  {
// if there is nothing to write, then return
   if(table.Records().Total()==0) return;

// working with SQLite via SQLite provider
   CSQLiteConnection *conn=new CSQLiteConnection();
   conn.ConnectionString(DbConnectionString());

// using stored procedure to write data
   CSQLiteCommand *cmd=new CSQLiteCommand();
   cmd.CommandText(StringFormat("INSERT INTO %s VALUES(@time, @price, @volume)", Symbol()));
   cmd.Connection(conn);

// adding parameters
   CSQLiteParameter *pTime=new CSQLiteParameter();
   pTime.ParameterName("@time");
   cmd.Parameters().Add(pTime);

   CSQLiteParameter *pPrice=new CSQLiteParameter();
   pPrice.ParameterName("@price");
   cmd.Parameters().Add(pPrice);

   CSQLiteParameter *pVolume=new CSQLiteParameter();
   pVolume.ParameterName("@volume");
   cmd.Parameters().Add(pVolume);

   conn.Open();

   if(CheckAdoError())
     {
      ResetAdoError();
      delete cmd;
      delete conn;
      return;
     }

// ! explicitly starting transaction
   CSQLiteTransaction *tran=conn.BeginTransaction();

   for(int i=0; i<table.Records().Total(); i++)
     {
      CAdoRecord *row=table.Records().GetRecord(i);

      // filling parameters with values
      CAdoValue *vTime=new CAdoValue();
      MqlDateTime mdt;
      mdt=row.GetValue(0).ToDatetime();
      vTime.SetValue(mdt);
      pTime.Value(vTime);

      CAdoValue *vPrice=new CAdoValue();
      vPrice.SetValue(row.GetValue(1).ToDouble());
      pPrice.Value(vPrice);

      CAdoValue *vVolume=new CAdoValue();
      vVolume.SetValue(row.GetValue(2).ToDouble());
      pVolume.Value(vVolume);

      // adding record
      cmd.ExecuteNonQuery();
     }

// completing transaction
   if(!CheckAdoError())
      tran.Commit();
   else tran.Rollback();

   conn.Close();

   delete tran;
   delete cmd;
   delete conn;

   ResetAdoError();
  }

Quiero tratar los detalles de la implementación de esta función.

Primero, todo se hace en la transacción, aunque esto es lógico. Pero esto no se hizo así por motivos de seguridad de datos, sino por motivos de productividad: si se añade una entrada sin una transacción explítica, el servidor crea una transacción implícitamente, guarda los datos en la tabla y elimina una transacción. ¡Y esto se debe hacer por cada tick! ¡Además, la base de datos entera se bloquea al grabar una nueva entrada! Merece la pena señalar que los comandos no requieren necesariamente una transacción. De nuevo, no entiendo completamente la razón. Supongo que se debe a la falta de transacciones múltiples.

En segundo lugar, creamos un comando solo una vez, y después, en un bucle, le asignamos parámetros y lo ejecutamos. De nuevo, esto se debe al tema de la productividad: el comando se compila (optimiza) una vez, y después se trabaja con una versión compilada. 

Pero bueno, vayamos al grano. Echemos un vistazo al indicador BuySellVolume SQLite.mq5 mismo:

  Listado 3.6

// including file with the indicator class
#include "BsvSqlite.mqh"

//+------------------------------------------------------------------+
//| Indicator Properties                                             |
//+------------------------------------------------------------------+
#property indicator_separate_window

#property indicator_buffers 2
#property indicator_plots   2

#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  Red
#property indicator_width1  2

#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  SteelBlue
#property indicator_width2  2

//+------------------------------------------------------------------+
//| Input parameters of indicator                                    |
//+------------------------------------------------------------------+
input datetime StartTime=D'2010.04.04';   // start calculations from this date

//+------------------------------------------------------------------+
//| Data Buffers
//+------------------------------------------------------------------+
double ExtBuyBuffer[];
double ExtSellBuffer[];

//+------------------------------------------------------------------+
//| Variables
//+------------------------------------------------------------------+
// declaring indicator class
CBsvSqlite bsv;
//+------------------------------------------------------------------+
//| OnInit
//+------------------------------------------------------------------+
int OnInit()
  {
// setting indicator properties
   IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume");
   IndicatorSetInteger(INDICATOR_DIGITS,2);
// buffer for 'buy'
   SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA);
   PlotIndexSetString(0,PLOT_LABEL,"Buy");
// buffer for 'sell'
   SetIndexBuffer(1,ExtSellBuffer,INDICATOR_DATA);
   PlotIndexSetString(1,PLOT_LABEL,"Sell");

// calling the Init function of indicator class
   bsv.Init();

// setting the timer to load ticks into database
   EventSetTimer(60);

   return(0);
  }
//+------------------------------------------------------------------+
//| OnDeinit
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
// if there are unsaved data left, then save them
   bsv.SaveData();
  }
//+------------------------------------------------------------------+
//| OnCalculate
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(prev_calculated==0)
     {
      // calculating the time of the nearest bar
      datetime st[];
      CopyTime(Symbol(),Period(),StartTime,1,st);
      // loading data
      bsv.LoadData(st[0],time,ExtBuyBuffer,ExtSellBuffer);
     }

// processing incoming tick
   bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer);

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnTimer
//+------------------------------------------------------------------+
void OnTimer()
  {
// saving data
   bsv.SaveData();
  }

Solo la clase de función ha cambiado, el resto del código permanece igual.

Por ahora, la implementación de la tercera versión del indicador ha acabado: usted mismo puede ver el resultado.

 

Figura 5. El indicador BuySellVolume enlazado a la base de datos SQLite 3.6 en EURUSD M5

Por cierto, a diferencia del Estudio de Gestión del Servidor SQL, en SQLite no hay utilidades estándar para trabajar con bases de datos. Por tanto, para no trabajar con una "caja negra", puede descargarse la utilidad correspondiente de desarrolladores externos. Personalmente, a mí me gusta SQLiteMan: es fácil de usar y al mismo tiempo tiene toda la funcionalidad necesaria. Puede descargárselo desde aquí: http://sourceforge.net/projects/sqliteman/.

Conclusión

Si está leyendo estas líneas, significa que todo ha acabado ;). Debo confesar que no esperaba que este artículo acabara siendo tan largo. Por tanto, las preguntas son inevitables, y estaré encantado de responderlas.

Como podemos observar, cada solución tiene sus ventajas e inconvenientes. La primera variante se distingue por su independencia, la segunda por su eficiencia, y la tercera por su portabilidad. La que elija dependerá de usted.

¿Le resultó útil el indicador implementado? Igualmente, usted decide. Para mí, se trata de un experimento muy interesante.

Aprovecho para despedirme. ¡Nos vemos!

Descripción de contenidos de archivos:

 # Nombre de archivo Descripción
1
 Sources_en.zip
 Contiene los códigos fuente de todos los indicadores y la biblioteca AdoSuite. Debe descomprimirlo en la carpeta correspondiente de su terminal. Propósito de los indicadores: sin el uso de base de datos (BuySellVolume.mq5), trabajando con la base de datos del Servidor SQL 2008 (BuySellVolume SqlServer.mq5) y trabajando con la base de datos SQLite (BuySellVolume SQLite.mq5).
2
 BuySellVolume-DB-SqlServer.zip
 Archivo de base de datos del Servidor SQL 2008*
3
 BuySellVolume-DB-SQLite.zip
 Archivo de base de datos SQLite*
4
 System.Data.SQLite.zip
 Archivo System.Data.SQLite.dll, necesario para trabajar con la base de datos SQLite
  5  Databases_MQL5_doc_en.zip  Archivo de códigos fuente, indicadores y documentación de la biblioteca AdoSuite

* Ambas bases de datos contienen datos de indicador de tick del 5 al 9 de abril, incluyendo los siguientes instrumentos: AUDNZD, EURUSD, GBPUSD, USDCAD, USDCHF, USDJPY.

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

Cómo crear un experto en sólo unos minutos con ayuda de EA Tree: Parte 1 Cómo crear un experto en sólo unos minutos con ayuda de EA Tree: Parte 1
El programa EA Tree es el primer instrumento que permite construir el código de un asesor, sobre la base del método de esquema de bloques "drag and drop". La creación de asesores en EA Tree se lleva a cabo mediante la construcción de bloques que pueden contener funciones del lenguaje MQL5, indicadores técnicos y personalizados, o valores numéricos. Las salidas de los bloques pueden conectarse con las entradas de otros bloques, formando un "árbol de bloques". En base al árbol de bloques, el programa EA Tree genera el código fuente del asesor, que después puede ser compilado en la plataforma comercial MetaTrader 5.
Transferir Indicadores de MQL4 a MQL5 Transferir Indicadores de MQL4 a MQL5
Este artículo está dedicado a las peculiaridades de transferir construcciones de precio escritas en MQL4 a MQL5. Para facilitar el proceso de transferir cálculos de indicador de MQL4 a MQL5, se recomienda la biblioteca de funciones mql4_2_mql5.mqh. Su uso se decribe en la base de transferencia de los indicadores MACD, Stochastic y RSI.
Fundamentos de la estadística Fundamentos de la estadística
Cada trader utiliza en su trabajo este u otro tipo de cálculos estadísticos, incluso si se declara seguidor del análisis fundamental. Este artículo le ayudará a familiarizarse con los fundamentos de la estadística, con sus elementos básicos, además de hablarle de su importancia a la hora de tomar decisiones.
Crear un juego de la "Serpiente" en MQL5 Crear un juego de la "Serpiente" en MQL5
Este artículo describe un ejemplo de programación del juego de la "Serpiente". En MQL5, la programación para juegos se hizo posible principalmente a causa de sus herramientas para controlar eventos. La programación orientada al objeto simplifica inmensamente este proceso. En este artículo aprenderá sobre las herramientas de procesamiento de eventos, los ejemplos de uso de las clases de la Biblioteca MQL5 Estándar y detalles de llamadas de funciones periódicas.