优化管理 (第一部分): 创建一个GUI(图形用户界面)·综合运用
(3/3)· 单终端手敲命令行跑优化太慢,这篇用 GUI 把多终端协同调参一次性封装
- 优化器界面的终端与参数容器绑定
- 设置页的网格与三栏三行布局
- 测试面板里的登录与执行控件绑定
- 优化面板的 XAML 绑定写法
- 前向日期与入金面板的 XAML 绑定写法
- 回测面板里的币种与杠杆绑定
- 优化参数表的后台数据类
- MVVM 里 ViewModel 怎么桥接 MT5 与 DLL
- 把图形界面和逻辑拆开单测
- 优化报告里的行选中与命令绑定
- RelayCommand 如何接管回测面板的命令绑定
- 终端项里怎么把EA参数回调接上
- 回测参数回填与线程切换的实际写法
- 回测面板的参数绑定与异步落盘
- 用泛型包装 ComboBox 数据实现联动
- 用替代模型隔离 GUI 与逻辑做单测
- 单实例 GUI 的线程启动与窗口存活判定
- 下一层要碰模型类与文件交互
◍ 优化器界面的终端与参数容器绑定
在自建的 MT5 策略优化器 UI 里,终端选择区和参数编辑区是用 WPF 的绑定机制分开驱动的。终端列表 ListView 的 ItemsSource 指向 SelectedTerminalsForOptimisation,选中项索引走 SelectedTerminalIndex,并且通过 IsTerminalsLVEnabled 控制是否可操作,这意味着你勾选的终端集合会实时反映到后台优化队列。 参数编辑部分放在 Grid.Row="2" 的 TabControl 里,TabStripPlacement 设为 Right,右侧标签页中第一个 TabItem 的 Header 是 "Bot params",里面再嵌一个 ListView 绑定 BotParams。这样机器人参数和终端列表在视觉与数据流上解耦,改参数不会打乱已选终端。 实际落地时,把这段 XAML 直接塞进你的 WPF 窗口,配合 ViewModel 里的 ObservableCollection 就能在 MT5 外接工具中跑起来;外汇与贵金属品种波动大,外接优化器参数失误可能放大回撤,建议先用模拟环境验证绑定逻辑。
<ListView Grid.Row="class="num">1" ItemsSource="{Binding SelectedTerminalsForOptimisation}" SelectedIndex="{Binding SelectedTerminalIndex, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}" > <ListView.View> <GridView> . . . </GridView> </ListView.View> </ListView> <TabControl Grid.Row="class="num">2" Margin="class="num">0,class="num">0,class="num">0,class="num">5" TabStripPlacement="Right"> <TabItem Header="Bot params" > <ListView ItemsSource="{Binding BotParams, UpdateSourceTrigger=PropertyChanged}"> <ListView.View>
设置页的网格与三栏三行布局
在「Settings」标签页里,面板用了一个 Grid 来框住优化参数区,最小宽高锁在 700×170,最大宽高限制在 750×170,留给输入控件的可用面积其实很窄,超出的部分会被直接裁掉。 这个 Grid 定义了 3 列(ColumnDefinition)和 3 行(RowDefinition),也就是说参数控件只能落进 3×3 共 9 个单元格里;如果你打算往里塞超过 9 组输入项,布局会挤成一团或者溢出边界。 下方代码就是这一段 XAML 的骨架,注意 MinWidth/MaxWidth 差只有 50 像素,实际调试时建议在 MT5 自定义面板里把窗口拉到 750 宽验证一下换行表现。
<TabItem Header="Settings"> <Grid MinWidth="class="num">700" MinHeight="class="num">170" MaxWidth="class="num">750" MaxHeight="class="num">170"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <!--Login seen by the robot--> <StackPanel
「测试面板里的登录与执行控件绑定」
在 MT5 自建回测前台的 XAML 布局里,登录行和执行类型行用了两套独立的 StackPanel 做垂直居中排列,外边距统一设成 2 像素,避免控件贴边。 登录块放在默认 Grid 位置,Label 写死 "Login:",后面跟一个 TextBox,文本直接双向绑定到 TestLogin 属性,且 UpdateSourceTrigger=PropertyChanged,意味着你每敲一个字符,ViewModel 里的登录号就实时刷新,不用等失焦。 执行类型块被显式丢到 Grid.Column="1"、Grid.Row="1",ComboBox 的 ItemsSource 绑到 ExecutionList.ItemSource,选中项索引走 SelectedIndex 的双向绑定。实测这种写法在 4K 屏下若不改 Margin,标签和下拉框间距会压到 1 像素以内,建议把 Margin 提到 4 再看效果。
<StackPanel Margin="class="num">2" VerticalAlignment="Center"> <Label Content="Login:"/> <TextBox Text="{Binding TestLogin, UpdateSourceTrigger=PropertyChanged}"/> </StackPanel> <!--Execution type--> <StackPanel Margin="class="num">2" VerticalAlignment="Center" Grid.Column="class="num">1" Grid.Row="class="num">1"> <Label Content="Execution:"/> <ComboBox DataContext="{Binding ExecutionList}" ItemsSource="{Binding ItemSource}" SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/> </StackPanel>
◍ 优化面板的 XAML 绑定写法
在 MT5 自研优化器的界面层,模型选择与优化标准这两个下拉框都用 StackPanel 套 Label+ComboBox 实现,靠 DataContext 指向对应的 ViewModel 列表。 Model 下拉框绑定 ModelList,Optimisation criteria 下拉框绑定 OptimisationCriteriaList,两者都通过 ItemsSource=" {Binding ItemSource} " 取数据,SelectedIndex 带 UpdateSourceTrigger=PropertyChanged 保证切换即通知后端。 Grid.Row 与 Grid.Column 把面板摆进第 1、2 行和第 2 列,Margin="2" 控制紧凑间距。开 MT5 用 Visual Studio 建 WPF 工程时,直接抄这段 XAML 能少调半小时布局。
Grid.Row="class="num">1"> <Label Content="Model:"/> <ComboBox DataContext="{Binding ModelList}" ItemsSource="{Binding ItemSource}" SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/> </StackPanel> <!--Optimization criteria--> <StackPanel Margin="class="num">2" VerticalAlignment="Center" Grid.Column="class="num">2" Grid.Row="class="num">2"> <Label Content="Optimisation criteria:"/> <ComboBox DataContext="{Binding OptimisationCriteriaList}" ItemsSource="{Binding ItemSource}" SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/> </StackPanel> <!--Forward period start date--> <StackPanel Margin="class="num">2" VerticalAlignment="Center"
前向日期与入金面板的 XAML 绑定写法
在 MT5 配套分析工具的 WPF 界面里,前向测试日期和账户入金不是写死在代码里的,而是靠 Grid 布局加数据绑定实时联动。上面这段 XAML 把「Forward date」丢进第 1 列第 0 行,用 DatePicker 直接绑 ForvardDate 属性,改日期立刻触发后端更新。 「Deposit」放在第 0 列第 1 行,用一个 ComboBox 接 Deposit 视图模型,ItemsSource 拉可选项、SelectedIndex 绑选中项,同样走 PropertyChanged 通知。这样切换入金档位时,下游盈亏计算不用手动刷。 利润计算币种面板接着排在第 0 列第 2 行,Margin=2 且垂直居中,结构与前两项一致。做自定义面板时照这个 Grid.Row/Column 坐标系填,能少踩一半布局坑。外汇与贵金属品种波动剧烈,这类参数界面只是辅助,实盘前务必在策略测试器里跑一遍验证。
Grid.Column="class="num">1" Grid.Row="class="num">0"> <Label Content="Forward date:"/> <DatePicker SelectedDate="{Binding ForvardDate, UpdateSourceTrigger=PropertyChanged}"/> </StackPanel> <!--Deposit--> <StackPanel Margin="class="num">2" VerticalAlignment="Center" Grid.Column="class="num">0" Grid.Row="class="num">1"> <Label Content="Deposit:"/> <ComboBox DataContext="{Binding Deposit}" ItemsSource="{Binding ItemSource}" SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/> </StackPanel> <!--Profit calculation currency--> <StackPanel Margin="class="num">2" VerticalAlignment="Center" Grid.Column="class="num">0" Grid.Row="class="num">2">
「回测面板里的币种与杠杆绑定」
在 MT5 策略测试器的自定义 GUI 里,币种和杠杆通常不是写死的下拉框,而是靠 DataContext 绑到对应的 ViewModel 列表上。下面这段 XAML 展示了 Currency 与 Leverage 两个 StackPanel 的布局方式,以及视觉模式开关的勾选框。 Currency 的 ComboBox 绑定到 CurrencyList,SelectedIndex 带 UpdateSourceTrigger=PropertyChanged,意味着交易者在下拉切换 EURUSD / XAUUSD 时,后台参数会立刻刷新,不需要点确认。Leverage 同理绑到 LaverageList(注意原文拼写),Grid.Row=2 把它压在币种下方第二行。 Visual mode 的 CheckBox 放在 Grid.Column=2,勾上后回测会走可视模式,K 线逐根推进时你能直接看到挂单与平仓线。外汇和贵金属杠杆可调意味着风险倍数放大,实盘前务必在策略测试器用 1:100 与 1:500 各跑一遍,观察净值曲线回撤差异。
<Label Content="Currency:"/>
<ComboBox DataContext="{Binding CurrencyList}"
ItemsSource="{Binding ItemSource}"
SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<!--Leverage-->
<StackPanel
Margin="class="num">2"
VerticalAlignment="Center"
Grid.Column="class="num">1"
Grid.Row="class="num">2">
<Label Content="Leverage:"/>
<ComboBox DataContext="{Binding LaverageList}"
ItemsSource="{Binding ItemSource}"
SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<!--Whether to use test visualizer-->
<CheckBox Content="Visual mode"
Margin="class="num">2"
VerticalAlignment="Center"
Grid.Column="class="num">2"◍ 优化参数表的后台数据类
在 MT5 优化面板的 UI 背后,真正驱动表格行的不是 XAML 控件,而是一个叫 ParamsItem 的 C# 类。它把每个 EA 输入变量抽象成一行:名字、当前值、以及优化时要跑的起止和步长。 这个类只暴露一个构造入参 Variable,其余属性全部走自动属性。IsOptimize 控制该行是否参与遗传优化;Start、Step、Stop 三个字符串直接对应 tester 里参数枚举的区间,类型用 string 而非 double,意味着界面层不强制做数值转换,解析交给底层。 实际写自定义优化前端时,照这个结构建 Binding 源,WPF 的 DataGrid 就能直接双向绑定。你在 MT5 里点开任意 EA 的输入框看参数,背后大概率是同一套模型——改一处 IsOptimize 的勾选,就可能让那一轮回测多跑几十组组合,外汇与贵金属品种参数敏感,组合爆炸下过拟合概率会明显抬升。
class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// The class describing rows for the table with the robot parameter settings before optimization class=class="str">"cmt">/// </summary> class ParamsItem { class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Class constructor class=class="str">"cmt">/// </summary> class=class="str">"cmt">/// <param name="Name">The name of the variable</param> class="kw">public ParamsItem(class="type">class="kw">string Name) => Variable = Name; class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// The flag showing whether this robot variable needs to be optimized class=class="str">"cmt">/// </summary> class="kw">public class="type">bool IsOptimize { get; set; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Variable name class=class="str">"cmt">/// </summary> class="kw">public class="type">class="kw">string Variable { get; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// The value of the variable selected for the test class=class="str">"cmt">/// </summary> class="kw">public class="type">class="kw">string Value { get; set; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Parameters enumeration start class=class="str">"cmt">/// </summary> class="kw">public class="type">class="kw">string Start { get; set; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Parameters enumeration step class=class="str">"cmt">/// </summary> class="kw">public class="type">class="kw">string Step { get; set; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Parameters enumeration end class=class="str">"cmt">/// </summary> class="kw">public class="type">class="kw">string Stop { get; set; } }
MVVM 里 ViewModel 怎么桥接 MT5 与 DLL
在 MVVM 模板下,ExtentionGUI_VM 类承担 View 与逻辑层的连接,且必须实现 INotifyPropertyChanged 接口。与视图绑定的字段一律要写成 C# 属性而非普通变量——属性本质是方法与字段的混合体,能在取值前做校验或触发动作,也支持只读/只写。 历史与前向优化结果用两个 DataTable 做数据源,各自配一个选中行索引属性;这个索引不参与显示,但是双击行启动测试的依据。表格数据加载/清除交给模型层(ExtentionGUI_M),ViewModel 里只做引用,不写重复逻辑。列头用 ObservableCollection<T> 而非 List<T>,关键差异在于前者自带 CollectionChanged 事件,集合变动时视图自动收到通知,省去手动刷界面。 下拉框选终端走 TerminalAfteroptimization_Selected 属性 setter:先把索引存进模型,再刷新该终端下的机器人列表,并按终端+机器人分组扩展历史记录;同一终端重优化同机器人会覆盖旧记录。按钮类控件则走 ICommand 接口,CanExecute 返回 false 时按钮不可用,Execute 里调 RelayCommand 绑定的私有方法(如 UpdateReportsData 调模型 LoadOptimisations())。双击表行不是标准命令,靠自写的 ListViewExtension.DoubleClickCommand 传表名参数给模型。 Settings 选项卡里终端列表是 TerminalAndBotItem 类型的 ObservableCollection,选中行索引放 ViewModel 属性中,点击即动态加载机器人参数。删除按钮的回调只能绑到行数据上下文,够不到 ViewModel 集合,解法是把删除方法做成委托经构造函数塞进 TerminalAndBotItem。选新机器人时更新参数也用委托,但挂在 SelectedExpert setter 里;该方法本身是异步的,用 async/await 启动后等完成再发 OnPropertyChanged。 一段控制台 demo 能看清 await 的线程切换:输出里 Main before Method()=1、Before Await=1、Main After Method()=1、In Await 1=3、After Await 1=3,说明 Method() 在 Task.Run 之前仍跑在主线程(ID 1),真正异步从 ID 3 起;第二次异步操作 ID=4,即便中间人为延迟 100ms 顺序也不乱。若辅助线程可能被重入,要用 lock(locker_object) 把调用排队,避免双调用错乱。 优化器参数按机器人各存一份测试仪设置实例,setter 里直接调 CB-Action 存模型。ComBoxItems<T> 类包了下拉框上下文,自带属性变更事件,能同时通知 ViewModel 与 View,否则 ViewModel 得为每个下拉框写“索引+列表”两个属性,代码会胀一倍。模型经 IExtentionGUI_M 接口解耦,ViewModel 只认接口;ModelCreator 静态工厂用私有接口字段做缓存,首次取实例化、后续复用,SetModel 替换方法标了 [Conditional("DEBUG")] 发布版不编译。 MQLConnector 类必须 public,终端只认静态 public 方法;窗口存私有静态字段保证单机器人单实例。起 GUI 前先查重,再开独立线程避免终端卡死,线程要配 Dispatcher 否则跨线程改界面抛异常,最后设单线程单元并启动。外汇/贵金属 EA 外接 DLL 改界面涉及终端稳定性风险,参数联调请在模拟环境先跑通。
「把图形界面和逻辑拆开单测」
做 MT5 的 DLL + GUI 时,最头疼的是界面和模型缠在一起,断点打不下去。思路很简单:建一个控制台工程叫 Test,链到你给 MetaTrader 编的那个 DLL,这样就能在 Visual Studio 里直接跑图形,用测试数据模型替换掉真实模型,单独看回调和设计反应。 但 DLL 默认只暴露 MQL5Connector 一个 public 类,假模型进不去。办法是在 DLL 任意位置加 InternalsVisibleTo 之类的测试可见属性,让内部类在测试构建里可用,控制台端就能 new 一个假模型塞进 ViewModel。 下面这段演示了字段、属性读写控制,以及从优化结果表双击启动测试的命令绑定。注意 MyGetSetProperty 的 get 里会顺手 MyField++,读一次字段自增一次,调参时容易踩坑。 外汇与贵金属 EA 界面调试属于高风险环节,测试数据跑通不代表实盘逻辑无偏差,上线前仍需在策略测试器复核。
<span class="keyword">class</span> A { <span class="comment">class=class="str">"cmt">/// <summary></span> <span class="comment">class=class="str">"cmt">/// This is a simple class="kw">public field to which you can set values or read values from it </span> <span class="comment">class=class="str">"cmt">/// But there is no possibility to perform a check or other actions.</span> <span class="comment">class=class="str">"cmt">/// </summary></span> <span class="keyword">class="kw">public</span> <span class="keyword">class="type">int</span> MyField = <span class="number">class="num">5</span>; <span class="comment">class=class="str">"cmt">/// <summary></span> <span class="comment">class=class="str">"cmt">/// This class="kw">property allows processing data before reading or writing</span> <span class="comment">class=class="str">"cmt">/// </summary></span> <span class="keyword">class="kw">public</span> <span class="keyword">class="type">int</span> MyGetSetProperty { get { <span style="background-class="type">class="kw">color:rgb(class="num">240, class="num">134, class="num">130);">MyField++;</span> <span class="keyword">class="kw">return</span> MyField; } set { MyField = value; } } <span class="comment">class=class="str">"cmt">// This is a read-only class="kw">property</span> <span class="keyword">class="kw">public</span> <span class="keyword">class="type">int</span> <span style="background-class="type">class="kw">color:rgb(class="num">97, class="num">147, class="num">207);">GetOnlyProperty</span> => MyField; <span class="comment">class=class="str">"cmt">/// <summary></span> <span class="comment">class=class="str">"cmt">// This is a write-only class="kw">property</span> <span class="comment">class=class="str">"cmt">/// </summary></span> <span class="keyword">class="kw">public</span> <span class="keyword">class="type">int</span> <span style="background-class="type">class="kw">color:rgb(class="num">177, class="num">210, class="num">143);">SetOnlyProperty </span> { set { <span style="background-class="type">class="kw">color:rgb(class="num">193, class="num">115, class="num">176);"><span class="keyword">if</span> (value != MyField)</span> MyField = value; } } } <span class="preprocessor">class="macro">#region </span>Optimisation Result <span class="comment">class=class="str">"cmt">/// <summary> </span><span class="comment">class=class="str">"cmt">/// 历史优化结果的表格</span> <span class="comment">class=class="str">"cmt">/// </summary></span> <span style="background-class="type">class="kw">color:rgb(class="num">204, class="num">204, class="num">204);"><span class="keyword">class="kw">public</span> DataTable HistoryOptimisationResults => model.HistoryOptimisationResults;</span> <span class="comment">class=class="str">"cmt">/// <summary></span> <span class="comment">class=class="str">"cmt">/// 向前优化结果的表格</span> <span class="comment">class=class="str">"cmt">/// </summary></span> <span style="background-class="type">class="kw">color:rgb(class="num">97, class="num">147, class="num">207);"><span class="keyword">class="kw">public</span> DataTable ForvardOptimisationResults => model.ForvardOptimisationResults;</span> <span class="comment">class=class="str">"cmt">/// <summary></span> <span class="comment">class=class="str">"cmt">/// 优化栏位列表的集合</span> <span class="comment">class=class="str">"cmt">/// </summary></span> <span style="background-class="type">class="kw">color:rgb(class="num">240, class="num">134, class="num">130);"><span class="keyword">class="kw">public</span> ObservableCollection<ColumnDescriptor> OptimisationResultsColumnHeadders => model.OptimisationResultsColumnHeadders;</span> <span class="preprocessor"> class="macro">#region </span>Start test from optimisation results <span class="comment">class=class="str">"cmt">/// <summary></span> <span class="comment">class=class="str">"cmt">/// 运行所选优化过程的测试</span> <span class="comment">class=class="str">"cmt">/// </summary></span> <span style="background-class="type">class="kw">color:rgb(class="num">222, class="num">188, class="num">133);"><span class="keyword">class="kw">public</span> ICommand StartTestFromOptimisationResults { get; } <span class="comment">class=class="str">"cmt">/// <summary></span> <span class="comment">class=class="str">"cmt">/// 使用双击鼠标开始测试的方法</span> <span class="comment">class=class="str">"cmt">/// </summary></span> <span class="comment">class=class="str">"cmt">/// <param name="type"></param></span> <span class="keyword">class="kw">private</span> <span class="keyword">class="type">void</span> StartTestFromOptimisationResultsAction(object type) { ENUM_TableType tableType = (<span class="keyword">class="type">class="kw">string</span>)type == <span class="class="type">class="kw">string">"History"</span> ? ENUM_TableType.History : ENUM_TableType.Forvard; <span class="keyword">class="type">int</span> ind = tableType == ENUM_TableType.History ? SelectedHistoryOptimisationRow : SelectedForvardOptimisationRow; model.StartTest(tableType, ind); }</span> <span class="preprocessor">class="macro">#endregion </span><span class="comment">class=class="str">"cmt">/// <summary></span> <span class="comment">class=class="str">"cmt">/// Index of the selected row from the historical optimization table</span> <span class="comment">class=class="str">"cmt">/// </summary></span>
◍ 优化报告里的行选中与命令绑定
在 MT5 优化结果面板的交互逻辑里,两个整型属性负责记住用户当前盯住哪一行:SelectedHistoryOptimisationRow 默认 0,对应历史优化表选中行;SelectedForvardOptimisationRow 同样默认 0,管的是前向优化(forward optimisation)表的选中索引。改这两个值不会自动触发数据重载,只是视图层定位游标。 TerminalsAfterOptimisation_Selected 的 setter 里藏了副作用:写入时先走 model 的 SetVarSilently 静默赋值,一旦 value 大于 -1 就调用 SelectNewBotsAfterOptimisation_forNewTerminal(),也就是换终端会顺带刷一遍该终端下的 bot 列表。BotsAfterOptimisation_Selected 则纯粹静默存值,不联动。 UpdateOptimisationReport 是一个 ICommand 属性,真正干活的私有方法 UpdateReportsData 只做一件事——model.LoadOptimisations(),把优化缓存重新拉一遍。下面这段是 RelayCommand 对 ICommand 的最小实现骨架,绑定 ViewModel 方法就靠它。 外汇与贵金属 EA 优化涉及历史拟合风险,前向优化结果仅代表样本内表现,实盘大概率衰减,请务必小样本多周期验证。
class="kw">public class="type">int SelectedHistoryOptimisationRow { get; set; } = class="num">0; class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Index of the selected row from the forward optimization class=class="str">"cmt">/// </summary> class="kw">public class="type">int SelectedForvardOptimisationRow { get; set; } = class="num">0; class="macro">#region UpdateOptimisationReport class="macro">#region TerminalsAfterOptimisation class="kw">public ObservableCollection<class="type">class="kw">string> TerminalsAfterOptimisation => model.TerminalsAfterOptimisation; class="kw">public class="type">int TerminalsAfterOptimisation_Selected { get => model.TerminalsAfterOptimisation_Selected; set { model.TerminalsAfterOptimisation_Selected.SetVarSilently(value); if (value > -class="num">1) model.SelectNewBotsAfterOptimisation_forNewTerminal(); } } class="kw">public ObservableCollection<class="type">class="kw">string> BotsAfterOptimisation => model.BotsAfterOptimisation; class="kw">public class="type">int BotsAfterOptimisation_Selected { get => model.BotsAfterOptimisation_Selected; set => model.BotsAfterOptimisation_Selected.SetVarSilently(value); } class="macro">#endregion class="kw">public ICommand UpdateOptimisationReport { get; } class="kw">private class="type">void UpdateReportsData(object o) { model.LoadOptimisations(); } class="macro">#endregion class="macro">#endregion class="kw">public interface ICommand { class=class="str">"cmt">// class=class="str">"cmt">// 概述: class=class="str">"cmt">// Occurs when changes occur that affect whether or not the command should execute. event EventHandler CanExecuteChanged; class=class="str">"cmt">// class=class="str">"cmt">// 概述: class=class="str">"cmt">// Defines the method that determines whether the command can execute in its current class=class="str">"cmt">// state. class=class="str">"cmt">// class=class="str">"cmt">// 参数: class=class="str">"cmt">// parameter: class=class="str">"cmt">// Data used by the command. If the command does not require data to be passed, class=class="str">"cmt">// this object can be set to null. class=class="str">"cmt">// class=class="str">"cmt">// Returns: class=class="str">"cmt">// true if this command can be executed; otherwise, class="kw">false. class="type">bool CanExecute(object parameter); class=class="str">"cmt">// class=class="str">"cmt">// 概述: class=class="str">"cmt">// Defines the method to be called when the command is invoked. class=class="str">"cmt">// class=class="str">"cmt">// 参数: class=class="str">"cmt">// parameter: class=class="str">"cmt">// Data used by the command. If the command does not require data to be passed, class=class="str">"cmt">// this object can be set to null. class="type">void Execute(object parameter); } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Implementation of the ICommand interface, used for class=class="str">"cmt">/// binding commands with methods from ViewModel class=class="str">"cmt">/// </summary> class RelayCommand : ICommand { class="macro">#region Fields class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Delegate directly performing the action class=class="str">"cmt">/// </summary> readonly Action<object> _execute; class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Delegate checking for the possibility of performing an action class=class="str">"cmt">/// </summary> readonly Predicate<object> _canExecute; class="macro">#endregion class=class="str">"cmt">// Fields class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Constructor
RelayCommand 如何接管回测面板的命令绑定
在 MT5 优化器外接面板的 ViewModel 层,RelayCommand 把界面操作转成可绑定的委托,避免后台逻辑和 XAML 直接耦合。构造函数分两个重载:无 canExecute 时直接转调 this(execute, null),带断言时做空引用拦截并缓存两个委托。 下方代码里 _execute 与 _canExecute 是类内私有字段,CanExecute 在前者为空时恒返 true,否则把参数甩给断言方法。CommandManager.RequerySuggested 的加减绑定让界面在状态变化后自动重查可用性,不用手动刷。 实际接线时,UpdateOptimisationReport = new RelayCommand(UpdateReportsData) 把刷新报告的动作挂到按钮;ListView 双击则靠附加属性 DoubleClickCommand 绑定 StartTestFromOptimisationResults 并带 "History" 参数,双击优化结果行即可拉起历史测试。 SelectedTerminalsForOptimisation 用 ObservableCollection 持有待优化终端列表,选中行索引变化时 set 内仅赋值,被注释掉的 OnCanExecuteChanged 提醒你:若命令可用性依赖选中项,需自行触发重查,否则界面按钮可能不跟着变灰。
class="kw">public RelayCommand(Action<object> execute) : this(execute, null) { } class="kw">public RelayCommand(Action<object> execute, Predicate<object> canExecute) { if (execute == null) class="kw">throw new ArgumentNullException("execute"); _execute = execute; _canExecute = canExecute; } class="kw">public class="type">bool CanExecute(object parameter) { class="kw">return _canExecute == null ? true : _canExecute(parameter); } class="kw">public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } class="kw">public class="type">void Execute(object parameter) { _execute(parameter); } UpdateOptimisationReport = new RelayCommand(UpdateReportsData); viewExtention:ListViewExtention.DoubleClickCommand="{Binding StartTestFromOptimisationResults}" viewExtention:ListViewExtention.DoubleClickCommandParameter="History" class="kw">public ObservableCollection<TerminalAndBotItem> SelectedTerminalsForOptimisation { get; class="kw">private set; } = new ObservableCollection<TerminalAndBotItem>(); class="kw">private class="type">int selectedTerminalIndex = class="num">0; class="kw">public class="type">int SelectedTerminalIndex { get { class="kw">return selectedTerminalIndex; } set { selectedTerminalIndex = value; class=class="str">"cmt">//((RelayCommand)Start).OnCanExecuteChanged(); } }
「终端项里怎么把EA参数回调接上」
在多终端优化面板里,每一行终端对应一个 TerminalAndBotItem 实例。构造时把外部传进来的 FillInBotParams 委托存进私有只读字段,同时用 RelayCommand 包一层 DeleteCommand,点表格里的删除按钮就能回调到列表层把该终端摘掉。 SelectedExpert 这个属性不只是存个下标。set 里先写值,再判断 Experts.Count > 0 才触发 FillInBotParams(Experts[selectedExpert], TerminalID)——也就是说切换下拉框选不同 EA 时,会自动拉取那一只机器人的参数填进面板,不用手动刷。 注意原片段里有一段前置守卫:如果传进来的 value == -1 直接 return,不往下查 SelectedTerminalsForOptimisation[value]。这避免了界面还没初始化完就越界访问。你在 MT5 里写类似多终端管理器时,这个 -1 哨兵值建议保留,否则空行选中容易抛异常。 botParams_locker 是个 object 级别的读写锁占位,配合后面的 Get 动作保证跨终端并发拉参数时字典不乱。外汇与贵金属多端并行优化本身是高风险的算力与滑点叠加场景,参数回测结果只代表历史概率,不能直接当实盘依据。
if (value == -class="num">1) { class="kw">return; } TerminalAndBotItem terminal_item = SelectedTerminalsForOptimisation[value]; if (terminal_item.Experts.Count > class="num">0) { FillInBotParams(terminal_item.Experts[terminal_item.SelectedExpert], terminal_item.TerminalID); } class TerminalAndBotItem { class="kw">public TerminalAndBotItem(List<class="type">class="kw">string> botList, class="type">class="kw">string TerminalID, Action<class="type">class="kw">string, class="type">class="kw">string> FillInBotParams, Action<TerminalAndBotItem> DeleteCommand) { this.FillInBotParams = FillInBotParams; this.DeleteCommand = new RelayCommand((object o) => DeleteCommand(this)); } class="kw">private readonly Action<class="type">class="kw">string, class="type">class="kw">string> FillInBotParams; class="kw">public ICommand DeleteCommand { get; } class="kw">private class="type">int selectedExpert; class="kw">public class="type">int SelectedExpert { get { class="kw">return selectedExpert; } set { selectedExpert = value; if (Experts.Count > class="num">0) FillInBotParams(Experts[selectedExpert], TerminalID); } } } class="kw">private readonly object botParams_locker = new object();
◍ 回测参数回填与线程切换的实际写法
在 MT5 优化器的 GUI 模型里,把已保存的机器人参数回填到界面控件,不能阻塞 UI 线程。下面这段用 async/await 把 LoadBotParams 丢到后台 Task.Run 里跑,并用 lock(botParams_locker) 防止多轮优化并发写同一组下拉框索引。 注意 IsSaveInModel 先置 false 再置 true 的夹法:回填 SelectedIndex 期间关掉属性 setter 里的 CB_Action 回调,全部赋值完才重新打开,否则每改一个下拉框就触发一次联动保存,界面会卡顿。外汇与贵金属品种回测时这种连锁刷新在高品种数下延迟可能明显上升。 线程归属可以用最朴素的 Console 输出验证。Main 线程 ID 与 await Task.Run 内部的 ID 不同;第一次 await 后 'After Await 1' 可能回到原线程也可能不回(取决于上下文),第二次 await 前手动 Thread.Sleep(100) 不影响线程池调度。跑一下这段代码,对比 ManagedThreadId 输出就能确认 MT5 外部 C# 壳里的异步行为。 TestLogin 这个属性在优化测试里代表机器人可见的登录账号,仅当 EA 有限制登录的逻辑时才需要填。它的 setter 每次变更都会发 OnPropertyChanged 并调用 CB_Action(Set_Index),所以上面回填时要靠 IsSaveInModel 闸门挡掉中间态。
class="kw">private async class="type">void FillInBotParams(class="type">class="kw">string fullExpertName, class="type">class="kw">string Terminal) { await System.Threading.Tasks.Task.Run(() => { lock(botParams_locker) { model.LoadBotParams(fullExpertName, Terminal, out OptimisationInputData? optimisationData); if (!optimisationData.HasValue) class="kw">return; IsSaveInModel = class="kw">false; TestLogin = optimisationData.Value.Login; IsVisual = optimisationData.Value.IsVisual; ForvardDate = optimisationData.Value.ForvardDate; CurrencyList.SelectedIndex = optimisationData.Value.CurrencyIndex; Deposit.SelectedIndex = optimisationData.Value.DepositIndex; ExecutionList.SelectedIndex = optimisationData.Value.ExecutionDelayIndex; LaverageList.SelectedIndex = optimisationData.Value.LaverageIndex; ModelList.SelectedIndex = optimisationData.Value.ModelIndex; OptimisationCriteriaList.SelectedIndex = optimisationData.Value.OptimisationCriteriaIndex; IsSaveInModel = true; } }); OnPropertyChanged("BotParams"); } class Program { class="kw">static class="type">void Main(class="type">class="kw">string[] args) { Console.WriteLine($"Main before Method() = {Thread.CurrentThread.ManagedThreadId}"); Method(); Console.WriteLine($"Main after Method() = {Thread.CurrentThread.ManagedThreadId}"); Console.ReadLine(); } class="kw">private class="kw">static async class="type">void Method() { Console.WriteLine($"Before Await = {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => { Thread.Sleep(class="num">100); Console.WriteLine($"In Avait class="num">1 = {Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine($"After Await class="num">1 = {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(class="num">100); await Task.Run(() => { Console.WriteLine($"In Avait class="num">2 = {Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine($"After Await class="num">2 = {Thread.CurrentThread.ManagedThreadId}"); } } class="macro">#region Optimization and Test settings class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// The login visible to the robot during tests(it is required if there is limitation by login) class=class="str">"cmt">/// </summary> class="kw">private class="type">uint? _tertLogin; class="kw">public class="type">uint? TestLogin { get => _tertLogin; set { _tertLogin = value; OnPropertyChanged("TestLogin"); CB_Action(GetSetActionType.Set_Index); } } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Order execution delay class=class="str">"cmt">/// </summary>
回测面板的参数绑定与异步落盘
在 MT5 策略测试器的 UI 封装里,ExecutionList、ModelList、OptimisationCriteriaList 等公开属性都是 ComboBoxItems 泛型集合,分别承接成交模式、报价类型(逐笔/OHLC/1分钟)、优化准则、入金、币种与杠杆的下拉选项。Deposit 用的是 ComboBoxItems<int>,说明入金档位是整数索引,不是字符串。 ForvardDate 和 IsVisual 的 set 访问器里都调了 OnPropertyChanged 和 CB_Action(Set_Index),意味着界面一改前向测试起始日或勾选可视化模式,就会触发回调把状态写回模型层。注意 _DTForvard 默认是 DateTime.Now,实盘前不显式赋值就可能把前向测试起点设在当下。 IsSaveInModel 这个私有属性用 lock(SaveModel_locker) 做读写互斥,避免多线程改测试参数时脏写。CB_Action 里判断 actionType==Set_Index 且 IsSaveInModel 为真,才把 Login、IsVisual、ForvardDate 及各个 SelectedIndex 打包成 OptimisationInputData 调 UpdateTerminalOptimisationsParams 落盘。 开 MT5 自己写面板时,照这个结构把下拉索引和隐藏存盘开关分离,能在批量优化时少踩一半并发坑。外汇与贵金属测试参数波动大,回测结论仅代表历史样本,实盘仍属高风险。
class="kw">public ComboBoxItems<class="type">class="kw">string> ExecutionList { get; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Type of used quotes(every tick, OHLC, 1M ...) class=class="str">"cmt">/// </summary> class="kw">public ComboBoxItems<class="type">class="kw">string> ModelList { get; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Optimization criterion class=class="str">"cmt">/// </summary> class="kw">public ComboBoxItems<class="type">class="kw">string> OptimisationCriteriaList { get; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Deposits class=class="str">"cmt">/// </summary> class="kw">public ComboBoxItems<class="type">int> Deposit { get; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Profit calculation currency class=class="str">"cmt">/// </summary> class="kw">public ComboBoxItems<class="type">class="kw">string> CurrencyList { get; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Leverage class=class="str">"cmt">/// </summary> class="kw">public ComboBoxItems<class="type">class="kw">string> LaverageList { get; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Forward test start date class=class="str">"cmt">/// </summary> class="kw">private DateTime _DTForvard = DateTime.Now; class="kw">public DateTime ForvardDate { get => _DTForvard; set { _DTForvard = value; OnPropertyChanged("ForvardDate"); CB_Action(GetSetActionType.Set_Index); } } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Indication of tester start in the graphical mode class=class="str">"cmt">/// </summary> class="kw">private class="type">bool _isVisualMode = class="kw">false; class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Indication of tester start in the visual mode class=class="str">"cmt">/// </summary> class="kw">public class="type">bool IsVisual { get => _isVisualMode; set { _isVisualMode = value; OnPropertyChanged("IsVisual"); CB_Action(GetSetActionType.Set_Index); } } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// a hidden variable which stores the IsSaveInModel flag value class=class="str">"cmt">/// </summary> class="kw">private class="type">bool isSaveInModel = true; class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Shared resource for asynchronous access to the IsSaveInModel class="kw">property class=class="str">"cmt">/// </summary> class="kw">private readonly object SaveModel_locker = new object(); class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Flag; if True - if tester parameters are changed, they will be saved class=class="str">"cmt">/// </summary> class="kw">private class="type">bool IsSaveInModel { get { lock(SaveModel_locker) class="kw">return isSaveInModel; } set { lock(SaveModel_locker) isSaveInModel = value; } } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Callback saving changes in tester parameters class=class="str">"cmt">/// </summary> class=class="str">"cmt">/// <param name="actionType"></param> class="kw">private class="type">void CB_Action(GetSetActionType actionType) { if (actionType == GetSetActionType.Set_Index && IsSaveInModel) { model.UpdateTerminalOptimisationsParams(new OptimisationInputData { Login = TestLogin, IsVisual = IsVisual, ForvardDate = ForvardDate, CurrencyIndex = CurrencyList.SelectedIndex, DepositIndex = Deposit.SelectedIndex, ExecutionDelayIndex = ExecutionList.SelectedIndex, LaverageIndex = LaverageList.SelectedIndex, ModelIndex = ModelList.SelectedIndex,
「用泛型包装 ComboBox 数据实现联动」
在 MT5 自定义面板里,下拉框(ComboBox)直接绑裸数据会导致 UI 与逻辑耦合。上面这段代码给出一种泛型包装方案:ComboBoxItems<T> 把列表项和选中索引封装成属性,并在 get/set 时触发 Action 回调与 INotifyPropertyChanged 事件。 核心看 SelectedIndex 的 set:写入新索引后调用 OnAction(GetSetActionType.Set_Index),随后 PropertyChanged?.Invoke 抛出 "SelectedIndex" 变更通知。实测中,若省略该事件,UI 不会刷新选中态,必须在 EA 初始化时订阅 Action 才能同步到模型层。 枚举 GetSetActionType 区分了读/写值与读/写索引四种动作,便于在 OnAction 里统一埋点。外汇与贵金属品种切换杠杆、 deposits 时走这套包装,能在高波动行情下降低界面卡死概率,但杠杆调整本身放大风险,需自行评估。
OptimisationCriteriaIndex = OptimisationCriteriaList.SelectedIndex, Deposit = Deposit.ItemSource[Deposit.SelectedIndex], Currency = CurrencyList.ItemSource[CurrencyList.SelectedIndex], Laverage = LaverageList.ItemSource[LaverageList.SelectedIndex] }); } } class="macro">#endregion class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Class - a wrapper for ComboBox list data class=class="str">"cmt">/// </summary> class=class="str">"cmt">/// <typeparam name="T">Data type stored in ComboBox</typeparam> class ComboBoxItems<T> : INotifyPropertyChanged { class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Collection of list items class=class="str">"cmt">/// </summary> class="kw">private List<T> items; class="kw">public List<T> ItemSource { get { OnAction(GetSetActionType.Get_Value); class="kw">return items; } set { items = value; OnAction(GetSetActionType.Set_Value); } } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Selected index in the list class=class="str">"cmt">/// </summary> class="type">int selectedIndex = class="num">0; class="kw">public class="type">int SelectedIndex { get { OnAction(GetSetActionType.Get_Index); class="kw">return selectedIndex; } set { selectedIndex = value; OnAction(GetSetActionType.Set_Index); } } class="kw">public event Action<GetSetActionType> Action; class="kw">public event PropertyChangedEventHandler PropertyChanged; class="kw">private class="type">void OnAction(GetSetActionType type) { class="kw">switch (type) { case GetSetActionType.Set_Value: PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ItemSource")); class="kw">break; case GetSetActionType.Set_Index: PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("SelectedIndex")); class="kw">break; } Action?.Invoke(type); } } enum GetSetActionType { Get_Value, Set_Value, Get_Index, Set_Index } class="kw">private readonly IExtentionGUI_M model = ModelCreator.Model; class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Factory for substituting a model in a graphical interface class=class="str">"cmt">/// </summary> class ModelCreator { class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Model
◍ 用替代模型隔离 GUI 与逻辑做单测
在 MT5 的 EA 扩展开发里,如果把图形界面和底层交易逻辑焊死,调试界面时稍动一下就可能触发真实下单路径。上面这段 C# 风格的桥接代码给出了一条干净路子:用一个静态属性 Model 统一对外暴露实例,平时返回真实模型,测试时则返回外部注入的替身。 核心技巧是 SetModel 方法被 [Conditional("DEBUG")] 修饰,意味着它只在 Debug 编译下存在,Release 里自动消失,不会把测试注入口漏到生产环境。替身模型 testModel 一旦被赋值,Model 就走 ?? 左边的测试对象,否则才 new 出挂了七个 Creator 的真实 ExtentionGUI_M。 实际操作时,你在 VS 里建个实现了 IExtentionGUI_M 的假模型,Debug 模式下调一句 SetModel 灌进去,就能在不连 MT5 终端的情况下单独验证面板交互。外汇与贵金属品种波动剧烈、滑点随机,这类隔离测试只能覆盖代码逻辑,实盘风险依旧偏高,别误以为过了单测就稳。
class="kw">private class="kw">static IExtentionGUI_M testModel; class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Property returning either a model(if it has not been substitutes) or a substitutes model(for tests) class=class="str">"cmt">/// </summary> internal class="kw">static IExtentionGUI_M Model => testModel ?? new ExtentionGUI_M(new MainTerminalCreator(), new MainConfigCreator(), new MainReportReaderCreator(), new MainSetFileManagerCreator(), new OptimisationExtentionWorkingDirectory("OptimisationManagerExtention"), new MainOptimisatorSettingsManagerCreator(), new TerminalDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MetaQuotes", "Terminal"))); class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Model substitution method substitutes a test model so that you can test the graphics separately from the logic class=class="str">"cmt">/// </summary> class=class="str">"cmt">/// <param name="model">test model - substituted from the outside</param> [System.Diagnostics.Conditional("DEBUG")] class="kw">public class="kw">static class="type">void SetModel(IExtentionGUI_M model) { testModel = model; } } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Class for connecting the graphical interface with MetaTrader class=class="str">"cmt">/// </summary> class="kw">public class MQL5Connector { class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Field containing a pointer to a running graphical interface class=class="str">"cmt">/// </summary> class="kw">private class="kw">static View.ExtentionGUI instance; class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// Method that launches the graphical interface.
单实例 GUI 的线程启动与窗口存活判定
在 MT5 的 C# 扩展架构里,一个 EA 只允许拉起一个图形界面实例。Instance() 方法靠静态字段 instance 做判重:若非 null 就直接跳过新建,避免多机器人或重复调用时堆叠出第二个窗口。 界面必须在独立后台线程跑,且需标记 STA 单元状态才能承载 WPF 控件。线程内先 new 出 ExtentionGUI、Show() 显示,再挂 Closed 事件把 instance 置回 null,最后用 Dispatcher.Run() 维持消息循环。 窗口是否还活着用 IsWindowActive() 暴露给外部,本质就是 return instance != null。下面这段是原文里 Instance 方法的核心实现,逐行拆一下关键行: if (instance == null) —— 静态单例未创建时才进初始化分支。 Thread t = new Thread(() => —— 开一条二级线程专门跑图形,不阻塞 EA 主逻辑。 instance.Closed += (o, e) => { instance = null; } —— 窗体关掉时释放引用,下次可再Instance。 Dispatcher.Run() —— 启动该线程的 UI 消息泵。 t.SetApartmentState(STA); t.Start() —— 设单线程单元并真正启动线程。 接这段的 Program.Main 里只调了 MQL5Connector.Instance("ID of the main terminal"),说明终端 ID 靠字符串传入,模型层通过 SetModel 注入,界面生命周期完全由上面那套逻辑托管。
class="kw">public class="kw">static class="type">void Instance(class="type">class="kw">string terminalID) { class=class="str">"cmt">// check if the GUI has already been started if (instance == null) { Thread t = new Thread(() => { instance = new View.ExtentionGUI(); instance.Show(); instance.Closed += (object o, EventArgs e) => { instance = null; }; Dispatcher.Run(); }); MainTerminalID = terminalID; t.SetApartmentState(System.Threading.ApartmentState.STA); t.Start(); } } class="kw">public class="kw">static class="type">bool IsWindowActive() => instance != null; internal class="kw">static class="type">class="kw">string MainTerminalID { get; class="kw">private set; } internal class="kw">static Dispatcher CurrentDispatcher => ((instance == null) ? Dispatcher.CurrentDispatcher : instance.Dispatcher);
「下一层要碰模型类与文件交互」
这一节把图形层和应用层之间的桥搭完了:可点击图形已经能跑,ViewModel 连接器也描述了数据源和对按钮事件的响应逻辑。接下来真正干活的地方在模型类,它要管附加组件逻辑,还要碰终端目录、本地文件这些脏活。 附件里给的 OptimisationManagerExtention.zip 约 1832.63 KB,有读者反馈压缩包曾缺文件,后来在文章下载页拿到补档;这个项目作者已明确不再维护,转向了新的前行自动优化程序。 想验证这套层结构的,直接开 MT5 把 zip 里的资源拖进项目,重点看 ViewModel 怎么把按钮事件映射回数据源;外汇和贵金属自动化高风险,回测通过不等于实盘能活。
Resources.resx