添加后缀映射功能
This commit is contained in:
@@ -21,6 +21,7 @@ try:
|
||||
from mt5_auto_runner import MT5AutoRunner
|
||||
from result_parser import ResultParser
|
||||
from report_generator import ReportGenerator
|
||||
from symbol_resolver import resolve_symbol
|
||||
except Exception as _e:
|
||||
_IMPORT_ERRORS.append(repr(_e))
|
||||
sys.stderr.write(f"[IMPORT FAIL] {_e}\n")
|
||||
@@ -29,6 +30,7 @@ except Exception as _e:
|
||||
MT5AutoRunner = None # type: ignore
|
||||
ResultParser = None # type: ignore
|
||||
ReportGenerator = None # type: ignore
|
||||
resolve_symbol = None # type: ignore
|
||||
|
||||
|
||||
def _gui_log(msg: str) -> None:
|
||||
@@ -66,6 +68,18 @@ def _mt5_startupinfo():
|
||||
return si
|
||||
|
||||
|
||||
# MT5 策略测试器回测模式(与 MT5 官方界面"每个报价/1分钟OHLC/仅开盘价/数学计算/基于每个真实报价"一致)
|
||||
# 值是写入 INI 的 Model= 编号。批量页和优化页共用本列表,切换显示文字时只改这一处。
|
||||
MT5_MODELS = [
|
||||
'每个报价 (0)',
|
||||
'1分钟OHLC (1)',
|
||||
'仅开盘价 (2)',
|
||||
'数学计算 (3)',
|
||||
'基于每个真实报价 (4)',
|
||||
]
|
||||
MT5_MODEL_MAP = {int(s.split('(')[1].rstrip(')')): s for s in MT5_MODELS}
|
||||
|
||||
|
||||
def _find_mt5_hwnds(pid, max_seconds=3.0):
|
||||
hwnds = []
|
||||
deadline = time.time() + max_seconds
|
||||
@@ -401,6 +415,13 @@ class MT5BacktestGUI:
|
||||
cb = ttk.Checkbutton(sym_frame, text=sym, variable=var)
|
||||
cb.grid(row=i//10, column=i%10, sticky='w', padx=2, pady=1)
|
||||
self.symbol_vars[sym] = var
|
||||
var.trace_add('write', lambda *_: self._update_batch_symbol_hint())
|
||||
|
||||
self.batch_sym_hint_var = tk.StringVar(value="")
|
||||
ttk.Label(sym_frame, textvariable=self.batch_sym_hint_var,
|
||||
foreground='#0066cc', wraplength=900, justify='left'
|
||||
).grid(row=99, column=0, columnspan=10, sticky='w', padx=2, pady=(4, 2))
|
||||
self._update_batch_symbol_hint()
|
||||
|
||||
tf_frame = ttk.LabelFrame(main_frame, text="时间周期")
|
||||
tf_frame.pack(fill='x', padx=5, pady=5)
|
||||
@@ -450,21 +471,32 @@ class MT5BacktestGUI:
|
||||
model_frame.pack(side='left', fill='x', padx=5)
|
||||
|
||||
self.model_var = tk.IntVar(value=self.config["backtest_settings"]["model"])
|
||||
ttk.Radiobutton(model_frame, text="Every Tick (0)", variable=self.model_var, value=0).pack(anchor='w', padx=2)
|
||||
ttk.Radiobutton(model_frame, text="1分钟OHLC (1)", variable=self.model_var, value=1).pack(anchor='w', padx=2)
|
||||
ttk.Radiobutton(model_frame, text="真实Tick (4)", variable=self.model_var, value=4).pack(anchor='w', padx=2)
|
||||
for m in MT5_MODELS:
|
||||
val = int(m.split('(')[1].rstrip(')'))
|
||||
ttk.Radiobutton(model_frame, text=m, variable=self.model_var, value=val).pack(anchor='w', padx=2)
|
||||
|
||||
delay_frame = ttk.LabelFrame(date_model_frame, text="执行延迟")
|
||||
delay_frame.pack(side='left', fill='x', padx=5)
|
||||
|
||||
self.delay_type = tk.IntVar(value=0)
|
||||
saved_delay = int((self.config.get("backtest_settings") or {}).get("execution_delay", 0) or 0)
|
||||
if saved_delay == 0:
|
||||
delay_type_value = 0
|
||||
delay_value_text = "100"
|
||||
elif saved_delay == -1:
|
||||
delay_type_value = 1
|
||||
delay_value_text = "100"
|
||||
else:
|
||||
delay_type_value = 2
|
||||
delay_value_text = str(saved_delay)
|
||||
|
||||
self.delay_type = tk.IntVar(value=delay_type_value)
|
||||
ttk.Radiobutton(delay_frame, text="无延迟 (0)", variable=self.delay_type, value=0).pack(anchor='w', padx=2)
|
||||
ttk.Radiobutton(delay_frame, text="随机延迟 (-1)", variable=self.delay_type, value=1).pack(anchor='w', padx=2)
|
||||
delay_row = ttk.Frame(delay_frame)
|
||||
delay_row.pack()
|
||||
ttk.Radiobutton(delay_row, text="固定延迟:", variable=self.delay_type, value=2).pack(side='left', padx=2)
|
||||
self.delay_value = ttk.Entry(delay_row, width=6)
|
||||
self.delay_value.insert(0, "100")
|
||||
self.delay_value.insert(0, delay_value_text)
|
||||
self.delay_value.pack(side='left')
|
||||
ttk.Label(delay_row, text="ms").pack(side='left')
|
||||
|
||||
@@ -645,7 +677,9 @@ class MT5BacktestGUI:
|
||||
"shutdown_terminal": True,
|
||||
"optimization": 0,
|
||||
"forward_mode": forward_mode,
|
||||
"forward_date": forward_date
|
||||
"forward_date": forward_date,
|
||||
# symbol_aliases 在 GUI 里没有编辑控件,直接从内存配置透传(避免保存时把它清掉)
|
||||
"symbol_aliases": (self.config.get("backtest_settings") or {}).get("symbol_aliases") or {},
|
||||
},
|
||||
"mt5_settings": {
|
||||
"terminal_path": self.mt5_path.get(),
|
||||
@@ -1469,18 +1503,21 @@ class MT5BacktestGUI:
|
||||
|
||||
sym_row = ttk.Frame(test_frame)
|
||||
sym_row.pack(fill='x', padx=5, pady=3)
|
||||
ttk.Label(sym_row, text="品种:").pack(side='left', padx=5)
|
||||
ttk.Label(sym_row, text="逻辑品种:").pack(side='left', padx=5)
|
||||
self.opt_symbol = ttk.Combobox(sym_row, width=12, values=['EURUSD', 'GBPUSD', 'USDJPY', 'USDCAD', 'USDCHF', 'AUDUSD', 'NZDUSD', 'EURGBP', 'EURJPY', 'GBPJPY', 'EURCHF', 'AUDJPY', 'EURAUD', 'GBPAUD', 'EURCAD', 'GBPCAD', 'XAUUSD', 'XAGUSD', 'XBRUSD', 'XTIUSD', 'BTCUSD', 'ETHUSD', 'LTCUSD', 'USDCNH', 'USDHKD', 'USDCZK', 'USDDKK', 'USDHUF', 'USDMXN', 'USDPLN', 'USDTHB', 'NOKJPY', 'SEKJPY', 'XRPUSD', 'XLMUSD', 'AUS200', 'CHINA50', 'ES35', 'STOXX50', 'F40', 'HK50', 'IT40', 'JP225', 'UK100', 'US2000', 'US30', 'US500', 'USTEC', 'CA60', 'NETH25', 'SE30', 'SWI20', 'CHINAH', 'NOR25', 'TecDE30'])
|
||||
self.opt_symbol.set('XAUUSD')
|
||||
self.opt_symbol.pack(side='left', padx=5)
|
||||
|
||||
self.opt_symbol_resolved = ttk.Label(sym_row, text="→ XAUUSD", foreground='#666666')
|
||||
self.opt_symbol_resolved.pack(side='left', padx=(10, 2))
|
||||
|
||||
ttk.Label(sym_row, text="周期:").pack(side='left', padx=10)
|
||||
self.opt_period = ttk.Combobox(sym_row, width=8, values=['M1', 'M5', 'M15', 'M30', 'H1', 'H4', 'D1', 'W1'])
|
||||
self.opt_period.set('M1')
|
||||
self.opt_period.pack(side='left', padx=5)
|
||||
|
||||
ttk.Label(sym_row, text="模式:").pack(side='left', padx=10)
|
||||
self.opt_model = ttk.Combobox(sym_row, width=18, values=['Every Tick (0)', '1分钟OHLC (1)', '仅开盘价 (2)', '数学计算 (3)', '真实Tick (4)'])
|
||||
self.opt_model = ttk.Combobox(sym_row, width=20, values=MT5_MODELS)
|
||||
self.opt_model.set('1分钟OHLC (1)')
|
||||
self.opt_model.pack(side='left', padx=5)
|
||||
|
||||
@@ -1521,6 +1558,9 @@ class MT5BacktestGUI:
|
||||
self.opt_to_date.insert(0, "2026.01.04")
|
||||
self.opt_to_date.pack(side='left', padx=5)
|
||||
|
||||
self.opt_symbol.bind('<<ComboboxSelected>>', lambda e: self._update_resolved_symbol_hint())
|
||||
self.opt_symbol.bind('<FocusOut>', lambda e: self._update_resolved_symbol_hint())
|
||||
|
||||
acct_row = ttk.Frame(test_frame)
|
||||
acct_row.pack(fill='x', padx=5, pady=3)
|
||||
ttk.Label(acct_row, text="保证金:").pack(side='left', padx=5)
|
||||
@@ -1574,6 +1614,7 @@ class MT5BacktestGUI:
|
||||
self.opt_log.pack(fill='both', expand=True, padx=5, pady=5)
|
||||
|
||||
self._refresh_opt_ea_list()
|
||||
self._update_resolved_symbol_hint()
|
||||
|
||||
def _browse_opt_dir(self):
|
||||
path = filedialog.askdirectory(initialdir=self.opt_dir.get())
|
||||
@@ -1736,18 +1777,12 @@ class MT5BacktestGUI:
|
||||
|
||||
if test_cfg.get('symbol'):
|
||||
self.opt_symbol.set(str(test_cfg.get('symbol')))
|
||||
self._update_resolved_symbol_hint()
|
||||
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)'))
|
||||
self.opt_model.set(MT5_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')))
|
||||
@@ -1861,6 +1896,53 @@ class MT5BacktestGUI:
|
||||
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 _format_resolved_hint(self, symbols):
|
||||
"""Format a hint string showing logical -> resolved symbol mapping.
|
||||
|
||||
Shared by batch backtest tab (multi-select) and optimizer tab (single).
|
||||
Returns "" when nothing is selected or no aliases are configured.
|
||||
"""
|
||||
if not symbols:
|
||||
return ""
|
||||
aliases = (self.config.get('backtest_settings') or {}).get('symbol_aliases') or {}
|
||||
if not aliases:
|
||||
return ""
|
||||
parts = []
|
||||
for sym in symbols:
|
||||
if not sym:
|
||||
continue
|
||||
resolved = resolve_symbol(sym, {'symbol_aliases': aliases}) if resolve_symbol else sym
|
||||
if resolved == sym:
|
||||
parts.append(f"{sym}(直通)")
|
||||
else:
|
||||
parts.append(f"{sym}→{resolved}")
|
||||
return "实际品种:" + ", ".join(parts) if parts else ""
|
||||
|
||||
def _update_batch_symbol_hint(self):
|
||||
if not hasattr(self, 'batch_sym_hint_var'):
|
||||
return
|
||||
checked = [s for s, v in self.symbol_vars.items() if v.get()]
|
||||
self.batch_sym_hint_var.set(self._format_resolved_hint(checked))
|
||||
|
||||
def _resolve_test_config(self):
|
||||
"""Build the minimal dict the resolver needs (only symbol_aliases)."""
|
||||
return {
|
||||
'symbol_aliases': (self.config.get('backtest_settings') or {}).get('symbol_aliases') or {},
|
||||
}
|
||||
|
||||
def _update_resolved_symbol_hint(self):
|
||||
if not hasattr(self, 'opt_symbol_resolved') or resolve_symbol is None:
|
||||
return
|
||||
try:
|
||||
sym = self.opt_symbol.get().split()[0] if ' ' in self.opt_symbol.get() else self.opt_symbol.get()
|
||||
resolved = resolve_symbol(sym, self._resolve_test_config())
|
||||
except Exception:
|
||||
resolved = self.opt_symbol.get()
|
||||
if resolved == sym:
|
||||
self.opt_symbol_resolved.config(text=f"→ {resolved}", foreground='#666666')
|
||||
else:
|
||||
self.opt_symbol_resolved.config(text=f"→ {resolved} (别名)", foreground='#0066cc')
|
||||
|
||||
def _save_ea_config(self, quiet=False):
|
||||
ea_name = self.opt_ea_var.get()
|
||||
if not ea_name:
|
||||
@@ -1895,39 +1977,39 @@ class MT5BacktestGUI:
|
||||
'step': step_val,
|
||||
'optimize': poptimize == 'Y' if poptimize else True
|
||||
}
|
||||
import json
|
||||
config_path = os.path.join(configs_dir, ea_name + '.json')
|
||||
config_data = {
|
||||
import json
|
||||
config_path = os.path.join(configs_dir, ea_name + '.json')
|
||||
config_data = {
|
||||
'ea_name': ea_name,
|
||||
'ea_path': self.opt_ea_paths.get(ea_name, f"MY-EA\\{ea_name}.ex5"),
|
||||
'description': 'Edited via GUI',
|
||||
'search_strategy': 'auto',
|
||||
'parameters': params,
|
||||
'test_config': {
|
||||
'search_strategy': 'auto',
|
||||
'parameters': params,
|
||||
'test_config': {
|
||||
'symbol': self.opt_symbol.get().split()[0] if ' ' in self.opt_symbol.get() else self.opt_symbol.get(),
|
||||
'period': self.opt_period.get(),
|
||||
'from_date': self.opt_from_date.get(),
|
||||
'to_date': self.opt_to_date.get(),
|
||||
'model': int(self.opt_model.get().split('(')[1].split(')')[0]) if '(' in self.opt_model.get() else 1,
|
||||
'execution_delay': self._get_opt_execution_delay(),
|
||||
'optimization_mode': self._get_opt_optimization_mode(),
|
||||
'optimization_criterion': self._get_opt_optimization_criterion(),
|
||||
'deposit': int(self.opt_deposit.get()) if self.opt_deposit.get().isdigit() else 10000,
|
||||
'leverage': self.opt_leverage.get(),
|
||||
'timeout_min': int(self.opt_timeout_min.get()) if self.opt_timeout_min.get().isdigit() else 60
|
||||
},
|
||||
'walk_forward': {
|
||||
'enabled': self.wf_enabled.get(),
|
||||
'forward_mode': wf_mode_val,
|
||||
'forward_date': wf_forward_date if (self.wf_enabled.get() and wf_mode_val == 4) else ''
|
||||
}
|
||||
'model': int(self.opt_model.get().split('(')[1].split(')')[0]) if '(' in self.opt_model.get() else 1,
|
||||
'execution_delay': self._get_opt_execution_delay(),
|
||||
'optimization_mode': self._get_opt_optimization_mode(),
|
||||
'optimization_criterion': self._get_opt_optimization_criterion(),
|
||||
'deposit': int(self.opt_deposit.get()) if self.opt_deposit.get().isdigit() else 10000,
|
||||
'leverage': self.opt_leverage.get(),
|
||||
'timeout_min': int(self.opt_timeout_min.get()) if self.opt_timeout_min.get().isdigit() else 60
|
||||
},
|
||||
'walk_forward': {
|
||||
'enabled': self.wf_enabled.get(),
|
||||
'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
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user