优化结果的可视化评估
概述
自定义优化准则为优化智能交易系统提供了非常方便的设置。 但如果我们需要检查多个条件,我们则应运行多个优化,而这可能需要花费大量时间。 更好的解决方案是能够在一次优化过程中测试多个自定义准则。 甚而,最好能够即时查看余额和净值图形。
有多种可视化选项总是好的。 我们的大脑通过眼睛接收的信息超过 80%。 故此,在本文中,我们将研究创建优化图形,及选择最优定制准则的方法。
我们还将看到如何利用网站上发表的文章和论坛评论,在几乎不了解 MQL5 的情况下创建所需的解决方案。
格式化任务
- 收集每次优化通测的数据。
- 为每次优化通测构建余额/净值图。
- 计算若干自定义优化准则。
- 按照自定义优化准则针对图形按升序进行排序。
- 显示所有自定义准则的最优结果。
问题解决步骤
由于我们必须修改智能交易系统的代码,然而我们要尽量减少这些修改。
- 因此,整个数据收集代码将在单独的包含文件 SkrShotOpt.mqh 中实现,而自定义准则将在 CustomCriterion.mqh 文件中计算。
- ScreenShotOptimization.mq5 屏幕截图将绘制图形,并保存屏幕截图。
因此,我们只需要在智能交易系统中添加几行代码。
1. 收集数据。 SkrShotOpt.mqh
净值的最大值和最小值将在 OnTick() 函数里写入。
double _Equity = AccountInfoDouble(ACCOUNT_EQUITY); if(tempEquityMax < _Equity) tempEquityMax = _Equity; if(tempEquityMin > _Equity) tempEquityMin = _Equity;
为了避免每次即时报价来临时都要检查持仓变化,我们将在 OnTradeTransaction() 函数中跟踪持仓变化
void IsOnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request, const MqlTradeResult & result) { if(trans.type == TRADE_TRANSACTION_DEAL_ADD) if(HistoryDealSelect(trans.deal)) { if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY) _deal_entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY); if(trans.deal_type == DEAL_TYPE_BUY || trans.deal_type == DEAL_TYPE_SELL) if(_deal_entry == DEAL_ENTRY_IN || _deal_entry == DEAL_ENTRY_OUT || _deal_entry == DEAL_ENTRY_INOUT || _deal_entry == DEAL_ENTRY_OUT_BY) allowed = true; } }
当开仓成交的数量发生变化时,填写余额和净值数组。
if(allowed) // if there was a trade { double accBalance = AccountInfoDouble(ACCOUNT_BALANCE); double accEquity = AccountInfoDouble(ACCOUNT_EQUITY); ArrayResize(balance, _size + 1); ArrayResize(equity, _size + 1); balance[_size] = accBalance; if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY) // if a new position appeared equity[_size] = accEquity; else // if position closed { if(changesB < accBalance) equity[_size] = tempEquityMin; else switch(s_view) { case min_max_E: equity[_size] = tempEquityMax; break; default: equity[_size] = tempEquityMin; break; } tempEquityMax = accEquity; tempEquityMin = accEquity; } _size = _size + 1; changesPos = PositionsTotal(); changesB = accBalance; _deal_entry = -1; allowed = false; }
含有帧数据的文件大小是有限的。 如果有多笔交易,文件大小会增加,处理起来会很困难。 因此,只应向其写入最必要的信息。
交易开始时,写入余额和净值数额:
- 交易完成后,如果成交以亏损了结,则写入最大净值数额
- 如果成交以盈利了结,则写入最少净值数额。
因此,几乎所有成交都有四个数组值:开盘时的余额和净值、收盘时的余额和最大/最小净值。
也许会出现在一次即时报价期间,一笔持仓被平仓,然后另开一笔新仓的情况。 在此情况下,只会写入一笔持仓。 这不会影响图形的可视化,同时大大减少了数组。
将收集的数据保存到文件
收集可盈利的优化通测才有意义。 此参数在设置中实现,因此,如果您有需要,可以另外登记亏损通测。 至于前向通测,它们也都会有记录。
在每次测试结束时,会触发测试器事件,此时利用 FrameAdd() 函数,把收集到的数据将写入一个文件。 而测试器事件由 OnTester() 函数处理。
bool FrameAdd( const string name, // public name/tag long id, // public id double value, // value const void& data[] // array of any type );
此处提供了一个关于如何使用 FrameAdd() 函数的详细且清晰的示例:https://www.mql5.com/ru/forum/11277/page4#comment_469771
由于 FrameAdd() 只能写入一个数组和一个数值,但除了余额和净值之外,传递 ENUM_STATISTICS 枚举的数值之一也很不错,数据将按顺序写入一个数组,而数组大小将写入所传递的 “value” 数值。
if(id == 1) // if it is a backward pass { // if profit % and the number of trades exceed those specified in the settings, the pass is written into the file if(TesterStatistics(STAT_PROFIT) / TesterStatistics(STAT_INITIAL_DEPOSIT) * 100 > _profit && TesterStatistics(STAT_TRADES) >= trades) { double TeSt[42]; // total number of elements in the ENUM_STATISTICS enumeration is 41 IsRecordStat(TeSt); // writing testing statistics to the array IsCorrect(); // adjusting balance and equity arrays if(m_sort != none) { while((sort)size_sort != none) size_sort++; double LRB[], LRE[], coeff[]; Coeff = Criterion(balance, equity, LRB, LRE, TeSt, coeff, 3);// calculating custom criterion ArrayInsert(balance, equity, _size + 1, 0); // joining balance and equity arrays into one ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add to the resulting array the array with the ENUM_STATISTICS data FrameAdd(name, id, _size + 1, balance); // write the frame into the file } else { ArrayInsert(balance, equity, _size + 1, 0); // joining balance and equity arrays into one ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add to the resulting array the array with the ENUM_STATISTICS data FrameAdd(name, id, _size + 1, balance); // write the frame into the file } } }
前向通测的处理方式与回测类似,但实际上它们是优化的推测。 这就是为什么只写入它们的余额和净值,而非写入 ENUM_STATISTICS 值。
如果在测试结束时仍有持仓,测试器会将其平仓。
这意味着,如果存储持仓数量的变量在测试结束时与实际不符,我们需要一笔虚拟成交了结(写入当前余额和净值)。
void IsCorrect() { if(changesPos > 0) // if there is an open position by the testing end time, it should be virtually closed as the tester will close such a position { _size++; ArrayResize(balance, _size + 1); ArrayResize(equity, _size + 1); if(balance[_size - 2] > AccountInfoDouble(ACCOUNT_BALANCE)) { balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE); switch(s_view) { case min_max_E: equity[_size - 1] = tempEquityMax; break; default: equity[_size - 1] = tempEquityMin; break; } } else { balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE); equity[_size - 1] = tempEquityMin; } balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE); equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY); } else { ArrayResize(balance, _size + 1); ArrayResize(equity, _size + 1); balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE); equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY); } }
数据写入在此处完成。
从文件中读取数据。 ScreenShotOptimization.mq5
优化之后,将在以下路径创建一个含有帧数据的文件:C:\Users\user name\AppData\Roaming\MetaQuotes\Terminal\terminal ID\MQL5\Files\Tester。 文件名为 EA_name.symbol.timeframe.mqd。 优化之后不能立即访问该文件。 但如果您重新启动终端,可用常规文件函数访问该文件。
该文件可在 C:\Users\user name\AppData\Roaming\MetaQuotes\Terminal\terminal ID\MQL5\Files\Tester 处找到。
int count = 0; long search_handle = FileFindFirst("Tester\\*.mqd", FileName); do { if(FileName != "") count++; FileName = "Tester\\" + FileName; } while(FileFindNext(search_handle, FileName)); FileFindClose(search_handle);
首先,读入结构。
FRAME Frame = {0}; FileReadStruct(handle, Frame); struct FRAME { ulong Pass; long ID; short String[64]; double Value; int SizeOfArray; long Tmp[2]; void GetArrayB(int handle, Data & m_FB) { ArrayFree(m_FB.Balance); FileReadArray(handle, m_FB.Balance, 0, (int)Value); ArrayFree(m_FB.Equity); FileReadArray(handle, m_FB.Equity, 0, (int)Value); ArrayFree(m_FB.TeSt); FileReadArray(handle, m_FB.TeSt, 0, (SizeOfArray / sizeof(m_FB.TeSt[0]) - (int)Value * 2)); } void GetArrayF(int handle, Data & m_FB, int size) { FileReadArray(handle, m_FB.Balance, size, (int)Value); FileReadArray(handle, m_FB.Equity, size, (int)Value); } };
在 FRAME 结构函数中,填充 Data 结构函数,并从中构造进一步的图表。
struct Data { ulong Pass; long id; int size; double Balance[]; double Equity[]; double LRegressB[]; double LRegressE[]; double coeff[]; double TeSt[]; }; Data m_Data[];
由于绘制数千个屏幕截图非常耗时,因此在脚本设置中指定一个参数,以便在利润低于指定百分比时禁用保存屏幕截图。
含有帧数据的文件在循环中进行处理。
为了绘制图形,我们需要一个数据数组。 因此,最先写入的是所有符合最低盈利百分比准则的回测结果。
然后迭代所有回测,并根据回测编号为它们选择相应的回测。 前向通测余额数组也会被添加到回测余额数组当中。
该解决方案可以绘制两种类型的图形。 其中一个类似于策略测试器中的图形,即前向通测从初始本金开始。
前向通测的第二种变体从回测结束时的资金额开始。 在这种情况下,回测利润值被添加到前向通测余额和净值中,并写入回测数组的末尾。
当然,这只在前向通测优化执行期间才能实现。
int handle = FileOpen(FileName, FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_BIN); if(handle != INVALID_HANDLE) { FileSeek(handle, 260, SEEK_SET); while(Res && !IsStopped()) { FRAME Frame = {0}; // read from the file to the Frame structure Res = (FileReadStruct(handle, Frame) == sizeof(Frame)); if(Res) if(Frame.ID == 1) // if it is a Backward pass, write data to the m_Data structure { ArrayResize(m_Data, size + 1); m_Data[size].Pass = Frame.Pass; m_Data[size].id = Frame.ID; m_Data[size].size = (int)Frame.Value; Frame.GetArrayB(handle, m_Data[size]); // write data to the m_Data structure arrays // if profit of this pass corresponds to the input settings, immediately calculate optimization criteria if(m_Data[size].TeSt[STAT_PROFIT] / m_Data[size].TeSt[STAT_INITIAL_DEPOSIT] * 100 >= profitPersent) { Criterion(m_Data[size].Balance, m_Data[size].Equity, m_Data[size].LRegressB, m_Data[size].LRegressE, m_Data[size].TeSt, m_Data[size].coeff, m_lineR); size++; } } else // if it is a Forward pass, write to the end of the m_Data data structures if(m_Forward != BackOnly) // if drawing of only Backward passes is not selected in settings for(int i = 0; i < size; i++) { if(Frame.Pass == m_Data[i].Pass) // if Back and Forward pass numbers match { int m = 0; if(m_Forward == Back_Next_Forward) // if selected drawing of Forward graph as a continuation of Backward { Frame.GetArrayF(handle, m_Data[i], m_Data[i].size - 1); // write data at the end of the the m_Data structure array, with a one-trade shift for(int x = m_Data[i].size - 1; x < m_Data[i].size + (int)Frame.Value - 1; x++) { m_Data[i].Balance[x] = m_Data[i].Balance[x] + m_Data[i].TeSt[STAT_PROFIT]; // add profit of the Backward test to the Forward pass m_Data[i].Equity[x] = m_Data[i].Equity[x] + m_Data[i].TeSt[STAT_PROFIT]; } m = 1; } else Frame.GetArrayF(handle, m_Data[i], m_Data[i].size); // if drawing of a Forward pass from a starting balance is selected m_Data[i].coeff[Forward_Trade] = (int)(Frame.Value / 2); // number of forward trades (not exact)) m_Data[i].coeff[Profit_Forward] = m_Data[i].Balance[m_Data[i].size + (int)Frame.Value - m - 1] - m_Data[i].Balance[m_Data[i].size - m]; break; } if(i == size - 1) // if no Backward is found for this Forward pass, move the file pointer to the end of writing FileSeek(handle, Frame.SizeOfArray, SEEK_CUR); // of this frame as if we read array data from the file } } FileClose(handle); //---
构建图形
图形绘制函数。
string _GraphPlot(double& y1[], double& y2[], double& LRegressB[], double& LRegressE[], double& coeff[], double& TeSt[], ulong pass) { CGraphic graphic; //--- create graphic bool res = false; if(ObjectFind(0, "Graphic") >= 0) res = graphic.Attach(0, "Graphic"); else res = graphic.Create(0, "Graphic", 0, 0, 0, _width, _height); if(!res) return(NULL); graphic.BackgroundMain(FolderName); // print the Expert Advisor name graphic.BackgroundMainSize(FontSet + 1); // font size for the Expert Advisor name graphic.IndentLeft(FontSet); graphic.HistoryNameSize(FontSet); // font size for the line names graphic.HistorySymbolSize(FontSet); graphic.XAxis().Name("pass " + IntegerToString(pass)); // show the pass number along the X axis graphic.XAxis().NameSize(FontSet + 1); graphic.XAxis().ValuesSize(12); // price font size graphic.YAxis().ValuesSize(12); //--- add curves CCurve *curve = graphic.CurveAdd(y1, ColorToARGB(clrBlue), CURVE_POINTS_AND_LINES, "Balance"); // plot the balance graph curve.LinesWidth(widthL); // graph line width curve.PointsSize(widthL + 1); // size of dots on the balance graph CCurve *curve1 = graphic.CurveAdd(y2, ColorToARGB(clrGreen), CURVE_LINES, "Equity"); // plot the equity graph curve1.LinesWidth(widthL); int size = 0; switch(m_lineR) // plot the regression line { case lineR_Balance: // balance regression line { size = ArraySize(LRegressB); CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance"); curve2.LinesWidth(widthL); } break; case lineR_Equity: // equity regression line { size = ArraySize(LRegressE); CCurve *curve2 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity"); curve2.LinesWidth(widthL); } break; case lineR_BalanceEquity: // balance and equity regression line { size = ArraySize(LRegressB); CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance"); curve2.LinesWidth(widthL); CCurve *curve3 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity"); curve2.LinesWidth(widthL); } break; default: break; } //--- plot curves graphic.CurvePlotAll(); // Important!!! All lines and captions must be created after creating the graph; otherwise, the graph will override them if(size == 0) { size = ArraySize(LRegressE); if(size == 0) size = ArraySize(LRegressB); } int x1 = graphic.ScaleX(size - 1); //Scales the value of the number of trades along the X axis graphic.LineAdd(x1, 30, x1, _height - 45, ColorToARGB(clrBlue), LINE_END_BUTT); // construct the vertical line denoting the end of the Backward period string txt = ""; int txt_x = 70;// text indent along the X axis int txt_y = 30;// text indent along the Y axis graphic.FontSet("Arial", FontSet);// Set current font parameters for(int i = 0; i < size_sort; i++) // Write all coefficients and criteria on the chart { if(coeff[i] == 0) continue; if(i == 1 || i == 3) txt = StringFormat("%s = %d", EnumToString((sort)i), (int)coeff[i]); else if(i == 0 || i == 2) txt = StringFormat("%s = %.2f", EnumToString((sort)i), coeff[i]); else txt = StringFormat("%s = %.4f", EnumToString((sort)i), coeff[i]); graphic.TextAdd(txt_x, txt_y + FontSet * i, txt, ColorToARGB(clrGreen)); } txt_y = txt_y + FontSet * (size_sort - 1); txt = StringFormat("Profitability = %.2f", TeSt[STAT_PROFIT_FACTOR]); graphic.TextAdd(txt_x, txt_y + FontSet, txt, ColorToARGB(clrGreen)); txt = StringFormat("Expected payoff = %.2f", TeSt[STAT_EXPECTED_PAYOFF]); graphic.TextAdd(txt_x, txt_y + FontSet * 2, txt, ColorToARGB(clrGreen)); graphic.Update(); //--- return resource name return graphic.ChartObjectName(); }
您可以从以下文章中了解有关如何使用 CGraphic 的更多信息:
图表截图保存到文件目录下的单独文件夹当中。 文件夹名为 EA_name.symbol.timeframe。
bool BitmapObjectToFile(const string ObjName, const string _FileName, const bool FullImage = true) { if(ObjName == "") return(true); const ENUM_OBJECT Type = (ENUM_OBJECT)ObjectGetInteger(0, ObjName, OBJPROP_TYPE); bool Res = (Type == OBJ_BITMAP_LABEL) || (Type == OBJ_BITMAP); if(Res) { const string Name = __FUNCTION__ + (string)MathRand(); ObjectCreate(0, Name, OBJ_CHART, 0, 0, 0); ObjectSetInteger(0, Name, OBJPROP_XDISTANCE, -5e3); const long chart = ObjectGetInteger(0, Name, OBJPROP_CHART_ID); Res = ChartSetInteger(chart, CHART_SHOW, false) && ObjectCreate(chart, Name, OBJ_BITMAP_LABEL, 0, 0, 0) && ObjectSetString(chart, Name, OBJPROP_BMPFILE, ObjectGetString(0, ObjName, OBJPROP_BMPFILE)) && (FullImage || (ObjectSetInteger(chart, Name, OBJPROP_XSIZE, ObjectGetInteger(0, ObjName, OBJPROP_XSIZE)) && ObjectSetInteger(chart, Name, OBJPROP_YSIZE, ObjectGetInteger(0, ObjName, OBJPROP_YSIZE)) && ObjectSetInteger(chart, Name, OBJPROP_XOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_XOFFSET)) && ObjectSetInteger(chart, Name, OBJPROP_YOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_YOFFSET)))) && ChartScreenShot(chart, FolderName + "\\" + _FileName, (int)ObjectGetInteger(chart, Name, OBJPROP_XSIZE), (int)ObjectGetInteger(chart, Name, OBJPROP_YSIZE)); ObjectDelete(0, Name); } return(Res); }
这些是结果图形。
文件夹中的图形。
如果选择了“保存所有屏幕截图”,则屏幕截图名称由自定义准则组成,该准则的择选是:排序+利润+通测编号。
如果只选择了最佳通测结果,屏幕截图名称由“自定义准则+利润”组成。
此处是脚本创建的图形的模样。
下面是来自策略测试器的相同图形
我在这里展示了非常相似的图表,但在大多数情况下,它们会有所不同。 这是因为在策略测试器中,沿 X 轴的成交与时间有关,而脚本绘制的图形中,X 轴与交易数量有关。 此外,因为我们必须在一个帧数据中写入最少的信息,从而保证文件足够小,而由脚本创建的图形其净值额不足以进行分析。 与此同时,这些数据足以对优化通测的效率进行初步评估。 它也足以计算自定义优化准则。
优化之后,在运行 ScreenShotOptimization 之前,请重启终端!
最初,我只想可视化所有优化图形。 但当我实现了这个脚本,并在文件夹中看到了 7000 个屏幕截图时,我才明白:不可能处理如此多的图形。 取而代之,我们需要根据某些准则来筛选其中最佳的。
我很久以前就注意到算法交易者分为两类:
- 他们中的一些人认为,EA 应该在一个非常大的时间区间内进行优化,相当于几年甚至几十年,然后 EA 就会完美工作。
- 其他人则认为 EA 必须定期重新优化,选用一个小的时间区间,例如:一个优化月 + 一个交易周;或三个优化月 + 一个交易月;或任何其它合适的重新优化时间表。
我属于第二种类型。
这就是为什么我决定搜索优化准则,并将其作为一个过滤器来筛选最佳通测结果。
创建自定义优化准则
所有自定义优化准则将在单独的包含文件 CustomCriterion.mqh 中计算,因为这些计算最终将即用于绘制图形的脚本操作,以及我们进行优化的智能交易系统。
在创建自己的自定义优化准则之前,我查阅了很多相关资料。
该文章详细介绍了线性回归,及其利用 AlgLib 库进行的计算。 它还对 R^2 的确定系数,及其在测试结果中的应用进行了很好的描述。 我建议大家阅读这篇文章。
用于计算线性回归 R^2 和盈利能力的函数:
void Coeff(double& Array[], double& LR[], double& coeff[], double& TeSt[], int total, int c) { //-- Fill the matrix: Y - Array value, X - ordinal number of the value CMatrixDouble xy(total, 2); for(int i = 0; i < total; i++) { xy[i].Set(0, i); xy[i].Set(1, Array[i]); } //-- Find coefficients a and b of the linear model y = a*x + b; int retcode = 0; double a, b; CLinReg::LRLine(xy, total, retcode, b, a); //-- Generate the linear regression values for each X; ArrayResize(LR, total); for(int x = 0; x < total; x++) LR[x] = x * a + b; if(m_calc == c) { //-- Find the coefficient of correlation of values with their linear regression corr = CAlglib::PearsonCorr2(Array, LR); //-- Find R^2 and its sign coeff[r2] = MathPow(corr, 2.0); int sign = 1; if(Array[0] > Array[total - 1]) sign = -1; coeff[r2] *= sign; //-- Find LR Standard Error if(total - 2 == 0) stand_err = 0; else { for(int i = 0; i < total; i++) { double delta = MathAbs(Array[i] - LR[i]); stand_err = stand_err + delta * delta; } stand_err = MathSqrt(stand_err / (total - 2)); } } //-- Find ProfitStability = Profit_LR/stand_err if(stand_err == 0) coeff[ProfitStability] = 0; else coeff[ProfitStability] = (LR[total - 1] - LR[0]) / stand_err; }
基于余额值的优化策略,将其结果与“余额 + 最大锋锐比率” 准则进行比较。
从这篇文章中,我得到了 ProfitStability(利润稳定性)自定义优化准则。 它的计算很简单:首先,我们计算 LR 标准误差,即,回归线与余额或净值线的平均偏差。 然后,从回归线的结果值中扣除起始值,来获得 TrendProfit(趋势利润)。
ProfitStability(利润稳定性)的计算为 TrendProfit(趋势利润)与 LR 标准误差 的比率:
本文详细描述了该优化准则的所有优点和缺点。 它还提供了大量测试,并取 ProfitStability(利润稳定性)与其它优化准则进行比较。
由于线性回归既可以计算余额,也可以计算净值,并且盈利能力与线性回归计算绑定,因此盈利能力的计算能够在线性回归计算函数中实现。
这是于 2011 年发表的一篇相当古老的文章,但它很有趣,而且仍然相关。 我在本文中使用了一个计算交易系统安全系数(Trading System Safety Factor - TSSF)的公式。
TSSF = Avg.Win / Avg.Loss ((110% - %Win) / (%Win-10%) + 1)
if(TeSt[STAT_PROFIT_TRADES] == 0 || TeSt[STAT_LOSS_TRADES] == 0 || TeSt[STAT_TRADES] == 0) coeff[TSSF] = 0; else { double avg_win = TeSt[STAT_GROSS_PROFIT] / TeSt[STAT_PROFIT_TRADES]; double avg_loss = -TeSt[STAT_GROSS_LOSS] / TeSt[STAT_LOSS_TRADES]; double win_perc = 100.0 * TeSt[STAT_PROFIT_TRADES] / TeSt[STAT_TRADES]; // Calculate the secure ratio for this percentage of profitable deals: if((win_perc - 10.0) + 1.0 == 0) coeff[TSSF] = 0; else { double teor = (110.0 - win_perc) / (win_perc - 10.0) + 1.0; // Calculate the real ratio: double real = avg_win / avg_loss; if(teor != 0) coeff[TSSF] = real / teor; else coeff[TSSF] = 0; } }
在本文中,我采用用了 LinearFactor,计算如下:
- LinearFactor = MaxDeviation/EndBalance
- MaxDeviaton = Max(MathAbs(Balance[i]-AverageLine))
- AverageLine=StartBalance+K*i
- K=(EndBalance-StartBalance)/n
- n - 测试中的成交数量
有关详细信息,请参阅上述文章。 这篇文章很有趣,它提供了许多有用的东西。
展望未来,我要说的是,我没办法找到一个适合任何智能交易系统的通用自定义优化准则。 针对不同的智能交易系统,采用不同的准则,才能提供最佳结果。
在某些 EA 当中,LinearFactor 有极好的效果。
double MaxDeviaton = 0; double K = (Balance[total - 1] - Balance[0]) / total; for(int i = 0; i < total; i++) { if(i == 0) MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i)); else if(MathAbs(Balance[i] - (Balance[0] + K * i) > MaxDeviaton)) MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i)); } if(MaxDeviaton ==0 || Balance[0] == 0) coeff[LinearFactor] = 0; else coeff[LinearFactor] = 1 / (MaxDeviaton / Balance[0]);
在个人通信中,作者提到这个准则可以进一步增强,他解释了如何加强,但我无法在代码中实现这些技巧。
因此,我在代码中添加了四个自定义优化准则。
- R^2 - 判定系数。
- ProfitStability(利润稳定性)。
- TSSF - 交易系统安全系数(Trading System Safety Factor)
- LinearFactor(线性系数)。
所有这些优化准则都在我们的项目中。
遗憾的是,我无法添加 “Complex Criterion max(复杂标准 max)”,因为我无法找到它的计算方法。
我的优化准则
基于所有这些文章,我们可以继续创建自己的优化准则。
我们打算看哪个余额图形? 当然,一条理想的曲线应该是一条充满自信的直线。
我们来看一张有利润的图形。
在优化过程中,我们不能比较几个 EA 的结果,但这些都是相同 EA 的结果,这就是为什么我决定不考虑 EA 生成盈利的时间。
此外,我不考虑交易量。 但是,如果手数是动态计算的,则有必要以某种方式在自定义标准中计算交易量(这未实现)。
有多少交易? 我并不在乎有多少交易会为我带来上千盈利,一次或数百次交易,这就是为什么我也忽略了它们的数量。 不过,请注意,如果交易样本太少,线性回归的计算不能保证正确性。
在这个图形中什么是最重要的?首先,它是盈利。 我决定评估相对盈利,即,相对于初始余额的利润。
Relative_Prof = TeSt[STAT_PROFIT] / TeSt[STAT_INITIAL_DEPOSIT];
另一个非常重要的参数是 drawdown(回撤)。
测试其中的回撤是如何计算的? 将左侧的最大净值额与右侧的最小净值额进行比较。
高于余额的数额是相当令人不爽的 — 实际上这笔钱我们并未赚到。 但当数额低于余额时,这真的很令人痛苦。
对我来说,低于余额的最大回撤是最重要的。 因此,交易不应该造成太大伤害。
double equityDD(const double & Balance[], const double & Equity[], const double & TeSt[], const double & coeff[], const int total) { if(TeSt[STAT_INITIAL_DEPOSIT] == 0) return(0); double Balance_max = Balance[0]; double Equity_min = Equity[0]; difference_B_E = 0; double Max_Balance = 0; switch((int)TeSt[41]) { case 0: difference_B_E = TeSt[STAT_EQUITY_DD]; break; default: for(int i = 0; i < total - 1; i++) { if(Balance_max < Balance[i]) Balance_max = Balance[i]; if(Balance[i] == 10963) Sleep(1); if(Balance_max - Equity[i + 1] > difference_B_E) { Equity_min = Equity[i + 1]; difference_B_E = Balance_max - Equity_min; Max_Balance = Balance_max; } } break; } return(1 - difference_B_E / TeSt[STAT_INITIAL_DEPOSIT]); }
因为自定义准则值应该按升序考虑,我从一个准则值中扣除了回撤。 因此,值越高,回撤幅度越小。
我称其为最终数值(equity_rel),即,相对于初始余额的回撤
事实证明,为了正确计算 equity_rel,以前所用的净值采集方法并不适用。 由于一些最低的净值数额丢失,我不得不用两个变量来实现保存净值数额。 第一种选项是当亏损结束时,保存最大净值;在盈利结束时,保存最小净值。 第二个操作只保存最小净值。
为了告知脚本所用的净值采集方法,这些选项已写入测试统计数组 TeSt[41]。 此外,在 EquityDD() 函数中,我们根据净值采集方法计算 equity_rel 和 difference_B_E。
//---
接下来,我决定合并不同的数据,并检查结果。
//---
基于 equity_rel,就能够计算和替代挽回因子。
difference_B_E — 按货币计算的最大净值回撤。
coeff[c_recovery_factor] = coeff[Profit_Bak] / difference_B_E;
为了令曲线更接近于一条直线,我在第二个替代挽回因子中加入了 R^2
coeff[c_recovery_factor_r2] = coeff[Profit_Bak] / difference_B_E * coeff[r2];
由于设置允许基于余额或净值选择相关性计算,因此如果我们只记录最小净值数额,R^2 将与回撤相关。
自定义准则 “相对利润 * R^2” 公式可以生成有趣的结果。
coeff[profit_r2] = relative_prof * coeff[r2];
它在研究相关性有多大时也很有用。 因此,下一个自定义准则如下所示。
相对盈利 *R^2 / 标准误差
if(stand_err == 0) coeff[profit_r2_Err] = 0; else coeff[profit_r2_Err] = relative_prof * coeff[r2] / stand_err;
现在我们有了相对利润,相对于初始余额的净值回撤和 R^2,我们可以创建一个公式,参考了利润、回撤、和曲线与直线的接近度
relative_prof + equity_rel + r2;
如果我们想让这些参数变得更重要呢? 因此,我添加了权重变量 “ratio”。
现在我们还有三个自定义优化准则。
coeff[profit_R_equity_r2] = relative_prof * ratio + coeff[equity_rel] + coeff[r2]; coeff[profit_equity_R_r2] = relative_prof + coeff[equity_rel] * ratio + coeff[r2]; coeff[profit_equity_r2_R] = relative_prof + coeff[equity_rel] + coeff[r2] * ratio;
我们总共有 12 个自定义优化准则。
1. R^2 - 判定系数
2. ProfitStability
3. TSSF - 交易系统安全系数
4. LinearFactor
5. equity_rel
6. c_recovery_factor
7. c_recovery_factor_r2
8. profit_r2
9. profit_r2_Err
10. profit_R_equity_r2
11. profit_equity_R_r2
12. profit_equity_r2_R
检查结果
为了检查结果,我们需要创建一个简单的智能交易系统...
根据临时文章计划,此处是一个简单的智能交易系统代码。 不幸的是,两个简单的 EA 均未展现出预期的结果。
因此,我不得不选择一个创建订单的 EA,并使用它显示结果(我隐藏了 EA 的名称)。
假设现在是四月底,我们计划在真实账户上启动 EA。 如何找出优化准则,令其能够交易获利?
我们启动一个为期三个月的前向优化。
优化之后重启终端。
运行脚本,仅在设置中选择最佳结果。 以下是文件夹中的结果。
然后,我依视觉从所有结果中选择最好的前向通测。 我有几个类似的结果,所以我选择了 profit_equity_R_r2,因为在这个优化中,优先考虑的是较低的回撤。
在策略测试器的同一时期如下所示:
以下是用于比较的最大余额:
以下是最复杂的准则:
正如您所看到的,与最大余额和最大复杂度相比,基于最佳 profit_equity_R_r2 通测的情况下,图表上的交易要少得多,利润大致相同,然而图形要平滑得多。
因此,我们确定了自定义准则:profit_equity_R_r2。 现在,我们来看看如果我们在过去的三个月里运行优化,并且在优化过程中得到了最佳设置,并决定在五月份切换到这个设置,会发生什么。
我们运行前向优化,并检查。
优化设置。
在 EA 设置中,设置执行优化的自定义准则。
由此,如果我们在过去三个月里基于 profit_equity_R_r2 自定义准则优化 EA,
之后在 4 月 1 日至 5 月 1 日期间,以获得的设置进行交易,我们将赚取 750 个单位,净值回撤 300 个单位。
现在,我们检查一下 fxsaber 开发的 Validate EA 的性能!
我们看看 EA 在四个月里是如何交易的。 验证设置:优化三个月,并交易一个月。
如您所见,EA 经受住了这次压力测试!
我们将其与拥有相同设置,但基于最大复杂准则优化的图表进行比较。
EA 幸存下来,但是...
结束语
优势:
- 您可以同步查看优化结果的所有图形。
- 能为 EA 找到最佳自定义优化准则。
缺点:
由于记录数据的限制,这些图形的信息量比策略测试器中的图形要少。
随着成交大量进行,含有帧数据的文件会大幅增长,变得无法读取。
//---
正如实验所表明的,没有独占矛头的超级准则:不同的准则为不同的智能交易系统提供最佳结果。
但我们已拥有一整套这样的准则。 如果读者支持这个思路,选择的范围会更广。
//---
其中一位测试人员建议在文章中讲述脚本设置,如此这般,不愿意学习材料的用户就可以直接使用代码,而不必研究细节。
如何使用
为了使用此代码,请下载下面附带的 zip 文件,将其解压缩,并复制到 MQL5 文件夹。
在终端里,选择 “文件 -> 打开数据文件夹 -> 在新文件夹的空白处点击鼠标右键 -> 插入"。 如果出现提示询问您是否要替换目标文件夹中的文件,请选择“替换”。
接下来,运行 MetaEditor,在其中打开 EA,并进行以下修改:
1. 在 OnTick() 函数里插入 IsOnTick();
2. 在 EA 底部添加以下代码:
#include <SkrShotOpt.mqh> double OnTester() {return(IsOnTester());} void OnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request,const MqlTradeResult & result) { IsOnTradeTransaction(trans, request, result); }如果 EA 已经有了 OnTradeTransaction() 函数,则在其中添加 IsOnTradeTransaction(trans, request, result);
3. 按下“编译”。
如果有错误生成,通知与匹配变量名相关,则需要更改名称。
设置
一旦您插入代码之后,EA 设置中将显示另外几行。
不要勾选这些选项框来优化这些设置!!!这些设置不会影响优化结果,因此不要对其进行优化
- Write the pass if trades more than:如果您的 EA 执行了太多交易,则可以增加此参数,从而减少写入帧数据文件的数据量。
- Write the pass if profit exceeds % — 默认情况下,亏损通测结果将被删除。 如果您你不想看到利润低于初始余额的某个百分比,您可以改变它。
- Select equity values — 如果需要正确计算以下自定义准则,请设置 "save only min equity",quity_rel,c_recovery_factor,c_recovery_factor_r2,profit_r2,profit_r2_Err,profit_R_equity_r2,profit_equity_R_r2,profit_equity_r2_R
如果您想拥有与策略测试器中类似的图形,请选择 “save min and max equity”
- Custom criterion — 如果其设置为 “none”,则会将帧记录到文件中,但不会计算自定义准则(不会增加优化时间),
但在这种情况下,您不能按自定义准则选择优化。 此外,以下所有参数都不会产生任何影响。
如果要按自定义准则运行优化,应在此参数中选择一些自定义准则。
不要忘记,自定义准则的计算取决于参数 “select equity values” 和 “calculate criterion by”
- Calculate criterion by - 根据余额或净值计算 R^2;影响使用 R^2 的所有自定义准则
//----
脚本设置。
- Draw regression line:选择要绘制的回归线类型 - 余额、净值、余额和净值、无。
- Profit percent more than - 打印屏幕截图需要大量时间,因此您可以选择只打印利润超过参数的屏幕截图。
- Only best results - 如果为 true,则只保存含有每个自定义准则最佳结果的屏幕截图;否则会保存所有。
- Custom criterion - 如果选择了所有屏幕截图,此参数可用于设置自定义准则,根据该准则对文件夹中的屏幕截图进行排序。
- ratio - 计算自定义准则的权重 profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.
- Calculate criterion by - 根据余额或净值计算 R^2;影响使用 R^2 的所有自定义准则
r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.
- Graph - 在测试器中的 “Back separately from Forward” 图形之间进行选择,即前向通测以初始余额开始,
或 "Back continued by Forward" - 前向通测从回测的最后结束余额开始。
//---
所有在该网站上发表的文章对编写本程序都非常有帮助。
我要感谢本文提到的所有文章的作者!
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/9922