Continuous Walk-Forward Optimization (Part 7): Binding Auto Optimizer's logical part with graphics and controlling graphics from the program

Andrey Azatskiy | 5 August, 2020

In this article, we will consider how the logical part of the program is connected with its graphical representation. We will look at the entire optimization running process, from its very beginning, and will analyze all the stages up to the auto optimizer class. We will also see how the logical program part is connected with is display, as well as consider methods for managing graphics from the application code. The previous articles within this series:

  1. Continuous Walk-Forward Optimization (Part 1): Working with optimization reports
  2. Continuous Walk-Forward Optimization (Part 2): Mechanism for creating an optimization report for any robot
  3. Continuous Walk-Forward Optimization (Part 3): Adapting a Robot to the Auto Optimizer
  4. Continuous Walk-Forward Optimization (Part 4): Optimization Manager (Auto Optimizer)
  5. Continuous Walk-Forward Optimization (Part 5): Auto optimizer project overview and creation of a GUI
  6. Continuous Walk-Forward Optimization (Part 6): Auto optimizer's logical part and structure

ViewModel class and interaction with the graphics layer

As already mentioned earlier, ViewModel is the connector between the graphical part of the application and the software implementation of the logic. It is the program graphics representation, which implements application logic calls and graphics reaction to the callbacks of the logical part of the application. Accordingly, a public property from the ViewModel part corresponds to each editable field in the graphical part of the application. These properties can either be getters, in which case they cannot be changed from the graphics, or setters, which allows overwriting the object hidden behind this property. In previous parts, we have already considered in detail the data binding technology. Therefore, I will only provide a few examples here. 

Text fields are connected using properties that have both write and read access. As an example, consider a field that indicates the name of an asset on which optimization will be performed. The XAML markup for this field is extremely simple.

<TextBox Width="100"          IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"          Text="{Binding AssetName}"/>

In addition to setting the width of the text window, it also has fields IsEnabled and Text. The first one sets whether the field is available for editing. If it is set to true, the field becomes available for editing. If it is false, then the field is locked. The "Text" field contains the text entered in this field. Then, there is a construction in curly braces opposite each of them. Its content sets the connection of the object with a certain public property from the ViewModel class specified after the "Binding" parameter.

This can be followed by a number of parameters. For example, the UpdateSourceTrigger parameter indicates the method of updating of the graphical part of this application. The value used in our example (PropertyChanged), indicates that the graphical part will only be updated upon triggering of the OnPropertyChanged event from the ViewModel class with a passed name specified after the "Binding" parameters (which is "EnableMainTogles" in our case).

If the "Text" parameter is bound not to a string but let's say to a double parameter, only numbers will be allowed in this field. If it is bound to an int type, then only integer numbers will be allowed. In other words, this implementation allows setting requirements for the entered value type.

In the ViewModel part, the fields are presented as follows:

The IsEnabled parameter:

/// <summary> /// If the switch = false, then the most important fields are not available /// </summary> public bool EnableMainTogles { get; private set; } = true;

and the Text parameter:

/// <summary> /// Name of the asset selected for tests / optimization /// </summary> public string AssetName { get; set; }

As you can see, both of them have access to both write and read data. The only difference is that the EnableMainTogles property provides write access only from the AutoOptimiserVM class (that is, from itself), so it cannot be edited from the outside.

If we consider any collection of data, such as, for example, a list of forward optimization results, then it corresponds to a property containing a list of values. Let us consider a table with the results of forward passes:

<ListView ItemsSource="{Binding ForwardOptimisations}"           SelectedIndex="{Binding SelectedForwardItem}"           v:ListViewExtention.DoubleClickCommand="{Binding StartTestForward}">     <ListView.View>         <GridView>             <GridViewColumn Header="Date From"                             DisplayMemberBinding="{Binding From}"/>             <GridViewColumn Header="Date Till"                             DisplayMemberBinding="{Binding Till}"/>             <GridViewColumn Header="Payoff"                             DisplayMemberBinding="{Binding Payoff}"/>             <GridViewColumn Header="Profit pactor"                             DisplayMemberBinding="{Binding ProfitFactor}"/>             <GridViewColumn Header="Average Profit Factor"                             DisplayMemberBinding="{Binding AverageProfitFactor}"/>             <GridViewColumn Header="Recovery factor"                             DisplayMemberBinding="{Binding RecoveryFactor}"/>             <GridViewColumn Header="Average Recovery Factor"                             DisplayMemberBinding="{Binding AverageRecoveryFactor}"/>             <GridViewColumn Header="PL"                             DisplayMemberBinding="{Binding PL}"/>             <GridViewColumn Header="DD"                             DisplayMemberBinding="{Binding DD}"/>             <GridViewColumn Header="Altman Z score"                             DisplayMemberBinding="{Binding AltmanZScore}"/>             <GridViewColumn Header="Total trades"                             DisplayMemberBinding="{Binding TotalTrades}"/>             <GridViewColumn Header="VaR 90"                             DisplayMemberBinding="{Binding VaR90}"/>             <GridViewColumn Header="VaR 95"                             DisplayMemberBinding="{Binding VaR95}"/>             <GridViewColumn Header="VaR 99"                             DisplayMemberBinding="{Binding VaR99}"/>             <GridViewColumn Header="Mx"                             DisplayMemberBinding="{Binding Mx}"/>             <GridViewColumn Header="Std"                             DisplayMemberBinding="{Binding Std}"/>         </GridView>     </ListView.View> </ListView>

As can be seen from the markup, the ListView type table is a reference of the table class itself. This is followed by the creation of a grid in which the data will be stored, and a column with the data. By mentioning the creation of a class reference, I was referring to the ListView class. This seemingly simple XAML markup stands for a rather complex and well-thought-out mechanism, that allows describing classes and working with the class objects using the markup language. All fields that we associate with the AutoOptimiserVM class are exactly the properties of these classes. In the above example with the table, we deal with three classes:

The ItemsSource property of the ListView class indicates a collection of elements which the table consists of. After connecting this property with a collection from ViewModel we have a kind of a DataContext for the Window class, which operates within our table. Since we are talking about a table, the table representing collection must consist of classes that have public properties for each of the tables. After binding the ItemsSource property with a property from ViewModel, which represents a table with data, we can bind each of the columns with the desired column value from the given table. Also, the table has a connection of the SelectedIndex property with the SelectedForwardItem property from ViewModel. This is needed for ViewModel to know which row the user has selected in this table.

In the ViewModel part, the property with which the presented table is bound is implemented as follows:

/// <summary> /// Selected forward tests /// </summary> public ObservableCollection<ReportItem> ForwardOptimisations { get; } = new ObservableCollection<ReportItem>();

The ObservableCollection class from the C# standard library is an object which notifies the graphics about modifications. This is because the class already has the mentioned event and calls it every time when updating the list of its elements. As for the rest, it is a standard collection of data.

The SelectedForwardItem property performed several roles: it stores data on the selected table row and serves as a row selection callback.

/// <summary> /// Selected forward pass /// </summary> private int _selectedForwardItem; public int SelectedForwardItem {     get => _selectedForwardItem;     set     {         _selectedForwardItem = value;         if (value > -1)         {             FillInBotParams(model.ForwardOptimisations[value]);             FillInDailyPL(model.ForwardOptimisations[value]);             FillInMaxPLDD(model.ForwardOptimisations[value]);         }     } } 

Since the property is used as a callback, due to which the specification of reaction to setting of a value (in our example) is expected, the setter must contain the implementation of this reaction and serve as a function. Due to this, the property value is stored in the private variable. To receive a value from this variable, we directly access it from the getter. To set a value, in the setter set to it a value stored as 'value'. The 'value' variable is not titled and serves as a certain alias for the set value, provided by the C# language. If 'value' is greater than -1, fill other related tables in the Results tab, which are updated in accordance with the selected row. These are the tables with trading robot parameters, average profit, losses for a day of the week and highest/lowest PL values. The check performed in the 'if' condition is needed because if the selected table element index is -1, this means that the table is empty and thus there is no need to populate the related tables. The implementation of the called methods is available in the AutoOptimiserVM class code.

Here is the implementation of the class that describes the line with the optimization result.

/// <summary> /// Class - a wrapper for a report item (for a graphical interval) /// </summary> class ReportItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="item">Item</param>     public ReportItem(OptimisationResult item)     {         result = item;     }     /// <summary>     /// Report item     /// </summary>     private readonly OptimisationResult result;     public DateTime From => result.report.DateBorders.From;     public DateTime Till => result.report.DateBorders.Till;     public double SortBy => result.SortBy;     public double Payoff => result.report.OptimisationCoefficients.Payoff;     public double ProfitFactor => result.report.OptimisationCoefficients.ProfitFactor;     public double AverageProfitFactor => result.report.OptimisationCoefficients.AverageProfitFactor;     public double RecoveryFactor => result.report.OptimisationCoefficients.RecoveryFactor;     public double AverageRecoveryFactor => result.report.OptimisationCoefficients.AverageRecoveryFactor;     public double PL => result.report.OptimisationCoefficients.PL;     public double DD => result.report.OptimisationCoefficients.DD;     public double AltmanZScore => result.report.OptimisationCoefficients.AltmanZScore;     public int TotalTrades => result.report.OptimisationCoefficients.TotalTrades;     public double VaR90 => result.report.OptimisationCoefficients.VaR.Q_90;     public double VaR95 => result.report.OptimisationCoefficients.VaR.Q_95;     public double VaR99 => result.report.OptimisationCoefficients.VaR.Q_99;     public double Mx => result.report.OptimisationCoefficients.VaR.Mx;     public double Std => result.report.OptimisationCoefficients.VaR.Std; }

The class is provided here to demonstrate a string representation of an optimization pass in a code. Each table column is associated with an appropriate property of the specific class instance. The class itself is a wrapper for the OptimisationResult structure that was considered in the first article.

All buttons or double clicks on a table row are connected by the Command property from ViewModel, the basic type of which is ICommand. We already considered this technology in earlier articles concerning the creation of the graphical interface. 

ViewModel class and interaction with the data model

Let us begin this chapter with the optimization start and stop callbacks which are combined in the same button. 


Clicking the StartStop button calls the _StartStopOptimisation method from the AutoOptimiserVM class. Further, there are two alternatives: stopping the optimization and starting the optimization. As can be seen from the diagram, when the IsOptimisationInProcess property of the optimizer class returns true, we execute the first part of the logic and request the StopOptimisation method from the data model class. The method then redirects this call to the optimizer. If optimization has not been started, then the StartOptimisation method from the data model class is called. The method is asynchronous, and thus the called Start method will continue operation even after _StartStopOptimisation operation completes. 

We have considered the chain of calls performed upon the method call. Now, let us view the code block that describes the connection of these method calls with the graphical part and Model with ViewModel. The XAML graphical markups is not difficult and is therefore not presented here. As for the ViewModel part, the properties and the method responsible for the optimization launch are as follows:

private void _StartStopOptimisation(object o) {     if (model.Optimiser.IsOptimisationInProcess)     {         model.StopOptimisation();     }     else     {         EnableMainTogles = false;         OnPropertyChanged("EnableMainTogles");         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettings.Find(x => x.Name == "Deposit").SelectedParam),             BotParams = BotParams?.Select(x => x.Param).ToList(),             CompareData = FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border)),             Currency = OptimiserSettings.Find(x => x.Name == "Currency").SelectedParam,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettings.Find(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettings.Find(x => x.Name == "Laverage").SelectedParam),             Model = GetEnum<ENUM_Model>(OptimiserSettings.Find(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = GetEnum<ENUM_OptimisationMode>(OptimiserSettings.Find(x => x.Name == "Optimisation mode").SelectedParam),             RelativePathToBot = OptimiserSettings.Find(x => x.Name == "Available experts").SelectedParam,             Symb = AssetName,             TF = GetEnum<ENUM_Timeframes>(OptimiserSettings.Find(x => x.Name == "TF").SelectedParam),             HistoryBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.History) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.History)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             ForwardBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.Forward) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.Forward)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             SortingFlags = SorterItems.Select(x => x.Sorter)         };         model.StartOptimisation(optimiserInputData, FileWritingMode == "Append", DirPrefix);     } } /// <summary> /// Callback for the graphical interface - run optimization / test /// </summary> public ICommand StartStopOptimisation { get; }

As can be seen from the code and from the diagram, the method is divided into two branches of the 'If Else' condition. The first one stops the optimization process if it is running. The second one launches the process otherwise.

At the time of optimization launch, we lock the main fields of the graphical interface by setting EnableMainTogles = false, and then proceed to forming input parameters. To start an optimization, we need to create the OptimistionInputData structure which is filled from the OptimiserSettings, BotParams, FilterItems, SorterItems and DateBorders collections. Values arrive into these structures directly from the graphical interface by using the already mentioned data binding mechanism. Upon completion of the formation of this structure, we run the previously discussed StartOptimisation method for an instance of the data model class. The StartStopOptimisation property in the constructor.

// Callback of optimization start/stop buttons StartStopOptimisation = new RelayCommand(_StartStopOptimisation);

It is instantiated by the RelayCommand class instance that implements the ICommand interface which is required for binding ViewModel commands with the Command property from the application's graphical part.

Once all optimizations have been performed and tables in the results tab have been formed (or once they have been uploaded by using the Load button and by selecting an optimization from the list), you can launch a test of the selected optimization pass on any of the required time intervals by double clicking in the desired optimization pass. 

private void _StartTest(List<OptimisationResult> results, int ind) {     try     {         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettingsForResults_fixed.First(x => x.Key == "Deposit").Value),             Currency = OptimiserSettingsForResults_fixed.First(x => x.Key == "Currency").Value,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettingsForResults_changing.First(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettingsForResults_fixed.First(x => x.Key == "Laverage").Value),             Model = GetEnum<ENUM_Model>(OptimiserSettingsForResults_changing.First(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = ENUM_OptimisationMode.Disabled,             RelativePathToBot = OptimiserSettingsForResults_fixed.First(x => x.Key == "Expert").Value,             ForwardBorders = new List<DateBorders>(),             HistoryBorders = new List<DateBorders> { new DateBorders(TestFrom, TestTill) },             Symb = OptimiserSettingsForResults_fixed.First(x => x.Key == "Symbol").Value,             TF = (ENUM_Timeframes)Enum.Parse(typeof(ENUM_Timeframes), OptimiserSettingsForResults_fixed.First(x => x.Key == "TF").Value),             SortingFlags = null,             CompareData = null,             BotParams = results[ind].report.BotParams.Select(x => new ParamsItem { Variable = x.Key, Value = x.Value }).ToList()         };         model.StartTest(optimiserInputData);     }     catch (Exception e)     {         System.Windows.MessageBox.Show(e.Message);     } }

Then we create a structure with input parameters and launch a test. In case of an error during the method execution process, show an error message in a MessageBox. The method implementation has already been discussed. However, let us once again have a look at the instantiation of properties containing this callback. We have three different tables:

Therefore, three callbacks have been created. This is required for a correct processing of each table data. 

/// <summary>
/// Run a test from a table with forward tests
/// </summary>
public ICommand StartTestForward { get; }
/// <summary>
/// Run a test from a table with historical tests
/// </summary>
public ICommand StartTestHistory { get; }
/// <summary>
/// Run a test from a table with optimization results
/// </summary>
public ICommand StartTestReport { get; }

Their implementation is performed via setting lambda functions:

StartTestReport = new RelayCommand((object o) => {     _StartTest(model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[SelectedReportDateBorder]], SelecterReportItem); }); // Callback for the test start upon the event of double-clicking on the table with historical tests StartTestHistory = new RelayCommand((object o) => {     _StartTest(model.HistoryOptimisations, SelectedHistoryItem); }); // Callback for the test start upon the event of double-clicking on the table with historical tests StartTestForward = new RelayCommand((object o) => {     _StartTest(model.ForwardOptimisations, SelectedForwardItem); });

This approach enables the creation of the required list with optimization results which is used for obtaining the robot parameters, which the algorithm passes to a file (for details please see Part 3 of this article series). 

After the optimization process is over and the best results are selected and tested using historical and forward data, a list with all optimization passes is saved. Due to this process, the user can check the operation logic of the selected optimizer as well as select other passes manually by changing filtering and sorting factors. Therefore, there is a possibility to use the built-in mechanism to filter optimization results and to sort them according to several criteria simultaneously. This mechanism is implemented in the data model, but the input parameters for the mechanism are generated in the ViewModel class.

/// <summary> /// Sort reports /// </summary> /// <param name="o"></param> private void _SortResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IEnumerable<SortBy> sortFlags = SorterItems.Select(x => x.Sorter);     if (sortFlags.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.SortResults(ReportDateBorders[SelectedReportDateBorder], sortFlags); } public ICommand SortResults { get; } /// <summary> /// Filtering reports /// </summary> /// <param name="o"></param> private void _FilterResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData =         FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border));     if (compareData.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.FilterResults(ReportDateBorders[SelectedReportDateBorder], compareData); } public ICommand FilterResults { get; }

These two methods have similar implementations. They check the presence of data filtering parameters (i.e. that the table is not empty) and redirect their execution to the data model class. Both methods of the data model class redirect execution to the appropriate extension method described in the first article.

The sorting method has the following signatures:

public static IEnumerable<OptimisationResult> SortOptimisations(this IEnumerable<OptimisationResult> results,                                                                         OrderBy order, IEnumerable<SortBy> sortingFlags,                                                                         Func<SortBy, SortMethod> sortMethod = null)

The filtering method:

public static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,                                                                   IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData)

This is performed in an asynchronous mode, to avoid graphics locking while performing sorting (which can take more than one second depending on the amount of data).

Speaking about data sorting, let us view the implementation of connection between two data sorting tables and data filtering. In the auto optimizer, both the results tab and the settings tab (main) have an area with the data sorting and filtering table data — that is what we are talking about.

   

In the above screenshot, this area is marked in the optimization results tab. The idea is that if we add any sorting parameter in this area and then switch to another tab (the settings tab in our example), the same value added will appear in the same area. Now, if we remove this value from this area on the settings tab with and then switch back to the tab with the optimization results, we will see that the value has also been removed from this tab. This is because both tables are linked to the same property.

Sorting tables are linked to the following property:

/// <summary> /// Selected sorting options /// </summary> public ObservableCollection<SorterItem> SorterItems { get; } = new ObservableCollection<SorterItem>();

Filter tables are linked to:   

/// <summary> /// Selected filters /// </summary> public ObservableCollection<FilterItem> FilterItems { get; } = new ObservableCollection<FilterItem>();

The classes that describe rows in these tables have some repeating fields and are entitled in the same file, where ViewModel.

/// <summary> /// Wrapper class for enum SortBy (for graphical interval) /// </summary> class SorterItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="sorter">Sort parameter</param>     /// <param name="deleteItem">Delete from list callback</param>     public SorterItem(SortBy sorter, Action<object> deleteItem)     {         Sorter = sorter;         Delete = new RelayCommand((object o) => deleteItem(this));      }      /// <summary>      /// Sort element      /// </summary>      public SortBy Sorter { get; }      /// <summary>      /// Item delete callback      /// </summary>      public ICommand Delete { get; } } /// <summary> /// Wrapper class for enum SortBy and CompareType flags (for GUI) /// </summary> class FilterItem : SorterItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="sorter">Sort element</param>     /// <param name="deleteItem">Deletion callback</param>     /// <param name="compareType">Comparison method</param>     /// <param name="border">Comparable value</param>     public FilterItem(SortBy sorter, Action<object> deleteItem,                       CompareType compareType, double border) : base(sorter, deleteItem)     {         CompareType = compareType;         Border = border;     }     /// <summary>     /// Comparison type     /// </summary>     public CompareType CompareType { get; }     /// <summary>     /// Comparable value     /// </summary>     public double Border { get; } }

The SorterItem class is an object that presents table rows of the selected parameters for sorting. In addition to the sorting parameter, it also contains the property pointing to the callback of deletion of this specific parameter from the list. Please note that this callback is set externally via a delegate. The data filter class is inherited from the sort class: there is no need to write already implemented fields twice as we can simply inherit it from the base class. In addition to the earlier considered set of parameters, it has a data comparison type with a threshold and this threshold value itself.

The presence of deletion methods in the class presenting a line allows adding a Delete button next to each line, as it is done in the current implementation. It is convenient for users and has an interesting implementation. The deletion methods are implemented outside the classes. They are set as delegates because they need access to data collections located in the class representing ViewModel. Their implementation is quite simple and is therefore not provided here. These methods only call the Delete method for the desired data collection instance.

After completion of some of the events that require reaction from the graphical layer, the OnPropertyChanged event is called. The event callback in the class representing ViewModel is implemented as follows:

private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) {     // The test has completed, or you need to resume the availability of the buttons locked at the optimization or test start     if (e.PropertyName == "StopTest" ||         e.PropertyName == "ResumeEnablingTogle")     {         // button accessibility switch = true         EnableMainTogles = true;         // Reset status and progress         Status = "";         Progress = 0;         // Notify the GUI of changes         dispatcher.Invoke(() =>         {             OnPropertyChanged("EnableMainTogles");             OnPropertyChanged("Status");             OnPropertyChanged("Progress");         });     }     // Changed the list of passed optimization passes     if (e.PropertyName == "AllOptimisationResults")     {         dispatcher.Invoke(() =>         {             // Clear the previously saved optimization passes and add new ones             ReportDateBorders.Clear();             foreach (var item in model.AllOptimisationResults.AllOptimisationResults.Keys)             {                 ReportDateBorders.Add(item);             }             // Select the very first date             SelectedReportDateBorder = 0;             // Fill in the fixed settings of the tester in accordance with the settings of the uploaded results             ReplaceBotFixedParam("Expert", model.AllOptimisationResults.Expert);             ReplaceBotFixedParam("Deposit", model.AllOptimisationResults.Deposit.ToString());             ReplaceBotFixedParam("Currency", model.AllOptimisationResults.Currency);             ReplaceBotFixedParam("Laverage", model.AllOptimisationResults.Laverage.ToString());             OnPropertyChanged("OptimiserSettingsForResults_fixed");         });         // Notify when data loading is complete         System.Windows.MessageBox.Show("Report params where updated");     }     // Filter or sort optimization passes     if (e.PropertyName == "SortedResults" ||         e.PropertyName == "FilteredResults")     {         dispatcher.Invoke(() =>         {             SelectedReportDateBorder = SelectedReportDateBorder;         });     }     // Updated forward optimization data     if (e.PropertyName == "ForwardOptimisations")     {         dispatcher.Invoke(() =>         {             ForwardOptimisations.Clear();             foreach (var item in model.ForwardOptimisations)             {                 ForwardOptimisations.Add(new ReportItem(item));             }         });     }     // Updated historical optimization data     if (e.PropertyName == "HistoryOptimisations")     {         dispatcher.Invoke(() =>         {             HistoryOptimisations.Clear();             foreach (var item in model.HistoryOptimisations)             {                 HistoryOptimisations.Add(new ReportItem(item));             }         });     }     // Save (*.csv) file with optimization/test results     if (e.PropertyName == "CSV")     {         System.Windows.MessageBox.Show("(*.csv) File saved");     } }

All conditions in this callback check the PropertyName property from the "e" input parameter. The first condition is met if the test completes and the data model is requested to unlock the GUI. When this condition triggers, we unlock the GUI, reset the progress bar status and the progress bars to initial values. Note that this event can be called in the secondary thread context, and the graphics notification (OnPropertyChanged event call) must always be done in the primary thread context, i.e. in the same thread with a GUI. Therefore, in order to avoid errors, call this event from the dispatcher class. Dispatcher allows accessing GUI from this window's thread context.

The next condition is called once the data model updates the list of all performed optimizations. To enable selection of optimization lists via a combobox, we need to fill it with appropriate optimization dates. This is done by this code part. It also fills fixed tester parameters:

After that, it shows a MessageBox notifying that the update of parameters and of tables with the optimization pass reports has completed.

Once filtering or sorting is complete, the corresponding condition is triggered. However, in order to understand its implementation, let us consider the implementation of the SelectedReportDateBorder property.

#region Selected optimisation date border index keeper private int _selectedReportDateBorder; public int SelectedReportDateBorder {     get => _selectedReportDateBorder;     set     {         AllOptimisations.Clear();         if (value == -1)         {             _selectedReportDateBorder = 0;             return;         }         _selectedReportDateBorder = value;         if (ReportDateBorders.Count == 0)             return;         List<OptimisationResult> collection = model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[value]];         foreach (var item in collection)         {             AllOptimisations.Add(new ReportItem(item));         }     } } #endregion

The setter part updates the AllOptimisations collection in the ViewModel class, and thus the code in the condition makes sense now. In other words, by setting the SelectedReportDateBorder parameter to itself, we simply avoid the duplication of this loop. 

Conditions related to the update of Forward and Historical tables serve the same role as the previous condition, i.e. data synchronization between ViewModel and Model. This synchronization is needed because we cannot directly refer to the structures that the data model operates on, since corresponding classes are required to describe table rows, where each column is represented by a property. These classes are created as wrappers for the structures used in the data model. The ReportItem class is used for tables with optimization results, which was considered in the previous chapter.

Conclusion

This article precedes the last one within the series of articles devoted to the walk-forward optimization and an auto optimizer that implements this process. We have considered the structure of the most significant parts of the created application. The first article described the part of the application that is responsible for working with reports and for saving them in xml files. The second and third parts contained a description of how a report for the auto-optimizer is generated and how an Expert Advisor can be connected with the report loading program interface, which was described in the first article. The fourth part contained program use instructions: by that time, we have considered required steps for connecting any robot to the auto-optimizer.

In parts 5, 6 and 7 we considered the auto optimizer program, which controls the process. We started with its graphical part (the fifth article), then examined its operation logic (sixth article) and the connection between them (the current article). In comments to the fifth article, users added some suggestions regarding the application UI. The most interesting of them have already been implemented.

The current part does not contain these improvements, because the primary idea was to describe the previous work. The next article (which will be the last one) will contain the indicated improvements and will provide a description of how you can create your own optimizer. By optimizer, I mean the logic of running optimizations. The current optimizer logic has already been considered earlier (mainly in the fourth article). Therefore, the last article will provide an instruction of how to create similar logic. We will use the existing optimization logic as the bases and will consider a step-by-step process of how to create your own optimizer.

The attachment contains the auto optimizer project with a trading robot analyzed in article 4. To use the project, please compile the auto optimizer project file and the testing robot file. Then copy ReportManager.dll (described in the first article) to the MQL5/Libraries directory, and you can begin to test the EA. Please refer to articles 3 and 4 within this series for details on how to connect the auto optimizer to your Expert Advisors.

Here is the description of the compilation process for all those who have not worked with Visual Studio. The project can be compiled in VisualStudio in different ways, here are three of them:

  1. The easiest is to press CTRL+SHIFT+B.
  2. A more visual method is to click on the green array in the editor — this will launch the application in the code debug mode and will perform the compilation (if the Debug compilation mode is selected).
  3. Another option is to use the Build command from the menu.

The compiled program will then depend in the folder MetaTrader Auto Optimiser/bin/Debug (or MetaTrader Auto Optimiser/bin/Release — depending on the selected compilation method).