MetaTrader 5 中的出价/要价(Bid/Ask)点差分析·进阶篇
📊

MetaTrader 5 中的出价/要价(Bid/Ask)点差分析·进阶篇

(2/3)· 市价单进出场看似只差几个点,往返成本却可能吃掉你 40% 的风险本金,这篇用 MT5 真实数据拆开看

含代码示例 第 2/3 篇

不少交易者盯着平台显示的 5 点点差觉得手续费公道,却从没算过新闻波动时实际平均点差会翻倍。用 5 点止损剥头皮的人,往往在被点掉 2 个点成本后才发现胜率根本撑不住。本篇接上篇概念,继续深挖 MT5 里 Bid/Ask 的历史报价该怎么拉出来算账。

「把点差换算成跨品种可比的标准点」

做跨货币对点差对比时,直接拿各自平台的 _Point 比没意义,因为报价精度不同。原文思路是锚定 EURUSD 的 5 点标准点差(0.00005 除以前一根 M1 收盘价,约对应 1.2 的 EURUSD 标准价),再按当前品种的近期均价把这套尺度缩放到本品种,得到 stdPoint。 具体做法是取最近不超过 200 根 K 线的收盘价求和算 avgPrice,再用 avgPrice*stdSpread/5.0 并保留 6 位小数得到 stdPoint。这样画水平线时,每 5 个 stdPoint 就是「一个 EURUSD 标准点差」的当量,肉眼就能看出当前品种点差是 EURUSD 的几倍。 代码里把 0 到 9 级水平线依次设为 0、5*stdPoint 到 45*stdPoint,相当于在副图上标出 0~9 个标准点差的台阶。外汇与贵金属杠杆高、点差跳动快,这类标准化仅用于横向观察,不预示任何方向。 实盘验证时把 IndicatorSetInteger(INDICATOR_LEVELS,20) 留着,前面只写了 10 条线,剩下 10 条若不齐补上即可;改 numRecentBarsBack 能调节 avgPrice 的回看窗。

MQL5 / C++
  class=class="str">"cmt">// Get the standard class="num">5 point spread for the standard EURUSD currency
  class="type">class="kw">double stdSpread=class="num">0.00005/iClose("EURUSD",PERIOD_M1,class="num">1); class=class="str">"cmt">// class="num">1.2 ~=  EURUSD std price
  
  class=class="str">"cmt">//Find out the current average price of the instrument we are class="kw">using, so we can standardise the spread and _Point
  class="type">int CheckAvgPriceBars=MathMin(numRecentBarsBack, class="num">200);
  
  class="type">int i=class="num">0;
  for(; i<CheckAvgPriceBars; i++)
  {
      sumPrice+=iClose(_Symbol,PERIOD_CURRENT,i);
  }
  avgPrice=sumPrice/(i? i: class="num">1.0);
  
  class=class="str">"cmt">//convert the stdSpread to stdPoint by dividing by class="num">5, so we compare  apples with apples, not oranges
  class="type">class="kw">double stdPoint=StringToDouble(DoubleToString(avgPrice*stdSpread/class="num">5.0,class="num">6));
  Print(i, "=bars done, avgPrice=", DoubleToString(avgPrice,class="num">6),
              " std=", DoubleToString(class="num">1.2*stdSpread, class="num">6),
              " stdPoint=", DoubleToString(stdPoint, class="num">6)
        );
  
  SetIndexBuffer(class="num">0,ActSpreadBuf,INDICATOR_DATA);        
  SetIndexBuffer(class="num">1,DeclaredSpreadBuf,INDICATOR_DATA);    
  
  class="type">class="kw">string indName ="BAS("+_Symbol;
          indName+=" TF="+class="type">class="kw">string(_Period);
          indName+=" stdPoint="+DoubleToString(stdPoint, class="num">6);
          indName+=") Last("+class="type">class="kw">string(numRecentBarsBack)+") Bars";
          
  IndicatorSetString(INDICATOR_SHORTNAME, indName);
  
  IndicatorSetInteger(INDICATOR_DIGITS,class="num">6);
  
  IndicatorSetDouble(INDICATOR_MINIMUM, class="num">0.0);
  IndicatorSetInteger(INDICATOR_LEVELS, class="num">20);    
  
  class=class="str">"cmt">//mark out each standard EURUSD class="num">5 point spread, to compare this currencies spread with EURUSD
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">0,  class="num">0.000000);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">1,  class="num">5*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">2, class="num">10*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">3, class="num">15*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">4, class="num">20*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">5, class="num">25*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">6, class="num">30*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">7, class="num">35*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">8, class="num">40*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">9, class="num">45*stdPoint);

用标准差点值铺出 50 到 95 的网格线

自定义指标里想画一条随品种波动尺度自适应的参考带,靠的是 IndicatorSetDouble 配合 INDICATOR_LEVELVALUE 枚举。上面这段把第 10 到第 19 条水平线分别设成 50 到 95 乘以 stdPoint 的值,stdPoint 通常是 NormalizeDouble(_Point, digits) 这类标准化后的点值,所以 XAUUSD 和 EURUSD 上同一行代码画出来的绝对价格间距会差出一个数量级。 逐行拆开看:IndicatorSetDouble(INDICATOR_LEVELVALUE, 10, 50*stdPoint) 意思是给索引 10 的 Level 设定数值为 50 倍点值;索引从 10 起跳是为了把前 0~9 号留给更低的带(比如 0~45)。一直到索引 19 的 95*stdPoint,十档线等距铺开,步长固定为 5*stdPoint。 回 MT5 里把 stdPoint 打印出来对比:EURUSD 的 _Point 是 0.00001,50*stdPoint 仅 0.0005;XAUUSD 的 _Point 是 0.01,同样一行就是 0.5 美金。外汇与贵金属杠杆高、跳空频繁,这类参考带只提示概率区域,破带不意味趋势确认,实盘请先开策略测试器验证间距是否贴合你的周期。

MQL5 / C++
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">10,class="num">50*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">11,class="num">55*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">12,class="num">60*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">13,class="num">65*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">14,class="num">70*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">15,class="num">75*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">16,class="num">80*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">17,class="num">85*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">18,class="num">90*stdPoint);
  IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">19,class="num">95*stdPoint);
  
class="kw">return(INIT_SUCCEEDED);
}

◍ 用 CopyTicksRange 圈住单根 K 线的tick流

在 MT5 自定义指标里,想精确统计某根蜡烛内部的逐笔报价,关键不是全量拉取,而是用 CopyTicksRange() 只截取「前一根柱起点」到「当前柱起点」这段毫秒区间。datetime 数组 time[] 精度只到秒,所以必须乘 1000 转成毫秒再喂给函数,否则区间边界会错位。 代码里先把日志日期格式定为 含分钟 的输出(dtFormat),OnCalculate 开头就拦了两个异常:_StopFlag 置位或 bars 数不足 2 直接弹 Alert 并原样返回,避免空数据崩脚本。 prev_calculated 的控制逻辑很实用:大于 2 且等于 rates_total 时,只给当前柱写 declared spread 就 return;若是新柱刚开始,则把 ActSpreadBuf 当前位清成 EMPTY_VALUE 再 return,等于把真实价差缓冲在新 K 线开盘时重置。 static int start 用 rates_total - numRecentBarsBack 锁定回看起点,随后声明 MqlTick tickBuf[] 及求和变量。出价与要价 tick 分别计数(bid_tick / ask_tick),虽然示例未画图,但 bid 的 tick 数应当和 tick_volume[] 里的值对得上——打开数据窗口就能交叉验证。外汇与贵金属杠杆高,tick 统计仅作微观结构参考,不代表任何方向判定。

MQL5 / C++
class=class="str">"cmt">//--- Global variables
class=class="str">"cmt">//--- Set the date formatting for printing to the log
class="kw">const class="type">uint dtFormat=class="type">uint(TIME_DATE|TIME_MINUTES);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator iteration function                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double &open[],
                class="kw">const class="type">class="kw">double &high[],
                class="kw">const class="type">class="kw">double &low[],
                class="kw">const class="type">class="kw">double &close[],
                class="kw">const class="type">long &tick_volume[],
                class="kw">const class="type">long &volume[],
                class="kw">const class="type">int &spread[])
{
  class=class="str">"cmt">//--- Check for no data or Stop flag before we begin              
  if(_StopFlag || rates_total<class="num">2)  
  {
      Alert("Error, StopFlag=", _StopFlag, " class="macro">#Bars=", rates_total);    
      class="kw">return(rates_total);  
  }

  class=class="str">"cmt">//only do the report at indicator start up or refresh 
  if(prev_calculated>class="num">2)
  {             
    class=class="str">"cmt">// if we have already nulled the ActSpreadBuf just do the DeclaredSpreadBuf[] and class="kw">return.
    if(prev_calculated==rates_total)
    {
      class="type">int currBar=rates_total-class="num">1;
      DeclaredSpreadBuf[currBar]=spread[currBar]*_Point;
      class="kw">return(rates_total);
    }
    class=class="str">"cmt">// else its the start of a new bar so null the ActSpreadBuf 
    else
    {
      class="type">int currBar=rates_total-class="num">1;
      ActSpreadBuf[currBar]=EMPTY_VALUE;
      class="kw">return(rates_total);
    }
  }

  class="kw">static class="type">int start=rates_total-numRecentBarsBack;

  class="type">MqlTick tickBuf[];

  class="type">class="kw">double sumSpread=class="num">0;
  class="type">class="kw">double thisSpread=class="num">0;

  class="type">int ticks=class="num">0;
  class="type">int bid_tick=class="num">0;
  class="type">int ask_tick=class="num">0;
  class="type">int k=class="num">0;
}

「用逐笔 tick 算真实平均点差」

经纪商在 MT5 里通过 spread[] 数组给的是「声明点差」,但盘口实际成交摩擦要用 bid/ask 逐笔 tick 重新算一遍才靠得住。下面这段逻辑就是按每根 K 线的时间窗抓取 info 类 tick,累加 ask-bid 再除以 tick 数,得到该窗内的真实平均点差。 先给两个缓冲区 ActSpreadBuf、DeclaredSpreadBuf 填 EMPTY_VALUE,避免历史残留污染绘图。随后从 start 到 rates_total 遍历,每根 bar 用 CopyTicksRange 拉取上一根 bar 起止时间(乘 1000 转毫秒)内的 COPY_TICKS_INFO 数据,只关心买卖价变动。 循环里用位与判断 TICK_FLAG_ASK / TICK_FLAG_BID 分别计数,同时把每次 tick 的 ask-bid 累加进 sumSpread。若 ticks>0 才做除法写进 ActSpreadBuf[i-1],否则写 EMPTY_VALUE——这一步防的是极端行情或数据缺口下的除零崩坏。DeclaredSpreadBuf 则直接存 spread[i-1]*_Point 作对照。 开启 doPrint 后,终端会按 bar 打印 NumTicks、买卖 tick 数、平均点差(已转成点数保留 1 位)和声明点差。跑一遍你会发现,伦敦盘重叠时段平均点差可能比声明值低 0.3~0.8 点,而数据行情瞬间可能飙到 3 倍以上,外汇与贵金属杠杆交易高风险,点差异常扩张会直接吃掉止损空间。

MQL5 / C++
ArrayInitialize(ActSpreadBuf, EMPTY_VALUE);
ArrayInitialize(DeclaredSpreadBuf, EMPTY_VALUE);

for(class="type">int i=start; i<rates_total; i++)
{
   sumSpread=class="num">0;
   thisSpread=class="num">0;
   bid_tick=class="num">0;
   ask_tick=class="num">0;
   k=class="num">0;
   
   ticks=CopyTicksRange(_Symbol, tickBuf,
                         COPY_TICKS_INFO, class=class="str">"cmt">// Only bid and ask changes are required
                         time[i-class="num">1]*class="num">1000,  class=class="str">"cmt">// Start time of previous bar
                         time[i  ]*class="num">1000   class=class="str">"cmt">// End time of previous bar
                         );
   
   class="kw">while(k<ticks)
   {
       if((tickBuf[k].flags&TICK_FLAG_ASK)==TICK_FLAG_ASK)  
           ask_tick++;
           
       if((tickBuf[k].flags&TICK_FLAG_BID)==TICK_FLAG_BID)  
           bid_tick++;
       
       sumSpread+=tickBuf[k].ask-tickBuf[k].bid;
       
       k++;
   }
   
   class=class="str">"cmt">// Ensure no divide by zero errors for any missing tick data
   if(ticks>class="num">0) {                      
       thisSpread=sumSpread/ticks;
       ActSpreadBuf[i-class="num">1]=thisSpread;  
   }
   else  {
       thisSpread=class="num">0.0;
       ActSpreadBuf[i-class="num">1]=EMPTY_VALUE; 
   }
   DeclaredSpreadBuf[i-class="num">1]=spread[i-class="num">1]*_Point;
   
   if(doPrint)
   {                 
       Print(TimeToString(time[i-class="num">1], dtFormat),
             "  NumTicks="+class="type">class="kw">string(ticks),
             "  b="+class="type">class="kw">string(bid_tick),
             "  a="+class="type">class="kw">string(ask_tick),
             "  AvgSpread=",  DoubleToString(thisSpread/_Point, class="num">1),
             "  DeclaredSpread=", class="type">class="kw">string(spread[i-class="num">1])
       );
   }

}
把点差核算交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到历史平均点差与成本占比,不用自己导报价写脚本。

常见问题

当前点差只是当下买卖指示线的差值,而真实平均点差需统计近期成交时的 Bid/Ask 偏离,新闻或波动期常达平台宣称值两倍。
若实际平均点差 2 点,往返成本占 5 点止损的 40%,价格仅回撤 3 点就触发止损,胜率要求被大幅抬高。
可通过 CopyTickRange 或 MqlTick 结构在逐笔回调中记录 bid/ask,再按时间段求均值,具体代码见本节示例。
可以,小布盯盘对应品种页已用 AIGC 算出历史平均点差与不同止损尺度的成本百分比,省去手动建模。
作者实测多数小波动约 7 点,5 点腿长偏短易被噪声扫损,可参考 10 点止损留盈灵活平仓的思路。