神经网络在交易中的应用:将混沌理论融入时间序列预测(Attraos)
概述
金融市场时间序列代表价格数据、交易量和其他经济指标的序列,这些序列在众多因素的影响下随时间演变。它们反映了复杂的动态过程,包括市场趋势、周期以及由经济新闻、参与者行为和宏观经济状况驱动的短期波动。对金融时间序列的准确预测对于风险管理、交易策略制定、投资组合优化以及算法交易至关重要。预测错误可能导致重大财务损失,因此开发更精确的时间序列分析方法成为分析师、交易员和金融机构的首要任务。
现代金融时间序列预测方法广泛采用机器学习技术,包括神经网络和深度学习模型。然而,大多数传统方法都基于统计技术和线性模型,在分析金融市场特有的高度波动和混沌数据时,这些方法显得力不从心。市场过程往往表现出非线性依赖性、对初始条件的敏感性以及复杂的动态性,这使得预测成为一项具有挑战性的任务。传统模型也难以对突发市场事件进行建模,如危机、流动性骤变或投资者恐慌引发的大规模资产抛售。因此,开发能够适应金融市场复杂动态的方法是一个至关重要的研究方向。
为了应对这些挑战,Attraos 框架的作者在“长期时间序列预测的吸引子记忆:混沌视角”一文中,融合了混沌理论的原则,将时间序列视为多维混沌动力系统的低维投影。这种方法能够捕捉到市场变量之间隐藏的非线性依赖关系,从而提高预测的准确性。在时间序列分析中应用混沌动力学方法,可以识别市场数据中的持久结构,并将其纳入预测模型。
Attraos 框架解决了两个关键问题。首先,它使用相空间重构方法对隐藏的动态过程进行建模。这使得它能够识别潜在的模式,并考虑市场变量之间的非线性相互作用,如资产、宏观经济指标和市场流动性之间的相关性。其次,Attraos 采用频域局部演化的策略,能够适应不断变化的市场条件,并增强吸引子的差异化。与基于数据分布固定假设的传统模型不同,Attraos 能够动态适应不断变化的市场结构,从而在各种时间范围内提供更准确的预测。
此外,该模型还包含一个多分辨率动态记忆模块,使其能够在适应不断变化的市场条件的同时,记住并融入历史价格变动模式。这在金融市场中尤为重要,因为同样的模式可能会在不同的时间间隔内重复出现,但幅度和强度会有所不同。该模型能够从多个粒度级别的数据中学习,相较于传统方法具有显著优势,因为传统方法往往忽视了市场过程的多尺度特性。
框架作者所进行的实验表明,Attraos 的性能优于传统的时间序列预测方法,在可训练参数更少的情况下提供了更准确的预测。值得注意的是,利用混沌动力学使模型能够更好地捕捉长期依赖性并减轻累积误差的影响,这对于中长期投资策略至关重要。
Attraos 算法
在混沌理论中,随时间演变的多维动态系统可能表现出看似随机且复杂的行为。然而,详细的分析揭示了支配其动态的隐藏规律。
根据塔肯斯定理,如果一个系统是确定性的,那么其相空间可以通过对单个变量的观测来重构。该定理基于这样一个理念:只要有足够长的时间序列,即使只有一维数据可用,也能恢复系统的多维轨迹。这种方法能够识别出隐藏吸引子,这些吸引子代表相空间中的区域,系统轨迹会随着时间的推移而收敛到这些区域。
在金融市场中,股票价格、货币对和其他资产数据等时间序列包含着市场体系内部动态的信息。尽管外部环境看似混乱,市场行为可能遵循确定性规律,这些规律可以通过相空间重构方法揭示出来。应用这些方法,分析师能够识别出特征吸引子,这些吸引子可以作为预测未来价格变动的基础。
Attraos 框架基于混沌理论和相空间重构原理构建,能够在无需预先了解动力系统底层结构的情况下,创建其拓扑等效模型。核心思想是在多维相空间中表示时间序列,以揭示隐藏的模式。这是通过选择关键参数来实现的 — 嵌入维度 d 和时间延迟 τ,它们构成了时间序列的多维表示:
![]()
这种方法可以减少噪声影响,突出过程的结构要素,并提高预测准确性。相空间重构能够构建一个稳定的相图,反映系统随时间变化的动态特性,从而便于分析和建模。
Attraos 中的数据处理从相空间重构( PSR ) 模块开始,该模块负责选择最佳的 d 和 τ 值来正确表示系统动态。这一步骤至关重要,因为参数选择不当会严重扭曲重建的相图。随后,多维数据管理单元(MDMU)使用多维张量将数据划分为非重叠段。这降低了计算复杂度,加速了模型收敛,并通过捕捉关键的进化模式形成了系统的动态记忆。因此,预测结果在面对潜在的数据退化时,会变得更加稳定和稳健。MDMU 还实现了对重要时间序列分量的自适应选择,动态排除无信息量的元素,并强调对系统演化影响最大的关键因素。
Attraos 架构的一个关键要素是线性矩阵近似(LMA)模块,该模块实现了线性矩阵近似方法。它采用多项式投影来提取系统动力学的主要特征,并使用参数化的对角矩阵来定义“测量窗口”。这使得能够随着输入数据的变化,精确提取局部动态特性并进行自适应模型修正。使用对角矩阵可以降低计算复杂度,从而能够高效地处理多维数据结构。本模块中的系统演化形式化为:
![]()
其中 M 是参数化的对角状态矩阵, ϵ 表示随机建模误差。采用包括自适应非线性投影在内的各种进化变体,以捕捉复杂系统的变化并提高预测准确性。
在处理动态过程时,离散投影(DP)模块采用组合方法对系统进行离散化处理。具体而言,采用了指数矩阵表示法,从而在序列数据分析中提供了较高的精度。这种方法确保了对系统演化的正确近似,并减轻了累积误差的影响,这对于长时间序列的分析尤为重要。该模块还采用了自适应数据量化技术,在不显著增加计算开销的情况下优化了近似精度。
Attraos 中嵌入的其他自适应机制包括一种具有频率放大的局部演化策略,该策略调节时间序列的频谱特性,并补偿由随机过程引起的失真。这是通过高频分量滤波和数据谱密度控制来实现的。还实现了动态结构的多层次表示,其中分析窗口逐渐扩大,从而减少了投影误差,提高了近似精度。这使得该框架能够捕捉不同尺度下吸引子的复杂拓扑结构,这对于复杂动态系统至关重要。混合学习方法将经典数值技术与现代机器学习算法相结合,进一步增强了模型的适应性和泛化能力。
通过误差最小化和吸引子偏差控制来确保 Attraos 的稳定性。计算吸引子距离,监测其变化,并在检测到显著偏差时对系统轨迹进行校正。这些措施能够稳定预测结果,并在动态不稳定的情况下保持模型的高准确性。统计监测方法能自动识别不稳定区域,并实时调整模型。主动参数控制机制根据系统状态动态调整优化参数,从而进一步提高适应性和预测准确性。
下面提供了 Attraos 框架的原始可视化图。

使用 MQL5 实现
在讨论了Attraos框架的理论方面之后,我们现在转向文章的实践部分,其中我们使用MQL5实现了我们对所提出方法的解释。
我们首先准备 Attraos 算法有效运行所需的关键流程。该算法高度依赖于对角矩阵,这些矩阵在计算中发挥着关键作用,能够简化数据处理并减少内存使用。
对角矩阵是一种方阵,其中除主对角线上的元素外,其余元素均为零。在传统线性代数中,此类矩阵以 n*n 二维数组的形式存储。然而,这种做法效率低下,因为绝大多数元素都是零。一个更优的解决方案是,仅将非零对角元素作为长度为 n 的一维数组进行存储。这显著减少了内存使用,并通过避免对零元素进行操作来加速计算。
使用对角矩阵的向量表示需要对线性代数运算采用专门的算法,尤其是对角矩阵与任意矩阵的乘法运算。标准矩阵乘法算法假定所有元素都显式存储,这会导致不必要的计算。在这种情况下,只需将该对角向量中的每个元素乘以另一个矩阵中对应的行即可。这简化了算法,提高了效率。
为了最大限度地提高性能,该过程是在 OpenCL 环境下实现的,利用了 GPU 的并行计算能力。核心思想是,结果矩阵的每个元素都是独立计算的,仅使用相关的对角向量元素。这降低了计算复杂度,加快了执行速度。
对角矩阵乘法
乘法算法在 DiagMatMult 内核中实现,在三维任务空间中运行。前两个维度对应于第二个矩阵的维度,而第三个维度则反映了用于处理来自所分析多维时间序列的单位序列投影的独立矩阵的数量。
如上所述,将一个对角矩阵的向量表示与任意矩阵相乘的运算,涉及将该对角向量的每个元素与第二个矩阵的对应行相乘。为了优化 GPU 执行,我们将计算线程分组为工作组。在每个工作组中,只有一个线程访问全局内存以获取所需的对角向量元素,并将其存储在本地内存中。剩余线程使用本地内存中的这个值来执行必要的计算,而无需额外的全局内存访问。这显著提高了算法执行速度,并减少了全局内存与处理单元之间数据传输的开销。
DiagMatMult 内核接收指向三个数据缓冲区的指针作为参数。其中两个存储输入数据,第三个存储运算结果。我们还提供了一个选项,用于对乘法结果应用激活函数。
__kernel void DiagMatMult(__global const float * diag, __global const float * matr, __global float * result, int activation) { size_t row = get_global_id(0); size_t col = get_local_id(1); size_t var = get_global_id(2); size_t rows = get_global_size(0); size_t cols = get_local_size(1);
在内核主体中,第一步是在任务空间的所有三个维度上识别当前线程。然后,工作组中的第一个线程将主对角线元素存储到本地内存中,并使用屏障对线程进行同步。
__local float local_diag[1]; if(cols==0) local_diag[0] = diag[row + var * rows]; barrier(CLK_LOCAL_MEM_FENCE);
接下来,我们计算任意矩阵缓冲区中到所需元素的偏移量。
int shift = (row + var * rows) * cols + col;
需要注意的是,任意矩阵和结果矩阵的维度必须相同。因此,计算出的偏移量对于结果矩阵也有效。
然后,我们从任意矩阵缓冲区中提取所需的元素,并将其与之前存储在本地内存中的对角元素相乘。使用指定的激活函数对结果进行激活。输出结果随后被保存在结果矩阵缓冲区中。
float res = local_diag[0] * matr[shift]; //--- result[shift] = Activation(res, activation); }
上述内核实现了对角矩阵与任意矩阵相乘的前向传播。然而,这只是整个过程的一半。在模型训练过程中,我们还需要通过此操作传播误差梯度。为了实现这一目标,我们创建了一个额外的内核 DiagMatMultGrad ,它涉及一个稍微复杂一些的算法。
在这个内核中,参数中增加了用于存储相应误差梯度的缓冲区。但我们排除了激活函数指针。假设结果梯度已经通过相关激活函数的导数进行了校正。
__kernel void DiagMatMultGrad(__global const float *diag, __global float *grad_diag, __global const float *matr, __global float * grad_matr, __global const float * grad_result) { size_t row = get_global_id(0); size_t col = get_local_id(1); size_t var = get_global_id(2); size_t rows = get_global_size(0); size_t cols = get_local_size(1); size_t vars = get_global_size(2);
在内核主体中,我们使用与前向传播内核相同的方法,在三维任务空间中识别当前线程。
与前馈内核类似,每个工作组只有一个线程将所需的对角矩阵元素检索到本地数组中。
__local float local_diag[LOCAL_ARRAY_SIZE]; if(cols==0) local_diag[0] = diag[row + var * rows]; barrier(CLK_LOCAL_MEM_FENCE);
我们还会在工作组内同步线程。
接下来,我们计算任意矩阵缓冲区与结果矩阵缓冲区中的偏移量。
int shift = (row + var * rows) * cols + col; //--- float grad = grad_result[shift]; float inp = matr[shift];
我们将所需的矩阵元素保存在局部变量中。
至此,准备工作已完成。我们可以计算关于任意矩阵的误差梯度。这是通过将结果矩阵中对应元素的误差梯度与之前存储在局部数组中的对角元素相乘来实现的。
grad_matr[shift] = IsNaNOrInf(local_diag[0] * grad, 0); barrier(CLK_LOCAL_MEM_FENCE);
生成的梯度被存储在相应的全局内存缓冲区中,工作组内的线程实现同步。
接下来,我们确定对角矩阵元素的误差梯度。在此,我们必须从结果缓冲区中相应行的所有元素中聚合值。这需要对工作组中所有线程的值进行求和。
为了实现这一目标,我们在本地数组元素上实现了一个并行归约循环。
int loc = col % LOCAL_ARRAY_SIZE; #pragma unroll for(int c = 0; c < cols; c += LOCAL_ARRAY_SIZE) { if(c <= col && (c + LOCAL_ARRAY_SIZE) > col) { if(c == 0) local_diag[loc] = IsNaNOrInf(grad * inp, 0); else local_diag[loc] += IsNaNOrInf(grad * inp, 0); } barrier(CLK_LOCAL_MEM_FENCE); }
每次迭代中活动线程的最大数量受限于局部数组中的元素数量。每次迭代后,我们都会确保工作组的线程同步,然后再继续下一轮迭代,处理下一批活动线程。
接下来,我们添加一个循环,用于并行求和局部数组的元素。
int count = min(LOCAL_ARRAY_SIZE, (int)cols); int ls = count; #pragma unroll do { count = (count + 1) / 2; if((col + count) < ls) { local_diag[col] += local_diag[col + count]; local_diag[col + count] = 0; } barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
每次迭代中,活跃线程的数量都会减少。然而,每一步都保持同步,以确保计算的正确性。
为了将最终聚合值存储到全局内存中,每个工作组只需要一个线程。
if(col == 0) grad_diag[row + var * rows] = IsNaNOrInf(local_diag[0], 0); }
这种方法最大限度地减少了代价高昂的全局内存访问,并最大限度地提高了线程间的并行执行效率,从而降低了整体训练开销。
并行扫描算法
在继续 OpenCL 方面的工作之后,我们现在开始实现 Attraos 框架算法。下一个任务是实现并行扫描算法,该算法用于在考虑交互系数矩阵 A 和归一化因子 H 的情况下有效地更新输入数据数组 X。该算法的主要思想是使用二进制分解迭代地计算前缀和,将计算复杂度从顺序方法的典型 O(L) 降低到 O(log L) 。
数组 X = { x 0 , x 1 , ..., x L-1 } 根据以下递归式使用相邻元素进行更新:
![]()
其中 θ 1 和 θ 2 在算法过程中迭代确定。向量 A 表示相邻元素之间的相互作用系数矩阵, H 对计算结果进行归一化。这些参数定义了一种自适应的权重分布,使算法能够考虑数据的结构,并有效建模复杂的依赖关系。
该过程在 PScan 内核中实现。内核参数包括指向四个数据缓冲区的指针:三个用于输入,一个用于写入结果。
__kernel void PScan(__global const float* A, __global const float* X, __global const float* H, __global float* X_out) { const size_t idx = get_local_id(0); const size_t dim = get_global_id(1); const size_t L = get_local_size(0); const size_t D = get_global_size(1);
内核在二维任务空间中执行,线程沿第一维度分组。线程标识和任务空间维度是在内核内部确定的。
迭代次数 num_steps 的计算方法是取序列长度的二进制对数:
const int num_steps = (int)log2((float)L);
使用二进制的对数可以确保对数据进行完整遍历所需的迭代次数最少,从而优化计算资源的利用。
为了减少代价高昂的全局内存访问,会创建局部数组来临时存储值。
__local float local_A[1024]; __local float local_X[1024]; __local float local_H[1024];
每个线程都将数据从全局内存加载到本地内存中,从而降低后续操作的延迟并提高整体性能。
//--- Load data to local memory int offset = dim + idx * D; local_A[idx] = A[offset]; local_X[idx] = X[offset]; local_H[idx] = H[offset]; barrier(CLK_LOCAL_MEM_FENCE);
加载数据后,我们同步工作组内的线程,确保在继续计算之前正确读取和写入数据。
接下来,进行主要操作阶段。它涉及对数值进行并行求和。循环每次迭代时,活动线程的数量减半。
//--- Scan #pragma unroll for(int step = 0; step < num_steps; step++) { int halfT = L >> (step + 1); if(idx < halfT) { int base = idx * 2; local_X[base + 1] += local_A[base + 1] * local_X[base]; local_X[base + 1] *= local_H[base + 1]; local_A[base + 1] *= local_A[base]; } barrier(CLK_LOCAL_MEM_FENCE); }
在循环中,输入数组值被求和,使用 H 进行归一化,并更新交互系数 A ,在迭代更新期间保持计算顺序。使用 #pragma unroll 进行优化可以让编译器提前展开循环,从而减少分支开销并提供更高效的数据处理。
循环迭代完成后,结果值将从本地内存传输到全局结果缓冲区。
//--- Save result
X_out[offset] = local_X[idx];
}
这种方法显著加快了数据处理速度,实现了并行扫描,同时优化了计算资源的利用。
下一步是构建并行扫描的反向传播算法。我们创建了一个新的内核 PScan_CalcHiddenGradient ,其主要任务是通过反向扫描对参数进行求导。
内核参数包括指向用于存储相应误差梯度的缓冲区的指针。
__kernel void PScan_CalcHiddenGradient(__global const float* A, __global float* grad_A, __global const float* X, __global float* grad_X, __global const float* H, __global float* grad_H, __global const float* grad_X_out) { const size_t idx = get_local_id(0); const size_t dim = get_global_id(1); const size_t L = get_local_size(0); const size_t D = get_global_size(1); const int num_steps = (int)log2((float)L);
该算法首先在任务空间中识别线程,此任务空间与前馈传递过程中所采用的任务空间相似。由于并行扫描是在多次迭代中执行的,因此步骤数按序列长度的二进制对数计算,这使得该过程可以分解为具有值顺序合并的层次结构。
该实现的一个重要方面是最大限度地减少对全局内存的访问,这是通过使用本地内存数组来实现的。由于本地内存比全局内存提供更快的数据访问速度,因此可以显著提高性能。为了存储误差梯度的原始数据和中间值,我们声明了相应的缓冲区。
__local float local_A[1024]; __local float local_X[1024]; __local float local_H[1024]; __local float local_grad_X[1024]; __local float local_grad_A[1024]; __local float local_grad_H[1024];
声明局部数组后,我们将源数据从全局缓冲区转移到这些数组中。这样,每个线程都可以加载与其对应的元素。然后,我们对本地组内的线程进行同步,防止访问冲突。
//--- Load data to local memory int offset = idx * D + dim; local_A[idx] = A[offset]; local_X[idx] = X[offset]; local_H[idx] = H[offset]; local_grad_X[idx] = grad_X_out[offset]; local_grad_A[idx] = 0.0f; local_grad_H[idx] = 0.0f; barrier(CLK_LOCAL_MEM_FENCE);
接下来是算法的关键阶段 — 反向扫描。此阶段以迭代过程的方式实施。在每次迭代中,正在处理的数组的大小都会减少。
//--- Reverse Scan (Backward) #pragma unroll for(int step = num_steps - 1; step >= 0; step--) { int halfT = L >> (step + 1); if(idx < halfT) { int base = idx * 2; // Compute gradients float grad_next = local_grad_X[base + 1] * local_H[base + 1]; local_grad_H[base + 1] = local_grad_X[base + 1] * local_X[base]; local_grad_A[base + 1] = local_grad_X[base + 1] * local_X[base]; local_grad_X[base] += local_A[base + 1] * grad_next; } barrier(CLK_LOCAL_MEM_FENCE); }
在循环体中,我们首先计算参与给定迭代的活动线程数( halfT )。然后我们确定误差梯度( grad_next ),它是通过将当前值乘以相应的归一化系数 H 来确定的。接下来,我们使用当前值 X 计算关于归一化系数 H 和相互作用 A 的导数。为了正确地向后传播误差,梯度值 X 会根据相互作用系数进行调整。在循环的每次迭代中,我们必须同步工作组内的线程。
在内核操作结束时,必须将更新的误差梯度传回全局内存。
//--- Save gradients
grad_A[offset] = local_grad_A[idx];
grad_X[offset] = local_grad_X[idx];
grad_H[offset] = local_grad_H[idx];
}
由于使用了本地内存且全局内存访问次数最少,这种数据处理方法具有很高的计算效率。
至此,我们在 OpenCL 端实现方面的工作就完成了。完整的 OpenCL 程序代码可在附件中找到。
我们工作的下一阶段是在主程序端构建算法。但是,由于我们即将达到文章格式限制,我们将稍作休息,并在下一部分继续构建 Attraos 框架。
结论
在本文中,我们探讨了 Attraos 框架,该框架提出了一种基于混沌理论的时间序列预测算法。时间序列被解读为多维混沌动力系统的投影,从而能够识别出传统统计或回归模型无法获取的隐藏模式。Attraos 实现了相空间重构和动态记忆机制,从而能够检测市场数据中稳定的非线性依赖关系,并提高预测精度。
与不考虑变量之间复杂多维相互作用的传统线性模型不同, Attraos 基于混沌吸引子的内部结构运行,从而确保了较高的预测准确性和对不断变化的市场条件的适应性。这种方法可以检测出最初看起来随机的过程中的确定性成分,这对于高频数据分析和短期金融预测尤为重要。
在实践部分,我们开始使用 MQL5 和 OpenCL 技术来实现我们提出的方法的愿景,通过在 GPU 上进行并行数据处理,显著加快了计算速度。这使得 Attraos 方法能够应用于现实世界的交易系统和自动化分析平台,从而实现对大型数据集的高速处理和对不断变化的市场条件的快速适应。
在下一篇文章中,我们将继续实现这些方法,并通过在实际历史数据上的验证来将其完善。
参考文献列表
本文中用到的程序
| # | 名称 | 类型 | 描述 |
|---|---|---|---|
| 1 | Research.mq5 | EA | 样本采集 EA |
| 2 | ResearchRealORL.mq5 | EA | 使用 Real-ORL 方法采集样本的 EA |
| 3 | Study.mq5 | EA | 模型训练 EA |
| 4 | Test.mq5 | EA | 模型测试 EA |
| 5 | Trajectory.mqh | 类库 | 系统状态和模型架构描述结构 |
| 6 | NeuroNet.mqh | 类库 | 用于创建神经网络的类库 |
| 7 | NeuroNet.cl | 代码库 | OpenCL 程序代码 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/17351
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
数据科学与机器学习(第四十部分):斐波那契回调位在机器学习中的应用
新手在交易中的10个基本错误
查看新文章:交易中的神经网络:将混沌理论融入时间序列预测(Attraos)。
作者:Dmitriy Gizlyk德米特里-吉兹里克