添加后缀映射功能
This commit is contained in:
@@ -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