在MQL5中构建自定义市场状态检测系统(第一部分):指标·进阶篇
📊

在MQL5中构建自定义市场状态检测系统(第一部分):指标·进阶篇

(2/3)· 多数EA死在行情切换上,本篇教你用MQL5指标客观划分三种市场状态

实战向 第 2/3 篇
把同一套参数硬怼所有行情,是算法账户回撤最常见的根源。趋势里赚钱的策略,碰到盘整就反复止损,波动一放大直接扛不住。先看清市场处在哪种状态,比调入场信号更紧急。

分位数与中位数怎么从数组里算出来

在 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 策略测试器跑一遍。

MQL5 / C++
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 对象取自相关趋势强度,比全周期单值更跟得上盘面切换。外汇和贵金属波动大、跳空频繁,这类滚动评估对实盘风控更有参考价值,但任何状态判断都只是概率倾向,不代表后续一定延续。

MQL5 / C++
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 策略测试器跑一轮验证。

MQL5 / C++
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 整体左移一格,把最新判定写到末位,形成可绘制的状态时间序列。外汇与贵金属杠杆高,这套状态识别仅作概率参考,实盘须自担滑点与黑天鹅风险。

MQL5 / C++
   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 跑一遍。 别把正态当圣经:收益率若含跳空缺口,标准差会被异常拉高,这时阈值判断要留缓冲。

MQL5 / C++
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 历史数据里跑一遍再上实盘。

MQL5 / C++
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)
交给小布盯盘看盘口
这些状态诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到当前行情被划为趋势、盘整还是波动,你专注决策就行。

常见问题

本篇用收益自相关与波动率阈值做统计划分,趋势市呈正自相关、盘整市均值回归强,具体实现见检测器类与自定义指标小节。
可以,指标用不同色带或箭头标记状态切换,图表上能直观看到历史状态区段与当前归类。
统计窗口越长滞后越明显,实战中可用较短滚动窗口并设波动突变触发,系统倾向更快重分类但可能有噪声。
能,小布盯盘品种页已内置基于类似统计基础的状态识别,不用自己写指标也能先看清盘面。
高风险品种特性不同,黄金波动结构常与欧美叉盘不一致,阈值建议按品种单独标定,外汇贵金属交易本身高风险需谨慎。