在MQL5中构建自定义市场状态检测系统(第一部分):指标·进阶篇
(2/3)· 多数EA死在行情切换上,本篇教你用MQL5指标客观划分三种市场状态
分位数与中位数怎么从数组里算出来
在 MT5 里做价格行为统计,经常要拿一批样本算某个分位点。下面这段逻辑先把数据排好序,再用线性插值取百分位数,比单纯取整更贴近真实分布。 Percentile 方法先拦截空数据和越界百分比:样本数小于等于 0,或 percentile 不在 0–100 之间,直接返回 0.0。若 m_isSorted 为 false,就把原数据拷到 m_sortedData 并调用 ArraySort,置排序标志为 true,避免重复排序。 位置计算用 (percentile/100.0)*(m_dataSize-1),例如 50 分位在 10 个样本时落在索引 4.5。MathFloor 和 MathCeil 分别取上下邻位;若两索引相等直接返回该值,否则按小数比例在相邻两数间插值。 Median 只是 Percentile(50.0) 的包装,对外暴露更直观。外汇与贵金属波动有跳空风险,这类统计只反映历史样本特征,信号可能失效,实盘前务必在 MT5 策略测试器跑一遍。
class=class="str">"cmt">// Return the negative of autocorrelation, so positive values class=class="str">"cmt">// indicate stronger mean reversion class="kw">return -ac1; } class="type">class="kw">double CStatistics::Percentile(class="type">class="kw">double percentile) { if(m_dataSize <= class="num">0 || percentile < class="num">0.0 || percentile > class="num">100.0) class="kw">return class="num">0.0; class=class="str">"cmt">// Sort data if needed if(!m_isSorted) { ArrayResize(m_sortedData, m_dataSize); for(class="type">int i = class="num">0; i < m_dataSize; i++) m_sortedData[i] = m_data[i]; ArraySort(m_sortedData); m_isSorted = true; } class=class="str">"cmt">// Calculate position class="type">class="kw">double position = (percentile / class="num">100.0) * (m_dataSize - class="num">1); class="type">int lowerIndex = (class="type">int)MathFloor(position); class="type">int upperIndex = (class="type">int)MathCeil(position); class=class="str">"cmt">// Handle edge cases if(lowerIndex == upperIndex) class="kw">return m_sortedData[lowerIndex]; class=class="str">"cmt">// Interpolate class="type">class="kw">double fraction = position - lowerIndex; class="kw">return m_sortedData[lowerIndex] + fraction * (m_sortedData[upperIndex] - m_sortedData[lowerIndex]); } class="type">class="kw">double CStatistics::Median() { class="kw">return Percentile(class="num">50.0); }
◍ 把统计度量变成五类市况的判别器
市场状态检测器把前面做好的统计工具收拢到一个类里,直接输出当前属于哪一类市况。系统定义了五种状态:上涨趋势、下跌趋势、震荡、高波动、未定义,分别用 0 到 4 的枚举值标记,整个检测链路都围绕这几个值转。 CMarketRegimeDetector 的构造函数强制回溯周期至少 20 根 K 线,理由是低于这个样本量统计显著性不够;平滑周期必须为正。析构函数手动释放数组内存,在 MQL5 自动垃圾回收之外算是个好习惯,不是必须但能避免大周期跑久了的内存碎片。 分类逻辑是分层的,波动率优先于趋势。DetermineRegime() 先比最新波动率和过去 20 根均值,超阈值就判为波动市;否则再看绝对趋势强度是否过阈,过了按平滑周期内价格方向定多空;都不满足就是震荡。这种顺序是因为趋势策略在波动市里特别容易吃亏,先拦掉最危险的形态。 计算层用了滚动窗口而不是整段回溯一次性算。CalculateVolatility() 在每个时间点算收益率标准差,CalculateTrendStrength() 给每个窗口建临时 CStatistics 对象取自相关趋势强度,比全周期单值更跟得上盘面切换。外汇和贵金属波动大、跳空频繁,这类滚动评估对实盘风控更有参考价值,但任何状态判断都只是概率倾向,不代表后续一定延续。
class=class="str">"cmt">// Define market regime types enum ENUM_MARKET_REGIME { REGIME_TRENDING_UP = class="num">0, class=class="str">"cmt">// Trending up regime REGIME_TRENDING_DOWN = class="num">1, class=class="str">"cmt">// Trending down regime REGIME_RANGING = class="num">2, class=class="str">"cmt">// Ranging/sideways regime REGIME_VOLATILE = class="num">3, class=class="str">"cmt">// Volatile/chaotic regime REGIME_UNDEFINED = class="num">4 class=class="str">"cmt">// Undefined regime(class="kw">default) }; class CMarketRegimeDetector { class="kw">private: class=class="str">"cmt">// Configuration class="type">int m_lookbackPeriod; class=class="str">"cmt">// Period for calculations class="type">int m_smoothingPeriod; class=class="str">"cmt">// Period for smoothing regime transitions class="type">class="kw">double m_trendThreshold; class=class="str">"cmt">// Threshold for trend detection class="type">class="kw">double m_volatilityThreshold; class=class="str">"cmt">// Threshold for volatility detection class=class="str">"cmt">// Data buffers class="type">class="kw">double m_priceData[]; class=class="str">"cmt">// Price data buffer class="type">class="kw">double m_returns[]; class=class="str">"cmt">// Returns data buffer class="type">class="kw">double m_volatility[]; class=class="str">"cmt">// Volatility buffer class="type">class="kw">double m_trendStrength[]; class=class="str">"cmt">// Trend strength buffer class="type">class="kw">double m_regimeBuffer[]; class=class="str">"cmt">// Regime classification buffer class=class="str">"cmt">// Statistics objects CStatistics m_priceStats; class=class="str">"cmt">// Statistics for price data CStatistics m_returnsStats; class=class="str">"cmt">// Statistics for returns data CStatistics m_volatilityStats; class=class="str">"cmt">// Statistics for volatility data class=class="str">"cmt">// Current state ENUM_MARKET_REGIME m_currentRegime; class=class="str">"cmt">// Current detected regime class=class="str">"cmt">// Helper methods class="type">void CalculateReturns(); class="type">void CalculateVolatility();
「行情状态探测器的接口与构造函数落地」
做市场状态识别,第一步是把类接口和内存布局定清楚。下面这段 CMarketRegimeDetector 的头文件声明,暴露了回看周期默认 100 根 K 线、平滑周期默认 10 根,并预留了趋势强度与波动率两个阈值接口,分别对应 0.2 与 1.5 的初始值。 构造函数里有个容易踩的坑:lookbackPeriod 若传入小于等于 20 的值,会被强制拉回 100;smoothingPeriod 若小于等于 0 则回落到 10。这意味着你直接写 CMarketRegimeDetector(50) 是生效的,但写 CMarketRegimeDetector(10) 实际跑的还是 100 周期回看。 六个核心数组(价格、收益、波动率、趋势强度、状态缓冲等)在构造时一次性 ArrayResize 到 lookbackPeriod 或减一长度,并用 ArrayInitialize 清零。REGIME_UNDEFINED 作为初始状态写进 m_regimeBuffer,保证指标在首根有效数据前不会误报趋势。 析构函数调用 ArrayFree 释放数组,MQL5 虽自动回收,但显式释放能在 EA 频繁重载时降低内存碎片。外汇与贵金属波动剧烈,这类状态机若参数误设,可能将震荡误判为趋势,交易前请在 MT5 策略测试器跑一轮验证。
class="type">void CalculateTrendStrength(); ENUM_MARKET_REGIME DetermineRegime(); class="kw">public: class=class="str">"cmt">// Constructor and destructor CMarketRegimeDetector(class="type">int lookbackPeriod = class="num">100, class="type">int smoothingPeriod = class="num">10); ~CMarketRegimeDetector(); class=class="str">"cmt">// Configuration methods class="type">void SetLookbackPeriod(class="type">int period); class="type">void SetSmoothingPeriod(class="type">int period); class="type">void SetTrendThreshold(class="type">class="kw">double threshold); class="type">void SetVolatilityThreshold(class="type">class="kw">double threshold); class=class="str">"cmt">// Processing methods class="type">bool Initialize(); class="type">bool ProcessData(class="kw">const class="type">class="kw">double &price[], class="type">int size); class=class="str">"cmt">// Access methods ENUM_MARKET_REGIME GetCurrentRegime() class="kw">const { class="kw">return m_currentRegime; } class="type">class="kw">string GetRegimeDescription() class="kw">const; class="type">class="kw">double GetTrendStrength() class="kw">const; class="type">class="kw">double GetVolatility() class="kw">const; class=class="str">"cmt">// Buffer access for indicators class="type">bool GetRegimeBuffer(class="type">class="kw">double &buffer[]) class="kw">const; class="type">bool GetTrendStrengthBuffer(class="type">class="kw">double &buffer[]) class="kw">const; class="type">bool GetVolatilityBuffer(class="type">class="kw">double &buffer[]) class="kw">const; }; CMarketRegimeDetector::CMarketRegimeDetector(class="type">int lookbackPeriod, class="type">int smoothingPeriod) { class=class="str">"cmt">// Set class="kw">default parameters m_lookbackPeriod = (lookbackPeriod > class="num">20) ? lookbackPeriod : class="num">100; m_smoothingPeriod = (smoothingPeriod > class="num">0) ? smoothingPeriod : class="num">10; m_trendThreshold = class="num">0.2; m_volatilityThreshold = class="num">1.5; class=class="str">"cmt">// Initialize current regime m_currentRegime = REGIME_UNDEFINED; class=class="str">"cmt">// Initialize buffers ArrayResize(m_priceData, m_lookbackPeriod); ArrayResize(m_returns, m_lookbackPeriod - class="num">1); ArrayResize(m_volatility, m_lookbackPeriod - class="num">1); ArrayResize(m_trendStrength, m_lookbackPeriod - class="num">1); ArrayResize(m_regimeBuffer, m_lookbackPeriod); class=class="str">"cmt">// Initialize buffers with zeros ArrayInitialize(m_priceData, class="num">0.0); ArrayInitialize(m_returns, class="num">0.0); ArrayInitialize(m_volatility, class="num">0.0); ArrayInitialize(m_trendStrength, class="num">0.0); ArrayInitialize(m_regimeBuffer, (class="type">class="kw">double)REGIME_UNDEFINED); } CMarketRegimeDetector::~CMarketRegimeDetector() { class=class="str">"cmt">// Free memory(not strictly necessary in MQL5, but good practice) ArrayFree(m_priceData); ArrayFree(m_returns); ArrayFree(m_volatility); ArrayFree(m_trendStrength);
回看窗口变更后如何重建缓冲区
市场状态检测类在运行时若调整回看周期,必须先守住下限:SetLookbackPeriod 里硬性要求 period > 20,否则直接 return 不做任何事。这个 20 根 K 线的最小样本量,是后续波动率与趋势强度统计有意义的前提,低于它噪声会完全淹没信号。 通过校验后,函数按新周期对五个数组重定容:价格序列取 m_lookbackPeriod 长度,而收益、波动率、趋势强度三者因是相邻价差,故长度为 m_lookbackPeriod - 1;状态缓冲同样预留 m_lookbackPeriod 格。重定容完毕立即调 Initialize() 把历史脏数据清掉。 Initialize() 用 ArrayInitialize 把前四类缓冲填 0.0,状态缓冲填 REGIME_UNDEFINED,并将 m_currentRegime 复位。注意这里释放旧内存用的是 ArrayFree(m_regimeBuffer) 的配对逻辑,避免 MT5 终端在长期切换周期时内存累积。 真正喂数在 ProcessData():当传入 price 数组长度不足 m_lookbackPeriod 时直接拒收返回 false。够长则截取末尾 m_lookbackPeriod 根填入 m_priceData,依次跑 CalculateReturns / CalculateVolatility / CalculateTrendStrength,再由 DetermineRegime() 得出当前市况。 最后一段把 m_regimeBuffer 整体左移一格,把最新判定写到末位,形成可绘制的状态时间序列。外汇与贵金属杠杆高,这套状态识别仅作概率参考,实盘须自担滑点与黑天鹅风险。
ArrayFree(m_regimeBuffer); } class="type">void CMarketRegimeDetector::SetLookbackPeriod(class="type">int period) { if(period <= class="num">20) class="kw">return; m_lookbackPeriod = period; class=class="str">"cmt">// Resize buffers ArrayResize(m_priceData, m_lookbackPeriod); ArrayResize(m_returns, m_lookbackPeriod - class="num">1); ArrayResize(m_volatility, m_lookbackPeriod - class="num">1); ArrayResize(m_trendStrength, m_lookbackPeriod - class="num">1); ArrayResize(m_regimeBuffer, m_lookbackPeriod); class=class="str">"cmt">// Re-initialize Initialize(); } class="type">void CMarketRegimeDetector::SetSmoothingPeriod(class="type">int period) { if(period <= class="num">0) class="kw">return; m_smoothingPeriod = period; } class="type">void CMarketRegimeDetector::SetTrendThreshold(class="type">class="kw">double threshold) { if(threshold <= class="num">0.0) class="kw">return; m_trendThreshold = threshold; } class="type">void CMarketRegimeDetector::SetVolatilityThreshold(class="type">class="kw">double threshold) { if(threshold <= class="num">0.0) class="kw">return; m_volatilityThreshold = threshold; } class="type">bool CMarketRegimeDetector::Initialize() { class=class="str">"cmt">// Initialize buffers with zeros ArrayInitialize(m_priceData, class="num">0.0); ArrayInitialize(m_returns, class="num">0.0); ArrayInitialize(m_volatility, class="num">0.0); ArrayInitialize(m_trendStrength, class="num">0.0); ArrayInitialize(m_regimeBuffer, (class="type">class="kw">double)REGIME_UNDEFINED); class=class="str">"cmt">// Reset current regime m_currentRegime = REGIME_UNDEFINED; class="kw">return true; } class="type">bool CMarketRegimeDetector::ProcessData(class="kw">const class="type">class="kw">double &price[], class="type">int size) { if(size < m_lookbackPeriod) class="kw">return false; class=class="str">"cmt">// Copy the most recent price data for(class="type">int i = class="num">0; i < m_lookbackPeriod; i++) m_priceData[i] = price[size - m_lookbackPeriod + i]; class=class="str">"cmt">// Calculate returns, volatility, and trend strength CalculateReturns(); CalculateVolatility(); CalculateTrendStrength(); class=class="str">"cmt">// Determine the current market regime m_currentRegime = DetermineRegime(); class=class="str">"cmt">// Update regime buffer for indicator display for(class="type">int i = class="num">0; i < m_lookbackPeriod - class="num">1; i++) m_regimeBuffer[i] = m_regimeBuffer[i + class="num">1]; m_regimeBuffer[m_lookbackPeriod - class="num">1] = (class="type">class="kw">double)m_currentRegime; class="kw">return true; } class="type">void CMarketRegimeDetector::CalculateReturns() { for(class="type">int i = class="num">0; i < m_lookbackPeriod - class="num">1; i++) { class=class="str">"cmt">// Calculate percentage returns
◍ 波动率与趋势强度的滚动窗口算法
市场状态检测里,收益率序列只是原料。真正拿来区分震荡与趋势的,是波动率(标准差)和趋势强度(自相关)这两个滚动指标。下面这段实现把窗口大小做了硬约束:波动率窗口取 20 与回看周期减一的最小值,趋势强度窗口取 50 与回看周期减一的最小值。 波动率计算先对窗口内收益率求均值,再跑平方偏差求和、除以自由度(windowSize-1)后开方。前 windowSize-1 个位置因样本不足直接置 0,这是避免小样本标准差失真。 趋势强度段复用了统计对象的 SetData,把滚动窗口拷贝进临时数组后交给自相关逻辑。外汇与贵金属杠杆高、跳空频繁,这类统计指标在重大数据行情中可能瞬时失效,验证时建议先用 EURUSD 的 M15 跑一遍。 别把正态当圣经:收益率若含跳空缺口,标准差会被异常拉高,这时阈值判断要留缓冲。
if(m_priceData[i] != class="num">0.0) m_returns[i] = (m_priceData[i + class="num">1] - m_priceData[i]) / m_priceData[i] * class="num">100.0; else m_returns[i] = class="num">0.0; } class=class="str">"cmt">// Update returns statistics m_returnsStats.SetData(m_returns, m_lookbackPeriod - class="num">1); } class="type">void CMarketRegimeDetector::CalculateVolatility() { class=class="str">"cmt">// Use a rolling window for volatility calculation class="type">int windowSize = MathMin(class="num">20, m_lookbackPeriod - class="num">1); for(class="type">int i = class="num">0; i < m_lookbackPeriod - class="num">1; i++) { if(i < windowSize - class="num">1) { m_volatility[i] = class="num">0.0; class="kw">continue; } class="type">class="kw">double sum = class="num">0.0; class="type">class="kw">double mean = class="num">0.0; class=class="str">"cmt">// Calculate mean for(class="type">int j = class="num">0; j < windowSize; j++) mean += m_returns[i - j]; mean /= windowSize; class=class="str">"cmt">// Calculate standard deviation for(class="type">int j = class="num">0; j < windowSize; j++) sum += MathPow(m_returns[i - j] - mean, class="num">2); m_volatility[i] = MathSqrt(sum / (windowSize - class="num">1)); } class=class="str">"cmt">// Update volatility statistics m_volatilityStats.SetData(m_volatility, m_lookbackPeriod - class="num">1); } class="type">void CMarketRegimeDetector::CalculateTrendStrength() { class=class="str">"cmt">// Use a rolling window for trend strength calculation class="type">int windowSize = MathMin(class="num">50, m_lookbackPeriod - class="num">1); for(class="type">int i = class="num">0; i < m_lookbackPeriod - class="num">1; i++) { if(i < windowSize - class="num">1) { m_trendStrength[i] = class="num">0.0; class="kw">continue; } class="type">class="kw">double window[]; ArrayResize(window, windowSize); class=class="str">"cmt">// Copy data to window for(class="type">int j = class="num">0; j < windowSize; j++) window[j] = m_returns[i - j]; class=class="str">"cmt">// Create temporary statistics object CStatistics tempStats; tempStats.SetData(window, windowSize); class=class="str">"cmt">// Calculate trend strength class="kw">using autocorrelation
「用波动率与趋势强度切分市场状态」
判定逻辑在 DetermineRegime() 里完成:先取倒数第 2 根 K 线的趋势强度与波动率,再用前 20 根(索引 lookbackPeriod-22 到 lookbackPeriod-3)的波动率算均值做对照。 波动率若超过均值乘以 m_volatilityThreshold,直接归为 REGIME_VOLATILE;否则看趋势强度绝对值是否过 m_trendThreshold,再结合 smoothingPeriod 区间的价格变化方向给出上行或下行趋势。 GetRegimeDescription() 只是把枚举翻成字符串,方便面板或日志打印;GetTrendStrength() 则在样本不足(lookbackPeriod<=2)时做保护。外汇与贵金属这类高杠杆品种,状态误判会放大回撤,参数阈值建议先在 MT5 历史数据里跑一遍再上实盘。
m_trendStrength[i] = tempStats.TrendStrength(); } class=class="str">"cmt">// Update price statistics m_priceStats.SetData(m_priceData, m_lookbackPeriod); } ENUM_MARKET_REGIME CMarketRegimeDetector::DetermineRegime() { class=class="str">"cmt">// Get the latest values class="type">class="kw">double latestTrendStrength = m_trendStrength[m_lookbackPeriod - class="num">2]; class="type">class="kw">double latestVolatility = m_volatility[m_lookbackPeriod - class="num">2]; class=class="str">"cmt">// Get the average volatility for comparison class="type">class="kw">double avgVolatility = class="num">0.0; class="type">int count = class="num">0; for(class="type">int i = m_lookbackPeriod - class="num">22; i < m_lookbackPeriod - class="num">2; i++) { if(i >= class="num">0) { avgVolatility += m_volatility[i]; count++; } } if(count > class="num">0) avgVolatility /= count; else avgVolatility = latestVolatility; class=class="str">"cmt">// Determine price direction class="type">class="kw">double priceChange = m_priceData[m_lookbackPeriod - class="num">1] - m_priceData[m_lookbackPeriod - m_smoothingPeriod - class="num">1]; class=class="str">"cmt">// Classify the regime if(latestVolatility > avgVolatility * m_volatilityThreshold) { class=class="str">"cmt">// Highly volatile market class="kw">return REGIME_VOLATILE; } else if(MathAbs(latestTrendStrength) > m_trendThreshold) { class=class="str">"cmt">// Trending market if(priceChange > class="num">0) class="kw">return REGIME_TRENDING_UP; else class="kw">return REGIME_TRENDING_DOWN; } else { class=class="str">"cmt">// Ranging market class="kw">return REGIME_RANGING; } } class="type">class="kw">string CMarketRegimeDetector::GetRegimeDescription() class="kw">const { class="kw">switch(m_currentRegime) { case REGIME_TRENDING_UP: class="kw">return "Trending Up"; case REGIME_TRENDING_DOWN: class="kw">return "Trending Down"; case REGIME_RANGING: class="kw">return "Ranging"; case REGIME_VOLATILE: class="kw">return "Volatile"; class="kw">default: class="kw">return "Undefined"; } } class="type">class="kw">double CMarketRegimeDetector::GetTrendStrength() class="kw">const { if(m_lookbackPeriod <= class="num">2)