
Application Pratique des Bases de Données pour l'Analyse des Marchés
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!
# | 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





- Applications de trading gratuites
- Plus de 8 000 signaux à copier
- Actualités économiques pour explorer les marchés financiers
Vous acceptez la politique du site Web et les conditions d'utilisation