优化修改正常

This commit is contained in:
2026-07-06 04:24:43 +08:00
parent 160c68da39
commit 15716894bc
4 changed files with 178 additions and 74 deletions
+16 -17
View File
@@ -1,35 +1,34 @@
backtest_settings:
currency: USD
date_range:
from: 2018.01.01
to: 2026.06.04
deposit: 1000.0
execution_delay: 100
from: 2024.01.01
to: 2025.12.31
deposit: 10000
execution_delay: 0
forward_date: ''
forward_mode: 2
leverage: 1:500
forward_mode: 0
leverage: 1:100
model: 0
optimization: 0
replace_report: true
shutdown_terminal: true
symbols:
- EURUSD
- XAUUSD
timeframes:
- M5
- H1
visual: 0
eas:
- filename: MY-EA\without source code\The Gold Reaper MT5_4.3_fix.ex5
name: The Gold Reaper MT5_4.3_fix
eas: []
execution:
kill_between: true
skip_existing: true
timeout_per_test: 300000
# mt5_settings: 路径支持占位符与自动检测(见 scripts/mt5_paths.py
# {PROJECT_ROOT} → 项目根目录绝对路径(推荐,确保项目拷到任意位置都能跑)
# 留空字符串 → 启动时自动扫描常见位置(Program Files / %APPDATA%/MetaQuotes/Terminal
# 绝对路径 → 直接使用(适配定制安装或便携版)
timeout_per_test: 600
# MT5 路径说明:
# 这部分会被 GUI 接管(启动时从 optimizer/mt5_state.json 读取;失焦时写回 mt5_state.json)。
# 留空也没事,GUI 自己会用上次保存的值。
mt5_settings:
terminal_path: "" # 留空=自动检测; 或填 "C:\\Program Files\\MetaTrader 5 EXNESS\\terminal64.exe"
data_dir: "" # 留空=自动检测; 或填 "%APPDATA%\\MetaQuotes\\Terminal\\<hash>"
terminal_path: ""
data_dir: ""
ini_dir: "{PROJECT_ROOT}/config/generated"
reports_dir: "{PROJECT_ROOT}/reports"
+149 -53
View File
@@ -121,22 +121,56 @@ def _clear_mt5_chart_workspace(data_dir):
pass
def _launch_mt5(mt5_path, ini_path, data_dir):
def _launch_mt5(mt5_path, ini_path, data_dir, log_lines: list = None):
"""启动 MT5 终端跑测试。同步等 5 秒检查进程还活着,活着就让它跑。"""
if log_lines is None:
log_lines = []
_clear_mt5_chart_workspace(data_dir)
process = subprocess.Popen(
[mt5_path, "/config:" + os.path.abspath(ini_path)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
startupinfo=_mt5_startupinfo(),
)
threading.Thread(
target=_minimize_mt5_windows, args=(process.pid,), daemon=True
).start()
cmd = [mt5_path, "/config:" + os.path.abspath(ini_path)]
log_lines.append(f" 命令: {' '.join(cmd)!s}")
if not os.path.isfile(mt5_path):
log_lines.append(f" [ERR] terminal64.exe 不存在: {mt5_path!r}")
return None
try:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=_mt5_startupinfo(),
)
except OSError as e:
log_lines.append(f" [ERR] 启动失败: {e!r}")
return None
except Exception as e:
log_lines.append(f" [ERR] 异常: {e!r}")
return None
log_lines.append(f" pid={process.pid}")
# 同步短等待(5s),检查是否启动后立即挂掉
try:
process.wait(timeout=5.0)
log_lines.append(f" [WARN] MT5 进程在 5s 内退出, returncode={process.returncode}")
try:
err = (process.stderr.read() or b"").decode('utf-8', 'replace')
if err.strip():
log_lines.append(f" stderr: {err[:500]}")
except Exception:
pass
return process
except subprocess.TimeoutExpired:
log_lines.append(" MT5 正常运行中(5s 后仍在跑)")
threading.Thread(target=_minimize_mt5_windows, args=(process.pid,), daemon=True).start()
return process
class MT5BacktestGUI:
_CONFIG_FILE = "config/ea_configs.yaml"
_CONFIG_FILE = "config/ea_configs.yaml" # 仅给"回测配置"标签页用
_MT5_STATE_FILE = "optimizer/mt5_state.json" # 优化器用的 MT5 路径,独立小文件
def __init__(self, root):
self.root = root
@@ -152,26 +186,40 @@ class MT5BacktestGUI:
self._load_config_to_gui()
def _load_or_default_config(self):
"""已保存过配置就读 YAML;第一次用走默认(终端路径留空让用户填)。"""
"""回测配置(yaml + MT5 路径(optimizer/mt5_state.json 优先,缺失字段从 yaml 兜底)。"""
default = self._load_default_config()
# 1) 优先读 mt5_state.json(路径专用小文件)
if os.path.isfile(self._MT5_STATE_FILE):
try:
with open(self._MT5_STATE_FILE, "r", encoding="utf-8") as f:
state = json.load(f)
for k in ("terminal_path", "data_dir", "reports_dir", "ini_dir"):
if k in state and state[k]:
default["mt5_settings"][k] = state[k]
_gui_log(f"已加载 MT5 路径: {self._MT5_STATE_FILE}")
except Exception as e:
_gui_log(f"读 mt5_state 失败 ({e})")
# 2) 用 yaml 兜底(老数据 / 用户编辑过的字段;mt5_state 已有的路径字段不覆盖)
if os.path.isfile(self._CONFIG_FILE):
try:
with open(self._CONFIG_FILE, "r", encoding="utf-8") as f:
saved = yaml.safe_load(f) or {}
default = self._load_default_config()
# 浅合并:saved 覆盖 default;缺失字段用 default 补全
for k in ("backtest_settings", "mt5_settings", "execution"):
if k in saved:
default[k].update(saved[k])
if "eas" in saved:
if k in saved and isinstance(saved.get(k), dict):
for kk, vv in saved[k].items():
if vv in (None, "", []):
continue
# mt5_state 已经填了相同字段,跳过 yaml 的(避免覆盖最新值)
if k == "mt5_settings" and kk in ("terminal_path", "data_dir", "reports_dir", "ini_dir"):
if default["mt5_settings"].get(kk):
continue
default[k][kk] = vv
if "eas" in saved and saved["eas"]:
default["eas"] = saved["eas"]
# 占位符解析(仅做 {PROJECT_ROOT} 展开,无自动探测)
if resolve_mt5_settings:
default["mt5_settings"] = resolve_mt5_settings(default["mt5_settings"])
_gui_log(f"已加载保存的配置: {self._CONFIG_FILE}")
return default
_gui_log(f"已加载配置: {self._CONFIG_FILE}")
except Exception as e:
_gui_log(f"配置失败 ({e}),使用默认")
return self._load_default_config()
_gui_log(f" yaml 失败 ({e}),使用默认")
return default
def _load_default_config(self):
return {
@@ -433,18 +481,21 @@ class MT5BacktestGUI:
self.mt5_path = ttk.Entry(path_frame, width=50)
self.mt5_path.insert(0, self.config["mt5_settings"].get("terminal_path", ""))
self.mt5_path.grid(row=0, column=1, padx=5, pady=3, sticky='ew')
self.mt5_path.bind('<FocusOut>', lambda e: self._persist_path_settings())
ttk.Button(path_frame, text="浏览...", command=self._browse_mt5).grid(row=0, column=2, padx=5)
ttk.Label(path_frame, text="MT5数据目录 (含 MQL5/Experts 的那一层):").grid(row=1, column=0, sticky='w', padx=5, pady=3)
self.data_dir = ttk.Entry(path_frame, width=50)
self.data_dir.insert(0, self.config["mt5_settings"].get("data_dir", ""))
self.data_dir.grid(row=1, column=1, padx=5, pady=3, sticky='ew')
self.data_dir.bind('<FocusOut>', lambda e: self._persist_path_settings())
ttk.Button(path_frame, text="浏览...", command=self._browse_data).grid(row=1, column=2, padx=5)
ttk.Label(path_frame, text="报告输出目录:").grid(row=2, column=0, sticky='w', padx=5, pady=3)
self.reports_dir = ttk.Entry(path_frame, width=50)
self.reports_dir.insert(0, self.config["mt5_settings"].get("reports_dir", ""))
self.reports_dir.grid(row=2, column=1, padx=5, pady=3, sticky='ew')
self.reports_dir.bind('<FocusOut>', lambda e: self._persist_path_settings())
ttk.Button(path_frame, text="浏览...", command=self._browse_reports).grid(row=2, column=2, padx=5)
path_hint = ttk.Label(path_frame,
@@ -591,6 +642,24 @@ class MT5BacktestGUI:
yaml.dump(config, f, allow_unicode=True)
messagebox.showinfo("保存成功", "配置已保存到 config/ea_configs.yaml")
def _persist_path_settings(self):
"""三个路径输入框失焦时调用:写到 optimizer/mt5_state.json(不写 yaml)。"""
try:
self.config.setdefault("mt5_settings", {})
self.config["mt5_settings"]["terminal_path"] = self.mt5_path.get()
self.config["mt5_settings"]["data_dir"] = self.data_dir.get()
self.config["mt5_settings"]["reports_dir"] = self.reports_dir.get()
reports_parent = os.path.dirname(self.reports_dir.get() or ".") or "."
self.config["mt5_settings"]["ini_dir"] = os.path.join(reports_parent, "config", "generated")
os.makedirs(os.path.dirname(self._MT5_STATE_FILE) or ".", exist_ok=True)
state = {k: self.config["mt5_settings"][k] for k in ("terminal_path", "data_dir", "reports_dir", "ini_dir")}
with open(self._MT5_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False, indent=2)
_gui_log(f"已保存 MT5 路径: {self._MT5_STATE_FILE}")
except Exception as e:
_gui_log(f"自动保存失败: {e}")
def _on_start_backtest(self):
_gui_log("开始回测 button clicked")
if _IMPORT_ERRORS:
@@ -1477,15 +1546,23 @@ class MT5BacktestGUI:
def _refresh_opt_ea_list(self):
configs_dir = os.path.join(self.opt_dir.get(), 'configs')
if not os.path.exists(configs_dir):
self.opt_log.insert('end', f"配置目录不存在: {configs_dir}\n")
self.opt_log.insert('end', f" [ERR] 配置目录不存在: {configs_dir}\n")
self.opt_log.insert('end', f" 创建方法: mkdir {configs_dir}\n")
return
sys.path.insert(0, os.path.join(os.getcwd(), 'scripts', 'optimizer'))
from ea_config_parser import load_all_ea_configs
configs = load_all_ea_configs(configs_dir)
if not configs:
self.opt_log.insert('end', f" [WARN] {configs_dir} 下没有 JSON 配置\n")
self.opt_log.insert('end', f" 请把 EA 的参数配置 .json 放进该目录\n")
self.opt_ea_combo['values'] = []
return
installed_names = None
mt5_data = self.config.get('mt5_settings', {}).get('data_dir', '')
if mt5_data:
mt5_data = self.data_dir.get()
if not mt5_data:
self.opt_log.insert('end', f" [WARN] '回测配置' 里 MT5 数据目录为空,跳过 EA 存在性校验\n")
else:
try:
from ea_auto_scanner import EAAutoScanner
scanner = EAAutoScanner(mt5_data)
@@ -1494,9 +1571,13 @@ class MT5BacktestGUI:
installed_names = {e['name'] for e in experts}
for e in experts:
self.opt_ea_paths[e['name']] = e['path']
else:
self.opt_log.insert('end', f" [WARN] {mt5_data}\\MQL5\\Experts 下没有 .ex5 文件\n")
except Exception as e:
self.opt_log.insert('end', f"扫描MT5失败,跳过校验: {e}\n")
installed_names = None
import traceback
self.opt_log.insert('end', f" [ERR] 扫描MT5失败: {e}\n")
self.opt_log.insert('end', f" 请确认 '回测配置' 里的 MT5 数据目录正确\n")
self.opt_log.insert('end', f" 目录应包含 MQL5\\Experts 子目录\n")
filtered_names = []
removed_names = []
@@ -1520,7 +1601,7 @@ class MT5BacktestGUI:
self.opt_log.insert('end', f"已加载 {len(configs)} 个EA配置\n")
def _auto_scan_eas(self):
mt5_data = self.config.get('mt5_settings', {}).get('data_dir', '')
mt5_data = self.data_dir.get()
if not mt5_data:
messagebox.showwarning('警告', '请先在回测配置中设置MT5数据目录')
return
@@ -1563,7 +1644,9 @@ class MT5BacktestGUI:
self.opt_ea_var.set(ea_list[0])
self._auto_load_ea_with_set(mt5_data, ea_list[0])
def _auto_load_ea_with_set(self, mt5_data, ea_name):
def _auto_load_ea_with_set(self, mt5_data, ea_name=None):
if ea_name is None:
ea_name = self.opt_ea_var.get()
for item in self.param_tree.get_children():
self.param_tree.delete(item)
@@ -1596,7 +1679,7 @@ class MT5BacktestGUI:
def _on_ea_selected(self, event=None):
ea_name = self.opt_ea_var.get()
if ea_name:
mt5_data = self.config.get('mt5_settings', {}).get('data_dir', '')
mt5_data = self.data_dir.get()
self._auto_load_ea_with_set(mt5_data, ea_name)
def _load_ea_params(self, ea_name):
@@ -1605,7 +1688,7 @@ class MT5BacktestGUI:
configs_dir = os.path.join(self.opt_dir.get(), 'configs')
sys.path.insert(0, os.path.join(os.getcwd(), 'scripts', 'optimizer'))
from ea_config_parser import load_ea_config
mt5_data = self.config.get('mt5_settings', {}).get('data_dir', '')
mt5_data = self.data_dir.get()
config_path = os.path.join(configs_dir, ea_name + '.json')
if os.path.exists(config_path):
@@ -1894,9 +1977,9 @@ class MT5BacktestGUI:
if not ea_name:
messagebox.showwarning('警告', '请先选择一个EA')
return
mt5_path = self.config.get('mt5_settings', {}).get('terminal_path', '')
if not mt5_path or not os.path.exists(mt5_path):
messagebox.showerror('错误', 'MT5终端路径无效')
mt5_path = self.mt5_path.get()
if not mt5_path or not os.path.isfile(mt5_path):
messagebox.showerror('错误', f'MT5终端路径无效: {mt5_path!r}\n请在"回测配置"标签页用"浏览..."按钮设置 terminal64.exe')
return
sys.path.insert(0, os.path.join(os.getcwd(), 'scripts', 'optimizer'))
from ea_batch_optimizer import EABatchOptimizer
@@ -1908,7 +1991,7 @@ class MT5BacktestGUI:
return
try:
config = load_ea_config(config_path)
optimizer = EABatchOptimizer(self.opt_dir.get())
optimizer = EABatchOptimizer(self.opt_dir.get(), mt5_data_dir=self.data_dir.get())
self.opt_log.insert('end', f"正在生成优化配置...\n")
self.opt_log.update()
ini_path = optimizer.generate_ini_for_ea(config, None)
@@ -1917,9 +2000,12 @@ class MT5BacktestGUI:
abs_ini_path = os.path.abspath(ini_path)
self.opt_log.insert('end', "正在启动MT5优化器...\n")
self.opt_log.update()
mt5_data_dir = self.config.get('mt5_settings', {}).get('data_dir', '')
_launch_mt5(mt5_path, abs_ini_path, mt5_data_dir)
self.opt_log.insert('end', "MT5已启动\n")
mt5_data_dir = self.data_dir.get()
log_lines: list = []
_launch_mt5(mt5_path, abs_ini_path, mt5_data_dir, log_lines=log_lines)
for line in log_lines:
self.opt_log.insert('end', line + "\n")
self.opt_log.update()
mode = self.opt_optimization_mode.get().split('(')[0] if '(' in self.opt_optimization_mode.get() else '遗传算法'
self.opt_log.insert('end', f"MT5优化器运行中({mode})\n")
self.opt_log.insert('end', "优化完成后点击'分析结果'查看\n")
@@ -1927,9 +2013,14 @@ class MT5BacktestGUI:
self.opt_log.insert('end', f"错误: {str(e)}\n")
def _run_batch_optimize(self):
mt5_path = self.config.get('mt5_settings', {}).get('terminal_path', '')
if not mt5_path or not os.path.exists(mt5_path):
messagebox.showerror('错误', 'MT5终端路径无效')
_gui_log("点击: 批量执行优化")
mt5_path = self.mt5_path.get()
if not mt5_path or not os.path.isfile(mt5_path):
messagebox.showerror('错误', f'MT5终端路径无效: {mt5_path!r}\n请在"回测配置"标签页用"浏览..."按钮设置 terminal64.exe')
return
mt5_data_dir = self.data_dir.get()
if not mt5_data_dir or not os.path.isdir(mt5_data_dir):
messagebox.showerror('错误', f'MT5数据目录无效: {mt5_data_dir!r}\n请在"回测配置"标签页设置 MT5 数据目录')
return
sys.path.insert(0, os.path.join(os.getcwd(), 'scripts', 'optimizer'))
from ea_batch_optimizer import EABatchOptimizer
@@ -1945,16 +2036,12 @@ class MT5BacktestGUI:
messagebox.showwarning('警告', '没有找到EA配置')
return
result = messagebox.askyesno('确认', f'将对 {len(configs)} 个EA进行批量优化\n优化完成后自动进入下一个\n\n是否继续?')
if not result:
return
self.opt_log.insert('end', f"开始批量优化,共 {len(configs)} 个EA\n")
self.opt_log.insert('end', "每个EA优化完成后会自动关闭MT5并启动下一个\n")
self.opt_log.update()
_gui_log(f"开始批量优化,共 {len(configs)} 个EA")
optimizer = EABatchOptimizer(self.opt_dir.get())
mt5_data_dir = self.config.get('mt5_settings', {}).get('data_dir', '')
optimizer = EABatchOptimizer(self.opt_dir.get(), mt5_data_dir=self.data_dir.get())
reports_dir = os.path.join(self.opt_dir.get(), 'reports')
os.makedirs(reports_dir, exist_ok=True)
@@ -1967,8 +2054,17 @@ class MT5BacktestGUI:
abs_ini_path = os.path.abspath(ini_path)
self.opt_log.insert('end', f"启动MT5: {ea_name}\n")
self.opt_log.update()
_gui_log(f"启动MT5: {ea_name}, ini={abs_ini_path}")
process = _launch_mt5(mt5_path, abs_ini_path, mt5_data_dir)
log_lines: list = []
process = _launch_mt5(mt5_path, abs_ini_path, mt5_data_dir, log_lines=log_lines)
for line in log_lines:
self.opt_log.insert('end', line + "\n")
_gui_log(line)
self.opt_log.update()
if process is None:
self.opt_log.insert('end', "启动失败,跳过该EA\n")
continue
xml_name = ea_name + '_optimization.xml'
xml_path = os.path.join(mt5_data_dir, xml_name)
@@ -2031,8 +2127,8 @@ class MT5BacktestGUI:
sys.path.insert(0, os.path.join(os.getcwd(), 'scripts', 'optimizer'))
from ea_batch_optimizer import EABatchOptimizer
from analyze_results import ResultAnalyzer
optimizer = EABatchOptimizer(self.opt_dir.get())
mt5_terminal_dir = self.config.get('mt5_settings', {}).get('data_dir', '')
optimizer = EABatchOptimizer(self.opt_dir.get(), mt5_data_dir=self.data_dir.get())
mt5_terminal_dir = self.data_dir.get()
self.opt_log.insert('end', "正在分析结果...\n")
self.opt_log.update()
@@ -2127,4 +2223,4 @@ def main():
if __name__ == "__main__":
main()
main()
+6
View File
@@ -0,0 +1,6 @@
[04:15:21] 已加载配置: config/ea_configs.yaml
[04:15:39] 已加载配置: config/ea_configs.yaml
[04:15:39] 已加载 MT5 路径: optimizer/mt5_state.json
[04:15:39] 已加载配置: config/ea_configs.yaml
[04:15:39] 已加载 MT5 路径: optimizer/mt5_state.json
[04:15:39] 已加载配置: config/ea_configs.yaml
+7 -4
View File
@@ -13,12 +13,12 @@ from ea_config_parser import load_ea_config, load_all_ea_configs, EAConfig
from parameter_constraint import create_constraint_engine
from smart_search import SmartSearch
from analyze_results import ResultAnalyzer
from mt5_paths import auto_detect_data_dir
class EABatchOptimizer:
def __init__(self, base_dir: str = 'optimizer'):
def __init__(self, base_dir: str = 'optimizer', mt5_data_dir: str = ''):
self.base_dir = base_dir
self.mt5_data_dir = mt5_data_dir
self.configs_dir = os.path.join(base_dir, 'configs')
self.set_files_dir = os.path.join(base_dir, 'set_files')
self.reports_dir = os.path.join(base_dir, 'reports')
@@ -97,8 +97,11 @@ class EABatchOptimizer:
ini_path = os.path.join(self.configs_dir, ini_filename)
set_filename = ea_config.ea_name + '_optimization.set'
data_dir = auto_detect_data_dir() or r'C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal\010E047102812FC0C18890992854220E'
mt5_tester_dir = os.path.join(data_dir, 'MQL5', 'Profiles', 'Tester')
if not self.mt5_data_dir:
raise FileNotFoundError(
"EABatchOptimizer: mt5_data_dir 未设置。请在 GUI '回测配置' 里设置 MT5 数据目录"
)
mt5_tester_dir = os.path.join(self.mt5_data_dir, 'MQL5', 'Profiles', 'Tester')
set_path = os.path.join(mt5_tester_dir, set_filename)
date_from = test_config.get('from_date', '2025.01.01')