在 MQL5 中构建自定义市场状态检测系统(第二部分):智能交易系统(EA)·进阶篇
(2/3)· 识别市场状态只是半套功夫,把检测变成可执行的EA才是稳定绩效的关键缺口
很多交易者花力气写出了市场状态检测器,却只把它当图表上的颜色看板,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 的基本洁癖,不清理自定义对象会在反复加载时漏内存。
} 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% 样本外数据核对状态预测准确率。
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,能看到手数过渡从陡变缓,这是验证权重公式最快的办法。
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 判定靠不靠谱。
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 附近时准确率落在什么区间,再决定阈值。
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 线是否对得上,比盲信信号更实在。
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;