English Русский 中文 Español Deutsch 日本語 한국어 Français Italiano Türkçe
Aplicação prática de bancos de dados para análise de mercado

Aplicação prática de bancos de dados para análise de mercado

MetaTrader 5Integração | 9 janeiro 2014, 14:13
2 542 0
Alexander
Alexander

Introdução

Trabalhar com dados se tornou a tarefa principal para o software moderno - tanto para aplicativos autônomos quanto para os de rede. Para solucionar esse problema um software especializado foi criado. São sistemas de gerenciamento de banco de dados (DBMS) que podem estruturar, sistematizar e organizar dados para seu armazenamento e processamento. Esses softwares são a base de atividades de informação em todos os setores - da fabricação às finanças e telecomunicações.

Quanto aos negócios, a maioria dos analistas não utiliza bancos de dados em seu trabalho. Mas existem tarefas para as quais essa solução teria que ser útil.

Esse artigo abrange um exemplo de tarefa: o indicador de ponto, que guarda e carrega dados do banco de dados.

Algoritmo BuySellVolume

BuySellVolume - esse nome simples que dei ao indicador que tem um algoritmo mais simples ainda: pega o tempo (t) e o preço (p) de dois pontos sequenciais (tick1 e tick2). Vamos calcular a diferença entre eles:

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

O valor do volume é calculado utilizando essa fórmula:

v2 = Δp / Δt

Então, nosso volume é diretamente proporcional ao número de pontos, pelos quais o preço se deslocou, e é inversamente proporcional ao tempo, gasto para tal. Se Δt = 0, então ao invés disso o valor 0.5 é tomado. Assim, obtemos um tipo de valor de atividade de compradores e vendedores no mercado.

1. A implementação do indicador sem utilizar o banco de dados

Eu acho que seria lógico considerar primeiramente um indicador com funcionalidade específica, mas sem interação com o banco de dados. Em minha opinião, a melhor solução é criar uma classe base, que fará os cálculos apropriados, e é derivada para perceber a interação com o banco de dados Para implementar isso precisaremos da biblioteca AdoSuite. Então, clique no link e baixe-a.

Primeiramente, crie o arquivo BsvEngine.mqh e conecte as classes de dados AdoSuite:

#include 

Depois crie uma classe indicadora de base, a qual implementará todas as funções necessárias, exceto o trabalho com o banco de dados. Se parece conforme o seguinte:

Listagem 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();
  };

Quero mencionar que de modo a aumentar a produtividade, os dados são colocados em buffers (memórias) especiais (TickBuffer e VolumeBuffer) e depois, após um certo período de tempo, são carregados dentro do banco de dados.

Vamos considerar a implementação da ordem de classe. Vamos começar com o construtor:

Listagem 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;
  } 

Aqui, acredito que tudo deve estar claro: as variáveis são inicializadas e os tamanhos inicias dos buffers são ajustados.

A seguir vem a implementação do método Init():

Listagem 1.3

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

Aqui verificamos se é possível trabalhar com o banco de dados. Na classe base DbCheckAvailable() sempre retorna falso, porque o trabalho com banco de dados será somente a partir de classes derivadas. Acho que você notou que as funções DbConnectionString(), DbCheckAvailable(), DbLoadData(), DbSaveData() não tem nenhum significado especial ainda. Essas são as funções que substituímos pelos descendentes para unir a um banco de dados específico.

A listagem 1.4 mostra a implementação da função ProcessTick(), que é chamada na nova chegada da teca, insere a teca no buffer e calcula os valores para nosso indicador. Para fazer isso 2 buffers de indicador são passados para a função: um é utilizado para armazenar a atividade dos compradores, o outro - para armazenar a atividade dos vendedores.

Listagem 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++;
  }

A função LoadData() carrega dados do banco de dados para o intervalo de tempo atual por um período de tempo específico.

Listagem 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;
  }

O LoadData() aciona a função DbLoadData() a qual deve ser substituída pelo sucessor e voltar para uma tabela de três colunas - a barra de tempo, o valor de buffer dos compradores e o valor de buffer dos vendedores.

Outra função é utilizada aqui - FindIndexByTime(). No momento em que esse artigo é escrito, eu ainda não encontrei uma função de busca binária para uma série temporal na biblioteca padrão, então eu mesmo a escrevi.

E, finalmente, a função SaveData() para armazenamento de dados.

Listagem 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 vemos, no método uma tabela é formada com a informação necessária para o indicador e é passada para a função DbSaveData(), que salva os dados para o banco de dados. Depois de armazenar, nós somente limpamos o buffer.

Então, nossa estrutura está pronta - agora vamos visualizar a listagem 1.7 como se parece o indicador BuySellVolume.mq5:

Listagem 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();
  }

Muito simples, na minha opinião. No indicador somente duas funções da classe são acionadas: ProcessTick() e SaveData(). A função ProcessTick() é utilizada para cálculos e a função SaveData() é necessária para reajustar o buffer com pontos, apesar dela não salvar dados.

Vamos tentar compilar e ai está! - O indicador começa a mostrar valores:

Figura 1. O indicador BuySellVolume sem link para o banco de dados no GBPUSD M1

Excelente! Os pontos estão em funcionamento, o indicador está calculando. A vantagem de tal solução - precisamos somente do indicador por si só (ex5) por seu trabalho e nada mais. No entanto, ao mudar o período de tempo, ou o instrumento, ou quando você fecha o terminal, os dados são irrecuperavelmente perdidos. Parar evitar isso vamos ver como podemos adicionar salvamento e carregamento de dados em nosso indicador.

2. Vinculando ao SQL Server 2008

Nesse momento eu tenho dois DBMSd instalados em meu computador - o SQL Server 2008 e o Db2 9.7. Eu escolhi o SQL Server, já que eu assumo que a maioria dos leitores estão mais familiarizados com o SQL Server do que com o Db2.

Para começar, vamos criar um novo banco de dados BuySellVolume para o SQL Server 2008 (pelo SQL Server Management Studio ou quaisquer outros meios) e um novo arquivo BsvMsSql.mqh ao qual incluiremos o arquivo com a classe básica CBsvEngine:

#include "BsvEngine.mqh"

O SQL Server é equipado com o driver OLE DB de modo que podemos trabalhar com ele pelo provedor OleDb, incluído na biblioteca AdoSuite. Para fazer isso, inclua as classes necessárias:

#include 

E crie efetivamente uma classe derivada:

Listagem 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);

  };

Tudo que precisamos é substituir quatro funções que são responsáveis por funcionar diretamente com o banco de dados. Vamos começar do início. O método DbConnectionString() retorna uma cadeia para conectar ao banco de dados.

No meu caso, se parece conforme abaixo:

Listagem 2.2

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

Da cadeia de conexão vemos que operamos através do driver MS SQL OLE-DB com o servidor SQLEXPRESS, localizado na máquina local. Nós estamos conectando o banco de dados BuySellVolume utilizando a autenticação Windows (outra opção - entrar explícitamente com o login e senha).

O próximo passo é implementar a função DbCheckAvailable(). Mas primeiro, vejamos o que essa função realmente deve fazer.

Foi dito que ela verifica a possibilidade de operar com o banco de dados. Para algumas extensões, isso é verdadeiro. De fato, seu propósito principal é verificar se há uma tabela para armazenar dados para o instrumento atual, e se não há - criar uma.Se estas ações terminarem com um erro, ela retornará falso, o que significa que a leitura e escrita dos dados do indicador a partir da tabela serão ignorados, e o indicador irá funcionar de modo similar a este, que nós já implementamos (veja a listagem 1.7).

Sugiro trabalhar com dados por procedimentos armazenados (SP) do SQL Server. Por que utilizá-los? Só por preferência.Isto é só uma preferência, naturalmente, mas acho que usar SPs é uma solução mais elegante do que escrever consultas (queries) no código (que também precisam de mais tempo para compilar, embora não seja aplicável a este caso, já que consultas dinâmicas serão usadas. :)

O procedimento armazenado For DbCheckAvailable() se parece conforme abaixo:

Listagem 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

Vemos que se a tabela desejada não estiver no banco de dados, a consulta dinâmica (como uma cadeia), a qual cria uma tabela, é formada e executada. Quando o procedimento armazenado é criado - é o momento de lidar com a função DbCheckAvailable():

Listagem 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;
  }

Como podemos ver, somos capazes de trabalhar com procedimentos armazenados do servidor - só precisamos ajustar a propriedade CommandType para CMDTYPE_STOREDPROCEDURE, depois passar os parâmetros necessários e executar. Da forma como foi idealizado, em caso de erro a função DbCheckAvailable irá responder como false.

A seguir, vamos gravar um procedimento armazenado para a função DbLoadData. Já que o banco de dados armazena dados para cada ponto, precisamos criar os dados para eles para cada barra de período necessário.Fiz o seguinte procedimento:

Listagem 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 

A única coisa a se mencionar - o tempo de abertura da primeira barra preenchida deve ser passado como @startTime, senão obteremos o deslocamento.

Vamos considerar a implementação do DbLoadData() da seguinte listagem:

Listagem 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;
  }

Aqui estamos acionando o procedimento armazenado, as ferramentas de passe, a data de início do cálculo , a data de término do cálculo e o período atual do gráfico em minutos. Então utilizando a classe COleDbDataAdapter, estaremos lendo o resultado na tabela, do qual os buffers de nossos indicadores estarão preenchidos.

E o passo final será implementar o DbSaveData():

Listagem 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

Note que o xml com os dados dos pontos armazenados deve ser passado como parâmetro @ticks dentro do procedimento Esta decisão foi tomada devido a razões de desempenho - é mais fácil chamar o procedimento uma vez e enviar os 20 pontos, do que chamar o procedimento 20 vezes, passando um só ponto. Vejamos como a cadeia xml deve ser formada na seguinte listagem:

Listagem 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();
  }

Quase metade dessa função leva o formação dessa mesma cadeia com o xml. Além disso, essa cadeia é passada para o procedimento armazenado e lá ele é analisado.

Por enquanto a implementação da interação com o SQL Server 2008 está finalizada e podemos implementar o indicador BuySellVolume SqlServer.mq5.

Como você verá, a implementação dessa versão é similar a implementação da última, exceto por algumas mudanças que serão discutidas mais adiante.

Listagem 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();
  }

A primeira diferença que salta os olhos - a presença do parâmetro de entrada StartTime. Esse parâmetro tem por objetivo limitar o intervalo de carregamento de dados para o indicador. O fato é que uma grande quantidade de dados pode levar um tempo longo de cálculo, apesar de que, na verdade, dados obsoletos não nos interessam.

A segunda diferença - o tipo da variável bsv é alterado para outro.

A terceira diferença - o carregamento de dados no primeiro cálculo do indicador de dados foi incluído, assim como a função Init() à função OnInit() e a função SaveData() à OnDeinit().

Agora, vamos tentar compilar o indicador e ver o resultado.

Figura 2. O indicador BuySellVolume vinculado ao banco de dados do SQL Server 2008 no EURUSD M15

Feito! Agora nossos dados estão guardados e podemos alternar livremente entre os períodos de tempo.

3. Vinculando ao SQLite 3.6

"Atirar com canhões em moscas" - Eu acho que você entende o que quero dizer. Para essa tarefa implantar o SQL Server é meio ridículo. Claro, se você tiver esse DBMS instalado e está usando o ativamente, deve ser a opção preferida. Mas e se você quiser dar o indicador a alguém que está distante de todas essas tecnologias e deseja um mínimo de esforço para a solução funcionar?

Aqui está a terceira versão do indicador, que, diferente das anteriores, funciona com um banco de dados que tem uma arquitetura de servidor de arquivos.Nesta abordagem, na maioria dos casos você somente precisará de alguns DLLs com o kernel do banco de dados.

Apesar de eu nunca ter trabalhado com o SQLite antes, eu o escolhi por sua simplicidade, rapidez e leveza. Inicialmente, temos somente a API para trabalhar com os programas, escritas em C++ e TCL, mas também encontrei o driver ODBC e o fornecedor do ADO.NET de desenvolvedores terceirizados. Já que o AdoSuite permite trabalhar com fontes de dados via ODBC, seria melhor baixar e instalar o driver do ODBC. Mas da forma que eu entendo, o seu suporte foi descontinuado a mais de um ano atrás e, além disso, o ADO.NET, teoricamente, deve funcionar mais rápido.

Então vamos ver o que precisa ser feito de modo que possamos trabalhar com o SQLite pelo provedor ADO.NET de nosso indicador.

Duas ações nos trarão para o nosso objetivo:

  • Primeiro, você deve baixar e instalar o provedor. Aqui está o site http://sqlite.phxsoftware.com/, onde o link para baixar está disponível. De todos esses arquivos, estamos interessados na montagem do System.Data.SQLite.dll. Ele inclui o próprio SQLite kernel e o provedor ADO.NET. Para comodidade, eu anexei esta biblioteca a esse artigo. Após baixar, abra a pasta Windows\assembly no Windows Explorer (!). Você deve ver uma lista de montagens, conforme mostrado na figura 3:

Figura 3. O Explorer pode exibir o GAC (cache de montagem global) como uma lista


Agora, arraste e solte(!) o System.Data.SQLite.dll para essa pasta.

Como resultado, a montagem é colocada no cache de montagem global (GAC), e podemos trabalhar com ele.


Figura 4. System.Data.SQLite.dll instalado no GAC

Por enquanto, a configuração do provedor está completa.

  • A segunda ação preparatória que devemos fazer - é escrever no provedor AdoSuite para funcionar com o SQLite. Isto é escrito de forma rápida e fácil (para mim levou cerca de 15 minutos). Não irei colocar seu código aqui para que esse artigo não fique ainda mais enorme. Você pode ver o código nos arquivos, anexo a esse artigo.

Agora - quando tudo estiver feito - você poe começar a escrever um indicador. Para o banco de dados do SQLite vamos criar um novo arquivo vazio na pasta MQL5\Files. O SQLite não é exigente quanto a extensão do arquivo, então vamos chamá-lo simplesmente de - BuySellVolume.sqlite.

De fato, não é necessário criar o arquivo: eles será criado automaticamente quando você consultar o banco de dados pela primeira vez, especificado na cadeia de conexão (veja a listagem 3.2). Aqui criamos ele explicitamente somente de modo a torná-lo simples, de onde ele veio.

Crie um novo arquivo chamado BsvSqlite.mqh, inclua nossa classe base e o provedor para o SQLite:

#include "BsvEngine.mqh"
#include 

A classe derivada tem a mesma forma que a anterior, exceto o nome:

Listagem 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);

  };

Agora vamos prosseguir com a implementação dos métodos.

O DbConnectionString() se parece do seguinte modo:

Listagem 3.2

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

Como você vê, a cadeia de conexão parece muito mais simples e somente indica a localização de nossa base.

Aqui o caminho relativo é indicado, mas o caminho absoluto também é permitido: "Data Source = c:\Program Files\Metatrader 5\MQL 5\Files\BuySellVolume.sqlite".

A listagem 3.3 mostra o código DbCheckAvailable(). Já que o SQLite não nos oferece nada parecido com procedimentos armazenados, agora todas as consultas são escritas diretamente do código.

Listagem 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;
  }

O resultado desta função é idêntico ao equivalente para o SQL Server. Uma coisa que eu gostaria de mencionar - seus tipos de campos para a tabela. O que é engraçado é que os tipos de campos têm pouco sentido no SQLite. Além disso, não existem tipos de dados double e datetime nele (pelo menos, eles não são incluídos nos padrões). Todos os valores são armazenados na forma de cadeia, e depois moldados no tipo necessário.

Então qual o sentido de declarar colunas como DOUBLE E DATETIME? Não sei das complicações da operação, mas na consulta o ADO.NET os converte nos tipos DOUBLE E DATETIME automaticamente. Mas isso nem sempre é verdadeiro, já que existem alguns momentos em que um deles vai surgir na seguinte listagem.

Então, vamos considerar a listagem da seguinte função DbLoadData():

Listagem 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;
  }

Essa função funciona da mesma forma que a sua implementação para o MS SQL. Mas por que existe o ciclo no final da função? Sim, nesta consulta mágica todas as minhas tentativas de volta do DATETIME foram mal sucedidas. A ausência do tipo DATETIME no SQLite é evidente - ao invés da data, a cadeia no formato YYYY-MM-DD hh:mm:ss retorna. Mas ela pode ser facilmente moldada em uma forma, que seja compreensível para a função StringToTime, e usamos essa vantagem.

E, finalmente, a função DbSaveData():

Listagem 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();
  }

Quero abranger os detalhes dessa implementação de função.

Primeiramente, tudo é feito na transição, apesar disso ser lógico. Mas isso foi feito não devido a motivos de segurança dos dados - foi feito devido a motivos de desempenho: se um registro é adicionado sem uma transação específica, o servidor cria uma transação implicitamente, insere um registro na tabela e apaga a transação. E isso é feito para cada ponto! Além disso, todo o banco de dados é travado quando o registro está sendo gravado! Vale a pena notar que comandos não necessariamente necessitam de transações. Novamente, eu ainda não entendi completamente porque isso está acontecendo. Eu suponho que isso seja devido a falta de múltiplas transações.

Em segundo lugar, criamos um comando uma vez e depois, em um ciclo, atribuímos parâmetros e os executamos. Este é, novamente, a questão da produtividade, já que o comando é compilado (otimizado) uma vez, e depois o trabalho está feito com uma versão compilada.

Bem, vamos direto à questão. Vamos observar para o próprio indicador BuySellVolume SQLite.mq5:

Listagem 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();
  }

Somente a classe da função foi alterada, o resto do código permaneceu inalterado.

Por enquanto a implementação da terceira versão do indicador está terminada - você pode ver o resultado.

Figura 5. O indicador BuySellVolume vinculado ao banco de dados do SQLite 3.5 no EURUSD M5

A propósito, ao contrário do Sql Server Management Studio, no SQLite não existem utilitários padrão para funcionar com os bancos de dados. Portanto, de modo a não trabalhar com uma "caixa preta", você pode baixar o utilitário apropriado de desenvolvedores terceiros. Pessoalmente, eu gosto do SQLiteMan - é fácil de usar e ao mesmo tempo tem toda a funcionalidade necessária. Você pode baixá-lo aqui: http://sourceforge.net/projects/sqliteman/.

Conclusão

Se você lê essas linhas, então tudo está encerrado. ;) Eu devo confessar que não esperava que esse artigo fosse ficar tão enorme. Portanto, as perguntas que certamente responderei, são inevitáveis.

Como vemos, cada solução tem os seus prós e contras. A primeira variante difere pela sua independência, a segunda - pelo seu desempenho, e a terceira - pela sua portabilidade. Qual delas escolher - depende de você.

O indicador implementado é útil? Cabe a você decidir. Quanto a mim - é um espécime muito interessante.

Ao fazê-lo, deixe eu me despedir. Até mais!

Descrição dos conteúdos dos arquivos:

# Nome do arquivo Descrição
1
Sources_en.zip
Contém os códigos-fonte de todos os indicadores e a biblioteca AdoSuite. Ele deve ser descompactado dentro da pasta apropriada do seu terminal. Objetivo dos indicadores: sem o uso do banco de dados (BuySellVolume.mq5) funcionando com o banco de dados do SQL Server 2008 (BuySellVolume SqlServer.mq5) e funcionando com o banco de dados do SQLite (BuySellVolume SQLite.mq5).
2
BuySellVolume-DB-SqlServer.zip
Arquivo do banco de dados do SQL Server 2008*.
3
BuySellVolume-DB-SQLite.zip
Arquivo do banco de dados do SQLite*.
4
System.Data.SQLite.zip
Arquivo System.Data.SQLite.dll, necessário para trabalhar com o banco de dados do SQLite.
5 Databases_MQL5_doc_en.zip Códigos-fonte, indicadores e o arquivo de documentação da biblioteca do AdoSuite.

* Ambos bancos de dados contém dados do indicador de ponto de 5 a 9 de Abril inclusive para os seguintes instrumentos: AUDNZD, EURUSD, GBPUSD, USDCAD, USDCHF, USDJPY.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/69

OOP no MQL5 por exemplo: Processando os avisos e os códigos de erro OOP no MQL5 por exemplo: Processando os avisos e os códigos de erro
O artigo descreve um exemplo de criação de classe para trabalho com os códigos de retorno do servidor de negócio e todos os erros que ocorrem durante a execução do programa MQL. Leia o artigo e você aprenderá como trabalhar com classes e objetos no MQL5. Ao mesmo tempo, esta é uma ferramenta conveniente para manipular erros; e você ainda pode mudar esta ferramenta de acordo com suas necessidades específicas.
Transferindo indicadores do MQL4 para o MQL5 Transferindo indicadores do MQL4 para o MQL5
Este artigo é dedicado às peculiaridades da transferência de construções de preço escritas no MQL4 para MQL5. Para facilitar o processo de transferência de cálculos do indicador do MQL4 para MQL5, é sugerida a biblioteca de funções mql4_2_mql5.mqh. Sua utilização é descrita com base na transferência de indicadores RSI, estocásticos e MACD.
Sistemas de negociação adaptativos e seu uso no terminal do cliente do MetaTrader 5 Sistemas de negociação adaptativos e seu uso no terminal do cliente do MetaTrader 5
Este artigo sugere uma variável de um sistema adaptativo que consiste em várias estratégias, cada uma realizando suas operações de negociação "virtuais". Negociações reais são realizadas de acordo com os sinais da estratégia mais rentável no momento. Obrigado por utilizar a abordagem orientada a objeto, classes para trabalhar com dados e classes de negociação da biblioteca padrão, a arquitetura do sistema pareceu ser simples e escalável; agora você pode facilmente criar e analisar os sistemas adaptativos que incluem centenas de estratégias de negociação.
Criando um jogo de "Snake" no MQL5 Criando um jogo de "Snake" no MQL5
Este artigo descreve um exemplo da programação do jogo "Snake". No MQL5, a programação do jogo tornou-se possível principalmente por causa dos recursos de manuseio de evento. A programação orientada a objeto simplifica em muito este processo. Neste artigo, você aprenderá os recursos de processamento de eventos, os exemplos de uso de classes da Biblioteca MQL5 Padrão e detalhes das chamadas de função periódica.