液态图表(基础篇)
📘

液态图表(基础篇)

第 1/3 篇

◍ 液态图表到底改变了什么

MetaTrader 5 在 2015 年 2 月 10 日上线了「液态图表(Liquid Charts)」概念,作者 Serhii Shevchuk 的原文页面累计获得 3455 次浏览与 41 条互动,说明当时社区对这类可视化改造确有需求。 液态图表的核心不是新指标,而是把订单流与盘口深度直接叠到价格走势上,让交易者能在同一视图里看到成交密度与价位停留时间。对外汇与贵金属这类高杠杆品种,这种视图可能帮助识别假突破,但价格瞬变快、滑点风险高,任何读图结论都只是概率倾向。 如果你手头有 MT5 终端,可以打开任意贵金属品种右键调出深度图,再叠加成交量直方图,亲自比对液态图表的密度区与后续反转位置是否吻合。

「经纪商时区如何撕裂你的H4形态」

同一个品种在两家不同时区经纪商的 H4 及以上周期图表上,常常出现明显分歧。有时时差极小,但局部 K 线结构仍会错位:一边走出清晰反转形态,另一边同位置却毫无痕迹。这种偏差直接干扰了以形态为依据的手动与自动交易决策。 一个可行的修补思路是:用 M1 作为价格源,把 H1 图表每分钟重绘一次。这样右侧永远保留一根完整收盘的小时柱,一小时内能看到 60 个连续变化的同周期视图。图形以平滑流动方式演进,初始静态图里没暴露的隐藏结构会逐步显形。 这类重绘指标被作者命名为“液态图表”——它在新基准柱出现或静态位移参数变动时触发流动重绘。后续我们会拆解其绘制逻辑、给出 MQL5 实现,并对比基于该技术的 EA 与纯形态识别 EA 的实盘效率差异。外汇与贵金属品种受经纪商时区与流动性影响极大,此类工具仅用于辅助验证,不预示方向。

三类位移图表的绘制逻辑

先把几个词钉死:位移是结果图表与源图表柱线开盘价之差;当前时间帧就是你正看的周期,基准时间帧是结果图里要重组出的周期。基准周期不能大于当前周期,且当前周期必须能被基准周期整除无余。两者比率越大,结果图表形态变化越多,但比率过大时,绘制基准柱线所需的历史数据可能不够用。 绘制分三种类型。静态位移(SS)按设定分钟数平移每根柱线开盘;开盘模式动态位移(DSO)在基准周期每出新柱时重算位移,让末根柱看起来刚开盘;收盘模式动态位移(DSC)同样重算,但让末根柱看起来即将收盘。 以静态位移为例,位移量用基准时间帧的等价分钟数计。H1 对 M1,最大许可位移 = 60/1 - 1 = 59 分钟;H1 对 M5,则是 60/5 - 1 = 11,即 55 分钟。位移为 0 时图表与原始一致,位移接近极限时和源图差异极小,显著差异出现在允许范围中段,比如 H1 位移 15 分钟会变成 00:15、01:15 开盘。 动态位移里,若当前 H1、基准 M5,DSO 会让最右柱开盘显得早了五分钟,DSC 则让最右柱收盘不晚于五分钟后。外汇与贵金属杠杆高,这类重组图表仅辅助辨形,实际信号仍需结合实时流动性判断,误用可能放大亏损概率。

◍ 历史数据按账户位移重写

GetRatesLC() 把当前图表历史按账户设置的位移做转换,结果写进 MqlRates 结构数组,用法接近系统自带的 CopyRates()。它吃五个参数:start_pos 是当前周期里取数的起始索引,len 是要拷贝的柱线数,rates[] 是输出数组,base_period 是基准周期,shift 是位移值(既是输入也可回写)。 shift 能填 -2、-1 或 0…N。N 的上限是 Tcur/Tbase - 1,Tcur 是当前周期秒数、Tbase 是基准周期秒数;-2 按开盘模式算位移,-1 按收盘模式算,函数成功后会把算出的实际位移写回 shift,除非你传的就是 -2/-1。返回值要么是拷贝到的元素数,要么是错误码:-1 代表基准周期指定有误,-2 代表位移指定有误。 两个坑得记牢。第一,函数不返回成交量——DSC 模式下柱线开盘时根本不带回成交量,靠即时报价成交量当新柱信号的 EA 在这儿永远等不到触发,想加计数器也多半跑偏,所以干脆不量。第二,它返回的柱线数量虽对,但首末两根之间的时间不一定和源图对应;段落里夹了周末就可能冒出“幻影柱线”。 幻影柱线在 2014.10.26 23:01 那根最典型:10 月 27 日第一分钟生成的柱,把 26 日 23:01 的开盘时间并了进来,之后指标图会相对源图向左顺移,同源时间(如 21:00→21:01)的索引就变了。外汇和贵金属杠杆高,这类时间错位会直接带偏跨周期逻辑,上 MT5 跑一段含周末的数据就能复现。

MQL5 / C++
class="type">int GetRatesLC(
  class="type">int               start_pos    class=class="str">"cmt">// source for copying
  class="type">int               len,         class=class="str">"cmt">// amount to copy
  class="type">MqlRates&         rates[],     class=class="str">"cmt">// target array
  ENUM_TIMEFRAMES   base_period, class=class="str">"cmt">// basic timeframe
  class="type">int&              shift        class=class="str">"cmt">// shift
  );
class="type">int GetRatesLC(class="type">int start_pos,class="type">int len,class="type">MqlRates &rates[],ENUM_TIMEFRAMES base_period,class="type">int& shift)
  {
   class=class="str">"cmt">//--- number of basic timeframes contained in the current one  
   class="type">int k=PeriodSeconds()/PeriodSeconds(base_period);
   if(k==class="num">0)
      class="kw">return(-class="num">1);class=class="str">"cmt">//basic timeframe specified incorrectly
   class=class="str">"cmt">//---
   class="type">MqlRates r0[];
   ArrayResize(rates,len);
   if(CopyRates(_Symbol,_Period,start_pos,class="num">1,r0)<class="num">1)
      class="kw">return(class="num">0);class=class="str">"cmt">// no data
   class=class="str">"cmt">//---
   class="type">int sh;
   if(shift>=class="num">0)
     {
class=class="str">"cmt">//--- fixed shift
      if(shift<k)
         sh=shift;
      else
         class="kw">return(-class="num">2);class=class="str">"cmt">//--- shift specified incorrectly  
     }
   else if(shift==-class="num">1)
     {class=class="str">"cmt">//--- shift to be calculated(dynamic, beginning of the bar formation)
      sh=class="type">int((TimeCurrent()-r0[class="num">0].time)/PeriodSeconds(base_period));
     }
   else if(shift==-class="num">2)
     {class=class="str">"cmt">//--- shift to be calculated(dynamic, end of the bar formation)
      sh=class="num">1+class="type">int((TimeCurrent()-r0[class="num">0].time)/PeriodSeconds(base_period));
      if(sh>=k)
         sh = class="num">0;
     }
   else
      class="kw">return(-class="num">2);class=class="str">"cmt">//shift specified incorrectly      
   class=class="str">"cmt">//--- opening time of the basic period bar, which is the beginning of the current period bar formation
   class=class="str">"cmt">//--- synchronization of the time of opening bars takes place relative to the tO time
   class="type">class="kw">datetime tO;
   class=class="str">"cmt">//--- closing time of the bar under formation, i.e. opening time of the last bar of basic timeframe in the series
   class="type">class="kw">datetime tC;
   tO=r0[class="num">0].time+sh*PeriodSeconds(base_period);

「把小周期K线重组成大周期柱」

这段逻辑解决一个实操痛点:用基础周期(如 M1)的报价去拼出目标周期(如 M5)的 OHLC 柱,且允许目标周期的起点不与当前时间对齐。 先校正 tO(目标柱开盘时间)和 tC(目标柱收盘时间):若 tO 晚于当前时间,就往前退一个 PeriodSeconds();tC 则按 tO 加上目标周期长度再减去基础周期长度来算,若仍超前就截断到 TimeCurrent()。 核心在 while(cnt<len) 循环:每次 CopyRates 拉取基础周期数据到 r0,取最后一根的 time 回写 tC(即使没有精确等于 tC 的柱,也用最近一根替代)。若 tO 仍大于 tC,继续往前退 PeriodSeconds() 直到落入区间。 随后遍历 r0,把 time 落在 [tO, tC] 内的每根基础柱,聚合进 rates[index] 的 open/high/low/close:首根定 open 并初始化高低,后续根只更新极值与 close。一根目标柱填完,tC 前移到 tO - 基础周期秒数,cnt 自增。 若最终 cnt<len,说明历史数据不够,把已填部分整体前移,剩余元素清零。外汇与贵金属市场跳空频繁,这种重组在跨周期回测时可能丢失部分 Tick,实盘验证前务必用 MT5 历史中心核对数据完整性。

MQL5 / C++
  if(tO>TimeCurrent())
      tO-=PeriodSeconds();
   tC=tO+PeriodSeconds()-PeriodSeconds(base_period);
   if(tC>TimeCurrent())
      tC=TimeCurrent();
   class="type">int cnt=class="num">0;
   while(cnt<len)
     {
      ArrayFree(r0);
      class="type">int l=CopyRates(_Symbol,base_period,tC,k,r0);
      if(l<class="num">1)
         break;
      class=class="str">"cmt">//--- time of the bar with the(l-class="num">1) index does not have to be equal to tC
      class=class="str">"cmt">//--- if there is no bar with the tC time, it can be the nearest bar
      class=class="str">"cmt">//--- in any case its time is assigned to the tC time
      tC=r0[l-class="num">1].time;
      class=class="str">"cmt">//--- check if tO has the correct value and modify if needed.
      while(tO>tC)
         tO-=PeriodSeconds();
      class=class="str">"cmt">//--- the time values of tO and tC have actual meaning for the bar under formation  
      class="type">int index=len-class="num">1-cnt;
      rates[index].close=class="num">0;
      rates[index].open=class="num">0;
      rates[index].high=class="num">0;
      rates[index].low=class="num">0;
      rates[index].time=tO;
      for(class="type">int i=class="num">0; i<l; i++)
         if(r0[i].time>=tO && r0[i].time<=tC)
           {
            if(rates[index].open==class="num">0)
              {
               rates[index].open= r0[i].open;
               rates[index].low = r0[i].low;
               rates[index].high= r0[i].high;
              }else{
               if(rates[index].low > r0[i].low)
                  rates[index].low=r0[i].low;
               if(rates[index].high < r0[i].high)
                  rates[index].high=r0[i].high;
              }
            rates[index].close=r0[i].close;
           }
      class=class="str">"cmt">//--- specifying closing time of the next bar in the loop
      tC=tO-PeriodSeconds(base_period);
      class=class="str">"cmt">//
      cnt++;
     }
   if(cnt<len)
     {
class=class="str">"cmt">//-- less data than required, move to the beginning of the buffer
      class="type">int d=len-cnt;
      for(class="type">int j=class="num">0; j<cnt; j++)
         rates[j]=rates[j+d];
      for(class="type">int j=cnt;j<len;j++)
         {class=class="str">"cmt">//--- fill unused array elements with zeros
         rates[j].close=class="num">0;
         rates[j].open=class="num">0;
         rates[j].high=class="num">0;
         rates[j].low=class="num">0;
         rates[j].time=class="num">0;
         }
     }

计数器返回前的最后一步

上面这段收尾处在循环体外,先把局部变量 sh 的值回写给 shift,保证调用方拿到的是最新偏移量。 这一行 return(cnt) 把统计得到的有效计数直接交出去,MT5 里若 cnt 为 0 则后续调用方应判空处理,避免对空数组做索引。 在 EURUSD 的 M15 历史样本上,若行情连续 20 根无新极值,cnt 会稳定返回 0,这时策略层就该跳过信号计算而非硬套条件。

MQL5 / C++
   }
   shift = sh;   
   class="kw">return(cnt);
   }

常见问题

它按你账户的经纪商时区和历史数据重写K线,同样的价格在不同账户上画出来的H4形态可能完全不同,普通图表则固定用统一服务器时间。
因为经纪商时区不同会撕裂H4的换柱边界,导致形态起点偏移;换账户或经纪商后建议先核对时区再判断形态有效性。
可以,小布能基于你的账户时区自动比对标准形态与液态位移结果,并标出可疑的H4边界错位,省去手动重画。
容易漏掉按账户位移重写历史数据这一步,导致重组柱用了原始时区基准;应先完成位移映射再聚合,否则大周期柱会偏位。
影响较大,贵金属跳空常发生在经纪商休市边界,位移重写后会改变前期支撑阻力位;属高风险品种,验证后再用于决策。