交易中的混沌理论(第二部分):深入探索·综合运用
🌀

交易中的混沌理论(第二部分):深入探索·综合运用

(3/3)· 从嵌入维数到自动优化,把前兩篇的混沌概念压进一套可跑的系统

新手友好 第 3/3 篇

很多交易者把混沌理论当成图一乐的科普,真到写 EA 时就退回均线交叉。前兩篇铺垫了李雅普诺夫指数和分形维数,这一篇直接把它们塞进 MQL5 指标和专家顾问,让混乱程度变成可量化的开仓过滤器。

用伪最近邻挑出相空间的最小维度

时间延迟定下来之后,相空间重建的下一个卡点是嵌入维数。伪最近邻(FNN)的思路很直接:不断加维度,直到相空间里本来挨得近的点在更高维里依然挨得近——一旦维度够用,几何结构就稳定了,再往上加只会徒增噪声。 下面这段 MQL5 实现了核心判定与搜索。IsFalseNeighbor 比较 dim 维与 dim+1 维下两点距离的相对跳变,超过 threshold 就判为假邻居;FindOptimalEmbeddingDimension 从 1 扫到 maxDim,逐点找最近邻并统计假邻居占比,低于 tolerance 就返回当前维。

MQL5 / C++
class="type">bool IsFalseNeighbor(class="kw">const class="type">class="kw">double &price[], class="type">int index1, class="type">int index2, class="type">int dim, class="type">int delay, class="type">class="kw">double threshold)
{
   class="type">class="kw">double dist1 = class="num">0, dist2 = class="num">0;
   for(class="type">int i = class="num">0; i < dim; i++)
   {
      class="type">class="kw">double diff = price[index1 - i * delay] - price[index2 - i * delay];
      dist1 += diff * diff;
   }
   dist1 = MathSqrt(dist1);

   class="type">class="kw">double diffNext = price[index1 - dim * delay] - price[index2 - dim * delay];
   dist2 = MathSqrt(dist1 * dist1 + diffNext * diffNext);

   class="kw">return (MathAbs(dist2 - dist1) / dist1 > threshold);
}
class="type">int FindOptimalEmbeddingDimension(class="kw">const class="type">class="kw">double &price[], class="type">int delay, class="type">int maxDim, class="type">class="kw">double threshold = class="num">0.1, class="type">class="kw">double tolerance = class="num">0.01)
{
   class="type">int size = ArraySize(price);
   class="type">int minRequiredSize = (maxDim - class="num">1) * delay + class="num">1;
   if(size < minRequiredSize) class="kw">return class="num">1;

   for(class="type">int dim = class="num">1; dim < maxDim; dim++)
   {
      class="type">int falseNeighbors = class="num">0;
      class="type">int totalNeighbors = class="num">0;

      for(class="type">int i = (dim + class="num">1) * delay; i < size; i++)
      {
         class="type">int nearestNeighbor = -class="num">1;
         class="type">class="kw">double minDist = DBL_MAX;

         for(class="type">int j = (dim + class="num">1) * delay; j < size; j++)
         {
            if(i == j) class="kw">continue;

            class="type">class="kw">double dist = class="num">0;
            for(class="type">int k = class="num">0; k < dim; k++)
            {
               class="type">class="kw">double diff = price[i - k * delay] - price[j - k * delay];
               dist += diff * diff;
            }

            if(dist < minDist)
            {
               minDist = dist;
               nearestNeighbor = j;
            }
         }

         if(nearestNeighbor != -class="num">1)
         {
            totalNeighbors++;
            if(IsFalseNeighbor(price, i, nearestNeighbor, dim, delay, threshold))
            {
               falseNeighbors++;
            }
         }
      }

      if(totalNeighbors > class="num">0 && (class="type">class="kw">double)falseNeighbors / totalNeighbors < tolerance)
      {
         class="kw">return dim + class="num">1;
      }
   }
   class="kw">return maxDim;
}
逐行拆一下关键处:IsFalseNeighbor 里 dist1 是 dim 维欧氏距离,diffNext 补上第 dim 维的差,dist2 用勾股定理合成高维距离;比值超 threshold 说明这点是因为维度不够才显得近,属假邻居。主函数里 minRequiredSize 检查样本够不够——(maxDim-1)*delay+1 是最低长度,不够直接返 1。 四个参数得手动调:delay 用前文 ACF 或 MI 的结果;maxDim 别太小,EURUSD 日线常要试到 10 以上;threshold 默认 0.1、tolerance 默认 0.01,但外汇贵金属波动有跳空,阈值放宽到 0.15 / 0.02 可能更稳。开 MT5 把这段贴进脚本,换不同品种跑一遍,看返回维数是否随样本区间漂移。

MQL5 / C++
class="type">bool IsFalseNeighbor(class="kw">const class="type">class="kw">double &price[], class="type">int index1, class="type">int index2, class="type">int dim, class="type">int delay, class="type">class="kw">double threshold)
{
   class="type">class="kw">double dist1 = class="num">0, dist2 = class="num">0;
   for(class="type">int i = class="num">0; i < dim; i++)
   {
      class="type">class="kw">double diff = price[index1 - i * delay] - price[index2 - i * delay];
      dist1 += diff * diff;
   }
   dist1 = MathSqrt(dist1);

   class="type">class="kw">double diffNext = price[index1 - dim * delay] - price[index2 - dim * delay];
   dist2 = MathSqrt(dist1 * dist1 + diffNext * diffNext);

   class="kw">return (MathAbs(dist2 - dist1) / dist1 > threshold);
}
class="type">int FindOptimalEmbeddingDimension(class="kw">const class="type">class="kw">double &price[], class="type">int delay, class="type">int maxDim, class="type">class="kw">double threshold = class="num">0.1, class="type">class="kw">double tolerance = class="num">0.01)
{
   class="type">int size = ArraySize(price);
   class="type">int minRequiredSize = (maxDim - class="num">1) * delay + class="num">1;
   if(size < minRequiredSize) class="kw">return class="num">1;

   for(class="type">int dim = class="num">1; dim < maxDim; dim++)
   {
      class="type">int falseNeighbors = class="num">0;
      class="type">int totalNeighbors = class="num">0;

      for(class="type">int i = (dim + class="num">1) * delay; i < size; i++)
      {
         class="type">int nearestNeighbor = -class="num">1;
         class="type">class="kw">double minDist = DBL_MAX;

         for(class="type">int j = (dim + class="num">1) * delay; j < size; j++)
         {
            if(i == j) class="kw">continue;

            class="type">class="kw">double dist = class="num">0;
            for(class="type">int k = class="num">0; k < dim; k++)
            {
               class="type">class="kw">double diff = price[i - k * delay] - price[j - k * delay];
               dist += diff * diff;
            }

            if(dist < minDist)
            {
               minDist = dist;
               nearestNeighbor = j;
            }
         }

         if(nearestNeighbor != -class="num">1)
         {
            totalNeighbors++;
            if(IsFalseNeighbor(price, i, nearestNeighbor, dim, delay, threshold))
            {
               falseNeighbors++;
            }
         }
      }

      if(totalNeighbors > class="num">0 && (class="type">class="kw">double)falseNeighbors / totalNeighbors < tolerance)
      {
         class="kw">return dim + class="num">1;
      }
   }
   class="kw">return maxDim;
}

「假近邻比例卡住嵌入维数」

上面这段收尾逻辑干的事很直接:把统计出的 falseNeighbors 转成比率,再和 tolerance 比对,决定是否采用当前 dim。fnnRatio 低于容差就认为该维度已足够刻画吸引子结构,直接 return dim 退出循环。 这里 totalNeighbors 是前面双层距离比较里累计的有效邻居总数,falseNeighbors 则是那些在抬升维度后‘掉队’的伪邻居计数。若遍历到 maxDim 仍没跌破 tolerance,函数退回到 maxDim,意味着序列在该上限内未收敛到稳定维数。 实盘验证时建议把 tolerance 设在 0.05~0.1 区间跑黄金 1 小时线:低于 0.05 容易过拟合噪声,高于 0.1 则可能漏掉真实混沌结构。外汇与贵金属杠杆高、滑点突变频繁,该维数仅作状态参考,不构成方向判定。

MQL5 / C++
      falseNeighbors++;
      }
    }
    
    class="type">class="kw">double fnnRatio = (class="type">class="kw">double)falseNeighbors / totalNeighbors;
    if(fnnRatio < tolerance)
      class="kw">return dim;
  }
  
  class="kw">return maxDim;
}

◍ MT5里用最近邻法做混沌预测

混沌预测的核心假设很直接:相空间里离得近的历史状态,未来一段的演化路径倾向相似。所以只要先重建好相空间,就能拿当前状态去翻历史,找k个最邻近点,用它们后续的实际走势反推当下可能的方向。 这套逻辑落到MT5指标上分三步:时间延迟法重建相空间、给当前状态抓k个最近邻、按邻居的未来值做加权预测。权重与距离成反比,离得越近话语权越大,近邻给出的路径通常更可信。 从实盘截图看,该指标在EURUSD这类品种上曾提前约10根K线显示出价格方向倾向,但外汇与贵金属属高风险市场,信号失效概率不低,务必先在历史数据回放里验证。 下面这段MQL5代码可直接编译:嵌入维度默认3、时滞5、邻居数10、预测步长10、回看1000根。改InpNeighbors或InpTimeDelay,就能肉眼比对红蓝线(预测vs实际)的贴合度。 代码逐行拆解: #property段声明版权与指标挂载主图、双缓冲(实际蓝线/预测红线); input段暴露5个可调参数,嵌入维、时滞、邻居数、预测步长、回看窗; OnInit里把两个数组绑成数据缓冲,短名设"Chaos Theory Predictor"; OnCalculate接收OHLC与成交量引用,准备逐根计算。

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_chart_window
class="macro">#class="kw">property indicator_buffers class="num">2
class="macro">#class="kw">property indicator_plots   class="num">2
class="macro">#class="kw">property indicator_label1  "Actual"
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_color1  clrBlue
class="macro">#class="kw">property indicator_label2  "Predicted"
class="macro">#class="kw">property indicator_type2   DRAW_LINE
class="macro">#class="kw">property indicator_color2  clrRed
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 ActualBuffer[];
class="type">class="kw">double PredictedBuffer[];
class="type">int OnInit()
{
   SetIndexBuffer(class="num">0, ActualBuffer, INDICATOR_DATA);
   SetIndexBuffer(class="num">1, PredictedBuffer, INDICATOR_DATA);
   
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
   IndicatorSetString(INDICATOR_SHORTNAME, "Chaos Theory Predictor");
   
   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 指标计算:先用嵌入维度和时间延迟把收盘价排成相空间向量,再在历史窗口里找距离最近的若干个邻居,按距离反比加权外推未来若干根 K 线的价位。外汇与贵金属市场高杠杆、跳空频繁,这类预测只反映历史形态的重现概率,不预示必然方向。 核心入口里 startprev_calculatedInpLookback + InpEmbeddingDimension * InpTimeDelay + InpForecastHorizon 的较大值,保证回溯窗口和预测步长不越界;循环里仅当 i 足够大且 i + InpForecastHorizon 未超出数组时才调用 PredictPrice,否则 ActualBuffer 照写现价而预测缓冲留空。 PredictPrice 先按 index - i * InpTimeDelay 抽取当前相点,任意索引为负立即返回 0 防越界;随后对 dataSize 个历史相点算欧氏距离并携索引排序。取前 InpNeighbors 个邻居,权重 1.0/(dist+0.0001) 避免除零,预测值 = 邻居在 +InpForecastHorizon 处的加权均价。把 InpNeighbors 从默认调小到 3~5,在 EURUSD 的 M15 上往往比 10 邻居更跟得上拐点,但过拟合风险同步上升。 排序用冒泡实现 SortDistancesWithIndices,距离与索引数组同步交换;若你实盘品种点值极小,可把 0.0001 改成 1e-8 以削弱远距离邻居的噪声贡献,改完直接编译挂图表看 PredictedBuffer 与实线贴合度。

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++)
  {
    ActualBuffer[i] = close[i];
    if (i >= InpEmbeddingDimension * InpTimeDelay && i + InpForecastHorizon < rates_total)
    {
      PredictedBuffer[i] = PredictPrice(close, i);
    }
  }
  
  class="kw">return(rates_total);
}

class="type">class="kw">double PredictPrice(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[];
  class="type">int indices[];
  ArrayResize(distances, dataSize);
  ArrayResize(indices, dataSize);
  for(class="type">int i = class="num">0; i < dataSize; i++)
  {
    class="type">class="kw">double dist = 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];
      dist += diff * diff;
    }
    distances[i] = MathSqrt(dist);
    indices[i] = i;
  }
  class=class="str">"cmt">// Custom sort function for sorting distances and indices together
  SortDistancesWithIndices(distances, indices, dataSize);
  class="type">class="kw">double prediction = class="num">0;
  class="type">class="kw">double weightSum = class="num">0;
  for(class="type">int i = class="num">0; i < InpNeighbors; i++)
  {
    class="type">int neighborIndex = index - indices[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 weight = class="num">1.0 / (distances[i] + class="num">0.0001);  class=class="str">"cmt">// Avoid division by zero
    prediction += weight * price[neighborIndex + InpForecastHorizon];
    weightSum += weight;
  }
  class="kw">return prediction / weightSum;
}

class="type">void SortDistancesWithIndices(class="type">class="kw">double &distances[], class="type">int &indices[], class="type">int size)
{
  for(class="type">int i = class="num">0; i < size - class="num">1; i++)
  {
    for(class="type">int j = i + class="num">1; j < size; j++)
    {
      if(distances[i] > distances[j])
      {
        class="type">class="kw">double tempDist = distances[i];
        distances[i] = distances[j];

「距离数组与索引的同步交换」

在 K 近邻筛选的嵌套循环里,当发现更小距离 tempDist 时,不仅要更新 distances[j],还要同步交换 indices 数组里的对应元素。 上面这段代码做了两件事:先把 tempDist 写回 distances[j],随后用临时变量 tempIndex 把 indices[i] 与 indices[j] 互换。若只改距离不换索引,最终取前 K 个 indices 时会拿到错误样本的位置。 开 MT5 把这段塞进你的 KNN 函数里,跑一组 500 根 H1 收盘价,打印前 5 个 indices,确认它们对应的 distances 确实是升序的最小五值。外汇与贵金属波动剧烈,这类向量化查找误差会直接放大信号噪声,属高风险操作。

MQL5 / C++
            distances[j] = tempDist;
            class="type">int tempIndex = indices[i];
            indices[i] = indices[j];
            indices[j] = tempIndex;
         }
      }
   }
}

◍ 把混沌相空间塞进EA里跑

这套EA把相空间重构和最近邻预测直接做成了MT5自动交易逻辑,核心不是指标叠加,而是用历史轨迹的几何结构去推下一步价格。默认参数里嵌入维度3、时间延迟5、邻居数10、预测步长10、回看1000根K线、固定0.1手,这几个值你开MT5后第一件事就该挨个改着测。 每根新K线触发时,EA先跑ACF自相关和FNN伪最近邻算法,把optimalTimeDelay和optimalEmbeddingDimension当场算出来,而不是死用输入值。接着在重建好的相空间里,拿当前状态跟回看期内所有过去状态算距离,取最近的10个邻居,用距离反比加权算出它们未来价格的平均,作为PredictPrice的输出。 交易动作很粗暴:预测价高于现价就平掉所有卖单开买单,低于现价就平掉所有买单开卖单。外汇和贵金属杠杆高、滑点乱,这种反转式全仓切换在实盘里可能频繁挨耳光,先用策略测试器跑几年Tick再看胜率。 代码里OnTick开头用g_last_bar_time拦住同根K线重复计算,只在新柱处理一次;PositionClose失败也没重试,真要上模拟盘得自己补容错。下面这段是原文EA骨架,逐行拆完你能直接抄去改。 #property copyright "Copyright 2024, Author" // 版权声明,编译用元数据,无实际逻辑 #property link "https://www.example.com" // 作者链接备注 #property version "1.00" // EA版本号 #property strict // 开启严格编译模式,类型检查更狠 #include <Arrays\ArrayObj.mqh> // 引入对象数组库 #include <Trade\Trade.mqh> // 引入交易类库 CTrade Trade; // 实例化交易对象 input int InpEmbeddingDimension = 3; // 相空间嵌入维度,默认3 input int InpTimeDelay = 5; // 重建时间延迟,默认5 input int InpNeighbors = 10; // 最近邻数量,默认10 input int InpForecastHorizon = 10; // 预测步长,默认10 input int InpLookback = 1000; // 回看K线数,默认1000 input double InpLotSize = 0.1; // 开仓手数,默认0.1 ulong g_ticket = 0; // 全局订单票号占位 datetime g_last_bar_time = 0; // 记录上一根K线时间,防重算 double optimalTimeDelay; // 优化后的时间延迟 double optimalEmbeddingDimension; // 优化后的嵌入维度 int OnInit() { return(INIT_SUCCEEDED); } // 初始化函数,这里空跑返回成功 void OnDeinit(const int reason) { } // 退出函数,未做资源清理 void OnTick() { OptimizeParameters(); // 每tick先优化延迟和维度参数 if(g_last_bar_time == iTime(_Symbol, PERIOD_CURRENT, 0)) return; // 同一根K线就直接退出 g_last_bar_time = iTime(_Symbol, PERIOD_CURRENT, 0); // 更新最后处理时间 double prediction = PredictPrice(iClose(_Symbol, PERIOD_CURRENT, 0), 0); // 用当前收盘价预测 Comment(prediction); // 右下角显示预测值 if(prediction > iClose(_Symbol, PERIOD_CURRENT, 0)) // 预测高于现价看多 { // Close selling for(int i = PositionsTotal() - 1; i >= 0; i--) // 倒序遍历所有持仓 { if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) // 找本品种卖单 { ulong ticket = PositionGetInteger(POSITION_TICKET); // 取ticket if(!Trade.PositionClose(ticket)) // 尝试平仓,失败无处理

MQL5 / C++
class="macro">#class="kw">property copyright "Copyright class="num">2024, Author"
class="macro">#class="kw">property link      "https:class=class="str">"cmt">//www.example.com"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property strict
class="macro">#include <Arrays\ArrayObj.mqh>
class="macro">#include <Trade\Trade.mqh>
CTrade Trade;
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="kw">input class="type">class="kw">double  InpLotSize            = class="num">0.1;   class=class="str">"cmt">// Lot size
class="type">class="kw">ulong g_ticket = class="num">0;
class="type">class="kw">datetime g_last_bar_time = class="num">0;
class="type">class="kw">double optimalTimeDelay;
class="type">class="kw">double optimalEmbeddingDimension;
class="type">int OnInit()
{
   class="kw">return(INIT_SUCCEEDED);
}
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
}
class="type">void OnTick()
{  
   OptimizeParameters();
   if(g_last_bar_time == iTime(_Symbol, PERIOD_CURRENT, class="num">0)) class="kw">return;
   g_last_bar_time = iTime(_Symbol, PERIOD_CURRENT, class="num">0);
   class="type">class="kw">double prediction = PredictPrice(iClose(_Symbol, PERIOD_CURRENT, class="num">0), class="num">0);
   Comment(prediction);
   if(prediction > iClose(_Symbol, PERIOD_CURRENT, class="num">0))
   {
      class=class="str">"cmt">// Close selling
      for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--)
      {
         if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
         {
            class="type">class="kw">ulong ticket = PositionGetInteger(POSITION_TICKET);
            if(!Trade.PositionClose(ticket))

混沌预测下的反向平仓与开仓分支

当模型预测价高于当前收盘价时,EA 先逆向清掉已有 SELL 单,再按市价 ask 开立 BUY 单,注释标记 // Open buy 后直接取 SymbolInfoDouble(_Symbol, SYMBOL_ASK) 作为进场价。若 Trade.Buy 返回票号为 0,说明下单失败,立刻 PrintGetLastError 代码供排查。 反之若预测价低于收盘价,逻辑镜像处理:遍历持仓用 PositionGetInteger(POSITION_TYPE) 锁定 BUY 单并 Trade.PositionClose 平仓,随后以 bid 价 Trade.Sell 开空,魔数字符串写死为 ChaosSell。这两段平仓循环都从 PositionsTotal()-1 倒序遍历,避免删除元素时索引错位。 PredictPrice 函数把相空间重构落到了代码层:vectorSizeoptimalEmbeddingDimensiondataSizeInpLookback,用 index + i * optimalTimeDelay 抽取延迟坐标。距离计算走平方再 MathSqrt 的欧氏路数,结果存进 distances[] 并保留 indices[] 原始序,下一步就是按距离排序挑近邻——外汇与贵金属杠杆高,这类信号在滑点扩大时可能频繁假突破,上 MT5 用策略测试器跑 EURUSD 15M 能直接看成交率。

MQL5 / C++
        Print("Failed to close SELL position: ", GetLastError());
      }
   }
   
   class=class="str">"cmt">// Open buy
   class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   class="type">class="kw">ulong ticket = Trade.Buy(InpLotSize, _Symbol, ask, class="num">0, class="num">0, "ChaosBuy");
   if(ticket == class="num">0)
      Print("Failed to open BUY position: ", GetLastError());
   }
   else if(prediction < iClose(_Symbol, PERIOD_CURRENT, class="num">0))
   {
      class=class="str">"cmt">// Close buying
      for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--)
      {
         if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
         {
            class="type">class="kw">ulong ticket = PositionGetInteger(POSITION_TICKET);
            if(!Trade.PositionClose(ticket))
               Print("Failed to close BUY position: ", GetLastError());
         }
      }
      
      class=class="str">"cmt">// Open sell
      class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      class="type">class="kw">ulong ticket = Trade.Sell(InpLotSize, _Symbol, bid, class="num">0, class="num">0, "ChaosSell");
      if(ticket == class="num">0)
         Print("Failed to open SELL position: ", GetLastError());
   }
}
class="type">class="kw">double PredictPrice(class="type">class="kw">double price, class="type">int index)
{
   class="type">int vectorSize = optimalEmbeddingDimension;
   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++)
   {
      currentVector[i] = iClose(_Symbol, PERIOD_CURRENT, index + i * optimalTimeDelay);
   }
   
   class="type">class="kw">double distances[];
   class="type">int indices[];
   ArrayResize(distances, dataSize);
   ArrayResize(indices, dataSize);
   
   for(class="type">int i = class="num">0; i < dataSize; i++)
   {
      class="type">class="kw">double dist = class="num">0;
      for(class="type">int j = class="num">0; j < vectorSize; j++)
      {
         class="type">class="kw">double diff = currentVector[j] - iClose(_Symbol, PERIOD_CURRENT, index + i + j * optimalTimeDelay);
         dist += diff * diff;
      }
      distances[i] = MathSqrt(dist);
      indices[i] = i;
   }
   
   class=class="str">"cmt">// Use SortDoubleArray to sort by &class="macro">#x27;distances&class="macro">#x27; array values

「KNN预测与自相关滞后的代码实现」

下面这段 KNN 预测收尾逻辑,核心是用距离倒数做加权:越近的邻居权重越高,最后除以权重和得到预测价。注意权重里 distances[i]+0.0001 的偏置,避免距离为 0 时除零导致预测爆掉。 SortDoubleArray 是个冒泡排序,把距离数组升序排,同时把 indices 同步交换,保证排完序后 indices 仍指向原序列位置。复杂度是 O(n²),样本量 InpLookback 设到 500 以上时回测会明显变慢,建议实盘前先用历史数据压测。 FindOptimalLagACF 用自相关函数找序列「失忆点」:从 lag=1 开始算 ACF,一旦绝对值 ≤ threshold(默认 0.1)就返回该 lag。若 maxLag 内都没跌破阈值,则返回 maxLag,意味着序列在该窗口内仍有记忆。 外汇与贵金属杠杆高、滑点跳空频繁,这类基于历史距离的模型在重大数据行情中可能完全失效,信号仅作概率参考。

MQL5 / C++
  SortDoubleArray(distances, indices);

  class="type">class="kw">double prediction = class="num">0;
  class="type">class="kw">double weightSum = class="num">0;

  for(class="type">int i = class="num">0; i < InpNeighbors; i++)
  {
    class="type">int neighborIndex = index + indices[i];
    class="type">class="kw">double weight = class="num">1.0 / (distances[i] + class="num">0.0001);
    prediction += weight * iClose(_Symbol, PERIOD_CURRENT, neighborIndex + InpForecastHorizon);
    weightSum += weight;
  }

  class="kw">return prediction / weightSum;
}
class="type">void SortDoubleArray(class="type">class="kw">double &distances[], class="type">int &indices[])
{
    class="type">int size = ArraySize(distances);
    for(class="type">int i = class="num">0; i < size - class="num">1; i++)
    {
        for(class="type">int j = i + class="num">1; j < size; j++)
        {
            if(distances[i] > distances[j])
            {
                class=class="str">"cmt">// Swap distances
                class="type">class="kw">double tempDist = distances[i];
                distances[i] = distances[j];
                distances[j] = tempDist;
                
                class=class="str">"cmt">// Swap corresponding indices
                class="type">int tempIndex = indices[i];
                indices[i] = indices[j];
                indices[j] = tempIndex;
            }
        }
    }
}
class="type">int FindOptimalLagACF(class="type">int maxLag, class="type">class="kw">double threshold = class="num">0.1)
{
  class="type">int size = InpLookback;
  class="type">class="kw">double series[];
  ArraySetAsSeries(series, true);
  CopyClose(_Symbol, PERIOD_CURRENT, class="num">0, size, series);

  class="type">class="kw">double mean = class="num">0;
  for(class="type">int i = class="num">0; i < size; i++)
    mean += series[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(series[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 += (series[i] - mean) * (series[i + lag] - mean);
    acf /= (size - lag) * variance;
    
    if(MathAbs(acf) <= threshold)
      class="kw">return lag;
  }

  class="kw">return maxLag;
}
class="type">int FindOptimalEmbeddingDimension(class="type">int delay, class="type">int maxDim, class="type">class="kw">double threshold = class="num">0.1, class="type">class="kw">double tolerance = class="num">0.01)
{
  class="type">int size = InpLookback;
  class="type">class="kw">double series[];

◍ 用假近邻法卡死嵌入维度

相空间重构里,嵌入维数给低了会把本来分开的轨迹揉成一团,给高了又平白吃掉算力。这段代码用假近邻率(FNN)在 1 到 maxDim 之间反推一个够用的维数:当某维数下假近邻占比低于 tolerance,就认定维度已饱和直接返回。 核心循环先拿 ArraySetAsSeries 把收盘价序列倒序,再用 CopyClose 抓当前品种当前周期最近的 size 根 K 线。外层 dim 从 1 跑起,内层双重循环对每个点找同维欧氏距离最近的邻居,一旦 IsFalseNeighbor 判定跨维后距离突跳超过 threshold,就记一次假近邻。 IsFalseNeighbor 只比 dim 维与 dim+1 维的距离差:若 (dist2-dist1)/dist1 大于阈值,说明这俩在更高维被撕开,是投影假象。实盘里 threshold 取 10 到 15 往往更稳,tolerance 设 0.05 以下才肯罢手。 OptimizeParameters 把时间延迟交给自相关函数(最多看 50 滞后期),嵌入维上限压到 10,最后 Print 出两个数。外汇与贵金属波动具有高杠杆与跳空风险,这套值只是重构输入,不代表任何方向胜率,开 MT5 把 delay 和 tolerance 调两轮再信。

MQL5 / C++
  ArraySetAsSeries(series, true);
  CopyClose(_Symbol, PERIOD_CURRENT, class="num">0, size, series);
  
  for(class="type">int dim = class="num">1; dim < maxDim; dim++)
  {
      class="type">int falseNeighbors = class="num">0;
      class="type">int totalNeighbors = class="num">0;
      
      for(class="type">int i = (dim + class="num">1) * delay; i < size; i++)
      {
         class="type">int nearestNeighbor = -class="num">1;
         class="type">class="kw">double minDist = DBL_MAX;
         
         for(class="type">int j = (dim + class="num">1) * delay; j < size; j++)
         {
            if(i == j) class="kw">continue;
            
            class="type">class="kw">double dist = class="num">0;
            for(class="type">int k = class="num">0; k < dim; k++)
            {
               class="type">class="kw">double diff = series[i - k * delay] - series[j - k * delay];
               dist += diff * diff;
            }
            
            if(dist < minDist)
            {
               minDist = dist;
               nearestNeighbor = j;
            }
         }
         
         if(nearestNeighbor != -class="num">1)
         {
            totalNeighbors++;
            if(IsFalseNeighbor(series, i, nearestNeighbor, dim, delay, threshold))
               falseNeighbors++;
         }
      }
      
      class="type">class="kw">double fnnRatio = (class="type">class="kw">double)falseNeighbors / totalNeighbors;
      if(fnnRatio < tolerance)
         class="kw">return dim;
  }
  
  class="kw">return maxDim;
}
class="type">bool IsFalseNeighbor(class="kw">const class="type">class="kw">double &price[], class="type">int index1, class="type">int index2, class="type">int dim, class="type">int delay, class="type">class="kw">double threshold)
{
  class="type">class="kw">double dist1 = class="num">0, dist2 = class="num">0;
  for(class="type">int i = class="num">0; i < dim; i++)
  {
      class="type">class="kw">double diff = price[index1 - i * delay] - price[index2 - i * delay];
      dist1 += diff * diff;
  }
  dist1 = MathSqrt(dist1);
  
  class="type">class="kw">double diffNext = price[index1 - dim * delay] - price[index2 - dim * delay];
  dist2 = MathSqrt(dist1 * dist1 + diffNext * diffNext);
  
  class="kw">return (MathAbs(dist2 - dist1) / dist1 > threshold);
}
class="type">void OptimizeParameters()
{
  class="type">class="kw">double optimalTimeDelay = FindOptimalLagACF(class="num">50);
  class="type">class="kw">double optimalEmbeddingDimension = FindOptimalEmbeddingDimension(optimalTimeDelay, class="num">10);
  Print("Optimal Time Delay: ", optimalTimeDelay);

把最优嵌入维数打印到日志

在相空间重构里,嵌入维数选错会让吸引子折叠或冗余,MQL5 里算完最优值后必须落地确认。 上面这行把 optimalEmbeddingDimension 直接打到 Experts 日志,开 MT5 跑完脚本第一件事就是看这个数,别盲信默认参数。 外汇与贵金属波动高、跳空多,同一品种不同周期算出的维数可能差 2~3 维,建议每个周期单独验证。

MQL5 / C++
  Print("Optimal Embedding Dimension: ", optimalEmbeddingDimension);
}

「跨品种自动优化实测」

把同一套 EA 丢进 MT5 的自动优化器,分别跑 EURUSD、AUD、GBPUSD 三个品种,起点统一取 2016 年 1 月 1 日。这种批量回测能快速暴露参数在某类波动率结构下的脆弱点,而不是只盯一个盘面自我安慰。 EURUSD 从 2016 年初开始的优化结果,倾向显示欧美在窄幅震荡段对止损参数最敏感;AUD 系品种因商品属性叠加跳空,参数平原(flat plateau)往往更宽,过拟合概率更高。 GBPUSD 在脱欧后波动率阶跃的区间内,优化器给出的夏普倾向低于前两者,但回撤分布更肥尾。外汇与贵金属均为高风险品种,跨品种优化结论仅代表历史样本,实盘前务必用 MT5 的自定义品种或 tick 级数据重跑一遍。

◍ 把混沌 EA 推向量产前的两道关

混沌理论驱动的 EA 若想从 demo 走向实盘,下一步绕不开大规模跨周期、跨品种回测。只在 EURUSD 的 H1 上跑出漂亮曲线远远不够,你需要把测试铺到 M15、H4 乃至 XAUUSD 这类高波动标的,观察其在趋势市与震荡市里的效率衰减边界。 引入机器学习做参数寻优是可行路径,但别指望一键自动适应。用 MQL5 的 CTrade 配合自定义特征工程,把波动率分位、混沌指数作为输入,让优化器在样本外数据上挑参数,才可能提升对突变行情的韧性。 风险端必须补动态仓位。静态手数在乱流里会放大回撤,按当前 ATR 与混沌波动预测值反比缩放头寸,公式可参考 lot = RiskPercent * AccountEquity() / (ATR_Multiplier * SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE))。外汇与贵金属杠杆高,这类动态头寸管理只降低概率性风险,不消除爆仓可能。

混沌不是圣杯,但值得继续挖

前面几节把相空间重构、嵌入维数、时间延迟和最近邻预测都拆过了,落到实盘,这套 EA 在多个货币对上跑出了正收益,但不同品种差异明显,有的年份夏普好看,有的直接回撤吃瘪。 金融市场本身是高维受扰系统,模型外因子(宏观突发、流动性断裂)根本塞不进相空间,长期预测在混沌框架下天然失效,这是硬约束不是调参能救的。 把混沌方法和机器学习、大数据叠一层,可能比单用最近邻更有戏,但外汇和贵金属杠杆高、跳空频繁,任何算法都只是概率优势,实盘前请用 MT5 策略测试器拿真实点差回测验证。

把重复诊断交给小布盯盘
这些分形维数与伪最近邻的繁琐计算,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时混乱度读数,你只管判断要不要顺趋势下手。

常见问题

维数偏低说明重构相空间失真,混沌特征可能被平滑掉,策略容易把噪声当信号,实盘胜率倾向下降。
取决于格子层级和样本长度,H1 以上周期通常可接受,秒级刷新的小周期建议降采样以免占用主线程。
不一定,高维数多对应波动放大,趋势可能更猛但回撤也深,概率上更该缩手或拉宽止损。
目前内置的是混乱度与分形结构诊断,不直接下单;把它当过滤器,比自己手算盒计数省下大量盯盘时间。
看样本外与样本内曲线背离度,若优化区间漂亮、实跑就衰,大概率是拟合了噪声,外汇贵金属高风险下尤需警惕。