连续前行优化 (第七部分): 将自动优化器的逻辑部分与图形绑定,并通过程序控制图形·进阶篇
🧩

连续前行优化 (第七部分): 将自动优化器的逻辑部分与图形绑定,并通过程序控制图形·进阶篇

(2/3)· 从 ViewModel 连接器到程序控图,逐层拆解自动优化器图形与逻辑怎么真正咬合

新手友好 第 2/3 篇

接上篇,我们继续深挖自动优化器的内部运转。很多交易者跑完优化只盯着结果表格,却没意识到逻辑层和图形层若没接稳,中途改参、暂停、看进度都会卡顿甚至误触。本文聚焦两者如何绑定,以及怎样从代码侧直接指挥图形。

启动与停止优化在 ViewModel 里的分支逻辑

点下 StartStop 按钮,AutoOptimiserVM 里的 _StartStopOptimisation 被调用。它靠 model.Optimiser.IsOptimisationInProcess 这个布尔量判断当前状态:true 就走停止分支,false 才走启动分支,两条路互斥。 启动分支里第一件事是把 EnableMainTogles 置为 false 并通知界面,这样主区域被锁住,用户不能再改输入。随后用图形界面绑定的值填一个 OptimistionInputData 结构,字段来自 OptimiserSettings、BotParams、FilterItems 等集合,结构成形后丢给数据模型的 StartOptimisation,该方法是异步的,按钮回调结束不代表优化结束。 排序和过滤表格在「结果」与「设置」两个选项卡里共享同一属性。任一处增删排序参数,切到另一选项卡会看到同样变化,因为底层都链到 SorterItems / FilterItems。SorterItem 类带一个由外部委派注入的删除回调,FilterItem 从它继承并多了比较类型和阈值两个字段。 OnPropertyChanged 回调里用 e.PropertyName 做条件分发。测试完成且模型请求解锁时,重置进度条并解 GUI 锁;注意此事件可能从辅助线程进来,通知必须经由 Dispatcher 抛回主线程,否则 MT5 界面会抛异常。数据模型刷新优化列表后,会顺带把智能交易系统名称、存款、存款货币、杠杆写进测试器固定参数并弹 MessageBox。

MQL5 / C++
class="kw">private class="type">void _StartStopOptimisation(object o)
{
    if (model.Optimiser.IsOptimisationInProcess)
    {
        model.StopOptimisation();
    }
    else
    {
        EnableMainTogles = class="kw">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, class="type">class="kw">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),

「把面板参数灌进优化器输入结构」

在 MT5 的 AIGC 优化框架里,图形面板的「开始优化」按钮并不直接调策略,而是先把界面上的设置项映射成一个 OptimiserInputData 对象。下面这段赋值逻辑就是干这个的:从 OptimiserSettings 列表里按 Name 捞参数,再转成对应的枚举或字符串。 注意 HistoryBorders 与 ForwardBorders 的写法——用 LINQ 的 Any 判断有没有对应类型的边界,没有就给一个空 List<DateBorders>,避免后续回测引擎在空引用上崩。外汇与贵金属品种做这类历史+前向切分测试时波动跳空频繁,空列表兜底能少踩很多坑,属高风险操作须自行验证。 紧跟着的 model.StartOptimisation(...) 才是真正点火,Append 模式由 FileWritingMode 字符串控制,DirPrefix 决定结果落盘目录。 另一段 _StartTest 则是单轮测试通道:Deposit、Currency、Laverage 从 fixed 字典取,Execution Mode 和 Optimisation model 从 changing 下拉取,OptimisationMode 被硬设为 Disabled 表示只测不优。HistoryBorders 直接用 new DateBorders(TestFrom, TestTill) 包一层,前向边界留空。

MQL5 / C++
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);
}
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Callback for the graphical interface - run optimization / test
class=class="str">"cmt">/// </summary>
class="kw">public ICommand StartStopOptimisation { get; }
class=class="str">"cmt">// Callback of optimization start/stop buttons
StartStopOptimisation = new RelayCommand(_StartStopOptimisation);
class="kw">private class="type">void _StartTest(List<OptimisationResult> results, class="type">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,

◍ 回测报告的二次触发与筛选

把优化结果重新跑一遍,核心在 _StartTest 里组装 optimiserInputData:从配置里解析 TF 周期、把 BotParams 映射成变量名与数值的列表,再丢给 model.StartTest。 异常直接用 MessageBox 弹 e.Message,调试时能看到具体哪一行参数解析失败,不用去翻日志。 界面层给了三个入口命令:StartTestReport 取 AllOptimisationResults 指定日期边界的那组,StartTestHistory 取 HistoryOptimisations,StartTestForward 取 ForwardOptimisations,都是双击表格行触发 RelayCommand。 排序和过滤各自有守卫:ReportDateBorders 为空、排序/比较条件为空或 AllOptimisations 为空就直接 return,避免空集合崩在底层。 _SortResults 把界面勾选的 Sorter 丢给 model.SortResults;_FilterResults 把 FilterItems 转成「排序字段→(比较符,阈值)」的字典再调 model.FilterResults,阈值由 x.Border 决定。

MQL5 / C++
TF = (ENUM_Timeframes)Enum.Parse(class="kw">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);
}
class="kw">catch (Exception e)
{
System.Windows.MessageBox.Show(e.Message);
}
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Run a test from a table with forward tests
class=class="str">"cmt">/// </summary>
class="kw">public ICommand StartTestForward { get; }
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Run a test from a table with historical tests
class=class="str">"cmt">/// </summary>
class="kw">public ICommand StartTestHistory { get; }
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Run a test from a table with optimization results
class=class="str">"cmt">/// </summary>
class="kw">public ICommand StartTestReport { get; }
StartTestReport = new RelayCommand((object o) =>
{
_StartTest(model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[SelectedReportDateBorder]], SelecterReportItem);
});
class=class="str">"cmt">// Callback for the test start upon the event of class="type">class="kw">double-clicking on the table with historical tests
StartTestHistory = new RelayCommand((object o) =>
{
_StartTest(model.HistoryOptimisations, SelectedHistoryItem);
});
class=class="str">"cmt">// Callback for the test start upon the event of class="type">class="kw">double-clicking on the table with historical tests
StartTestForward = new RelayCommand((object o) =>
{
_StartTest(model.ForwardOptimisations, SelectedForwardItem);
});
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Sort reports
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="o"></param>
class="kw">private class="type">void _SortResults(object o)
{
if (ReportDateBorders.Count == class="num">0)
class="kw">return;
IEnumerable<SortBy> sortFlags = SorterItems.Select(x => x.Sorter);
if (sortFlags.Count() == class="num">0)
class="kw">return;
if (AllOptimisations.Count == class="num">0)
class="kw">return;
model.SortResults(ReportDateBorders[SelectedReportDateBorder], sortFlags);
}
class="kw">public ICommand SortResults { get; }
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Filtering reports
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="o"></param>
class="kw">private class="type">void _FilterResults(object o)
{
if (ReportDateBorders.Count == class="num">0)
class="kw">return;
IDictionary<SortBy, KeyValuePair<CompareType, class="type">class="kw">double>> compareData =
FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, class="type">class="kw">double>(x.CompareType, x.Border));
if (compareData.Count() == class="num">0)
class="kw">return;
if (AllOptimisations.Count == class="num">0)
class="kw">return;
model.FilterResults(ReportDateBorders[SelectedReportDateBorder], compareData);
}
class="kw">public ICommand FilterResults { get; }

把排序与过滤做成可绑定的 GUI 对象

在 MT5 优化结果后处理里,若想把排序、过滤直接挂到界面,用 ObservableCollection 包一层比裸枚举省事得多。下面两段静态扩展负责按多字段排序和按阈值过滤,返回的都是 IEnumerable<OptimisationResult>,可继续 LINQ 链式调用。 OrderBy 扩展接收 IEnumerable<SortBy> sortingFlags,允许传入 sortMethod 委托自定义升序降序逻辑;FiltreOptimisations 则吃一张字典,键是 SortBy,值是 CompareType 与 double 边界的键值对,例如把 Profit Factor 设成大于 1.2 才留。 SorterItem 是 SortBy 的包装类,构造时塞进一个 Action<object> 删除回调,内部用 RelayCommand 包成 ICommand,界面点叉就能从集合移除。FilterItem 继承它,多带了比较方式与边界值两个字段,足以支撑 GUI 里的条件行。 实盘外汇与贵金属回测高风险,这类封装只解决人机交互,不保证任何参数组合在未来样本外盈利。开 MT5 新建一个指标或 EA 的 ViewModel,把这两个集合接上 ListBox,就能边跑优化边人工筛。

MQL5 / C++
class="kw">public class="kw">static IEnumerable<OptimisationResult> OrderBy(this IEnumerable<OptimisationResult> order, IEnumerable<SortBy> sortingFlags,
	Func<SortBy, SortMethod> sortMethod = null)

class="kw">public class="kw">static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,
	IDictionary<SortBy, KeyValuePair<CompareType, class="type">class="kw">double>> compareData)

class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Selected sorting options
class=class="str">"cmt">/// </summary>
class="kw">public ObservableCollection<SorterItem> SorterItems { get; } = new ObservableCollection<SorterItem>();

class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Selected filters
class=class="str">"cmt">/// </summary>
class="kw">public ObservableCollection<FilterItem> FilterItems { get; } = new ObservableCollection<FilterItem>();

class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Wrapper class for enum SortBy(for graphical interval)
class=class="str">"cmt">/// </summary>
class SorterItem
{
	class=class="str">"cmt">/// <summary>
	class=class="str">"cmt">/// Constructor
	class=class="str">"cmt">/// </summary>
	class=class="str">"cmt">/// <param name="sorter">Sort parameter</param>
	class=class="str">"cmt">/// <param name="deleteItem">Delete from list callback</param>
	class="kw">public SorterItem(SortBy sorter, Action<object> deleteItem)
	{
		Sorter = sorter;
		Delete = new RelayCommand((object o) => deleteItem(this));
	}
	class=class="str">"cmt">/// <summary>
	class=class="str">"cmt">/// Sort element
	class=class="str">"cmt">/// </summary>
	class="kw">public SortBy Sorter { get; }
	class=class="str">"cmt">/// <summary>
	class=class="str">"cmt">/// Item class="kw">delete callback
	class=class="str">"cmt">/// </summary>
	class="kw">public ICommand Delete { get; }
}

class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Wrapper class for enum SortBy and CompareType flags(for GUI)
class=class="str">"cmt">/// </summary>
class FilterItem : SorterItem
{
	class=class="str">"cmt">/// <summary>
	class=class="str">"cmt">/// Constructor
	class=class="str">"cmt">/// </summary>
	class=class="str">"cmt">/// <param name="sorter">Sort element</param>
	class=class="str">"cmt">/// <param name="deleteItem">Deletion callback</param>
	class=class="str">"cmt">/// <param name="compareType">Comparison method</param>
	class=class="str">"cmt">/// <param name="border">Comparable value</param>
	class="kw">public FilterItem(SortBy sorter, Action<object> deleteItem,
			CompareType compareType, class="type">class="kw">double border) : base(sorter, deleteItem)
	{
交给小布盯盘看盘口
这些诊断与图形联动的逻辑,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时优化状态,把重复劳动交给小布,你专注决策。

常见问题

它是图形部分与逻辑实现之间的连接器,暴露的公共属性对应界面可编辑字段,并通过事件回调让图形随逻辑状态变化。
可以,小布盯盘内置了类似的诊断与状态呈现,打开对应品种页就能看到优化进度与参数变动,不必自己写 GUI 绑定代码。
绑定 double 仅允许数字输入,绑定 int 则限制为整数,输入类型由背后属性类型自动约束。
表示仅当 ViewModel 中 OnPropertyChanged 事件触发时图形才更新,避免无谓刷新,提升响应效率。
关于优化管理器的完整讨论见《连续前行优化·基础篇:优化管理器与自动优化器项目概述》。