添加后缀映射功能

This commit is contained in:
2026-07-06 21:58:56 +08:00
parent f94728fa88
commit 29428b415b
16 changed files with 3157 additions and 48 deletions
+143
View File
@@ -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()
+96
View File
@@ -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()
+60
View File
@@ -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()
+16
View File
@@ -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}")