针对市场分析的数据库的具体应用(基础篇)
◍ 用 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 上,报价跳动时指标实时出值。缺点也明显:切周期、换品种或关客户端,缓存数据全丢,不可恢复。
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 先验证。
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 吞吐高,实盘跑这套落库逻辑前务必在模拟盘验证写入延迟,高频品种可能拖慢主图响应。
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">// 设置指标属性