交易中的混沌理论(第二部分):深入探索·进阶篇
递归图指标的核心计算骨架
把递归量化(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,防止嵌入维度展开时读到数组外。这一层不写稳,回测到行情头尾就爆数组越界错误。
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 里用历史数据反推一个分位值再固定。 这套计算对外汇/贵金属属于高风险研判辅助,距离近只代表历史形态概率接近,不预示后续必走同方向。
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 标准报价数组,后面该写邻域搜索与均值逻辑(原文截断)。
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 数据验证分布。
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 根并不罕见,定期重算比一次定死更稳。
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 回测验证。
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; }