MetaTrader 5 中的出价/要价(Bid/Ask)点差分析·进阶篇
(2/3)· 市价单进出场看似只差几个点,往返成本却可能吃掉你 40% 的风险本金,这篇用 MT5 真实数据拆开看
不少交易者盯着平台显示的 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 的回看窗。
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 美金。外汇与贵金属杠杆高、跳空频繁,这类参考带只提示概率区域,破带不意味趋势确认,实盘请先开策略测试器验证间距是否贴合你的周期。
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 统计仅作微观结构参考,不代表任何方向判定。
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 倍以上,外汇与贵金属杠杆交易高风险,点差异常扩张会直接吃掉止损空间。
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]) ); } }