diff --git a/gui.py b/gui.py index 036bdb5..d216f58 100644 --- a/gui.py +++ b/gui.py @@ -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('<>', 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('', 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') diff --git a/scripts/optimizer/ea_auto_scanner.py b/scripts/optimizer/ea_auto_scanner.py index 2a63490..62c8f07 100644 --- a/scripts/optimizer/ea_auto_scanner.py +++ b/scripts/optimizer/ea_auto_scanner.py @@ -35,7 +35,7 @@ class EAAutoScanner: "from_date": "2025.01.01", "to_date": "2025.12.31", "model": 1, "deposit": 10000, "leverage": "1:100" }, - "walk_forward": {"enabled": False} + "walk_forward": {"enabled": False, "forward_mode": 0, "forward_date": ""} } if params: for pname, pinfo in params.items(): @@ -84,4 +84,4 @@ if __name__ == "__main__": fname = cfg["ea_name"].replace(" ", "_") + ".json" with open(os.path.join(args.output, fname), "w", encoding="utf-8") as f: json.dump(cfg, f, indent=4, ensure_ascii=False) - print(f"Saved: {fname}") \ No newline at end of file + print(f"Saved: {fname}") diff --git a/scripts/optimizer/ea_batch_optimizer.py b/scripts/optimizer/ea_batch_optimizer.py index 4379076..3d8b252 100644 --- a/scripts/optimizer/ea_batch_optimizer.py +++ b/scripts/optimizer/ea_batch_optimizer.py @@ -168,6 +168,11 @@ class EABatchOptimizer: if wf.get('enabled', False): fm = wf.get('forward_mode', 2) ini_lines.append('ForwardMode=' + str(fm)) + if fm == 4: + fwd = str(wf.get('forward_date', '') or '').strip() + if not fwd: + raise ValueError('walk_forward.forward_mode=4 时必须提供 walk_forward.forward_date (YYYY.MM.DD)') + ini_lines.append('ForwardDate=' + fwd) else: ini_lines.append('ForwardMode=0') @@ -328,4 +333,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/scripts/scan_set_prompt.py b/scripts/scan_set_prompt.py index 14496a9..879b0e1 100644 --- a/scripts/scan_set_prompt.py +++ b/scripts/scan_set_prompt.py @@ -283,7 +283,8 @@ def generate_prompt(results: List[SetFileResult], output_dir: str = '.') -> List lines.append(' },') lines.append(' "walk_forward": {') lines.append(' "enabled": true, // 是否启用前向测试') - lines.append(' "forward_mode": 2 // 前向模式: 0=禁用, 1=OOS 50%, 2=OOS 33%, 3=OOS 25%') + lines.append(' "forward_mode": 2, // 前向模式: 0=禁用, 1=OOS 50%, 2=OOS 33%, 3=OOS 25%, 4=自定义日期') + lines.append(' "forward_date": "" // 仅当 forward_mode=4 时必填, 格式 YYYY.MM.DD') lines.append(' }') lines.append('}') lines.append('```')