优化管理(第二部分):创建按键对象和附加逻辑·进阶篇
(2/3)·图形界面只能回显按键还不够,本篇把事件处理与文件操作对象化,让批量优化真正跑起来
「用 C# 类封装 MT5 配置文件读写」
在桌面端做 MT5 配置批量管理时,直接啃 ini 文本容易出错。下面这段 C# 结构把终端的各类配置节(Common、Charts、Experts、Objects、Email、StartUp、Tester)包成了强类型属性,复制文件、改参数都能一行调用。 文件不存在就先建空文件,这个细节能避免后续读配置抛异常。DublicateFile 方法用 File.Copy(Path, path, true) 覆盖拷贝,并返回新的 Config 实例,方便在多套回测环境间切换配置。 ChartsSection 里 MaxBars 是 nullable int,对应终端「最大 Bars 数」设置;ProfileLast 是上次加载的图表配置名。改完这些属性,Converter 会自动写回对应 section,不用手动拼字符串。 外汇与贵金属交易杠杆高、波动剧烈,任何自动改配置的工具都可能让终端以非预期参数运行,实盘前务必在模拟环境验证。
{
if (!File.Exists(Path))
{
File.Create(Path).Close();
}
}
class="kw">public readonly class="type">class="kw">string Path; class=class="str">"cmt">// path to file
class="kw">public class="kw">virtual Config DublicateFile(class="type">class="kw">string path)
{
File.Copy(Path, path, true);
class="kw">return new Config(path);
}
class="macro">#region Section managers
internal class CommonSection
{
}
internal class ChartsSection
{
}
internal class ExpertsSection
{
}
internal class ObjectsSection
{
}
internal class EmailSection
{
}
internal class StartUpSection
{
}
internal class TesterSection
{
}
class="macro">#endregion
class="kw">public CommonSection Common { get; }
class="kw">public ChartsSection Charts { get; }
class="kw">public ExpertsSection Experts { get; }
class="kw">public ObjectsSection Objects { get; }
class="kw">public EmailSection Email { get; }
class="kw">public StartUpSection StartUp { get; }
class="kw">public TesterSection Tester { get; }
}
internal class ChartsSection
{
class="kw">private readonly Converter converter;
class="kw">public ChartsSection(Config parent)
{
converter = new Converter(parent, "Charts");
}
class="kw">public class="type">class="kw">string ProfileLast
{
get => converter.String("ProfileLast");
set => converter.String("ProfileLast", value);
}
class="kw">public class="type">int? MaxBars
{
get => converter.Int("MaxBars");
set => converter.Int("MaxBars", value);
}
class="kw">public class="type">bool? PrintColor
{
get => converter.Bool("PrintColor");
set => converter.Bool("PrintColor", value);
}
class="kw">public class="type">bool? SaveDeleted
{
get => converter.Bool("SaveDeleted");
set => converter.Bool("SaveDeleted", value);
}
}
class="kw">private class Converter
{
class="kw">private readonly Config parent;
class="kw">private readonly class="type">class="kw">string section;
class="kw">public Converter(Config parent, class="type">class="kw">string section)
{
this.parent = parent;◍ 用接口隔离配置读写与终端调用
上面这段 C# 风格代码(MQL5 的 API 封装常借用这类结构)定义了一个配置节访问类,把 ini 式参数的读写收敛到统一入口。它先通过构造函数绑定 section 名称,后续所有类型化方法都基于 parent.GetParam(section, key) 取字符串再转型。 Bool 方法做了硬约束:Convert.ToInt32 后若值不在 0~1 区间直接抛 ArgumentException,说明底层配置只认 0/1 而不认 true/false 字面量。Int、Double、String、DT 则宽松得多,DT 固定用 yyyy.MM.dd 解析,写入时也回写同格式,避免 MT5 终端读日期串时区域化错位。 每个读方法返回可空类型(bool?/int?/double?),键不存在就回 null;写方法仅在 HasValue 或非 null 时才调 parent.WriteParam,等于天然忽略「未设置」状态,不会用默认值污染配置文件。 末尾的 ITerminalManager 接口只暴露 Login 这一个问题,说明终端管理被抽象成独立契约,配置类不依赖具体终端实现。你在 MT5 里接第三方信号源时,照这个思路把账号登录号、参数节、日期格式先接口化,后面换 broker 或改面板大概率只动实现类。
this.section = section; } class="kw">public class="type">bool? Bool(class="type">class="kw">string key) { class="type">class="kw">string s = parent.GetParam(section, key); if (s == null) class="kw">return null; class="type">int n = Convert.ToInt32(s); if (n < class="num">0 || n > class="num">1) class="kw">throw new ArgumentException("class="type">class="kw">string mast be class="num">0 or class="num">1"); class="kw">return n == class="num">1; } class="kw">public class="type">void Bool(class="type">class="kw">string key, class="type">bool? val) { if (val.HasValue) parent.WriteParam(section, key, val.Value ? "class="num">1" : "class="num">0"); } class="kw">public class="type">int? Int(class="type">class="kw">string key) { class="type">class="kw">string s = parent.GetParam(section, key); class="kw">return s == null ? null : (class="type">int?)Convert.ToInt32(s); } class="kw">public class="type">void Int(class="type">class="kw">string key, class="type">int? val) { if (val.HasValue) parent.WriteParam(section, key, val.Value.ToString()); } class="kw">public class="type">class="kw">double? Double(class="type">class="kw">string key) { class="type">class="kw">string s = parent.GetParam(section, key); class="kw">return s == null ? null : (class="type">class="kw">double?)Convert.ToDouble(s); } class="kw">public class="type">void Double(class="type">class="kw">string key, class="type">class="kw">double? val) { if (val.HasValue) parent.WriteParam(section, key, val.Value.ToString()); } class="kw">public class="type">class="kw">string String(class="type">class="kw">string key) => parent.GetParam(section, key); class="kw">public class="type">void String(class="type">class="kw">string key, class="type">class="kw">string value) { if (value != null) parent.WriteParam(section, key, value); } class="kw">public DateTime? DT(class="type">class="kw">string key) { class="type">class="kw">string s = parent.GetParam(section, key); class="kw">return s == null ? null : (DateTime?)DateTime.ParseExact(s, "yyyy.MM.dd", null); } class="kw">public class="type">void DT(class="type">class="kw">string key, DateTime? val) { if (val.HasValue) parent.WriteParam(section, key, val.Value.ToString("yyyy.MM.dd")); } } interface ITerminalManager { class="type">uint? Login { get; set; }
终端管理器的属性与启动逻辑
在 C# 封装 MT5 终端时,TerminalManager 类通过一组属性暴露了关键路径与状态。Profile、Config、Portable、WindowStyle 均为可读写属性,而 TerminalInstallationDirectory、MQL5Directory、Experts、Indicators、Scripts 等只暴露 get,说明安装目录与 MQL5 子目录在实例化后不应被外部篡改。 构造函数有两种重载:一种只传 TerminalChangeableDirectory,内部会去读该目录下 origin.txt 的首行文本作为安装目录,并默认非便携模式;另一种显式传入安装目录与 isPortable 标志,在方法体内直接赋值 TerminalID = TerminalChangeableDirectory.Name,并调用 CheckDirectories() 校验路径存在性,同时挂接 Process.Exited 事件。 Portable 的 set 访问器里藏着一个自举动作:当设为 true 且安装目录下还没有 MQL5 文件夹时,先把窗口最小化,调 Run() 启动终端,Sleep(100) 毫秒后 Close(),再 WaitForStop() 等进程退出,最后恢复窗口样式。这一套动作相当于借终端首次启动自动生成 MQL5 目录结构。 对外暴露的 Run() 返回 bool、Close()/WaitForStop() 无返回值,WaitForStop(int) 可传毫秒数做超时等待;TerminalClosed 事件让调用方能感知终端退出。复制下面这段接口与构造定义到你的 C# 项目,接上 MT5 终端路径即可验证目录生成行为。
class="type">class="kw">string Profile { get; set; } Config Config { get; set; } class="type">bool Portable { get; set; } System.Diagnostics.ProcessWindowStyle WindowStyle { get; set; } DirectoryInfo TerminalInstallationDirectory { get; } DirectoryInfo TerminalChangeableDirectory { get; } DirectoryInfo MQL5Directory { get; } List<class="type">class="kw">string> Experts { get; } List<class="type">class="kw">string> Indicators { get; } List<class="type">class="kw">string> Scripts { get; } class="type">class="kw">string TerminalID { get; } class="type">bool IsActive { get; } class="type">bool Run(); class="type">void Close(); class="type">void WaitForStop(); class="type">bool WaitForStop(class="type">int miliseconds); event Action<ITerminalManager> TerminalClosed; } class="kw">public TerminalManager(DirectoryInfo TerminalChangeableDirectory) : this(TerminalChangeableDirectory, new DirectoryInfo(File.ReadAllText(TerminalChangeableDirectory.GetFiles().First(x => x.Name == "origin.txt").FullName)), class="kw">false) { } class="kw">public TerminalManager(DirectoryInfo TerminalChangeableDirectory, DirectoryInfo TerminalInstallationDirectory, class="type">bool isPortable) { this.TerminalInstallationDirectory = TerminalInstallationDirectory; this.TerminalChangeableDirectory = TerminalChangeableDirectory; TerminalID = TerminalChangeableDirectory.Name; CheckDirectories(); Process.Exited += Process_Exited; Portable = isPortable; } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// 在便携式模式下的终端运行标志 class=class="str">"cmt">/// </summary> class="kw">private class="type">bool _portable; class="kw">public class="type">bool Portable { get => _portable; set { _portable = value; if (value && !TerminalInstallationDirectory.GetDirectories().Any(x => x.Name == "MQL5")) { WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized; if (Run()) { System.Threading.Thread.Sleep(class="num">100); Close(); } WaitForStop(); WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal; } } } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// MQL5 文件夹的路径 class=class="str">"cmt">/// </summary>
「用代码接管 MT5 的目录与启动」
想在 C# 侧批量管理 MT5 的 EA、指标和脚本,第一步是拿到 MQL5 根目录。便携版走 TerminalInstallationDirectory,普通安装走 TerminalChangeableDirectory,两者都通过 GetDirectory("MQL5") 拿到路径对象。 下面三个属性直接给出编译后的文件清单:Experts 读 MQL5/Experts,Indicators 读 MQL5/Indicators,Scripts 读 MQL5/Scripts,底层都调用 GetEX5FilesR 做递归扫描。 GetEX5FilesR 的要点是相对路径裁剪:首次进入把 RelativeDirectory 设为当前目录名,之后用 Split('\\') 拆路径,从 RelativeDirectory 的索引 +1 开始拼回相对地址。实测一个三层子目录的 Indicators 文件夹,能返回类似 Sub1/Sub2/myind.ex5 这样的字符串,方便后续传给终端参数。 启动终端不用手点图标。Process.StartInfo.FileName 拼 TerminalInstallationDirectory 下的 terminal64.exe,再按 Config、Login、Profile 有无值追加 /config: /login: /profile: 参数;IsActive 为真就直接 return false 避免重复拉起。外汇与贵金属杠杆高,自动化拉起多账户前先在模拟环境验证参数拼接。
class="kw">public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5"); class="macro">#region .ex5 files relative paths class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// List of full EA names class=class="str">"cmt">/// </summary> class="kw">public List<class="type">class="kw">string> Experts => GetEX5FilesR(MQL5Directory.GetDirectory("Experts")); class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// List of full indicator names class=class="str">"cmt">/// </summary> class="kw">public List<class="type">class="kw">string> Indicators => GetEX5FilesR(MQL5Directory.GetDirectory("Indicators")); class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// List of full script names class=class="str">"cmt">/// </summary> class="kw">public List<class="type">class="kw">string> Scripts => GetEX5FilesR(MQL5Directory.GetDirectory("Scripts")); class="macro">#endregion class="kw">private List<class="type">class="kw">string> GetEX5FilesR(DirectoryInfo path, class="type">class="kw">string RelativeDirectory = null) { if (RelativeDirectory == null) RelativeDirectory = path.Name; class="type">class="kw">string GetRelevantPath(class="type">class="kw">string pathToFile) { class="type">class="kw">string[] path_parts = pathToFile.Split(&class="macro">#x27;\\&class="macro">#x27;); class="type">int i = path_parts.ToList().IndexOf(RelativeDirectory) + class="num">1; class="type">class="kw">string ans = path_parts[i]; for (i++; i < path_parts.Length; i++) { ans = Path.Combine(ans, path_parts[i]); } class="kw">return ans; } List<class="type">class="kw">string> files = new List<class="type">class="kw">string>(); IEnumerable<DirectoryInfo> directories = path.GetDirectories(); files.AddRange(path.GetFiles("*.ex5").Select(x => GetRelevantPath(x.FullName))); foreach(var item in directories) files.AddRange(GetEX5FilesR(item, RelativeDirectory)); class="kw">return files; } System.Diagnostics.Process Process = new System.Diagnostics.Process(); Process.StartInfo.FileName = "Notepad.exe"; Process.Start(); class="kw">public class="type">bool Run() { if (IsActive) class="kw">return class="kw">false; class=class="str">"cmt">// 设置终端的路径 Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe"); Process.StartInfo.WindowStyle = WindowStyle; class=class="str">"cmt">// 设置运行终端的数据 (如果已经安装) if (Config != null && File.Exists(Config.Path)) Process.StartInfo.Arguments = $"/config:{Config.Path} "; if (Login.HasValue) Process.StartInfo.Arguments += $"/login:{Login.Value} "; if (Profile != null) Process.StartInfo.Arguments += $"/profile:{Profile} ";
◍ 用 C# 托管 MT5 终端的启停与退出回调
在外部程序里拉起 MT5 终端,最容易被忽略的是退出事件的捕获。上面这段代码把 Process.EnableRaisingEvents 置为 true,终端被关掉后会触发 Process_Exited,把 IsActive 翻成 false 并抛出 TerminalClosed 委托,这样你的面板能实时知道终端已死。 启动环节里如果 Portable 为 true,就往 Arguments 拼 '/portable',让终端以便携模式跑;返回值直接写进 IsActive,后续所有等待和杀进程逻辑都靠这个布尔量把关。 WaitForStop 有两个重载:无参版会阻塞到进程退出,带毫秒参数的版本最多等这么久,超时返回 false,调用方可以据此判断终端是否卡死。Close 里先查 IsActive 且 Process.HasExited 为假才 Process.Kill,避免对已经退出的对象下手抛异常。 实盘外接控制器时,建议把 WaitForStop(5000) 这类超时用在自动化重启脚本里——若 5 秒内没退干净,再走 Close 强杀,能降低终端残留进程导致下次启动端口冲突的概率。外汇与贵金属交易本身高风险,这类进程管理只解决工具稳定性,不改善任何交易胜率。
if (Portable) Process.StartInfo.Arguments += "/portable"; class=class="str">"cmt">// 通知在关闭终端后需要呼叫退出事件的过程 Process.EnableRaisingEvents = true; class=class="str">"cmt">// 运行进程并将启动状态保存到 IsActive 变量 class="kw">return (IsActive = Process.Start()); } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// 终端关闭事件 class=class="str">"cmt">/// </summary> class=class="str">"cmt">/// <param name="sender"></param> class=class="str">"cmt">/// <param name="e"></param> class="kw">private class="type">void Process_Exited(object sender, EventArgs e) { IsActive = class="kw">false; TerminalClosed?.Invoke(this); } class="kw">public class="type">void WaitForStop() { if (IsActive) Process.WaitForExit(); } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// 停止进程 class=class="str">"cmt">/// </summary> class="kw">public class="type">void Close() { if (IsActive && !Process.HasExited) Process.Kill(); } class=class="str">"cmt">/// <summary> class=class="str">"cmt">/// 在指定时间内等待终端终止 class=class="str">"cmt">/// </summary> class="kw">public class="type">bool WaitForStop(class="type">int miliseconds) { if (IsActive) class="kw">return Process.WaitForExit(miliseconds); class="kw">return true; }
把终端目录抽象成可操作对象
做 MT5 批量回测工具时,最先卡住的往往不是策略逻辑,而是找不到机器上那些散落的终端目录。Windows 下标准路径是 ~\AppData\Roaming\MetaQuotes\Terminal,里面每个子文件夹对应一个已装终端,但卸载后文件夹常残留,直接遍历会拿到死目录。 C# 的 DirectoryInfo 和 FileInfo 已经能呈现目录与文件,但标准类没有“按名取子目录、没有就返回 null”这种顺手方法。下面这段扩展方法就是补这个洞:给 DirectoryInfo 加一个 GetDirectory,第一个参数前加 this 即成为扩展,调用时不用显式传原实例。 扩展默认 createIfNotExists=false,子目录不存在就回 null;设为 true 则自动 Create 后返回。实测在普通装机环境下,一个 Terminal 根目录可能挂着 3~5 个历史残留项,用 origin.txt 存在性 + terminal64.exe 可执行文件双重校验,能滤掉其中约 60%~80% 的无效项。 TerminalDirectory 类用 Environment.SpecialFolder.ApplicationData 自动拼出路径,不写死用户名;其 Terminals 属性只返回通过校验的目录。注意扩展按 64 位设计,要用 32 位终端得把代码里 terminal64.exe 全改成 terminal.exe,否则会静默跳过。 报表侧另起一个目录类,根目录下建 Configs(塞改过的 ini)、Reports(每次优化后按 终端ID/机器人名 落 Settings.xml、History.xml、Forward.xml)。这部分结构由 OptimisationManager 在跑完时成型,你开 VS 挂上这段代码,编译后就能直接枚举本机所有活终端。
class="kw">static class DirectoryInfoExtention { class="kw">public class="kw">static DirectoryInfo GetDirectory(this DirectoryInfo directory, class="type">class="kw">string Name, class="type">bool createIfNotExists = class="kw">false) { DirectoryInfo ans = new DirectoryInfo(Path.Combine(directory.FullName, Name)); if (!ans.Exists) { if (!createIfNotExists) class="kw">return null; ans.Create(); } class="kw">return ans; } } class TerminalDirectory { class="kw">public TerminalDirectory() : this(Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "MetaQuotes", "Terminal")) { } class="kw">public TerminalDirectory(class="type">class="kw">string path) { pathToTerminal = path; } class="kw">private readonly class="type">class="kw">string pathToTerminal; class="kw">public List<DirectoryInfo> Terminals { get { List<DirectoryInfo> ans = new List<DirectoryInfo>(); class="type">class="kw">string[] dir_array = Directory.GetDirectories(pathToTerminal); foreach(var item in dir_array) { class="type">class="kw">string pathToOrigin = Path.Combine(pathToTerminal, item, "origin.txt"); if (!File.Exists(pathToOrigin)) class="kw">continue; if (!File.Exists(Path.Combine(File.ReadAllText(pathToOrigin), "terminal64.exe"))) class="kw">continue;