在 MQL5 中构建自定义市场状态检测系统(第二部分):智能交易系统(EA)·进阶篇
🤖

在 MQL5 中构建自定义市场状态检测系统(第二部分):智能交易系统(EA)·进阶篇

(2/3)· 识别市场状态只是半套功夫,把检测变成可执行的EA才是稳定绩效的关键缺口

含代码示例实战向 第 2/3 篇

很多交易者花力气写出了市场状态检测器,却只把它当图表上的颜色看板,EA 该下单时依旧一套逻辑用到死。状态切换的瞬间往往正是亏损最密集的区段,因为静态策略在过渡期反应最迟钝。本篇接着把检测器接进真正的交易执行层。

◍ 市价单的下单结构与内存清理

这段 MT5 代码演示了在识别到市场状态后,如何用 MqlTradeRequest 结构直接发市价单。request.deviation 设成 10 点、magic 写死 123456,方便后续在账户历史里筛出本 EA 的成交;comment 里拼了 Detector.GetRegimeDescription(),下单同时把当时 regime 写进订单备注,回测或复盘时一眼能看出背景。 逐行看:ZeroMemory 先把 request 和 result 清零,避免残留字段导致 OrderSend 报错;action 用 TRADE_ACTION_DEAL 走即时成交,price 按买卖方向取 ask 或 bid,sl/tp 由外部 slLevel、tpLevel 传入;type_filling 选 ORDER_FILLING_FOK,不足额就全撤,适合流动性一般的贵金属品种,外汇主流对也常用。 下单成功与否靠 OrderSend 返回值判断,并打印 retcode 和 comment,失败时能直接看到 MT5 的错误码(例如 10019 代表禁止交易时段)。外汇与贵金属杠杆高,FOK 模式在跳空行情可能整单不成交,实盘前建议在策略测试器用 2022 年 3 月英镑闪崩那段数据跑一遍。 OnDeinit 里把 Detector 指针 delete 并置 NULL,顺手 Comment("") 清掉屏幕文字,防止 EA 卸载后图表还留着旧提示。这是 MT5 EA 的基本洁癖,不清理自定义对象会在反复加载时漏内存。

MQL5 / C++
   }
   
   class=class="str">"cmt">// Execute trade
   class="type">MqlTradeRequest request;
   class="type">MqlTradeResult result;
   
   ZeroMemory(request);
   ZeroMemory(result);
   
   request.action = TRADE_ACTION_DEAL;
   request.symbol = Symbol();
   request.volume = lotSize;
   request.type = orderType;
   request.price = (orderType == ORDER_TYPE_BUY) ? ask : bid;
   request.sl = slLevel;
   request.tp = tpLevel;
   request.deviation = class="num">10;
   request.magic = class="num">123456; class=class="str">"cmt">// Magic number for this EA
   request.comment = "Market Regime: " + Detector.GetRegimeDescription();
   request.type_filling = ORDER_FILLING_FOK;
   
   class="type">bool success = OrderSend(request, result);
   
   if(success)
   {
      Print("Trade executed successfully: ", result.retcode, " ", result.comment);
   }
   else
   {
      Print("Trade execution failed: ", result.retcode, " ", result.comment);
   }
}
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
   class=class="str">"cmt">// Clean up
   if(Detector != NULL)
   {
      class="kw">delete Detector;
      Detector = NULL;
   }
   
   class=class="str">"cmt">// Clear the comment
   Comment("");
}

「把状态检测塞进实盘前的调参与过渡处理」

市场状态检测能不能在外汇或贵金属上跑出信号质量,先看三个参数的落地区间。回看周期建议从 50 到 200 根 K 线之间起步,趋势阈值常见起点是 0.1–0.3,波动率阈值常见起点是 1.5–2.5;这些只是锚点,具体品种的典型状态持续时间和波动性格会逼着你往两侧挪。 状态切换那一下最容易亏在滑点和执行上。用平滑周期卡一道:要求状态至少连续占住若干根 K 线才被确认,能压住暂时性噪音。下面那段 SmoothRegimeTransition 就是思路——维护近 20 次状态历史,返回出现次数最多的状态,而不是每根 K 线都跳。 仓位别在过渡期硬切。渐进式调整拿新旧状态的仓位做加权,市场变脸时曲线更平。集成到现有系统有两种用法:按状态过滤信号,只做和当前状态匹配的交易;或按状态改策略参数,把止损、加仓逻辑重新标定。 回测时把回看、趋势阈值、波动率阈值分别扫一遍,代码里的循环就是现成骨架。外汇和贵金属杠杆高、跳空频繁,参数过拟合会在实盘放大亏损概率,建议每轮优化后留 20% 样本外数据核对状态预测准确率。

MQL5 / C++
class=class="str">"cmt">// Example of testing different lookback periods
for(class="type">int lookback = class="num">50; lookback <= class="num">200; lookback += class="num">25)
{
   CMarketRegimeDetector detector(lookback, SmoothingPeriod);
   detector.SetTrendThreshold(TrendThreshold);
   detector.SetVolatilityThreshold(VolatilityThreshold);
   
   class=class="str">"cmt">// Process historical data and evaluate performance
   class=class="str">"cmt">// ...
}
class=class="str">"cmt">// Example of testing different trend thresholds
for(class="type">class="kw">double threshold = class="num">0.1; threshold <= class="num">0.5; threshold += class="num">0.05)
{
   CMarketRegimeDetector detector(LookbackPeriod, SmoothingPeriod);
   detector.SetTrendThreshold(threshold);
   detector.SetVolatilityThreshold(VolatilityThreshold);
   
   class=class="str">"cmt">// Process historical data and evaluate performance
   class=class="str">"cmt">// ...
}
class=class="str">"cmt">// Example of testing different volatility thresholds
for(class="type">class="kw">double threshold = class="num">1.0; threshold <= class="num">3.0; threshold += class="num">0.25)
{
   CMarketRegimeDetector detector(LookbackPeriod, SmoothingPeriod);
   detector.SetTrendThreshold(TrendThreshold);
   detector.SetVolatilityThreshold(threshold);
   
   class=class="str">"cmt">// Process historical data and evaluate performance
   class=class="str">"cmt">// ...
}
class=class="str">"cmt">// Example of implementing smoothed regime transitions
ENUM_MARKET_REGIME SmoothRegimeTransition(ENUM_MARKET_REGIME newRegime)
{
   class="kw">static ENUM_MARKET_REGIME regimeHistory[class="num">20];
   class="kw">static class="type">int historyCount = class="num">0;
   
   class=class="str">"cmt">// Add new regime to history
   for(class="type">int i = class="num">19; i > class="num">0; i--)
      regimeHistory[i] = regimeHistory[i-class="num">1];
      
   regimeHistory[class="num">0] = newRegime;
   
   if(historyCount < class="num">20)
      historyCount++;
      
   class=class="str">"cmt">// Count occurrences of each regime
   class="type">int regimeCounts[class="num">5] = {class="num">0};
   
   for(class="type">int i = class="num">0; i < historyCount; i++)
      regimeCounts[regimeHistory[i]]++;
      
   class=class="str">"cmt">// Find most common regime
   class="type">int maxCount = class="num">0;
   ENUM_MARKET_REGIME dominantRegime = REGIME_UNDEFINED;
   
   for(class="type">int i = class="num">0; i < class="num">5; i++)
   {
      if(regimeCounts[i] > maxCount)
      {
         maxCount = regimeCounts[i];
         dominantRegime = (ENUM_MARKET_REGIME)i;
      }
   }
   
   class="kw">return dominantRegime;
}
class=class="str">"cmt">// Example of gradual position sizing during transitions

过渡期手数与信号过滤的实写

市场状态切换时直接切手数容易在边界上吃滑点,这段代码把前状态与现状态按剩余过渡根数做了线性加权。maxTransitionBars 是允许的最大过渡长度,transitionBars 是已经走过的根数,越靠近新状态,新状态手数权重越高。 previousWeight = (maxTransitionBars - transitionBars) / maxTransitionBars,currentWeight = transitionBars / maxTransitionBars,两者相加恒为 1.0。若 previousRegime == currentRegime 或 transitionBars >= maxTransitionBars,函数直接返回现状态手数,不再插值。 ExecuteTradeSignal 里对四种状态给了不同放行逻辑:趋势状态只吃同向信号(例如 REGIME_TRENDING_UP 仅 strategySignal==1 才返 true),震荡状态非 0 信号全接,波动状态额外要求 IsStrongSignal 过滤。外汇与贵金属杠杆高,状态误判时这类过滤也可能连续失效,实盘前请在 MT5 策略测试器用历史数据核对过渡权重曲线。 把下面函数直接塞进 EA 的仓位模块,调一下 maxTransitionBars 从 5 改到 20,能看到手数过渡从陡变缓,这是验证权重公式最快的办法。

MQL5 / C++
class="type">class="kw">double CalculateTransitionLotSize(ENUM_MARKET_REGIME previousRegime,
                                      ENUM_MARKET_REGIME currentRegime,
                                      class="type">int transitionBars,
                                      class="type">int maxTransitionBars)
{
   class=class="str">"cmt">// Base lot sizes for each regime
   class="type">class="kw">double regimeLotSizes[class="num">5] = {
       TrendingLotSize,    class=class="str">"cmt">// REGIME_TRENDING_UP
       TrendingLotSize,    class=class="str">"cmt">// REGIME_TRENDING_DOWN
       RangingLotSize,     class=class="str">"cmt">// REGIME_RANGING
       VolatileLotSize,    class=class="str">"cmt">// REGIME_VOLATILE
       class="num">0.0                 class=class="str">"cmt">// REGIME_UNDEFINED
   };
   
   class=class="str">"cmt">// If not in transition, use current regime&class="macro">#x27;s lot size
   if(previousRegime == currentRegime || transitionBars >= maxTransitionBars)
       class="kw">return regimeLotSizes[currentRegime];
       
   class=class="str">"cmt">// Calculate weighted average during transition
   class="type">class="kw">double previousWeight = (class="type">class="kw">double)(maxTransitionBars - transitionBars) / maxTransitionBars;
   class="type">class="kw">double currentWeight = (class="type">class="kw">double)transitionBars / maxTransitionBars;
   
   class="kw">return regimeLotSizes[previousRegime] * previousWeight +
           regimeLotSizes[currentRegime] * currentWeight;
}
class=class="str">"cmt">// Example of strategy selection based on market regime
class="type">bool ExecuteTradeSignal(ENUM_MARKET_REGIME regime, class="type">int strategySignal)
{
   class=class="str">"cmt">// Strategy signal: class="num">1 = buy, -class="num">1 = sell, class="num">0 = no signal
   
   class="kw">switch(regime)
   {
       case REGIME_TRENDING_UP:
       case REGIME_TRENDING_DOWN:
           class=class="str">"cmt">// In trending regimes, only take signals in the direction of the trend
           if((regime == REGIME_TRENDING_UP && strategySignal == class="num">1) ||
              (regime == REGIME_TRENDING_DOWN && strategySignal == -class="num">1))
              class="kw">return true;
           break;
           
       case REGIME_RANGING:
           class=class="str">"cmt">// In ranging regimes, take all signals
           if(strategySignal != class="num">0)
              class="kw">return true;
           break;
           
       case REGIME_VOLATILE:
           class=class="str">"cmt">// In volatile regimes, be more selective
           class=class="str">"cmt">// Only take strong signals(implementation depends on strategy)
           if(IsStrongSignal(strategySignal))
              class="kw">return true;
           break;

◍ 按市况切换均线周期与止损止盈

把市场切成趋势、震荡、高波动、未定义四类后,策略参数不能一套用到底。下面这段逻辑就是按 regime 动态改 FastPeriod / SlowPeriod 和止损止盈的硬写法。 趋势市(上行或下行)给长均线:快线 20、慢线 50,止损止盈走 Trending 系列宽值;震荡市缩到快 10 慢 25,用 Ranging 的紧止损小盈利;高波动市更进一步,快 5 慢 15,配 Volatile 宽止损大盈利。 未定义市直接 default 兜底:快 14 慢 28、止损 100 点、盈利 200 点,相当于退回最中庸的出厂设置。外汇与贵金属杠杆高,参数切换只是降低错配概率,不等于过滤掉风险。 另有一段 MonitorRegimeDetectionPerformance 用静态变量记 regimeTransitions 与 correctPredictions,拿上次 regime 和本次对比来算识别准确率——你开 MT5 把这两个计数器 print 出来,跑两周就能看出自己的 regime 判定靠不靠谱。

MQL5 / C++
class="kw">default:
   class=class="str">"cmt">// In undefined regimes, don&class="macro">#x27;t trade
   break;
}

class="kw">return false;
}
class=class="str">"cmt">// Example of parameter adaptation based on market regime
class="type">void AdaptStrategyParameters(ENUM_MARKET_REGIME regime)
{
   class="kw">switch(regime)
   {
      case REGIME_TRENDING_UP:
      case REGIME_TRENDING_DOWN:
         class=class="str">"cmt">// In trending regimes, use longer moving averages
         FastPeriod = class="num">20;
         SlowPeriod = class="num">50;
         class=class="str">"cmt">// Use wider stop losses
         StopLoss = TrendingStopLoss;
         class=class="str">"cmt">// Use larger take profits
         TakeProfit = TrendingTakeProfit;
         break;
         
      case REGIME_RANGING:
         class=class="str">"cmt">// In ranging regimes, use shorter moving averages
         FastPeriod = class="num">10;
         SlowPeriod = class="num">25;
         class=class="str">"cmt">// Use tighter stop losses
         StopLoss = RangingStopLoss;
         class=class="str">"cmt">// Use smaller take profits
         TakeProfit = RangingTakeProfit;
         break;
         
      case REGIME_VOLATILE:
         class=class="str">"cmt">// In volatile regimes, use very class="type">class="kw">short moving averages
         FastPeriod = class="num">5;
         SlowPeriod = class="num">15;
         class=class="str">"cmt">// Use wider stop losses
         StopLoss = VolatileStopLoss;
         class=class="str">"cmt">// Use larger take profits
         TakeProfit = VolatileTakeProfit;
         break;
         
      class="kw">default:
         class=class="str">"cmt">// In undefined regimes, use class="kw">default parameters
         FastPeriod = class="num">14;
         SlowPeriod = class="num">28;
         StopLoss = class="num">100;
         TakeProfit = class="num">200;
         break;
   }
}
class=class="str">"cmt">// Example of performance monitoring for regime detection
class="type">void MonitorRegimeDetectionPerformance()
{
   class="kw">static class="type">int regimeTransitions = class="num">0;
   class="kw">static class="type">int correctPredictions = class="num">0;
   class="kw">static ENUM_MARKET_REGIME lastRegime = REGIME_UNDEFINED;
   
   class=class="str">"cmt">// Get current regime
   ENUM_MARKET_REGIME currentRegime = Detector.GetCurrentRegime();
   
   class=class="str">"cmt">// If regime has changed, evaluate the previous regime&class="macro">#x27;s prediction

「状态切换时怎么算识别准不准」

市场状态识别最怕「换个名字就当没发生过」。上面这段逻辑只在状态真正发生跳变、且上一状态不是未定义时才会计一次转换,避免开盘或数据缺失造成的脏计数。 regimeTransitions 累计的是「有效切换次数」,correctPredictions 则是你自己的 EvaluateRegimePrediction 判为正确的次数。两者相除乘 100 才是准确率,这个分母必须排除 UNDEFINED 初值,否则前几次会把胜率人为拉低。 Print 输出的格式是「Regime Detection Accuracy: 73.25% (58/79)」这类字符串,直接在 MT5 专家日志里能看到。外汇与贵金属波动受杠杆和跳空影响大,状态识别准确率仅代表历史样本倾向,实盘仍属高风险。 把 EvaluateRegimePrediction 留成你自己的判定函数很关键:比如上一状态判为趋势、实际后续 N 根收盘同向突破,才算 correct。先拿 EURUSD 的 M15 跑两周,看日志里分母到 80 附近时准确率落在什么区间,再决定阈值。

MQL5 / C++
if(currentRegime != lastRegime && lastRegime != REGIME_UNDEFINED)
{
    regimeTransitions++;
    
    class=class="str">"cmt">// Evaluate if the previous regime&class="macro">#x27;s prediction was correct
    class=class="str">"cmt">// Implementation depends on your specific evaluation criteria
    if(EvaluateRegimePrediction(lastRegime))
        correctPredictions++;
        
    class=class="str">"cmt">// Log performance metrics
    class="type">class="kw">double accuracy = (class="type">class="kw">double)correctPredictions / regimeTransitions * class="num">100.0;
    Print("Regime Detection Accuracy: ", DoubleToString(accuracy, class="num">2), "% (",
          correctPredictions, "/", regimeTransitions, ")");
}

lastRegime = currentRegime;

多周期状态概览直接挂图左上角

这套指标不画任何线,只在主图左上角用文字列出你勾选的各周期市场状态(上涨/下跌趋势、震荡、高波动)。它依赖一个状态检测类,给每个被选中的周期建一个独立实例,互不干扰地维护自己的内部计算。 输入参数里 LookbackPeriod 默认 100、TrendThreshold 默认 0.2、VolatilityThreshold 默认 1.5,这三个值会统一灌给所有周期的检测器;UseM1 到 UseMN1 九个布尔开关决定参与分析的周期,示例里 M15/H1/H4/D1 等设了 true,M1/W1/MN1 设了 false。 OnInit 里先调 InitializeTimeframes 统计 true 的数量,把对应 PERIOD_ 常量塞进 Timeframes 数组,再把 Detectors 数组resize好,逐个 new 出检测器对象。OnCalculate 每 tick 跑一次:先用 CopyClose 按各周期取最近 LookbackPeriod 根收盘价,哪怕指标挂在 M15 图上也能抓到 H4 的收价,喂给对应实例的 ProcessData,最后用 Comment 把汇总文本刷在角落。 OnDeinit 会 delete 掉所有检测器对象防内存泄漏,并清掉注释。外汇和贵金属波动剧烈、杠杆风险高,多周期共振只提高概率倾向,不代表方向必现。 把下面这段直接存成 .mq5 挂上 MT5,改几个 UseX 开关就能看到跨周期状态同屏;先别急着写 EA,手点切换周期观察文字输出和肉眼 K 线是否对得上,比盲信信号更实在。

MQL5 / C++
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_buffers class="num">0
class="macro">#class="kw">property indicator_plots  class="num">0
class=class="str">"cmt">// Include the Market Regime Detector
class="macro">#include <MarketRegimeEnum.mqh>
class="macro">#include <MarketRegimeDetector.mqh>
class=class="str">"cmt">// Input parameters
input class="type">int      LookbackPeriod = class="num">100;      class=class="str">"cmt">// Lookback period for calculations
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">// Timeframes to analyze
input class="type">bool     UseM1 = false;             class=class="str">"cmt">// Use class="num">1-minute timeframe
input class="type">bool     UseM5 = false;             class=class="str">"cmt">// Use class="num">5-minute timeframe
input class="type">bool     UseM15 = true;             class=class="str">"cmt">// Use class="num">15-minute timeframe
input class="type">bool     UseM30 = true;             class=class="str">"cmt">// Use class="num">30-minute timeframe
input class="type">bool     UseH1 = true;              class=class="str">"cmt">// Use class="num">1-hour timeframe
input class="type">bool     UseH4 = true;              class=class="str">"cmt">// Use class="num">4-hour timeframe
input class="type">bool     UseD1 = true;              class=class="str">"cmt">// Use Daily timeframe
input class="type">bool     UseW1 = false;             class=class="str">"cmt">// Use Weekly timeframe
input class="type">bool     UseMN1 = false;            class=class="str">"cmt">// Use Monthly timeframe
class=class="str">"cmt">// Global variables
CMarketRegimeDetector *Detectors[];
ENUM_TIMEFRAMES Timeframes[];
class="type">int TimeframeCount = class="num">0;
把状态切换监控交给小布
小布盯盘的 AIGC 已内置多时间周期状态诊断,打开对应品种页即可看到当前趋势、震荡或高波动标签,你只需把精力放在 EA 逻辑校验上。

常见问题

趋势市场走趋势跟踪逻辑,震荡市场切到均值回归;高波动时改用缩仓位的突破策略,具体参数在 EA 输入项里可调。
不同品种和时间周期的参数优化要分开做,跨周期直接套用容易在状态过渡期放大滑点和假信号。
需要在代码层加状态确认计数或冷却时间,等检测器连续 N 根 K 线确认新状态再切换策略分支。
可以,小布的品种页会标出多周期状态,配合预警规则能在状态跃迁时推送,省去你自己写监测脚本。
实战中常用大周期定方向、小周期找入场;冲突期降低仓位比硬选一边更稳妥,外汇贵金属本身杠杆高风险大。