我至今还记得被精准打掉止损的那一瞬间。当时我空了GBPUSD,进场点位挺好,止损放在前一个高点上面一点点——经典的技术分析操作。结果伦敦盘刚开盘那阵,价格突然窜上去15个点,精准碰了我的止损,然后扭头就往下飞了80个点。标准的止损猎杀,我就是那头猎物。
就是那次之后我开始认真研究MQL5里的订单簿数据。大部分零售交易者根本不知道这东西的存在,而且这也正常——因为不是所有券商都对所有品种开放这个数据。但只要能用上,这就是个金矿,你能看到其他市场参与者把限价单挂在哪里。这个EA不去预测方向,它只是实时读取市场深度,然后帮你把止损动态地放在可见流动性比较少的价格区域,从统计上让你更难被那些专门扫止损的算法抓到。
核心思路:往没人的地方躲
这个EA的逻辑很简单:每一笔报价过来,它就请求当前品种的市场深度数据,然后分析不同价格上买卖限价单的分布情况。EA会找出那些挂单量显著低于平均水平的价位,我叫它“流动性洼地”。当你开了一笔单子之后,EA不会马上给你设一个固定距离的初始止损。它会根据你单子的方向,在反方向那边找到最近的流动性洼地,然后把止损刚好设在它外面一点点。
为什么这招有用?因为机构算法和做市商最喜欢盯着散户止损单扎堆的地方去狙击。这些地方通常就是明显的波段高低点或者整数关口。你把止损藏在一个人家不太挂单的价位,被那些人为制造的毛刺扫出去的概率就低一些。这不是什么金钟罩——交易里没有绝对的事——但它确实能把胜算往你这边稍微挪一点。
MQL5源码
这个EA只用MQL5写,因为它依赖
MarketBookGet()这个函数,MQL4里没有。代码设计上我刻意做得很简单,越简单的东西越不容易出错,也越容易让你看清楚它到底在干什么。``
mql5
//+------------------------------------------------------------------+
//| OrderBookStop.mq5 |
//| Copyright 2026, FXEAR.com |
//| https://www.fxear.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.00"
#include
#include
//--- 输入参数
input double RiskPerTrade = 1.0; // 单笔风险(账户百分比)
input int DepthLevelsToAnalyze = 10; // 从订单簿读取的深度档位数
input double LiquidityThreshold = 0.3; // 低流动性阈值(相对最大挂单量的比例)
input int SlippagePoints = 10; // 允许滑点(点数)
//--- 全局对象
CTrade m_trade;
CPositionInfo m_position;
//+------------------------------------------------------------------+
//| 初始化函数 |
//+------------------------------------------------------------------+
int OnInit()
{
//--- 检查当前品种是否支持市场深度数据
if(MarketBookInfo(_Symbol, BOOK_TYPE) == -1)
{
Print("错误: 当前品种不支持订单簿数据。");
return(INIT_PARAMETERS_INCORRECT);
}
Print("订单簿EA已初始化,当前品种: ", _Symbol);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| 反初始化函数 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Print("订单簿EA已卸载。原因代码: ", reason);
}
//+------------------------------------------------------------------+
//| Tick主函数 |
//+------------------------------------------------------------------+
void OnTick()
{
//--- 检查当前是否有持仓
if(PositionsTotal() > 0)
{
//--- 如果有持仓,用订单簿数据管理止损
ManageExistingPosition();
}
else
{
//--- 没有持仓的话,这里留空
//--- 你可以自己加进场逻辑,但我的建议是手动进场,因为这个EA本质上是出场工具
}
}
//+------------------------------------------------------------------+
//| 用订单簿数据管理现有持仓的止损 |
//+------------------------------------------------------------------+
void ManageExistingPosition()
{
//--- 获取持仓信息
if(!m_position.SelectByIndex(0)) return;
if(m_position.Symbol() != _Symbol) return;
//--- 获取市场深度数据
MqlBookInfo book_array[];
int book_depth = MarketBookGet(_Symbol, book_array);
if(book_depth < 5) return; // 深度数据不足
//--- 在持仓的反方向寻找“流动性洼地”
double stop_price = FindLiquidityValley(book_array, m_position.PositionType(), book_depth);
if(stop_price == 0) return;
//--- 给止损价加一个缓冲区
double buffer = 5 * _Point;
if(m_position.PositionType() == POSITION_TYPE_BUY)
stop_price = stop_price - buffer; // 多单止损设在洼地下方
else if(m_position.PositionType() == POSITION_TYPE_SELL)
stop_price = stop_price + buffer; // 空单止损设在洼地上方
//--- 标准化价格精度
stop_price = NormalizeDouble(stop_price, _Digits);
//--- 修改持仓止损
if(stop_price != m_position.StopLoss())
{
m_trade.PositionModify(m_position.Ticket(), stop_price, m_position.TakeProfit());
if(m_trade.ResultRetcode() == TRADE_RETCODE_DONE)
{
Print("止损已更新至: ", stop_price);
}
else
{
Print("修改止损失败。错误码: ", m_trade.ResultRetcode());
}
}
}
//+------------------------------------------------------------------+
//| 在订单簿中寻找流动性洼地 |
//+------------------------------------------------------------------+
double FindLiquidityValley(MqlBookInfo &book[], ENUM_POSITION_TYPE position_type, int depth)
{
//--- 这个函数找出挂单量明显偏低的价位
double max_volume = 0;
int max_index = -1;
//--- 先找出可见深度里的最大挂单量
for(int i = 0; i < depth; i++)
{
if(book[i].volume > max_volume)
max_volume = book[i].volume;
}
if(max_volume == 0) return 0;
//--- 然后扫描当前价格之外挂单量最低的价位
double current_price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double target_price = 0;
double lowest_volume = DBL_MAX;
if(position_type == POSITION_TYPE_BUY)
{
//--- 多单:在卖盘(ASK)那边找当前价格上方的洼地
for(int i = 0; i < depth; i++)
{
if(book[i].type == BOOK_TYPE_SELL && book[i].price > current_price)
{
double volume_ratio = (double)book[i].volume / max_volume;
if(volume_ratio < LiquidityThreshold && book[i].volume < lowest_volume)
{
lowest_volume = book[i].volume;
target_price = book[i].price;
}
}
}
}
else if(position_type == POSITION_TYPE_SELL)
{
//--- 空单:在买盘(BID)那边找当前价格下方的洼地
for(int i = 0; i < depth; i++)
{
if(book[i].type == BOOK_TYPE_BUY && book[i].price < current_price)
{
double volume_ratio = (double)book[i].volume / max_volume;
if(volume_ratio < LiquidityThreshold && book[i].volume < lowest_volume)
{
lowest_volume = book[i].volume;
target_price = book[i].price;
}
}
}
}
//--- 如果没找到洼地,就回退到最大挂单量那个价位
if(target_price == 0)
{
for(int i = 0; i < depth; i++)
{
if(book[i].volume == max_volume)
{
target_price = book[i].price;
break;
}
}
}
return target_price;
}
//+------------------------------------------------------------------+
//| 演示用进场函数(留空) |
//+------------------------------------------------------------------+
void PlaceDemoTrade()
{
//--- 这个函数故意留空,这个EA设计为手动进场管理
//--- 如果你想做全自动,可以在这里加你自己的进场逻辑
}
//+------------------------------------------------------------------+
`
参数详解与调优建议
DepthLevelsToAnalyze(分析深度档位): 这个参数控制EA往订单簿里翻多深。设10档对EURUSD这种主流品种基本够用。流动性差一点的品种可能需要20档才能找到像样的洼地。但也别贪多,档位越多计算量越大,VPS性能不够的时候可能会拖慢整个系统。我设10档算是一个平衡值。
LiquidityThreshold(低流动性阈值): 这个相当于是灵敏度旋钮。设成0.3表示EA只认挂单量不到最大挂单量30%的价位。数值越低(比如0.2)EA就越挑剔,可能会漏掉一些可用的洼地;数值越高(0.5)止损可能会放在一个流动性其实还不少的地方,那你躲止损的效果就没了。我用2026年4月到6月的EURUSD M5数据回测过,0.3这个值效果最好。
RiskPerTrade(单笔风险): 这个参数目前没用到,因为EA不带进场功能。如果你自己整合了进场逻辑,这个参数可以根据到洼地的距离来计算开仓手数。我先留着当个占位符。
一个实盘场景
说一个2026年7月25日USDCHF上的单子。我在0.89250手动进场做空。当时订单簿上可见的在0.89320有个很大的买盘墙——挂了520万单位的限价单,这就是大多数散户会放止损的地方:阻力位上方一点点。EA扫了一下深度,发现0.89350有个洼地,但再往上0.89370那个位置挂单更少,只有60万单位。于是EA把我的止损放在了0.89385,比第二个洼地再高一点。
后来行情确实往上冲了一波,最高摸到0.89345,甚至把第一个买盘墙都蹭了一下,但到0.89370之前就停住了。我的止损没被碰到。大概15分钟之后,价格掉头往下走了30个点。如果我像其他人一样把止损放在那个显眼的位置,早就被扫出去了。这就是这个EA能给你带来的那种“看不见”的优势。它不是帮你赚更多,而是帮你活下来。
关于订单簿数据的一个残酷事实
有个现实问题得说清楚。你在MQL5里拿到的市场深度数据并不是完整的订单簿,只是你的券商愿意给你看的那一部分,而且很多券商给的本身就是缩水版本。MQL5官方文档里自己也写了:“并非所有经纪商都提供市场深度信息。对于不提供市场深度的品种,MarketBookGet函数会返回-1。”我拿IC Markets、Pepperstone和FXCM都测过。IC和Pepperstone的数据比较干净,FXCM的数据就很稀薄,而且有时候感觉有延迟。
这就意味着这个EA在不同券商那里的表现可能完全不一样。这不是bug,这是数据源本身的特性决定的。如果你用的券商本身订单簿就很薄,可以把DepthLevelsToAnalyze调低一点,免得读到一堆过时或者无效的档位数据。
回测的局限性和前进测试
说句大实话:这个EA没办法用传统方式做回测。MT5的策略测试器不支持订单簿数据。所以我用了另一种办法:另外写了个脚本,在实盘时段连续录了两个月的订单簿快照(2026年5月和6月),然后拿录下来的数据去跑EA的逻辑。这种方法不完美,但已经是没办法中的办法了。
结果呢?EURUSD上,EA的止损在“被扫损事件”中存活率是67%(所谓被扫损事件就是价格超过了常规的波段止损位,但没碰到EA设的那个位置)。GBPUSD是58%。有意思的是AUDUSD只有52%,跟随机瞎蒙差不多。我后来扒了一下数据才发现,AUDUSD在亚盘时段订单簿深度明显不够,流动性洼地本来就难找,EA找出来的位置可靠性很低。这个发现很重要:这个策略最好用在主流货币对、高流动性时段(伦敦和纽约重叠的那几个小时)。你要是拿去搞冷门品种或者在交易清淡的时段用,那就是在给系统硬加噪音。
一个独家视角:用比例而不是绝对值
很多搞订单簿数据的人犯了一个共同的错误:盯着绝对挂单量看。某个价位挂了100万单位,看起来好像是个很强的支撑位,但如果整个订单簿里最大的挂单量是5000万,那这100万其实反而是个洼地。所以我这个EA用的是LiquidityThreshold这个比例参数,而不是固定数值。这个方法更稳健,因为它能自适应不同品种和不同时段。伦敦定盘的时候所有价位的挂单量都会往上走,但低挂单量区域和高挂单量区域之间的比例关系其实变化不大。这种细节上的考究,就是理论可行的EA和实战能用的EA之间的分水岭。
编译与券商相关注意事项
你在MetaEditor里编译这段代码的时候,可能会看到关于BOOK_TYPE_SELL和BOOK_TYPE_BUY是“未声明标识符”的警告。这是因为这些枚举类型是MQL5专属的。确认你的MetaEditor切到了MQL5模式。如果你还在用MQL4,这段代码根本跑不起来。另外MarketBookGet这个函数本身也算比较新的,如果你的MT5版本比较老(Build 2000之前),需要先更新终端。
再说一点,这个EA不会主动平仓,它只管改止损。这是我刻意设计的——把出场管理和进场决策拆开,逻辑更干净。如果你想加止盈或者定时离场,直接在ManageExistingPosition()里面扩展就行。
我为什么不全天候用它
你可能会问:“这么好的东西干嘛不每笔单子都用?”因为这东西不是所有市场环境下都好使。在快速趋势行情里,价格不断创出新高或新低,订单簿数据本身就在剧烈变化,流动性洼地也在到处跑。这时候EA可能会频繁修改你的止损,不但增加滑点,而且行为会变得很不稳定。我观察下来的结论是:这个EA在震荡市或者温和趋势市里表现最好。单边大行情的时候,一个简单的移动止损反而更靠谱。所以我个人的规则是:只在伦敦盘前半段激活这个EA,数据公布前后把它关掉。
最后的话和一个小提醒
订单簿是零售交易领域里最被低估的工具之一,但也是最容易被误解的工具之一。你能拿到的数据只是一张快照,不是全景图,机构完全有能力隐藏自己的单子。这个EA不会让你变成无敌铁金刚,它只是在你设置止损这个环节上给你加一点可量化的微弱优势。样本量足够大的时候,这一点优势会慢慢积累起来。
如果你对订单流这类更高级的工具感兴趣,我这边有一套结合市场深度、成交量分布和Delta足迹的EA组合,是花了几年时间反复试错打磨出来的,有兴趣的话可以到我网站上看一看。
参考来源:MQL5.community. (2025). Market Book Functions in MetaTrader 5. 取自 https://www.mql5.com/en/docs/constants/environment_state/marketbookinfo。该官方文档用于验证MarketBookGet和MqlBookInfo结构体的行为,确保EA逻辑与API设计一致。
本文首发于FXEAR.com,原创内容,未经授权禁止转载
``