交易中的神经网络:降低锐度强化变换器效率(SAMformer)
概述
多变量时间序列预测是一项经典的机器学习任务,涉及分析时间序列数据,从而基于历史形态预测未来趋势。由于特征相关性、及长期时态依赖性,这是一个颇具挑战性的问题。这种学习问题在按顺序收集观察结果的实际应用中很常见(例如,医疗数据、电力消耗、股票价格)。
最近,基于变换器的架构在自然语言处理、和计算机视觉任务中取得了突破性的性能。变换器在处理顺序数据时特别有效,令其非常适合时间序列预测。然而,最先进的多变量时间序列预测,仍常用更简单的基于 MLP 的模型来达成。
最近将变换器应用于时间序列数据的研究,主要集中在优化注意力机制,以便降低二次计算成本;或分解时间序列,以便更好地捕获其潜在形态。然而,论文《SAMformer:配合锐度感知最小化和通道级注意力,解锁变换器在时间序列预测中的潜力》的作者曝光了一个严重问题:在缺乏大规模数据的情况下,变换器的训练不稳定性。
在计算机视觉和 NLP 中,已观察到注意力矩阵可能会遭受熵坍缩或秩坍缩。已提出了若干种方式来缓解这些问题。然而,时间序列预测中,如何有效地训练变换器架构,且不会过度拟合,仍是一个悬而未决的问题。作者旨在演示解决训练不稳定性,就可显著提升变换器在长期多变量预测中的性能,这与之前关于其局限性的既定观点形成鲜明对比。
1. SAMformer 算法
专注于给定长度为 L(回溯窗口)的 D-维时间序列的情况下,在多变量系统中进行长期预测。输入数据表示为矩阵 𝐗 ∈ RD×L。意图是预测下一个 H 值(预测横向范围),表示为 𝐘 ∈ RD×H。假设访问一个训练集,由 N 个观测值组成,目标是训练一个预测模型 f𝝎: RD×L→RD×L,参数 ω,即最小化训练数据的均方误差(MSE)。
最新发现表明,变换器的性能,与直接把输入数据投影到预测值中的已训练简单线性神经网络相当。为了调研这种现象,SAMformer 框架采用了一个生成式模型,模拟合成回归任务,仿造时间序列预测设置。作者使用线性模型,自随机输入数据生成延续的时间序列,并往输出里加入少量噪声。该过程产生了 15,000 个输入-输出对,划分 10,000 个用于训练,及 5,000 个用于验证。
利用该生成式方法,SAMformer 的作者设计了一个变换器架构,具备有效解决预测任务的能力,且没有不必要的复杂性。为了达成这一目标,它们仅保留了自注意力模块、及残差连接来简化传统的变换器编码器。取代了 FeedForward 模块,直接用线性层预测后续值。
重点要注意,SAMformer 框架采用通道级注意力,这简化了任务,并降低了过度参数化的风险,是以注意力矩阵由于 L>D 而明显变小。甚至,通道级注意力于此更对口,因为数据生成遵循雷同的过程。
为了理解注意力在解决该任务中的角色,作者提出了一个名为随机变换器的模型。在该模型中,仅优化了预测层,而自注意力模块的参数在训练期间,以被固定为随机初始化值。这有效地迫使变换器的举动如同线性模型。比较两个模型获得的局部最小值。调用 Adam 方法和 Oracle 模型(对应于最小二乘解)进行优化,如下图所示(如原始论文所示)。

第一个令人惊讶的发现是,两个变换器模型都无法复原合成回归任务的线性依赖性,这凸显出在如此简单的架构中,即使配以有利设计,也展现优化明显缺乏普适。这一观察抓住了不同优化器和学习率设置的真相。据此,SAMformer 作者得出结论,变换器的普适能力受限,主要源于注意力模块内的训练困难。
为了更好地理解这种现象,SAMformer 的作者可视化了不同训练局次的注意力矩阵,并发现注意力矩阵在第一局之后与恒等矩阵非常雷同,而此后的变化很小,特别是当 Softmax 函数放大了注意力值之间的差异时。该行为揭示了注意力熵坍缩的开始,导致了全秩注意力矩阵,作者将其判定为变换器训练刚性的原因之一。
SAMformer 作者还观察到熵坍缩与变换器损失局面之间的关系。相比随机变换器,标准变换器收敛到锐度的最小值,并展现出明显低熵(由于随机变换器中的注意力权重在初始化时是固定的,因此其熵贯穿整个训练维持常量)。这些失控形态表明,由于训练期间熵坍缩和锐度损失局面的双重影响,变换器表现不佳。
最近研究确认,变换器的损耗局面确比其它架构更尖锐。这或许有助于解释变换器训练不稳定性、及低性能,尤其是在较小数据集上训练时。
为了解决这些挑战,并提升普适性和训练稳定性,SAMformer 作者探索了两种方式。第一个涉及锐度感知最小化(SAM),它修改训练意向如下:
![]()
其中 ρ>0 是超参数,ω 表示模型参数。
第二种方式引入了所有权重矩阵进行重新参数化,即利用频谱归一化、以及称为 σReparam 的附加可训练标量。
结果凸显所提议方案在达成预期成果方面的成功。值得注意的是,这是独用 SAM 实现的,尽管增加了注意力矩阵熵,燃 σReparam 方法仍未能接近优化性能。甚至,在 SAM 下达成的锐度比标准变换器低若干数量级,而 SAM 下的注意力熵仍与基线变换器相当,在训练的后期阶段仅略有增加。这示意在这种场景中熵坍缩是良性的。
SAMformer 框架进一步协同可逆实例归一化(RevIN)。该方法已被证明,可有效处理时间序列中训练数据和测试数据之间的分布偏移。如上述研究演示,该模型利用 SAM 进行了优化,引导其朝向较平坦的局部最小值。总体上,该结果是一个搭配单一编码器模块的简化变换器模型,如下图所示(作者的原始可视化)。

重点需强调,SAMformer 保留了由 D×D 矩阵表示的通道注意力,不同于空间(或时态)注意力,其典型情况下依赖其它模型中的 L×L 矩阵。该设计提供了两个关键优势:
- 特征的排列不变性,剔除了通常在注意力层之前应用的定位编码的需求;
- 降低了计算时间和内存复杂性,如同大多数现世数据集中的 D≤L。
2. 利用 MQL5 实现
在涵盖了 SAMformer 框架的理论层面之后,我们现转入利用 MQL5 实际实现。此刻,重要的是准确定义我们预计在模型中实现什么、以及如何实现。我们就近考察 SAMformer 作者建议的组件:
- 将变换器编码器修剪为带有残差连接的自注意力模块;
- 通道级注意力;
- 可逆归一化(RevIN);
- SAM 优化。
编码器修剪是一个有趣的层面。然而,实际上,它的主要价值在于降低可训练参数的数量。功能上,我们如何把神经层标记为编码器 FeedForward 模块的一部分、或放置在注意力之后作为预测层,模型行为均不受影响,冈在原始框架中所做。
为了实现通道级注意力,只需在投喂到注意力块之前,转置输入数据足矣。该步无需模型结构上的更改。
我们已熟悉了可逆实例归一化(RevIN)。余下的任务是实现 SAM 优化,其运作要寻找位于邻域中的均匀低损耗值参数集。
SAM 优化算法涉及若干步骤。首先,执行前馈通验,计算相对于模型参数的损耗梯度。然后,这些梯度被归一化,并加到当前参数之中,并按锐度系数缩放。按这些扰动参数执行第二遍前馈通验,并计算新的梯度。然后,我们减去之前添加的扰动,恢复原始权重。最后更新参数,即调用标准优化器 — SGD 或 Adam。SAMformer 作者建议采用后者。
一个重要的细节是 SAMformer 的作者跨整个模型的梯度进行了归一化。这定是计算密集型的。这会把降低模型参数数量的相关性拔高。如是结果,修剪内层和降低注意力头的数量,变为实际需要。这就是 SAMformer 框架作者所做的。
然而,在我们的实现中,我们略有不同:我们在各个神经层级上执行梯度归一化。甚至,对于单一神经元输出有贡献的每个参数组,我们都分别执行梯度归一化。我们从开发 OpenCL 端新内核来开始实现程序。
2.1扩展 OpenCL 程序
或许正如您从之前的工作中注意到的那样,我们主要依赖于两种类型的神经层:全连接,和卷积。我们所有的注意力模块都用卷积层构建,应用时不重叠,以便分析和转换序列中的各个元素。因此,我们选择配合 SAM 优化来增强这两种层类型。在 OpenCL 端,我们将开发两个内核:一个进行梯度归一化,另一个生成扰动权重 ω+ε。
我们先为全连接层 CalcEpsilonWeights 创建一个内核。该内核接收指向四个数据缓冲区和锐度色散系数的指针。三个缓冲区保存输入数据,而第四个缓冲区设计用来存储输出结果。
__kernel void CalcEpsilonWeights(__global const float *matrix_w, __global const float *matrix_g, __global const float *matrix_i, __global float *matrix_epsw, const float rho ) { const size_t inp = get_local_id(0); const size_t inputs = get_local_size(0) - 1; const size_t out = get_global_id(1);
我们计划在二维任务空间中调用该内核,按第一维对线程进行分组。在内核主体中,我们即刻辨别任务空间所有维度的当前执行线程。
接下来,我们在设备上声明一个局部内存数组,来促进同一工作组内线程之间的数据交换。
__local float temp[LOCAL_ARRAY_SIZE]; const int ls = min((int)inputs, (int)LOCAL_ARRAY_SIZE);
在随后步骤中,我们计算每个所分析元素的误差梯度,及输入和输出梯度缓冲区中相应元素的乘积。然后,我们按相关联参数的绝对值缩放该结果。这样,对层输出贡献更显著的参数,就会提升影响力。
const int shift_w = out * (inputs + 1) + inp; const float w =IsNaNOrInf(matrix_w[shift_w],0); float grad = fabs(w) * IsNaNOrInf(matrix_g[out],0) * (inputs == inp ? 1.0f : IsNaNOrInf(matrix_i[inp],0));
最后,我们计算所得梯度的 L2 范数。这涉及汇总工作组内已计算数值的平方,遵循我们之前实现中所用方式,即使用局部内存数组和两个缩减循环。
const int local_shift = inp % ls; for(int i = 0; i <= inputs; i += ls) { if(i <= inp && inp < (i + ls)) temp[local_shift] = (i == 0 ? 0 : temp[local_shift]) + IsNaNOrInf(grad * grad,0); barrier(CLK_LOCAL_MEM_FENCE); } //--- int count = ls; do { count = (count + 1) / 2; if(inp < count) temp[inp] += ((inp + count) < inputs ? IsNaNOrInf(temp[inp + count],0) : 0); if(inp + count < inputs) temp[inp + count] = 0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
累积总和的平方根代表梯度的 L2 范数。使用该数值,我们计算调整后的参数值。
float norm = sqrt(IsNaNOrInf(temp[0],0)); float epsw = IsNaNOrInf(w * w * grad * rho / (norm + 1.2e-7), w); //--- matrix_epsw[shift_w] = epsw; }
然后,我们将结果值保存在全局结果缓冲区的相应元素当中。
运用类似的方式构建 CalcEpsilonWeightsConv 内核,该内核针对卷积层执行初始参数调整。不过,如您所知,卷积层有其自身的特征。典型情况下,它们包含少量参数,但每个参数都与输入数据层的多个元素交互,并影响结果缓冲区中若干元素的数值。如是结果,每个参数的梯度是通过聚合其来自输出缓冲区的若干个元素的影响来计算的。
这种特定于卷积的行为也会影响内核参数。此处出现了两个附加常量,定义输入序列的大小、和输入窗口的步幅。
__kernel void CalcEpsilonWeightsConv(__global const float *matrix_w, __global const float *matrix_g, __global const float *matrix_i, __global float *matrix_epsw, const int inputs, const float rho, const int step ) { //--- const size_t inp = get_local_id(0); const size_t window_in = get_local_size(0) - 1; const size_t out = get_global_id(1); const size_t window_out = get_global_size(1); const size_t v = get_global_id(2); const size_t variables = get_global_size(2);
我们还将任务空间扩展到三个维度。第一个维度对应于输入数据窗口,通过偏移量扩展。第二个维度表示卷积过滤器的数量。第三个维度计量独立输入序列的数量。如前,我们按第一个维度将操作线程分组至工作组。
在内核内部,我们辨别所有任务空间维度的当前执行线程。然后,我们在 OpenCL 关联环境中初始化一个局部内存数组,来促进工作组内的线程间通信。
__local float temp[LOCAL_ARRAY_SIZE]; const int ls = min((int)(window_in + 1), (int)LOCAL_ARRAY_SIZE);
接下来,我们计算输出缓冲区中每个滤波器的元素数量,并判定其在数据缓冲区中的相应偏移量。
const int shift_w = (out + v * window_out) * (window_in + 1) + inp; const int total = (inputs - window_in + step - 1) / step; const int shift_out = v * total * window_out + out; const int shift_in = v * inputs + inp; const float w = IsNaNOrInf(matrix_w[shift_w], 0);
此刻,我们还把分析中的参数的当前值存储在局部变量之中。该优化降低了后续步骤中对全局内存的访问次数。
在下一阶段,我们按照输出缓冲区中所有元素受到所分析参数的影响,收集梯度贡献度。
float grad = 0; for(int t = 0; t < total; t++) { if(inp != window_in && (inp + t * step) >= inputs) break; float g = IsNaNOrInf(matrix_g[t * window_out + shift_out],0); float i = IsNaNOrInf(inp == window_in ? 1.0f : matrix_i[t * step + shift_in],0); grad += IsNaNOrInf(g * i,0); }
然后,我们按参数的绝对值缩放所收集梯度。
grad *= fabs(w);
随后,我们应用前面描述的两阶段约简算法,汇总工作组内梯度的平方。
const int local_shift = inp % ls; for(int i = 0; i <= inputs; i += ls) { if(i <= inp && inp < (i + ls)) temp[local_shift] = (i == 0 ? 0 : temp[local_shift]) + IsNaNOrInf(grad * grad,0); barrier(CLK_LOCAL_MEM_FENCE); } //--- int count = ls; do { count = (count + 1) / 2; if(inp < count) temp[inp] += ((inp + count) < inputs ? IsNaNOrInf(temp[inp + count],0) : 0); if(inp + count < inputs) temp[inp + count] = 0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
所得总和的平方根产生所需的误差梯度 L2 范数。
float norm = sqrt(IsNaNOrInf(temp[0],0)); float epsw = IsNaNOrInf(w * w * grad * rho / (norm + 1.2e-7),w); //--- matrix_epsw[shift_w] = epsw; }
然后,我们计算调整后的参数值,并将其存储在结果缓冲区的相应元素之中。
我们在 OpenCL 端的实现工作到此完结。完整的代码可在附件中找到。
2.2搭配 SAM 优化的全连接层
在完成 OpenCL 端的工作后,我们转至函数库实现,其中我们为集成 SAM 优化的全连接层创建对象 — CNeuronBaseSAMOCL。新类结构如下所示。
class CNeuronBaseSAMOCL : public CNeuronBaseOCL { protected: float fRho; CBufferFloat cWeightsSAM; //--- virtual bool calcEpsilonWeights(CNeuronBaseSAMOCL *NeuronOCL); virtual bool feedForwardSAM(CNeuronBaseSAMOCL *NeuronOCL); virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); public: CNeuronBaseSAMOCL(void) {}; ~CNeuronBaseSAMOCL(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons, float rho, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(int const file_handle); virtual bool Load(int const file_handle); //--- virtual int Type(void) const { return defNeuronBaseSAMOCL; } virtual int Activation(void) const { return (fRho == 0 ? (int)None : (int)activation); } virtual int getWeightsSAMIndex(void) { return cWeightsSAM.GetIndex(); } //--- virtual CLayerDescription* GetLayerInfo(void); virtual void SetOpenCL(COpenCLMy *obj); };
如您从结构中所见,主要功能继承自基础全连接层。基本上,该类是基础层的副本,重写了参数更新方法,并协同 SAM 优化逻辑。
也就是说,我们添加了一个包装方法 calcEpsilonWeights 来与之前描述的相应内核交互,并且我们还创建了前向通验方法的修改版,即使用名为 feedForwardSAM 的替换权重缓冲区。
值得注意的是,在最初的 SAMformer 框架中,作者将 ε 应用于模型参数,然后减去它,来恢复原始状态。我们以不同的方式处理这个问题。我们把扰动的参数存储在单独的缓冲区之中。这令我们能够绕过 ε 减法步骤,从而降低总执行时间。但特事特例。
模型扰动参数的缓冲区声明为静态,允许我们将构造函数和析构函数留空。所有声明和继承的对象的初始化都在 Init 方法中执行。
bool CNeuronBaseSAMOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons, float rho, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, numNeurons, optimization_type, batch)) return false;
在方法参数中,我们接收确定所创建对象架构的主要常量。在方法内部,我们立即调用父类的 Init 方法,其中已实现了继承组件的验证和初始化。
一旦父类方法成功完成,我们将锐度半径系数存储在内部变量之中。
fRho = fabs(rho); if(fRho == 0 || !Weights) return true;
接下来,我们检查锐度系数值、及参数矩阵的存在。如果系数等于 “0”,或参数矩阵不存在(意味着该层没有外出连接),则该方法成功退出。否则,我们需要为替代参数创建一个缓冲区。结构上,它与主要权重缓冲区雷同,但在该阶段初始化为零值。
if(!cWeightsSAM.BufferInit(Weights.Total(), 0) || !cWeightsSAM.BufferCreate(OpenCL)) return false; //--- return true; }
这样就完成了该方法。
我们建议您自行查看 OpenCL 内核排队的包装器方法。在附件中提供了它们的代码。我们转到参数更新方法:updateInputWeights。
bool CNeuronBaseSAMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; if(NeuronOCL.Type() != Type() || fRho == 0) return CNeuronBaseOCL::updateInputWeights(NeuronOCL);
该方法如常接收指向输入数据对象的指针。我们立即验证指针,因为如果指针无效,任何进一步的操作都会导致严重错误。
我们还验证输入数据对象的类型,因为它在这种境况下很重要。此外,锐度系数必须大于 “0”。否则,SAM 逻辑将退化为标准优化。然后我们调用父类的相关方法。
一旦这些检查通过后,我们继续执行 SAM 方法的操作。回想一下,SAM 算法涉及完整的前馈和反向传播通验,依据 ε 扰动参数分派误差梯度。然而,我们早些时候已确立我们的 SAM 实现在单一层级运作。这就浮现出一个问题:我们从何处获得每一层的目标值?
初看,该方案似乎很简单 — 只需将最后一次前馈通验结果与误差梯度相加即可。但这里有一个警告。当梯度由子层通验时,典型情况下是由激活函数的导数进行调整。因此,简单的求和会扭曲结果。一种选项是实现一种机制,即基于激活导数逆转梯度校正。然而,我们找到了一个更简单、更有效的方案:我们重写激活函数返回方法,如此这般若锐度系数为零,则该方法返回 None。这样,我们从下一层接收误差梯度生料,未经激活导数修改。因此,我们能够将前馈通验结果与误差梯度相加。这两者的合计为我们给出被分析层的有效目标。
if(!SumAndNormilize(Gradient, Output, Gradient, 1, false, 0, 0, 0, 1)) return false;
接下来我们调用包装器方法来获取调整后的模型参数。
if(!calcEpsilonWeights(NeuronOCL)) return false;
我们按扰动参数执行前馈通验。
if(!feedForwardSAM(NeuronOCL)) return false;
此刻,误差梯度缓冲区包含目标值,而结果缓冲区保存由扰动参数产生的输出。为了判定这些数值之间的偏差,我们简单地调用父类方法来计算与目标输出的偏差。
float error = 1; if(!calcOutputGradients(Gradient, error)) return false;
现在,我们仅需基于更新后的误差梯度更新模型参数。这是通过调用来自父类的相应方法做到的。
return CNeuronBaseOCL::updateInputWeights(NeuronOCL);
}
关于文件操作方法,应当说几句话。为了节省磁盘空间,我们选择不保存扰动权重缓冲区 cWeightsSAM。保留其数据并无实际价值,因为该缓冲区仅在参数更新期间相关。每次调用都会覆盖它。因此,所保存数据的大小仅增加了一个浮点元素(系数)。
bool CNeuronBaseSAMOCL::Save(const int file_handle) { if(!CNeuronBaseOCL::Save(file_handle)) return false; if(FileWriteFloat(file_handle, fRho) < INT_VALUE) return false; //--- return true; }
另一方面,执行所需功能仍需 cWeightsSAM 缓冲区。它的大小至关重要,因为它必须足以容纳当前层的所有参数。因此,当我们加载以前保存的模型时,需要重新创建它。在数据加载方法中,我们首先调用来自基类的等效方法。
bool CNeuronBaseSAMOCL::Load(const int file_handle) { if(!CNeuronBaseOCL::Load(file_handle)) return false;
接下来,我们检查超出基本结构之外的文件内容,如果存在,我们读取锐度系数。
if(FileIsEnding(file_handle)) return false; fRho = FileReadFloat(file_handle);
然后,我们验证锐度系数是否非零,并确保存在有效的参数矩阵(注意:在没有外出连接层的情况下,其指针可能无效)。
if(fRho == 0 || !Weights) return true;
如果任一检查失败,参数优化将降级为基本方法,并且无需重新创建所调整参数的缓冲区。因此,我们成功退出该方法。
应当注意,检查未通过对于 SAM 优化至关重要,但对于整个模型操作无关紧要。因此,程序继续使用基本优化方法。
如果需要创建缓冲区,我们首先清除现有缓冲区。我们故意跳过检查清除操作的结果。这是因为在加载时,可能缓冲区尚不存在。
cWeightsSAM.BufferFree();
然后,我们初始化一个大小为零的新缓冲区,并创建其 OpenCL 副本。
if(!cWeightsSAM.BufferInit(Weights.Total(), 0) || !cWeightsSAM.BufferCreate(OpenCL)) return false; //--- return true; }
这次,我们会验证这些操作的执行,因为它们的成功对于模型的进一步操作至关重要。待至完成,我们将操作状态返回给调用函数。
我们针对 SAM 优化支持的全连接层(CNeuronBaseSAMOCL)实现的讨论到此完结。该类及其方法的完整源代码可在提供的附件中找到。
不幸的是,我们已达到本文的篇幅限制,但我们尚未完成这项工作。在下一篇文章中,我们将继续实现、并研究卷积层,以及 SAM 功能的实现。我们还将考察所提议技术在变换器架构中的应用,当然,还将测试所提议方式依据真实历史数据的性能。
结束语
SAMformer 为变换器模型在多元时间序列长期预测中的核心缺点(譬如训练复杂性、及小型数据集的普适性差)提供了有效的解决方案。通过使用浅层架构和锐度感知优化,SAMformer 不仅避免了糟糕的局部最小值,而且性能优于最先进的方法。甚至,它所用的参数更少。由作者提出的结果确认了它作为时间序列任务通用工具的潜力。
在我们文章的实践部分,我们已利用 MQL5 构建了我们对所提议方式的愿景。但我们的工作仍在进行中。在下一篇文章中,我们将评估所提议方式在解决我们问题方面的实用价值。
参考
文章中所用程序
| # | 名称 | 类型 | 说明 |
|---|---|---|---|
| 1 | Research.mq5 | 智能系统 | 收集样本的智能系统 |
| 2 | ResearchRealORL.mq5 | 智能系统 | 利用 Real-ORL 方法收集样本的智能系统 |
| 3 | Study.mq5 | 智能系统 | 模型训练智能系统 |
| 4 | StudyEncoder.mq5 | 智能系统 | 编码训练 EA |
| 5 | Test.mq5 | 智能系统 | 模型测试智能系统 |
| 6 | Trajectory.mqh | 类库 | 系统状态描述结构 |
| 7 | NeuroNet.mqh | 类库 | 创建神经网络的类库 |
| 8 | NeuroNet.cl | 函数库 | OpenCL 程序代码库 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/16388
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
基于Python与MQL5的多模块交易机器人(第一部分):构建基础架构与首个模块
迁移至 MQL5 Algo Forge(第 2 部分):使用多个存储库
价格行为分析工具包开发(第六部分):均值回归信号捕捉器
开发回放系统(第 72 部分):异常通信(一)