
流动性攫取交易策略
流动性攫取交易策略是智能资金概念(SMC)的核心组成部分,旨在识别并利用市场中机构投资者的操作行为。该策略聚焦于高流动性区域(如支撑位或阻力位),在这些区域,大额订单可引发价格波动,随后市场恢复原有趋势。本文将详细阐释流动性攫取的概念,并概述如何在MQL5中开发流动性攫取交易策略的智能交易系统(EA)。
策略概述:核心概念与战术
流动性攫取策略聚焦于利用高流动性区域(如支撑位和阻力位)进行交易,机构交易者常在这些区域操纵价格以触发止损单并加剧市场波动。该策略利用常见的市场动态来预测并利用这些价格波动获利。
核心概念
- 止损猎杀:推动价格触发止损单,引发连锁式买入或卖出。
- 订单分层与虚假下单:使用虚假订单误导交易者对市场方向的判断。
- 冰山订单:将大额交易拆分为多个可见的小额订单以隐藏真实交易规模。
- 动量点燃:制造人为的动量以吸引其他交易者入场,随后反向操作。
- 支撑与阻力位操纵:利用关键价格水平引发可预测的市场反应。
- 心理价位点:利用整数价位影响交易者的行为。
市场操纵是指故意影响证券价格或交易量,以制造误导性的交易环境。尽管大多数机构交易者都遵守法律和道德标准,但仍有部分人可能会采取操纵性手段来实现特定的战略目标。以下是关于市场操纵发生的原因及方式的简要概述:
动机:
- 通过短期价格波动和套利实现利润最大化。
- 向竞争对手隐瞒交易意图。
- 以最小的市场影响执行大额交易。
战略:
- 触发止损单以创造流动性或推动价格走势。
- 利用冰山订单或虚假下单控制订单流。
- 瞄准支撑位、阻力位或心理价位等关键价格水平。
了解这些市场动态,有助于交易者预测机构行为,并将这些见解融入自动化工具中,制定更有效的交易策略。
我们希望利用止损单的累积以及大型市场参与者的操作所引发的临时价格波动获利。通过识别流动性区域,交易者旨在在价格反转并延续原有趋势之前,以最优点位入场,从而获得有利的风险回报机会。
以下是该策略的大致示意图:
策略开发
我建议在编写EA时采用以下顺序:
- 考虑该策略所需的各项功能,并确定如何将这些功能封装到不同的组件中。
- 逐个编写每个函数,在编写过程中声明相关的全局变量或初始化相关句柄。
- 在实现每个函数后,对其进行审核,并考虑如何将它们连接起来,例如通过传递参数或在其他函数中调用函数。
- 最后,轮到OnTick()函数,并利用前面步骤中的函数来开发交易逻辑。
首先,我们尝试将规则量化。 供需关系交易(SMC)主要由主观交易者进行交易,因为其中涉及许多难以量化的细微差别。客观地说,很难定义操纵行为必须具备的确切特征。一种方法是分析订单流中的成交量变化,但经纪人提供的成交量数据往往并不可靠。在外汇等非集中交易的市场中,交易并非集中进行;而在期货等集中交易的市场中,大多数经纪人提供的是来自其流动性提供商的数据,而非集中数据。更直接且可优化的方法是采用技术分析,这也是本文将采用的方法。为了将规则量化,我们将策略简化为以下几个部分:
- 在关键价位处形成拒绝K线形态,其中关键价位定义为回溯周期内的最高点或最低点。
- 在该拒绝K线形态之后,价格反转并在较短的回溯周期内突破关键价位。
- 最后,如果整体走势与大趋势一致(由价格相对于移动平均线的位置来判断),则以固定的止损或止盈水平入场交易。
请注意,我们可以添加更多规则以更好地模拟市场操纵的特征,但建议尽量保持策略简单,以避免过度拟合。
接下来,我们编写相关函数。这些是在计算止盈和止损后执行订单、并跟踪订单票据所必需的函数。
//+------------------------------------------------------------------+ //| Expert trade transaction handling function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } //+------------------------------------------------------------------+ //| Execute sell trade function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid, _Digits); double tp = NormalizeDouble(bid - tpp * _Point, _Digits); double sl = NormalizeDouble(bid + slp * _Point, _Digits); trade.Sell(lott, _Symbol, bid, sl, tp); sellpos = trade.ResultOrder(); } //+------------------------------------------------------------------+ //| Execute buy trade function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask, _Digits); double tp = NormalizeDouble(ask + tpp * _Point, _Digits); double sl = NormalizeDouble(ask - slp * _Point, _Digits); trade.Buy(lott, _Symbol, ask, sl, tp); buypos = trade.ResultOrder(); }
这两个函数用于在给定范围内识别并返回最高点或最低点,同时通过验证该点位是否存在引发价格反转的支撑或阻力,确保该点位符合关键价位水平的标准。
//+------------------------------------------------------------------+ //| find the key level high given a look-back period | //+------------------------------------------------------------------+ double findhigh(int Range = 0) { double highesthigh = 0; for (int i = BarsN; i < Range; i++) { double high = iHigh(_Symbol, PERIOD_CURRENT, i); if (i > BarsN && iHighest(_Symbol, PERIOD_CURRENT, MODE_HIGH, BarsN * 2 + 1, i - BarsN) == i) //used to make sure there's rejection for this high { if (high > highesthigh) { return high; } } highesthigh = MathMax(highesthigh, high); } return 99999; } //+------------------------------------------------------------------+ //| find the key level low given a look-back period | //+------------------------------------------------------------------+ double findlow(int Range = 0) { double lowestlow = DBL_MAX; for (int i = BarsN; i < Range; i++) { double low = iLow(_Symbol, PERIOD_CURRENT, i); if (i > BarsN && iLowest(_Symbol, PERIOD_CURRENT, MODE_LOW, BarsN * 2 + 1, i - BarsN) == i) { if (lowestlow > low) { return low; } } lowestlow = MathMin(lowestlow, low); } return -1; }
findhigh()函数通过检查在指定范围内最高价是否出现在当前K线(i)上,以及在更大范围(回溯周期的两倍)内的最高点是否也与该K线重合,来确保在识别出的高点处存在价格受阻(拒绝)情况。这表明出现了价格受阻,因为价格在达到该水平后无法继续上破走高。如果条件成立,该函数将返回该高点值作为潜在的关键价位。findlow()函数的逻辑则与之相反。
这两个函数用于检测最后一根已收盘的K线是否在关键价位处形成受阻K线形态,这样可以被视为市场掠夺流动性的行为。
//+------------------------------------------------------------------+ //| Check if the market rejected in the upward direction | //+------------------------------------------------------------------+ bool IsRejectionUp(int shift=1) { // Get the values of the last candle (shift = 1) double open = iOpen(_Symbol,PERIOD_CURRENT, shift); double close = iClose(_Symbol,PERIOD_CURRENT, shift); double high = iHigh(_Symbol,PERIOD_CURRENT, shift); double low = iLow(_Symbol,PERIOD_CURRENT,shift); // Calculate the body size double bodySize = MathAbs(close - open); // Calculate the lower wick size double lowerWickSize = open < close ? open - low : close - low; // Check if the lower wick is significantly larger than the body if (lowerWickSize >= wickToBodyRatio * bodySize&&low<findlow(DistanceRange)&&high>findlow(DistanceRange)) { return true; } return false; } //+------------------------------------------------------------------+ //| Check if the market rejected in the downward direction | //+------------------------------------------------------------------+ bool IsRejectionDown(int shift = 1) { // Get the values of the last candle (shift = 1) double open = iOpen(_Symbol,PERIOD_CURRENT, shift); double close = iClose(_Symbol,PERIOD_CURRENT, shift); double high = iHigh(_Symbol,PERIOD_CURRENT, shift); double low = iLow(_Symbol,PERIOD_CURRENT,shift); // Calculate the body size double bodySize = MathAbs(close - open); // Calculate the upper wick size double upperWickSize = open > close ? high - open : high - close; // Check if the upper wick is significantly larger than the body if (upperWickSize >= wickToBodyRatio * bodySize&&high>findhigh(DistanceRange)&&low<findhigh(DistanceRange)) { return true; } return false; }
当一根K线的影线长度明显大于其实体长度,且该K线的方向与前一根K线方向相反时,即表明这根K线呈现出了价格受阻形态。
利用前面提到的那两个函数,我们创建了新的函数,它们会在指定的回溯周期内进行循环遍历,以检测是否存在市场掠夺流动性的行为。这些函数将用于检查在观察到反转和突破信号之前,是否出现了此类掠夺行为。
//+------------------------------------------------------------------+ //| check if there were rejection up for the short look-back period | //+------------------------------------------------------------------+ bool WasRejectionUp(){ for(int i=1; i<CandlesBeforeBreakout;i++){ if(IsRejectionUp(i)) return true; } return false; } //+------------------------------------------------------------------+ //| check if there were rejection down for the short look-back period| //+------------------------------------------------------------------+ bool WasRejectionDown(){ for(int i=1; i<CandlesBeforeBreakout;i++){ if(IsRejectionDown(i)) return true; } return false; }
要获取当前移动平均线的数值,我们需先在 OnInit()函数中初始化其句柄。
int handleMa; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMa = iMA(_Symbol, PERIOD_CURRENT, MaPeriods, 0, MODE_SMA,PRICE_CLOSE); if (handleMa == INVALID_HANDLE) { Print("Failed to get indicator handles. Error: ", GetLastError()); return INIT_FAILED; } return INIT_SUCCEEDED; }
通过创建一个缓冲区数组,并将移动平均线句柄的值复制到该数组中,即可轻松获取移动平均线的值,操作如下:
double ma[]; if (CopyBuffer(handleMa, 0, 1, 1, ma) <= 0) { Print("Failed to copy MA data. Error: ", GetLastError()); return; }
最后,轮到OnTick()函数,利用您已经定义好的函数将交易逻辑应用到程序中。通过检查当前K线是否与上次保存的已收盘K线不同,确保仅在新K线形成时才计算交易信号。这种方法可节省计算资源,并使交易执行更加流畅。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol, PERIOD_CURRENT); if (barsTotal != bars) { barsTotal = bars;
然后,我们只需按如下方式应用交易信号条件:
if(WasRejectionDown()&&bid<ma[0]&&bid<findlow(CandlesBeforeBreakout)) executeSell(); else if(WasRejectionUp()&&ask>ma[0]&&ask>findhigh(CandlesBeforeBreakout)) executeBuy();
完成这一步骤后,尝试编译程序,并进入回测可视化工具检查EA是否正常运行。
在回测可视化工具中,典型的入场信号会呈现如下形态:
建议
尽管我们已经完成了该策略的核心思路,但对于在实盘市场中使用此EA,我有以下几点建议:
1. 市场操纵行为发生迅速,因此最好采用日内交易策略,并使用5分钟或15分钟等时间框架。较低的时间框架可能更容易出现虚假信号,而较高的时间框架则可能对市场操纵行为的反应过于迟缓。
2. 市场操纵行为通常发生在高波动性时段,例如外汇市场的纽约/伦敦交易时段,或股票市场的开盘/收盘时段。建议添加一个功能,将交易限制在这些特定时段内,具体实现方式如下:
//+------------------------------------------------------------------+ //| Check if the current time is within the specified trading hours | //+------------------------------------------------------------------+ bool IsWithinTradingHours() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; if (( currentHour >= startHour1 && currentHour < endHour1) || ( currentHour >= startHour2 && currentHour < endHour2)) { return true; } return false; }
3. 如果价格在关键价位附近盘整,可能会连续触发双向的多次交易。为确保每次仅执行一笔交易,我们增加另一条标准:该EA的两个仓位订单编号均须设为0,这表明没有未平仓头寸。我们通过在OnTick()函数中编写以下代码行来将它们重置为0。
if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; }
我们对原始代码进行更新,以纳入刚刚所做的更改。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol, PERIOD_CURRENT); if (barsTotal != bars) { barsTotal = bars; double ma[]; double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); if (CopyBuffer(handleMa, 0, 1, 1, ma) <= 0) { Print("Failed to copy MA data. Error: ", GetLastError()); return; } if(WasRejectionDown()&&IsWithinTradingHours()&&sellpos==buypos&&bid<ma[0]&&bid<findlow(CandlesBeforeBreakout)) executeSell(); else if(WasRejectionUp()&&IsWithinTradingHours()&&sellpos==buypos&&ask>ma[0]&&ask>findhigh(CandlesBeforeBreakout)) executeBuy(); if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } }
回测
在本文中,我们将在5分钟时间框架下,将该EA应用于英镑兑美元(GBPUSD)货币对进行测试。
以下是我们决定为该EA采用的参数设置:
重要提示:
- 关于止盈和止损,我们根据日内波动情况选择了一个合理的点数。由于该策略本质上属于顺势交易,因此建议盈亏比大于1。
- 距离范围(DistanceRange)是用于搜索流动性吸纳信号关键价位的回溯周期。
- 同理,突破前K线数(CandlesBeforeBreakout)是用于搜索突破信号近期关键价位的回溯周期。
- 影线与实体的比例可以调整为交易者认为足以说明拒绝形态的数值。
- 交易时间基于您经纪商的服务器时间。对于我的经纪商(格林尼治标准时间GMT+0),外汇市场波动较大的纽约时段为13:00至19:00。
现在,让我们运行2020年11月1日至2024年11月1日的回测:
该策略在过去四年的回测中取得了不错的成绩。
结论
在本文中,我们首先介绍了流动性吸纳的概念及其背后的驱动因素。接着,我们提供了从零开始构建该策略EA的分步指南。随后,我们给出了优化该EA的附加建议。最后,我们通过为期四年、包含200多笔交易的回测,展示了该策略的潜在盈利能力。
我们希望您觉得这一策略有用,并能激发您在此基础上进行进一步开发,无论是创建类似策略还是优化其设置。该EA的对应文件已附于下方。欢迎下载并尝试使用。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16518
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。




感谢您提供的代码,文章写得很好,组合得也很好,代码非常有用,谢谢。有趣的是,如果您在社交媒体上看到 SMC 交易者的回报非常不同。我将审查这些交易,并在外部范围尝试追踪止损和追踪tp或一些斐波那契(Fibonacci)。
感谢您的评论!是的,我确实在社交媒体上看到过 SMC 交易者。一般来说,我认为他们在抢占流动性方面的策略并不一致。有些人追求两个假突破而不是一个,有些人则看交易量。总的来说,他们的行动都有一些自己的想法,因此很难评估其策略的有效性。尽管如此,我还是期待着您在跟踪 sl/tp 和斐波那契范围方面的实验结果。