以 delta 指标为例开发股票交易量控制指标·综合运用
(3/3)· 把实际成交量拆成买卖 delta 并不难,难在逐笔数据稳定和回测可信度
◍ 用逐笔报价给当前K线算买卖量
在 MT5 里做订单流分析,核心是把 tick 数据按 K 线归桶。下面这个函数负责处理「当前未闭合柱」的买/卖成交量累计,靠 firstLaunch 标志区分 EA 重启后的首次清扫。 静态变量 sumVolBuy、sumVolSell 跨调用保留合计值,bNum 记录已写入缓冲区的柱索引,初值用 WRONG_VALUE 占位。首次启动时必须把它们清零,否则会带着上一会话的脏数据继续累加。 函数先取 _ticks 里最后一条逐笔报价索引 limit,用 limitTime 做循环终止判断:当第 i 个 tick 的时间戳等于 limitTime,说明本轮扫完,直接 return。接着用 _ticks.GetTickTime(i) 与 time[rates_total-1]+PeriodSeconds() 比较,若超过,说明下一根图表上还不存在的 K 线已开始成形,此时把请求起点拨到该 tick 的毫秒时间并退出,避免越界写。 若 _ticks.IsNewCandle(i) 为真,代表下一根已完成 K 线起步;当 bNum>=0 且 sumVolBuy 或 sumVolSell 大于 0,说明上一根完整柱的参数已齐,才进入总交易量管理逻辑。外汇与贵金属 tick 流动性断层频繁,这种边界检查少一步就可能把成交量算到幽灵柱上。
class="type">void CalculateCurrentBar(class="kw">const class="type">bool firstLaunch, class=class="str">"cmt">// 函数首次启动标志 class="kw">const class="type">int rates_total, class=class="str">"cmt">// 已计算柱线数量 class="kw">const class="type">class="kw">datetime& time[], class=class="str">"cmt">// 柱线开盘时间数组 class="kw">const class="type">long& volume[] class=class="str">"cmt">// 实际交易量数组 ) class=class="str">"cmt">//--- 总交易量 class="kw">static class="type">long sumVolBuy=class="num">0; class="kw">static class="type">long sumVolSell=class="num">0; class=class="str">"cmt">//--- 写入缓冲区的柱线索引 class="kw">static class="type">int bNum=WRONG_VALUE; class=class="str">"cmt">//--- 检查首次启动标志 if(firstLaunch) class=class="str">"cmt">// 首次启动的情况 { class=class="str">"cmt">//--- 重置交易量合计 sumVolBuy=class="num">0; sumVolSell=class="num">0; class=class="str">"cmt">//--- 重置烛台索引 bNum=WRONG_VALUE; } class=class="str">"cmt">//--- 获取数组中最后一个逐笔报价的索引 class="kw">const class="type">int limit=_ticks.GetSize()-class="num">1; class=class="str">"cmt">//--- &class="macro">#x27;limit&class="macro">#x27; 逐笔报价时间 class="kw">const class="type">class="kw">ulong limitTime=_ticks.GetFrom(); class=class="str">"cmt">//--- class="num">1. 取第 i 个逐笔报价与第 limit 个逐笔报价比较(检查循环完成) if( _ticks.GetTickTimeMs( i ) == limitTime ) class=class="str">"cmt">// 如果逐笔报价时间等于 limit 逐笔报价 class="kw">return; class=class="str">"cmt">// 退出 class=class="str">"cmt">//--- class="num">2. 检查图表上不存在的烛台是否开始成形 if(_ticks.GetTickTime(i)>=time[rates_total-class="num">1]+PeriodSeconds()) class=class="str">"cmt">// 如果烛台开始形成 { class=class="str">"cmt">//--- class="num">2.1. 设置(校正)下一个逐笔报价的请求时间 _ticks.SetFrom(_ticks.GetTickTimeMs(i)); class=class="str">"cmt">//--- 退出 class="kw">return; } class=class="str">"cmt">//--- class="num">3. 定义保存逐笔报价的烛台 if(_ticks.IsNewCandle(i)) class=class="str">"cmt">// 如果下一根烛台开始成形 { class=class="str">"cmt">//--- class="num">3.1. 检查已形成的(完整的)烛台索引是否已保存 if(bNum>=class="num">0) class=class="str">"cmt">// 如果索引已保存 { class=class="str">"cmt">//--- 检查交易量数值是否已保存 if(sumVolBuy>class="num">0 || sumVolSell>class="num">0) class=class="str">"cmt">// 如果所有参数均已保存 { class=class="str">"cmt">//--- class="num">3.1.class="num">1. 管理烛台的总交易量
逐笔报价归类与烛台切换的计数逻辑
这段逻辑干两件事:在每根烛台走完时把累计的买卖量交给 VolumeControl 做展示,然后清零计数器并记下新烛台索引 bNum=rates_total-1。注意 rates_total 是包含当前未闭合烛台的总数,减 1 才是已闭合的最后一根,跨周期统计别错位。 AddVolToSum 是真正的分类器:用 tick.flags 按位与 TICK_FLAG_BUY / TICK_FLAG_SELL 判断方向。若双向位同时置位会打印 unknown direction 错误,纯非交易 tick 也会报错,只有单边买或卖才把 tick.volume 累加到对应 long 变量。 实盘里外汇和贵金属点差跳变时会出现非交易 tick,这类量不会进 sumVolBuy/sumVolSell,统计买卖失衡时可能偏小。MT5 里接真实账户跑一遍,看 Experts 日志里 ERROR 频率,能判断你的品种 tick 流干不干净。
VolumeControl(true,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell); } } class=class="str">"cmt">//--- class="num">3.2. 重置先前的烛台交易量 sumVolBuy=class="num">0; sumVolSell=class="num">0; class=class="str">"cmt">//--- class="num">3.3. 记住当前的烛台索引 bNum=rates_total-class="num">1; } class=class="str">"cmt">//--- class="num">4. 将逐笔报价的交易量加入必要的分量 AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将逐笔报价交易量添加到总交易量中 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void AddVolToSum(class="kw">const class="type">MqlTick &tick, class=class="str">"cmt">// 检查逐笔报价参数 class="type">long& sumVolBuy, class=class="str">"cmt">// 总计买入交易量 (out) class="type">long& sumVolSell class=class="str">"cmt">// 总计卖出交易量 (out) ) { class=class="str">"cmt">//--- 检查逐笔报价方向 if(( tick.flags&TICK_FLAG_BUY)==TICK_FLAG_BUY && ( tick.flags&TICK_FLAG_SELL)==TICK_FLAG_SELL) class=class="str">"cmt">// 如果逐笔报价是双向的 Print(__FUNCTION__,": ERROR! Tick &class="macro">#x27;"+GetMsToStringTime(tick.time_msc)+"&class="macro">#x27; is of unknown direction!"); else if(( tick.flags&TICK_FLAG_BUY)==TICK_FLAG_BUY) class=class="str">"cmt">// 万一是买入 sumVolBuy+=(class="type">long)tick.volume; else if(( tick.flags&TICK_FLAG_SELL)==TICK_FLAG_SELL) class=class="str">"cmt">// 万一是卖出 sumVolSell+=(class="type">long)tick.volume; else class=class="str">"cmt">// 如果逐笔报价并非交易 Print(__FUNCTION__,": ERROR! Tick &class="macro">#x27;"+GetMsToStringTime(tick.time_msc)+"&class="macro">#x27; is not a trading one!"); } class=class="str">"cmt">//--- class="num">5. 向缓冲区输入数值 DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
「实时计算的七个执行节点」
MT5 自定义指标在实时行情下跑 delta 成交量,核心不是每笔 tick 都重算,而是抓住新柱线成形这一刻做状态复位。rates_total 大于 prev_calculated 就说明来了新 K 线,这时先把 rates_total-1 的缓冲区用 EMPTY_VALUE 清空,避免上一根残留值污染图形。 如果此前在 rates_total-2 这根刚收盘的柱线上挂过重复控制标志(_repeatedControl 且 _controlNum 匹配),就要触发重检查,把那根柱子的逐笔报价重新拉一遍,重算买卖量与 delta 再和参考值比对。这一步专治实时阶段漏算或报价延迟导致的交易量偏差。 实际取数靠 GetTicks() 和底层的 CopyTicks(),拿到上次指标启动后的新逐笔报价后,用 SetFrom() 记住最后一条报价时间,下次启动直接续接历史,不重复下载。最后调 CalculateCurrentBar(false, ...) 进实时分支——firstLaunch 传 false,和历史预计算共用同一过程但走不同路径。 外汇与贵金属市场高杠杆、滑点频繁,实时重检查只能降低漏算概率,不保证 delta 判定绝对准确,参数调完建议开 MT5 用真实 tick 验证至少 200 根柱线。
class=class="str">"cmt">//--- class="num">1. 检查新柱线成形 if(rates_total>prev_calculated) class=class="str">"cmt">// 如果新柱线情况 { class=class="str">"cmt">//--- 用空值初始化 rates_total-class="num">1 缓冲区索引 BuffersIndexInitialize(rates_total-class="num">1,EMPTY_VALUE); class=class="str">"cmt">//--- class="num">2. 检查 rates_total-class="num">2 的柱线交易量是否应该被跟踪 if(_repeatedControl && _controlNum==rates_total-class="num">2) { class=class="str">"cmt">//--- class="num">3. 重检查 RepeatedControl(false,_controlNum,time[_controlNum]); } class=class="str">"cmt">//--- class="num">4. 重置再次检查数值 _repeatedControl=false; _controlNum=WRONG_VALUE; } class=class="str">"cmt">//--- class="num">5. 下载新的逐笔报价 if(!_ticks.GetTicks() ) class=class="str">"cmt">// 如果不成功 class="kw">return( prev_calculated ); class=class="str">"cmt">// 出错退出 class=class="str">"cmt">//--- class="num">6. 记住所获得的最后一次逐笔报价历史的时刻 _ticks.SetFrom(); class=class="str">"cmt">//--- class="num">7. 实时计算 CalculateCurrentBar(false,rates_total,time,volume);
◍ 测试器逐笔报价是单发还是集簇
MT5 策略测试器本质是个独立程序,即便功能上和终端趋同,处理逐笔报价的方式也和实盘终端不一样。终端里同一时间戳可能一次性塞进来多笔业务,形成报价集簇;而测试器在基于真实逐笔报价模式下,会把集簇里的每一笔依次单独投喂给 OnCalculate,严格“一次一个”。 这种差异不意味着谁更好或更糟,只代表回测和实盘的指标计算路径可能略有偏差,尤其涉及成交量控制和依赖报价到达顺序的逻辑时。外汇与贵金属杠杆高、滑点不可控,回测通过不代表实盘同效,需把这种偏差纳入风险考量。 想自己验证,在 MT5 里加载 test_tickPack 指标,按 F12 切到“基于真实逐笔报价”模式跑一遍。下面这段日志就是某次运行的近似输出:2018.07.13 10:00:00 这一秒内,OnCalculate 先后收到 4、2、3、4、5、6、7 笔 tick,时间戳从 .564 一路挤到 .572,全是 FLAG_BUY,直观展示了测试器把集簇拆散逐次递送的特性。
class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00 OnCalculate: Received ticks class="num">4. [class="num">0] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.564, [class="num">3] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571 FLAG_BUY class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00 OnCalculate: Received ticks class="num">2. [class="num">0] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571, [class="num">1] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571 FLAG_BUY class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00 OnCalculate: Received ticks class="num">3. [class="num">0] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571, [class="num">2] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571 FLAG_BUY class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00 OnCalculate: Received ticks class="num">4. [class="num">0] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571, [class="num">3] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571 FLAG_BUY class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00 OnCalculate: Received ticks class="num">5. [class="num">0] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571, [class="num">4] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571 FLAG_BUY class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00 OnCalculate: Received ticks class="num">6. [class="num">0] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571, [class="num">5] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571 FLAG_BUY class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00 OnCalculate: Received ticks class="num">7. [class="num">0] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.571, [class="num">6] = class="num">2018.07.class="num">13 class="num">10:class="num">00:class="num">00.572 FLAG_BUY
从 Delta 条到自研成交量的下一步
蓝条代表某根烛台上买方主导、红条代表卖方主导,这套 Delta 着色逻辑直接把逐笔报价的买卖压力可视化了。在 RTS-6.18 合约上跑出来的图例显示,颜色切换往往领先于价格实体的反向刺穿,可作为临场读盘的一个辅助信号。 基于逐笔报价去估算实际成交量,等于给股票类品种补上了 MT5 常规成交量缺失的那块拼图。这类还原出来的真实成交量,对辨别价格走势背后的参与深度比默认 Volume 指标更贴近盘口事实。 把这个 Delta 指标当作种子,往上能扩出一系列基于真实成交密度的衍生工具。外汇与贵金属虽无集中撮合的真实逐笔池,但借助经纪商流式报价做近似还原时,仍要清楚点差滑点会放大误判,属高风险验证。 改进版指标会在作者资料的产品板块放出,届时直接拖进 MT5 用历史品种回看着色节奏,比读文字更快摸清边界。
「把这条线请下神坛」
上面那套 Delta 指标配套文件,实际就四个件:Delta_article.mq5 是指标本体,Ticks_article.mqh 管逐笔报价,test_getTicksRange.mq5 测一毫秒内多簇报价,test_tickPack.mq5 验回测有没有吃到 tick。压缩包 MQL5.zip 标称 16.21 KB,想在 MT5 里复现,直接把这几个拖进对应目录编译即可。 从评论区看,2023 到 2025 年一直有人找「改进版累积 Delta」却找不到,说明原版只是最小可用骨架,不是拿来就能实盘的圣杯。外汇和贵金属杠杆高、滑点狠,这类基于成交差的工具只帮你看博弈倾斜,信号失效是常态。 真要验它的边界,自己开 MT5 用 test_tickPack 跑一遍历史,看哪些品种 tick 密度够、哪些直接空窗。工具是人手的延伸,别供着。