English Русский 中文 Español Deutsch 日本語 Português 한국어 Italiano Türkçe
Application Pratique des Bases de Données pour l'Analyse des Marchés

Application Pratique des Bases de Données pour l'Analyse des Marchés

MetaTrader 5Intégration | 16 novembre 2021, 15:50
114 0
Alexander
Alexander

Introduction

Travailler avec des données est devenu la tâche principale des logiciels modernes, à la fois pour les applications autonomes et en réseau. Pour résoudre ce problème, un logiciel spécialisé a été créé. Ce sont des Systèmes de Gestion de Bases de Données (SGBD), qui peuvent structurer, systématiser et organiser les données pour leur stockage et leur traitement informatique. Ces logiciels sont à la base des activités d'information dans tous les secteurs - de la fabrication à la finance et aux télécommunications. 

Quant au trading, la plupart des analystes n'utilisent pas de bases de données dans leur travail. Mais il y a des tâches où une telle solution devrait être pratique. 

Cet article couvre l'une de ces tâches : l'indicateur de coche, qui enregistre et charge les données de la base de données.

Algorithme BuySellVolume  

BuySellVolume - ce nom simple que j'ai attribué à l'indicateur avec un algorithme encore plus simple : prenez le temps (t) et le prix (p) de deux ticks séquentiels (tick1 et tick2). Calculons la différence entre eux :

Δt = t2 - t1     (secondes)
Δp = p2 - p1    (points)

La valeur du volume est calculée à l'aide de cette formule :

v2 = Δp / Δt

Ainsi, notre volume est directement proportionnel au nombre de points à partir duquel le prix a évolué, et est inversement proportionnel au temps passé pour cela. Si Δt = 0, alors au lieu de cela, la valeur 0,5 est prise. Ainsi, nous obtenons une sorte de valeur d'activité des acheteurs et des vendeurs sur le marché. 

1. Implémentation de l'indicateur sans utiliser la base de données

Je pense qu'il serait logique d’examiner d'abord un indicateur avec des fonctionnalités indiquées, mais sans interaction avec la base de données. À mon avis, la meilleure solution est de créer une classe de base, qui se chargera des calculs appropriés, et ses dérivés pour réaliser l'interaction avec la base de données. Pour implémenter cela, nous aurons besoin de la bibliothèque AdoSuite. Alors, cliquez sur le lien et téléchargez-le.

Tout d'abord, créez le fichier BsvEngine.mqh et connectez les classes de données AdoSuite :

#include <Ado\Data.mqh>

Créez ,ensuite, une classe d'indicateur de base, qui implémentera toutes les fonctions nécessaires, sauf le travail avec la base de données. Il ressemble à ce qui suit :

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

Je tiens à noter qu'afin d'augmenter la productivité de la solution, les données sont placées dans des tampons spéciaux (TickBuffer et VolumeBuffer), puis après un certain temps sont téléchargées dans la base de données. 

Examinons l'ordre d'implémentation des classes. Commençons par le constructeur :

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

Ici, je pense que tout doit être clair : les variables sont initialisées et les tailles initiales des tampons sont définies.

Vient ensuite l'implémentation de la méthode Init() :

 Liste 1.3

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

Ici, nous vérifions s'il est possible de travailler avec la base de données. Dans la classe de base , DbCheckAvailable() renvoie toujours false, car l'utilisation de la base de données ne sera effectuée qu'à partir de classes dérivées. Je pense que vous avez peut-être remarqué que les fonctions DbConnectionString(), DbCheckAvailable(), DbLoadData(), DbSaveData() n'ont pas encore d’importance particulière. Ce sont les fonctions que nous redéfinissons dans les descendants pour les lier à une base de données spécifique. 

Le listing 1.4 indique l'implémentation de la fonction ProcessTick(), qui est appelée à l'arrivée du nouveau teck, insère le teck dans le buffer et calcule les valeurs pour notre indicateur. Pour le faire, 2 tampons indicateurs sont transmis à la fonction : l'un est utilisé pour stocker l'activité des acheteurs, l'autre - pour stocker l'activité des vendeurs. 

  Liste 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 fonction LoadData() charge les données de la base de données pour la période actuelle pendant une période spécifiée. 

  Liste 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() appelle la fonction DbLoadData(), qui doit être remplacée dans les successeurs et retourner une table avec trois colonnes - le temps de barre, la valeur tampon des acheteurs et la valeur tampon des vendeurs.

Une autre fonction est utilisée ici - FindIndexByTime(). Au moment de rédiger cet article, je n'ai pas trouvé de fonction de recherche binaire pour les séries chronologiques dans la bibliothèque standard, je l'ai donc rédigée moi-même.

Et, enfin, la fonction SaveData() pour stocker les données : 

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

Comme nous le constatons, dans la méthode, un tableau est formé avec les informations nécessaires pour l'indicateur et il est transmis à la fonction DbSaveData(), qui enregistre les données dans la base de données. Après l'enregistrement, nous effaçons simplement le tampon.

Donc, notre cadre de travail est prêt - examinons maintenant la liste 1.7 à quoi ressemble l'indicateur BuySellVolume.mq5 : 

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

Très simple, à mon avis. Dans l'indicateur, seules deux fonctions de la classe sont appelées : ProcessTick() et SaveData(). La fonction ProcessTick() est utilisée pour les calculs et la fonction SaveData() est nécessaire pour réinitialiser le tampon avec des tics, bien qu'elle n'enregistre pas les données.

Essayons de compiler et "voila" - l'indicateur commence à afficher des valeurs :

 

 Figure 1. Indicateur BuySellVolume sans lien vers la base de données sur GBPUSD M1

Excellent Les tiques tournent, l'indicateur calcule. L'avantage d'une telle solution - nous n'avons besoin que de l'indicateur lui-même (ex5) pour son travail et rien de plus. Cependant, lors d'un changement de période, d'instrument ou lorsque vous fermez le terminal, les données sont irrémédiablement perdues. Pour éviter cela, voyons comment nous pouvons ajouter des données de sauvegarde et de chargement dans notre indicateur.

2. Lien vers SQL Server 2008

Pour le moment, j'ai deux DBMSd installés sur mon ordinateur - SQL Server 2008 et Db2 9.7. J'ai choisi SQL Server, car je suppose que la plupart des lecteurs sont plus familiers avec SQL Server qu'avec Db2.

Pour commencer, créons une nouvelle base de données BuySellVolume pour SQL Server 2008 (via SQL Server Management Studio ou tout autre moyen) et un nouveau fichier BsvMsSql.mqh, auquel nous inclurons le fichier avec la classe de base CBsvEngine :

#include "BsvEngine.mqh"

SQL Server est doté du pilote OLE DB, nous pouvons donc l'utiliser via le fournisseur OleDb, inclus dans la bibliothèque AdoSuite. Pour le faire, incluez les classes nécessaires :

#include <Ado\Providers\OleDb.mqh>

Et créez en fait une classe dérivée :

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

  };

Tout ce dont nous avons besoin - c'est de remplacer quatre fonctions, qui sont chargées de travailler directement avec la base de données. Commençons depuis le début. La méthode DbConnectionString() renvoie une chaîne pour se connecter à la base de données.

Dans mon cas, cela ressemble à ceci :

Liste 2.2

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

À partir de la chaîne de connexion, nous voyons que nous travaillons via le pilote MS SQL OLE-DB avec le serveur SQLEXPRESS, situé sur la machine locale. Nous nous connectons à la base de données BuySellVolume à l'aide de l'authentification Windows (autre option - entrez explicitement l’identifiant et le mot de passe).

L'étape suivante consiste à implémenter la fonction DbCheckAvailable(). Mais d'abord, voyons ce que devrait vraiment faire cette fonction.

Il a été dit qu'elle vérifie la possibilité de travailler avec la base de données. Dans une certaine mesure, c'est vrai. En fait, son objectif principal - est de vérifier s'il existe un tableau pour stocker les données pour l'instrument actuel, et si ce n'est pas le cas - de le créer. Si ces actions se terminent par une erreur, elle renverra false, cela signifierait que la lecture et l'écriture des données d'indicateur du tableau seront ignorées, et l'indicateur fonctionnera de la même manière que nous avons déjà implémenté (voir Listing 1.7).

Je suggère de travailler avec des données via des procédures stockées (SP) de SQL Server. Pourquoi les utiliser ? Je voulais juste. C'est une question de goût bien sûr, mais je pense que l'utilisation des SP est une solution plus élégante que de rédiger des requêtes dans le code (qui nécessitent également plus de temps pour compiler, bien que ce ne soit pas applicable à ce cas, car les requêtes dynamiques seront utilisées :)

Pour DbCheckAvailable(), la procédure stockée se présente comme suit :

Liste 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

Nous constatons que si le tableau souhaite n'est pas dans la base de données, une requête dynamique (sous forme de chaîne), qui crée un tableau, est formé et exécuté. Lorsque la procédure stockée est créée, il est temps de gérer avec la fonction DbCheckAvailable() : 

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

Comme nous le constatons, nous sommes capables de travailler avec les procédures stockées du serveur - nous avons juste besoin de définir la propriété CommandType sur CMDTYPE_STOREDPROCEDURE, puis de passer les paramètres nécessaires et d'exécuter. Comme il a été conçu, en cas d'erreur, la fonction DbCheckAvailable retournera false. 

Ensuite, rédigeons une procédure stockée pour la fonction DbLoadData. Étant donné que la base de données stocke des données pour chaque tick, nous devons en créer des données pour chaque barre de période requise. J'ai établi la procédure suivante :

  Liste 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 

La seule chose à noter - l'heure d'ouverture de la première barre remplie doit être transmise en tant que @startTime, sinon nous obtiendrons le décalage.

Examinons l'implémentation de DbLoadData() à partir de la liste suivante :

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

Ici, nous appelons la procédure stockée, les outils de transmission, la date de début du calcul, la date de fin du calcul et la période actuelle du graphique en minutes. Ensuite, en utilisant la classe COleDbDataAdapter, nous lisons le résultat dans le tableau, à partir duquel les tampons de notre indicateur seront remplis.

Et la dernière étape sera d'implémenter le DbSaveData() :

  Liste 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

Veuillez noter que le xml avec les données de ticks stockées doit être transmis en tant que paramètre @ticks dans la procédure. Cette décision a été prise pour des raisons d’efficacité - il est plus facile d'appeler la procédure une fois et d’y envoyer 20 ticks, que de l'appeler 20 fois , en y transmettant une coche. Voyons comment la chaîne xml doit être créée dans la liste suivante : 

Liste 2.8

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

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

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

      StringAdd(xml,"<Tick>");

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

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

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

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

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

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

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

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

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

   conn.Open();

// executing stored procedure
   cmd.ExecuteNonQuery();

   conn.Close();

   delete cmd;
   delete conn;

   ResetAdoError();
  }

La bonne partie de cette fonction prend la formation de cette même chaîne avec xml. En outre, cette chaîne est transmise à la procédure stockée et y est analysée.

Pour l'instant, l'implémentation de l'interaction avec SQL Server 2008 est terminée, et nous pouvons implémenter l'indicateur BuySellVolume SqlServer.mq5.

Comme vous le verrez, l’implémentation de cette version est similaire à l’implémentation de la dernière, à l'exception de quelques modifications qui seront discutées plus loin.

  Liste 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 première différence qui frappe à l'œil - la présence du paramètre d'entrée StartTime. Ce paramètre est destiné à limiter l'intervalle de chargement des données pour l'indicateur. Le fait est qu'une grande quantité de données peut prendre un temps de calcul long, bien qu'en fait les données obsolètes ne nous intéressent pas.

La deuxième différence - le type de la variable bsv est modifié vers un autre.

La troisième différence - le chargement des données sur le premier calcul des données de l'indicateur a été ajouté, ainsi que la fonction Init() dans OnInit() et la fonction SaveData() dans OnDeinit().

Essayons maintenant de compiler l'indicateur et d’examiner le résultat : 

 

Figure 2. L'indicateur BuySellVolume lié à la base de données SQL Server 2008 sur EURUSD M15

Terminé! Désormais, nos données sont enregistrées et nous pouvons librement basculer entre les périodes.

3. Lien vers SQLite 3.6

"La montagne qi couche d’une souris" - Je pense que vous comprenez ce que je veux dire. Pour cette tâche, le déploiement de SQL Server est plutôt ridicule. Bien sûr, si vous avez déjà installé ce SGBD et que vous l'utilisez activement, cela peut être l'option préférée. Mais que faire si vous souhaitez donner un indicateur à quelqu'un qui est loin de toutes ces technologies et qui souhaite un minimum d'efforts pour que la solution fonctionne ?

Voici la troisième version de l'indicateur qui, contrairement aux précédentes, fonctionne avec une base de données dotée d'une architecture de serveur de fichiers. Dans cette approche, dans la plupart des cas, vous n'aurez besoin que de quelques DLL avec le noyau de la base de données.

Bien que je n'avais jamais travaillé avec SQLite auparavant, je l'ai choisi pour sa simplicité, sa rapidité et sa légèreté. Au départ, nous n'avons qu'une API pour travailler à partir de programmes, écrits en C++ et TCL, mais j'ai également trouvé le pilote ODBC et le fournisseur ADO.NET de développeurs tiers. Puisque AdoSuite permet de travailler avec des sources de données via ODBC, cela semble préférable de télécharger et d'installer le pilote ODBC. Mais si j'ai bien compris, sa prise en charge a été interrompue il y a plus d'un an et, de plus, ADO.NET devrait théoriquement fonctionner plus rapidement.

Voyons donc ce qu'il faut faire pour pouvoir travailler avec SQLite via le fournisseur ADO.NET à partir de notre indicateur.

Deux actions nous amèneront à notre objectif :

  • Tout d'abord, vous devez télécharger et installer le fournisseur. Voici le site Web officiel http://sqlite.phxsoftware.com/, où le lien de téléchargement est disponible. De tous ces fichiers, nous nous intéressons au System.Data.SQLite.dll. Assemblée. Il comprend le noyau SQLite lui-même et le fournisseur ADO.NET. Pour plus de commodité, j'ai joint cette bibliothèque à l'article. Après le téléchargement, ouvrez le dossier Windows\assembly dans l'Explorateur Windows (!). Vous devriez voir une liste d'assemblages, comme l’indique la figure 3 :

 

Figure 3. L'explorateur peut afficher le GAC (global assembly cache) sous forme de liste


Maintenant, glissez-déposez (!) System.Data.SQLite.dll dans ce dossier.

En conséquence, l'assemblage est placé dans le Global Assembly Cache (GAC), et nous pouvons travailler avec :


Figure 4.  System.Data.SQLite.dll installé dans le GAC

Pour l'instant, la configuration du fournisseur est terminée.

  • La deuxième action préparatoire que nous devons entreprendre est de rédiger le fournisseur AdoSuite pour qu'il fonctionne avec SQLite. Il est rédigé rapidement et facilement (pour moi, cela a pris environ 15 minutes). Je ne posterai pas son code ici pour que l'article ne devienne pas plus grand Vous pouvez voir le code dans les fichiers joints à cet article.

Maintenant, lorsque tout est terminé, vous pouvez entamer la rédaction un indicateur. Pour la base de données SQLite, créons un nouveau fichier vide dans le dossier MQL5\Files. SQLite n'est pas difficile pour l'extension de fichier,donc, appelons-le simplement - BuySellVolume.sqlite.

En fait, il n'est pas nécessaire de créer le fichier : il sera automatiquement créé quand vous demandez pour la première fois la base de données, indiquée dans la chaîne de connexion (voir l'extrait 3.2). Ici, nous ne le créons explicitement que pour indiquer clairement d'où il vient.

Créez un nouveau fichier appelé BsvSqlite.mqh, incluez notre classe de base et notre fournisseur pour SQLite : 

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

 La classe dérivée dispose de la même forme que la précédente, à l'exception du nom :

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

  };

Passons maintenant à l'implémentation des méthodes.

Le DbConnectionString() se présente comme suit :

    Liste 3.2

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

Comme vous le constatez, la chaîne de connexion semble beaucoup plus simple et indique uniquement l'emplacement de notre base.

Ici le chemin relatif est indiqué, mais aussi le chemin absolu est autorisé : "Data Source = c:\Program Files\Metatrader 5\MQL 5\Files\BuySellVolume.sqlite".

Le listing 3.3 indique le code DbCheckAvailable(). Étant donné que SQLite ne nous offre rien de comparable aux procédures stockées, toutes les requêtes sont désormais rédigées directement dans le code :

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

Le résultat de cette fonction est identique à l'équivalent pour SQL Server. Une chose que je souhaiterais noter - ce sont les types de champs pour la table. Ce qui est amusant, c'est que les types de champs ne sont pas d’une grande importance pour SQLite. De plus, il n'y a pas de types de données DOUBLE et DATETIME (au moins, ils ne sont pas inclus dans les types standard). Toutes les valeurs sont stockées sous forme de chaîne, puis converties dynamiquement dans le type requis.

Alors, quel est l'intérêt de déclarer des colonnes comme DOUBLE et DATETIME ? Je ne connais pas les subtilités de l'opération, mais sur requête, ADO.NET les convertit automatiquement en types DOUBLE et DATETIME. Mais ce n'est pas toujours vrai, car il y a des moments, dont l'un apparaîtra dans la liste suivante.

Examinons donc la liste de la fonction DbLoadData() suivante :

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

Cette fonction fonctionne de la même manière que son implémentation pour MS SQL. Mais pourquoi y a-t-il la boucle à la fin de la fonction ? Oui, dans cette requête magique, toutes mes tentatives pour renvoyer le DATETIME ont échoué. L'absence de type DATETIME dans SQLite est évidente - au lieu de la date, la chaîne au format AAAA-MM-JJ hh:mm:ss est renvoyée. Mais elle peut facilement être convertie dans un formulaire, ce qui est compréhensible pour la fonction StringToTime, et nous avons profité de cet avantage.

Et, enfin, la fonction DbSaveData() :

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

Je souhaite couvrir les détails de cette implémentation de fonction.

Tout d'abord, tout se fait dans la transaction, même si c'est logique. Mais cela n'a pas été fait pour des raisons de sécurité des données - cela a été fait pour des raisons d’efficacité : si une entrée est ajoutée sans transaction explicite, le serveur crée une transaction implicitement, insère un enregistrement dans le tableau et supprime une transaction. Et c'est fait pour chaque tique ! De plus, toute la base de données est verrouillée lors de l'enregistrement de l'entrée ! Il convient de noter que les commandes n'ont pas nécessairement besoin de transaction. Encore une fois, je n'ai pas entièrement compris pourquoi cela se produit. Je suppose que cela est dû au manque de transactions multiples.

Deuxièmement, nous créons une commande une fois, puis dans une boucle, nous attribuons des paramètres et l'exécutons. C'est encore une fois la question de la productivité, car la commande est compilée (optimisée) une fois, puis le travail est effectué avec une version compilée. 

Bon, allons droit au but. Examinons l'indicateur BuySellVolume SQLite.mq5 lui-même :

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

Seule la classe de fonction a évolué, le reste du code est resté inchangé.

Pour l'instant, l’implémentation de la troisième version de l'indicateur est terminée - vous pouvez voir le résultat.

 

Figure 5. L'indicateur BuySellVolume lié à la base de données SQLite 3.6 sur EURUSD M5

Au passage, contrairement à Sql Server Management Studio dans SQLite, il n'y a pas d'utilitaires standard pour travailler avec les bases de données. Par conséquent, afin de ne pas travailler avec la "boîte noire", vous pouvez télécharger l'utilitaire approprié auprès de développeurs tiers. Personnellement, j'aime SQLiteMan - il est facile à utiliser et dispose en même temps de toutes les fonctionnalités nécessaires. Vous pouvez le télécharger ici :sourceforge.net/projects/sqliteman/.

Conclusion

Si vous lisez ces lignes, alors tout est fini ;). Je dois avouer que je ne m'attendais pas à ce que cet article soit si volumineux. Par conséquent, les questions, auxquelles je répondrai certainement, sont inévitables.

Comme nous le constatons, chaque solution a ses avantages et ses inconvénients. La première variante se distingue par son indépendance, la seconde - par ses performances, et la troisième - par sa portabilité. Lequel choisir - c'est à vous de décider.

L’indicateur implémenté est-t-il utile ? Idem à vous de décider. Quant à moi - c'est un spécimen très intéressant.

Ce faisant, permettez-moi de vous dire au revoir. À plus tard!

Description du contenu des archives :

 # Nom de fichier Description
1
 Sources_en.zip
 Comprend les codes sources de tous les indicateurs et de la bibliothèque AdoSuite. Il doit être décompressé dans le dossier approprié de votre terminal. Objectif des indicateurs : sans utilisation de la base de données (BuySellVolume.mq5), fonctionnant avec la base de données SQL Server 2008 (BuySellVolume SqlServer.mq5) et fonctionnant avec la base de données SQLite (BuySellVolume SQLite.mq5).
2
 BuySellVolume-DB-SqlServer.zip
 Archive de la base de données SQL Server 2008*
3
 BuySellVolume-DB-SQLite.zip
 Archive de la base de données SQLite*
4
 System.Data.SQLite.zip
 Archive System.Data.SQLite.dll, nécessaire pour travailler avec la base de données SQLite
  5  Databases_MQL5_doc_en.zip  Codes sources, indicateurs et archive de documentation de la bibliothèque AdoSuite

* Les deux bases de données comportent des données d'indicateurs de tick du 5 au 9 avril inclus pour les instruments suivants : AUDNZD, EURUSD, GBPUSD, USDCAD, USDCHF, USDJPY.

Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/69

POO en MQL5 par exemple : Traitement des Codes d'Avertissement et d'Erreur POO en MQL5 par exemple : Traitement des Codes d'Avertissement et d'Erreur
L'article décrit un exemple de création d'une classe pour travailler avec les codes de retour du serveur trade et toutes les erreurs qui se produisent lors de l'exécution du programme MQL. Lisez l'article et vous apprendrez à travailler avec des classes et des objets en MQL5. En même temps, c'est un outil pratique pour gérer les erreurs ; et vous pouvez modifier davantage cet outil en fonction de vos besoins spécifiques.
Transfert d'indicateurs de MQL4 vers MQL5 Transfert d'indicateurs de MQL4 vers MQL5
Cet article est dédié aux spécificités du transfert des constructions de prix rédigées en MQL4 vers MQL5. Pour rendre le processus de transfert des calculs d'indicateurs de MQL4 à MQL5 plus facile, la bibliothèque de fonctions mql4_2_mql5.mqh est suggérée. Son usage est décrit sur la base du transfert des indicateurs MACD, Stochastique et RSI.
MetaTrader 5 Publication des prévisions de trading et des relevés de trading en direct par e-mail sur les blogs, les réseaux sociaux et les sites internet dédiés MetaTrader 5 Publication des prévisions de trading et des relevés de trading en direct par e-mail sur les blogs, les réseaux sociaux et les sites internet dédiés
Cet article vise à présenter des solutions prêtes à l'emploi pour la publication de prévisions à l'aide de MetaTrader 5. Il couvre un éventail d'idées : de l'utilisation de sites Web dédiés pour la publication des relevés MetaTrader à la création de son propre site Web sans pratiquement aucune expérience de programmation Web nécessaire et enfin à l'intégration à un service de micro-blogging de réseau social qui permet à de nombreux lecteurs de rejoindre et de suivre les prévisions. Toutes les solutions présentées ici sont 100% gratuites et peuvent être configurées par toute personne ayant une connaissance de base des services e-mail et ftp. Il n'y a aucun obstacle à utiliser les mêmes techniques pour les services d'hébergement professionnel et de prévision des échanges de trade.
Créer un jeu "Serpent" en MQL5 Créer un jeu "Serpent" en MQL5
Cet article décrit un exemple de programmation de jeu "Snake". Dans MQL5, la programmation du jeu est devenue possible principalement grâce aux fonctionnalités de gestion des événements. La programmation orientée-objet simplifie considérablement ce processus. Dans cet article, vous allez apprendre les fonctionnalités de traitement des événements, les exemples d'utilisation des classes Standard MQL5 Library et les détails des appels de fonction périodiques.