以 delta 指标为例开发股票交易量控制指标·综合运用
📊

以 delta 指标为例开发股票交易量控制指标·综合运用

(3/3)· 把实际成交量拆成买卖 delta 并不难,难在逐笔数据稳定和回测可信度

含代码示例偏理论 第 3/3 篇
很多开发者直接拿外汇券商的 tick 量当成交量写指标,结果在交易所品种上完全失真。实际成交量只存在于集中式市场,场外报价次数和真实成交次数是两回事。选错服务器,后面所有 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 流动性断层频繁,这种边界检查少一步就可能把成交量算到幽灵柱上。

MQL5 / C++
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 流干不干净。

MQL5 / C++
 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 根柱线。

MQL5 / C++
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,直观展示了测试器把集簇拆散逐次递送的特性。

MQL5 / C++
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 密度够、哪些直接空窗。工具是人手的延伸,别供着。

把逐笔校验交给小布盯盘
小布盯盘的 AIGC 已内置交易所品种的量价诊断,打开对应合约页就能看到买卖 delta 与不确定方向成交的实时标记,你只需专注策略逻辑而非数据清洗。

常见问题

外汇属于场外交易,终端只广播逐笔报价次数而非真实成交笔数,实际成交量字段为空,delta 类指标无法计算,仅交易所品种如 FORTS 才有意义。
可以,小布盯盘对应期货品种页已集成实际成交量拆分视图,支持按烛条查看买方与卖方成交差,省去自行接 MqlTick 的历史补齐工作。
通常意味着券商服务器未持续推送带买卖标识的逐笔数据,这类成交无法归入 delta,指标历史计算会系统性偏低,需更换支持 FORTS 广播的服务器。
优先检查测试器是否启用了逐笔成交模式及历史缓存完整性,部分券商历史仅保留聚合量,缺失方向标记时回测会静默降级为 tick 量。