Практическое применение баз данных для анализа рынков
Введение
Работа с данными стала главной задачей современного программного обеспечения, как автономных, так и сетевых прикладных программ. Для ее решения было создано специализированное программное обеспечение - системы управления базами данных (СУБД), которые позволяют структурировать, систематизировать и организовывать данные для их компьютерного хранения и обработки. Они составляют фундамент информационной деятельности во всех сферах - начиная с производства и заканчивая финансами и телекоммуникациями.
Что касается трейдинга, то основная масса аналитиков не прибегает к использованию баз данных (БД) в своей работе. Но бывают задачи, где такое решение пришлось бы кстати.
В данной статье я рассмотрю одну такую задачу: тиковый индикатор, который сохраняет и загружает данные из баз данных.
Алгоритм 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(), хоть и не производит сохранение данных, необходима для обнуления буфера с тиками.
Попробуем скомпилировать и "вуа-ля" – индикатор начал показывать значения:
Рис 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().
Теперь попробуем скомпилировать индикатор и посмотреть результат:
Рис 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) и мы сможем работать с ней:
Рис 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(); }
Поменялся только класс функции, остальной код остался неизменным.
На этом реализация третьей версии индикатора окончена - можно смотреть результат.
Рис 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.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
статья очень полезная и буду к ней не раз обращаться для создания собственных баз данных.
правда рановато для неё ещё но в целом уже чувствуется вся мощь пятой версии.
Мне нужно, например, сохранить некоторый массив в базу данных. Написал скрипт.
Выглядит примерно так:
Но вылетает при компиляции ошибка такая: "'Values' - cannot call protected member function".
Ошибка связана со строкой:
Хотя в функции CBsvEngine::SaveData(void)
такие строки работали нормально: