在 MQL5 中构建自定义市场状态检测系统(第二部分):智能交易系统(EA)(基础篇)
◍ 先看清这套EA要解决的问题
原文给出的系列背景是:在 MQL5 里搭建自定义市场状态检测系统,本篇是第二部分,落点是把状态判断写成一个可跑的 EA。作者 Sahil Bagdi 于 2026 年 1 月 7 日发布,原文标注阅读量 902、评论 1,说明这套内容偏冷门实操,不是大众向指标科普。 从目录结构看,这一篇会依次覆盖:EA 创建、实践考量与优化、多时间周期市场状态指标、回测结果评估。也就是说,读者拿到手的不只是代码片段,而是一个能直接挂 MT5 跑、且带多周期状态过滤的自动化框架雏形。 外汇与贵金属市场高杠杆、高波动,任何 EA 在历史表现都只代表过去概率,实盘前必须自己用策略测试器重跑一遍同周期回测。
从识别市况到自动换挡
上一节我们已经把市场状态检测的地基打好了:CMarketRegimeDetector 类能客观区分趋势、震荡和高波动,MarketRegimeIndicator 也能在图表上直接画出来。但只把状态识别出来,等于任务做了一半——检测器再准,不落到下单动作上,就只是个分析玩具。 真正有价值的是让 EA 根据状态自动切换打法:趋势强时跑跟踪,横盘切均值回归,波动飙升就改风控参数。这一节要填的空,就是把「识别」接上「执行」。 我们要写的 MarketRegimeEA 会直接挂上之前的检测器,按状态选策略、调手数和止损止盈,还会处理状态切换时的平滑过渡。外汇和贵金属杠杆高、跳空频繁,这种自动换挡逻辑尤其要小心过渡期假信号,实盘前务必在 MT5 策略测试器里按品种和周期分开优化参数。
「把状态检测塞进一个会自己换打法的EA」
这套 EA 的核心思路是:让市场状态检测器在每根新 K 线后重新判定环境,再切换对应的交易逻辑。趋势市走趋势跟踪,震荡市做 RSI 均值回归,高波动市用布林带突破但把手数压到最小——三种状态的手数分别是 0.1 / 0.05 / 0.02,止损点数 100 / 50 / 150,止盈点数 200 / 80 / 300,参数全开放给用户输入。 OnInit() 里只干一件事:按 LookbackPeriod=100、SmoothingPeriod=10 把检测器 new 出来,设好 TrendThreshold=0.2 和 VolatilityThreshold=1.5,失败就返回 INIT_FAILED。OnTick() 先判新 K 线、攒够样本再调 Detector 处理收盘价数组;这里 ArraySetAsSeries(close, true) 不能省,它保证索引 0 是最新价,是 MQL5 时间序列的硬约定。 ExecuteRegimeBasedStrategy() 按状态分派:上升趋势买、下降趋势卖;RSI 超卖买超买卖;布林带突破跟进但仓位缩到 VolatileLotSize。每种分支都现算止损止盈再 OrderSend()。把波动率直接挂钩仓位,是高波动状态控风险的关键一步。 别把参数当摆设 TrendThreshold 在 0.1–0.5 之间、VolatilityThreshold 在 1.0–3.0 之间,这两个值直接决定 EA 多久切一次打法。拿 EURUSD 或 XAUUSD 在 MT5 里先跑历史样本,看切状态频率再反调阈值,比盲设更靠谱。外汇和贵金属杠杆高,实盘前务必用策略测试器验证。
class=class="str">"cmt">// Include the Market Regime Detector class="macro">#include <MarketRegimeEnum.mqh> class="macro">#include <MarketRegimeDetector.mqh> class=class="str">"cmt">// EA input parameters input class="type">int LookbackPeriod = class="num">100; class=class="str">"cmt">// Lookback period for calculations input class="type">int SmoothingPeriod = class="num">10; class=class="str">"cmt">// Smoothing period for regime transitions input class="type">class="kw">double TrendThreshold = class="num">0.2; class=class="str">"cmt">// Threshold for trend detection(class="num">0.1-class="num">0.5) input class="type">class="kw">double VolatilityThreshold = class="num">1.5; class=class="str">"cmt">// Threshold for volatility detection(class="num">1.0-class="num">3.0) class=class="str">"cmt">// Trading parameters input class="type">class="kw">double TrendingLotSize = class="num">0.1; class=class="str">"cmt">// Lot size for trending regimes input class="type">class="kw">double RangingLotSize = class="num">0.05; class=class="str">"cmt">// Lot size for ranging regimes input class="type">class="kw">double VolatileLotSize = class="num">0.02; class=class="str">"cmt">// Lot size for volatile regimes input class="type">int TrendingStopLoss = class="num">100; class=class="str">"cmt">// Stop loss in points for trending regimes input class="type">int RangingStopLoss = class="num">50; class=class="str">"cmt">// Stop loss in points for ranging regimes input class="type">int VolatileStopLoss = class="num">150; class=class="str">"cmt">// Stop loss in points for volatile regimes input class="type">int TrendingTakeProfit = class="num">200; class=class="str">"cmt">// Take profit in points for trending regimes input class="type">int RangingTakeProfit = class="num">80; class=class="str">"cmt">// Take profit in points for ranging regimes input class="type">int VolatileTakeProfit = class="num">300; class=class="str">"cmt">// Take profit in points for volatile regimes class=class="str">"cmt">// Global variables CMarketRegimeDetector *Detector = NULL; class="type">int OnBarCount = class="num">0; class="type">class="kw">datetime LastBarTime = class="num">0; class="type">int OnInit() { class=class="str">"cmt">// Create and initialize the Market Regime Detector Detector = new CMarketRegimeDetector(LookbackPeriod, SmoothingPeriod); if(Detector == NULL) { Print("Failed to create Market Regime Detector"); class="kw">return INIT_FAILED; } class=class="str">"cmt">// Configure the detector Detector.SetTrendThreshold(TrendThreshold);
◍ 用 OnTick 驱动市场状态识别与下单闸门
这段逻辑把「新K线判定」做成了事件过滤器的核心:用 iTime 取当前周期第 0 根 Bar 的时间,和上次记录的 LastBarTime 比对,相等就直接 return,保证一段行情里只在每根 Bar 闭合后跑一次分析。 数据积累阶段用 OnBarCount 和 LookbackPeriod 做计数,不足时在图上提示「Accumulating data: X of Y bars」,例如回测 EURUSD M15 时若 LookbackPeriod=100,前 100 根 Bar 都不会触发策略,避免样本过少导致波动率估计失真。外汇与贵金属杠杆高,样本窗太短容易误判 regime,实盘前建议在策略测试器里先跑满这个预热长度。 CopyClose 取近 LookbackPeriod 根收盘价,ArraySetAsSeries(close, true) 让索引 0 对应最新 Bar;若 copied != LookbackPeriod 就打印失败条数并退出,这是 MT5 常见坑——切换品种或离线图时常取不满。 Detector.ProcessData 跑完后,用 GetCurrentRegime / GetTrendStrength / GetVolatility 把状态、趋势强度、波动率刷到 Comment,趋势强度与波动率都保留 4 位小数便于肉眼比对阈值。ExecuteRegimeBasedStrategy 开头用 PositionsTotal()>0 直接 return,等于给账户上了「同屏只持一单」的硬闸门,降低多 regime 信号叠加带来的敞口风险。 开仓参数那段先抓 ask/bid/point 三个报价原子量,再按 regime 分支决定 lotSize,后续你只要把 lotSize 赋值和订单发送补上,就能在 MT5 里跑一套 regime 过滤的轻量策略。
Detector.SetVolatilityThreshold(VolatilityThreshold); Detector.Initialize(); class=class="str">"cmt">// Initialize variables OnBarCount = class="num">0; LastBarTime = class="num">0; class="kw">return INIT_SUCCEEDED; } class="type">void OnTick() { class=class="str">"cmt">// Check for new bar class="type">class="kw">datetime currentBarTime = iTime(Symbol(), PERIOD_CURRENT, class="num">0); if(currentBarTime == LastBarTime) class="kw">return; class=class="str">"cmt">// No new bar LastBarTime = currentBarTime; OnBarCount++; class=class="str">"cmt">// Wait for enough bars to accumulate if(OnBarCount < LookbackPeriod) { Comment("Accumulating data: ", OnBarCount, " of ", LookbackPeriod, " bars"); class="kw">return; } class=class="str">"cmt">// Get price data class="type">class="kw">double close[]; ArraySetAsSeries(close, true); class="type">int copied = CopyClose(Symbol(), PERIOD_CURRENT, class="num">0, LookbackPeriod, close); if(copied != LookbackPeriod) { Print("Failed to copy price data: copied = ", copied, " of ", LookbackPeriod); class="kw">return; } class=class="str">"cmt">// Process data with the detector if(!Detector.ProcessData(close, LookbackPeriod)) { Print("Failed to process data with Market Regime Detector"); class="kw">return; } class=class="str">"cmt">// Get current market regime ENUM_MARKET_REGIME currentRegime = Detector.GetCurrentRegime(); class=class="str">"cmt">// Display current regime information class="type">class="kw">string regimeText = "Current Market Regime: " + Detector.GetRegimeDescription(); class="type">class="kw">string trendText = "Trend Strength: " + DoubleToString(Detector.GetTrendStrength(), class="num">4); class="type">class="kw">string volatilityText = "Volatility: " + DoubleToString(Detector.GetVolatility(), class="num">4); Comment(regimeText + "\n" + trendText + "\n" + volatilityText); class=class="str">"cmt">// Execute trading strategy based on market regime ExecuteRegimeBasedStrategy(currentRegime); } class="type">void ExecuteRegimeBasedStrategy(ENUM_MARKET_REGIME regime) { class=class="str">"cmt">// Check if we already have open positions if(PositionsTotal() > class="num">0) class="kw">return; class=class="str">"cmt">// Don&class="macro">#x27;t open new positions if we already have one class=class="str">"cmt">// Get current market information class="type">class="kw">double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); class="type">class="kw">double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); class="type">class="kw">double point = SymbolInfoDouble(Symbol(), SYMBOL_POINT); class=class="str">"cmt">// Determine trading parameters based on regime class="type">class="kw">double lotSize = class="num">0.0;
按市况切换下单参数的代码骨架
这段逻辑把「趋势 / 震荡 / 波动」三种市况映射到不同的手数、止损、止盈与买卖方向上。趋势段直接跟方向:上涨吃多、下跌吃空,参数套用 Trending 系列变量;震荡段则借 RSI(14) 做均值回归,低于 30 考虑多、高于 70 考虑空,中间地带直接 return 不出手。 波动段原文只开了头,用布林带做突破的思路已经点出,但 upper[]、lower[] 数组刚设了序列方向就被截断。你可以把 iBands 的缓冲拷进来,价格刺穿上轨倾向跟多、下轨倾向跟空,参数接 Volatile 系列。 外汇与贵金属杠杆高,市况误判会让止损形同虚设,任何方向判断都只是概率倾向,开 MT5 把这段补完跑回测再上实盘。
class="type">int stopLoss = class="num">0; class="type">int takeProfit = class="num">0; ENUM_ORDER_TYPE orderType = ORDER_TYPE_BUY; class="kw">switch(regime) { case REGIME_TRENDING_UP: { lotSize = TrendingLotSize; stopLoss = TrendingStopLoss; takeProfit = TrendingTakeProfit; orderType = ORDER_TYPE_BUY; class=class="str">"cmt">// Buy in uptrend break; } case REGIME_TRENDING_DOWN: { lotSize = TrendingLotSize; stopLoss = TrendingStopLoss; takeProfit = TrendingTakeProfit; orderType = ORDER_TYPE_SELL; class=class="str">"cmt">// Sell in downtrend break; } case REGIME_RANGING: { class=class="str">"cmt">// In ranging markets, we can use mean-reversion strategies class=class="str">"cmt">// For simplicity, we&class="macro">#x27;ll use RSI to determine overbought/oversold class="type">class="kw">double rsi[]; ArraySetAsSeries(rsi, true); class="type">int rsiCopied = CopyBuffer(iRSI(Symbol(), PERIOD_CURRENT, class="num">14, PRICE_CLOSE), class="num">0, class="num">0, class="num">2, rsi); if(rsiCopied != class="num">2) class="kw">return; lotSize = RangingLotSize; stopLoss = RangingStopLoss; takeProfit = RangingTakeProfit; if(rsi[class="num">0] < class="num">30) class=class="str">"cmt">// Oversold orderType = ORDER_TYPE_BUY; else if(rsi[class="num">0] > class="num">70) class=class="str">"cmt">// Overbought orderType = ORDER_TYPE_SELL; else class="kw">return; class=class="str">"cmt">// No signal break; } case REGIME_VOLATILE: { class=class="str">"cmt">// In volatile markets, we can use breakout strategies class=class="str">"cmt">// For simplicity, we&class="macro">#x27;ll use Bollinger Bands class="type">class="kw">double upper[], lower[]; ArraySetAsSeries(upper, true);
「布林带突破信号的代码落地」
把波动率区制切到「动荡」后,EA 要从布林带上下轨抓取突破。下面这段直接取当前品种、当前周期、参数 20/2/0 的收盘价轨道,并把上下轨数组设成时间序列方便按索引读最新值。 代码里先各拉 2 根 K 线的 upper、lower 缓冲,若 CopyBuffer 返回值不是 2 就直接 return,避免半截数据触发错单。随后用 close[1] 与 close[0] 对照上一根和当前根是否站上 upper 或跌破 lower,满足前者挂 BUY,满足后者挂 SELL,否则无信号退出。 止损止盈按 VolatileLotSize / VolatileStopLoss / VolatileTakeProfit 套用,BUY 单 slLevel = ask - stopLoss*point、tpLevel = ask + takeProfit*point;SELL 反向。外汇与贵金属杠杆高,布林突破在消息面跳空时可能直接越过轨道,实盘前请在 MT5 策略测试器用 2020—2023 年数据跑一遍验证触发频率。
ArraySetAsSeries(lower, true); class="type">int bbCopied1 = CopyBuffer(iBands(Symbol(), PERIOD_CURRENT, class="num">20, class="num">2, class="num">0, PRICE_CLOSE), class="num">1, class="num">0, class="num">2, upper); class="type">int bbCopied2 = CopyBuffer(iBands(Symbol(), PERIOD_CURRENT, class="num">20, class="num">2, class="num">0, PRICE_CLOSE), class="num">2, class="num">0, class="num">2, lower); if(bbCopied1 != class="num">2 || bbCopied2 != class="num">2) class="kw">return; lotSize = VolatileLotSize; stopLoss = VolatileStopLoss; takeProfit = VolatileTakeProfit; class="type">class="kw">double close[]; ArraySetAsSeries(close, true); class="type">int copied = CopyClose(Symbol(), PERIOD_CURRENT, class="num">0, class="num">2, close); if(copied != class="num">2) class="kw">return; if(close[class="num">1] < upper[class="num">1] && close[class="num">0] > upper[class="num">0]) class=class="str">"cmt">// Breakout above upper band orderType = ORDER_TYPE_BUY; else if(close[class="num">1] > lower[class="num">1] && close[class="num">0] < lower[class="num">0]) class=class="str">"cmt">// Breakout below lower band orderType = ORDER_TYPE_SELL; else class="kw">return; class=class="str">"cmt">// No signal break; } class="kw">default: class="kw">return; class=class="str">"cmt">// No trading in undefined regime } class=class="str">"cmt">// Calculate stop loss and take profit levels class="type">class="kw">double slLevel = class="num">0.0; class="type">class="kw">double tpLevel = class="num">0.0; if(orderType == ORDER_TYPE_BUY) { slLevel = ask - stopLoss * point; tpLevel = ask + takeProfit * point; } else if(orderType == ORDER_TYPE_SELL) { slLevel = bid + stopLoss * point; tpLevel = bid - takeProfit * point;