针对市场分析的数据库的具体应用·进阶篇
🗄️

针对市场分析的数据库的具体应用·进阶篇

(2/3)·大多数分析师从不在交易里用数据库,但逐笔量价活动一旦要回放,文件缓存根本撑不住

含代码示例 第 2/3 篇
把逐笔 tick 量价只写进内存缓存,终端一重启历史就清零,回测和复盘全废。很多人以为指标算完就完事,真要到跨周期比对买卖活跃度才发现数据根本没落地。数据库不是大公司的专利,Tick 级分析缺它寸步难行。

◍ 买卖量指标的初始化与报价处理骨架

在 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 新建指标文件粘进去就能编译验证。

MQL5 / C++
  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 实测编译通过,切周期不用重算。外汇与贵金属属高风险品种,回测结论仅代表历史数据表现,未来可能失效。

MQL5 / C++
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 参数必须和库里成交表名一致,否则直接报无效对象。外汇与贵金属杠杆高,这类历史聚合仅用于复盘,不代表未来概率。

MQL5 / C++
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 时间一致,否则回测对齐会偏。

MQL5 / C++
  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 阻塞的高风险。

MQL5 / C++
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 秒后缓存文件是否随报价增长。

MQL5 / C++
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();
  }
让小布替你跑这套落库检查
小布盯盘的 AIGC 已内置品种级 Tick 活动诊断,打开对应品种页即可看到买卖量倾向分布,不必自己先搭 SQL Server 才能验证思路。

常见问题

同毫秒内多笔同价变动若直接除零会断算,原文用 0.5 作退化近似值保连续性,实际高频环境建议用上游合并后的 tick 时间戳降低偏差。
基类只管缓存与计算,数据库可用与否交给派生类改写,这样同一套量价引擎既能跑无库模式也能接 SQL Server,不改核心逻辑。
可把连接串放 ini 由 AdoSuite 读取,或仅在派生类内部构造,不要写进公共指标参数面板,减少实盘账户环境被导出风险。
目前小布内置的是云端品种页诊断,本地库需你自己用 MQL5 跑通落库;若只想看买卖活跃度分布,小布对应页已覆盖大部分场景。
取决于品种波动与硬盘 IO,秒级内高频品种可设 1–3 秒批量写,过短会增加 SQL 连接开销,过长则重启丢窗变大。
杠杆与滑点使 Tick 量价仅作概率参考,不代表方向确定性,且跨平台 Tick 时间戳对齐差异可能扭曲 Δt,需先做时区与精度归一。