
一种采用纯MQL5语言实现的基于能量学习的特征选择算法
引言
在算法交易的领域中,机器学习的广泛应用,促使数据挖掘技术被用来发掘金融数据中的隐藏模式。在这一背景下,从业人员经常面临挑战,即如何从众多变量中筛选出最有可能对实现特定目标或解决特定问题有用的变量。本文探讨了特征选择算法的实现,该算法旨在评估一组候选变量在给定预测任务中的相关性。
Yun Li, Jennie Si, Guojing Zhou, Shasha Huang, and Songcan Chen共同撰写了一篇题为 《FREL:一种稳定的特征选择算法》的研究论文。该论文介绍了一种名为基于正则化能量的特征加权学习(FREL)的算法,该算法作为一种特征选择或加权技术,旨在同时提供准确性和稳定性。在我们的讨论中,我们概述了基于正则化能量的学习和特征加权的理论基础。此外,我们还通过一个MQL5程序示例(编写为脚本),说明了所提出方法的有效性,以此突出该方法作为特征选择工具的潜力。
加权最近邻分类
FREL背后的概念灵感来源于一种被称为加权最近邻分类的技术,该技术利用数据集中点之间的距离来进行预测。通过为每个特征确定适当的权重,这种方法可以提高预测的准确性。加权最近邻分类是k-最近邻(k-NN)算法的一种变体,k-NN算法是机器学习领域中用于分类任务的常用方法。在标准的k-NN分类中,算法在分类新数据点时,会查看训练集中k个最近的数据点,最终将这些邻居中的多数类别分配给新数据点。
然而,在加权最近邻分类中,不是仅仅统计最邻近数据的投票量,而是根据每个邻近数据与新数据点的距离为其投票加权。其原理是,较近的相邻数据应该对分类决策产生更大的影响,而较远的则影响较小。这个加权过程涉及计算新数据点与训练集中每个点之间的距离。常用的距离度量包括欧几里得距离、曼哈顿(Manhattan)距离或余弦相似度,具体选择取决于数据的特性。在这种情况下,我们使用曼哈顿距离(也称为城市街区距离)来计算数据点之间的距离。计算这个距离的公式如下所示。其中,w表示权重,测试用例是相对于其他训练数据进行评估的,这些训练数据被称为训练案例。
理解基于能量的模型
机器学习中的基于能量的模型是一个多功能的框架,既适用于有监督学习任务,也适用于无监督学习任务。它的工作原理是为各种数据配置分配能量值,并学习一个能够区分理想配置和不理想配置的模型。这是通过最小化观测数据的能量,同时最大化未观测数据或不理想数据配置的能量来实现的。
基于能量的模型的核心在于定义一个能量函数,用E()表示。这个函数以输入变量的配置(或称为预测变量)以及一组模型参数作为输入。能量函数的输出是输入变量配置相关性的指征。例如,在评估回归模型的背景下,能量函数可以表示为均方误差。当将相关预测变量输入到均方误差方程中时,输出值往往较小,这反映了较高的相关性。相反,较差的预测变量会导致较大的均方误差值。能量函数为每个可能的变量配置分配一个标量值。
训练能量基模型的目标是学习能量函数的参数,以便为相关输入变量分配低能量,为不相关输入变量分配高能量。这包括定义一个目标函数,该函数会对为正确变量分配高能量,对错误变量分配低能量的情况进行惩罚。为了实现这一点,目标是识别出产生最低能量的错误变量配置,这种配置很可能导致模型做出错误的预测。面的函数表示输入配置x和模型参数w的能量,该能量输出错误的值y,这个值太低,以至于无法与产生准确预测的输入变量配置区分开来。
最终,目标函数旨在最大化最低能量的错误配置与最近的正确配置变量之间的差异。该能量配置的公示如下所示。
目标函数,也被称为损失函数,包含一个基于每个样本的平均损失函数。下面给出的是对数损失(log loss)函数。
各种衡量损失的规则都可以作为每个样本的损失函数,如hinge损失、对数损失(log loss)、平方损失和平方指数损失等,具体选择取决于应用场景。
综上所述,这些是FREL背后的基本概念。接下来的部分将深入探讨算法本身的具体内容。
FREL算法
为了有效应用FREL算法,必须遵循某些基本考虑因素。首先,仔细评估训练数据至关重要。FREL非常适合于将一组候选变量映射到单个目标的数据集。同样重要的是确保变量的尺度相似。如果向FREL提供尺度不一致的候选预测变量,最终结果可能会严重扭曲。
其次,由于FREL是基于能量的学习过程,因此它需要定义一个包含加权参数的能量函数。因此,所使用的模型应该配置为接受一组候选变量以及相应的权重。例如,如果以均方误差作为能量函数,并且模型是回归模型,那么纳入加权参数就相对直接了。每个权重将与一个候选预测变量配对。
最后,必须选择一个单样本的损失函数来确定损失函数。损失函数结合了模型的加权参数,是(一系列)函数中被最小化以获得最优权重的函数。
FREL算法的核心步骤如下:
- 从一个包含n个观测值和d个候选预测变量的训练数据集开始,这些预测变量对应于n个目标值。目标是确定从d个候选预测变量中最相关的预测变量,以确定目标值。这导致为每个d个预测变量分配权重,表示该变量相对于其他变量的重要性。较大的权重表示在定义目标值时具有更大的相关性。
- 最初,将所有权重的值设为1。
- 对训练数据中的每个观测值应用加权最近邻分类,以识别产生最低能量的错误变量配置以及具有高能量的正确变量配置的最近邻配置。使用这些能量值和所选的损失函数来计算每个样本的损失。
- 最后,使用适当的优化程序最小化目标损失函数,可以选择性地加入正则化。这构成了FREL算法的核心。
用MQL5实现FREL
本文中展示的FREL实现采用了Powell优化方法。虽然使用的具体优化方法不是关键,但不同方法的结果应该相对一致。在此实现中,Powell方法被表示为“PowellsMethod”类,该类在Powells.mqh中定义。FREL算法被封装在“FREL”类中,该类是“PowellsMethod”类的子类,并在frel.mqh中指定。
//+------------------------------------------------------------------+ //| constructor | //+------------------------------------------------------------------+ FREL(matrix &in_data,int numboot=1, int bootsize=0) { m_data = in_data; m_num_boot=(numboot>0)?numboot:1; m_bootsize=(bootsize>2 && bootsize<=int(m_data.Rows()) && m_num_boot>1)?bootsize:int(m_data.Rows()); if(ArrayResize(m_indices, int(m_data.Rows()))!=int(m_data.Rows()) || ArrayResize(m_target_bin, int(m_data.Rows()))!=int(m_data.Rows()) || ArrayResize(m_trial_weights, int(m_data.Cols()-1))!=int(m_data.Cols()-1) || ArrayResize(m_work_weights, int(m_data.Cols()-1))!=int(m_data.Cols()-1) ) { Print(__FUNCTION__, " error ", GetLastError()); m_memory_allocated = false; } else m_memory_allocated = true; }
让我们深入了解参数化构造函数的描述。它至少需要一个参数:训练数据的矩阵。重要的是要注意训练数据在矩阵中的结构方式。每一行代表一个观测值或单个样本,而列则代表要评估的候选变量或预测变量。目标值应位于矩阵的最后一列。构造函数的可选参数在下表中进一步说明。
参数名称 | 数据类型 | 默认值 | 说明 |
---|---|---|---|
numboot | integer | 1 | "numboot" 参数用于指定要重复采样的次数 |
bootsize | integer | 0 | “bootsize”定义了每个自举样本的大小。使用这些变量要非常小心。如果使用的值大于矩阵中的观测值数量,“numboot”将自动回退到1,并且“bootsize”将设置为观测值的数量。 |
用户只需要熟悉一个方法就可以使用“FREL”类:“WeighVars()”。
//+-----------------------------------------------------------------------+ //| Find the most relevant variables from a dataset of candidate variables| //+-----------------------------------------------------------------------+ bool WeighVars(int num_bins_target, double reg_factor,int &index[],double &weights[]) { if(!m_memory_allocated) { Print(" INTERNAL ERROR "); return false; } if(num_bins_target<=1 || num_bins_target>int(m_data.Rows())) { Print(__FUNCTION__, " invalid function parameter: num_bins_target. Parameter should be >=2 "); return false; } int ret=0; double target[], target_thresholds[] ; double sum ; int n_cases = int(m_data.Rows()); m_reg_factor = MathAbs(reg_factor); m_loss = 0.0; if(ArrayResize(index,int(m_data.Cols()-1))!=int(m_data.Cols()-1) || !np::vecAsArray(m_data.Col(m_data.Cols()-1),target) ) { Print(__FUNCTION__, " error ", GetLastError()); return false; } int k = num_bins_target ; if(!bin_array(target, k, target_thresholds, m_target_bin)) return false; if(k<num_bins_target) { Print("error bins of target vector ", num_bins_target," : ", k); return false; } for(int i=0 ; i<n_cases ; i++) { if(m_target_bin[i] >= num_bins_target) { Print("error m_target_bin array at index ", i, " is ",m_target_bin[i], " should be less than ", num_bins_target); return false; } } ret = calc_wt(num_bins_target,m_loss,weights); if(ret<0) return false; sum = 0.0 ; for(ulong var=0 ; var<m_data.Cols()-1 ; var++) { weights[var] = m_data.Col(var).Std() * exp(weights[var]); sum += weights[var] ; } for(ulong var=0 ; var<m_data.Cols()-1 ; var++) { weights[var] *= 100.0 / sum ; index[var] = int(var) ; } MathQuickSortDescending(weights,index,0,int(weights.Size()-1)) ; return true; }
这个方法评估构造函数中指定的训练数据。它返回一个布尔值,“false”表示过程未能完成。这个方法的参数如下:
- "num_bins_target":一个整数,定义了目标值将被划分成的箱(bin)的数量。此参数应设置为任何大于等于2的整数,但小于等于训练数据中的观测数。
- "reg_factor":一个正的双精度浮点数值,用于控制正则化的程度。值为0时,禁用正则化。
- "index[]":一个整数数组,用于存储部分操作结果的输出。它包含按与目标的相关性降序排列的原始列索引,这些索引将被构造函数所用。
- "weights[]":一个双精度浮点数数组,包含按降序排列的最优权重。
当调用"WeighVars()"方法时,会从矩阵中提取目标值,并将它们放入一个数组中,以便准备调用私有方法"bin_array()"。这个方法将数组分割成大小大致相等的类别,并在成功完成后输出两个数组。"upperbound_thresholds[]"是一个数组,包含每个类别的上界阈值,而整数数组"categories[]"包含索引值,这些索引值表示每个对应的目标值所属的类别。会检查这些值中的每一个,以确保所有目标值都被正确地分箱(binned)。
//+------------------------------------------------------------------+ //| calculates the optimal weights of candidate variables | //+------------------------------------------------------------------+ int calc_wt(int num_bins_target,double &loss_value, double &w[]) { int ret,rand_error, class_count[] ; ret = 0; if(ArrayResize(class_count,num_bins_target)!=num_bins_target || (w.Size()!=uint(m_data.Cols()-1) && ArrayResize(w,int(m_data.Cols()-1))!=int(m_data.Cols()-1))) { Print(__FUNCTION__, " error ", GetLastError()); return -1; } ArrayInitialize(w,0.0); loss_value = 0.0 ; for(ulong i=0 ; i<m_data.Rows() ; i++) m_indices[i] = int(i) ; for(int ibootstrap=0 ; ibootstrap<m_num_boot; ibootstrap++) { Comment(" Bootstrap iteration ", ibootstrap+1); ArrayInitialize(class_count,0); int ii, j, k, m; ii = int (m_data.Rows()) ; while(ii > 1) { m = int (m_data.Rows()) - ii ; if(m >= m_bootsize) break ; j = (int)(MathRandomUniform(0.0,1.0,rand_error) * ii) ; if(j >= ii) j = ii - 1 ; k = m_indices[m] ; m_indices[m] = m_indices[m+j] ; m_indices[m+j] = k ; --ii ; ++class_count[m_target_bin[m_indices[m]]] ; } for(int i=0 ; i<num_bins_target ; i++) { if(class_count[i] < 2) Print(__FUNCTION__, " class at ", i, " has less than 2 members. 考虑调整参数Frel(number of partitions or bootstrap sample size)"); } ArrayInitialize(m_trial_weights,0.0); ret += Optimize(m_trial_weights); loss_value += PowellsMethod::GetFret() ; for(ulong i=0 ; i<m_data.Cols()-1 ; i++) w[i] += m_trial_weights[i] ; } for(ulong i=0 ; i<m_data.Cols()-1; i++) w[i] /= double(m_num_boot) ; return ret ; }
权重估计从调用"calc_wt()"方法开始。在这里,进行自举抽样(bootstrap sampling),在优化初始权重之前对数据进行随机打乱。优化是通过父类方法"Optimize()"来执行的。每个自举抽样的最优权重被累加到"w[]"数组中,在退出"calc_wt()"方法之前,会对这些权重进行平均计算。
//+------------------------------------------------------------------+ //| function minimized by Powells optimization method | //+------------------------------------------------------------------+ virtual double func(const double& p[]) { double pen = 0.0 ; for(ulong i=0 ; i<m_data.Cols()-1 ; i++) { if(p[i] > 4.0) { m_work_weights[i] = exp(4.0) + p[i] - 4.0 ; pen += (p[i] - 4.0) * (p[i] - 4.0) ; } else if(p[i] < -3.0) { m_work_weights[i] = exp(-3.0) + p[i] + 3.0 ; pen += (p[i] + 3.0) * (p[i] + 3.0) ; } else m_work_weights[i] = exp(p[i]) ; } return (loss(m_work_weights) + pen) ; }
记住,被最小化的函数是损失函数,它由父类中被重写的一个方法“func()”表示。
//+------------------------------------------------------------------+ //| calculates the loss function | //+------------------------------------------------------------------+ double loss(double &w[]) { double totaloss = total_loss(w); totaloss/=double(m_data.Rows()); if(m_reg_factor>0.0) { for(ulong i=0; i<m_data.Cols()-1;i++) totaloss+=m_reg_factor*pow(w[i],2.0); } return totaloss; }
在“func()”方法中,调用了“loss()”方法,这进而触发了作为私有方法“total_loss()”实现的每个样本的损失函数的计算。
//+------------------------------------------------------------------+ //| loss over all data | //+------------------------------------------------------------------+ double total_loss(double &w[]) { int category,first, other ; double distance, top, bottom, loss ; loss = 0.0 ; for(int i=0; i<m_bootsize; i++) { other = m_indices[i] ; category = m_target_bin[other] ; top = bottom = DBL_MAX ; for(int iother=0 ; iother<m_bootsize; iother++) { first = m_indices[iother] ; if(first == other) continue ; distance = 0.0 ; for(ulong v=0 ; v<m_data.Cols()-1; v++) { distance += w[v] * fabs(m_data[other][v] - m_data[first][v]) ; } if(m_target_bin[first] == category) { if(distance < top) top = distance ; } else { if(distance < bottom) bottom = distance ; } } distance = top - bottom ; if(distance > 30.0) loss += distance ; else loss += log(1.0 + exp(distance)); } return loss ; }
在所有自举运算完成后,计算得到的平均最优权重会被写入用户提供的“weights[]”数组中。在按降序排序之前,会对权重进行转换,使它们的总和达到100,从而提高可读性。
使用FREL进行特征选择的示例
为了演示FREL算法,我们提供了“FrelExample.mq5”脚本。该脚本利用FREL来分析一个随机生成的包含候选变量和目标变量的数据集,以识别出最佳的预测变量。用户可以调整FREL算法的所有参数以及合成数据集的某些特性。这包括观测值的总数(num_observations)和候选预测变量的数量(num_candidate_predictors)。以下是展示脚本中用户可调整输入的片段:
//---user adjustable input parameters input int number_of_partitions = 8; //Number of partitions input double regularization_factor = 0.0; //Regularization factor input int number_of_bootstraps = 1; input int bootstrap_size = 0; input ulong num_observations = 2000;// Sample size of random dataset input ulong num_candidate_predictors = 10;// Maximum number of candidate predictors in dataset
脚本生成了一个具有num_observations行和num_candidate_predictors + 1列的随机数矩阵。最后一列被覆盖为索引为1、3、5和7的列之和,这一列作为数据集的目标变量。
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { srand(126); //---check user input parameters if(number_of_partitions<2 || num_observations<10 || num_candidate_predictors<8) { Print("Invalid input parameters"); return; } //---the data matrix dataset; //---initialize size of random dataset dataset.Init(num_observations,num_candidate_predictors+1); //---fill dataset with random data dataset.Random(1.0,10.0); //---set the target variable in the last column if(!dataset.Col(dataset.Col(1) + dataset.Col(3) + dataset.Col(5) + dataset.Col(7),num_candidate_predictors)) { Print("error ", GetLastError()); return; } //---initialize Frel object FREL frel(dataset,number_of_bootstraps,bootstrap_size); //---declare containers to recieve output from Frel operation double optimal_weights[]; int index[]; //--- ulong timeIT = GetTickCount64(); //---find the most relevant predictors if(!frel.WeighVars(number_of_partitions,regularization_factor,index,optimal_weights)) return; //---calculate runtime Print("Runtime of FREL ", GetTickCount64() - timeIT, " ms"); //---display results for(uint i = 0; i<optimal_weights.Size(); i++) Print("Predictor at Column index ", index[i], " weight ", optimal_weights[i]); } //+------------------------------------------------------------------+
目标是观察FREL是否能够恰当地为变量分配权重,特别是将第1、3、5和7列指定为与目标变量相关性最强的列。最初,我们使用默认参数执行脚本,注意到此时正则化被禁用,并且只指定了一次采样。
输出
ON 0 18:12:30.906 FrelExample (BTCUSD,D1) Runtime of FREL 273375 ms GD 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 7 weight 24.46987538756267 IH 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 3 weight 24.22319404776024 EL 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 5 weight 22.26820806768701 LP 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 1 weight 22.13748732798876 DD 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 0 weight 1.162036446785271 KK 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 8 weight 1.1532145209345603 RO 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 4 weight 1.1496286906955606 RS 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 2 weight 1.1472521997561425 NG 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 6 weight 1.14561384476096 DK 0 18:12:30.906 FrelExample (BTCUSD,D1) Predictor at Column index 9 weight 1.14348946606884
接下来,我们通过测试正则化程度为0.1和1.0的情况,来研究正则化对估计权重的影响。
输出
MQ 0 18:19:03.951 FrelExample (BTCUSD,D1) Runtime of FREL 331296 ms QD 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 3 weight 19.63442784832085 PK 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 5 weight 19.009699240770477 GO 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 7 weight 18.823288529399388 GQ 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 1 weight 18.18026689510982 NE 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 0 weight 4.106428447842871 KI 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 8 weight 4.075425288243113 OM 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 2 weight 4.070169243578418 MQ 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 6 weight 4.051103060690134 FE 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 9 weight 4.025271426001863 FJ 0 18:19:03.951 FrelExample (BTCUSD,D1) Predictor at Column index 4 weight 4.0239200200430805
输出
HP 0 18:25:43.421 FrelExample (BTCUSD,D1) Runtime of FREL 362984 ms FF 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 3 weight 10.353013480731704 JJ 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 7 weight 10.227015183302557 IM 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 5 weight 10.213781888319609 KQ 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 1 weight 10.079770794877978 PF 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 0 weight 9.948300319843046 QJ 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 8 weight 9.938367489770178 KN 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 2 weight 9.897336276433514 DQ 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 6 weight 9.79559491756489 EF 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 9 weight 9.774541742551756 CI 0 18:25:43.421 FrelExample (BTCUSD,D1) Predictor at Column index 4 weight 9.77227790660475
正则化测试的结果表明,权重开始分散到其他变量上,偏离了正确的变量。很可能,正则化程度的增加会导致权重之间的差异减小,从而难以区分有用变量和无关变量。
在检查我们测试的运行结果时,很明显FREL的运行速度相对较慢。瓶颈很可能在于“total_loss()”函数,因为优化器执行时需要多次遍历整个数据集。为了提高运行效率,我们采用较小的样本量进行了多次抽样。以下结果是从使用40个样本进行100次抽样运行中获得的。
输出
IN 0 18:30:55.441 FrelExample (BTCUSD,D1) Runtime of FREL 22985 ms OK 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 3 weight 18.706272752181135 OL 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 1 weight 18.32079620338284 RS 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 5 weight 18.194009676469012 HG 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 7 weight 16.298306686632337 MI 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 4 weight 5.838867272535404 LM 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 9 weight 5.249285089162589 FQ 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 8 weight 4.791606631149278 DE 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 6 weight 4.770223641360407 KI 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 0 weight 3.974977300216029 KM 0 18:30:55.441 FrelExample (BTCUSD,D1) Predictor at Column index 2 weight 3.855654746910961
结论
在本文中,我们介绍了使用基于正则化能量模型进行特征加权的MQL5实现。我们简要概述了该算法的理论基础,并在一个合成数据集上展示了其有效性。尽管结果很有前景,但我们发现该算法的计算成本很高,导致分析速度较慢。为解决这一问题,我们提出了使用较小的样本量进行多次自举抽样(bootstrap)的方法,这显著提高了算法的整体执行速度。此外,采用多线程或GPU加速也能极大地有助于我们的实现。尽管如此,我们还是鼓励对该方法感兴趣的人根据自己的需求对代码进行定制。本文包含了所有讨论过的代码,每个源文件都在下面的表格中进行了详细说明。
源文件 | 说明 |
---|---|
Mql5\include\np.mqh | 一个含各种向量和矩阵操作工具的头文件 |
Mql5\include\Powells.mqh | 包含实现函数最小化的Powell方法的PowellsMethod类的定义 |
Mql5\include\frel.mqh | 包含代表基于正则化能量的特征加权学习算法的FREL类的定义 |
MQL5 脚本 | 一个演示使用FREL类的脚本 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/14865
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



