针对市场分析的数据库的具体应用(基础篇)
📘

针对市场分析的数据库的具体应用(基础篇)

第 1/3 篇

◍ 用 MT5 内置数据库做行情特征统计

MT5 客户端自 2013 年起就支持把市场分析所需的结构化数据落库,早期社区里一个具体案例在 2013-12-17 发布后拿到了 2180 次查看与 8 条讨论,说明交易者很早就在绕开裸 K 截屏、改用可查询的数据沉淀。 这类思路的核心是把每根 Bar 的 OHLC、成交量、以及你自定义的指标值写进本地或远程数据库,而不是只靠图表肉眼翻。等你积累了数万根样本,就能直接 SQL 出『某货币对亚盘时段突破前一日最高价后,欧盘延续的概率』这类硬问题。 外汇与贵金属属高杠杆品种,任何统计优势都只是概率倾斜,实盘前务必用 MT5 策略测试器先跑一遍样本外数据。

为什么交易里也得碰数据库

现代软件绝大多数时间都在和数据打交道,独立程序也好、Web 应用也好,底层都依赖数据库管理系统(DBMS)来做结构化存储与检索。制造业、金融、电信这些领域,DBMS 已经是信息运转的地基。 交易圈里多数分析师做盘面还是靠表格和文本,不碰数据库。但有些事绕不开:比如要把逐笔价格变动长期落盘,或者把历史指标状态跨会话调出来,这时候就得用 DBMS 方案。 这篇文章要做的,就是一个把价格变动写进数据库、又能从库里读出来重画的指标。你可以在 MT5 里建个本地库,先验证它落盘是否完整,再决定要不要接更大容量的方案。外汇和贵金属波动快、杠杆高,落盘断点可能造成信号错位,实盘前务必在模拟环境跑通。

「用 tick 间隔拆出买卖活跃度」

BuySellVolume 的思路很直白:取相邻两个 tick 的时间 t 与价格 p,分别算差值。时间差 Δt = t2 - t1,单位秒;价格差 Δp = p2 - p1,单位点。 量值按 v2 = Δp / Δt 计算。含义是:价格动得越多、耗时越短,这一刻的活跃度读数越高;它和价格移动点数成正比,和经历时间成反比。 原文规定当 Δt = 0 时改用 0.5 参与计算,避免除零。这样每个 tick 都能落出一个活动值,用来近似看买家或卖家的发力强度。外汇与贵金属杠杆高、滑点随机,这类读数只反映局部微观活跃,不能单独当方向依据,开 MT5 加载后建议先盯欧元兑美元 M1 看数值跳动节奏。

◍ 先跑通不落库的计算基类

做买压卖压指标时,第一版完全可以撇开数据库:把报价和成交量先塞进内存缓存,让指标自己算、自己画。这样编译出来的 ex5 不依赖任何外部文件,MT5 里直接加载就能跑。 基类 CBsvEngine 承担除数据库交互外的全部职责。DbCheckAvailable() 在基类里写死返回 false,DbLoadData / DbSaveData 也只是空壳,真正连库的逻辑留给后面的派生类去覆写。 为了提高效率,价格变动先写进 TickBuffer 和 VolumeBuffer 两个数组,攒够一批再统一处理,而不是每跳都去碰库。构造函数里把两个缓存各预留 500 个元素,TicksInBuffer 计数为 0,DbAvailable 置 false。 ProcessTick() 在新报价到来时被调用,把变动插入缓存并算出买卖双方活动值,两个 double 数组引用分别接买盘缓冲和卖盘缓冲。LoadData() 按当前图表时间范围从库拉数据,内部调了自写的 FindIndexByTime() 做时间序列二分查找——标准库里没有现成的,只能自己补。 SaveData() 负责把缓存整理成表交给 DbSaveData(),写完即清缓存。指标文件 BuySellVolume.mq5 本体极薄,只调 ProcessTick() 和 SaveData() 两个方法。 实测编译后挂在 GBPUSD M1 上,报价跳动时指标实时出值。缺点也明显:切周期、换品种或关客户端,缓存数据全丢,不可恢复。

MQL5 / C++
class="macro">#include <Ado\Data.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// BuySellVolume指标类(不保存到数据库中)                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBsvEngine
  {
class="kw">private:
   class="type">MqlTick         TickBuffer[];      class=class="str">"cmt">// 报价数据缓存
   class="type">class="kw">double          VolumeBuffer[];    class=class="str">"cmt">// 交易量缓存
   class="type">int             TicksInBuffer;     class=class="str">"cmt">// 缓存中的报价数量
   class="type">bool            DbAvailable;       class=class="str">"cmt">// 表征是否可以操作数据库
   class="type">long            FindIndexByTime(const class="type">class="kw">datetime &time[],class="type">class="kw">datetime barTime,class="type">long left,class="type">long right);
class="kw">protected:
   class="kw">virtual class="type">class="kw">string DbConnectionString() { class="kw">return NULL; }
   class="kw">virtual class="type">bool DbCheckAvailable() { class="kw">return false; }
   class="kw">virtual CAdoTable *DbLoadData(const class="type">class="kw">datetime startTime,const class="type">class="kw">datetime endTime) { class="kw">return NULL; }
   class="kw">virtual class="type">void DbSaveData(CAdoTable *table) { class="kw">return; }
class="kw">public:
                    CBsvEngine();
   class="type">void            Init();
   class="type">void            ProcessTick(class="type">class="kw">double &buyBuffer[],class="type">class="kw">double &sellBuffer[]);
   class="type">void            LoadData(const class="type">class="kw">datetime startTime,const class="type">class="kw">datetime &time[],class="type">class="kw">double &buyBuffer[],class="type">class="kw">double &sellBuffer[]);
   class="type">void            SaveData();
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// 构造函数                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CBsvEngine::CBsvEngine(class="type">void)
  {
class=class="str">"cmt">// 首先,我们可以放置500个报价数据到缓存中
   ArrayResize(TickBuffer,class="num">500);
   ArrayResize(VolumeBuffer,class="num">500);
   TicksInBuffer=class="num">0;
   DbAvailable=false;

} 
class=class="str">"cmt">//+-------------------------------------------------------------------+

逐笔报价怎么喂给买卖量指标

想在 MT5 里把逐笔报价转成买卖压力,核心是把每次 tick 的 bid 跳变按时间归一化。下面这段引擎代码展示了从 OnInit 到 ProcessTick 再到 LoadData 的完整链路,复制进 EA 的 Class 里就能跑。 Init 只在启动时探一次数据库。DbAvailable 为假就弹窗走离线,不阻断后续逻辑。 ProcessTick 里有个容易忽略的点:缓存满就一次性扩 500 个元素的数组(ArrayResize(TickBuffer,bufSize+500)),不是按 tick 逐个扩,避免频繁重分配拖慢回测。 diff 用 MathRound((bid差)*10^_Digits) 把价格跳变转成整数点,再除以时间跨度 span 得到单位时间买卖量;同秒 arrival 时硬算成 0.5 秒分母。diff>0 加买盘缓冲,否则加卖盘缓冲。 LoadData 从数据库拉历史,用 FindIndexByTime 对齐到对应 K 线索引再回填,数据库不可用时直接 return 不加载。外汇和贵金属报价稀疏时段同秒 tick 多,0.5 秒假定可能高估瞬时卖压,实盘前建议用 EURUSD 的 M1 先验证。

MQL5 / C++
CBsvEngine::Init(class="type">void)
  {
  DbAvailable=DbCheckAvailable();
   if(!DbAvailable)
      Alert("Unable to work with database. Working offline.");
}

CBsvEngine::ProcessTick(class="type">class="kw">double &buyBuffer[],class="type">class="kw">double &sellBuffer[])
  {
class=class="str">"cmt">// 如果报价的缓存分配不足,让我们增加它们
   class="type">int bufSize=ArraySize(TickBuffer);
   if(TicksInBuffer>=bufSize)
     {
       ArrayResize(TickBuffer,bufSize+class="num">500);
       ArrayResize(VolumeBuffer,bufSize+class="num">500);
     }
class=class="str">"cmt">// 获取最后的报价并将它写入缓存
   SymbolInfoTick(Symbol(),TickBuffer[TicksInBuffer]);
   if(TicksInBuffer>class="num">0)
     {
       class=class="str">"cmt">// 计算时间差异
       class="type">int span=(class="type">int)(TickBuffer[TicksInBuffer].time-TickBuffer[TicksInBuffer-class="num">1].time);
       class=class="str">"cmt">//  
       class="type">int diff=(class="type">int)MathRound((TickBuffer[TicksInBuffer].bid-TickBuffer[TicksInBuffer-class="num">1].bid)*MathPow(class="num">10,_Digits));
       class=class="str">"cmt">// 计算交易量。如果报价和先前的报价同一秒来到,我们认为时间等于0.5秒
       VolumeBuffer[TicksInBuffer]=span>class="num">0 ?(class="type">class="kw">double)diff/(class="type">class="kw">double)span :(class="type">class="kw">double)diff/class="num">0.5;
       class=class="str">"cmt">// 用数据填充指标缓存
       class="type">int index=ArraySize(buyBuffer)-class="num">1;
       if(diff>class="num">0) buyBuffer[index]+=VolumeBuffer[TicksInBuffer];
       else sellBuffer[index]+=VolumeBuffer[TicksInBuffer];
     }
   TicksInBuffer++;
}

CBsvEngine::LoadData(const class="type">class="kw">datetime startTime,const class="type">class="kw">datetime &time[],class="type">class="kw">double &buyBuffer[],class="type">class="kw">double &sellBuffer[])
  {
class=class="str">"cmt">// 如果数据库不可用,那么不要加载数据
   if(!DbAvailable) class="kw">return;
class=class="str">"cmt">// 从数据库中获取数据
   CAdoTable *table=DbLoadData(startTime,TimeCurrent());
   if(CheckPointer(table)==POINTER_INVALID) class="kw">return;
class=class="str">"cmt">// 用接收到的数据填充缓存
   for(class="type">int i=class="num">0; i<table.Records().Total(); i++)
     {
       class=class="str">"cmt">// 获取记录数据
       CAdoRecord *row=table.Records().GetRecord(i);
       class=class="str">"cmt">// 获取相应柱形的索引
       class="type">MqlDateTime mdt;
       mdt=row.GetValue(class="num">0).ToDatetime();
       class="type">long index=FindIndexByTime(time,StructToTime(mdt));
       class=class="str">"cmt">// 用数据填充缓存
       if(index!=-class="num">1)
         {

「把逐笔报价落库再画成买卖直方图」

这段实现把 Tick 缓存和成交量先塞进 ADO 表,再统一写库,避免每跳一次就碰一次磁盘。表结构固定三列:Time 用 ADOTYPE_DATETIME,Price 与 Volume 都是 ADOTYPE_DOUBLE,循环从 i=1 跑到 TicksInBuffer-1,把 bid 价写进第 1 列、成交量写进第 2 列。 写完后有个细节容易漏:TickBuffer[0] 被赋值为最后一个元素,TicksInBuffer 重置为 1。这意味着下一轮采集以最近一个报价为基准做差,数据库里不会重复存上一轮尾笔。 指标端用独立子窗口挂两个直方图缓冲,indicator_buffers 和 indicator_plots 都设 2;买向柱线红色宽 2,卖向柱线 SteelBlue 宽 2。你在 MT5 里加载后,若只看到空白子窗,先查 DbAvailable 是否为真——类里没连上库,SaveData 直接跳过,缓冲就不会有数。 外汇与贵金属 Tick 吞吐高,实盘跑这套落库逻辑前务必在模拟盘验证写入延迟,高频品种可能拖慢主图响应。

MQL5 / C++
buyBuffer[index]+=row.GetValue(class="num">1).ToDouble();
sellBuffer[index]+=row.GetValue(class="num">2).ToDouble();
}

}
  class="kw">delete table;

}
class=class="str">"cmt">//+---------------------------------------------------------------------+
class=class="str">"cmt">// 将TickBuffer 和 VolumeBuffer 缓存保存到数据库中                         |
class=class="str">"cmt">//+---------------------------------------------------------------------+
CBsvEngine::SaveData(class="type">void)
  {
  if(DbAvailable)
    {
    class=class="str">"cmt">// 创建表格将数据传送到SaveDataToDb
    CAdoTable *table=new CAdoTable();
    table.Columns().AddColumn("Time", ADOTYPE_DATETIME);
    table.Columns().AddColumn("Price", ADOTYPE_DOUBLE);
    table.Columns().AddColumn("Volume", ADOTYPE_DOUBLE);
    class=class="str">"cmt">// 用缓存中的数据填充表格
    for(class="type">int i=class="num">1; i<TicksInBuffer; i++)
      {
       CAdoRecord *row=table.CreateRecord();
       row.Values().GetValue(class="num">0).SetValue(TickBuffer[i].time);
       row.Values().GetValue(class="num">1).SetValue(TickBuffer[i].bid);
       row.Values().GetValue(class="num">2).SetValue(VolumeBuffer[i]);
       table.Records().Add(row);
      }
    class=class="str">"cmt">// 将数据保存到数据库中
    DbSaveData(table);
    if(CheckPointer(table)!=POINTER_INVALID)
       class="kw">delete table;
    }
class=class="str">"cmt">// 将最近一个报价写入起始位,用于比较
   TickBuffer[class="num">0] = TickBuffer[TicksInBuffer - class="num">1];
   TicksInBuffer = class="num">1;
}
class=class="str">"cmt">// 包含指标类的文件
class="macro">#include "BsvEngine.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="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">// 声明指标类
CBsvEngine bsv;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnInit                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">// 设置指标属性

常见问题

不一定。先跑通不落库的计算基类,用内置能力做 tick 间隔和买卖活跃度统计,确认有用再考虑落库。
把 tick 间隔拆出来,间隔越短通常成交越密集;再把逐笔报价喂给买卖量指标,短间隔叠加量大就是活跃时段。
可以。把逐笔报价落库和画直方图的规则告诉小布,它能替你跑这套统计并直接出图,你只看结论。
先检查计算基类是否只统计了部分时段;另外不同品种 tick 频率天然不同,贵金属和外汇差异明显,别跨品种硬比。
看柱体是否集中在近期短间隔区间,若活跃度突增且买卖量失衡,价格可能倾向延续;外汇贵金属高风险,需结合其他信号。