Files
mt_mt5_backertest/scripts/ini_generator.py
T
2026-07-06 03:32:12 +08:00

256 lines
9.7 KiB
Python

import os
import sys
import yaml
from datetime import datetime
from itertools import product
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
class INIGenerator:
def __init__(self, source):
"""
source: YAML 文件路径 str 或 已加载的 config dict(GUI 内存直接传入用)。
"""
if isinstance(source, dict):
self.config_path = None
self.config = source
else:
self.config_path = source
self.config = self._load_config()
self.config["mt5_settings"] = resolve_mt5_settings(self.config.get("mt5_settings", {}))
self.output_dir = self.config["mt5_settings"]["ini_dir"]
os.makedirs(self.output_dir, exist_ok=True)
def _load_config(self) -> Dict:
with open(self.config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def _scan_experts_dir(self) -> List[str]:
data_dir = self.config["mt5_settings"].get("data_dir", "")
if not data_dir:
mt5_base = os.path.dirname(self.config["mt5_settings"]["terminal_path"])
data_dir = mt5_base
experts_dir = os.path.join(data_dir, "MQL5", "Experts")
ea_files = []
if not os.path.exists(experts_dir):
return []
for root, dirs, files in os.walk(experts_dir):
for f in files:
if f.endswith('.ex5'):
rel_path = os.path.relpath(os.path.join(root, f), experts_dir)
ea_files.append(rel_path)
return ea_files
def _load_set_file(self, set_file_path: str) -> Dict[str, Any]:
params = {}
if not os.path.exists(set_file_path):
return params
with open(set_file_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith(';') and '=' in line:
key, value = line.split('=', 1)
try:
params[key.strip()] = float(value.strip())
except:
params[key.strip()] = value.strip()
return params
def _generate_parameter_combinations(self, parameters: Dict[str, List]) -> List[Dict]:
if not parameters:
return [{}]
keys = list(parameters.keys())
values = list(parameters.values())
combinations = list(product(*values))
return [dict(zip(keys, combo)) for combo in combinations]
def _get_timeframe_code(self, timeframe: str) -> str:
mapping = {
"M1": "M1", "M5": "M5", "M15": "M15",
"H1": "H1", "H4": "H4", "D1": "D1", "W1": "W1"
}
return mapping.get(timeframe, "H1")
def _params_hash(self, parameters: Dict) -> str:
if not parameters:
return "default"
sorted_params = sorted(parameters.items())
param_str = "_".join([f"{k}{v}" for k, v in sorted_params])
return str(abs(hash(param_str)))[:8]
def _generate_filename(self, ea_name: str, symbol: str,
timeframe: str, parameters: Dict) -> str:
safe_ea_name = ea_name.replace("\\", "_").replace("/", "_").replace("..", "")
for c in '()[]{}|\\/*?:"\'<>':
safe_ea_name = safe_ea_name.replace(c, "_")
param_hash = self._params_hash(parameters)
return f"{safe_ea_name}_{symbol}_{timeframe}_{param_hash}.ini"
def _build_ini_content(self, ea_filename: str, symbol: str, timeframe: str,
bt_settings: Dict, parameters: Dict, ea_name: str,
set_file: Optional[str] = None) -> str:
date_from = bt_settings["date_range"]["from"]
date_to = bt_settings["date_range"]["to"]
report_name = f"{ea_name}_{symbol}_{timeframe}_{self._params_hash(parameters)}"
for c in '()[]{}|\\/*?:"\'<>':
report_name = report_name.replace(c, "_")
safe_report_name = report_name
ini_lines = [
"; MT5 Strategy Tester Configuration",
f"; Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"",
"[Tester]",
f"Expert={ea_filename}",
]
if set_file:
ini_lines.append(f"ExpertParameters={set_file}")
else:
ini_lines.append("ExpertParameters=")
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"Optimization={bt_settings['optimization']}",
])
if bt_settings.get("optimization"):
ini_lines.append("OptimizationCriterion=6")
ini_lines.extend([
f"FromDate={date_from}",
f"ToDate={date_to}",
f"ForwardMode={bt_settings.get('forward_mode', 0)}",
f"ForwardDate={bt_settings.get('forward_date', '')}",
f"Report={safe_report_name}",
f"ReplaceReport={1 if bt_settings['replace_report'] else 0}",
f"ShutdownTerminal={1 if bt_settings['shutdown_terminal'] else 0}",
f"Deposit={int(float(bt_settings['deposit']))}",
f"Currency={bt_settings['currency']}",
f"Leverage={bt_settings['leverage']}",
f"Visual={bt_settings['visual']}",
"ProfitInPips=0",
])
if parameters:
ini_lines.extend(["", "; EA Parameters"])
for param_name, param_value in parameters.items():
ini_lines.append(f"{param_name}={param_value}")
return "\n".join(ini_lines)
def generate_ini_files(self) -> List[str]:
generated_files = []
bt_settings = self.config["backtest_settings"]
set_files_dir = self.config["mt5_settings"].get("set_files_dir", "config/sets")
ea_configs = self.config.get("eas", [])
if not ea_configs:
print("No EAs configured, scanning MT5 Experts directory...")
ea_files = self._scan_experts_dir()
if ea_files:
print(f"Found {len(ea_files)} EA files: {ea_files}")
for ea_file in ea_files:
ea_name = os.path.splitext(ea_file)[0]
ea_configs.append({
"name": ea_name,
"filename": ea_file,
"description": "Auto-scanned EA"
})
else:
print("No EAs found in Experts directory")
return []
for ea in ea_configs:
ea_name = ea["name"]
ea_filename = ea["filename"]
set_file = ea.get("set_file")
parameters = ea.get("parameters", {})
if set_file:
set_file_path = os.path.join(set_files_dir, set_file)
set_params = self._load_set_file(set_file_path)
param_combinations = [set_params] if set_params else [{}]
else:
param_combinations = self._generate_parameter_combinations(parameters)
for symbol in bt_settings["symbols"]:
for timeframe in bt_settings["timeframes"]:
for param_combo in param_combinations:
ini_content = self._build_ini_content(
ea_filename, symbol, timeframe,
bt_settings, param_combo, ea_name, set_file
)
ini_filename = self._generate_filename(
ea_name, symbol, timeframe, param_combo
)
ini_path = os.path.join(self.output_dir, ini_filename)
with open(ini_path, 'w', encoding='utf-8') as f:
f.write(ini_content)
generated_files.append(ini_path)
return generated_files
def generate_batch_run_script(self, ini_files: List[str],
output_script: str = "run_backtests.bat"):
mt5_path = self.config["mt5_settings"]["terminal_path"]
lines = [
"@echo off",
"echo MT5 Batch Backtest Runner",
"echo =======================",
"",
f'SET "MT5_PATH={mt5_path}"',
f'SET "INI_DIR={self.output_dir}"',
"",
]
for ini_file in ini_files:
ini_filename = os.path.basename(ini_file)
lines.append(f'echo Running: {ini_filename}')
lines.append(
f'START "MT5" /WAIT "%MT5_PATH%" /portable /config:"%INI_DIR%\\{ini_filename}"'
)
lines.append("if errorlevel 1 echo Failed: " + ini_filename)
lines.append("")
lines.append("echo All backtests completed!")
lines.append("pause")
with open(output_script, 'w', encoding='utf-8') as f:
f.write("\n".join(lines))
return output_script
def main():
config_path = os.path.join(os.path.dirname(__file__), "..", "config", "ea_configs.yaml")
generator = INIGenerator(config_path)
print("Generating INI files...")
ini_files = generator.generate_ini_files()
print(f"Generated {len(ini_files)} INI files")
for f in ini_files[:5]:
print(f" - {os.path.basename(f)}")
if len(ini_files) > 5:
print(f" ... and {len(ini_files) - 5} more")
if ini_files:
print("\nGenerating batch run script...")
script_path = generator.generate_batch_run_script(ini_files)
print(f"Batch script: {script_path}")
if __name__ == "__main__":
main()