交易中的混沌理论(第二部分):深入探索·综合运用
(3/3)· 从嵌入维数到自动优化,把前兩篇的混沌概念压进一套可跑的系统
很多交易者把混沌理论当成图一乐的科普,真到写 EA 时就退回均线交叉。前兩篇铺垫了李雅普诺夫指数和分形维数,这一篇直接把它们塞进 MQL5 指标和专家顾问,让混乱程度变成可量化的开仓过滤器。
用伪最近邻挑出相空间的最小维度
时间延迟定下来之后,相空间重建的下一个卡点是嵌入维数。伪最近邻(FNN)的思路很直接:不断加维度,直到相空间里本来挨得近的点在更高维里依然挨得近——一旦维度够用,几何结构就稳定了,再往上加只会徒增噪声。 下面这段 MQL5 实现了核心判定与搜索。IsFalseNeighbor 比较 dim 维与 dim+1 维下两点距离的相对跳变,超过 threshold 就判为假邻居;FindOptimalEmbeddingDimension 从 1 扫到 maxDim,逐点找最近邻并统计假邻居占比,低于 tolerance 就返回当前维。
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; }
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 则可能漏掉真实混沌结构。外汇与贵金属杠杆高、滑点突变频繁,该维数仅作状态参考,不构成方向判定。
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与成交量引用,准备逐根计算。
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 线的价位。外汇与贵金属市场高杠杆、跳空频繁,这类预测只反映历史形态的重现概率,不预示必然方向。
核心入口里 start 取 prev_calculated 与 InpLookback + 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 与实线贴合度。
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 确实是升序的最小五值。外汇与贵金属波动剧烈,这类向量化查找误差会直接放大信号噪声,属高风险操作。
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)) // 尝试平仓,失败无处理
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,说明下单失败,立刻 Print 出 GetLastError 代码供排查。
反之若预测价低于收盘价,逻辑镜像处理:遍历持仓用 PositionGetInteger(POSITION_TYPE) 锁定 BUY 单并 Trade.PositionClose 平仓,随后以 bid 价 Trade.Sell 开空,魔数字符串写死为 ChaosSell。这两段平仓循环都从 PositionsTotal()-1 倒序遍历,避免删除元素时索引错位。
PredictPrice 函数把相空间重构落到了代码层:vectorSize 取 optimalEmbeddingDimension,dataSize 取 InpLookback,用 index + i * optimalTimeDelay 抽取延迟坐标。距离计算走平方再 MathSqrt 的欧氏路数,结果存进 distances[] 并保留 indices[] 原始序,下一步就是按距离排序挑近邻——外汇与贵金属杠杆高,这类信号在滑点扩大时可能频繁假突破,上 MT5 用策略测试器跑 EURUSD 15M 能直接看成交率。
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,意味着序列在该窗口内仍有记忆。 外汇与贵金属杠杆高、滑点跳空频繁,这类基于历史距离的模型在重大数据行情中可能完全失效,信号仅作概率参考。
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 调两轮再信。
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 维,建议每个周期单独验证。
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 策略测试器拿真实点差回测验证。