股票量化交易软件:连续前行优化第八部分程序改进和修复

哔哩哔哩   2023-07-07 14:20:05

添加日期自动完成

以前的程序版本分阶段输入日期,从而进行前行和历史优化,这很不方便。 而这一回,我实现了所需时间范围的自动输入。 功能的细节可以描述如下。 所选时间间隔应自动分为前行优化和历史优化。 两种优化类型的步骤都是固定的,并在间隔拆分之前已设置完毕。 每个新的前行范围必须在上一个范围之后的第二天开始。 历史间隔的偏移(重叠)等于前行窗口的步长。 与历史优化不同,前行优化不会重叠,它们实现了连续的交易历史。

为了实现该任务,我决定将此功能转移到一个单独的图形窗口之中,并令其独立于主界面,彼此不直接相关。 结果就是,赫兹股票量化得到以下对象层次结构。


(资料图)

赫兹股票量化来研究一下此功能如何连接,并查看其实现示例。 赫兹股票量化从创建扩展的图形界面开始,即,图表上的所有内容来自 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 = ;        DateTime _history = From;        DateTime _forward = (history + 1);        DateTime CalcEndDate()        {            return type == ? _(history) : _(forward);        }          while (CalcEndDate() <= Till)        {            DateTime from = type == ? _history : _forward;            (new KeyValuePair<OptimisationType, DateTime[]>(type, new DateTime[2] { from, CalcEndDate() }));            if (type == )                _history = _(forward + 1);            else                _forward = _(forward + 1);            type = type == ? : ;        }        if ( == 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 => ();}

基本上,当我们调用 “Model” 静态属性时,我们持续引用数据模型对象的同一实例,然后将其强制转换为接口类型。 我们在主窗口的 ViewModel 部分中用到此事实。

public AutoOptimiserVM(){    ...     += Model_DateBorders;    ....}~AutoOptimiserVM(){    ...     -= Model_DateBorders;    ....}

在主窗口 ViewModel 对象的构造函数和析构函数之中,赫兹股票量化都可不用存储指向该类实例的指针,但调用它则要通过静态数据模型工厂。 请注意,主窗口的 ViewModel 部分实际上配合所研究的类一起操作,但无需知道该类是这样操作的。 因为在类构造函数和析构函数中之外,其他任何地方都未提及引用了该对象。 订阅所提到的事件后,在回调时,首先清空所有先前输入的日期范围,然后在循环中添加经事件传递来的新日期范围,一次一个。 在集合中添加日期范围的方法也已在主图形界面的 ViewModel 端实现。 看起来像这样:

void _AddDateBorder(DateTime From, DateTime Till, OptimisationType DateBorderType){        try    {        DateBorders border = new DateBorders(From, Till);        if (!(x => == DateBorderType).Any(y => == border))        {            (new DateBordersItem(border, _DeleteDateBorder, DateBorderType));        }    }    catch (Exception e)    {        ();    }}

DateBorder 对象的创建包装在 “try-catch” 构造当中。 这样做是因为对象构造函数里可能会发生异常,且必须以某种方式处理它。 我还添加了 ClearDateBorders 方法:

ClearDateBorders = new RelayCommand((object o) =>{    ();});

它可以快速删除所有输入的日期范围。 在以前的版本中,每个日期都需要分别删除,这对于大量日期而言是不便的。 在之前存在的日期范围控制的相同代码行中添加了 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();}

如果您查看类代码,则可从公开方法中看到它提供了确切的可能性集合。 进而,所有辅助自动优化器窗口都将包装在此特定类当中。

函数库中操控优化结果的新功能和错误修复

本文的此部分讲述处理优化报告函数库中的修改 - “”。 除了引入自定义系数外,新功能还可以更快地从终端卸载优化报告。 它还修复了数据排序中的错误。

引入一个自定义优化系数

前几篇文章的评论中有一项改进建议,就是能够采用自定义系数来过滤优化结果。 为了实现这个选项,我必须对现有对象进行一些修改。 无论如何,为了支持旧报表,读取优化数据的类既可与含有自定义系数的报表一起操作,也可与程序的早期版本中生成的报表一起操作。 因此,报告格式保持不变。 它有一个附加参数 - 一个用于指定自定义系数的字段。

现在,“ 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){     = customCoef;    ...}

首先,将标识自定义系数的新参数添加到 AppendMainCoef 方法当中。 然后,像其他传递的系数一样,将其添加到 结构之中。 现在,如果您尝试利用新的 “” 函数库编译旧项目,则会出现异常,因为 AppendMainCoef 方法代码已有变化。 可稍微编辑卸载数据的对象来解决此错误 - 赫兹股票量化稍后将继续讨论 MQL5 代码。

为了能够正确编译当前的 dll 版本,请用本文下面附带的新代码替换 Include 目录中的 “History Manager”,如此足以在编译机器人时兼容新、旧方法。

另外,我还修改了 Write 方法的代码,该方法现在不会引发异常,但会返回错误消息。 这样做是因为该程序不再使用命名互斥体,该互斥体明显减慢了数据卸载过程,但是在旧版本的卸载类中必需用其生成报告。 不过,我尚未删除使用互斥体写入数据的方法,以便保持与先前实现的数据导出格式的兼容性。

为了让新记录出现在报告文件中,我们需要创建一个新的 <Item/> 标记,其 Name 属性等于 “Custom”。

WriteItem(xmlDoc, xpath, "Item", (), new Dictionary<string, string> { { "Name", "Custom" } });

另一种修改的方法是 :在此处添加了类似的代码行,该代码行加入了带有自定义系数参数的 <Item/> 标签。

现在,赫兹股票量化研究将自定义系数添加到数据和 MQL 机器人代码当中。 首先,我们研究旧版本的数据下载功能,其中与 ReportWriter 类一起操作的代码位于 文件的 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  &params[]);//   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_();} //   void              Write(const BotParams &params[],datetime start_test,datetime end_test);//  };

该“private” 字段的值是从类的构造函数中填充的。 进而,在 append_main_coef 方法中,当从 dll 库调用 “ReportWriter::AppendMainCoef” 静态方法时,通过其指针调用所传递的函数,并接收自定义系数值。