添加后缀映射功能
This commit is contained in:
Binary file not shown.
+2523
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,13 @@ mt5_backtest/
|
||||
│ ├── ini_generator.py # 生成MT5配置文件
|
||||
│ ├── mt5_auto_runner.py # 自动运行回测
|
||||
│ ├── batch_executor.py # 批量执行器
|
||||
│ ├── result_parser.py # 解析HTM/XML报告
|
||||
│ └── report_generator.py # 生成汇总报告
|
||||
│ ├── result_parser.py # 解析HTM/XML报告
|
||||
│ ├── report_generator.py # 生成汇总报告
|
||||
│ ├── symbol_resolver.py # 根品种别名解析(XAUUSD→XAUUSDc)
|
||||
│ └── optimizer/ # EA参数优化系统
|
||||
├── config/ # 配置文件目录
|
||||
├── reports/ # 回测报告输出目录
|
||||
├── tests/ # 自检脚本(test_*.py / smoke_*.py)
|
||||
├── logs/ # 日志目录
|
||||
└── README.md # 本文档
|
||||
```
|
||||
@@ -26,6 +29,7 @@ mt5_backtest/
|
||||
- 扫描 MT5 的 `MQL5/Experts` 目录获取 EA 列表
|
||||
- 根据配置的 EA品种、时间周期、参数组合生成 INI 配置文件
|
||||
- 每个 INI 文件对应一次回测任务
|
||||
- 写入 INI 的 `Symbol=` 行通过 `scripts/symbol_resolver.py` 查表替换(见下文"根品种别名映射")
|
||||
|
||||
### 2. 自动执行阶段 (mt5_auto_runner.py)
|
||||
- 通过命令行调用 MT5 终端: `terminal64.exe /config:xxx.ini`
|
||||
@@ -79,6 +83,10 @@ eas:
|
||||
backtest_settings:
|
||||
symbols: ["EURUSD", "GBPUSD", "XAUUSD", "USDJPY"]
|
||||
timeframes: ["M1", "M5", "M15", "H1"]
|
||||
# 可选:根品种别名映射(见下文"根品种别名映射")
|
||||
# symbol_aliases:
|
||||
# XAUUSD: XAUUSDc # 逻辑 XAUUSD -> 实际 XAUUSDc
|
||||
# XAGUSD: XAGUSDm # 逻辑 XAGUSD -> 实际 XAGUSDm
|
||||
date_range:
|
||||
from: "2026.01.01"
|
||||
to: "2026.01.04"
|
||||
@@ -147,6 +155,66 @@ backtest_settings:
|
||||
| 原油 | XBRUSD, XTIUSD |
|
||||
| 数字货币 | BTCUSD, ETHUSD, LTCUSD |
|
||||
|
||||
## 根品种别名映射 (`symbol_aliases`)
|
||||
|
||||
MT5 自带品种(如 `XAUUSD`)历史深度和精度有限;用户常在 MT5 里添加自定义品种
|
||||
(如 `XAUUSDc` / `XAUUSDm`)作为更长时间范围或优化用途的"替身"。本系统通过 `symbol_aliases`
|
||||
让你在 yaml 里写一次逻辑品种名、由系统按表替换成实际 MT5 品种。
|
||||
|
||||
### 写入 `config/ea_configs.yaml`
|
||||
|
||||
```yaml
|
||||
backtest_settings:
|
||||
symbols: [XAUUSD, XAGUSD, EURUSD] # 用户的"逻辑品种"
|
||||
symbol_aliases:
|
||||
XAUUSD: XAUUSDc # 逻辑 XAUUSD -> 实际 XAUUSDc
|
||||
XAGUSD: XAGUSDm # 逻辑 XAGUSD -> 实际 XAGUSDm
|
||||
# EURUSD 不列 -> 始终 EURUSD
|
||||
```
|
||||
|
||||
### 工作机制
|
||||
|
||||
1. `scripts/ini_generator.py` 和 `scripts/optimizer/ea_batch_optimizer.py` 在
|
||||
写 INI 的 `Symbol=` 之前调用 `scripts/symbol_resolver.resolve_symbol()`。
|
||||
2. 解析器是**纯查表**——没有任何"基于日期/优化/周期的智能判断"——写啥用啥:
|
||||
- 别名表里有这个逻辑品种 → 用别名替换
|
||||
- 没有 → 原样写入 `Symbol=`
|
||||
3. 两条调用路径共用同一份解析器(`scripts/symbol_resolver.py`),保证 GUI 回测页
|
||||
和优化器生成的 INI 行为一致。
|
||||
|
||||
### GUI 优化页
|
||||
|
||||
GUI 优化页下拉只显示**逻辑品种**(不再同时列出 XAUUSD / XAUUSDc)。旁边有一个
|
||||
`→ XAUUSDc` 提示标签,**实时反映 yaml 里 `symbol_aliases` 当前配置下实际会用什么品种**。
|
||||
改 yaml 后重启 GUI 即可看到提示变化,无需在 GUI 里反复切换。
|
||||
|
||||
### 验证
|
||||
|
||||
无需 MT5 也能验:
|
||||
|
||||
```bash
|
||||
python tests/test_symbol_resolver.py # 9 条查表断言
|
||||
python tests/test_ini_generator.py # 5 条端到端断言
|
||||
```
|
||||
|
||||
需要 MT5 验证完整流水线:
|
||||
|
||||
```bash
|
||||
python tests/smoke_alias_backtest.py
|
||||
# 跑两次真实回测:一次无别名 (Symbol=XAUUSD),一次有别名 (Symbol=XAUUSDc)
|
||||
# 期望:C:\Users\Administrator\Desktop\mt5-backtest\reports 下生成两份 .htm 报告
|
||||
# 报告"交易品种"字段分别是 XAUUSD 和 XAUUSDc
|
||||
```
|
||||
|
||||
> 注:`smoke_alias_backtest.py` 顶部硬编码了 MT5 路径和 data dir hash,复制到其他电脑后
|
||||
> 需要相应修改 `MT5_PATH` / `MT5_DATA_DIR` 常量。
|
||||
|
||||
### 扩展点(ponytail)
|
||||
|
||||
`scripts/symbol_resolver.py` 是共享函数入口,将来要把"配置源"切换成数据库或要做
|
||||
MT5 `SymbolSelect` 校验,**只改这一个文件**即可,所有调用方(ini 生成器、批量优化器、
|
||||
GUI 预览)同步生效。
|
||||
|
||||
## 如何添加新的时间周期
|
||||
|
||||
编辑 `config/ea_configs.yaml`:
|
||||
@@ -537,6 +605,7 @@ mt5_backtest/
|
||||
│ ├── batch_executor.py # 批量执行器
|
||||
│ ├── result_parser.py # 解析HTM/XML报告
|
||||
│ ├── report_generator.py # 生成汇总报告
|
||||
│ ├── symbol_resolver.py # 根品种别名解析(symbol_aliases 查表)
|
||||
│ └── optimizer/ # EA优化系统
|
||||
│ ├── ea_config_parser.py # EA配置解析
|
||||
│ ├── smart_search.py # 搜索策略
|
||||
@@ -545,6 +614,7 @@ mt5_backtest/
|
||||
│ └── ea_batch_optimizer.py # 批量优化器
|
||||
├── config/ # 配置文件
|
||||
├── reports/ # 回测报告目录
|
||||
├── tests/ # 自检脚本(test_symbol_resolver / test_ini_generator / smoke_alias_backtest)
|
||||
├── optimizer/ # 优化系统目录
|
||||
│ ├── configs/ # EA配置
|
||||
│ ├── set_files/ # SET文件
|
||||
@@ -566,6 +636,19 @@ mt5_backtest/
|
||||
### Q: 中文显示乱码
|
||||
HTM 报告采用 UTF-16-LE 编码,result_parser.py 会自动处理,无需手动转换
|
||||
|
||||
### Q: 想用 MT5 自定义品种(如 XAUUSDc)跑同一 EA
|
||||
在 `config/ea_configs.yaml` 的 `backtest_settings` 下加:
|
||||
```yaml
|
||||
symbol_aliases:
|
||||
XAUUSD: XAUUSDc
|
||||
```
|
||||
yaml 符号表里仍写 `XAUUSD`,系统会自动把 INI 的 `Symbol=` 替换为 `XAUUSDc`。
|
||||
详见上文"根品种别名映射"一节。
|
||||
|
||||
### Q: 改了 `symbol_aliases` 后 GUI 提示没变
|
||||
GUI 启动时读取一次 `config/ea_configs.yaml`,改完后重启 GUI 即可看到
|
||||
`→ XAUUSDc` 提示标签更新。无需重新生成报告历史。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
@@ -14,10 +14,46 @@ backtest_settings:
|
||||
shutdown_terminal: true
|
||||
symbols:
|
||||
- EURUSD
|
||||
- GBPUSD
|
||||
- USDJPY
|
||||
- USDCHF
|
||||
- EURJPY
|
||||
- EURGBP
|
||||
- GBPJPY
|
||||
- XAUUSD
|
||||
timeframes:
|
||||
- H1
|
||||
visual: 0
|
||||
|
||||
# ---- 根品种别名映射(可选,留空/删除整块 = 关闭此功能,行为与传统一致)----
|
||||
# 纯查表,没有任何"自动判断"。逻辑品种写在上面的 symbols 列表里,
|
||||
# 想让系统实际用哪个 MT5 品种就在这里写死。
|
||||
#
|
||||
# 用法:取消下面想启用的那一行(或多行)的注释即可。
|
||||
# 同一时间只能有一条 XAUUSD 映射生效;切换另一种后缀时把上一条注释掉。
|
||||
# 注意:GUI 的"保存配置"不会动这块内容(symbol_aliases 在 GUI 里没有编辑控件),
|
||||
# 改完直接保存即可,存盘不会被覆盖。GUI 重启后提示标签自动更新。
|
||||
# symbol_aliases:
|
||||
# XAUUSD: XAUUSDc # 常用:自定义品种 XAUUSDc(带扩展历史 / 精度)
|
||||
# XAUUSD: XAUUSDm # 切换:自定义品种 XAUUSDm
|
||||
# XAUUSD: Gold.v2 # 切换:完全不同的名字(broker 自定义 / ECN 后缀等)
|
||||
# XAUUSD: XAUUSD # 显式直通:等价于不写这条(写出来仅为自解释)
|
||||
# XAGUSD: XAGUSDc # 银同理
|
||||
# XAGUSD: XAGUSDm
|
||||
# BTCUSD: BTCUSD.sb # 加密货币自定义后缀
|
||||
# # EURUSD 没列 # 永远不需要列:未列出的逻辑品种按原名直传
|
||||
#
|
||||
# 关闭整个功能:把整块(包括 symbol_aliases: 这行)注释掉或删除。
|
||||
# 关闭后 GUI "实际品种"提示行自动消失,INI 里的 Symbol= 与逻辑名完全一致。
|
||||
symbol_aliases:
|
||||
EURUSD: EURUSDc
|
||||
GBPUSD: GBPUSDc
|
||||
USDJPY: USDJPYc
|
||||
USDCHF: USDCHFc
|
||||
EURJPY: EURJPYc
|
||||
EURGBP: EURGBPc
|
||||
GBPJPY: GBPJPYc
|
||||
XAUUSD: XAUUSDc
|
||||
eas: []
|
||||
execution:
|
||||
kill_between: true
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
2026-07-06 20:26:24,598 - INFO - Executing: MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini
|
||||
2026-07-06 20:26:24,620 - INFO - MT5 started PID: 18668
|
||||
2026-07-06 20:27:22,638 - INFO - MT5 process ended
|
||||
2026-07-06 20:27:22,638 - INFO - Report copied to: C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSD_H1_default.htm
|
||||
2026-07-06 20:27:22,638 - INFO - Success: MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini (58s)
|
||||
2026-07-06 20:27:22,644 - INFO - Executing: MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.ini
|
||||
2026-07-06 20:27:22,670 - INFO - MT5 started PID: 21372
|
||||
2026-07-06 20:27:48,676 - INFO - MT5 process ended
|
||||
2026-07-06 20:27:48,679 - INFO - Report copied to: C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.htm
|
||||
2026-07-06 20:27:48,679 - INFO - Success: MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.ini (26s)
|
||||
=== run 1: no alias (should pick XAUUSD) ===
|
||||
[no_alias] INI Symbol line: Symbol=XAUUSD
|
||||
[no_alias] runner status: completed, report_path: C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSD_H1_default.htm
|
||||
|
||||
=== run 2: alias XAUUSD -> XAUUSDc (should pick XAUUSDc) ===
|
||||
[with_alias] INI Symbol line: Symbol=XAUUSDc
|
||||
[with_alias] runner status: completed, report_path: C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.htm
|
||||
|
||||
=== summary ===
|
||||
INI1 (MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini) -> completed, C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSD_H1_default.htm
|
||||
INI2 (MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.ini) -> completed, C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.htm
|
||||
|
||||
Reports in C:\Users\Administrator\Desktop\mt5-backtest\reports:
|
||||
- MACD_MA_XAUUSD_5M_XAUUSD_H1_default.htm (52680 bytes) symbol=XAUUSD
|
||||
- MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.htm (28932 bytes) symbol=XAUUSDc
|
||||
|
||||
Distinct symbols found: ['XAUUSD', 'XAUUSDc']
|
||||
SUCCESS: both XAUUSD and XAUUSDc reports present in target dir.
|
||||
@@ -7,6 +7,7 @@ from typing import Dict, List, Any, Optional
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from mt5_paths import resolve_mt5_settings
|
||||
from symbol_resolver import resolve_symbol
|
||||
|
||||
|
||||
class INIGenerator:
|
||||
@@ -115,12 +116,16 @@ class INIGenerator:
|
||||
else:
|
||||
ini_lines.append("ExpertParameters=")
|
||||
|
||||
# MT5 tester delay is controlled by ExecutionMode itself:
|
||||
# 0 = no delay
|
||||
# -1 = random delay
|
||||
# > 0 = fixed delay in milliseconds
|
||||
execution_delay = int(bt_settings.get('execution_delay', 0) or 0)
|
||||
ini_lines.extend([
|
||||
f"Symbol={symbol}",
|
||||
f"Period={self._get_timeframe_code(timeframe)}",
|
||||
f"Model={bt_settings['model']}",
|
||||
f"ExecutionMode={bt_settings.get('execution_mode', 0)}",
|
||||
f"ExecutionDelay={bt_settings.get('execution_delay', 0)}",
|
||||
f"ExecutionMode={execution_delay}",
|
||||
f"Optimization={bt_settings['optimization']}",
|
||||
])
|
||||
if bt_settings.get("optimization"):
|
||||
@@ -184,14 +189,15 @@ class INIGenerator:
|
||||
param_combinations = self._generate_parameter_combinations(parameters)
|
||||
|
||||
for symbol in bt_settings["symbols"]:
|
||||
resolved_symbol = resolve_symbol(symbol, bt_settings)
|
||||
for timeframe in bt_settings["timeframes"]:
|
||||
for param_combo in param_combinations:
|
||||
ini_content = self._build_ini_content(
|
||||
ea_filename, symbol, timeframe,
|
||||
ea_filename, resolved_symbol, timeframe,
|
||||
bt_settings, param_combo, ea_name, set_file
|
||||
)
|
||||
ini_filename = self._generate_filename(
|
||||
ea_name, symbol, timeframe, param_combo
|
||||
ea_name, resolved_symbol, timeframe, param_combo
|
||||
)
|
||||
ini_path = os.path.join(self.output_dir, ini_filename)
|
||||
|
||||
@@ -253,4 +259,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -225,6 +225,13 @@ class MT5AutoRunner:
|
||||
|
||||
logger.info(f"Total INI files: {total}")
|
||||
|
||||
# First run must also start from a clean tester state. Otherwise the first test
|
||||
# can inherit an already-open terminal's previous deposit/delay/symbol settings,
|
||||
# while later tests look correct only because kill_between_tests runs after them.
|
||||
if kill_between_tests:
|
||||
self._kill_mt5()
|
||||
time.sleep(2)
|
||||
|
||||
for idx, ini_path in enumerate(ini_files, 1):
|
||||
logger.info(f"[{idx}/{total}] {os.path.basename(ini_path)}")
|
||||
|
||||
@@ -352,4 +359,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -13,6 +13,7 @@ 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 symbol_resolver import resolve_symbol
|
||||
|
||||
|
||||
class EABatchOptimizer:
|
||||
@@ -157,7 +158,7 @@ class EABatchOptimizer:
|
||||
'[Tester]',
|
||||
'Expert=' + ea_config.ea_path,
|
||||
'ExpertParameters=' + set_filename,
|
||||
'Symbol=' + test_config.get('symbol', 'EURUSD'),
|
||||
'Symbol=' + resolve_symbol(test_config.get('symbol', 'EURUSD'), test_config),
|
||||
'Period=' + test_config.get('period', 'H1'),
|
||||
'Model=' + str(test_config.get('model', 1)),
|
||||
'FromDate=' + date_from,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Root symbol alias resolver (pure lookup, no auto-switching).
|
||||
|
||||
用户视角的"逻辑品种"(如 XAUUSD)映射到 MT5 里实际使用的品种。
|
||||
|
||||
ponytail:
|
||||
- 没有别名表 / 没条目 -> 原样返回(零行为变化,向后兼容)
|
||||
- 有条目 -> 一比一映射,写啥用啥,不做任何基于日期/优化/周期的判断
|
||||
- 升级路径:要从 yaml 切到数据库 / MT5 SymbolSelect 校验,只改这里
|
||||
|
||||
配置示例(ea_configs.yaml):
|
||||
symbol_aliases:
|
||||
XAUUSD: XAUUSDc # 逻辑 XAUUSD -> 实际 XAUUSDc
|
||||
XAGUSD: XAGUSDm # 逻辑 XAGUSD -> 实际 XAGUSDm
|
||||
# EURUSD 没列 -> 始终 EURUSD
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def resolve_symbol(symbol: str, bt_settings: Dict[str, Any]) -> str:
|
||||
"""逻辑品种 -> 实际 MT5 品种。无任何自动判断,按表直查。"""
|
||||
if not symbol:
|
||||
return symbol
|
||||
aliases = bt_settings.get("symbol_aliases") or {}
|
||||
mapped = aliases.get(symbol)
|
||||
if not mapped: # None / "" / 缺失 -> 直通
|
||||
return symbol
|
||||
return mapped
|
||||
@@ -0,0 +1,143 @@
|
||||
# Real backtest smoke test for the symbol alias resolver.
|
||||
# Runs two backtests via the project's pipeline (INIGenerator + MT5AutoRunner):
|
||||
# 1) XAUUSD (no alias) -> INI Symbol=XAUUSD
|
||||
# 2) XAUUSD (alias -> XAUUSDc) -> INI Symbol=XAUUSDc
|
||||
# Success: two .htm reports in C:\Users\Administrator\Desktop\mt5-backtest\reports,
|
||||
# one with Symbol=XAUUSD, one with Symbol=XAUUSDc.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import shutil
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT_ROOT = os.path.normpath(os.path.join(HERE, ".."))
|
||||
SCRIPTS = os.path.join(PROJECT_ROOT, "scripts")
|
||||
EXAMPLES_ROOT = os.path.normpath(os.path.join(PROJECT_ROOT, "..", "examples"))
|
||||
|
||||
TARGET_REPORTS = r"C:\Users\Administrator\Desktop\mt5-backtest\reports"
|
||||
MT5_PATH = r"C:\Program Files\MetaTrader 5 IC Markets Global\terminal64.exe"
|
||||
MT5_DATA_DIR = r"C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal\010E047102812FC0C18890992854220E"
|
||||
MT5_TESTER_DIR = os.path.join(MT5_DATA_DIR, "MQL5", "Profiles", "Tester")
|
||||
|
||||
EA_NAME = "MACD_MA_XAUUSD_5M"
|
||||
EA_BASENAME = f"{EA_NAME}.ex5"
|
||||
EA_FULL_PATH = os.path.join(MT5_DATA_DIR, "MQL5", "Experts", EA_BASENAME)
|
||||
SET_SRC = os.path.join(EXAMPLES_ROOT, "configs", f"{EA_NAME}.set")
|
||||
SET_DEST_NAME = "MACD_MA_XAUUSD_5M_smoke.set"
|
||||
SET_DEST_PATH = os.path.join(MT5_TESTER_DIR, SET_DEST_NAME)
|
||||
|
||||
os.makedirs(TARGET_REPORTS, exist_ok=True)
|
||||
shutil.copy2(SET_SRC, SET_DEST_PATH)
|
||||
|
||||
sys.path.insert(0, SCRIPTS)
|
||||
from ini_generator import INIGenerator
|
||||
from mt5_auto_runner import MT5AutoRunner
|
||||
|
||||
|
||||
def build_cfg(aliases, out_ini_dir):
|
||||
return {
|
||||
"backtest_settings": {
|
||||
"currency": "USD",
|
||||
"date_range": {"from": "2026.05.01", "to": "2026.06.05"},
|
||||
"deposit": 10000,
|
||||
"execution_delay": 0,
|
||||
"forward_date": "",
|
||||
"forward_mode": 0,
|
||||
"leverage": "1:100",
|
||||
"model": 0,
|
||||
"optimization": 0,
|
||||
"replace_report": True,
|
||||
"shutdown_terminal": True,
|
||||
"symbols": ["XAUUSD"],
|
||||
"timeframes": ["H1"],
|
||||
"visual": 0,
|
||||
"symbol_aliases": aliases or {},
|
||||
},
|
||||
"eas": [
|
||||
{"name": EA_NAME, "filename": EA_FULL_PATH, "set_file": SET_DEST_NAME},
|
||||
],
|
||||
"mt5_settings": {
|
||||
"terminal_path": MT5_PATH,
|
||||
"data_dir": MT5_DATA_DIR,
|
||||
"ini_dir": out_ini_dir,
|
||||
"reports_dir": TARGET_REPORTS,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def patch_ini_to_inline(ini_path):
|
||||
"""Patch the INI to use relative Expert + inline [TesterInputs]
|
||||
(the format MT5 actually auto-starts with on this terminal)."""
|
||||
with open(ini_path, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
text = text.replace(f"Expert={EA_FULL_PATH}", f"Expert={EA_BASENAME}")
|
||||
text = text.replace(f"ExpertParameters={SET_DEST_NAME}", "ExpertParameters=")
|
||||
with open(SET_DEST_PATH, encoding="utf-8", errors="ignore") as f:
|
||||
set_text = f.read()
|
||||
if not text.rstrip().endswith("[TesterInputs]"):
|
||||
text = text.rstrip() + "\n\n[TesterInputs]\n" + set_text
|
||||
with open(ini_path, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def run_one(label, aliases, out_dir):
|
||||
cfg = build_cfg(aliases=aliases, out_ini_dir=out_dir)
|
||||
gen = INIGenerator(cfg)
|
||||
ini_files = gen.generate_ini_files()
|
||||
assert len(ini_files) == 1, f"{label}: expected 1 INI, got {len(ini_files)}"
|
||||
ini = ini_files[0]
|
||||
patch_ini_to_inline(ini)
|
||||
with open(ini, encoding="utf-8") as f:
|
||||
sym_line = next(l for l in f if l.startswith("Symbol="))
|
||||
print(f"[{label}] INI Symbol line: {sym_line.strip()}")
|
||||
|
||||
runner = MT5AutoRunner(cfg)
|
||||
res = runner._execute_single_ini(ini, timeout_min=15)
|
||||
print(f"[{label}] runner status: {res['status']}, report_path: {res.get('report_path')}")
|
||||
return ini, res
|
||||
|
||||
|
||||
def main():
|
||||
tmp_root = os.path.join(PROJECT_ROOT, "config", "generated_smoke")
|
||||
if os.path.exists(tmp_root):
|
||||
shutil.rmtree(tmp_root)
|
||||
os.makedirs(tmp_root, exist_ok=True)
|
||||
out1 = os.path.join(tmp_root, "no_alias"); os.makedirs(out1, exist_ok=True)
|
||||
out2 = os.path.join(tmp_root, "with_alias"); os.makedirs(out2, exist_ok=True)
|
||||
|
||||
print("=== run 1: no alias (should pick XAUUSD) ===")
|
||||
ini1, r1 = run_one("no_alias", aliases=None, out_dir=out1)
|
||||
print()
|
||||
print("=== run 2: alias XAUUSD -> XAUUSDc (should pick XAUUSDc) ===")
|
||||
ini2, r2 = run_one("with_alias", aliases={"XAUUSD": "XAUUSDc"}, out_dir=out2)
|
||||
print()
|
||||
|
||||
print("=== summary ===")
|
||||
print(f" INI1 ({os.path.basename(ini1)}) -> {r1['status']}, {r1.get('report_path')}")
|
||||
print(f" INI2 ({os.path.basename(ini2)}) -> {r2['status']}, {r2.get('report_path')}")
|
||||
|
||||
reports = sorted([f for f in os.listdir(TARGET_REPORTS)
|
||||
if f.startswith('MACD_MA_XAUUSD_5M_') and f.endswith('.htm')])
|
||||
print(f"\nReports in {TARGET_REPORTS}:")
|
||||
found = {"XAUUSD": [], "XAUUSDc": []}
|
||||
for r in reports:
|
||||
path = os.path.join(TARGET_REPORTS, r)
|
||||
size = os.path.getsize(path)
|
||||
# MT5 saves reports as UTF-16 LE with BOM
|
||||
text = open(path, encoding='utf-16', errors='ignore').read()
|
||||
m = re.search(r'交易品种:</td>\s*<td[^>]*><b>([^<]+)</b>', text)
|
||||
sym = m.group(1) if m else "?"
|
||||
found[sym].append(r)
|
||||
print(f" - {r} ({size} bytes) symbol={sym}")
|
||||
|
||||
print(f"\nDistinct symbols found: {sorted({k for k, v in found.items() if v})}")
|
||||
if not found["XAUUSD"]:
|
||||
raise AssertionError("No report with Symbol=XAUUSD")
|
||||
if not found["XAUUSDc"]:
|
||||
raise AssertionError("No report with Symbol=XAUUSDc")
|
||||
print("SUCCESS: both XAUUSD and XAUUSDc reports present in target dir.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,96 @@
|
||||
"""End-to-end: INIGenerator picks the right MT5 Symbol from explicit alias map.
|
||||
|
||||
Run: python tests/test_ini_generator.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.normpath(os.path.join(HERE, "..", "scripts")))
|
||||
|
||||
from ini_generator import INIGenerator
|
||||
|
||||
|
||||
def _make_cfg(aliases, symbols, out_dir):
|
||||
return {
|
||||
"mt5_settings": {"terminal_path": "", "data_dir": "",
|
||||
"reports_dir": "{PROJECT_ROOT}/reports", "ini_dir": out_dir},
|
||||
"backtest_settings": {
|
||||
"currency": "USD",
|
||||
"date_range": {"from": "2010.01.01", "to": "2024.12.31"},
|
||||
"deposit": 10000, "execution_delay": 0, "forward_date": "",
|
||||
"forward_mode": 0, "leverage": "1:100", "model": 0,
|
||||
"optimization": 0, "replace_report": True, "shutdown_terminal": True,
|
||||
"symbols": symbols, "timeframes": ["H1"], "visual": 0,
|
||||
"symbol_aliases": aliases,
|
||||
},
|
||||
"eas": [{"name": "DummyEA", "filename": "MQL5/Experts/DummyEA.ex5", "parameters": {}}],
|
||||
}
|
||||
|
||||
|
||||
def _grep_symbol(ini_path, target):
|
||||
with open(ini_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.startswith("Symbol="):
|
||||
return line.strip().split("=", 1)[1] == target
|
||||
return False
|
||||
|
||||
|
||||
def _get_ini_value(ini_path, key):
|
||||
with open(ini_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.startswith(f"{key}="):
|
||||
return line.strip().split("=", 1)[1]
|
||||
return None
|
||||
|
||||
|
||||
def expect(label, ok):
|
||||
print(f" {'PASS' if ok else 'FAIL'} {label}")
|
||||
if not ok:
|
||||
raise AssertionError(label)
|
||||
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cfg = _make_cfg({"XAUUSD": "XAUUSDc"}, ["XAUUSD", "EURUSD"], tmp)
|
||||
files = INIGenerator(cfg).generate_ini_files()
|
||||
expect("2 files", len(files) == 2)
|
||||
expect("XAUUSD alias applied", any(_grep_symbol(f, "XAUUSDc") for f in files))
|
||||
expect("EURUSD passes through", any(_grep_symbol(f, "EURUSD") for f in files))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cfg = _make_cfg({}, ["XAUUSD"], tmp)
|
||||
files = INIGenerator(cfg).generate_ini_files()
|
||||
expect("no alias -> bare", any(_grep_symbol(f, "XAUUSD") for f in files))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cfg = _make_cfg({"XAUUSD": "XAUUSDm"}, ["XAUUSD"], tmp)
|
||||
files = INIGenerator(cfg).generate_ini_files()
|
||||
expect("explicit override wins", any(_grep_symbol(f, "XAUUSDm") for f in files))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cfg = _make_cfg({}, ["XAUUSD"], tmp)
|
||||
cfg["backtest_settings"]["execution_delay"] = 0
|
||||
files = INIGenerator(cfg).generate_ini_files()
|
||||
expect("delay=0 -> ExecutionMode=0", _get_ini_value(files[0], "ExecutionMode") == "0")
|
||||
expect("delay=0 -> no ExecutionDelay line", _get_ini_value(files[0], "ExecutionDelay") is None)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cfg = _make_cfg({}, ["XAUUSD"], tmp)
|
||||
cfg["backtest_settings"]["execution_delay"] = -1
|
||||
files = INIGenerator(cfg).generate_ini_files()
|
||||
expect("delay=-1 -> ExecutionMode=-1", _get_ini_value(files[0], "ExecutionMode") == "-1")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cfg = _make_cfg({}, ["XAUUSD"], tmp)
|
||||
cfg["backtest_settings"]["execution_delay"] = 250
|
||||
files = INIGenerator(cfg).generate_ini_files()
|
||||
expect("delay=250 -> ExecutionMode=250", _get_ini_value(files[0], "ExecutionMode") == "250")
|
||||
|
||||
print("\nAll INI generator alias checks passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Symbol resolver self-check (pure lookup, no auto logic).
|
||||
|
||||
Run: python tests/test_symbol_resolver.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.normpath(os.path.join(HERE, "..", "scripts")))
|
||||
|
||||
from symbol_resolver import resolve_symbol
|
||||
|
||||
|
||||
def _bt(aliases=None):
|
||||
return {"symbol_aliases": aliases or {}}
|
||||
|
||||
|
||||
def expect(label, got, want):
|
||||
ok = got == want
|
||||
print(f" {'PASS' if ok else 'FAIL'} {label}: got={got!r} want={want!r}")
|
||||
if not ok:
|
||||
raise AssertionError(label)
|
||||
|
||||
|
||||
def main():
|
||||
print("[1] has alias -> use alias")
|
||||
expect("XAUUSD -> XAUUSDc", resolve_symbol("XAUUSD", _bt({"XAUUSD": "XAUUSDc"})), "XAUUSDc")
|
||||
|
||||
print("[2] has alias for another symbol -> that one passes through")
|
||||
expect("EURUSD no alias", resolve_symbol("EURUSD", _bt({"XAUUSD": "XAUUSDc"})), "EURUSD")
|
||||
|
||||
print("[3] no aliases config -> bare (back-compat)")
|
||||
expect("no config", resolve_symbol("XAUUSD", _bt()), "XAUUSD")
|
||||
|
||||
print("[4] alias value can be any string (suffix, full name, even identical)")
|
||||
expect("alias = c suffix", resolve_symbol("XAUUSD", _bt({"XAUUSD": "c"})), "c")
|
||||
|
||||
print("[5] empty symbol -> empty")
|
||||
expect("empty", resolve_symbol("", _bt({"": "XAUUSDc"})), "")
|
||||
|
||||
print("[6] aliases value None -> bare")
|
||||
expect("None value", resolve_symbol("XAUUSD", _bt({"XAUUSD": None})), "XAUUSD")
|
||||
|
||||
print("[7] symbol_aliases missing key entirely -> bare")
|
||||
expect("missing key", resolve_symbol("XAUUSD", {"date_range": {"from": "2020.01.01", "to": "2024.01.01"}}), "XAUUSD")
|
||||
|
||||
print("[8] long date span does NOT auto-switch (explicit override only)")
|
||||
expect("long span no alias", resolve_symbol("XAUUSD", {"date_range": {"from": "2010.01.01", "to": "2024.01.01"}}), "XAUUSD")
|
||||
|
||||
print("[9] explicit alias always wins regardless of date/optimization context")
|
||||
ctx = {"date_range": {"from": "2010.01.01", "to": "2024.01.01"},
|
||||
"optimization": 1, "symbol_aliases": {"XAUUSD": "XAUUSDm"}}
|
||||
expect("explicit + opt", resolve_symbol("XAUUSD", ctx), "XAUUSDm")
|
||||
|
||||
print("\nAll symbol resolver checks passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
# Verify the two backtest reports.
|
||||
import os, re
|
||||
|
||||
TARGET = r"C:\Users\Administrator\Desktop\mt5-backtest\reports"
|
||||
reports = sorted([f for f in os.listdir(TARGET) if f.startswith('MACD_MA_XAUUSD_5M_') and f.endswith('.htm')])
|
||||
|
||||
print(f"Found {len(reports)} candidate reports:")
|
||||
for r in reports:
|
||||
p = os.path.join(TARGET, r)
|
||||
size = os.path.getsize(p)
|
||||
text = open(p, encoding='utf-8', errors='ignore').read()
|
||||
m = re.search(r'交易品种:</td>\s*<td[^>]*><b>([^<]+)</b>', text)
|
||||
sym = m.group(1) if m else "(not found)"
|
||||
has_xauusd = 'XAUUSD' in text
|
||||
has_xauusdc = 'XAUUSDc' in text
|
||||
print(f" {r} size={size} symbol_in_report={sym} contains_XAUUSD={has_xauusd} contains_XAUUSDc={has_xauusdc}")
|
||||
Reference in New Issue
Block a user