处理数据成为现代软件的主要任务 - 独立应用程序和网络应用程序都是如此。为解决此问题而创建了专业软件。这些软件被称为数据库管理系统 (DBMS),能够针对它们的计算机存储和处理对数据进行构建、系统化和组织。这些软件是所有领域的信息活动的基础 - 从制造到金融再到电信。
对于交易,大多数分析师并不在他们的工作中使用数据库。但是对于一些任务,必须使用此类解决方案。
本文介绍此类任务中的一个:保存数据以及从数据库中加载数据的价格变动指标。
BuySellVolume - 我给指标一个简单的名称,但是其算法更加简单:获取两次连续价格变动(tick1 和 tick2)的时间 (t) 和价格 (p)。让我们计算它们之间的差异:
Δt = t2 - t1 (秒)
Δp = p2 - p1 (点)
使用以下公式计算量值:
v2 = Δp / Δt
因此,我们的量与价格移动点数成正比,与所用的时间成反比。如果 Δt = 0,则代之以采用 0.5。因此,我们获得了市场中买家和卖家的一种活动值。
我想,首先考虑一个具有指定功能但不与数据库互动的指标是符合逻辑的。我认为,最好的解决方案是创建一个执行相应计算的基类,并且其派生类实现与数据库的互动。为此,我们需要 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("Unable to work with database. Working offline."); }
在这里,我们检查是否可以处理数据库。 在基类中, 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); // 用于“买”的缓存 SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA); PlotIndexSetString(0,PLOT_LABEL,"Buy"); // 用于“卖”的缓存 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. 针对 GBPUSD M1 的没有数据库链接的 BuySellVolume 指标
太棒了!价格变动在跳动,指标在计算。此类解决方案的优点 - 对于其工作,我们仅需要指标本身 (ex5),不需要其他的东西。但是,当改变时间框架或工具时,或在您关闭客户端时,数据将不可挽回地丢失。为了避免这种情况,让我们看一看如何在我们的指标中添加保存和加载数据功能。
目前,我的计算机上安装了两个数据库管理系统 - SQL Server 2008 和 Db2 9.7。 我已经选择 SQL Server,因为与 Db2 相比,我假定大多数读者更熟悉 SQL Server。
要开始,先让我们针对 SQL Server 2008 创建一个新的数据库 BuySellVolume(通过 SQL Server Management Studio 或任何其他方式),并创建一个新的文件 BsvMsSql.mqh,该文件将包含 CBsvEngine 基类:
#include "BsvEngine.mqh"
SQL Server 配有 OLE DB 驱动程序,因此我们可以通过包含在 AdoSuite 库中的 OleDb 提供程序来使用它。为此,包含必要的类:
#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;"; }
从连接字符串,我们看到我们通过本地计算机上的 MS SQL OLE-DB 驱动程序及 SQLEXPRESS 服务器工作。我们使用 Windows 身份验证连接到 BuySellVolume 数据库(另一选择 - 显式输入登录名和密码)。
下一步是实施 DbCheckAvailable() 函数。 但是,首先让我们看一看在这个函数中应该做什么。
据说它会检查处理数据库的可能性。 在一定程度上这是真的。 事实上,它的主要目的在于检查是否存在存储当前工具的数据的表,如果不存在,则创建表。如果这些操作出错,则它返回 false,意味着忽略从表读取指标数据及将数据写入表,并且指标将以我们已经实施的方式工作(见清单 1.7)。
我建议通过 SQL Server 的存储过程 (SP) 处理数据。 为什么使用它们? 我只是想这样做。这是一个口味问题,但是我认为与在代码中写查询相比,使用 SP 是更好的解决方案(在代码中写查询还需要更多时间来编译,尽管它并不适合本例,因为将使用动态查询)。
对于 DbCheckAvailable(),存储过程如下所示:
清单 2.3
CREATE PROCEDURE [dbo].[CheckAvailable] @symbol NVARCHAR(30) AS BEGIN SET NOCOUNT ON; -- 如果没有表格,我们创建一个 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) { // 通过Oledb提供者操作ms sql 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, 仓位大小 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) { // 通过Oledb提供者操作ms sql 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
请注意,含有存储数据的 xml 应作为 @ticks 参数传递给过程。这样决定是因为性能原因 - 与调用 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>"); // 通过Oledb提供者操作ms sql 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); // 用于“买”的缓存 SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA); PlotIndexSetString(0,PLOT_LABEL,"Buy"); // 用于“卖”的缓存 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 变量的类型变了。
第三个差异 - 添加了在指标数据的第一次计算时的加载数据,在 OnInit() 中添加了 Init() 函数,在 OnDeinit() 中添加了 SaveData() 函数。
现在,让我们尝试编译指标并查看结果:
图 2. 使用 EURUSD M15,链接到 SQL Server 2008 数据库的 BuySellVolume 指标
好了!现在,我们保存了数据,并且能够任意切换时间框架。
“小题大做” - 我认为您明白我的意思。 对于此任务,部署 SQL Server 有点可笑。 当然,如果您已经安装了此数据库管理系统,并且您经常使用它,则这可能是首选项。 但是,如果您要向不熟悉这些技术并且希望用最少的工作量就让解决方案工作的人提供指标,又该怎么办?
在这里推出指标的第三版,与前面的版本不同,使用基于文件-服务器架构的数据库。采用这种方式,在大多数情形中,您仅需要几个具有数据库内核的 DLL。
尽管我以前从未用过 SQLite,但是因为其简单、快速且轻型,我选择它。 最初,我们仅有用 C++ 和 TCL 编写的 API 用于从程序工作,但是我还发现了第三方开发人员提供的 ODBC 驱动程序和 ADO.NET。因为 AdoSuite 允许通过 ODBC 使用数据源,似乎下载并安装 ODBC 驱动程序更好。但是如我理解的,其支持已经在一年前中断了,此外,ADO.NET 在理论上应更快。
因此,让我们看一看需要做什么,从而让我们能够从指标通过 ADO.NET 提供程序使用 SQLite。
两个操作将让我们实现目标:
图 3. 资源管理器可以列表方式显示 GAC(全局程序集缓存)
现在,将 System.Data.SQLite.dll 拖放到此文件夹。
结果,程序集被放在全局程序集缓存 (GAC) 中,我们可以使用它:
图 4. 在 GAC 中安装的 System.Data.SQLite.dll
现在完成了提供程序的设置。
现在 - 当一切就绪时 - 您可以开始编写指标。对于 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\MQL 5\Files\BuySellVolume.sqlite"。
清单 3.3 显示 DbCheckAvailable() 代码。因为 SQLite 不向我们提供诸如存储过程等任何东西,现在所有查询都是直接写在代码中的:
清单 3.3
//+------------------------------------------------------------------+ // 检查是否能够连接数据库 | //+------------------------------------------------------------------+ bool CBsvSqlite::DbCheckAvailable(void) { // 通过写入的SQLite供应器,操作SQLite CSQLiteConnection *conn=new CSQLiteConnection(); conn.ConnectionString(DbConnectionString()); // c用于检查交易对象表格可用性的命令 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 数据类型(至少它们未包含在标准版本中)。所有值都以字符串形式存储,然后动态转换为需要的类型。
那么,将列声明为 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 的所有尝试都不成功。SQLite 缺少 DATETIME 类型是显然的 - 代替日期,返回 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); // 用于“买”的缓存 SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA); PlotIndexSetString(0,PLOT_LABEL,"Buy"); // 用于“卖”的缓存 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. 使用 EURUSD M5,链接到 SQLite 3.6 数据库的 BuySellVolume 指标
顺便指出,与 Sql Server Management Studio 不同,在 SQLite 中没有用于处理数据库的标准实用程序。因此,为了不使用“黑箱”,您可以从第三方开发者下载相应的实用程序。个人而言,我喜欢 SQLiteMan - 它易于使用并且同时具有所有必需的功能。您可以从以下网址下载:http://sourceforge.net/projects/sqliteman/.
如果您读完了前面的内容,则就已经读完了全部内容。我必须承认,我并不期待本文成为长篇巨著。因此,我肯定要回答的问题,这是必然的。
如我们所见,每个解决方案都有利也有弊。第一个解决方案胜在其独立性,第二个胜在其性能,第三个胜在其轻便性。选择哪一个完全由您决定。
实施的指标有用吗?同样由您决定。对于我而言 - 这是一个非常有趣的范例。
在这种情况下,让我们说再见吧。再见!
# | 文件名 | 说明 |
---|---|---|
1 |
Sources_en.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 |
使用 SQLite 数据库所需的 System.Data.SQLite.dll 压缩文件 |
5 | Databases_MQL5_doc_en.zip | 源代码、指标和 AdoSuite 库说明文档压缩文件 |
* 两个数据库都包含专用于以下工具的 4 月 5 日至 9 日的价格变动指标数据:AUDNZD、EURUSD、GBPUSD、USDCAD、USDCHF、USDJPY。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程