Скачать MetaTrader 5

Практическое применение баз данных для анализа рынков

20 апреля 2010, 14:56
Alexander
3
4 382

Введение

Работа с данными стала главной задачей современного программного обеспечения, как автономных, так и сетевых прикладных программ. Для ее решения было создано специализированное программное обеспечение - системы управления базами данных (СУБД), которые позволяют структурировать, систематизировать и организовывать данные для их компьютерного хранения и обработки. Они составляют фундамент информационной деятельности во всех сферах - начиная с производства и заканчивая финансами и телекоммуникациями. 

Что касается трейдинга, то основная масса аналитиков не прибегает к использованию баз данных (БД) в своей работе. Но бывают задачи, где такое решение пришлось бы кстати. 

В данной статье я рассмотрю одну такую задачу: тиковый индикатор, который сохраняет и загружает данные из баз данных.

Алгоритм BuySellVolume 

BuySellVolume – вот такое незатейливое название я решил дать индикатору с еще более незатейливым алгоритмом: берем время (t) и цену (p) двух последовательных тиков (tick1 и tick2). Вычисляем между ними разницу:

     Δt = t2 – t1      (секунд)

     Δp = p2 – p1    (пунктов)

Значение объема вычисляется по формуле:

     v2 = Δp / Δt

Получается, наш объем прямо пропорционален количеству пунктов, на которое переместилась цена и обратно пропорционален времени, за которое она это сделала. Если Δt = 0, то вместо него берется значение 0.5. Таким образом, мы получаем в некотором роде значение активности продавцов и покупателей на рынке. 

1. Реализация индикатора без использования базы данных

Думаю, логично будет сначала рассмотреть индикатор с указанным функционалом, но без взаимодействия с базой данных. На мой взгляд, самым лучшим решением будет создать базовый класс, который будет делать соответствующие расчеты, а в производных уже реализовать взаимодействие с БД. Для реализации задуманного нам понадобится библиотека AdoSuite, так что переходим по ссылке и скачиваем ее.

Для начала создадим файл BsvEngine.mqh и подключим классы данных AdoSuite:

#include <Ado\Data.mqh>

Затем напишем базовый класс индикатора, который будет реализовывать все необходимые функции, кроме самой работы с базой данных. Вот как он выглядит:

Листинг 1.1

//+------------------------------------------------------------------+
// Класс движка индикатора BuySellVolume (без сохранения в базу)
//+------------------------------------------------------------------+
class CBsvEngine
  {
private:
   MqlTick           TickBuffer[];     // буфер тиков
   double            VolumeBuffer[];   // буфер объемов 
   int               TicksInBuffer;    // количество тиков в буфере

   bool              DbAvailable;      // указывает, возможно ли работать с базой данных     

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

Хочу обратить внимание, что для увеличения производительности решения данные помещаются в специальные буфера (TickBuffer и VolumeBuffer), а затем через определенный интервал времени выгружаются в базу.

Давайте по порядку рассмотрим реализацию класса. Начнем с конструктора:

Листинг 1.2

//+------------------------------------------------------------------+
// Конструктор класса
//+------------------------------------------------------------------+
CBsvEngine::CBsvEngine(void)
  {
// изначально можно поместить до 500 тиков в буффер
   ArrayResize(TickBuffer,500);
   ArrayResize(VolumeBuffer,500);
   TicksInBuffer=0;
   DbAvailable=false;
  }

Тут, думаю, должно быть все понятно: идет инициализация переменных и задание изначального размера буферов.

Далее представлена реализация метода Init():

 Листинг 1.3

//+------------------------------------------------------------------+
// Функция, вызываемая в событии OnInit
//+------------------------------------------------------------------+
CBsvEngine::Init(void)
  {
   DbAvailable=DbCheckAvailable();
   if(!DbAvailable)
      Alert("Невозможно осуществить работу с базой данных. Работаем в автономном режиме");
  }

Здесь производится проверка, имеется ли возможность работать с базой данных. В базовом классе DbCheckAvailable() всегда возвращает false, так как работа с БД будет осуществляться только из производных классов. Думаю, вы уже заметили, что функции DbConnectionString(), DbCheckAvailable(), DbLoadData(), DbSaveData() не несут особой смысловой нагрузки. Это как раз те функции, которые мы переопределим в потомках для привязки к конкретной БД. 

На листинге 1.4 показана реализация функции ProcessTick(), которая вызывается по приходу нового тика, заносит тик в буфер, и рассчитывает значения для нашего индикатора. Для этого в функцию передаются 2 индикаторных буфера: один служит для хранения активности покупателей, другой – для активности продавцов. 

 Листинг 1.4

//+------------------------------------------------------------------+
// Обрабатывает пришедший тик и обновляет данные индикатора
//+------------------------------------------------------------------+
CBsvEngine::ProcessTick(double &buyBuffer[],double &sellBuffer[])
  {
// если отведенного буфера под тики недостаточно, то увеличим его
   int bufSize=ArraySize(TickBuffer);
   if(TicksInBuffer>=bufSize)
     {
      ArrayResize(TickBuffer,bufSize+500);
      ArrayResize(VolumeBuffer,bufSize+500);
     }

// получаем последний тик и записываем его в буфер
   SymbolInfoTick(Symbol(),TickBuffer[TicksInBuffer]);

   if(TicksInBuffer>0)
     {
      // вычисляем разницу во времени
      int span=(int)(TickBuffer[TicksInBuffer].time-TickBuffer[TicksInBuffer-1].time);
      // вычисляем разницу в цене
      int diff=(int)MathRound((TickBuffer[TicksInBuffer].bid-TickBuffer[TicksInBuffer-1].bid)*MathPow(10,_Digits));

      // вычисляем объем. Если тик пришел в ту же секунду, что и предыдущий, то считаем время равным 0.5 секунды
      VolumeBuffer[TicksInBuffer]=span>0 ?(double)diff/(double)span :(double)diff/0.5;

      // заполняем буфера индикатора данными
      int index=ArraySize(buyBuffer)-1;
      if(diff>0) buyBuffer[index]+=VolumeBuffer[TicksInBuffer];
      else sellBuffer[index]+=VolumeBuffer[TicksInBuffer];
     }

   TicksInBuffer++;
  }

Функция LoadData() загружает данные из базы для текущего таймфрейма за указанный период времени.

 Листинг 1.5

//+------------------------------------------------------------------+
// Загружает исторические данные из базы
//+------------------------------------------------------------------+
CBsvEngine::LoadData(const datetime startTime,const datetime &time[],double &buyBuffer[],double &sellBuffer[])
  {
// если база данных недоступна, то никак не загрузим данные
   if(!DbAvailable) return;

// получаем данные из базы
   CAdoTable *table=DbLoadData(startTime,TimeCurrent());

   if(CheckPointer(table)==POINTER_INVALID) return;

// заполняем буфера полученными данными
   for(int i=0; i<table.Records().Total(); i++)
     {
      // получаем запись с данными
      CAdoRecord *row=table.Records().GetRecord(i);

      // получаем индекс соответствующего бара
      MqlDateTime mdt;
      mdt=row.GetValue(0).ToDatetime();
      long index=FindIndexByTime(time,StructToTime(mdt));

      // заполняем буфера данными
      if(index!=-1)
        {
         buyBuffer[index]+=row.GetValue(1).ToDouble();
         sellBuffer[index]+=row.GetValue(2).ToDouble();
        }
     }
   delete table;
  }

LoadData() вызывает функцию DbLoadData(), которая должна быть переопределена в наследниках и возвратить таблицу с тремя столбцами – временем бара, значением буфера покупателей и значением буфера продавцов.

Еще одна используемая здесь функция - это FindIndexByTime(). На момент написания статьи в стандартной библиотеке не нашел функции двоичного поиска для таймсерий, поэтому написал свою. 

И, наконец, функция сохранения данных SaveData(): 

Листинг 1.6 

//+------------------------------------------------------------------+
// Сохраняет данные из буферов TickBuffer и VolumeBuffer в базу
//+------------------------------------------------------------------+
CBsvEngine::SaveData(void)
  {
   if(DbAvailable)
     {
      // формируем таблицу для передачи в SaveDataToDb
      CAdoTable *table=new CAdoTable();
      table.Columns().AddColumn("Time", ADOTYPE_DATETIME);
      table.Columns().AddColumn("Price", ADOTYPE_DOUBLE);
      table.Columns().AddColumn("Volume", ADOTYPE_DOUBLE);

      // заполняем таблицу данными из буферов
      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);
        }

      // сохраняем данные в базу
      DbSaveData(table);

      if(CheckPointer(table)!=POINTER_INVALID)
         delete table;
     }

// записываем последний тик в начало, чтобы было с чем сравнивать
   TickBuffer[0] = TickBuffer[TicksInBuffer - 1];
   TicksInBuffer = 1;
  }

Как видим, в методе формируется таблица с необходимой для индикатора информацией и передается в DbSaveData(), которая и должна сохранить эти данные в базу. После записи мы просто обнуляем буфер.

Ну вот, наш каркас готов - теперь давайте посмотрим на листинге 1.7, как выглядит сам индикатор BuySellVolume.mq5:

Листинг 1.7

// подключаем файл с классом индикатора
#include "BsvEngine.mqh"

//+------------------------------------------------------------------+
//| свойства индикатора
//+------------------------------------------------------------------+
#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

//+------------------------------------------------------------------+
//| буфера данных
//+------------------------------------------------------------------+
double ExtBuyBuffer[];
double ExtSellBuffer[];

//+------------------------------------------------------------------+
//| переменные
//+------------------------------------------------------------------+
// объявляем класс индикатора
CBsvEngine bsv;
//+------------------------------------------------------------------+
//| OnInit
//+------------------------------------------------------------------+
int OnInit()
  {
// устанавливаем свойства индикатора
   IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume");
   IndicatorSetInteger(INDICATOR_DIGITS,2);
// буфер для buy
   SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA);
   PlotIndexSetString(0,PLOT_LABEL,"Buy");
// буфер для sell
   SetIndexBuffer(1,ExtSellBuffer,INDICATOR_DATA);
   PlotIndexSetString(1,PLOT_LABEL,"Sell");

// устанавливаем таймер для очищения буферов с тиками
   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[])
  {
// обрабатываем пришедший тик  
   bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer);

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnTimer
//+------------------------------------------------------------------+
void OnTimer()
  {
// сохраним данные
   bsv.SaveData();
  }

Проще некуда, по-моему. В индикаторе вызываются только две функции из написанного класса ProcessTick() и SaveData(): в ProcessTick() производятся расчеты, а функция SaveData(), хоть и не производит сохранение данных, необходима для обнуления буфера с тиками.

Попробуем скомпилировать и "вуа-ля" – индикатор начал показывать значения:

BuySellVolume без сохранения в базу 

 Рис 1. Индикатор BuySellVolume без привязки к базе данных на GBPUSD M1

Отлично! Тики тикают, индикатор считает. Достоинство такого решения – это то, что для работы индикатора нужен лишь сам индикатор (ex5), и ничего более. Вместе с тем, при смене таймфрейма, или инструмента, или при закрытии терминала, данные безвозвратно теряются. Чтобы этого избежать, давайте посмотрим, как можно добавить в наш индикатор сохранение и загрузку данных.

2. Привязываемся к SQL Server 2008

В данный момент у меня на компьютере установлены две СУБД - SQL Server 2008 и Db2 9.7. Из двух я выбрал SQL Server из расчета, что больше читателей скорее знакомы с SQL Server, чем с Db2.

Для начала, создадим новую базу данных BuySellVolume для SQL Server 2008 (через SQL Server Management Studio или любым другим доступным способом) и новый файл BsvMsSql.mqh, в который подключим файл с базовым классом CBsvEngine:

#include "BsvEngine.mqh"

SQL Server снабжен OLE DB драйвером, поэтому работу с ним можно осуществить через OleDb провайдер, входящий в библиотеку AdoSuite. Для этого подключим необходимые классы:

#include <Ado\Providers\OleDb.mqh>

И, собственно, создадим производный класс:

Листинг 2.1 

//+------------------------------------------------------------------+
// Класс индикатора BuySellVolume, прикрученный к базе MsSql
//+------------------------------------------------------------------+
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);

  };

Все что нам необходимо - это переопределить те четыре функции, которые отвечают непосредственно за работу с базой данных.

Начнем по-порядку. Метод DbConnectionString() возвращает строку подключения к базе данных.

В моем случае он выглядит следующим образом:

Листинг 2.2  

//+------------------------------------------------------------------+
// Возвращает строку соединения с базой
//+------------------------------------------------------------------+
string CBsvSqlServer::DbConnectionString(void)
  {
   return "Provider=SQLOLEDB;Server=.\SQLEXPRESS;Database=BuySellVolume;Trusted_Connection=yes;";
  }

Из строки подключения видим, что работаем через OLE-DB драйвер MS SQL, с сервером SQLEXPRESS, находящимся на локальной машине. Подключаемся к базе BuySellVolume, используя аутентификацию Windows (другой вариант - явно вводить логин, пароль).

Следующим шагом будет реализация функции DbCheckAvailable(). Но прежде давайте разберемся, что же в действительности должна делать функция?

Было сказано, что она проверяет возможность работы с базой данных. В какой-то степени это правда. На самом деле основное её предназначение - проверить существует ли таблица для хранения данных для текущего инструмента, и если нет, то создать её. И если при выполнении этих действий возникнет ошибка, она возвратит false, что будет означать, что запись и чтение данных индикатора из таблицы будут игнорироваться, и индикатор будет работать по аналогии с тем, который мы уже реализовали (см. листинг 1.7).

Работать с данными предлагаю через хранимые процедуры (ХП) SQL Server'a. Почему именно через них? Просто захотелось. Это дело вкуса конечно, но я считаю использование ХП более элегантным решением, чем писать запросы в коде (которые к тому же потребуют времени для компиляции, хотя к данному случаю это не относится, так как будут использованы динамические запросы :)

Для  DbCheckAvailable() хранимка имеет вид:

Листинг 2.3   

CREATE PROCEDURE [dbo].[CheckAvailable]
        @symbol NVARCHAR(30)
AS
BEGIN
        SET NOCOUNT ON;
        
        -- Ecли таблицы инструмента нет, то создадим
        IF OBJECT_ID(@symbol, N'U') IS NULL
        EXEC ('
                -- Создадим таблицу для инструмента
                CREATE TABLE ' + @symbol + ' (Time DATETIME NOT NULL,
                        Price REAL NOT NULL, 
                        Volume REAL NOT NULL);
                
                -- Создадим индекс для времени тика
                CREATE INDEX Ind' + @symbol + '
                ON  ' + @symbol + '(Time);
        ');
END

Мы видим, что если нужная таблица отсутствует в базе, то формируется и выполняется динамический запрос (в виде строки), который и создает таблицу. Когда хранимая процедура создана – самое время заняться функцией DbCheckAvailable():

 Листинг 2.4

//+------------------------------------------------------------------+
// Выполняет проверку имеется ли возможность подключиться к бд
//+------------------------------------------------------------------+
bool CBsvSqlServer::DbCheckAvailable(void)
  {
// работаем с ms sql через Oledb провайдер
   COleDbConnection *conn=new COleDbConnection();
   conn.ConnectionString(DbConnectionString());

// используем хранимую процедуру для создания таблицы
   COleDbCommand *cmd=new COleDbCommand();
   cmd.CommandText("CheckAvailable");
   cmd.CommandType(CMDTYPE_STOREDPROCEDURE);
   cmd.Connection(conn);

// передаем параметры в хранимую процедуру  
   CAdoValue *vSymbol=new CAdoValue();
   vSymbol.SetValue(Symbol());
   cmd.Parameters().Add("@symbol",vSymbol);

   conn.Open();

// выполняем хранимую процедуру
   cmd.ExecuteNonQuery();

   conn.Close();

   delete cmd;
   delete conn;

   if(CheckAdoError())
     {
      ResetAdoError();
      return false;
     }

   return true;
  }

Как видим, у нас имеется возможность работать с хранимыми процедурами сервера - нужно просто установить у команды свойство CommandType в CMDTYPE_STOREDPROCEDURE, затем передать нужные параметры и можно выполнять. Как и было задумано, в случае ошибки функция DbCheckAvailable вернет false. 

Далее, напишем хранимую процедуру для функции DbLoadData. Так как в базе хранятся данные для каждого тика, нам необходимо сформировать из них данные для каждого бара требуемого периода. У меня получилась вот такая процедура:

 Листинг 2.5 

СREATE PROCEDURE [dbo].[LoadData]
        @symbol NVARCHAR(30),   -- инструмент
        @startTime DATETIME,    -- начало расчета
        @endTime DATETIME,      -- конец расчета
        @period INT             -- период графика (в минутах)
AS
BEGIN
        SET NOCOUNT ON;
        
        -- конвертируем входные параметры в строки для передачи в динамический запрос
        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

Единственное, что нужно отметить – в качестве @startTime следует передавать время открытия первого заполняемого бара, иначе получим смещение.

Рассмотрим реализацию  DbLoadData() из следующего листинга:

Листинг 2.6  

//+------------------------------------------------------------------+
// Загружает данные из базы данных
//+------------------------------------------------------------------+
CAdoTable *CBsvSqlServer::DbLoadData(const datetime startTime,const datetime endTime)
  {
// работаем с ms sql через Oledb провайдер
   COleDbConnection *conn=new COleDbConnection();
   conn.ConnectionString(DbConnectionString());

// используем хранимую процедуру для расчета данных  
   COleDbCommand *cmd=new COleDbCommand();
   cmd.CommandText("LoadData");
   cmd.CommandType(CMDTYPE_STOREDPROCEDURE);
   cmd.Connection(conn);

// передаем параметры в хранимую процедуру  
   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);

// создаем таблицу и заполняем ее данными, которые возвратила хранимка
   CAdoTable *table=new CAdoTable();
   adapter.Fill(table);

   delete adapter;
   delete conn;

   if(CheckAdoError())
     {
      delete table;
      ResetAdoError();
      return NULL;
     }

   return table;
  }

Здесь мы вызываем хранимую процедуру, передаем инструмент, дату начала расчета, дату конца расчета, и текущий период графика в минутах. Затем с помощью класса COleDbDataAdapter считываем результат в таблицу, из которой и будут заполнены буфера нашего индикатора.

 И завершающим шагом будет реализация функционала для DbSaveData():

 Листинг 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;
                
                -- считываем данные из xml в таблицу
                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

Хочу обратить ваше внимание, что в качестве параметра @ticks в процедуру должен передаваться xml с данными о сохраняемых тиках. Такое решение было принято из соображений производительности – проще вызвать процедуру 1 раз и передать туда 20 тиков, чем вызвать 20 раз, передавая по тику. Давайте посмотрим, как же должна формироваться строка с xml из следующего листинга:

Листинг 2.8

//+------------------------------------------------------------------+
// Сохраняет данные в базу данных
//+------------------------------------------------------------------+
CBsvSqlServer::DbSaveData(CAdoTable *table)
  {
// если нечего записывать, то вернемся
   if(table.Records().Total()==0) return;

// формируем xml с данными для передачи в хранимую процедуру
   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>");

// работаем с ms sql через Oledb провайдер
   COleDbConnection *conn=new COleDbConnection();
   conn.ConnectionString(DbConnectionString());

// используем хранимую процедуру для записи данных
   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();

// выполняем хранимую процедуру
   cmd.ExecuteNonQuery();

   conn.Close();

   delete cmd;
   delete conn;

   ResetAdoError();
  }

Добрую половину функции как раз и занимает формирование этой самой строки с xml. Далее эта строка попадает в хранимку и там уже парсится.

На этом реализация взаимодействия с SQL Server 2008 закончена, и можно заняться реализацией индикатора BuySellVolume SqlServer.mq5.

Как вы увидите, реализация этой версии похожа на реализацию прошлой, за исключением некоторых изменений, о которых поговорим далее.

 Листинг 2.9

// подключаем файл с классом индикатора
#include "BsvSqlServer.mqh"

//+------------------------------------------------------------------+
//| свойства индикатора
//+------------------------------------------------------------------+
#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 datetime StartTime=D'2010.04.04';   // Начинать расчеты с

//+------------------------------------------------------------------+
//| буфера данных
//+------------------------------------------------------------------+
double ExtBuyBuffer[];
double ExtSellBuffer[];

//+------------------------------------------------------------------+
//| переменные
//+------------------------------------------------------------------+
// объявляем класс индикатора
CBsvSqlServer bsv;
//+------------------------------------------------------------------+
//| OnInit
//+------------------------------------------------------------------+
int OnInit()
  {
// устанавливаем свойства индикатора
   IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume");
   IndicatorSetInteger(INDICATOR_DIGITS,2);
// буфер для buy
   SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA);
   PlotIndexSetString(0,PLOT_LABEL,"Buy");
// буфер для sell
   SetIndexBuffer(1,ExtSellBuffer,INDICATOR_DATA);
   PlotIndexSetString(1,PLOT_LABEL,"Sell");

// вызовем функцию Init класса индикатора
   bsv.Init();

// устанавливаем таймер для выгрузки тиков в базу
   EventSetTimer(60);

   return(0);
  }
//+------------------------------------------------------------------+
//| OnDeinit
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
// если остались несохраненные данные, то сохраним их
   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)
     {
      // вычисляем время ближайшего бара
      datetime st[];
      CopyTime(Symbol(),Period(),StartTime,1,st);
      // загружаем данные
      bsv.LoadData(st[0],time,ExtBuyBuffer,ExtSellBuffer);
     }

// обрабатываем пришедший тик  
   bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer);

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnTimer
//+------------------------------------------------------------------+
void OnTimer()
  {
// сохраним данные
   bsv.SaveData();
  }

Первое отличие, которое бросается в глаза - наличие входного параметра StartTime. Этот параметр предназначен для ограничения интервала загрузки данных для индикатора. Дело в том, что при большом количестве данных расчет может занимать продолжительное время, хотя фактически устаревшие данные нас не интересуют.

Второе отличие - тип переменной bsv у нас поменялся на другой.

Третье отличие - была добавлена загрузка данных при первом расчете данных индикатора, а также функции Init() в OnInit() и SaveData() в OnDeinit().

Теперь попробуем скомпилировать индикатор и посмотреть результат: 

 BuySellVolume с сохранением данных в Sql Server

 Рис 2.  Индикатор BuySellVolume c привязкой к базе данных SQL Server 2008 на EURUSD M15

Готово! Теперь наши данные сохраняются, и мы можем свободно переключаться между таймфреймами.

3. Привязываемся к SQLite 3.6

«Из пушки по воробьям» - думаю, вы понимаете, о чем я. Для поставленной задачи развертывать SQL Server на машине слегка нелепо. Конечно, если у вас уже установлена эта СУБД, и вы ей активно пользуетесь, то это может быть предпочтительным вариантом. Но что если вы захотите дать индикатор кому-то, кто далек от всех этих технологий и хочет минимум движений, чтобы решение заработало?

Предлагаю вашему вниманию третий вариант индикатора, который, в отличии от предыдущего, работает с БД, имеющей файл-серверную архитектуру. При таком подходе в большинстве случаев нужно лишь таскать с собой пару dll с ядром СУБД.

Хотя я прежде не работал с SQLite, мой выбор пал на именно на нее из-за простоты, быстроты и легковесности. Изначально разработчик предоставляет лишь API для работы из программ, написанных на C++ и TCL, но в сети я также нашел ODBC драйвер и ADO.NET провайдер от сторонних разработчиков. Так как AdoSuite позволяет работать с источниками данных через ODBC, то, казалось бы, лучше скачать и установить драйвер ODBC. Но насколько я понял, поддержка оного прекращена более года назад, и к тому же ADO.NET теоретически должен работать быстрее.

Поэтому давайте посмотрим, что нужно сделать, чтобы мы смогли работать с SQLite через ADO.NET провайдер из нашего индикатора.

К поставленной цели нас приведут два действия:

  • Во-первых, необходимо скачать и установить сам провайдер. Вот официальный его сайт  http://sqlite.phxsoftware.com/, там же доступна ссылка для скачивания. Из всех файлов нас интересует сборка System.Data.SQLite.dll. Она включает само ядро SQLite и ADO.NET провайдер. Для удобства, я прикрепил библиотеку к статье. После скачивания, заходим папку Windows\Assembly через проводник Windows (!). Должен появиться список сборок, как показано на рисунке 3:

 Глобальный кеш сборок

  Рис 3. Проводник умеет отображать GAC (глобальный кеш сборок) в виде списка


Теперь, перетащим (!) System.Data.SQLite.dll в эту папку.

В результате сборка будет помещена в глобальный кеш сборок (GAC) и мы сможем работать с ней:

Сборка установлена в GAC

Рис 4.  System.Data.SQLite.dll установлена в GAC

На этом установка провайдера завершена.

  • Второе подготовительное действие, которое нужно выполнить – написать AdoSuite провайдер для работы с SQLite. Пишется он достаточно просто и быстро (на его написание ушло около 15 минут). Не буду приводить код, чтобы статья не разрасталась еще больше. Вы можете ознакомиться с ним в прикрепленных к статье файлах.

Теперь – когда все готово - можно приступать к написанию самого индикатора. Для базы данных SQLite создадим новый пустой файл в папке MQL5\Files. SQLite не привередлива к расширению файла, поэтому назовем его просто – BuySellVolume.sqlite.

На самом деле файл тоже создавать не обязательно: он автоматически будет создан при первом обращении к базе, указанной в строке подключения (см. листинг 3.2). Здесь мы его создаем явно лишь для того, чтобы было понятно, откуда он взялся.

Создадим новый файл BsvSqlite.mqh, подключим наш базовый класс и написанный провайдер для SQLite: 

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

 Производный класс имеет такой же вид, как и предыдущий, за исключением имени:

  Листинг 3.1

//+------------------------------------------------------------------+
// Класс индикатора BuySellVolume, прикрученный к базе SQLite
//+------------------------------------------------------------------+
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);

  };

Теперь перейдем к реализации методов.

DbConnectionString() имеет вид:

  Листинг 3.2

//+------------------------------------------------------------------+
// Возвращает строку соединения с базой
//+------------------------------------------------------------------+
string CBsvSqlite::DbConnectionString(void)
  {
   return "Data Source=MQL5\Files\BuySellVolume.sqlite";
  }

Как видим, строка подключения выглядит значительно проще и указывает только на местонахождение нашей базы.

Здесь указан относительный путь, но допускается также и полный: "Data Source=c:\Program Files\Metatrader 5\MQL5\Files\BuySellVolume.sqlite"

На листинге 3.3 приведен код DbCheckAvailable(). Так как ничего подобного хранимым процедурам SQLite нам не предлагает, теперь все запросы непосредственно прописываются в коде:

  Листинг 3.3 

//+------------------------------------------------------------------+
// Выполняет проверку имеется ли возможность подключиться к бд
//+------------------------------------------------------------------+
bool CBsvSqlite::DbCheckAvailable(void)
  {
// работаем с SQLite через написанный SQLite провайдер
   CSQLiteConnection *conn=new CSQLiteConnection();
   conn.ConnectionString(DbConnectionString());

// команда, проверяющая наличие таблицы для инструмента
   CSQLiteCommand *cmdCheck=new CSQLiteCommand();
   cmdCheck.Connection(conn);
   cmdCheck.CommandText(StringFormat("SELECT EXISTS(SELECT name FROM sqlite_master WHERE name = '%s')", Symbol()));

// команда, создающая таблицу по инструменту
   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()));

// команда, создающая индекс для времени
   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(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;
  }

Результат работы этой функции идентичен аналогичной для SQL Server. Единственное, что хотелось бы отметить - так это типы полей для таблицы. Самое забавное, что типы полей имеют для SQLite достаточно посредственное значение. Более того, типов DOUBLE и DATETIME там вообще не имеется (по крайней мере, они не входят в 4 стандартных). Все значения хранятся в строчном виде, а потом динамически преобразовываются к нужному типу.

Так какой же смысл тогда объявлять столбцы как DOUBLE и DATETIME? Не знаю тонкостей функционирования, но при выборке ADO.NET преобразует их к типам double и datetime автоматически. Но это не всегда так, существуют некоторые моменты, один из которых всплывет в следующем листинге.

Итак, давайте рассмотрим листинг следующей функции DbLoadData():

  Листинг 3.4

//+------------------------------------------------------------------+
// Загружает данные из базы данных
//+------------------------------------------------------------------+
CAdoTable *CBsvSqlite::DbLoadData(const datetime startTime,const datetime endTime)
  {
// работаем с SQLite через написанный SQLite провайдер
   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()));

// подставляем параметры
   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);

// создаем таблицу и заполняем ее данными
   CAdoTable *table=new CAdoTable();
   adapter.Fill(table);

   delete adapter;
   delete conn;

   if(CheckAdoError())
     {
      delete table;
      ResetAdoError();
      return NULL;
     }

// так как мы получаем строку с датой, а не саму дату, то необходимо переконвертировать
   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;
  }

Данная функция работает так же, как и ее реализация для MS SQL. Но откуда взялся цикл в конце функции? Да, в этом чудо-запросе все мои попытки вернуть datetime были безуспешны. Отсутствие типа DATETIME в SQLite дало о себе знать - вместо даты возвращается строка в формате YYYY-MM-DD hh:mm:ss. Но ее можно легко привести к виду, понятному для функции StringToTime, чем мы и воспользовались.

Ну и, наконец, функция DbSaveData():

 Листинг 3.5

//+------------------------------------------------------------------+
// Сохраняет данные в базу данных
//+------------------------------------------------------------------+
CBsvSqlite::DbSaveData(CAdoTable *table)
  {
// если нечего записывать, то вернемся
   if(table.Records().Total()==0) return;

// работаем с SQLite через SQLite провайдер
   CSQLiteConnection *conn=new CSQLiteConnection();
   conn.ConnectionString(DbConnectionString());

// используем хранимую процедуру для записи данных
   CSQLiteCommand *cmd=new CSQLiteCommand();
   cmd.CommandText(StringFormat("INSERT INTO %s VALUES(@time, @price, @volume)", Symbol()));
   cmd.Connection(conn);

// добавляем параметры
   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;
     }

// ! начинаем транзакцию явно
   CSQLiteTransaction *tran=conn.BeginTransaction();

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

      // заполняем параметры значениями
      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);

      // добавляем запись
      cmd.ExecuteNonQuery();
     }

// завершаем транзакцию
   if(!CheckAdoError())
      tran.Commit();
   else tran.Rollback();

   conn.Close();

   delete tran;
   delete cmd;
   delete conn;

   ResetAdoError();
  }

Хочу немного остановиться на особенностях реализации этой функции. 

Во-первых, это все осуществляется в транзакции, что, впрочем то, логично. Но это было сделано вовсе не из соображений сохранности данных - это было сделано из соображений производительности: при единичном добавлении записи без явной транзакции сервер создает транзакцию неявно, вставляет запись в таблицу, и удаляет транзакцию. И так для каждого тика! И это притом, что вся база данных на время записи блокируется! Стоит отметить, что командам необязательно указывать транзакцию. Опять же, до конца не понял, почему так получается. Предполагаю, что это из-за отсутствия множественных транзакций.

Во-вторых, мы создаем команду один раз, а потом в цикле присваиваем ей параметры и выполняем. Это, опять же, вопрос производительности, так как команда компилируется (оптимизируется) единожды, а потом уже работа ведется со скомпилированной ее версией. 

Итак, ближе к делу. Давайте взглянем на сам индикатор BuySellVolume SQLite.mq5:

 Листинг 3.6

// подключаем файл с классом индикатора
#include "BsvSqlite.mqh"

//+------------------------------------------------------------------+
//| свойства индикатора
//+------------------------------------------------------------------+
#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 datetime StartTime=D'2010.04.04';   // Начинать расчеты с

//+------------------------------------------------------------------+
//| буфера данных
//+------------------------------------------------------------------+
double ExtBuyBuffer[];
double ExtSellBuffer[];

//+------------------------------------------------------------------+
//| переменные
//+------------------------------------------------------------------+
// объявляем класс индикатора
CBsvSqlite bsv;
//+------------------------------------------------------------------+
//| OnInit
//+------------------------------------------------------------------+
int OnInit()
  {
// устанавливаем свойства индикатора
   IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume");
   IndicatorSetInteger(INDICATOR_DIGITS,2);
// буфер для buy
   SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA);
   PlotIndexSetString(0,PLOT_LABEL,"Buy");
// буфер для sell
   SetIndexBuffer(1,ExtSellBuffer,INDICATOR_DATA);
   PlotIndexSetString(1,PLOT_LABEL,"Sell");

// вызовем функцию Init класса индикатора
   bsv.Init();

// устанавливаем таймер для выгрузки тиков в базу
   EventSetTimer(60);

   return(0);
  }
//+------------------------------------------------------------------+
//| OnDeinit
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
// если остались несохраненные данные, то сохраним их
   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)
     {
      // вычисляем время ближайшего бара
      datetime st[];
      CopyTime(Symbol(),Period(),StartTime,1,st);
      // загружаем данные
      bsv.LoadData(st[0],time,ExtBuyBuffer,ExtSellBuffer);
     }

// обрабатываем пришедший тик  
   bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer);

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnTimer
//+------------------------------------------------------------------+
void OnTimer()
  {
// сохраним данные
   bsv.SaveData();
  }

Поменялся только класс функции, остальной код остался неизменным.

На этом реализация третьей версии индикатора окончена - можно смотреть результат.

 BuySellVolume c привязкой к SQLite

  Рис 5. Индикатор BuySellVolume c привязкой к базе данных SQLite 3.6 на EURUSDM5

Кстати, в отличии от Sql Server'овского Management Studio у SQLite нет никаких стандартных утилит для работы с базами данных. Поэтому, чтобы не работать с "черным ящиком", можете скачать соответствующую утилиту от сторонних разработчиков. Лично мне пришлась по душе программа SQLiteMan - она проста в использовании и в то же время обладает всей необходимой функциональностью. Скачать можно отсюда: http://sourceforge.net/projects/sqliteman/ .

Заключение

Если вы добрались до этих строк, значит все уже позади ;) Признаюсь, сам не ожидал, что статья получится такой объемной. Поэтому возникновение вопросов, на которые непременно отвечу, неизбежно.

Как видим, у каждого решения есть свои плюсы и минусы. Первый вариант отличается независимостью, второй - производительностью, а третий - портативностью. Какой из них выбрать - решать вам.

Полезен ли реализованный индикатор? Тоже решать вам. Как по мне - получился весьма интересный экземпляр.

На этом позвольте распрощаться. До новых встреч!

Описание содержимого архивов:

 № Имя файла Описание
1
 Sources.zip
 Содержит архив с исходными кодами всех индикаторов и библиотеки AdoSuite, его нужно распаковать в соответствующий каталог вашего терминала. Предназначение индикаторов: без использования БД (BuySellVolume.mq5), для работы с БД SQL Server 2008 (BuySellVolume SqlServer.mq5) и БД SQLite (BuySellVolume SQLite.mq5).
2
 BuySellVolume-DB-SqlServer.zip
 Архив БД SQL Server 2008*
3
 BuySellVolume-DB-SQLite.zip
 Архив БД SQLite*
4
 System.Data.SQLite.zip
 Архив с System.Data.SQLite.dll, необходимой для работы с БД SQLite
  5  Databases_MQL5_doc.zip  Архив с документацией по исходным кодам индикаторов и библиотеке AdoSuite

*Обе базы содержат данные тикового индикатора с 5 по 9 апреля включительно для инструментов:  AUDNZD, EURUSD, GBPUSD, USDCAD, USDCHF, USDJPY.

Прикрепленные файлы |
sources.zip (62.52 KB)
Vasily
Vasily | 22 апр 2010 в 16:36

статья очень полезная и буду к ней не раз обращаться для создания собственных баз данных.

правда рановато для неё ещё но в целом уже чувствуется вся мощь пятой версии.

Dennis Kirichenko
Dennis Kirichenko | 22 фев 2011 в 18:37
Alexander, подскажите пож-ста вот в такой ситуации...

Мне нужно, например, сохранить некоторый массив в базу данных. Написал скрипт.

Выглядит примерно так:

#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"

#include <Object.mqh>
#include <Arrays\List.mqh>

// подключаем OleDb компоненты
#include <Ado\Providers\OleDb.mqh>
#include <Ado\Data.mqh>

double PriceBuffer[];
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   long n1=100;
   MqlRates rates[];
   ArraySetAsSeries(rates,true);
   double nClose[];
   ArrayResize(nClose,n1);
   ArrayResize(t,n1);

   CSymbolInfo m_smbinf;
   m_smbinf.Name(Symbol());
   int dg=m_smbinf.Digits();

   int copied=CopyRates(Symbol(),0,0,n1,rates);
   for(int i=0;i<n1;i++)
     nClose[i]=rates[i].close;
     
// формируем таблицу для передачи в SaveDataToDb
   CAdoTable *table=new CAdoTable();
   table.Columns().AddColumn("Price",ADOTYPE_DOUBLE);
   ArrayResize(PriceBuffer,n1);
// заполняем даблицу данными из буферов
   for(int i=1; i<n1; i++)
     {
      CAdoRecord *row=table.CreateRecord();
      row.Values().GetValue(0).SetValue(PriceBuffer[i]);
      table.Records().Add(row);
     }
  }

Но вылетает при компиляции ошибка такая: "'Values' - cannot call protected member function".

Ошибка связана со строкой:

row.Values().GetValue(0).SetValue(PriceBuffer[i]);

Хотя в функции CBsvEngine::SaveData(void)

такие строки работали нормально:

// заполняем даблицу данными из буферов
      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);
        }

 

Serge
Serge | 1 апр 2011 в 19:38
Есть ли классы для работы с MySQL(через libmySQL.dll) и SQLite(через sqlite3.dll) без ADO из МТ5?
Создание активных панелей управления  на MQL5 для торговли Создание активных панелей управления на MQL5 для торговли

Статья посвящена разработке активных панелей управления на MQL5. Управление элементами интерфейса осуществляется при помощи механизма обработки событий, есть возможность гибкой настройки свойств элементов управления. Реализована работа с позициями а также возможность выставления, модификации и удаления рыночных и отложенных ордеров.

MQL5 для "чайников": Как проектировать и конструировать классы объектов MQL5 для "чайников": Как проектировать и конструировать классы объектов

На примере создания программы визуального программирования показано, как проектировать и конструировать классы на MQL5. Статья предназначена для начинающих разработчиков приложений МТ5. Предлагается простая и понятная технология создания собственных классов без глубокого погружения в теорию объектно-ориентированного программирования.

Пример разработки торговой системы, основанной на различиях  часовых поясов на разных континентах Пример разработки торговой системы, основанной на различиях часовых поясов на разных континентах

Листая страницы Интернета, можно найти множество стратегий, которые вам советуют делать то или иное. Давайте заглянем внутрь и посмотрим на сам процесс составления стратегии, основанной на различиях часовых поясов на разных континентах.

Связь ICQ и эксперта в MQL5 Связь ICQ и эксперта в MQL5

В статье рассматривается способ двустороннего обмена текстовыми сообщениями между клиентами ICQ, используя средства программирования языка MQL5. Материал заинтересует тех, кто хочет получать торговую информацию из работающего торгового терминала удаленно, например, через ICQ клиента в своем мобильном телефоне или КПК.