您应该知道的 MQL5 向导技术(第 01 部分):回归分析
1. 概述
MQL5 向导允许快速构建和部署智能系统,并在 MQL5 函数库中针对交易的大部分枯燥层面预先进行了编码。 这就允许交易者专注于他们的交易定制层面,例如特殊的入场和离场条件。 函数库中包括一些入场和离场信号类,如“加速器振荡器”指标信号,或“自适应移动平均”指标,和许多其它信号。 对于大多数交易者来说,除了基于滞后指标,也许他们并无转换到成功策略的可能。 这就是为什么您自行创建自定义信号的能力是基本要素。 在本文中,我们将探讨如何利用回归分析来实现这一点。
2. 创建类
2.1 回归分析,根据维基百科,是一套统计过程,用于估算因变量和一个或多个自变量之间的关系。 由于价格数据是一个时间序列,因此它对交易者的智能系统号非常有用。 而这,就可拥有一种测试先前价格和价格变化如何影响未来价格和价格变动的能力。 回归分析可由以下方程表示 其中 y 是因变量,故此预测变量取决于先前的 x 值,其值每个都具有各自的系数 β,和误差 ε。我们可以将 x 值和 y 值分别想象为前期和预测的价格水平。 除了操控价格水平,还可以以类似的方式查验价格变化。未知的 y 依赖于 xs、βs,和 ε。 其中只有 xs 和 β0(y 截距)是已知的。 y-截距是已知的,因为它是此前的价格 x i 1。 我们因之需要找到 βs 相应的每个 x,然后是 ε。因为每个 x i 1 在时间序列的前一时间点 是一个 y i,我们可以用联立方程求解 β 的值。 例如,如果价格的下一次变化仅取决于两次先前的变化,则我们当前的方程可以是:
且前面的方程应是:
由于我们分别估算误差 ε,我们可以求解 β 值的两个联立方程。 维基百科方程中 x 值的编号不是 MQL5 “系列”格式,这意味着编号最高的 x 是最新的。 我因之针对上述两个方程中的 x 值进行了重新编号,以便展示它们如何能够同时出现。 同样,我们从 y-截距 xi1 和 xi0 开始,表示方程 1 中的 β0。 联立方程的求解可以更好地利用矩阵来处理,从而提高效率。 这方面的工具在 MQL5 函数库中已有。
2.2 MQL5 函数库有扩展的统计类集合,以及通用算法,显然不需要从头开始编写它们。 其代码也对公众开放,这意味着它可以独立检验。 出于我们的目的,我们将使用 “solvers.mqh” 中的类 “CDenseSolver” 里的 “RMatrixSolve” 函数。该函数的核心是使用矩阵 LU 分解快速有效地求解 β 值。 MetaQuotes 存档中已经撰写过关于这方面的文章,维基百科也在这里有所解释。
在我们深入研究求解 β 值之前,先看看 “CExpertSignal” 类是如何构造的,因为它是我们类的基础。 在几乎所有可在向导中组装的智能系统信号类中,都有一个 “LongCondition” 函数和一个 “ShortCondition” 函数。 正如您所期望的,这两个函数返回一个值,分别设置您应该做多亦或做空。 该值需要是 0 到 100 范围内的整数,以便与向导的输入参数 “Signal_ThresholdOpen” 和 “Signal-Threshold Close” 进行映射。 典型情况,当进行交易时,您所希望的平仓条件比开仓条件更为保守。 这意味着开仓阈值将高于平仓阈值。 因此,在开发我们的信号时,我们打算有输入参数来计算平仓阈值,以及单独但相似的输入参数来计算开仓阈值。 计算条件时所用的输入选择将取决于我们是否有持仓。 如果我们有持仓,我们将使用平仓参数。 如果没有持仓,则我们采用开仓参数。 显示这两组参数的智能系统信号类接口清单如下。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CSignalDUAL_RA : public CExpertSignal { protected: CiMA m_h_ma; // highs MA handle CiMA m_l_ma; // lows MA handle CiATR m_ATR; //--- adjusted parameters int m_size; double m_open_determination,m_close_determination; int m_open_collinearity,m_open_data,m_open_error; int m_close_collinearity,m_close_data,m_close_error; public: CSignalDUAL_RA(); ~CSignalDUAL_RA(); //--- methods of setting adjustable parameters //--- PARAMETER FOR SETTING THE NUMBER OF INDEPENDENT VARIABLES void Size(int value) { m_size=value; } //--- PARAMETERS FOR SETTING THE OPEN 'THRESHOLD' FOR THE EXPERTSIGNAL CLASS void OpenCollinearity(int value) { m_open_collinearity=value; } void OpenDetermination(double value) { m_open_determination=value; } void OpenError(int value) { m_open_error=value; } void OpenData(int value) { m_open_data=value; } //--- PARAMETERS FOR SETTING THE CLOSE 'THRESHOLD' FOR THE EXPERTSIGNAL CLASS void CloseCollinearity(int value) { m_close_collinearity=value; } void CloseDetermination(double value) { m_close_determination=value; } void CloseError(int value) { m_close_error=value; } void CloseData(int value) { m_close_data=value; } //--- method of verification of settings virtual bool ValidationSettings(void); //--- method of creating the indicator and timeseries virtual bool InitIndicators(CIndicators *indicators); //--- methods for detection of levels of entering the market virtual bool OpenLongParams(double &price,double &sl,double &tp,datetime &expiration); virtual bool OpenShortParams(double &price,double &sl,double &tp,datetime &expiration); //--- methods of checking if the market models are formed virtual int LongCondition(void); virtual int ShortCondition(void); protected: //--- method of initialization of the oscillator bool InitRA(CIndicators *indicators); //--- methods of getting data int CheckDetermination(int ind,bool close); double CheckCollinearity(int ind,bool close); // double GetY(int ind,bool close); double GetE(int ind,bool close); double Data(int ind,bool close); // };
此外,这里列出了我们的 “LongCondition” 和 “ShortCondition” 函数。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CSignalDUAL_RA::LongCondition(void) { int _check=CheckDetermination(0,PositionSelect(m_symbol.Name())); if(_check>0){ return(_check); } return(0); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CSignalDUAL_RA::ShortCondition(void) { int _check=CheckDetermination(0,PositionSelect(m_symbol.Name())); if(_check<0){ return((int)fabs(_check)); } return(0); }
为了继续贯彻,并求解 β 值,我们将调用 “GetY” 函数。 如下清单。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CSignalDUAL_RA::GetY(int ind,bool close) { double _y=0.0; CMatrixDouble _a;_a.Resize(m_size,m_size); double _b[];ArrayResize(_b,m_size);ArrayInitialize(_b,0.0); for(int r=0;r<m_size;r++) { _b[r]=Data(r,close); for(int c=0;c<m_size;c++) { _a[r].Set(c,Data(r+c+1, close)); } } int _info=0; CDenseSolver _S; CDenseSolverReport _r; double _x[];ArrayResize(_x,m_size);ArrayInitialize(_x,0.0); _S.RMatrixSolve(_a,m_size,_b,_info,_r,_x); for(int r=0;r<m_size;r++) { _y+=(Data(r,close)*_x[r]); } //--- return(_y); }
所引用的 “Data” 函数将在所交易品种收盘价变化或相同收盘价的移动平均值变化之间切换。 所用选项将由 “m_open_data” 输入参数,或 “m_close_data” 输入参数定义,具体则取决于我们计算的是开仓阈值还是平仓阈值。 所选数据的列表显示在如下的枚举中。
enum Edata { DATA_TREND=0, // changes in moving average close DATA_RANGE=1 // changes in close };
选择此数据项的 “Data” 函数如下所示。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CSignalDUAL_RA::Data(int ind,bool close) { if(!close) { if(Edata(m_open_data)==DATA_TREND) { m_h_ma.Refresh(-1); return((m_l_ma.Main(StartIndex()+ind)-m_l_ma.Main(StartIndex()+ind+1))-(m_h_ma.Main(StartIndex()+ind)-m_h_ma.Main(StartIndex()+ind+1))); } else if(Edata(m_open_data)==DATA_RANGE) { return((Low(StartIndex()+ind)-Low(StartIndex()+ind+1))-(High(StartIndex()+ind)-High(StartIndex()+ind+1))); } } else if(close) { if(Edata(m_close_data)==DATA_TREND) { m_h_ma.Refresh(-1); return((m_l_ma.Main(StartIndex()+ind)-m_l_ma.Main(StartIndex()+ind+1))-(m_h_ma.Main(StartIndex()+ind)-m_h_ma.Main(StartIndex()+ind+1))); } else if(Edata(m_close_data)==DATA_RANGE) { return((Low(StartIndex()+ind)-Low(StartIndex()+ind+1))-(High(StartIndex()+ind)-High(StartIndex()+ind+1))); } } return(0.0); }
一旦我们得到 β 值,我们就可以继续估算误差。
2.3 标准误差根据维基百科可以用下面的公式估算。
以 s 作为标准偏差,n 为样本大小,该误差作为一个清醒的提醒,即并非所有预测,无所谓多么努力,在所有时间都能始终 100% 准确。 我们应该始终考虑到这一点,并期望我们会有一些误差。 公式中所示的标准偏差将在我们的预测值和实际值之间进行测量。 出于比较目的,我们还可以查看无修正误差,譬如在我们的预测和实际之间的最后一个差异。 这两个选项可以从下面的枚举中选择。
enum Eerror { ERROR_LAST=0, // use the last error ERROR_STANDARD=1 // use standard error }
调用上述公式时,“GetE” 函数将根据输入参数 “m_open_error” 或 “m_ close_errors” 返回我们的估算误差。 如下清单。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CSignalDUAL_RA::GetE(int ind,bool close) { if(!close) { if(Eerror(m_open_error)==ERROR_STANDARD) { double _se=0.0; for(int r=0;r<m_size;r++) { _se+=pow(Data(r,close)-GetY(r+1,close),2.0); } _se=sqrt(_se/(m_size-1)); _se=_se/sqrt(m_size); return(_se); } else if(Eerror(m_open_error)==ERROR_LAST){ return(Data(ind,close)-GetY(ind+1,close)); } } else if(close) { if(Eerror(m_close_error)==ERROR_STANDARD) { double _se=0.0; for(int r=0;r<m_size;r++){ _se+=pow(Data(r,close)-GetY(r+1,close),2.0); } _se=sqrt(_se/(m_size-1)); _se=_se/sqrt(m_size); return(_se); } else if(Eerror(m_close_error)==ERROR_LAST){ return(Data(ind,close)-GetY(ind+1,close)); } } //--- return(Data(ind,close)-GetY(ind+1,close)); }
再来一次,“m_open_error” 或 “m_close_error” 将取决于我们是否有持仓来选择。 一旦我们有了估算误差,我们就应该能够对 y 进行大致的预测。 然而,回归分析有许多缺陷。 其中一个缺陷是自变量过于相似,从而预测值过度膨胀。 这种现象称为共线性,值得检查。
2.4 维基百科在此定义的共线性,可以推测为根据 Investopedia,多元回归模型中两个或多个自变量之间的高度相互关系。它本身没有公式,且由方差膨胀系数(VIF)检测。 在所有自变量(x)中测量该因子,从而帮助了解这些变量在预测 y 时的独特性。 由下面的公式给出,其中 R 是每个自变量相对于其它自变量的回归。
为我们的目的贯彻,在考虑共线性时,我们将取两组最近的自变量数据集之间的 spearman 相关性的倒数,并将其归一化。 我们的数据集长度将由输入参数 “m_size” 设置,最小长度为 3。 通过归一化,我们将简单地从 2 中减去它,并反转结果。 然后,可以将该归一化的权重乘以误差估算值或预测值,或两者,或都不用。 在以下枚举里列出了这些选项。
enum Echeck { CHECK_Y=0, // check for y only CHECK_E=1, // check for the error only CHECK_ALL=2, // check for both the y and the error CHECK_NONE=-1 // do not use collinearity checks };
所应用权重的选择也由输入参数 “m_open_collinearity” 或 “m_close_collinearity” 设置。同样,取决于是否有持仓。 ‘CheckCollinearity’ 清单给出如下。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CSignalDUAL_RA::CheckCollinearity(int ind,bool close) { double _check=0.0; double _c=0.0,_array_1[],_array_2[],_r=0.0; ArrayResize(_array_1,m_size);ArrayResize(_array_2,m_size); ArrayInitialize(_array_1,0.0);ArrayInitialize(_array_2,0.0); for(int s=0; s<m_size; s++) { _array_1[s]=Data(ind+s,close); _array_2[s]=Data(m_size+ind+s,close); } _c=1.0/(2.0+fmin(-1.0,MathCorrelationSpearman(_array_1,_array_2,_r))); double _i=Data(m_size+ind,close), //y intercept _y=GetY(ind,close), //product sum of x and its B coefficients _e=GetE(ind,close); //error if(!close) { if(Echeck(m_open_collinearity)==CHECK_Y){ _check=_i+(_c*_y)+_e; } else if(Echeck(m_open_collinearity)==CHECK_E){ _check=_i+_y+(_c*_e); } else if(Echeck(m_open_collinearity)==CHECK_ALL){ _check=_i+(_c*(_y+_e)); } else if(Echeck(m_open_collinearity)==CHECK_NONE){ _check=_i+(_y+_e); } } else if(close) { if(Echeck(m_close_collinearity)==CHECK_Y){ _check=_i+(_c*_y)+_e; } else if(Echeck(m_close_collinearity)==CHECK_E){ _check=_i+_y+(_c*_e); } else if(Echeck(m_close_collinearity)==CHECK_ALL){ _check=_i+(_c*(_y+_e)); } else if(Echeck(m_close_collinearity)==CHECK_NONE){ _check=_i+(_y+_e); } } //--- return(_check); }
除了检查共线性外,有时回归分析的预测性会与市场的外部变化不符。 为了跟踪这一点,并测量信号自变量影响因变量(预测)的能力,我们用到判定系数。
2.5 判定系数是一种统计测量,当根据 Investopedia 预测给定事件的结果时,检验一个变量的差异如何由第二变量的差异解释。 维基百科还提供了一个更详尽的定义,下面所示的公式取自该处。
平方和的公式(y 为真实值,f 为预测值),
总和的公式(y 为真实值,ÿ 为这些值的移动平均值),
最后,系数本身也引用为 R 平方。
该系数的作用是测量我们的 xs 对 y 的影响程度。 这一点很重要,因为如前所述,在某些区间,回归衰退意味着远离市场会更安全。 通过过滤器监测,当系统可靠时,我们更有可能进行交易。 典型情况,您希望该系数大于 0,如 1 是理想值。 定义阈值时所用的输入参数是 “m_open_determination” 或 “m_close_determination”,再次取决于持仓的数量。 如果由下面列出的 “CheckDetermination” 函数计算出的判定系数小于该参数,则做多或做空条件将返回零。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CSignalDUAL_RA::CheckDetermination(int ind,bool close) { int _check=0; m_h_ma.Refresh(-1);m_l_ma.Refresh(-1); double _det=0.0,_ss_res=0.0,_ss_tot=0.0; for(int r=0;r<m_size;r++) { _ss_res+=pow(Data(r,close)-GetY(r+1,close),2.0); _ss_tot+=pow(Data(r,close)-((m_l_ma.Main(r)-m_l_ma.Main(r+1))-(m_h_ma.Main(r)-m_h_ma.Main(r+1))),2.0); } if(_ss_tot!=0.0) { _det=(1.0-(_ss_res/_ss_tot)); if(_det>=m_open_determination) { double _threshold=0.0; for(int r=0; r<m_size; r++){ _threshold=fmax(_threshold,fabs(Data(r,close))); } double _y=CheckCollinearity(ind,close); _check=int(round(100.0*_y/fmax(fabs(_y),fabs(_threshold)))); } } //--- return(_check); }
一旦我们能够检查判定系数,我们将有一个可行的信号。 接下来将在 MQL5 向导中将此信号组装到智能系统当中。
3. 使用 MQL5 向导组装
3.1 自定义辅助代码列表可与 MQL5 向导中的代码一起用来组装智能系统。 这是完全可选的,取决于交易者的风格。 出于本文中目的,我们打算研究基于品种的主要 ATR 放置自定义挂单,以及基于相同指标尾随持仓。 我们不打算使用目标止盈。
3.1.1 基于 ATR 的挂单可以通过重载函数 “OpenLongParams” 和 “OpenShortParams”,并在我们的信号类中自定义它们来设置,如下所示。
//+------------------------------------------------------------------+ //| Detecting the levels for buying | //+------------------------------------------------------------------+ bool CSignalDUAL_RA::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration) { CExpertSignal *general=(m_general!=-1) ? m_filters.At(m_general) : NULL; //--- if(general==NULL) { m_ATR.Refresh(-1); //--- if a base price is not specified explicitly, take the current market price double base_price=(m_base_price==0.0) ? m_symbol.Ask() : m_base_price; //--- price overload that sets entry price to be based on ATR price =m_symbol.NormalizePrice(base_price-(m_price_level*(m_ATR.Main(0)/m_symbol.Point()))*PriceLevelUnit()); sl =0.0; tp =0.0; expiration+=m_expiration*PeriodSeconds(m_period); return(true); } //--- return(general.OpenLongParams(price,sl,tp,expiration)); } //+------------------------------------------------------------------+ //| Detecting the levels for selling | //+------------------------------------------------------------------+ bool CSignalDUAL_RA::OpenShortParams(double &price,double &sl,double &tp,datetime &expiration) { CExpertSignal *general=(m_general!=-1) ? m_filters.At(m_general) : NULL; //--- if(general==NULL) { m_ATR.Refresh(-1); //--- if a base price is not specified explicitly, take the current market price double base_price=(m_base_price==0.0) ? m_symbol.Bid() : m_base_price; //--- price overload that sets entry price to be based on ATR price =m_symbol.NormalizePrice(base_price+(m_price_level*(m_ATR.Main(0)/m_symbol.Point()))*PriceLevelUnit()); sl =0.0; tp =0.0; expiration+=m_expiration*PeriodSeconds(m_period); return(true); } //--- return(general.OpenShortParams(price,sl,tp,expiration)); }
MQL5 向导生成的智能系统拥有输入参数 “Signal_PriceLevel”。 默认情况下,它为零,但如果分配了一个值,则表示交易品种的价格,与放置市价订单的当前价格之间的点数距离。 当输入为负值时,则会放置破位挂单。 当为正值时,放置限价挂单。它是一种双精度数据类型。 出于我们的目的,该输入将是一个分数,或 ATR 当前价格点数的乘数。
3.1.2 ATR 尾随类也是一个定制的 “CExpertTrailing” 类,也采用 ATR 来设置,并移动止损价位。 其关键函数的实现如下所列。
//+------------------------------------------------------------------+ //| Checking trailing stop and/or profit for long position. | //+------------------------------------------------------------------+ bool CTrailingATR::CheckTrailingStopLong(CPositionInfo *position,double &sl,double &tp) { //--- check if(position==NULL) return(false); //--- m_ATR.Refresh(-1); double level =NormalizeDouble(m_symbol.Bid()-m_symbol.StopsLevel()*m_symbol.Point(),m_symbol.Digits()); //--- sl adjustment to be based on ATR double new_sl=NormalizeDouble(level-(m_atr_weight*(m_ATR.Main(0)/m_symbol.Point())),m_symbol.Digits()); double pos_sl=position.StopLoss(); double base =(pos_sl==0.0) ? position.PriceOpen() : pos_sl; //--- sl=EMPTY_VALUE; tp=EMPTY_VALUE; if(new_sl>base && new_sl<level) sl=new_sl; //--- return(sl!=EMPTY_VALUE); } //+------------------------------------------------------------------+ //| Checking trailing stop and/or profit for short position. | //+------------------------------------------------------------------+ bool CTrailingATR::CheckTrailingStopShort(CPositionInfo *position,double &sl,double &tp) { //--- check if(position==NULL) return(false); //--- m_ATR.Refresh(-1); double level =NormalizeDouble(m_symbol.Ask()+m_symbol.StopsLevel()*m_symbol.Point(),m_symbol.Digits()); //--- sl adjustment to be based on ATR double new_sl=NormalizeDouble(level+(m_atr_weight*(m_ATR.Main(0)/m_symbol.Point())),m_symbol.Digits()); double pos_sl=position.StopLoss(); double base =(pos_sl==0.0) ? position.PriceOpen() : pos_sl; //--- sl=EMPTY_VALUE; tp=EMPTY_VALUE; if(new_sl<base && new_sl>level) sl=new_sl; //--- return(sl!=EMPTY_VALUE); }
“m_atr_weight” 将再次成为一个可优化参数,就像 “m_price_level” 一样,它设置了我们可以尾随持仓的接近程度。
3.2 向导组装将以直截了当的方式完成,唯一值得注意的阶段是选择我们的信号,如下所示。
以及添加如下所示的自定义尾随方法。
4. 在策略测试器里测试
4.1 编译是 MQL5 向导中组装之后的操作,从而创建智能系统文件,并确认代码中没有错误。
4.2 还需要在策略测试器的输入选项卡中设置智能系统的默认输入。 这里的关键是确保 “Signal_TakeLevel” 和 “Signal_StopLevel” 设置为零。 这是因为如上所述,在本文中,离场仅由尾随停止或 “Signal_ThresholdClose” 输入参数定义。
4.3 优化应在理想情况下依据您打算交易的经纪商的实际报价执行。 在本文中,我们将在 2018 年 1 月 1 日至 2021 年 1 月 01 日的 V 型区间内,时间帧为 4-小时,优化 EURUSD。 为了比较目的,我们将运行两个优化:第一个优化只用市价订单,而第二个优化将开立挂单。 我采用“开立”挂单,因为我们仍然考虑会仅用市价订单的选项,因为 “Signal_PriceLevel” 可以为零,因为优化是从负值到正值。 优化可以设置为优先于使用挂单的选项,如下所示。 此选项与不用挂单选项之间的唯一区别在于,后者将 “Signal_PriceLevel” 输入参数保留为 0,而非优化输入的一部分。
4.4 来自我们优化的结果如下所示。 首先是报告和净值曲线的最佳结果,来自只用市价订单交易。
报告部分 1,
然后采用挂单生成类似的报告和曲线。
报告部分 2,
我们的回归分析似乎表明,采用挂单的好处在于,在牺牲部分利润的情况下减少了回撤。 还可以进行其它修改来增强该系统,例如更改尾随停止类别,或资金管理类型。 出于测试目的,我们采用固定保证金百分比,并将优化标准设置为“复杂标准”。 在部署之前,最好尽可能广泛地基于历史跳价数据测试,并进行充分的前向验证测试,不过这超出了本文的范畴。
5. 结束语
5.1 MQL5 向导显然是一个足智多谋的工具,每位交易者都应在自己的武器库中装备。 我们在这里研究的是如何采用回归分析的一些统计概念,如共线性和判定系数,并将其作为稳健交易系统的基础。 下一步将是基于历史报价数据进行广泛测试,并探索该信号是否可与基于交易者经验,或 MQL5 函数库中内置信号的其它独特信号配对,从而提供更全面的交易系统。 与本系列文章中的情况一样,本文并非旨在提供一个圣杯,而是一个可以调整的过程,从而能更好地符合交易者应对市场的方式。 感谢阅读!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/11066