价格行为分析工具包开发(第二十七部分):利用移动平均线进行流动性扫单·进阶篇
(2/3)·零售交易者总在支撑阻力假突破处被止损,本篇用MA过滤把聪明钱扫单看清
流动性扫单EA的骨架与信号闸门
这套 EA 的核心任务是在 MT5 任意图表上标出假突破式的流动性扫单:监测一根 K 线跌破前一根波段低点(或涨破高点)后以套牢盘姿态收盘,再叠加可选 MA 过滤对齐趋势。代码开头用 #property strict 强制编译器做类型与语法严检,未声明变量或类型不匹配会在运行前被拦下,而不是等到实盘报错。 输入参数分三组:MA 组可切 UseMAFilter、MALength(周期)及六种类型(SMA/EMA/LWMA/VWMA/RMA/HMA);判定组用 SignalStrict 枚举切换宽松/严格模式,宽松只要求 l<l1(看涨),严格额外要求 c>h1;显示组用 LabelType 控制 BS 短标或 Bull Sweep 全文,ArrowOffsetPoints 把箭头推离 K 线避免重叠。ColorChangeOnly 开启后,必须出现 K 线颜色反转才留信号。 全局变量 lastBarTime 存最近处理 K 线时间,每个 tick 用 iTime(Symbol(),Period(),0) 比对,不同才跑 DetectLiquiditySweep(1),保证每根闭合 K 线最多一次信号,从机制上掐掉重绘。MAHandle 初始化为 INVALID_HANDLE,OnInit 里若 MALength<1 直接返回 INIT_FAILED,防御性杜绝 0/负周期计算。 DetectLiquiditySweep 先算 requiredBars=shift+MALength,Bars() 不足就 return,防止数组越界;之后取 shift 与 shift+1 的 o/h/l/c 共 8 个值。宽松看涨条件为 c>o 且 l<l1 且 c>o1 且 c1!=o1,严格则加 c>h1。MA 过滤走 CopyBuffer 取 maValue,PriceAboveMA=true 时只留 c>maValue 的看涨信号。最后 DrawSignal 画图、Experts 日志写 "Bullish sweep detected at [time], price=[close]",并弹屏警报告知。
◍ 箭头与均线的解耦画法
DrawSignal 函数把图表标注和信号检测拆开处理。它先用 iTime 取指定 shift 的 K 线开盘时间 t,再按品种最小变动单位 _Point 算偏移:看涨箭头落在最低价减 ArrowOffsetPoints*_Point,看跌落在最高价加同样偏移。EURUSD 的 _Point 是 0.0001,USDJPY 是 0.01,用 _Point 就不用给每个品种手写偏移量。 对象名用 StringFormat("LS_%I64u", (long)t) 拼出来,因为 K 线开盘时间是唯一 64 位整数,重跑 EA 也不会撞名。画之前 ObjectFind 查重,有就先删,避免图表刷新叠一堆冗余标记。PlotArrow 为 true 就画彩色箭头;为 false 且 LblType 非空,就改画 "BS" 或 "Bull Sweep" 这类文本。 非标准均线藏在两个辅助函数里。CalcVWMA 跑 MALength 根 K 线,把每根收盘价乘成交量(iVolume 读成 64 位再转 double)累进 numerator,成交量累进 denominator,最后返回 numerator/denominator,denominator 为 0 时直接给 0.0 防崩。这就是标准成交量加权均价。 CalcHMA 分两段加权来压滞后:前半段 half=MALength/2,权重从 half 递减到 1 算 w1;全段权重从 MALength 递减到 1 算 w2;HMA = 2*w1 - w2。近期权重被放大,波动市里对拐点的反应可能比普通 MA 快。EA 只在 Bars() 大于 shift+MALength 时才调它,历史不够就跳过。 下方输入参数里 MALength 默认 20、MAType 默认 SMA、ArrowOffsetPoints 默认 10,改这几个值就能直接看标注位置和高低。外汇与贵金属杠杆高,回测结论只代表历史概率,实盘前请在 MT5 用策略测试器自跑一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Liquidity Sweep with MA filter| class=class="str">"cmt">//| Copyright class="num">2025, MetaQuotes Ltd.| class=class="str">"cmt">//| [MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2025, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.0" class="macro">#class="kw">property strict class="macro">#include <Trade\Trade.mqh> class=class="str">"cmt">//--- Inputs: Moving Average Filter input class="type">bool UseMAFilter = class="kw">false; class=class="str">"cmt">// Enable Moving Average Filter input class="type">bool ShowMA = class="kw">false; class=class="str">"cmt">// Show MA on chart input class="type">int MALength = class="num">20; class=class="str">"cmt">// MA period(must be >=class="num">1) enum MA_Type {SMA=class="num">0, EMA, LWMA, VWMA, RMA, HMA}; input MA_Type MAType = SMA; class=class="str">"cmt">// Moving Average type input class="type">bool PriceAboveMA = true; class=class="str">"cmt">// Filter: price above MA? class=class="str">"cmt">//--- Inputs: Sweep Definition Strictness enum Strictness {LessStrict=class="num">0, Strict}; input Strictness SignalStrict = LessStrict; class=class="str">"cmt">// Signal strictness input class="type">bool ColorChangeOnly = class="kw">false; class=class="str">"cmt">// Only on class="type">class="kw">color-change candles class=class="str">"cmt">//--- Inputs: Label/Arrow & Color Customization enum LabelType {None=class="num">0, Short, Full}; input LabelType LblType = Full; class=class="str">"cmt">// Label type(None/Short/Full) input class="type">bool PlotArrow = true; class=class="str">"cmt">// Draw arrow on signal input class="type">int ArrowOffsetPoints = class="num">10; class=class="str">"cmt">// Offset(in points) above/below candles
「用新K线触发而非每跳重算」
MT5 上若把检测逻辑直接丢进 OnTick,每来一个报价就跑一遍,CPU 白白烧在重复计算上。上面这段把「当前 Bar 开盘时间」存进 lastBarTime,只有 iTime 返回的当前值跟上次不同,才调用一次 DetectLiquiditySweep(1)——也就是每根 K 线收盘切换时跑一次。 初始化阶段先卡一道 MALength:若外部输入小于 1,直接 Print 报错并返回 INIT_FAILED,避免后续数组越界。MAHandle 用 iMA 拿内置均线句柄,类型不是 VWMA/HMA 且开了过滤或显示时才建;建完若无效同样 INIT_FAILED,ShowMA 为真再用 ChartIndicatorAdd 挂到主图 0 号窗口。 OnDeinit 里用 IndicatorRelease 释放句柄,防止指标重载时句柄泄漏。DetectLiquiditySweep 开头先算 requiredBars = shift + MALength,Bars 总数不够就 return,这是实盘常见的「历史不足不交易」保护。 开 MT5 把这段贴进 EA 模板,把 MALength 设成 20 在 EURUSD 的 M15 跑一晚,你能直接看到日志里只在整点棒切换时打印检测,而非每跳刷屏。外汇与贵金属杠杆高,这类仅做结构触发的代码不预示方向,信号失效概率始终存在。
input class="type">class="kw">color BullishColor = clrLime; class=class="str">"cmt">// Color for bullish signals input class="type">class="kw">color BearishColor = clrRed; class=class="str">"cmt">// Color for bearish signals class=class="str">"cmt">//--- Globals class="type">class="kw">datetime lastBarTime = class="num">0; class=class="str">"cmt">// Timestamp of the last processed bar class="type">int MAHandle = INVALID_HANDLE; class=class="str">"cmt">// Handle for built‐in MA indicator class="type">int OnInit() { class=class="str">"cmt">// Validate MALength(cannot assign to an input directly) if(MALength < class="num">1) { Print("ERROR: MALength must be at least class="num">1. Current value = ", MALength); class="kw">return(INIT_FAILED); } class=class="str">"cmt">// Initialize timing so we only run when a new bar closes lastBarTime = iTime(Symbol(), Period(), class="num">0); class=class="str">"cmt">// … (rest of OnInit follows) … class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">// Create MA handle if class="kw">using a built-in MA(SMA, EMA, LWMA, RMA) if((MAType != VWMA && MAType != HMA) && (UseMAFilter || ShowMA)) { ENUM_MA_METHOD method = (ENUM_MA_METHOD)MAType; MAHandle = iMA(Symbol(), Period(), MALength, class="num">0, method, PRICE_CLOSE); if(MAHandle == INVALID_HANDLE) { Print("Failed to create MA handle(type=", EnumToString(MAType), ", length=", MALength, ")"); class="kw">return(INIT_FAILED); } if(ShowMA) ChartIndicatorAdd(class="num">0, class="num">0, MAHandle); } class="type">void OnDeinit(class="kw">const class="type">int reason) { if(MAHandle != INVALID_HANDLE) IndicatorRelease(MAHandle); } class="type">void OnTick() { class=class="str">"cmt">// Retrieve the current bar’s start time class="type">class="kw">datetime current = iTime(Symbol(), Period(), class="num">0); class=class="str">"cmt">// If the bar start time changed, call the detection routine once if(current != lastBarTime) { DetectLiquiditySweep(class="num">1); lastBarTime = current; } } class="type">void DetectLiquiditySweep(class="type">int shift) { class=class="str">"cmt">// Ensure there are at least(shift + MALength) bars of history class="type">int requiredBars = shift + MALength; if(Bars(Symbol(), Period()) <= requiredBars) { class=class="str">"cmt">// Not enough bars to compute custom MA or compare prices class="kw">return; } class=class="str">"cmt">// … (next steps in the function) … } class=class="str">"cmt">//--- Bar data for the current completed candle(index = shift) class="type">class="kw">double o = iOpen(Symbol(), Period(), shift); class="type">class="kw">double c = iClose(Symbol(), Period(), shift);
把流动性扫单信号接上过滤与告警
这段逻辑把前面算出的 K 线数据收口成可交易的扫单判定。先取当前与前置一根蜡烛的开高低收:当前柱用 shift 索引,前一根统一用 shift+1,避免手误写错偏移。 颜色反转标志很直白:bullCC 要求本根收大于开、且前一根收小于开;bearCC 反之。扫单本身分两档——LessStrict 只要求刺穿前低/前高并站回前一根开盘侧,Strict 则要求收盘直接越过前一根极端价(如 bullSweep 中 c > h1),假突破过滤更狠。 若勾了 ColorChangeOnly,就用 &= 把扫单与颜色反转绑定,信号必须是实体反转带动的扫单。UseMAFilter 打开后,VWMA/HMA 走自定义函数,其余类型用 CopyBuffer 取内置 MA 句柄;取不到数据直接 return,不画信号。 过滤完若 bullSweep 或 bearSweep 仍为真,调 DrawSignal 画图并用 PrintFormat 打日志,精度写到 %.5f(外汇五位数报价够用)。最后无论多空,只要触发就弹 Alert 报品种与周期。 外汇和贵金属杠杆高,扫单后延续概率不等于必然,建议先在 MT5 策略测试器用 2023 年 XAUUSD 小时图跑一遍 Strict 模式,看告警频率是否适配你的盯盘节奏。
class="type">class="kw">double h = iHigh(Symbol(), Period(), shift); class="type">class="kw">double l = iLow(Symbol(), Period(), shift); class=class="str">"cmt">//--- Bar data for the prior candle(index = shift + class="num">1) class="type">class="kw">double o1 = iOpen(Symbol(), Period(), shift + class="num">1); class="type">class="kw">double c1 = iClose(Symbol(), Period(), shift + class="num">1); class="type">class="kw">double h1 = iHigh(Symbol(), Period(), shift + class="num">1); class="type">class="kw">double l1 = iLow(Symbol(), Period(), shift + class="num">1); class=class="str">"cmt">// Compute class="type">class="kw">color-change flags class="type">bool bullCC = (c > o && c1 < o1); class="type">bool bearCC = (c < o && c1 > o1); class=class="str">"cmt">// Liquidity sweep flags(LessStrict or Strict) class="type">bool bullSweep, bearSweep; if(SignalStrict == LessStrict) { bullSweep = (c > o && l < l1 && c > o1 && c1 != o1); bearSweep = (c < o && h > h1 && c < o1 && c1 != o1); } else class=class="str">"cmt">// Strict { bullSweep = (c > o && l < l1 && c > h1 && c1 != o1); bearSweep = (c < o && h > h1 && c < l1 && c1 != o1); } class=class="str">"cmt">// If only class="type">class="kw">color-change sweeps are desired, AND‐combine with the raw sweep flags if(ColorChangeOnly) { bullSweep &= bullCC; class=class="str">"cmt">// must also be a bullish class="type">class="kw">color reversal bearSweep &= bearCC; class=class="str">"cmt">// must also be a bearish class="type">class="kw">color reversal } if(UseMAFilter) { class="type">class="kw">double maValue = class="num">0.0; class=class="str">"cmt">// Compute MA value at the same &class="macro">#x27;shift&class="macro">#x27; if(MAType == VWMA) maValue = CalcVWMA(shift); else if(MAType == HMA) maValue = CalcHMA(shift); else { class=class="str">"cmt">// Built‐in MA handle(SMA, EMA, LWMA, RMA) class="type">class="kw">double buf[]; if(CopyBuffer(MAHandle, class="num">0, shift, class="num">1, buf) != class="num">1) class="kw">return; class=class="str">"cmt">// no valid MA data available maValue = buf[class="num">0]; } class=class="str">"cmt">// Only allow bullish sweeps above MA or bearish sweeps below MA class="type">bool cond = PriceAboveMA ? (c > maValue) : (c < maValue); bullSweep &= cond; bearSweep &= !cond; } class=class="str">"cmt">// If a bullish sweep remains true after all filters, draw and log it if(bullSweep) { DrawSignal(shift, true); PrintFormat("Bullish sweep detected at %s, price=%.5f", TimeToString(iTime(Symbol(),Period(),shift)), c); } class=class="str">"cmt">// If a bearish sweep remains true after all filters, draw and log it if(bearSweep) { DrawSignal(shift, class="kw">false); PrintFormat("Bearish sweep detected at %s, price=%.5f", TimeToString(iTime(Symbol(),Period(),shift)), c); } class=class="str">"cmt">// In either case, fire a pop‐up alert if(bullSweep || bearSweep) { Alert("Liquidity Sweep detected on ", Symbol(), " ", EnumToString(Period())); } class="type">void DrawSignal(class="type">int shift, class="type">bool bullish) {