English Русский 中文 Español 日本語 Português 한국어 Français Italiano Türkçe
Praktische Anwendung von Datenbanken für die Marktanalyse

Praktische Anwendung von Datenbanken für die Marktanalyse

MetaTrader 5Integration | 8 März 2016, 08:40
804 0
Alexander
Alexander

Einleitung

Die Arbeit mit Daten ist zur Hauptaufgabe moderner Software geworden, sowohl autonomer als auch vernetzter Programme. Dazu wurde eine besondere Form von Software entwickelt. Diese Programme zur Verwaltung von Datenbanken (DBMS) ermöglichen die Strukturierung, Systematisierung und Organisation von Daten für ihre Speicherung und Verarbeitung auf Computern. Sie bilden die Grundlage für die Arbeit mit Informationen in allen Bereichen, von der Fertigung bis hin zum Finanzwesen und der Telekommunikation. 

Was den Börsenhandel betrifft, nutzen nur die wenigsten Analysten Datenbanken (DB) für ihre Arbeit. Es gibt jedoch Aufgaben, für die eine solche Lösung geeignet sein müsste. 

In diesem Beitrag befasse ich mich mit folgender Aufgabe: einem „Tick“-Indikator, der Daten in einer DB speichert und aus dieser lädt.

Der Algorithmus BuySellVolume 

BuySellVolume, ich habe mich für diese schnörkellose Bezeichnung für einen Indikator mit einem noch schnörkelloseren Algorithmus entschieden: wir nehmen dazu die Zeit (t) und den Kurs (p) zweier aufeinander folgender Kursänderungen (Tick1 und Tick2). Und berechnen die Differenz zwischen ihnen:

Δt = t2 - t1     (Sekunden)
Δp = p2 - p1    (Punkte)

Die Berechnung erfolgt anhand der Formel:

v2 = Δp / Δt

Es ergibt sich, dass unser Umfang unmittelbar proportional zur Anzahl der Punkte ist, um die sich der Kurs geändert hat, und umgekehrt proportional zu der dafür benötigten Zeit. Wenn Δt = 0, dann wird an seiner Stelle der Wert 0,5 genommen. So erhalten wir in gewisser Hinsicht einen Wert für die Aktivität der Käufer und Verkäufer auf dem Markt. 

1. Umsetzung eines Indikators ohne Datenbanknutzung

Ich halte es für naheliegend, zunächst einen Indikator mit besagter Funktion, jedoch ohne Datenbankanbindung zu untersuchen. Meiner Ansicht nach besteht die beste Lösung in der Erstellung einer Basisklasse zur Ausführung der entsprechenden Berechnungen sowie darin in ihren Ableitungen bereits die Anbindung an eine DB anzulegen. Dazu benötigen wir die Bibliothek AdoSuite, die wir durch Anklicken der Verknüpfung herunterladen.

Zunächst legen wir die Datei BsvEngine.mqh an und binden die Datenklassen der AdoSuite ein:

#include <Ado\Data.mqh>

Danach schreiben wir die Basisklasse des Indikators, die alle erforderlichen Funktionen mit Ausnahme der Zusammenarbeit mit einer Datenbank, umsetzen wird. Das sieht dann so aus:

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

Ich mache darauf aufmerksam, dass die Daten zur Steigerung der Leistungsfähigkeit der Lösung in besonderen Puffern (dem TickBuffer und dem VolumeBuffer) zwischengespeichert und von dort nach einer gewissen Zeit in eine Datenbank ausgelagert werden. 

Sehen wir uns die Umsetzung der Klasse Schritt für Schritt an. Wir beginnen mit dem Konstruktor:

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

Hier sollte meiner Ansicht nach alles klar sein: die Variablen werden bereitgestellt und die ursprüngliche Größe der Puffer festgelegt.

Als Nächstes wird die Umsetzung der Methode Init() vorgestellt:

 Aufstellung 1.3

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

Hier wird geprüft, ob mit der Datenbank gearbeitet werden kann. In der Basisklasse DbCheckAvailable() wird stets „false“ ausgegeben, da die Arbeit mit Datenbanken ausschließlich aus abgeleiteten Klassen erfolgt. Sie werden sicher bemerkt haben, dass die Funktionen DbConnectionString(), DbCheckAvailable(), DbLoadData() und DbSaveData() keine besonderen Inhalte aufweisen. Es handelt sich bei ihnen um eben die Funktionen, die in ihren „Nachkommen“ zur Anbindung an eine konkrete Datenbank (DB) überarbeitet werden. 

Aufstellung 1.4 zeigt die Umsetzung der Funktion ProcessTick(), die bei Eintreten einer neuen Kursänderung (eines neuen „Ticks“) aufgerufen wird, diesen in den Puffer einträgt und die entsprechenden Werte für unseren Indikator berechnet. Dazu werden 2 Indikatorpuffer auf die Funktion übertragen, einer zur Speicherung der Aktivität der Käufer und ein zweiter zur Speicherung der Aktivität der Verkäufer. 

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

Die Funktion LoadData() lädt die Daten für eine festgelegte Zeit aus der Datenbank für den aktuellen Zeitraum. 

  Aufstellung 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() ruft die Funktion DbLoadData() auf, die in den Nachkommen überarbeitet werden und eine Tabelle mit drei Spalten, für die Balkenzeit, sowie für die Werte des Käufer- und des Verkäuferpuffers ausgeben muss.

Eine weitere hier verwendete Funktion ist FindIndexByTime(). Als dieser Beitrag verfasst wurde, war in der Standardbibliothek keine binäre Suchfunktion für Zeitreihen zu finden, also habe ich selbst eine geschrieben.

Und zu guter Letzt die Funktion SaveData() zur Speicherung der Daten. 

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

Wie zu sehen wird in der Methode eine Tabelle mit den für den Indikator erforderlichen Informationen angelegt und zur Speicherung dieser Daten in der Datenbank an die Funktion DbSaveData() weitergegeben. Nach der Aufzeichnung wird der Puffer einfach geleert.

So, damit hätten wir das Skelett angelegt, jetzt sehen wir uns in der Aufstellung 1.7 an, wie der Indikator BuySellVolume.mq5 selbst aussieht: 

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

Einfacher geht‘s nicht, finde ich. In dem Indikator werden lediglich zwei Funktionen aus der angelegten Klasse aufgerufen: ProcessTick() und SaveData(). In der Funktion ProcessTick() werden die Berechnungen ausgeführt, während die Funktion SaveData(), auch wenn sie keine Daten speichert, zur Leerung des „Tick“-Puffers benötigt wird.

Versuchen wir, alles zusammenzustellen, und siehe da, der Indikator beginnt Werte anzuzeigen:

 

 Abbildung 1. Der Indikator BuySellVolume ohne Anbindung an die Datenbank zu GBPUSD M1

Ausgezeichnet! Die Ticks ticken, und der Indikator rechnet. Der Vorteil einer solchen Lösung besteht darin, dass für die Arbeit des Indikators nur der Indikator (ex5) selbst benötigt wird und sonst nichts. Allerdings gehen die Daten bei einem Wechsel des Zeitraums oder des Kürzels bzw. beim Schließen des Programms unwiederbringlich verloren. Um das zu vermeiden, lassen Sie uns sehen, wie wir unseren Indikator um die Speicherung und den Abruf der Daten erweitern können.

2. Anbindung an SQL Server 2008

Zurzeit sind auf meinem Rechner zwei DBMS installiert, SQL Server 2008 und Db2 9.7. Ich habe mich für den SQL Server entschieden, weil ich davon ausgehe, dass mehr Leser sich mit SQL Server auskennen als mit Db2.

Für den Anfang legen wir die neue Datenbank BuySellVolume für SQL Server 2008 (über SQL Server Management Studio oder auf eine andere verfügbare Weise) sowie die neue Datei BsvMsSql.mqh an, in die wir die Datei mit der Basisklasse CBsvEngine einbetten:

#include "BsvEngine.mqh"

SQL Server ist mit einem OLE DB-Treiber ausgestattet, weshalb wir die Arbeit mit ihm über die in der AdoSuite-Bibliothek enthaltene OLE DB-Anbieter-Schnittstelle abwickeln können. Dazu binden wir die erforderlichen Klassen ein:

#include <Ado\Providers\OleDb.mqh>

An sich schaffen wir eine abgeleitete Klasse:

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

  };

Alles, was wir tun müssen, ist die vier Funktionen zu überschreiben, die für die Arbeit mit Datenbanken unmittelbar zuständig sind. Aber eins nach dem anderen. Die Methode DbConnectionString() gibt die Zeile für die Anbindung der Datenbank (DB) aus.

In meinem Fall sieht sie folgendermaßen aus:

Aufstellung 2.2

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

Aus der Anbindungszeile können wir ersehen, dass wir über den OLE DB-Treiber für SQL von Microsoft mit dem Server SQLEXPRESS auf dem lokalen Rechner arbeiten. Wir werden mithilfe der Windows-Authentifizierung (oder einem anderen Verfahren, der ausdrücklichen Eingabe von Benutzernamen und Passwort) mit der Datenbank BuySellVolume verbunden.

Die nächste Etappe besteht in der Umsetzung der Funktion DbCheckAvailable(). Aber zunächst müssen wir klären, was diese Funktion eigentlich bewirken soll.

Wir haben gesagt, dass sie prüft, ob mit der Datenbank gearbeitet werden kann. Zum Teil stimmt das. In der Tat besteht ihr Hauptzweck in der Prüfung, ob eine Tabelle zur Speicherung der Daten zu dem jeweiligen Kürzel vorhanden ist, und, wenn nicht, eine solche anzulegen. Wenn bei der Ausführung dieser Operationen ein Fehler auftritt, gibt sie den Wert „false“ aus, was bedeutet, dass die Eingabe und das Auslesen der Indikatordaten in und aus der Tabelle ignoriert wird, und der Indikator ähnlich arbeiten wird wie der den wir bereits umgesetzt haben (s. Aufstellung 1.7).

Für die Arbeit mit den Daten schlage ich die Verwendung der gespeicherten Prozeduren (SP) von SQL Server vor. Warum gerade sie? Ich will es eben. Das ist natürlich reine Geschmackssache, aber ich halte die Verwendung der SP für eine elegantere Lösung, als die Abfragen in Code zu programmieren (was zudem mehr Zeit bei der Kompilierung in Anspruch nimmt, auch wenn das in diesem Fall nicht zutrifft, weil hier dynamische Abfragen verwendet werden:)

Die SP für DbCheckAvailable() sieht so aus:

Aufstellung 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

Wir sehen, dass bei Nichtvorhandensein der benötigten Tabelle in der Datenbank eine dynamische Abfrage (in Form einer Zeile) angelegt und ausgeführt wird, die diese Tabelle erstellt. Wenn die gespeicherte Prozedur erzeugt worden ist, wird es höchste Zeit, sich mit der Funktion DbCheckAvailable() zu befassen: 

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

Wie wir sehen, haben wir die Möglichkeit, mit den gespeicherten Prozeduren des Servers zu arbeiten, wir müssen lediglich die Eigenschaft CommandType des Befehls auf CMDTYPE_STOREDPROCEDURE einstellen und die erforderlichen Parameter übertragen, dann kann er ausgeführt werden. Wie beabsichtigt gibt die Funktion DbCheckAvailable bei einem Fehler den Wert „false“ aus. 

Weiterhin werden wir die gespeicherte Prozedur für die Funktion DbLoadData schreiben. Da in der Datenbank die Daten zu jeder Kursänderung (jedem Tick) gespeichert werden, müssen diese in Daten für jeden Balken des erforderlichen Zeitraums umgewandelt werden. Bei mir ist dabei diese Prozedur herausgekommen:

  Aufstellung 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 

Das Einzige, worauf dabei hinzuweisen ist, dass als @startTime der Zeitpunkt der Eröffnung des ersten gefüllten Balkens übermittelt werden muss, andernfalls würde sich eine Abweichung ergeben.

Sehen wir uns die Umsetzung der Funktion DbLoadData() anhand der folgenden Aufstellung an:

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

Hier werden die gespeicherte Prozedur aufgerufen und das Kürzel, Anfangs- wie Enddatum der Berechnung sowie der aktuelle Diagrammzeitraum in Minuten übermittelt. Anschließend lesen wir mithilfe der Klasse COleDbDataAdapter das Ergebnis in eine Tabelle ein, aus der auch die Puffer unseres Indikators gefüllt werden.

Der letzte Schritt ist die Umsetzung des Funktionsbestandes für DbSaveData():

  Aufstellung 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

Beachten Sie bitte, dass als Parameter @ticks eine XML-Datei mit den Daten bezüglich der gespeicherten Kursänderungen („Ticks“) in die Prozedur weitergegeben werden muss. Diese Entscheidung erfolgt aus Gründen der Leistungsfähigkeit, da es einfacher ist, eine Prozedur einmal aufzurufen und 20 „Ticks“ an sie weiterzugeben, als sie 20 Mal aufzurufen und jeweils nur eine Kursänderung weiterzugeben. Die folgende Aufstellung zeigt, wie die Zeile mit der XML-Datei angelegt wird. 

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

Etwa die Hälfte der Funktion nimmt nämlich ausgerechnet die Bildung eben dieser Zeile mit der XML-Datei ein. Des Weiteren geht diese Zeile in eine gespeicherte Prozedur ein und wird dort zergliedert.

Damit ist die Umsetzung der Wechselbeziehung zu SQL Server 2008 abgeschlossen, und wir können die Umsetzung des Indikators BuySellVolume SqlServer.mq5 in Angriff nehmen.

Wie Sie sehen werden, ähnelt die Umsetzung dieser Fassung derjenigen der vorhergehenden, mit Ausnahme einiger Änderungen, von denen jetzt die Rede sein wird.

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

Der erste Unterschied, der ins Auge fällt, ist der Eingangsparameter StartTime. Dieser Parameter dient zur Begrenzung des Zeitraums, aus dem die Daten für den Indikator geladen werden. Es geht darum, dass die Berechnung bei einer großen Datenmenge viel Zeit in Anspruch nehmen kann, obwohl uns faktisch veraltete Daten gar nicht interessieren.

Der zweite Unterschied besteht darin, dass sich die Art der Variablen bsv geändert hat.

Und nun der dritte Unterschied. Hinzugefügt wurden das Laden der Daten bei der ersten Berechnung der Indikatordaten sowie die Funktionen Init() in die Funktion OnInit() und SaveData() in die Funktion OnDeinit().

Wir werden jetzt versuchen, den Indikator zusammenzustellen und sehen uns das Ergebnis an: 

 

Abbildung 2. Der Indikator BuySellVolume mit Anschluss an die Datenbank SQL Server 2008 bezogen auf EURUSD M5

Fertig! Jetzt werden unsere Daten gespeichert, und wir können ungezwungen zwischen den Zeiträumen hin und her schalten.

3. Anbindung an SQLite 3.6

„Mit einer Kanone auf Spatzen schießen!“ Ich bin sicher, Sie verstehen, was ich meine. SQL Server nur für die gestellte Aufgabe auf einem Rechner zu installieren, scheint ein wenig überzogen. Wenn dieses DBMS bei Ihnen allerdings bereits installiert ist, und Sie es aktiv nutzen, ist das vielleicht die Variante der Wahl. Nur was ist, wenn Sie den Indikator an jemandem übergeben möchten, der mit dieser ganzen Technologie nichts am Hut hat, und möglichst wenig dafür tun müssen will, dass eine Lösung funktioniert?

Hier möchte ich Ihre Aufmerksamkeit auf die dritte Indikatorvariante richten. Sie arbeitet anders als die vorhergehende mit einer Datenbank mit Datei-Server-Architektur. Bei diesem Ansatz müssen in der Mehrzahl der Fälle lediglich ein paar DLL mit DBMS-Kern mit herüberziehen.

Obwohl ich vorher nicht mit SQLite gearbeitet habe, habe ich es aufgrund seiner Schlichtheit, Schnelligkeit und seines geringen Umfangs gewählt. Werksseitig stellt der Hersteller nur eine Programmieroberfläche für die Arbeit aus in C++ und TCL geschriebenen Programmen zur Verfügung, aber im Internet habe ich auch einen ODBC-Treiber und einen ADO.NET-Anbieter anderer Entwickler gefunden. Da die AdoSuite die Arbeit mit Datenquellen über ODBC zulässt, scheint es ratsam, den ODBC-Treiber herunterzuladen und zu installieren. Aber meines Wissens wurde die technische Betreuung vor über einem Jahr eingestellt, weswegen ADO.NET theoretisch schneller arbeiten müsste.

Deshalb lassen Sie uns sehen, was zu ist, damit wir über den ADO.NET-Anbieter aus unserem Indikator mit SQLite arbeiten können.

Zwei Operationen bringen uns ans gesetzte Ziel:

  • Zuerst muss der Anbieter selbst heruntergeladen und installiert werden. Auf der Webseite http://sqlite.phxsoftware.com/ gibt es eine Verknüpfung, über die er heruntergeladen werden kann. Unter all diesen Dateien interessiert uns vor allem die Programmgruppe System.Data.SQLite.dll. Sie beinhaltet den Kern von SQLite selbst sowie den ADO.NET-Anbieter. Der Einfachheit halber habe ich die Bibliothek in der Anlage zu diesem Beitrag angehängt. Nach dem Herunterladen gelangen wir mithilfe des Explorers von Windows (!) zu dem Verzeichnis Windows\assembly. Es muss eine Auflistung von Programmgruppen angezeigt werden wie in Abbildung 3:

 

Abbildung 3. Der Windows Explorer kann das GAC-Verzeichnis (Global Assembly Cache) als Liste anzeigen.


Jetzt ziehen (!) wir das Verzeichnis System.Data.SQLite.dll in diesen Ordner und legen es ab (Drag and Drop).

Daraufhin wird die Programmgruppe in dem globalen Verzeichnis GAC zwischengespeichert, und wir können mit ihr arbeiten:


Abbildung 4.  System.Data.SQLite.dll im GAC

Damit ist die Einrichtung des Anbieters abgeschlossen.

  • Die zweite unbedingt auszuführende Vorbereitungsmaßnahme besteht in der Programmierung des AdoSuite-Anbieters für die Arbeit mit SQLite. Das geht recht einfach und ist schnell erledigt (in ca. 15 Minuten). Ich werde den Code hier nicht anführen, um diesen Beitrag nicht noch länger zu machen. Sie können ihn in den Dateien im Anhang zu diesem Beitrag kennenlernen.

Jetzt, da alles vorbereitet ist, schreiben wir den Indikator selbst. Für die Datenbank SQLite legen wir in dem Ordner MQL5\Files einen neuen leeren Ordner an. SQLite ist in puncto Dateierweiterung nicht zimperlich, also nennen wir sie einfach BuySellVolume.sqlite.

Eigentlich muss die Datei gar nicht zwingend angelegt werden: das geschieht bei der ersten Abfrage der in der Anbindungszeile angegebenen Datenbank automatisch (siehe Aufstellung 3.2). Wir legen sie hier nur deshalb ausdrücklich an, um zu verdeutlichen, woher sie stammt.

Wir legen eine neue Datei unter der Bezeichnung BsvSqlite.mqh an, und verbinden sie mit unserer Basisklasse sowie dem programmierten Anbieter für SQLite: 

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

 Die abgeleitete Klasse sieht bis auf ihre Bezeichnung genauso aus wie ihre Vorgängerin:

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

  };

Weiter geht es mit der Umsetzung der Methoden.

Die Zeile DbConnectionString() sieht folgendermaßen aus:

    Aufstellung 3.2

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

Sie sehen, die Anbindungszeile sieht wesentlich einfacher aus und verweist lediglich auf den Fundort unserer Datenbank.

Hier wird der relative Pfad angegeben, aber auch der vollständige ist zulässig: Data Source=c:\Program Files\Metatrader 5\MQL 5\Files\BuySellVolume.sqlite.

In der Aufstellung 3.3 sehen wir den Code für DbCheckAvailable(). Da uns in SQLite nichts zur Verfügung steht, was den gespeicherten Prozeduren ähnlich ist, werden jetzt alle Abfragen unmittelbar im Code angemeldet:

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

Das Ergebnis der Tätigkeit dieser Funktion entspricht dem SQL Server-Äquivalent. Es sei lediglich darauf hingewiesen, dass es sich dabei um die Arten der Felder für die Tabelle handelt. Am komischsten ist dabei, dass die Arten der Felder für SQLite eine recht untergeordnete Bedeutung haben. Außerdem gibt es die Arten DOUBLE und DATETIME dort überhaupt nicht (zumindest gehören sie nicht zur Grundausstattung). Alle Werte werden in Zeilenform gespeichert und später dynamisch in die erforderliche Art umgewandelt.

Welchen Sinn macht es dann also, die Spalten DOUBLE und DATETIME zu deklarieren? Ich weiß nicht genau wie, aber, wenn sie abgerufen werden, wandelt ADO.NET sie automatisch in die Arten „double“ und „datetime“ um. Aber das ist nicht immer so, es gibt Situationen, in denen einer von ihnen in der folgenden Aufstellung auftaucht.

Sehen wir uns also die Code-Aufstellung der nächsten Funktion DbLoadData() an:

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

Diese Funktion arbeitet genauso wie ihre für MS SQL umgesetzte Variante. Aber woher stammt der Zyklus am Ende der Funktion? Ja, an dieser wundersamen Abfrage sind alle meine Versuche zur Ausgabe von „datetime“ kläglich gescheitert. Das Nichtvorhandensein der Feldart „DATETIME“ in SQLite ist unübersehbar, statt des Datums wird eine Zeile im Format YYYY-MM-DD hh:mm:ss ausgegeben. Diese lässt sich jedoch leicht in eine für die Funktion StringToTime verständliche Form bringen, was wir uns zunutze gemacht haben.

Und jetzt, zu guter Letzt die Funktion DbSaveData():

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

Ich möchte ein wenig bei den Besonderheiten der Umsetzung dieser Funktion verharren.

Erstens geschieht all das bei der Transaktion, was allerdings auch logisch ist. Aber weniger aus Gründen der Integrität der Daten als aus Gründen der Leistungsfähigkeit: bei einer einzigen Ergänzung einer Eintragung ohne erkennbare Transaktion legt der Server die Transaktion im Hintergrund an, erstellt einen Eintrag in der Tabelle und löscht die Transaktion. Und da bei jeder Kursänderung. Wobei die gesamte Datenbank während der Eintragung gesperrt ist. Es sei darauf hingewiesen, dass den Befehlen nicht notwendig eine Transaktion zugewiesen werden muss. Und wieder habe ich nicht vollkommen verstanden, warum das so gekommen ist. Ich vermute, das liegt an der geringen Anzahl von Transaktionen.

Zweitens legen wir den Befehl einmal an, weisen ihr dann in einem Arbeitsgang die Parameter zu und führen ihn aus. Dabei handelt es sich wieder um eine Frage der Leistungsfähigkeit, da der Befehl einmal zusammengestellt (optimiert) und anschließend mit seiner zusammengestellten Fassung gearbeitet wird. 

Damit kommen wir der Sache schon näher. Schauen wir uns jetzt den Indikator BuySellVolume SQLite.mq5 selbst an:

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

Geändert wurde lediglich die Klasse der Funktion, der übrige Code ist unverändert.

Damit ist die Umsetzung der dritten Fassung des Indikators abgeschlossen.

 

Abbildung 5. Der Indikator BuySellVolume mit Anschluss an die Datenbank SQLite 3.6 bezogen auf EURUSD M5

Übrigens verfügt SQLite im Unterschied zu Sql Server Management Studio nicht über Standardhilfsprogramme für die Arbeit mit Datenbanken. Deshalb können, um nicht mit einer „Blackbox“ arbeiten zu müssen, geeignete Hilfsprogramme von Fremdanbietern heruntergeladen werden. Mir persönlich gefällt das Programm SQLiteMan am besten, es ist einfach zu handhaben und bietet gleichzeitig alle notwendigen Funktionen. Hier kann es herunter geladen werden: http://sourceforge.net/projects/sqliteman/.

Fazit

Wenn man bis zu diesen Zeilen gelangt ist, hat man schon alles hinter sich;) Ich gebe zu, ich habe selbst nicht erwartet, dass dieser Beitrag so lang werden würde. Deshalb werden sich unweigerlich Fragen ergeben, die ich bestimmt beantworten werde.

Wie wir sehen, hat jede Lösung ihre Vor- und Nachteile. Die erste Variante zeichnet sich durch ihre Unabhängigkeit aus, die zweite durch ihre Leistungsfähigkeit und die dritte durch ihre Handlichkeit. Welchen Sie wählen, müssen Sie selbst entscheiden.

Ist der angelegte Indikator brauchbar? Auch das entscheiden Sie. Was mich betrifft, haben wir ein recht interessantes Exemplar geschaffen.

Damit möchte ich mich verabschieden. Bis zum nächsten Mal.

Der Inhalt der Archive:

 # Dateiname Beschreibung
1
 Sources_en.zip
 Enthält das Archiv mit dem Quellcode aller Indikatoren sowie die Bibliothek AdoSuite und muss in das entsprechende Verzeichnis Ihres Ausgabegerätes entpackt werden. Verwendungszweck der Indikatoren: ohne Datenbanknutzung (BuySellVolume.mq5), Arbeit mit der Datenbank SQL Server 2008 (BuySellVolume SqlServer.mq5) und Arbeit mit der Datenbank SQLite (BuySellVolume SQLite.mq5).
2
 BuySellVolume-DB-SqlServer.zip
 Archivdatei der Datenbank SQL Server 2008*
3
 BuySellVolume-DB-SQLite.zip
 Archivdatei der Datenbank SQLite*
4
 System.Data.SQLite.zip
 Archivdatei mit der für die Arbeit mit der Datenbank SQLite erforderlichen Programmbibliothek System.Data.SQLite.dll
  5  Databases_MQL5_doc_en.zip  Archivdatei mit der Dokumentation zu den Quellcodes der Indikatoren und der Bibliothek AdoSuite

*Beide Datenbanken enthalten Daten des „Tick“-Indikators von 5. bis 9. April jeweils einschließlich zu den Finanzinstrumenten: AUDNZD, EURUSD, GBPUSD, USDCAD, USDCHF und USDJPY.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/69

OOP in MQL5 anhand von Beispielen: Fehlercodes und Warnmeldungen bearbeiten OOP in MQL5 anhand von Beispielen: Fehlercodes und Warnmeldungen bearbeiten
In diesem Beitrag wird ein Beispiel für die Erstellung einer Klasse zur Verarbeitung von Meldungen des Handelsservers sowie aller während der Laufzeit des betreffenden MQL-Programms eintretenden Fehler. Wenn Sie diesen Artikel lesen, erfahren Sie, wie in MQL5 mit Klassen und Objekten gearbeitet wird. Zugleich ist dies ein handliches Werkzeug für den Umgang mit Fehlern, das Sie außerdem passgenau auf Ihre jeweiligen Bedürfnisse zuschneiden können.
Übertragung von MQL4-Indikatoren nach MQL5 Übertragung von MQL4-Indikatoren nach MQL5
Dieser Beitrag ist den Feinheiten der Übertragung in MQL4 programmierter Kurskonstruktion nach MQL5 gewidmet. Um die Übertragung von Indikatorberechnungen aus MQL4 nach MQL5 zu vereinfachen, empfiehlt sich die Funktionsbibliothek mql4_2_mql5.mqh. Ihre Verwendung wird am Beispiel der Übertragung der Indikatoren MACD, Stochastik und RSI veranschaulicht.
MetaTrader 5: Handelsprognosen und Umsatzzahlen per E-Mail in Blogs, sozialen Netzen und auf thematischen Webseiten veröffentlichen MetaTrader 5: Handelsprognosen und Umsatzzahlen per E-Mail in Blogs, sozialen Netzen und auf thematischen Webseiten veröffentlichen
Dieser Beitrag zielt darauf ab, gebrauchsfertige Lösungen für die Veröffentlichung von Prognosen mittels MetaTrader 5. Er behandelt eine Reihe von Gedanken: von der Verwendung thematischer Webseiten zur Veröffentlichung von MetaTrader-Meldungen über die Erstellung einer eigenen Webseite ohne Erfahrung in Sachen Webprogrammierung bis hin zur abschließenden Einbindung in den Mikrobloggingdienst eines sozialen Netzes, die zahlreichen Leserinnen und Lesern ermöglicht, die Prognosen zu lesen und zu verfolgen. Alle hier vorgestellten Lösungen sind hundertprozentig kostenlos und können von allen eingerichtet werden, die über E-Mail- und FTP-Grundkenntnisse verfügen. Dem Einsatz derselben Verfahren für berufsmäßige Webhosting- und gewerbliche Handelsprognosedienste steht nichts entgegen.
Erstellen eines "Schlangenspiels" in MQL5 Erstellen eines "Schlangenspiels" in MQL5
In diesem Beitrag wird ein Beispiel für die Programmierung eines Schlangenspiels vorgestellt. In MQL5 wird die Programmierung von Spielen in erster Linie durch die Ereignisverarbeitungsroutinen ermöglicht. Die objektorientierte Programmierung ist dabei eine große Hilfe. Sie werden in diesem Artikel neben den Ereignisverarbeitungsroutinen auch Anwendungsbeispiele für die Klassen der Standardbibliothek von MQL5 sowie Einzelheiten zu regelmäßig wiederkehrenden Funktionsaufrufen kennen lernen.