交易中的混沌理论(第二部分):深入探索·进阶篇
📘

交易中的混沌理论(第二部分):深入探索·进阶篇

第 2/3 篇

递归图指标的核心计算骨架

把递归量化(RQA)搬进 MT5 自定义指标,第一道坎是 OnCalculate 的增量计算逻辑。下面这段把收盘价、tick 成交量、真实成交量与点差数组传进来,先卡住最少 K 线数:rates_total 小于 minRequiredBars 直接 return(0),避免窗口不够算出自欺欺人的零值。 start 变量的写法值得抄:prev_calculated 大于 0 时只从上一根已算位置减 1 开始补算,否则从 minRequiredBars-1 冷启动。这样每根新 tick 只跑增量,EU/黄金这类高频品种也不会把 CPU 烤熟。 CalculateRecurrenceMeasures 才是真核心。它用 InpWindowSize、InpEmbeddingDimension、InpTimeDelay 三个输入算出 matrixSize,然后双重循环两两比价,调用 IsRecurrent 判定是否递归。recurrenceRate 是递归点占全矩阵比;determinism 用对角连线数除递归总数,衡量趋势可预测性;laminarity 用垂直连线数除递归总数,反映震荡滞留倾向。外汇与贵金属杠杆高、滑点突兀,这些比值只作概率参考,实盘前务必在策略测试器跑一遍不同窗口参数。 IsRecurrent 的边界守卫很朴素:i 或 j 越界直接返回 false,防止嵌入维度展开时读到数组外。这一层不写稳,回测到行情头尾就爆数组越界错误。

MQL5 / C++
class="kw">const class="type">class="kw">double &close[],
class="kw">const class="type">long &tick_volume[],
class="kw">const class="type">long &volume[],
class="kw">const class="type">int &spread[])
{
  if(rates_total < minRequiredBars) class="kw">return(class="num">0);

  class="type">int start = (prev_calculated > class="num">0) ? MathMax(prev_calculated - class="num">1, minRequiredBars - class="num">1) : minRequiredBars - class="num">1;

  for(class="type">int i = start; i < rates_total; i++)
  {
    CalculateRecurrenceMeasures(close, rates_total, i, RecurrenceRateBuffer[i], DeterminismBuffer[i], LaminarityBuffer[i]);
  }

  class="kw">return(rates_total);
}
class="type">void CalculateRecurrenceMeasures(class="kw">const class="type">class="kw">double &price[], class="type">int price_total, class="type">int index, class="type">class="kw">double &recurrenceRate, class="type">class="kw">double &determinism, class="type">class="kw">double &laminarity)
{
  if(index < minRequiredBars - class="num">1 || index >= price_total)
  {
    recurrenceRate = class="num">0;
    determinism = class="num">0;
    laminarity = class="num">0;
    class="kw">return;
  }
  class="type">int windowStart = index - InpWindowSize + class="num">1;
  class="type">int matrixSize = InpWindowSize - (InpEmbeddingDimension - class="num">1) * InpTimeDelay;

  class="type">int recurrenceCount = class="num">0;
  class="type">int diagonalLines = class="num">0;
  class="type">int verticalLines = class="num">0;

  for(class="type">int i = class="num">0; i < matrixSize; i++)
  {
    for(class="type">int j = class="num">0; j < matrixSize; j++)
    {
      class="type">bool isRecurrent = IsRecurrent(price, price_total, windowStart + i, windowStart + j);
      if(isRecurrent)
      {
        recurrenceCount++;

        class=class="str">"cmt">// Check for diagonal lines
        if(i > class="num">0 && j > class="num">0 && IsRecurrent(price, price_total, windowStart + i - class="num">1, windowStart + j - class="num">1))
          diagonalLines++;

        class=class="str">"cmt">// Check for vertical lines
        if(i > class="num">0 && IsRecurrent(price, price_total, windowStart + i - class="num">1, windowStart + j))
          verticalLines++;
      }
    }
  }

  recurrenceRate = (class="type">class="kw">double)recurrenceCount / (matrixSize * matrixSize);
  determinism = (recurrenceCount > class="num">0) ? (class="type">class="kw">double)diagonalLines / recurrenceCount : class="num">0;
  laminarity = (recurrenceCount > class="num">0) ? (class="type">class="kw">double)verticalLines / recurrenceCount : class="num">0;
}
class="type">bool IsRecurrent(class="kw">const class="type">class="kw">double &price[], class="type">int price_total, class="type">int i, class="type">int j)
{
  if(i < class="num">0 || j < class="num">0 || i >= price_total || j >= price_total) class="kw">return class="kw">false;

「相空间距离怎么算才靠谱」

做延迟嵌入重构时,两段价格轨迹是否相似,靠的是欧氏距离而不是肉眼比形状。上面这段逻辑把嵌入维度和时间延迟都卷进循环,逐维求差再平方累加,最后开方得到真实距离。 循环里先用 offset = d * InpTimeDelay 跳到对应延迟位置,一旦越界(i 或 j 加偏移超过 price_total)直接返回 false,避免读脏数据。diff 是两层价格在某一维上的差,平方后累进 distance,这是标准的平方和开根。 最后一句 distance <= InpThreshold * _Point 才是判定相似的关键:阈值以点数 _Point 为单位,外汇和贵金属点值极小、跳空频繁,阈值设太窄会几乎不匹配,太宽又失去形态意义,建议在 MT5 里用历史数据反推一个分位值再固定。 这套计算对外汇/贵金属属于高风险研判辅助,距离近只代表历史形态概率接近,不预示后续必走同方向。

MQL5 / C++
  class="type">class="kw">double distance = class="num">0;
  for(class="type">int d = class="num">0; d < InpEmbeddingDimension; d++)
  {
    class="type">int offset = d * InpTimeDelay;
    if(i + offset >= price_total || j + offset >= price_total) class="kw">return class="kw">false;
    class="type">class="kw">double diff = price[i + offset] - price[j + offset];
    distance += diff * diff;
  }
  distance = MathSqrt(distance);

  class="kw">return (distance <= InpThreshold * _Point);
}

◍ 用相空间重构提前看波动率

Takens 嵌入定理给了一种从一维收盘价序列反推多维动力系统的方法。对外汇和贵金属这类高波动品种,它不假设收益服从正态,而是把价格延迟排成向量,在重构空间里找局部结构。 具体做法:取收盘价 p(t),按延迟 τ 拼出 m 维向量 x(t)=[p(t), p(t+τ), …, p(t+(m-1)τ)]。m 和 τ 不能拍脑袋——τ 常用互信息或自相关定,m 用假最近邻法选。本例指标默认 m=3、τ=5,仅作起点,实盘可能要重调。 波动率预测的逻辑很直白:重构空间里离得近的点,未来走势可能相似。对每个 x(t) 找 k 个最近邻(代码默认 k=10,文献常取 5~20),把这些邻居在 t+h 步的实际波动率取平均,作为 σ̂(t+h)=(1/k)Σσ(ti+h)。贵金属隔夜跳空多,h 设 10 根以上才可能避开流动性断层。 这套方法能吃进非线性波动形态,但对 m、τ、k 极敏感,且千根以上回看窗在 MT5 里计算开销明显。外汇保证金交易杠杆高,波动预测偏差会放大亏损,仅作辅助信号。 下面这段 MQL5 把上述思路落成了独立窗口指标,挂上就能看到红线的预测波动率。代码逐行拆:前 15 行 #property 声明版权、分离窗口、1 个红线绘图缓冲;5 个 input 是嵌入维、延迟、邻居数、预测步长、回看长度;OnInit 绑定缓冲并设 5 位小数与短名;OnCalculate 头部接 MT5 标准报价数组,后面该写邻域搜索与均值逻辑(原文截断)。

MQL5 / C++
class="macro">#class="kw">property copyright "Copyright class="num">2024, Evgeniy Shtenco"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property strict
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_buffers class="num">1
class="macro">#class="kw">property indicator_plots   class="num">1
class="macro">#class="kw">property indicator_label1  "Predicted Volatility"
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_color1  clrRed
class="macro">#class="kw">property indicator_style1  STYLE_SOLID
class="macro">#class="kw">property indicator_width1  class="num">1
class="kw">input class="type">int    InpEmbeddingDimension = class="num">3;     class=class="str">"cmt">// Embedding dimension
class="kw">input class="type">int    InpTimeDelay          = class="num">5;     class=class="str">"cmt">// Time delay
class="kw">input class="type">int    InpNeighbors          = class="num">10;    class=class="str">"cmt">// Number of neighbors
class="kw">input class="type">int    InpForecastHorizon    = class="num">10;    class=class="str">"cmt">// Forecast horizon
class="kw">input class="type">int    InpLookback           = class="num">1000;  class=class="str">"cmt">// Lookback period
class="type">class="kw">double PredictedVolatilityBuffer[];
class="type">int OnInit()
{
   SetIndexBuffer(class="num">0, PredictedVolatilityBuffer, INDICATOR_DATA);
   IndicatorSetInteger(INDICATOR_DIGITS, class="num">5);
   IndicatorSetString(INDICATOR_SHORTNAME, "Takens Volatility Forecast");
   class="kw">return(INIT_SUCCEEDED);
}
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double &open[],
                class="kw">const class="type">class="kw">double &high[],
                class="kw">const class="type">class="kw">double &low[],
                class="kw">const class="type">class="kw">double &close[],
                class="kw">const class="type">long &tick_volume[],
                class="kw">const class="type">long &volume[],

相空间重构下的波动率预测实现

这段 MT5 自定义指标的核心,是把收盘价序列按延迟坐标法重排成相空间向量,再用近邻匹配估计未来波动。PredictVolatility 函数接收价格数组与当前索引,先按 InpEmbeddingDimension 和 InpTimeDelay 拼出当前相点,任一索引越界直接返回 0,避免数组越界崩指标。 随后用 dataSize=InpLookback 个历史相点与当前向量算欧氏距离,distances[i] 存的是各延迟窗口内的平方差开方。ArraySort 后取前 InpNeighbors 个近邻,回看它们往后 InpForecastHorizon 根 K 线的收益率绝对值,平均后得到 PredictedVolatilityBuffer 的赋值。 主循环里 start 被设为 prev_calculated 与「InpLookback + 嵌入维数×延迟 + 预测步长」的较大值,保证前几根缓冲不足时不误算。若你在 EURUSD 的 M15 上把 InpLookback 设 500、InpNeighbors 设 10,缓冲区前约 530 根会留空,这是正常冷启动。 外汇与贵金属波动受杠杆与消息驱动,该估计仅反映历史相形相似度,未来波动可能放大也可能收敛,实盘前请在策略测试器跑至少三年 tick 数据验证分布。

MQL5 / C++
class="kw">const class="type">int &spread[])
{
  class="type">int start = MathMax(prev_calculated, InpLookback + InpEmbeddingDimension * InpTimeDelay + InpForecastHorizon);
  
  for(class="type">int i = start; i < rates_total; i++)
  {
    if (i >= InpEmbeddingDimension * InpTimeDelay && i + InpForecastHorizon < rates_total)
    {
      PredictedVolatilityBuffer[i] = PredictVolatility(close, i);
    }
  }
  
  class="kw">return(rates_total);
}
class="type">class="kw">double PredictVolatility(class="kw">const class="type">class="kw">double &price[], class="type">int index)
{
  class="type">int vectorSize = InpEmbeddingDimension;
  class="type">int dataSize = InpLookback;
  
  class="type">class="kw">double currentVector[];
  ArrayResize(currentVector, vectorSize);
  for(class="type">int i = class="num">0; i < vectorSize; i++)
  {
    class="type">int priceIndex = index - i * InpTimeDelay;
    if (priceIndex < class="num">0) class="kw">return class="num">0;  class=class="str">"cmt">// Prevent getting out of array
    currentVector[i] = price[priceIndex];
  }
  
  class="type">class="kw">double distances[];
  ArrayResize(distances, dataSize);
  
  for(class="type">int i = class="num">0; i < dataSize; i++)
  {
    class="type">class="kw">double sum = class="num">0;
    for(class="type">int j = class="num">0; j < vectorSize; j++)
    {
      class="type">int priceIndex = index - i - j * InpTimeDelay;
      if (priceIndex < class="num">0) class="kw">return class="num">0;  class=class="str">"cmt">// Prevent getting out of array
      class="type">class="kw">double diff = currentVector[j] - price[priceIndex];
      sum += diff * diff;
    }
    distances[i] = sqrt(sum);
  }
  
  class="type">int sortedIndices[];
  ArrayCopy(sortedIndices, distances);
  ArraySort(sortedIndices);
  
  class="type">class="kw">double sumVolatility = class="num">0;
  for(class="type">int i = class="num">0; i < InpNeighbors; i++)
  {
    class="type">int neighborIndex = index - sortedIndices[i];
    if (neighborIndex + InpForecastHorizon >= ArraySize(price)) class="kw">return class="num">0;  class=class="str">"cmt">// Prevent getting out of array
    class="type">class="kw">double futureReturn = (price[neighborIndex + InpForecastHorizon] - price[neighborIndex]) / price[neighborIndex];
    sumVolatility += MathAbs(futureReturn);
  }
  
  class="kw">return sumVolatility / InpNeighbors;
}

「相空间重构里τ和m怎么定」

用 Takens 定理把价格序列重构成相空间,两个旋钮必须拧对:时间延迟 τ 和嵌入维数 m。拧歪了重建出来是变形的轨迹,后面算 Lyapunov 指数或分形维数都会跟着错,外汇和贵金属这种高噪声品种尤其容易误判。 自相关函数(ACF)是最省事的 τ 取法:让 ACF 第一次掉到接近零或者初始值的 1/e 位置,此时相邻采样点算「足够独立」。下面这段 MT5 函数默认阈值 0.1,扫到绝对值 ≤0.1 的 lag 就返回。 ACF 的短板在它只看线性相关性。金融序列里那些非线性纠缠它视而不见,所以用在 XAUUSD 小时图上常常把延迟估短。互信息(MI)走信息论路线,取 MI 曲线的第一个局部极小值作 τ,能咬住非线性依赖——代价是计算重、要分箱建联合直方图。 实盘里建议两种都跑一遍对照,并且 τ 会随波动率状态漂移。EURUSD 在降息落地前后最优 lag 从 18 根变到 7 根并不罕见,定期重算比一次定死更稳。

MQL5 / C++
class="type">int FindOptimalLagACF(class="kw">const class="type">class="kw">double &price[], class="type">int maxLag, class="type">class="kw">double threshold = class="num">0.1)
{
   class="type">int size = ArraySize(price);
   if(size <= maxLag) class="kw">return class="num">1;

   class="type">class="kw">double mean = class="num">0;
   for(class="type">int i = class="num">0; i < size; i++)
      mean += price[i];
   mean /= size;

   class="type">class="kw">double variance = class="num">0;
   for(class="type">int i = class="num">0; i < size; i++)
      variance += MathPow(price[i] - mean, class="num">2);
   variance /= size;

   for(class="type">int lag = class="num">1; lag <= maxLag; lag++)
   {
      class="type">class="kw">double acf = class="num">0;
      for(class="type">int i = class="num">0; i < size - lag; i++)
         acf += (price[i] - mean) * (price[i + lag] - mean);
      acf /= (size - lag) * variance;

      if(MathAbs(acf) <= threshold)
         class="kw">return lag;
   }

   class="kw">return maxLag;
}
class="type">class="kw">double CalculateMutualInformation(class="kw">const class="type">class="kw">double &price[], class="type">int lag, class="type">int bins = class="num">20)
{
   class="type">int size = ArraySize(price);
   if(size <= lag) class="kw">return class="num">0;

   class="type">class="kw">double minPrice = price[ArrayMinimum(price)];
   class="type">class="kw">double maxPrice = price[ArrayMaximum(price)];
   class="type">class="kw">double binSize = (maxPrice - minPrice) / bins;

   class="type">int histogram[];
   ArrayResize(histogram, bins * bins);
   ArrayInitialize(histogram, class="num">0);

   class="type">int totalPoints = class="num">0;

   for(class="type">int i = class="num">0; i < size - lag; i++)
   {
      class="type">int bin1 = (class="type">int)((price[i] - minPrice) / binSize);
      class="type">int bin2 = (class="type">int)((price[i + lag] - minPrice) / binSize);
      if(bin1 >= class="num">0 && bin1 < bins && bin2 >= class="num">0 && bin2 < bins)
      {
         histogram[bin1 * bins + bin2]++;
         totalPoints++;
      }
   }

   class="type">class="kw">double mutualInfo = class="num">0;
   for(class="type">int i = class="num">0; i < bins; i++)
   {
      for(class="type">int j = class="num">0; j < bins; j++)
      {
         if(histogram[i * bins + j] > class="num">0)
         {
            class="type">class="kw">double pxy = (class="type">class="kw">double)histogram[i * bins + j] / totalPoints;

◍ 用互信息拐点反推最佳延迟

上面这段收尾代码把互信息(MI)计算落到了可执行的搜索逻辑:先对联合分布做边缘概率累加,再套 MathLog 算信息量,最后用 FindOptimalLagMI 在 1 到 maxLag 之间找使 MI 首次触底回升的延迟。 核心在 FindOptimalLagMI 的 break 条件——它不遍历完所有 lag,而是当 mi 大于已记录的最小 minMI 时立刻停。这意味着最优延迟倾向出现在序列从「越延越不相关」转为「开始重新相关」的拐点,而非盲目取最小值。 实盘里把 maxLag 设成 30~50 根 BAR 去跑,黄金 1H 数据常落在 lag=6~12 区间出现 MI 极小值;该结论仅基于历史分布特征,外汇与贵金属杠杆交易高风险,延迟参数需自行在 MT5 回测验证。

MQL5 / C++
class="type">class="kw">double px = class="num">0, py = class="num">0;
      for(class="type">int k = class="num">0; k < bins; k++)
      {
         px += (class="type">class="kw">double)histogram[i * bins + k] / totalPoints;
         py += (class="type">class="kw">double)histogram[k * bins + j] / totalPoints;
      }
      mutualInfo += pxy * MathLog(pxy / (px * py));
         }
      }
   }

   class="kw">return mutualInfo;
}
class="type">int FindOptimalLagMI(class="kw">const class="type">class="kw">double &price[], class="type">int maxLag)
{
   class="type">class="kw">double minMI = DBL_MAX;
   class="type">int optimalLag = class="num">1;

   for(class="type">int lag = class="num">1; lag <= maxLag; lag++)
   {
      class="type">class="kw">double mi = CalculateMutualInformation(price, lag);
      if(mi < minMI)
      {
         minMI = mi;
         optimalLag = lag;
      }
      else if(mi > minMI)
      {
         break;
      }
   }

   class="kw">return optimalLag;
}

常见问题

先对价格序列做相空间重构得到多点,再逐对计算欧氏距离,小于阈值就置 1 否则 0,拼成 N×N 的 0/1 矩阵即为递归图。
单变量价格序列用欧氏距离足够且快;若叠加成交量等多维数据,马氏距离能去量纲干扰,但计算更重,按硬件量力而行。
可以,小布能直接对打开的品种做相空间重构与递归诊断,波动率结构变化会主动标出,你只需看结论。
用互信息函数第一个明显下凹拐点定τ,用虚假最近邻比例跌破阈值定m,黄金类品种τ常落在 8~20 根 bar。
把价格序列按不同τ算互信息曲线,找首次陡降后走平的位置即可;贵金属高风险,验证前先用模拟盘跑一遍。