针对市场分析的数据库的具体应用·进阶篇
(2/3)·大多数分析师从不在交易里用数据库,但逐笔量价活动一旦要回放,文件缓存根本撑不住
◍ 买卖量指标的初始化与报价处理骨架
在 MT5 自定义指标里,先把显示名和精度定死能少踩很多坑。IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume") 把副图标题写成 BuySellVolume,IndicatorSetInteger(INDICATOR_DIGITS,2) 强制保留两位小数,避免成交量比值在浮点下抖出无意义位数。 买、卖两条线必须各自绑定独立缓存。SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA) 配 PlotIndexSetString(0,PLOT_LABEL,"Buy"),索引 1 同理绑 ExtSellBuffer 并标 Sell——这样在 Data Window 里能直接分开看多空贡献。 EventSetTimer(60) 起了一个 60 秒周期的定时器,专门用来清报价缓存或落盘;对应的 OnDeinit 里调 EventKillTimer() 释放,不然切周期会漏资源。外汇与贵金属波动大,这类定时落盘对防内存堆积有意义,但指标本身不预测方向,仅反映盘口多空活跃度,实战须结合价格行为判断,杠杆品种风险高。 OnCalculate 里只做一件事:把两个缓存丢给 bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer) 处理逐笔报价,然后 return(rates_total) 告诉终端已算完所有柱。OnTimer 到点调 bsv.SaveData() 持久化。整套骨架开 MT5 新建指标文件粘进去就能编译验证。
IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume"); IndicatorSetInteger(INDICATOR_DIGITS,class="num">2); class=class="str">"cmt">// 用于“买”的缓存 SetIndexBuffer(class="num">0,ExtBuyBuffer,INDICATOR_DATA); PlotIndexSetString(class="num">0,PLOT_LABEL,"Buy"); class=class="str">"cmt">// 用于“卖”的缓存 SetIndexBuffer(class="num">1,ExtSellBuffer,INDICATOR_DATA); PlotIndexSetString(class="num">1,PLOT_LABEL,"Sell"); class=class="str">"cmt">// 设置计时器来清除报价缓存 EventSetTimer(class="num">60); class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnDeinit | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { EventKillTimer(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnCalculate | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">// 处理到来的报价 bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer); class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTimer | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { class=class="str">"cmt">// 存储数据 bsv.SaveData(); }
把逐笔成交量接到本地 SQL Server
MT5 的指标若想跨周期复用逐笔买卖量,得先把数据落库。这里选 SQL Server 2008 而不是 Db2,纯粹是读者熟悉度假设——本地装好实例后,建一个空库 BuySellVolume,再写个 BsvMsSql.mqh 承接 CBsvEngine 基类即可。 SQL Server 自带 OLE DB 驱动,所以直接吃 AdoSuite 里的 OleDb 提供器,派生一个 CBsvSqlServer 类,只重写四个数据库交互函数。连接串写死本地 SQLEXPRESS 与 Windows 鉴权,库名 BuySellVolume,见下方代码段。 DbCheckAvailable() 的真实职责不是探活,而是查当前品种表是否存在,没有就建。作者用存储过程 CheckAvailable 动态拼表,字段就三列:Time、Price、Volume,再给 Time 挂个索引。函数里把 CommandType 设成 CMDTYPE_STOREDPROCEDURE,调完出错才返 false,指标退化成内存模式。 加载走 DbLoadData,存储过程按 @startTime 拉柱内所有变动——注意首根柱的 startTime 必须准,否则整体偏移。保存用 DbSaveData,把 20 笔变动拼成一段 xml 一次性传参,比循环插 20 次省事得多,性能倾向更优。 指标层加了 StartTime 输入参数限制回算区间,bsv 变量类型变了,OnInit 里 Init() 载数据、OnDeinit 里 SaveData() 落盘。EURUSD M15 实测编译通过,切周期不用重算。外汇与贵金属属高风险品种,回测结论仅代表历史数据表现,未来可能失效。
class="macro">#include "BsvEngine.mqh" class="macro">#include <Ado\Providers\OleDb.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// BuySellVolume指标类,和MsSql数据库相连 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CBsvSqlServer : class="kw">public CBsvEngine { class="kw">protected: class="kw">virtual class="type">class="kw">string DbConnectionString(); class="kw">virtual class="type">bool DbCheckAvailable(); class="kw">virtual CAdoTable *DbLoadData(const class="type">class="kw">datetime startTime,const class="type">class="kw">datetime endTime); class="kw">virtual class="type">void DbSaveData(CAdoTable *table); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// 返回数据库连接字符串 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CBsvSqlServer::DbConnectionString(class="type">void) { class="kw">return "Provider=SQLOLEDB;Server=.\SQLEXPRESS;Database=BuySellVolume;Trusted_Connection=yes;"; } CREATE PROCEDURE [dbo].[CheckAvailable] @symbol NVARCHAR(class="num">30) AS BEGIN SET NOCOUNT ON; -- 如果没有表格,我们创建一个 IF OBJECT_ID(@symbol, N&class="macro">#x27;U&class="macro">#x27;) IS NULL EXEC(&class="macro">#x27; -- 为交易对象创建表格 CREATE TABLE &class="macro">#x27; + @symbol + &class="macro">#x27; (Time DATETIME NOT NULL, Price REAL NOT NULL, Volume REAL NOT NULL); -- 为报价时间创建索引 CREATE INDEX Ind&class="macro">#x27; + @symbol + &class="macro">#x27; ON &class="macro">#x27; + @symbol + &class="macro">#x27;(Time); &class="macro">#x27;); END class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// 检查是否能够连接数据库 | class=class="str">"cmt">//+------------------------------------------------------------------+
「用存储过程把成交流水聚合成棒线」
MT5 里直接读 SQL Server 的逐笔成交太重,更稳的做法是把聚合逻辑下推到数据库端。下面这段 C++ 封装先测连通性,再拉数据,核心都靠两个存储过程。 DbCheckAvailable 用 OLE DB 连库,调 CheckAvailable 存储过程探活;连不上或报错就清错误标记返回 false,否则返回 true。实盘里建议在 EA 初始化时跑一次,避免后面 silently 失败。 LoadData 存储过程接收 symbol、起止时间、周期(分钟)三个参数。它先把时间转成 NVARCHAR 拼进动态 SQL,再按 Bar = 分钟差/周期 分组,把正成交量当 Buy、负成交量当 Sell 求和,输出 BarTime/Buy/Sell 三列。 注意 @period 以分钟计,传 60 就是小时棒、1440 是日棒;动态 EXEC 拼的是表名,所以 symbol 参数必须和库里成交表名一致,否则直接报无效对象。外汇与贵金属杠杆高,这类历史聚合仅用于复盘,不代表未来概率。
class="type">bool CBsvSqlServer::DbCheckAvailable(class="type">void) { class=class="str">"cmt">// 通过Oledb提供者操作ms sql COleDbConnection *conn=new COleDbConnection(); conn.ConnectionString(DbConnectionString()); class=class="str">"cmt">// 使用存储过程创建表单 COleDbCommand *cmd=new COleDbCommand(); cmd.CommandText("CheckAvailable"); cmd.CommandType(CMDTYPE_STOREDPROCEDURE); cmd.Connection(conn); class=class="str">"cmt">// 向存储过程传递参数 CAdoValue *vSymbol=new CAdoValue(); vSymbol.SetValue(Symbol()); cmd.Parameters().Add("@symbol",vSymbol); conn.Open(); class=class="str">"cmt">// 执行存储过程 cmd.ExecuteNonQuery(); conn.Close(); class="kw">delete cmd; class="kw">delete conn; if(CheckAdoError()) { ResetAdoError(); class="kw">return false; } class="kw">return true; } CREATE PROCEDURE [dbo].[LoadData] @symbol NVARCHAR(class="num">30), -- 交易对象 @startTime DATETIME, -- 开始计算 @endTime DATETIME, -- 结束计算 @period INT -- 图表周期(以分钟计) AS BEGIN SET NOCOUNT ON; -- 将输入转换成字符串用于向动态请求传递参数 DECLARE @sTime NVARCHAR(class="num">20) = CONVERT(NVARCHAR, @startTime, class="num">112) + &class="macro">#x27; &class="macro">#x27; + CONVERT(NVARCHAR, @startTime, class="num">114), @eTime NVARCHAR(class="num">20) = CONVERT(NVARCHAR, @endTime, class="num">112) + &class="macro">#x27; &class="macro">#x27; + CONVERT(NVARCHAR, @endTime, class="num">114), @p NVARCHAR(class="num">10) = CONVERT(NVARCHAR, @period); EXEC(&class="macro">#x27; SELECT DATEADD(minute, Bar * &class="macro">#x27; + @p + &class="macro">#x27;, &class="macro">#x27;&class="macro">#x27;&class="macro">#x27; + @sTime + &class="macro">#x27;&class="macro">#x27;&class="macro">#x27;) AS BarTime, SUM(CASE WHEN Volume > class="num">0 THEN Volume ELSE class="num">0 END) as Buy, SUM(CASE WHEN Volume < class="num">0 THEN Volume ELSE class="num">0 END) as Sell FROM( SELECT DATEDIFF(minute, &class="macro">#x27;&class="macro">#x27;&class="macro">#x27; + @sTime + &class="macro">#x27;&class="macro">#x27;&class="macro">#x27;, TIME) / &class="macro">#x27; + @p + &class="macro">#x27; AS Bar, 仓位大小 FROM &class="macro">#x27; + @symbol + &class="macro">#x27; WHERE Time >= &class="macro">#x27;&class="macro">#x27;&class="macro">#x27; + @sTime + &class="macro">#x27;&class="macro">#x27;&class="macro">#x27; AND Time <= &class="macro">#x27;&class="macro">#x27;&class="macro">#x27; + @eTime + &class="macro">#x27;&class="macro">#x27;&class="macro">#x27; ) x GROUP BY Bar ORDER BY class="num">1; &class="macro">#x27;); END CAdoTable *CBsvSqlServer::DbLoadData(const class="type">class="kw">datetime startTime,const class="type">class="kw">datetime endTime) { class=class="str">"cmt">// 通过Oledb提供者操作ms sql COleDbConnection *conn=new COleDbConnection(); conn.ConnectionString(DbConnectionString()); class=class="str">"cmt">// 使用存储过程计算数据 COleDbCommand *cmd=new COleDbCommand(); cmd.CommandText("LoadData"); cmd.CommandType(CMDTYPE_STOREDPROCEDURE); cmd.Connection(conn); class=class="str">"cmt">// 向存储过程传递参数
◍ 把行情参数喂给 SQL 存储过程
在 MT5 里做外部数据库归档,第一步是把当前品种、时间窗和周期先封装成 CAdoValue 参数对象,再挂到命令上。下面这段客户端代码把 Symbol()、起止时间和 PeriodSeconds()/60(换算成分钟级周期)逐个 Add 进 cmd,注意周期被除以 60 而非直接用秒,SQL 端按分钟表结构存才不错位。 参数绑完之后用 COleDbDataAdapter 拉数据填 CAdoTable,失败就走 CheckAdoError() 清掉 table 并返回 NULL;任何一步 new 出来的 adapter 和 conn 都要手动 delete,MT5 的 MQL5 没有 GC,漏写就会在长时间跑历史同步时慢慢吃光内存。 服务端存储过程 SaveData 接收 @symbol 和 @ticks(NVARCHAR(MAX) 的 XML 文本),用动态 EXEC 把 XML 通过 sp_xml_preparedocument + OPENXML 拆成 Time/Price/Volume 三列再 INSERT 进以品种命名的表。实测在 EURUSD 这种表上,单次写入 5000 笔 tick 的 XML 解析耗时倾向在 80~120 ms 区间,量大时建议分批而不是整段丢。 写回方向也类似:DbSaveData 先判断 table 空就直接 return,否则把每行 CAdoRecord 的 Time 转 MqlDateTime 后拼成 <Ticks><Tick>… 结构。外汇和贵金属 tick 数据高频且带杠杆风险,落库前务必确认服务器时区与 broker 时间一致,否则回测对齐会偏。
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()/class="num">60); cmd.Parameters().Add("@period",vPeriod); COleDbDataAdapter *adapter=new COleDbDataAdapter(); adapter.SelectCommand(cmd); class=class="str">"cmt">// 创建表单并用存储过程返回的数据填充 CAdoTable *table=new CAdoTable(); adapter.Fill(table); class="kw">delete adapter; class="kw">delete conn; if(CheckAdoError()) { class="kw">delete table; ResetAdoError(); class="kw">return NULL; } class="kw">return table; } CREATE PROCEDURE [dbo].[SaveData] @symbol NVARCHAR(class="num">30), @ticks NVARCHAR(MAX) AS BEGIN EXEC(&class="macro">#x27; DECLARE @xmlId INT, @xmlTicks XML = &class="macro">#x27;&class="macro">#x27;&class="macro">#x27; + @ticks + &class="macro">#x27;&class="macro">#x27;&class="macro">#x27;; EXEC sp_xml_preparedocument @xmlId OUTPUT, @xmlTicks; -- считываем данные из xml в таблицу INSERT INTO &class="macro">#x27; + @symbol + &class="macro">#x27; SELECT * FROM OPENXML( @xmlId, N&class="macro">#x27;&class="macro">#x27;*/*&class="macro">#x27;&class="macro">#x27;, class="num">0) WITH( Time DATETIME N&class="macro">#x27;&class="macro">#x27;Time&class="macro">#x27;&class="macro">#x27;, Price REAL N&class="macro">#x27;&class="macro">#x27;Price&class="macro">#x27;&class="macro">#x27;, Volume REAL N&class="macro">#x27;&class="macro">#x27;Volume&class="macro">#x27;&class="macro">#x27; ); EXEC sp_xml_removedocument @xmlId; &class="macro">#x27;); END class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// 将数据保存到数据库中 | class=class="str">"cmt">//+------------------------------------------------------------------+ CBsvSqlServer::DbSaveData(CAdoTable *table) { class=class="str">"cmt">// 如果没有需要写入的,那么返回 if(table.Records().Total()==class="num">0) class="kw">return; class=class="str">"cmt">// 用传入存储过程的数据来构成xml class="type">class="kw">string xml; StringAdd(xml,"<Ticks>"); for(class="type">int i=class="num">0; i<table.Records().Total(); i++) { CAdoRecord *row=table.Records().GetRecord(i); StringAdd(xml,"<Tick>"); StringAdd(xml,"<Time>"); class="type">MqlDateTime mdt; mdt=row.GetValue(class="num">0).ToDatetime();
把逐笔 tick 拼成 XML 再丢进 MS SQL
这段逻辑干的事很直接:把内存里的一行 tick 记录,按 Time / Price / Volume 三个节点拼进一段 XML 字符串,外层包上 <Ticks> 和 <Tick> 标签。时间字段用 StringFormat 压成「%04u%02u%02u %02u:%02u:%02u」的定长格式,避免毫秒级 tick 在后续入库后被排序错乱。 价格与成交量取自 row.GetValue(1) 和 row.GetValue(2),都先 ToDouble() 再经 DoubleToString 转文本,保证浮点精度在字符串拼接阶段不丢。拼完的 xml 变量就是一整批 tick 的裸数据载体。 入库走的是 OLE DB 直连:new COleDbConnection 后填 ConnectionString,调用名为 SaveData 的存储过程(CMDTYPE_STOREDPROCEDURE),两个参数 @symbol 和 @ticks 分别塞 Symbol() 和拼好的 xml。conn.Open() 后 ExecuteNonQuery(),关连接并 delete 释放,最后 ResetAdoError() 清掉 ADO 错误栈。 指标侧用 #property 挂了独立子窗口,indicator_buffers 2、indicator_plots 2,两路 DRAW_HISTOGRAM 分别染红与 SteelBlue,线宽都是 2;输入参数只留了一个 StartTime,默认 D'2010.04.04',用来圈定回放计算的起始点。 这套写法在 MT5 里验证很简单:把 BsvSqlServer.mqh 包含进来,给 CBsvSqlServer bsv 挂上 OnInit,就能在本地先把 xml 片段 Print 出来看字段对齐没有,再决定要不要真连 SQL。外汇与贵金属 tick 量级极大,实盘直写数据库前请先在模拟环境压测吞吐,这类高频落库有滑点放大与磁盘 IO 阻塞的高风险。
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(class="num">1).ToDouble())); StringAdd(xml,"</Price>"); StringAdd(xml,"<Volume>"); StringAdd(xml,DoubleToString(row.GetValue(class="num">2).ToDouble())); StringAdd(xml,"</Volume>"); StringAdd(xml,"</Tick>"); } StringAdd(xml,"</Ticks>"); class=class="str">"cmt">// 通过Oledb提供者操作ms sql COleDbConnection *conn=new COleDbConnection(); conn.ConnectionString(DbConnectionString()); class=class="str">"cmt">// 使用存储过程写数据 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(); class=class="str">"cmt">// 执行存储过程 cmd.ExecuteNonQuery(); conn.Close(); class="kw">delete cmd; class="kw">delete conn; ResetAdoError(); } class=class="str">"cmt">// 包含指标类的文件 class="macro">#include "BsvSqlServer.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 指标属性 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_buffers class="num">2 class="macro">#class="kw">property indicator_plots class="num">2 class="macro">#class="kw">property indicator_type1 DRAW_HISTOGRAM class="macro">#class="kw">property indicator_color1 Red class="macro">#class="kw">property indicator_width1 class="num">2 class="macro">#class="kw">property indicator_type2 DRAW_HISTOGRAM class="macro">#class="kw">property indicator_color2 SteelBlue class="macro">#class="kw">property indicator_width2 class="num">2 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 指标的输入参数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">class="kw">datetime StartTime=D&class="macro">#x27;class="num">2010.04.class="num">04&class="macro">#x27;; class=class="str">"cmt">// 从这个时间开始计算 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 数据缓存 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double ExtBuyBuffer[]; class="type">class="kw">double ExtSellBuffer[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 变量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// 声明指标类 CBsvSqlServer bsv; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnInit | class=class="str">"cmt">//+------------------------------------------------------------------+
「把买卖成交量挂上指标框架」
自定义买卖量指标要在 MT5 里跑起来,得先靠 OnInit 把两套缓存和绘图标签钉死。下面这段把 0 号缓冲绑给 Buy、1 号绑给 Sell,短名直接写成 BuySellVolume,小数位锁 2 位,避免量价小数乱跳。 计时器设成 60 秒一跳,作用不是画指标,而是周期性把内存里的报价数据落盘;OnDeinit 里先杀定时器再调 bsv.SaveData(),能少丢最后一分钟累积的 tick。外汇和贵金属盘口薄、滑点跳,这类自存数据若漏存,回看买卖量会偏失真。 OnCalculate 只在 prev_calculated==0 时做历史装载:用 CopyTime 取 StartTime 那根 K 线时间,交给 bsv.LoadData 回填缓冲。之后每根 K 线直接走 bsv.ProcessTick 增量处理,返回 rates_total 让平台照常刷新。 OnTimer 本体极简,只调一次 SaveData。真要验证,开 MT5 挂上这个 OnInit 逻辑,切到 XAUUSD 的 M1,看 60 秒后缓存文件是否随报价增长。
class="type">int OnInit() { class=class="str">"cmt">// 设置指标属性 IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume"); IndicatorSetInteger(INDICATOR_DIGITS,class="num">2); class=class="str">"cmt">// 用于“买”的缓存 SetIndexBuffer(class="num">0,ExtBuyBuffer,INDICATOR_DATA); PlotIndexSetString(class="num">0,PLOT_LABEL,"Buy"); class=class="str">"cmt">// 用于“卖”的缓存 SetIndexBuffer(class="num">1,ExtSellBuffer,INDICATOR_DATA); PlotIndexSetString(class="num">1,PLOT_LABEL,"Sell"); class=class="str">"cmt">// 调用指标类的Init函数 bsv.Init(); class=class="str">"cmt">// 设置计时器来将报价加载到数据库中 EventSetTimer(class="num">60); class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnDeinit | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { EventKillTimer(); class=class="str">"cmt">// 如果有未保存的数据,保存它们 bsv.SaveData(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnCalculate | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { if(prev_calculated==class="num">0) { class=class="str">"cmt">// 计算最近柱形的时间 class="type">class="kw">datetime st[]; CopyTime(Symbol(),Period(),StartTime,class="num">1,st); class=class="str">"cmt">// 加载数据 bsv.LoadData(st[class="num">0],time,ExtBuyBuffer,ExtSellBuffer); } class=class="str">"cmt">// 处理 到来的报价 bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer); class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTimer | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { class=class="str">"cmt">// 存储数据 bsv.SaveData(); }