概述
根据本系列文章的用户和读者的评论和要求,程序已进行了修改。 本文包含一个自动优化器的新版本。 该版本实现了所需的功能,并提供了其他改进,这些是我运用该程序操作时发现的。 当前的优化执行速度与以前的版本相比快了很多倍,这是决定弃用互斥体,并避免一些其他减慢报告生成过程操的作结果。 现在,优化可以用于一组资产。 另外,在优化时占用内存的问题也已解决。
本系列的前几篇文章:
添加日期自动完成
以前的程序版本分阶段输入日期,从而进行前行和历史优化,这很不方便。 而这一回,我实现了所需时间范围的自动输入。 功能的细节可以描述如下。 所选时间间隔应自动分为前行优化和历史优化。 两种优化类型的步骤都是固定的,并在间隔拆分之前已设置完毕。 每个新的前行范围必须在上一个范围之后的第二天开始。 历史间隔的偏移(重叠)等于前行窗口的步长。 与历史优化不同,前行优化不会重叠,它们实现了连续的交易历史。
为了实现该任务,我决定将此功能转移到一个单独的图形窗口之中,并令其独立于主界面,彼此不直接相关。 结果就是,我们得到以下对象层次结构。
我们来研究一下此功能如何连接,并查看其实现示例。 我们从创建扩展的图形界面开始,即,图表上的所有内容来自 AutoFillInDateBorders 对象,该对象代表图形窗口,及以下。 该图片示意 GUI 元素,XAML 标记,以及由 AutoFillInDateBordersVM 类呈现的 ViewModel 部分中的字段。
如您所见,GUI 包括三个主要部分。 其中包括两个日历,用来输入优化期开始和结束日期;指定前行和历史间隔边界的表格;以及 “Set” 按钮,单击该按钮会将指定范围划分为相应的历史和前行窗口。 屏幕截图中的表格包含重复的三行,实际上只有两行:第一行负责历史日期范围,第二行设置前行范围。
表格中的 “Value” 是相应优化类型的步数,以天为单位的。 例如,如果历史间隔的值是 360 天,而前行值是 90,则意味着日历中指定的时间间隔将分为 360 天的历史优化间隔,和 90 天的前行间隔。 每个下一个历史优化窗口的开始将依据前行间隔步数平移。
class AutoFillInDateBordersM : IAutoFillInDateBordersM { private AutoFillInDateBordersM() { } private static AutoFillInDateBordersM instance; public static AutoFillInDateBordersM Instance() { if (instance == null) instance = new AutoFillInDateBordersM(); return instance; } public event Action<List<KeyValuePair<OptimisationType, DateTime[]>>> DateBorders; public void Calculate(DateTime From, DateTime Till, uint history, uint forward) { if (From >= Till) throw new ArgumentException("Date From must be less then date Till"); List<KeyValuePair<OptimisationType, DateTime[]>> data = new List<KeyValuePair<OptimisationType, DateTime[]>>(); OptimisationType type = OptimisationType.History; DateTime _history = From; DateTime _forward = From.AddDays(history + 1); DateTime CalcEndDate() { return type == OptimisationType.History ? _history.AddDays(history) : _forward.AddDays(forward); } while (CalcEndDate() <= Till) { DateTime from = type == OptimisationType.History ? _history : _forward; data.Add(new KeyValuePair<OptimisationType, DateTime[]>(type, new DateTime[2] { from, CalcEndDate() })); if (type == OptimisationType.History) _history = _history.AddDays(forward + 1); else _forward = _forward.AddDays(forward + 1); type = type == OptimisationType.History ? OptimisationType.Forward : OptimisationType.History; } if (data.Count == 0) throw new ArgumentException("Can`t create any date borders with set In sample (History) step"); DateBorders?.Invoke(data); } }
窗口数据的模型类是运用单例范式(Singletone pattern)编写的对象。 这样可以绕开扩展的图形窗口,令主窗口的 ViewModel 部分与数据模型进行交互。 在有趣的方法当中,对象仅包含“Calculate” ,用来计算日期范围,并在完成上述过程后调用 事件。 事件接收一对数值集合作为参数,其中键值是所分析间隔的类型(前行或历史优化),而其值是一个包含两个 DateTime 值的数组。 第一个表示所选间隔的开始日期,而第二个表示结束日期。
该方法会在一个循环中计算日期范围,备选是更改计算窗口的类型(前行或历史)。 首先,历史窗口类型设置为所有计算的起点。 在循环开始之前还设置了每种窗口类型的初始日期值。 在循环的每次迭代中,使用嵌套函数计算所选窗口类型的边界极值,然后依据极值范围日期验证该值。 如果日期超界,那么此为循环退出条件。 优化窗口范围是在循环里形成的。 然后,更新下一个窗口开始日期和窗口类型切换器。
所有操作之后,如果未发生任何错误,则利用所传递日期范围调用事件。 所有进一步的动作均由类来执行。 按下 “Set” 按钮回调可启动上述方法的执行。
为我们的扩展而建立的数据模型工厂以最简单的方式实现:
class AutoFillInDateBordersCreator { public static IAutoFillInDateBordersM Model => AutoFillInDateBordersM.Instance(); }
基本上,当我们调用 “Model” 静态属性时,我们持续引用数据模型对象的同一实例,然后将其强制转换为接口类型。 我们在主窗口的 ViewModel 部分中用到此事实。
public AutoOptimiserVM() { ... AutoFillInDateBordersCreator.Model.DateBorders += Model_DateBorders; .... } ~AutoOptimiserVM() { ... AutoFillInDateBordersCreator.Model.DateBorders -= Model_DateBorders; .... }
在主窗口 ViewModel 对象的构造函数和析构函数之中,我们都可不用存储指向该类实例的指针,但调用它则要通过静态数据模型工厂。 请注意,主窗口的 ViewModel 部分实际上配合所研究的类一起操作,但无需知道该类是这样操作的。 因为在类构造函数和析构函数中之外,其他任何地方都未提及引用了该对象。 订阅所提到的事件后,在回调时,首先清空所有先前输入的日期范围,然后在循环中添加经事件传递来的新日期范围,一次一个。 在集合中添加日期范围的方法也已在主图形界面的 ViewModel 端实现。 看起来像这样:
void _AddDateBorder(DateTime From, DateTime Till, OptimisationType DateBorderType) { try { DateBorders border = new DateBorders(From, Till); if (!DateBorders.Where(x => x.BorderType == DateBorderType).Any(y => y.DateBorders == border)) { DateBorders.Add(new DateBordersItem(border, _DeleteDateBorder, DateBorderType)); } } catch (Exception e) { System.Windows.MessageBox.Show(e.Message); } }
DateBorder 对象的创建包装在 “try-catch” 构造当中。 这样做是因为对象构造函数里可能会发生异常,且必须以某种方式处理它。 我还添加了 ClearDateBorders 方法:
ClearDateBorders = new RelayCommand((object o) => { DateBorders.Clear(); });
它可以快速删除所有输入的日期范围。 在以前的版本中,每个日期都需要分别删除,这对于大量日期而言是不便的。 在之前存在的日期范围控制的相同代码行中添加了 GUI 主窗口按钮调用所讲述的新创内容。
单击 “Autoset” 将触发一次回调,它调用 SubFormKeeper 类实例之中的 Open 方法。 该类被编写为包装器,其中封装嵌套的窗口创建过程。 这消除了主窗口 ViewModel 中不必要的属性和字段,并防止我们直接访问已创建的辅助窗口,因为本不该直接进行交互。
class SubFormKeeper { public SubFormKeeper(Func<Window> createWindow, Action<Window> subscribe_events = null, Action<Window> unSubscribe_events = null); public void Open(); public void Close(); }
如果您查看类代码,则可从公开方法中看到它提供了确切的可能性集合。 进而,所有辅助自动优化器窗口都将包装在此特定类当中。
函数库中操控优化结果的新功能和错误修复
本文的此部分讲述处理优化报告函数库中的修改 - “ReportManager.dll”。 除了引入自定义系数外,新功能还可以更快地从终端卸载优化报告。 它还修复了数据排序中的错误。
前几篇文章的评论中有一项改进建议,就是能够采用自定义系数来过滤优化结果。 为了实现这个选项,我必须对现有对象进行一些修改。 无论如何,为了支持旧报表,读取优化数据的类既可与含有自定义系数的报表一起操作,也可与程序的早期版本中生成的报表一起操作。 因此,报告格式保持不变。 它有一个附加参数 - 一个用于指定自定义系数的字段。
现在,“ SortBy” 枚举含有新参数 “Custom”,并已将相应的字段添加到 “Coefficients” 结构之中。 这会将系数添加到负责存储数据的对象当中,但不会将其添加到卸载和读取数据的对象之中。 数据写入是通过两种方法执行的,和一个拥有静态方法的类,它是为了从 MQL5 中保存报告。
public static void AppendMainCoef(double customCoef, double payoff, double profitFactor, double averageProfitFactor, double recoveryFactor, double averageRecoveryFactor, int totalTrades, double pl, double dd, double altmanZScore) { ReportItem.OptimisationCoefficients.Custom = customCoef; ... }
首先,将标识自定义系数的新参数添加到 AppendMainCoef 方法当中。 然后,像其他传递的系数一样,将其添加到 ReportWriter.ReportItem 结构之中。 现在,如果您尝试利用新的 “ReportManager.dll” 函数库编译旧项目,则会出现异常,因为 AppendMainCoef 方法代码已有变化。 可稍微编辑卸载数据的对象来解决此错误 - 我们稍后将继续讨论 MQL5 代码。
为了能够正确编译当前的 dll 版本,请用本文下面附带的新代码替换 Include 目录中的 “History Manager”,如此足以在编译机器人时兼容新、旧方法。
另外,我还修改了 Write 方法的代码,该方法现在不会引发异常,但会返回错误消息。 这样做是因为该程序不再使用命名互斥体,该互斥体明显减慢了数据卸载过程,但是在旧版本的卸载类中必需用其生成报告。 不过,我尚未删除使用互斥体写入数据的方法,以便保持与先前实现的数据导出格式的兼容性。
为了让新记录出现在报告文件中,我们需要创建一个新的 <Item/> 标记,其 Name 属性等于 “Custom”。
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.Custom.ToString(), new Dictionary<string, string> { { "Name", "Custom" } });
另一种修改的方法是 OptimisationResultsExtentions.ReportWriter:在此处添加了类似的代码行,该代码行加入了带有自定义系数参数的 <Item/> 标签。
现在,我们研究将自定义系数添加到数据和 MQL 机器人代码当中。 首先,我们研究旧版本的数据下载功能,其中与 ReportWriter 类一起操作的代码位于 XmlHistoryWriter.mqh 文件的 CXmlHistoryWriter 类当中。 创建了以下代码的引用,以便支持自定义系数:
typedef double(*TCustomFilter)();
上述类中的 “private” 字段存储此函数。
class CXmlHistoryWriter { private: const string _path_to_file,_mutex_name; CReportCreator _report_manager; TCustomFilter custom_filter; void append_bot_params(const BotParams ¶ms[]);// void append_main_coef(PL_detales &pl_detales, TotalResult &totalResult);// //double get_average_coef(CoefChartType type); void insert_day(PLDrawdown &day,ENUM_DAY_OF_WEEK day);// void append_days_pl();// public: CXmlHistoryWriter(string file_name,string mutex_name, CCCM *_comission_manager, TCustomFilter filter);// CXmlHistoryWriter(string mutex_name,CCCM *_comission_manager, TCustomFilter filter); ~CXmlHistoryWriter(void) {_report_manager.Clear();} // void Write(const BotParams ¶ms[],datetime start_test,datetime end_test);// };
该“private” 字段的值是从类的构造函数中填充的。 进而,在 append_main_coef 方法中,当从 dll 库调用 “ReportWriter::AppendMainCoef” 静态方法时,通过其指针调用所传递的函数,并接收自定义系数值。
该类并非直接使用,因为早前在第三篇文章中介绍过一个包装器 - 它就是 CAutoUploader 类。
class CAutoUploader { private: datetime From,Till; // Testing start and end dates CCCM *comission_manager; // Commission manager BotParams params[]; // List of parameters string mutexName; // Mutex name TCustomFilter custom_filter; public: CAutoUploader(CCCM *comission_manager, string mutexName, BotParams ¶ms[], TCustomFilter filter); CAutoUploader(CCCM *comission_manager, string mutexName, BotParams ¶ms[]); virtual ~CAutoUploader(void); virtual void OnTick(); // Calculating testing start and end dates };
double EmptyCustomCoefCallback() {return 0;} //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[], TCustomFilter filter) : comission_manager(_comission_manager), mutexName(_mutexName), From(0), Till(0), custom_filter(filter) { CopyBotParams(params,_params); } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[]) : comission_manager(_comission_manager), mutexName(_mutexName), From(0), Till(0), custom_filter(EmptyCustomCoefCallback) { CopyBotParams(params,_params); }
已创建了 “EmptyCustomCoefCallback” 函数,以便保存构造函数的旧版本。 此函数返回零作为自定义系数。 如果调用了该类的先前构造函数,则将该函数确切地传递给 CXmlHistoryWriter 类。 如果我们以第四篇文章中的示例为例,则可以向机器人里添加自定义系数,如下所示:
//+------------------------------------------------------------------+ //| SimpleMA.mq5 | //| Copyright 2019, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #include <Trade/Trade.mqh> #include <History manager/AutoLoader.mqh> // Include CAutoUploader #define TESTER_ONLY input int ma_fast = 10; // MA fast input int ma_slow = 50; // MA slow input int _sl_ = 20; // SL input int _tp_ = 60; // TP input double _lot_ = 1; // Lot size // Comission and price shift (Article 2) input double _comission_ = 0; // Comission input int _shift_ = 0; // Shift int ma_fast_handle,ma_slow_handle; const double tick_size = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE); CTrade trade; CAutoUploader * auto_optimiser;// Pointer to CAutoUploader class (Article 3) CCCM _comission_manager_;// Comission manager (Article 2) double CulculateMyCustomCoef() { return 0; } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- ... // Add Instance CAutoUploader class (Article3) auto_optimiser = new CAutoUploader(&_comission_manager_,"SimpleMAMutex",params,CulculateMyCustomCoef); //--- return(INIT_SUCCEEDED); } double OnTester() { return(CulculateMyCustomCoef()); } //+------------------------------------------------------------------+
此处,我把除自定义系数引入相关之外的实现部分删除了,故此处不必提供太多代码。 完整代码可在附件中找到。 首先,必须创建一个函数,该函数将计算自定义系数。 在上面的示例中,该函数返回零,且并未实现,但必须在其中定义自定义系数计算。 故此,将不会在 OnTester 回调中进行计算 - 将改为调用所定义的函数。 在创建 CAutoUploader 类的过程中,我们只需要调用一个新的重载构造函数,并在其中指定自定义系数即可。 自定义系数的添加至此完毕。
排除互斥体的决定增加了数据加载速度,但这种增加还不够。 在以前的版本中,我们必须对每个新记录执行许多操作,以便将数据添加到文件中:
这是 C# 标准库中使用的 XmlDocument 类的标准过程。 此过程很耗时。 甚至,此操作时间会随着文件的递增而增加。 我们在以前的版本中不得不接受此功能,因为我们无法在一个地方累积所有数据。 取而代之,我们在每次优化完成后保存数据。 在当前的实现中,数据是按帧累积的,因此我们可以立即将所有数据转换为所需的格式。 这是用先前编写的 “OptimisationResultsExtentions.ReportWriter” 方法实现的。 这是针对优化推算数组的扩展方法。 与 ReportWriter.Write 不同,该方法不会将数据添加到文件中,而是创建一个文件,并将所有优化推算逐行写入其中。 因此,现在可以在几秒钟内写入原来 ReportWriter.Write 需花费几分钟的数据数组。
在 ReportWriter 类中已创建包装器,允许调用 MQL5 中的 OptimisationResultsExtentions.ReportWriter 方法。
public class ReportWriter { private static ReportItem ReportItem; private static List<OptimisationResult> ReportData = new List<OptimisationResult>(); public static void AppendToReportData(string symbol, int tf, ulong StartDT, ulong FinishDT) { ReportItem.Symbol = symbol; ReportItem.TF = tf; ReportItem.DateBorders = new DateBorders(StartDT.UnixDTToDT(), FinishDT.UnixDTToDT()); ReportData.Add(ReportItem); ClearReportItem(); } public static void ClearReportItem() { ReportItem = new ReportItem(); } public static void ClearReportData() { ReportData.Clear(); } public static string WriteReportData(string pathToBot, string currency, double balance, int laverage, string pathToFile) { try { ReportData.ReportWriter(pathToBot, currency, balance, laverage, pathToFile); ClearReportData(); } catch (Exception e) { return e.Message; } ClearReportData(); return ""; } }
在 ReportWriter 类中,我们创建了 ReportData 字段,该字段将存储 ReportItem 元素的集合,如此这将是优化推算的集合。 这个思路是利用第一篇文章中所讲述的方法,将所有必需的数据从 MQL5 写入 ReportItem 结构。 然后,通过调用 AppendToReportData 方法,将它们添加到优化推算的集合当中。 这样,所需的数据集合将在 C# 端形成。 一旦将所有优化推算都添加到集合之后,调用 WriteReportData 方法,该方法调用 OptimizationResultsExtentions.ReportWriter 方法快速形成优化报告。
不幸的是,我在以前的程序版本中犯了一个错误,后来才注意到。 这个错误与第一篇文章中讲述的优化排序机制有关。 由于可根据许多标准执行数据分类,因此有必要确定这些标准中的哪个将最大化,哪些将最小化。 例如,没人会最大化亏损交易的数量。
为避免混淆,优化排序的方向有所不同:
为了令数据排序方法能够判定理应最大化和最小化的准则,已单独创建了一个返回相应变量的方法。 以前的方法实现如下:
private static SortMethod GetSortMethod(SortBy sortBy) { switch (sortBy) { case SortBy.Payoff: return SortMethod.Increasing; case SortBy.ProfitFactor: return SortMethod.Increasing; case SortBy.AverageProfitFactor: return SortMethod.Increasing; case SortBy.RecoveryFactor: return SortMethod.Increasing; case SortBy.AverageRecoveryFactor: return SortMethod.Increasing; case SortBy.PL: return SortMethod.Increasing; case SortBy.DD: return SortMethod.Decreasing; case SortBy.AltmanZScore: return SortMethod.Decreasing; case SortBy.TotalTrades: return SortMethod.Increasing; case SortBy.Q_90: return SortMethod.Decreasing; case SortBy.Q_95: return SortMethod.Decreasing; case SortBy.Q_99: return SortMethod.Decreasing; case SortBy.Mx: return SortMethod.Increasing; case SortBy.Std: return SortMethod.Decreasing; case SortBy.MaxProfit: return SortMethod.Increasing; case SortBy.MaxDD: return SortMethod.Decreasing; case SortBy.MaxProfitTotalTrades: return SortMethod.Increasing; case SortBy.MaxDDTotalTrades: return SortMethod.Decreasing; case SortBy.MaxProfitConsecutivesTrades: return SortMethod.Increasing; case SortBy.MaxDDConsecutivesTrades: return SortMethod.Decreasing; case SortBy.AverageDailyProfit_Mn: return SortMethod.Increasing; case SortBy.AverageDailyDD_Mn: return SortMethod.Decreasing; case SortBy.AverageDailyProfitTrades_Mn: return SortMethod.Increasing; case SortBy.AverageDailyDDTrades_Mn: return SortMethod.Decreasing; case SortBy.AverageDailyProfit_Tu: return SortMethod.Increasing; case SortBy.AverageDailyDD_Tu: return SortMethod.Decreasing; case SortBy.AverageDailyProfitTrades_Tu: return SortMethod.Increasing; case SortBy.AverageDailyDDTrades_Tu: return SortMethod.Decreasing; case SortBy.AverageDailyProfit_We: return SortMethod.Increasing; case SortBy.AverageDailyDD_We: return SortMethod.Decreasing; case SortBy.AverageDailyProfitTrades_We: return SortMethod.Increasing; case SortBy.AverageDailyDDTrades_We: return SortMethod.Decreasing; case SortBy.AverageDailyProfit_Th: return SortMethod.Increasing; case SortBy.AverageDailyDD_Th: return SortMethod.Decreasing; case SortBy.AverageDailyProfitTrades_Th: return SortMethod.Increasing; case SortBy.AverageDailyDDTrades_Th: return SortMethod.Decreasing; case SortBy.AverageDailyProfit_Fr: return SortMethod.Increasing; case SortBy.AverageDailyDD_Fr: return SortMethod.Decreasing; case SortBy.AverageDailyProfitTrades_Fr: return SortMethod.Increasing; case SortBy.AverageDailyDDTrades_Fr: return SortMethod.Decreasing; default: throw new ArgumentException($"Unaxpected Sortby variable {sortBy}"); } }
当前的实现如下:
private static OrderBy GetSortingDirection(SortBy sortBy) { switch (sortBy) { case SortBy.Custom: return OrderBy.Ascending; case SortBy.Payoff: return OrderBy.Ascending; case SortBy.ProfitFactor: return OrderBy.Ascending; case SortBy.AverageProfitFactor: return OrderBy.Ascending; case SortBy.RecoveryFactor: return OrderBy.Ascending; case SortBy.AverageRecoveryFactor: return Or-derBy.Ascending; case SortBy.PL: return OrderBy.Ascending; case SortBy.DD: return OrderBy.Ascending; case SortBy.AltmanZScore: return OrderBy.Descending; case SortBy.TotalTrades: return OrderBy.Ascending; case SortBy.Q_90: return OrderBy.Ascending; case SortBy.Q_95: return OrderBy.Ascending; case SortBy.Q_99: return OrderBy.Ascending; case SortBy.Mx: return OrderBy.Ascending; case SortBy.Std: return OrderBy.Descending; case SortBy.MaxProfit: return OrderBy.Ascending; case SortBy.MaxDD: return OrderBy.Ascending; case SortBy.MaxProfitTotalTrades: return OrderBy.Ascending; case SortBy.MaxDDTotalTrades: return OrderBy.Descending; case SortBy.MaxProfitConsecutivesTrades: return OrderBy.Ascending; case SortBy.MaxDDConsecutivesTrades: return OrderBy.Descending; case SortBy.AverageDailyProfit_Mn: return OrderBy.Ascending; case SortBy.AverageDailyDD_Mn: return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_Mn: return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_Mn: return OrderBy.Descending; case SortBy.AverageDailyProfit_Tu: return OrderBy.Ascending; case SortBy.AverageDailyDD_Tu: return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_Tu: return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_Tu: return OrderBy.Descending; case SortBy.AverageDailyProfit_We: return OrderBy.Ascending; case SortBy.AverageDailyDD_We: return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_We: return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_We: return OrderBy.Descending; case SortBy.AverageDailyProfit_Th: return OrderBy.Ascending; case SortBy.AverageDailyDD_Th: return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_Th: return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_Th: return OrderBy.Descending; case SortBy.AverageDailyProfit_Fr: return OrderBy.Ascending; case SortBy.AverageDailyDD_Fr: return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_Fr: return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_Fr: return OrderBy.Descending; default: throw new ArgumentException($"Unaxpected Sortby variable {sortBy}"); } }
您可从代码中可以看到,所选系数的排序方向已更改。 在以前的实现中,它们被标记为降序排列。 然而,我没有考虑到这些数据会有负数值,因此应该以升序而不是降序对其进行排序。 为了理解逻辑,请查看以下代码,该代码针对每个所传递数值实现了排序:
// If the minimum is below zero, shift all data by the negative minimum valueif (mm.Min < 0) { value += Math.Abs(mm.Min); mm.Max += Math.Abs(mm.Min); } // If the maximum is greater than zero, calculate if (mm.Max > 0) { // Calculate the coefficient according to the sorting method if (GetSortingDirection(item.Key) == OrderBy.Descending) { // Calculate the coefficient to sort in descending order data.SortBy += (1 - value / mm.Max) * coef; } else { // Calculate the coefficient to sort in ascending order data.SortBy += value / mm.Max * coef; } }
“Value” 是某个系数的数字值。 在数据进行排序之前,请检查所选择要排序的系数数组里是否最小值为负数值。 如果是的话,将这些数值转换为正数值,并把它们向上平移到最小系数值。 因此,我们将得到一个范围为 [0 ; (Max + |Min|)] 的数组。 当计算结果系数时,即将依据最终排序执行,我们把数据数组的每个第 i 个值除以排序数据数组中的最大值,令其值平移到范围 [0; 1]。如果排序方法是降序,则用 1 减去结果值,从而得到反转的权重数组。 这就是为什么以前的数据排序版本不正确的原因:由于实现了多因素排序逻辑,我们仅对权重数组进行了反转,而上述代码中标记的系数则无需这样做。 第一篇文章中已更详细地讲述了排序方法。 出于便利起见,将方法名称和返回类型更改为更合适的名称,但这丝毫不影响应用程序逻辑。
第二个错误是在只选择了一个排序准测的情况下,对优化结果数组进行排序的代码部分。 以前的实现如下:
if (order == OrderBy.Ascending) return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0))); else return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));
当前模样则如下:
if (order == GetSortingDirection(sortingFlags.ElementAt(0))) return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0))); else return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));
以前的版本未考虑 GetSortingDirection 方法指定的方向。 新的排序会遵照此准测。 例如,如果我们选择降序排序(最佳结果在顶部),则对于 SortBy.PL,将执行请求的降序排序,并且最高值将在顶部。 然而,对于 SortBy.MaxDDTotalTrades 参数(无盈利成交的总数),最小值将位于顶部,并且数组将按升序排序。 这保留了逻辑结构。 例如,如果我们选择 SortBy.MaxDDTotalTrades 作为准测,则根据先前的排序逻辑,我们将得到最差的推算。
自动加载机器人参数,和新的智能交易系统编写规则
新的参数加载逻辑在 “AutoUploader2.mqh” 文件中提供。 在机制描述之后,会提供一个示例,该示例基于第四篇文章中的智能交易系统。
class CAutoUploader2 { private: CAutoUploader2() {} static CCCM comission_manager; static datetime From,Till; static TCustomFilter on_tester; static TCallback on_tick, on_tester_deinit; static TOnTesterInit on_tester_init; static string frame_name; static long frame_id; static string file_name; static bool FillInData(Data &data); static void UploadData(const Data &data, double custom_coef, const BotParams ¶ms[]); public: static void OnTick(); static double OnTester(); static int OnTesterInit(); static void OnTesterDeinit(); static void SetUploadingFileName(string name); static void SetCallback(TCallback callback, ENUM_CALLBACK_TYPE type); static void SetCustomCoefCallback(TCustomFilter custom_filter_callback); static void SetOnTesterInit(TOnTesterInit on_tester_init_callback); static void AddComission(string symbol,double comission,double shift); static double GetComission(string symbol,double price,double volume); static void RemoveComission(string symbol); }; datetime CAutoUploader2::From = 0; datetime CAutoUploader2::Till = 0; TCustomFilter CAutoUploader2:: EmptyCustomCoefCallback; TCallback CAutoUploader2:: EmptyCallback; TOnTesterInit CAutoUploader2:: EmptyOnTesterInit; TCallback CAutoUploader2:: EmptyCallback; CCCM CAutoUploader2::comission_manager; string CAutoUploader2::frame_name = "AutoOptomiserFrame"; long CAutoUploader2::frame_id = 1; string CAutoUploader2::file_name = MQLInfoString(MQL_PROGRAM_NAME)+"_Report.xml";
新类只有静态方法。 这样就可避免必须将其实例化,由于删除了不必要的代码,故简化了 EA 开发过程。 该类拥有多个静态字段,包括与先前使用的类相似的日期范围,有关详细信息,请参阅第三篇文章,函数引用是为了测试完成后回调,优化帧和新的即时报价到达时回调,佣金管理器类(有关详细信息,请参见第二篇文章),帧名称和 ID,以及下载优化结果的文件名。
为了连接自动优化器,往 EA 里添加一个指向文件的链接,其中是已定义的一些回调。 如果 EA 用到此文件中定义的任何回调,则最简单的解决方案是创建一个函数操控所需回调,然后将依其指定的静态函数添加到回调的函数引用。
#ifndef CUSTOM_ON_TESTER double OnTester() { return CAutoUploader2::OnTester(); } #endif #ifndef CUSTOM_ON_TESTER_INIT int OnTesterInit() { return CAutoUploader2::OnTesterInit(); } #endif #ifndef CUSTOM_ON_TESTER_DEINIT void OnTesterDeinit() { CAutoUploader2::OnTesterDeinit(); } #endif #ifndef CUSTOM_ON_TICK void OnTick() { CAutoUploader2::OnTick(); } #endif
每个特定的回调都包装在预处理器条件中,如此通过定义相应的预处理器条件来避免在文件中定义它。 实现细节将在示例中进一步提供。
如果您决定自行描述这些回调,请不要忘记在已定义的回调开始时调用 CAutoUploader2 类的静态方法(如本代码片段所做)。 这是正确运行报告生成机制所必需的。
若要启用自定义的数据下载回调(如果尚未实现自己的回调),请将包含已实现函数的指针传递给 CAutoUploader2::SetCustomCoefCallback 静态方法。 为了管理佣金,请使用以下方法之一。
static void AddComission(string symbol,double comission,double shift); static double GetComission(string symbol,double price,double volume); static void RemoveComission(string symbol);
这就是相关的所有功能。 现在,我们看看它是如何工作的。
int CAutoUploader2::OnTesterInit(void) { return on_tester_init(); }
智能交易系统在 OnTesterInit 回调中调用 CAutoUploader2::OnTesterInit 方法(如果已启动优化),在该方法当中,它调用所传递的函数指针,或是在默认情况下将其替换为空函数。
void CAutoUploader2::OnTick(void) { if(MQLInfoInteger(MQL_OPTIMIZATION)==1 || MQLInfoInteger(MQL_TESTER)==1) { if(From == 0) From = iTime(_Symbol,PERIOD_M1,0); Till=iTime(_Symbol,PERIOD_M1,0); } on_tick(); }
然后,在每次即时报价,将优化开始的实时时间保存到相应的变量之中。 然后,EA 调用 on_tick 方法,发送该方法作为新的即时报价到达的回调,或默认的空回调。 仅当 EA 在测试器中运行时,优化时间才会保存。
double CAutoUploader2::OnTester(void) { double ret = on_tester(); Data data[1]; if(!FillInData(data[0])) return ret; if(MQLInfoInteger(MQL_OPTIMIZATION)==1) { if(!FrameAdd(frame_name, frame_id, ret, data)) Print(GetLastError()); } else if(MQLInfoInteger(MQL_TESTER)==1) { BotParams params[]; UploadData(data[0], ret, params, false); } return ret; }
测试器操作完成后,将在 OnTester 回调中调用 CAutoUploader2::OnTester 静态方法,其中帧已被保存(如果是优化的话),或将这些帧写入到文件(如果是测试)。 如果是测试,则过程在当前步骤结束,并依据配置文件中传递的命令关闭终端。 不过,如果这是一个优化过程,则会执行以下最后阶段:
input bool close_terminal_after_finishing_optimisation = false; // MetaTrader Auto Optimiser param (must be false if you run it from terminal) void CAutoUploader2::OnTesterDeinit(void) { ResetLastError(); if(FrameFilter(frame_name,frame_id)) { ulong pass; string name; long id; double coef_value; Data data[]; while(FrameNext(pass,name,id,coef_value,data)) { string parameters_list[]; uint params_count; BotParams params[]; if(FrameInputs(pass,parameters_list,params_count)) { for(uint i=0; i<params_count; i++) { string arr[]; StringSplit(parameters_list[i],'=',arr); BotParams item; item.name = arr[0]; item.value = arr[1]; ADD_TO_ARR(params,item); } } else Print("Can`t get params"); UploadData(data[0], coef_value, params, true); } CheckRetMessage(ReportWriter::WriteReportData(get_path_to_expert(), CharArrayToString(data[0].currency), data[0].balance, data[0].laverage, TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name)); } else { Print("Can`t select apropriate frames. Error code = " + IntegerToString(GetLastError())); ResetLastError(); } on_tester_deinit(); if(close_terminal_after_finishing_optimisation) { if(!TerminalClose(0)) { Print("==================================="); Print("Can`t close terminal from OnTesterDeinit error number: " + IntegerToString(GetLastError()) + " Close it by hands"); Print("==================================="); } } ExpertRemove(); }
最后的优化步骤是调用 CAutoUploader2::OnTesterDeinit() 静态方法。 在该方法里读取所有保存的帧,并形成含有优化报告的最终文件。 首先,重置上一个错误,并依据名称和 ID 过滤这些帧。 然后,在循环中读取每个帧,并获取其保存的数据,然后将其写入文件。
读取数据后,读取 EA 的输入参数以便进行优化,然后将接收到的信息添加到 C# 侧的静态类集合当中。退出循环后, 调用 ReportWriter::WriteReportData 方法将形成的集合写入文件。 然后,调用所传递的自定义回调,或默认的空引用。 这种方式有一点问题:为了让自动优化器运行,它必须能够重新启动终端,为此应首先关闭终端。
以前,为了解决此问题,配置文件的标志设置为 true。 不过,当操控这些帧时不能这样做,因为它们的最终处理将在优化停止后才开始,并且如果配置文件的 required 标志设置为 true,则我们将无法对其进行处理,因为终端将在 OnTerderDeinit 方法完成之前关闭。 为了解决该问题,我添加了一个输入变量,该变量将与包含文件一起添加到智能交易系统。 该变量是由自动优化器修改的,不应手动或在代码中进行修改。 如果将其设置为 true,则从 MQL5 调用终端关闭方法,否则终端不会关闭。 在所有情况描述完毕之后,将从图表中删除处理帧的 EA。
UploadData 方法既作为加载数据并添加到集合中的方法,又用作将某个测试器推算下载至文件的方法(如果它是测试而非优化的话)。
void CAutoUploader2::UploadData(const Data &data, double custom_coef, const BotParams ¶ms[], bool is_appent_to_collection) { int total = ArraySize(params); for(int i=0; i<total; i++) ReportWriter::AppendBotParam(params[i].name,params[i].value); ReportWriter::AppendMainCoef(custom_coef,data.payoff,data.profitFactor,data.averageProfitFactor, data.recoveryFactor,data.averageRecoveryFactor,data.totalTrades, data.pl,data.dd,data.altmanZScore); ReportWriter::AppendVaR(data.var_90,data.var_95,data.var_99,data.mx,data.std); ReportWriter::AppendMaxPLDD(data.max_profit,data.max_dd, data.totalProfitTrades,data.totalLooseTrades, data.consecutiveWins,data.consequtiveLoose); ReportWriter::AppendDay(MONDAY,data.averagePl_mn,data.averageDd_mn, data.numberProfitTrades_mn,data.numberLooseTrades_mn); ReportWriter::AppendDay(TUESDAY,data.averagePl_tu,data.averageDd_tu, data.numberProfitTrades_tu,data.numberLooseTrades_tu); ReportWriter::AppendDay(WEDNESDAY,data.averagePl_we,data.averageDd_we, data.numberProfitTrades_we,data.numberLooseTrades_we); ReportWriter::AppendDay(THURSDAY,data.averagePl_th,data.averageDd_th, data.numberProfitTrades_th,data.numberLooseTrades_th); ReportWriter::AppendDay(FRIDAY,data.averagePl_fr,data.averageDd_fr, data.numberProfitTrades_fr,data.numberLooseTrades_fr); if(is_appent_to_collection) { ReportWriter::AppendToReportData(_Symbol, data.tf, data.startDT, data.finishDT); return; } CheckRetMessage(ReportWriter::Write(get_path_to_expert(), CharArrayToString(data.currency), data.balance, data.laverage, TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name, _Symbol, data.tf, data.startDT, data.finishDT)); }
如果 is_appent_to_collection 标志为 true,则将该推算简单地添加到集合之中。如果为 false,则将当前推算加载到文件 。 从上面的代码可以看出,当我们读取帧,并将其添加到集合中,以便快速下载报告时,该标志等于 true。 如果我们在测试模式下运行智能交易系统,则采用 “false” 参数调用此方法,这意味着应将报告保存到文件中。
现在,让我们看看如何使用新逻辑添加链接以下载优化报告。 参考来自第四篇文章中的,之前创建的测试智能交易系统文件。 新方法的连接(除了在包含文件里引用之外)仅占用 3 行代码,而不是第四篇文章里所用示例的 16 行代码。 至于下载数据的回调,目前 EA 已实现了 “OnTick” 回调,而所有其他回调(“OnTester”,“OnTesterInit”,“OnTesterDeinit”)都在包含文件中实现。
//+------------------------------------------------------------------+ //| SimpleMA.mq5 | //| Copyright 2019, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #include <Trade/Trade.mqh> #define CUSTOM_ON_TICK // Tell to uploading system that we implement OnTick callback ourself #include <History manager/AutoUpLoader2.mqh> // Include CAutoUploader #define TESTER_ONLY input int ma_fast = 10; // MA fast input int ma_slow = 50; // MA slow input int _sl_ = 20; // SL input int _tp_ = 60; // TP input double _lot_ = 1; // Lot size // Comission and price shift (Article 2) input double _comission_ = 0; // Comission input int _shift_ = 0; // Shift int ma_fast_handle,ma_slow_handle; const double tick_size = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE); CTrade trade; //+------------------------------------------------------------------+ //| Custom coeffifient`s creator | //+------------------------------------------------------------------+ double CulculateMyCustomCoef() { return 0; } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- ... CAutoUploader2::SetCustomCoefCallback(CulculateMyCustomCoef); CAutoUploader2::AddComission(_Symbol,_comission_,_shift_); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { CAutoUploader2::OnTick(); // If CUSTOM_ON_TICK was defined ... } //+------------------------------------------------------------------+
红色示意利用新方法添加的数据下载界面。 从示例中可以看到,OnTester 回调仍在数据下载文件中实现。 为了启用自定义系数的计算,我们为其传递 CulculateMyCustomCoef 方法,该方法应包含实现此回调的自定义逻辑。 OnTick 回调实现仍保留在机器人之中。 为此,在定义数据下载过程的文件引用之前,先定义 CUSTOM_ON_TICK 变量。 您可以使用附件来更详细地研究 EA 实施,并将其与默认实现,以及以前的数据下载方法的实现进行比较。
优化启动方法的修改和其他改进
新版本含有许多其他改进。 其中之一是能够针对多种资产安排优化计划。
这项改进可在执行多种资产优化时节省时间。 已计划任务将昼夜运行,直到指定的列表结束。 为了启用此功能,我必须修改先前文章中讲述的优化启动过程。 以前,在按下“开始/停止”按钮之后,ViewModel 会立即将任务重定向到数据模型方法,此即称为完整周期,从开始优化到保存结果。 现在,我们首先调用该方法,循环遍历所传递的参数列表,然后启动优化,并将其保存到相应的目录当中。
public async void StartOptimisation(OptimiserInputData optimiserInputData, bool isAppend, string dirPrefix, List<string> assets) { if (assets.Count == 0) { ThrowException("Fill in asset name"); OnPropertyChanged("ResumeEnablingTogle"); return; } await Task.Run(() => { try { if (optimiserInputData.OptimisationMode == ENUM_OptimisationMode.Disabled && assets.Count > 1) { throw new Exception("For test there must be selected only one asset"); } StopOptimisationTougle = false; bool doWhile() { if (assets.Count == 0 || StopOptimisationTougle) return false; optimiserInputData.Symb = assets.First(); LoadingOptimisationTougle = assets.Count == 1; assets.Remove(assets.First()); return true; } while (doWhile()) { var data = optimiserInputData; // Copy input data StartOptimisation(data, isAppend, dirPrefix); } } catch (Exception e) { LoadingOptimisationTougle = true; OnPropertyChanged("ResumeEnablingTogle");м ThrowException?.Invoke(e.Message); } }); }
传递资产列表,且经检查其完整性之后,进入此方法的异步部分。 在循环中,调用先前研究的优化启动方法,该方法目前是同步的,因此它需等待优化过程完成。 由于经由结构传递的优化参数可能会在优化器类中被更改,故在每次开始新的优化之前,将其复制,并将初始数据投喂给每次新的优化。
继续条件,以及执行优化时的资产替换条件,由嵌套的 doWhile() 函数执行。 在函数主体中,检查循环退出条件,分配下一个资产的值,然后从列表中删除最后分配的资产。 因此,在循环的每个的迭代中,我们首先指示将在其上执行优化的资产,然后运行优化,依此类推,直到列表为空,或者直到优化完成信号 已发送。 在以前的实现中,可简单地由结束运行过程来紧急终止优化过程。 不过,在当前的实现中,该过程将切换到下一个迭代,而不是停止。 这就是为何针对优化终止方法进行了适当的调整。
/// <summary> /// Complete optimization from outside the optimizer /// </summary> public void StopOptimisation() { StopOptimisationTougle = true; LoadingOptimisationTougle = true; Optimiser.Stop(); var processes = System.Diagnostics.Process.GetProcesses().Where(x => x.ProcessName == "metatester64"); foreach (var item in processes) item.Kill(); } bool StopOptimisationTougle = false;
现在,当停止优化时,我们只需将此标志切换为 true。 资产循环看到该标志,并退出正在运行的迭代。 进而,将优化停止过程重定向到优化器类之后,我们必须终止正在运行的测试器过程,因为当终端紧急关闭时,这些过程通常会冻结,并保持运行状态,直到计算机重新启动。
为此目的,使用了一个附加标志 LoadingOptimisationTougle。 该标志指示是否将当前执行的优化加载到图形界面中,就像先前实现的那样。 为了加快处理过程,此标志始终为 “false”,直到强制停止该过程,或所传递的资产列表到达最后一项为止。 而且只有在此之后,当我们退出优化过程时,数据才会被加载到图形界面中。
从很久前的第一个程序版本开始,就有能力在重复优化过程中,将新推算附加到以前的基础上,而不是全新执行。 不过,为了确保画面流畅,应采用相同的参数启动新的优化。 为此目的,我引入了选项,可将以前执行的优化参数保存在存储优化结果的同一目录之中。 GUI 上已添加了一个单独的按钮,可以上传这些设置,以便进行新配置。
单击按钮后触发以下 AutoOptimiserVM 类的方法:
private void SetBotParams() { if (string.IsNullOrEmpty(SelectedOptimisation)) return; try { Status = "Filling bot params"; OnPropertyChanged("Status"); Progress = 100; OnPropertyChanged("Progress"); var botParams = model.GetBotParamsFromOptimisationPass(OptimiserSettings.First(x => x.Name == "Available experts").SelectedParam, SelectedOptimisation); for (int i = 0; i < BotParams.Count; i++) { if (!botParams.Any(x => x.Variable == BotParams[i].Vriable)) continue; BotParams[i] = new BotParamsData(botParams.First(x => x.Variable == BotParams[i].Vriable)); } } catch (Exception e) { MessageBox.Show(e.Message); } Status = null; OnPropertyChanged("Status"); Progress = 0; OnPropertyChanged("Progress") }
首先,我们从数据模型请求 EA 参数列表。 然后,循环遍历加载到 GUI 的所有参数,并检查所收参数列表中该参数是否可用。 如果找到该参数,则其会被替换为新值。 从 ComboBox 里选择的目录中读取名为 “ OptimisationSettings.set” 的文件,并由数据模型方法返回设置文件里的正确参数。 优化开始,直至完成后,该方法会生成文件。
还添加了一个选项,可以在加载后清除优化推算。 它们会占用过多的内存空间。 如果计算机的内存较低,则多次前行和历史测试可能会明显降低计算机的速度。 为了最大程度地减少资源占用,已删除了前行和历史优化推算上的重复数据。 现在,它们仅存储在数据模型当中。 GUI 中添加了一个特殊按钮 “Clear loaded results”,该按钮引用数据模型中的 ClearResults 方法。
void ClearOptimisationFields() { if (HistoryOptimisations.Count > 0) dispatcher.Invoke(() => HistoryOptimisations.Clear()); if (ForwardOptimisations.Count > 0) dispatcher.Invoke(() => ForwardOptimisations.Clear()); if (AllOptimisationResults.AllOptimisationResults.Count > 0) { AllOptimisationResults.AllOptimisationResults.Clear(); AllOptimisationResults = new ReportData { AllOptimisationResults = new Dictionary<DateBorders, List<OptimisationResult>>() }; } GC.Collect(); } public void ClearResults() { ClearOptimisationFields(); OnPropertyChanged("AllOptimisationResults"); OnPropertyChanged("ClearResults"); }
提及的方法是指 ClearOptimizationFields 私密方法,该方法清空 AutoOptimiserM 类中的优化报告集合。 不过,由于我们使用的是 C#,其内存并非由手工管理,而是自动化,因此还必须把所有已删除对象从内存中清除,以便从内存中清除应用数组,和删除数据。 这可以通过调用垃圾回收器(GC)类的静态 Collect 方法完成。 操作执行完毕之后,将从内存中删除以前存在的对象。
查看生成的优化推算之后,我们需要生成一个 *set 文件,以便将所需参数输入到智能交易系统当中。 以前,我们必须手动输入找到的参数,或者通过双击所选的优化行,启动测试器从而形成文件。
按钮上方的表格包含一个“关键字-数值”对列表,其中存储了优化参数。 在按钮上单击,调用来自数据模型中的一个方法,上述表格中的列表将传递给该方法。
public void SaveBotParams(IEnumerable<KeyValuePair<string, string>> data, string path) { SetFileManager setFileManager = new SetFileManager(path, true) { Params = data.Select(x => new ParamsItem { Variable = x.Key, Value = x.Value }).ToList() }; setFileManager.SaveParams(); }
该方法接收所创建文件的路径,将”关键字-数值“对数组转换为含有 EA 参数的结构,并将其保存在指定路径。 文件的路径是通过 ViewModel 的标准文件保存界面设置的。
附件包括完整的自动优化器项目,其中包含第四篇文章里研究的测试智能交易系统。 如果您要使用该 EA,请编译自动优化器项目和测试机器人。 然后将 ReportManager.dll(在第一篇文章中讲述)复制到 MQL5/Libraries 目录,您便可以开始测试 EA。 有关如何将自动优化器连接到 EA 的详细信息,请参阅本系列文章的第三、四篇。
这是针对所有未曾用过 Visual Studio 的人员提供的编译过程说明。 可以在 Visual Studio 中以不同的方式编译项目,以下是其中三种:
然后,取决于所选的编译方法,已编译程序将保存在文件夹 MetaTrader Auto Optimiser/bin/Debug,或 MetaTrader Auto Optimiser/bin/Release 之内。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...