|
|
|
@@ -1507,20 +1507,28 @@ class MT5BacktestGUI:
|
|
|
|
|
self.opt_leverage.set("1:100")
|
|
|
|
|
self.opt_leverage.pack(side='left', padx=5)
|
|
|
|
|
|
|
|
|
|
wf_frame = ttk.LabelFrame(test_frame, text="Walk-Forward 防过拟合")
|
|
|
|
|
wf_frame = ttk.LabelFrame(test_frame, text="向前优化 (Forward)")
|
|
|
|
|
wf_frame.pack(fill='x', padx=5, pady=5)
|
|
|
|
|
|
|
|
|
|
wf_row1 = ttk.Frame(wf_frame)
|
|
|
|
|
wf_row1.pack(fill='x', padx=5, pady=3)
|
|
|
|
|
self.wf_enabled = tk.BooleanVar(value=False)
|
|
|
|
|
ttk.Checkbutton(wf_row1, text="启用Walk-Forward", variable=self.wf_enabled).pack(side='left', padx=5)
|
|
|
|
|
ttk.Checkbutton(wf_row1, text="启用向前优化", variable=self.wf_enabled, command=self._update_wf_ui_state).pack(side='left', padx=5)
|
|
|
|
|
|
|
|
|
|
ttk.Label(wf_row1, text="OOS比例:").pack(side='left', padx=5)
|
|
|
|
|
self.wf_mode = ttk.Combobox(wf_row1, width=12, values=['1/2 (50%)', '1/3 (33%)', '1/4 (25%)'])
|
|
|
|
|
ttk.Label(wf_row1, text="模式:").pack(side='left', padx=5)
|
|
|
|
|
self.wf_mode = ttk.Combobox(wf_row1, width=18, values=['1/2 (50%)', '1/3 (33%)', '1/4 (25%)', '自定义日期 (4)'])
|
|
|
|
|
self.wf_mode.set('1/3 (33%)')
|
|
|
|
|
self.wf_mode.pack(side='left', padx=5)
|
|
|
|
|
self.wf_mode.bind('<<ComboboxSelected>>', lambda e: self._update_wf_ui_state())
|
|
|
|
|
|
|
|
|
|
self.wf_mode_map = {'1/2 (50%)': 1, '1/3 (33%)': 2, '1/4 (25%)': 3}
|
|
|
|
|
ttk.Label(wf_row1, text="ForwardDate:").pack(side='left', padx=(10, 5))
|
|
|
|
|
self.wf_custom_date = ttk.Entry(wf_row1, width=12)
|
|
|
|
|
self.wf_custom_date.insert(0, "")
|
|
|
|
|
self.wf_custom_date.pack(side='left', padx=5)
|
|
|
|
|
|
|
|
|
|
self.wf_mode_map = {'1/2 (50%)': 1, '1/3 (33%)': 2, '1/4 (25%)': 3, '自定义日期 (4)': 4}
|
|
|
|
|
self.wf_mode_reverse_map = {v: k for k, v in self.wf_mode_map.items()}
|
|
|
|
|
self._update_wf_ui_state()
|
|
|
|
|
|
|
|
|
|
action_frame = ttk.Frame(opt_frame)
|
|
|
|
|
action_frame.pack(fill='x', padx=5, pady=5)
|
|
|
|
@@ -1693,6 +1701,75 @@ class MT5BacktestGUI:
|
|
|
|
|
config_path = os.path.join(configs_dir, ea_name + '.json')
|
|
|
|
|
if os.path.exists(config_path):
|
|
|
|
|
config = load_ea_config(config_path)
|
|
|
|
|
test_cfg = config.test_config or {}
|
|
|
|
|
wf_cfg = config.walk_forward or {}
|
|
|
|
|
|
|
|
|
|
if test_cfg.get('symbol'):
|
|
|
|
|
self.opt_symbol.set(str(test_cfg.get('symbol')))
|
|
|
|
|
if test_cfg.get('period'):
|
|
|
|
|
self.opt_period.set(str(test_cfg.get('period')))
|
|
|
|
|
if 'model' in test_cfg:
|
|
|
|
|
model_val = int(test_cfg.get('model', 1))
|
|
|
|
|
model_map = {
|
|
|
|
|
0: 'Every Tick (0)',
|
|
|
|
|
1: '1分钟OHLC (1)',
|
|
|
|
|
2: '仅开盘价 (2)',
|
|
|
|
|
3: '数学计算 (3)',
|
|
|
|
|
4: '真实Tick (4)',
|
|
|
|
|
}
|
|
|
|
|
self.opt_model.set(model_map.get(model_val, '1分钟OHLC (1)'))
|
|
|
|
|
if test_cfg.get('from_date'):
|
|
|
|
|
self.opt_from_date.delete(0, tk.END)
|
|
|
|
|
self.opt_from_date.insert(0, str(test_cfg.get('from_date')))
|
|
|
|
|
if test_cfg.get('to_date'):
|
|
|
|
|
self.opt_to_date.delete(0, tk.END)
|
|
|
|
|
self.opt_to_date.insert(0, str(test_cfg.get('to_date')))
|
|
|
|
|
if 'deposit' in test_cfg:
|
|
|
|
|
self.opt_deposit.delete(0, tk.END)
|
|
|
|
|
self.opt_deposit.insert(0, str(test_cfg.get('deposit')))
|
|
|
|
|
if test_cfg.get('leverage'):
|
|
|
|
|
self.opt_leverage.set(str(test_cfg.get('leverage')))
|
|
|
|
|
|
|
|
|
|
exec_delay = int(test_cfg.get('execution_delay', 0) or 0)
|
|
|
|
|
if exec_delay == 0:
|
|
|
|
|
self.opt_delay_type.set('无延迟 (0)')
|
|
|
|
|
elif exec_delay == -1:
|
|
|
|
|
self.opt_delay_type.set('随机延迟 (-1)')
|
|
|
|
|
else:
|
|
|
|
|
self.opt_delay_type.set('固定延迟')
|
|
|
|
|
self.opt_delay_value.delete(0, tk.END)
|
|
|
|
|
self.opt_delay_value.insert(0, str(exec_delay))
|
|
|
|
|
|
|
|
|
|
opt_mode = int(test_cfg.get('optimization_mode', 2) or 2)
|
|
|
|
|
opt_mode_map = {
|
|
|
|
|
0: '禁用 (0)',
|
|
|
|
|
1: '慢速完整算法 (1)',
|
|
|
|
|
2: '快速遗传算法 (2)',
|
|
|
|
|
3: 'MarketWatch所有符号 (3)',
|
|
|
|
|
}
|
|
|
|
|
self.opt_optimization_mode.set(opt_mode_map.get(opt_mode, '快速遗传算法 (2)'))
|
|
|
|
|
|
|
|
|
|
opt_criterion = int(test_cfg.get('optimization_criterion', 1) or 1)
|
|
|
|
|
criterion_map = {
|
|
|
|
|
0: '余额最大值 (0)',
|
|
|
|
|
1: '盈利因子最大值 (1)',
|
|
|
|
|
2: '期望收益 (2)',
|
|
|
|
|
3: '回撤最小值 (3)',
|
|
|
|
|
4: '恢复因子 (4)',
|
|
|
|
|
5: '夏普比率 (5)',
|
|
|
|
|
6: '自定义 (6)',
|
|
|
|
|
7: '复合指标 (7)',
|
|
|
|
|
}
|
|
|
|
|
self.opt_criterion.set(criterion_map.get(opt_criterion, '盈利因子最大值 (1)'))
|
|
|
|
|
|
|
|
|
|
wf_enabled = bool(wf_cfg.get('enabled', False))
|
|
|
|
|
self.wf_enabled.set(wf_enabled)
|
|
|
|
|
wf_mode = int(wf_cfg.get('forward_mode', 2) or 2)
|
|
|
|
|
self.wf_mode.set(self.wf_mode_reverse_map.get(wf_mode, '1/3 (33%)'))
|
|
|
|
|
self.wf_custom_date.delete(0, tk.END)
|
|
|
|
|
self.wf_custom_date.insert(0, str(wf_cfg.get('forward_date', '') or ''))
|
|
|
|
|
self._update_wf_ui_state()
|
|
|
|
|
|
|
|
|
|
if config.parameters:
|
|
|
|
|
for pname, param in config.parameters.items():
|
|
|
|
|
optimize = 'Y' if param.optimize else 'N'
|
|
|
|
@@ -1744,11 +1821,25 @@ class MT5BacktestGUI:
|
|
|
|
|
entry.bind('<Escape>', lambda e: entry.destroy())
|
|
|
|
|
entry.place(x=event.x, y=event.y, width=100)
|
|
|
|
|
|
|
|
|
|
def _save_ea_config(self):
|
|
|
|
|
def _update_wf_ui_state(self):
|
|
|
|
|
"""ForwardDate 仅在启用向前优化且选择自定义日期(4)时可编辑。"""
|
|
|
|
|
enabled = self.wf_enabled.get()
|
|
|
|
|
self.wf_mode.configure(state='readonly' if enabled else 'disabled')
|
|
|
|
|
mode_val = self.wf_mode_map.get(self.wf_mode.get(), 2) if enabled else 0
|
|
|
|
|
self.wf_custom_date.configure(state='normal' if (enabled and mode_val == 4) else 'disabled')
|
|
|
|
|
|
|
|
|
|
def _save_ea_config(self, quiet=False):
|
|
|
|
|
ea_name = self.opt_ea_var.get()
|
|
|
|
|
if not ea_name:
|
|
|
|
|
messagebox.showwarning('警告', '请先选择一个EA')
|
|
|
|
|
return
|
|
|
|
|
if not quiet:
|
|
|
|
|
messagebox.showwarning('警告', '请先选择一个EA')
|
|
|
|
|
return False
|
|
|
|
|
wf_mode_val = self.wf_mode_map.get(self.wf_mode.get(), 2) if self.wf_enabled.get() else 0
|
|
|
|
|
wf_forward_date = self.wf_custom_date.get().strip()
|
|
|
|
|
if self.wf_enabled.get() and wf_mode_val == 4 and not wf_forward_date:
|
|
|
|
|
if not quiet:
|
|
|
|
|
messagebox.showwarning('警告', 'ForwardMode=自定义日期 (4) 时,必须填写 ForwardDate (YYYY.MM.DD)')
|
|
|
|
|
return False
|
|
|
|
|
configs_dir = os.path.join(self.opt_dir.get(), 'configs')
|
|
|
|
|
os.makedirs(configs_dir, exist_ok=True)
|
|
|
|
|
params = {}
|
|
|
|
@@ -1793,12 +1884,16 @@ class MT5BacktestGUI:
|
|
|
|
|
},
|
|
|
|
|
'walk_forward': {
|
|
|
|
|
'enabled': self.wf_enabled.get(),
|
|
|
|
|
'forward_mode': self.wf_mode_map.get(self.wf_mode.get(), 2)
|
|
|
|
|
'forward_mode': wf_mode_val,
|
|
|
|
|
'forward_date': wf_forward_date if (self.wf_enabled.get() and wf_mode_val == 4) else ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
with open(config_path, 'w', encoding='utf-8') as f:
|
|
|
|
|
json.dump(config_data, f, indent=4, ensure_ascii=False)
|
|
|
|
|
self.opt_log.insert('end', f"配置已保存: {config_path}\n")
|
|
|
|
|
if not quiet:
|
|
|
|
|
self.opt_log.update()
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def _get_opt_execution_delay(self):
|
|
|
|
|
delay_type = self.opt_delay_type.get()
|
|
|
|
@@ -1977,6 +2072,13 @@ class MT5BacktestGUI:
|
|
|
|
|
if not ea_name:
|
|
|
|
|
messagebox.showwarning('警告', '请先选择一个EA')
|
|
|
|
|
return
|
|
|
|
|
if not self._save_ea_config(quiet=True):
|
|
|
|
|
messagebox.showwarning('警告', '当前优化配置无效,请检查 ForwardDate 或先选择 EA')
|
|
|
|
|
return
|
|
|
|
|
wf_mode_val = self.wf_mode_map.get(self.wf_mode.get(), 2) if self.wf_enabled.get() else 0
|
|
|
|
|
if self.wf_enabled.get() and wf_mode_val == 4 and not self.wf_custom_date.get().strip():
|
|
|
|
|
messagebox.showwarning('警告', '当前选择了自定义向前优化日期,请先填写 ForwardDate 并保存配置')
|
|
|
|
|
return
|
|
|
|
|
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')
|
|
|
|
@@ -2014,6 +2116,13 @@ class MT5BacktestGUI:
|
|
|
|
|
|
|
|
|
|
def _run_batch_optimize(self):
|
|
|
|
|
_gui_log("点击: 批量执行优化")
|
|
|
|
|
if not self._save_ea_config(quiet=True):
|
|
|
|
|
messagebox.showwarning('警告', '当前优化配置无效,请检查 ForwardDate 或先选择 EA')
|
|
|
|
|
return
|
|
|
|
|
wf_mode_val = self.wf_mode_map.get(self.wf_mode.get(), 2) if self.wf_enabled.get() else 0
|
|
|
|
|
if self.wf_enabled.get() and wf_mode_val == 4 and not self.wf_custom_date.get().strip():
|
|
|
|
|
messagebox.showwarning('警告', '当前选择了自定义向前优化日期,请先填写 ForwardDate 并保存配置')
|
|
|
|
|
return
|
|
|
|
|
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')
|
|
|
|
|