English Русский 中文 Español Deutsch 日本語 Português 한국어 Français Türkçe
Applicazione Pratica dei Database per l'Analisi dei Mercati

Applicazione Pratica dei Database per l'Analisi dei Mercati

MetaTrader 5Integrazione | 16 dicembre 2021, 10:20
56 0
Alexander
Alexander

Introduzione

Lavorare con i dati è diventato il compito principale del software moderno, sia per applicazioni standalone che di rete. Per risolvere questo problema è stato creato un software specializzato. Si tratta di Database Management Systems (DBMS), in grado di strutturare, sistematizzare e organizzare i dati per l'archiviazione e l'elaborazione del proprio computer. Questi software sono alla base delle attività informative in tutti i settori, dall'industria manifatturiera alla finanza e alle telecomunicazioni. 

Per quanto riguarda il trading, la maggior parte degli analisti non utilizza database nel proprio lavoro. Ma ci sono compiti in cui una soluzione del genere dovrebbe essere utile. 

Questo articolo copre una di queste attività: l'indicatore tick, che salva e carica i dati dal database.

BuySellVolume Algorithm 

BuySellVolume - questo semplice nome l'ho dato all'indicatore con un algoritmo ancora più semplice: prendi il tempo (t) e il prezzo (p) di due tick sequenziali (tick1 e tick2). Calcoliamo la differenza tra loro:

Δt = t2 - t1     (secondi)
Δp = p2 - p1    (punti)

Il valore del volume viene calcolato utilizzando questa formula:

v2 = p / Δt

Quindi, il nostro volume è direttamente proporzionale al numero di punti, di cui il prezzo si è spostato, ed è inversamente proporzionale al tempo speso per esso. Se Δt = 0, al suo posto viene preso il valore 0,5. Pertanto, otteniamo una sorta di valore dell'attività di acquirenti e venditori sul mercato. 

1. Implementazione dell'indicatore senza l'utilizzo del database

Penso che sarebbe logico prima considerare un indicatore con funzionalità specificate, ma senza interazione con il database. A mio parere, la soluzione migliore è creare una classe base, che esegua i calcoli appropriati, e le sue derivate per realizzare l'interazione con il database. Per implementarlo avremo bisogno della libreria AdoSuite. Quindi, fai clic sul collegamento e scaricalo.

Innanzitutto, crea il file BsvEngine.mqh e collega le classi di dati AdoSuite:

#include <Ado\Data.mqh>

Successivamente crea una classe di indicatori di base che implementerà tutte le funzioni necessarie, tranne il lavoro con il database. Sembrerà come segue:

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

Voglio notare che per aumentare la produttività della soluzione, i dati vengono inseriti negli appositi buffer (TickBuffer e VolumeBuffer), e quindi dopo un certo periodo di tempo vengono caricati nel database. 

Consideriamo l'ordine di implementazione della classe. Iniziamo con il costruttore:

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

Qui penso che tutto dovrebbe essere chiaro: le variabili sono inizializzate e le dimensioni iniziali dei buffer sono impostate.

Poi viene l'implementazione del metodo Init():

 Lista 1.3

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

Qui controlliamo se è possibile lavorare con il database. Nella classe base DbCheckAvailable() restituisce sempre false, perché il lavoro con il database verrà eseguito solo dalle classi derivate. Penso che potresti aver notato che le funzioni DbConnectionString(), DbCheckAvailable(), DbLoadData(), DbSaveData() non hanno ancora alcun significato speciale. Queste sono le funzioni che sovrascriviamo nei discendenti per associare a un database specifico. 

La lista 1.4 mostra l'implementazione della funzione ProcessTick(), che viene chiamata all'arrivo del nuovo teak, inserisce il teak nel buffer e calcola i valori per il nostro indicatore. Per fare ciò, alla funzione vengono passati 2 buffer di indicatori: uno viene utilizzato per memorizzare l'attività degli acquirenti, l'altro per memorizzare l'attività dei venditori. 

  Lista 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 funzione LoadData() carica i dati dal database per il timeframe corrente per un periodo di tempo specificato. 

  Lista 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() chiama la funzione DbLoadData(), che deve essere sovrascritta nei successori e restituire una tabella con tre colonne: l'ora della barra, il valore del buffer degli acquirenti e il valore del buffer dei venditori.

Qui viene utilizzata un'altra funzione: FindIndexByTime(). Al momento della stesura di questo articolo non ho trovato una funzione di ricerca binaria per le timeserie nella libreria standard, quindi l'ho scritta da solo.

E, infine, la funzione SaveData() per la memorizzazione dei dati: 

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

Come vediamo, nel metodo viene formata una tabella con le informazioni necessarie per l'indicatore e viene passata alla funzione DbSaveData(), che salva i dati nel database. Dopo la registrazione, svuotiamo semplicemente il buffer.

Quindi, il nostro framework è pronto - ora diamo un'occhiata alla Lista 1.7 come appare l'indicatore BuySellVolume.mq5: 

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

Molto semplice, secondo me. Nell'indicatore vengono chiamate solo due funzioni della classe: ProcessTick() e SaveData(). La funzione ProcessTick() viene utilizzata per i calcoli e la funzione SaveData() è necessaria per reimpostare il buffer con i tic, sebbene non salvi i dati.

Proviamo a compilare e "voilà": l'indicatore inizia a mostrare i valori:

 

 Figura 1. Indicatore BuySellVolume senza collegamento al database su GBPUSD M1

Eccellente! I tick stanno spuntando, l'indicatore sta calcolando. Il vantaggio di tale soluzione: abbiamo bisogno solo dell'indicatore stesso (ex5) per il suo lavoro e niente di più. Tuttavia, quando si modifica il timeframe, o lo strumento, o quando si chiude il terminale, i dati vengono irrimediabilmente persi. Per evitare ciò, vediamo come aggiungere il salvataggio e il caricamento dei dati nel nostro indicatore.

2. Collegamento a SQL Server 2008

Al momento ho due DBMSd installati sul mio computer: SQL Server 2008 e Db2 9.7. Ho scelto SQL Server, poiché presumo che la maggior parte dei lettori abbia più familiarità con SQL Server che con Db2.

Per cominciare, creiamo un nuovo database BuySellVolume per SQL Server 2008 (tramite SQL Server Management Studio o qualsiasi altro mezzo) e un nuovo file BsvMsSql.mqh, al quale includeremo il file con classe CBsvEngine base:

#include "BsvEngine.mqh"

SQL Server è dotato di driver OLE DB, quindi possiamo lavorarci tramite il provider OleDb, incluso nella libreria AdoSuite. Per fare ciò, includi le classi necessarie:

#include <Ado\Providers\OleDb.mqh>

E in realtà crea una classe derivata:

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

  };

Tutto ciò di cui abbiamo bisogno è sovrascrivere quattro funzioni che sono responsabili del lavoro diretto con il database. Cominciamo dall'inizio. Il metodo DbConnectionString() restituisce una stringa per connettersi al database.

Nel mio caso appare come segue:

Lista 2.2

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

Dalla stringa di connessione vediamo che lavoriamo tramite il driver MS SQL OLE-DB con il server SQLEXPRESS, situato sulla macchina locale. Ci stiamo connettendo al database BuySellVolume utilizzando l'autenticazione di Windows (altra opzione - inserire esplicitamente login e password).

Il passaggio successivo consiste nell'implementare la funzione DbCheckAvailable(). Ma prima, vediamo cosa dovrebbe fare davvero questa funzione.

Si diceva che controlla la possibilità di lavorare con il database. In un certo senso questo è vero. In effetti, il suo scopo principale è verificare se esiste una tabella per memorizzare i dati per lo strumento corrente, e se non c’è, crearla. Se queste azioni finiranno con un errore, restituirà false, ciò significherebbe che la lettura e la scrittura dei dati dell'indicatore dalla tabella verranno ignorate e l'indicatore funzionerà in modo simile a quello che abbiamo già implementato (vedi Lista 1.7).

Suggerisco di lavorare con i dati tramite stored procedure (SP) di SQL Server. Perché usarli? Volevo solo farlo. Questa è una questione di gusti ovviamente, ma penso che l'utilizzo di SP sia una soluzione più elegante rispetto a scrivere query nel codice (che richiedono anche più tempo per la compilazione, anche se non è applicabile a questo caso, poiché verranno utilizzate dinamiche query :)

Per DbCheckAvailable() la procedura memorizzata ha il seguente aspetto:

Lista 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

Vediamo che se la tabella desiderata non è nel database, viene formata ed eseguita una query dinamica (come una stringa), che crea una tabella. Quando viene creata la procedura memorizzata, è il momento di gestirla con la funzione DbCheckAvailable(): 

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

Come vediamo, siamo in grado di lavorare con le procedure memorizzate del server: dobbiamo solo impostare la proprietà CommandType su CMDTYPE_STOREDPROCEDURE, quindi passare i parametri necessari ed eseguire. Come è stato concepito, in caso di errore la funzione DbCheckAvailable restituirà false. 

Quindi, scriviamo una procedura memorizzata per la funzione DbLoadData. Poiché il database memorizza i dati per ogni tick, dobbiamo creare dati da essi per ogni barra del periodo richiesto. Ho eseguito la seguente procedura:

  Lista 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 

L'unica cosa da notare: il tempo di apertura della prima barra piena dovrebbe essere passato come @startTime, altrimenti otterremo l'offset.

Consideriamo l'implementazione DbLoadData() dal seguente elenco:

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

Qui chiamiamo stored procedure, strumenti di passaggio, data di inizio del calcolo, data di fine del calcolo e periodo del grafico corrente in minuti. Quindi utilizzando la classe COleDbDataAdapter stiamo leggendo il risultato nella tabella da cui verranno riempiti i buffer del nostro indicatore.

E il passaggio finale sarà l'implementazione di DbSaveData():

  Lista 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

Si prega di notare che l'xml con i dati dei tick memorizzati deve essere passato come parametro @ticks nella procedura. Questa decisione è stata presa per motivi di prestazioni: è più facile chiamare la procedura una volta e inviare lì 20 tick, piuttosto che chiamarla 20 volte , passando lì un tick. Vediamo come dovrebbe essere formata la stringa xml nella seguente lista: 

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

Buona metà di questa funzione prende la formazione di questa stessa stringa con xml. Inoltre, questa stringa viene trasferita alla procedura memorizzata e lì viene analizzata.

Per ora l'implementazione dell'interazione con SQL Server 2008 è terminata e possiamo implementare l'indicatore BuySellVolume SqlServer.mq5.

Come vedrai, l'implementazione di questa versione è simile all'implementazione dell'ultima, fatta eccezione per alcune modifiche che verranno discusse ulteriormente.

  Lista 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 prima differenza che colpisce l'occhio - la presenza del parametro di input StartTime. Questo parametro ha lo scopo di limitare l'intervallo di caricamento dei dati per l'indicatore. Il fatto è che una grande quantità di dati può richiedere molto tempo di calcolo, anche se in realtà i dati obsoleti non ci interessano.

La seconda differenza: il tipo della variabile bsv è cambiato in un altro.

È stata aggiunta la terza differenza: il caricamento dei dati sul primo calcolo dei dati dell'indicatore, nonché la funzione Init() in OnInit() e la funzione SaveData() in OnDeinit().

Ora proviamo a compilare l'indicatore e vediamo il risultato: 

 

Figura 2. L'indicatore BuySellVolume collegato al database SQL Server 2008 su EURUSD M15

Fatto! Ora i nostri dati sono salvati e possiamo passare liberamente da un timeframe all'altro.

3. Collegamento a SQLite 3.6

"Per rompere una mosca sulla ruota" - penso che tu capisca cosa intendo. Per questa attività la distribuzione di SQL Server è piuttosto ridicola. Naturalmente, se hai già installato questo DBMS e lo stai utilizzando attivamente, potrebbe essere l'opzione preferita. Ma cosa succede se vuoi dare un indicatore a qualcuno che è lontano da tutte queste tecnologie e vuole un minimo di sforzi affinché la soluzione funzioni?

Ecco la terza versione di indicator, che, a differenza delle precedenti, funziona con un database che ha un'architettura file-server.In questo approccio nella maggior parte dei casi avrai bisogno solo di un paio di DLL con il kernel del database.

Sebbene non avessi mai lavorato con SQLite prima, l'ho scelto per la sua semplicità, velocità e leggerezza. Inizialmente, abbiamo solo API per lavorare da programmi, scritte in C++ e TCL, ma ho anche trovato il driver ODBC e il provider ADO.NET di sviluppatori di terze parti. Poiché AdoSuite consente di lavorare con origini dati tramite ODBC, sembrerebbe meglio scaricare e installare il driver ODBC. Ma a quanto ho capito, il suo supporto è stato interrotto più di un anno fa e, inoltre, ADO.NET teoricamente dovrebbe funzionare più velocemente.

Quindi diamo un'occhiata a cosa deve essere fatto in modo da poter lavorare con SQLite tramite il provider ADO.NET dal nostro indicatore.

Due azioni ci porteranno al nostro obiettivo:

  • Innanzitutto, devi scaricare e installare il provider. Ecco il sito Web ufficiale http://sqlite.phxsoftware.com/, dove è disponibile il collegamento per il download. Da tutti questi file siamo interessati al montaggioSystem.Data.SQLite.dll.. Include il kernel SQLite stesso e il provider ADO.NET. Per comodità, ho allegato questa libreria all'articolo. Dopo il download, apri la cartella Windows\assembly in Windows Explorer (!). Dovresti vedere un elenco di assembly, come mostrato nella Figura 3:

 

Figura 3. Explorer può visualizzare la GAC (cache di assemblaggio globale) come un elenco


Ora trascina e rilascia (!) System.Data.SQLite.dll in questa cartella.

Di conseguenza, l'assembly viene inserito nella global assembly cache (GAC) e possiamo lavorarci:


Figura 4.  System.Data.SQLite.dll installato nel GAC

Per ora la configurazione del provider è completa.

  • La seconda azione preparatoria che dobbiamo fare è scrivere il provider AdoSuite per lavorare con SQLite. È scritto rapidamente e facilmente (per me ci sono voluti circa 15 minuti). Non pubblicherò qui il suo codice affinché l'articolo non diventi più grande. Puoi vedere il codice nei file, allegati a questo articolo.

Ora, quando tutto è fatto, puoi iniziare a scrivere un indicatore. Per il database SQLite creiamo un nuovo file vuoto nella cartella MQL5\Files. SQLite non è esigente per l'estensione del file, quindi chiamiamola semplicemente - BuySellVolume.sqlite.

Infatti, non è necessario creare il file: verrà creato automaticamente alla prima interrogazione del database, specificato nella stringa di connessione (vedi Lista 3.2). Qui lo creiamo esplicitamente solo per chiarire da dove viene.

Crea un nuovo file chiamato BsvSqlite.mqh, includi la nostra classe base e il provider per SQLite: 

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

 La classe derivata ha la stessa forma della precedente, tranne il nome:

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

  };

Ora procediamo con l'implementazione dei metodi.

Il DbConnectionString() ha il seguente aspetto:

    Lista 3.2

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

Come vedi, la stringa di connessione sembra molto più semplice e indica solo la posizione della nostra base.

Qui è indicato il percorso relativo, ma è consentito anche il percorso assoluto: "Data Source = c:\Program Files\Metatrader 5\MQL 5\Files\BuySellVolume.sqlite".

La lista 3.3 mostra il codice DbCheckAvailable(). Poiché SQLite non offre nulla di simile alle procedure memorizzate, ora tutte le query sono scritte direttamente nel codice:

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

Il risultato di questa funzione è identico all'equivalente per SQL Server. Una cosa che vorrei notare: sono i tipi di campi per la tabella. La cosa divertente è che i tipi di campi hanno poco significato per SQLite. Inoltre, non ci sono i tipi di dati DOUBLE e DATETIME (almeno, non sono inclusi in quelli standard). Tutti i valori vengono archiviati sotto forma di stringa e quindi convertiti dinamicamente nel tipo necessario.

Quindi qual è il punto nel dichiarare le colonne come DOUBLE e DATETIME? Non si conoscono le complessità dell'operazione, ma su query ADO.NET le converte automaticamente nei tipi DOUBLE e DATETIME. Ma questo non è sempre vero, poiché ci sono alcuni momenti, uno dei quali emergerà nell'elenco seguente.

Quindi, consideriamo l'elenco della seguente funzione DbLoadData():

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

Questa funzione funziona allo stesso modo della sua implementazione per MS SQL. Ma perché c'è il ciclo alla fine della funzione? Sì, in questa query magica tutti i miei tentativi di restituire il DATETIME non hanno avuto successo. L'assenza del tipo DATETIME in SQLite è evidente: al posto della data viene restituita la stringa nel formato AAAA-MM-GG hh:mm:ss. Ma può essere facilmente inserito in un form, comprensibile per la funzione StringToTime, e abbiamo usato questo vantaggio.

E, infine, la funzione DbSaveData():

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

Voglio coprire i dettagli di questa implementazione della funzione.

Innanzitutto, tutto è fatto nella transazione, sebbene sia logico. Ma questo non è stato fatto per motivi di sicurezza dei dati - è stato fatto per motivi di prestazioni: se una voce viene aggiunta senza una transazione esplicita, il server crea una transazione implicitamente, inserisce un record nella tabella e rimuove una transazione. E questo è fatto per ogni tick! Inoltre, l'intero database è bloccato durante la registrazione dell'ingresso! Vale la pena notare che i comandi non richiedono necessariamente la transazione. Ancora una volta, non ho compreso appieno il motivo per cui sta accadendo. Suppongo che ciò sia dovuto alla mancanza di transazioni multiple.

In secondo luogo, creiamo un comando una volta, quindi in un ciclo assegniamo parametri ed eseguiamo. Questo, ancora una volta, è il problema della produttività, poiché il comando viene compilato (ottimizzato) una volta e quindi il lavoro viene svolto con una versione compilata. 

Bene, veniamo al punto. Diamo un'occhiata all'indicatore BuySellVolume SQLite.mq5 stesso:

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

È cambiata solo la classe della funzione, il resto del codice è rimasto invariato.

Per ora l'implementazione della terza versione dell'indicatore è terminata: puoi visualizzare il risultato.

 

Figura 5. L'indicatore BuySellVolume collegato al database SQLite 3.6 su EURUSD M5

A proposito, a differenza di Sql Server Management Studio in SQLite non ci sono utilità standard per lavorare con i database. Pertanto, per non lavorare con "scatola nera", è possibile scaricare l'utilità appropriata da sviluppatori di terze parti. Personalmente, mi piace SQLiteMan: è facile da usare e allo stesso tempo ha tutte le funzionalità necessarie. Puoi scaricarlo da qui: http://sourceforge.net/projects/sqliteman/.

Conclusione

Se leggi queste righe, allora tutto è finito;). Devo confessare che non mi aspettavo che questo articolo fosse così lungo. Pertanto, le domande, alle quali risponderò sicuramente, sono inevitabili.

Come si vede, ogni soluzione ha i suoi pro e i suoi contro. La prima variante differisce per la sua indipendenza, la seconda per le sue prestazioni e la terza per la sua portabilità. Quale scegliere dipende da te.

L'indicatore implementato è utile? Stessa cosa sta a te decidere. Quanto a me, è un esemplare molto interessante.

Così facendo, lasciatemi dire addio. Ci vediamo!

Descrizione dei contenuti degli archivi:

 # Filename Descrizione
1
 Sources_en.zip
 Contiene i codici sorgente di tutti gli indicatori e la libreria AdoSuite. Dovrebbe essere decompresso nella cartella appropriata del tuo terminale. Scopo degli indicatori: senza utilizzo di database (BuySellVolume.mq5), utilizzo del database SQL Server 2008 (BuySellVolume SqlServer.mq5) e utilizzo del database SQLite (BuySellVolume SQLite.mq5).
2
 BuySellVolume-DB-SqlServer.zip
 Archivio database SQL Server 2008*
3
 BuySellVolume-DB-SQLite.zip
 Archivio database SQLite*
4
 System.Data.SQLite.zip
 Archivio System.Data.SQLite.dll, necessario per lavorare con il database SQLite
  5  Databases_MQL5_doc_en.zip  Codici sorgente, indicatori e archivio della documentazione della libreria AdoSuite

* Entrambi i database contengono dati sugli indicatori di tick dal 5 al 9 aprile inclusi per i seguenti strumenti: AUDNZD, EURUSD, GBPUSD, USDCAD, USDCHF, USDJPY.

Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/69

OOP in MQL5 come Esempio: Elaborazione dei Codici di Avviso e di Errore OOP in MQL5 come Esempio: Elaborazione dei Codici di Avviso e di Errore
L'articolo descrive un esempio di creazione di una classe per lavorare con i codici di ritorno del server di trading e tutti gli errori che si verificano durante l'esecuzione del programma MQL. Leggi l'articolo e imparerai come lavorare con classi e oggetti in MQL5. Allo stesso tempo, questo è uno strumento conveniente per la gestione degli errori; e puoi modificare ulteriormente questo strumento in base alle tue esigenze specifiche.
Trasferimento di Indicatori da MQL4 a MQL5 Trasferimento di Indicatori da MQL4 a MQL5
Questo articolo è dedicato alle peculiarità del trasferimento delle costruzioni di prezzo scritte in MQL4 a MQL5. Per facilitare il processo di trasferimento dei calcoli degli indicatori da MQL4 a MQL5, si suggerisce la libreria di funzioni mql4_2_mql5.mqh. Il suo utilizzo è descritto sulla base del trasferimento degli indicatori MACD, Stocastico e RSI.
MetaTrader 5: Pubblicazione di previsioni di trading e dichiarazioni di trading in tempo reale via e-mail su blog, social network e siti web dedicati MetaTrader 5: Pubblicazione di previsioni di trading e dichiarazioni di trading in tempo reale via e-mail su blog, social network e siti web dedicati
Questo articolo mira a presentare soluzioni pronte per la pubblicazione di previsioni utilizzando MetaTrader 5. Copre una vasta gamma di idee: dall'utilizzo di siti web dedicati per la pubblicazione di dichiarazioni MetaTrader, attraverso la creazione del proprio sito Web senza praticamente alcuna esperienza di programmazione web necessaria e infine l'integrazione con un servizio di microblogging di social network che consente a molti lettori di aderire e seguire le previsioni. Tutte le soluzioni presentate qui sono gratuite al 100% e possono essere configurate da chiunque abbia una conoscenza di base dei servizi di posta elettronica e ftp. Non ci sono ostacoli all'utilizzo delle stesse tecniche per l'hosting professionale e per i servizi di previsione del trading commerciale.
Creazione di un Gioco "Snack" in MQL5 Creazione di un Gioco "Snack" in MQL5
Questo articolo descrive un esempio di programmazione del gioco "Snake". In MQL5, la programmazione del gioco è diventata possibile principalmente grazie alle funzionalità di gestione degli eventi. La programmazione orientata agli oggetti semplifica notevolmente questo processo. In questo articolo imparerai le funzionalità di elaborazione degli eventi, gli esempi di utilizzo delle classi della libreria Standard MQL5 e i dettagli delle chiamate di funzione periodiche.