参数优化报告自动保存
This commit is contained in:
@@ -168,6 +168,31 @@ def _launch_mt5(mt5_path, ini_path, data_dir, log_lines: list = None):
|
||||
return process
|
||||
|
||||
|
||||
def _find_latest_optimizer_xml(search_dirs, ea_name):
|
||||
"""在多个候选目录里找最新的优化 XML 结果。"""
|
||||
patterns = [
|
||||
f'{ea_name}_optimization.xml',
|
||||
f'{ea_name}_optimization[1].xml',
|
||||
f'{ea_name}_optimization[2].xml',
|
||||
]
|
||||
latest = None
|
||||
latest_mtime = 0
|
||||
for base in search_dirs:
|
||||
if not base or not os.path.isdir(base):
|
||||
continue
|
||||
for pattern in patterns:
|
||||
p = os.path.join(base, pattern)
|
||||
if os.path.exists(p):
|
||||
try:
|
||||
mt = os.path.getmtime(p)
|
||||
except OSError:
|
||||
continue
|
||||
if mt > latest_mtime:
|
||||
latest_mtime = mt
|
||||
latest = p
|
||||
return latest
|
||||
|
||||
|
||||
class MT5BacktestGUI:
|
||||
_CONFIG_FILE = "config/ea_configs.yaml" # 仅给"回测配置"标签页用
|
||||
_MT5_STATE_FILE = "optimizer/mt5_state.json" # 优化器用的 MT5 路径,独立小文件
|
||||
@@ -2175,16 +2200,17 @@ class MT5BacktestGUI:
|
||||
self.opt_log.insert('end', "启动失败,跳过该EA\n")
|
||||
continue
|
||||
|
||||
xml_name = ea_name + '_optimization.xml'
|
||||
xml_path = os.path.join(mt5_data_dir, xml_name)
|
||||
mt5_install_dir = os.path.dirname(mt5_path)
|
||||
xml_path = _find_latest_optimizer_xml([mt5_data_dir, mt5_install_dir], ea_name)
|
||||
|
||||
self.opt_log.insert('end', f"等待XML结果: {xml_path}\n")
|
||||
self.opt_log.insert('end', f"等待XML结果,候选目录: {mt5_data_dir} | {mt5_install_dir}\n")
|
||||
self.opt_log.update()
|
||||
|
||||
wait_timeout = 600
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < wait_timeout:
|
||||
if os.path.exists(xml_path):
|
||||
xml_path = _find_latest_optimizer_xml([mt5_data_dir, mt5_install_dir], ea_name)
|
||||
if xml_path and os.path.exists(xml_path):
|
||||
time.sleep(2)
|
||||
try:
|
||||
with open(xml_path, 'r', encoding='utf-8') as f:
|
||||
@@ -2197,14 +2223,16 @@ class MT5BacktestGUI:
|
||||
time.sleep(5)
|
||||
|
||||
if process.poll() is not None:
|
||||
if os.path.exists(xml_path):
|
||||
xml_path = _find_latest_optimizer_xml([mt5_data_dir, mt5_install_dir], ea_name)
|
||||
if xml_path and os.path.exists(xml_path):
|
||||
self.opt_log.insert('end', "MT5已退出,结果文件存在\n")
|
||||
self.opt_log.update()
|
||||
break
|
||||
self.opt_log.insert('end', "MT5已退出,等待结果文件...\n")
|
||||
self.opt_log.update()
|
||||
time.sleep(3)
|
||||
if os.path.exists(xml_path):
|
||||
xml_path = _find_latest_optimizer_xml([mt5_data_dir, mt5_install_dir], ea_name)
|
||||
if xml_path and os.path.exists(xml_path):
|
||||
break
|
||||
|
||||
if process.poll() is None:
|
||||
@@ -2216,11 +2244,13 @@ class MT5BacktestGUI:
|
||||
except:
|
||||
pass
|
||||
|
||||
dst_xml = os.path.join(reports_dir, ea_name, xml_name)
|
||||
if os.path.exists(xml_path):
|
||||
dst_xml = os.path.join(reports_dir, ea_name, ea_name + '_optimization.xml')
|
||||
if xml_path and os.path.exists(xml_path):
|
||||
os.makedirs(os.path.dirname(dst_xml), exist_ok=True)
|
||||
shutil.copy2(xml_path, dst_xml)
|
||||
self.opt_log.insert('end', f"已保存结果: {dst_xml}\n")
|
||||
else:
|
||||
self.opt_log.insert('end', f"未找到优化 XML,已检查: {mt5_data_dir} 与 {mt5_install_dir}\n")
|
||||
|
||||
self.opt_log.insert('end', f"{ea_name} 优化完成\n")
|
||||
self.opt_log.update()
|
||||
@@ -2237,17 +2267,17 @@ class MT5BacktestGUI:
|
||||
from ea_batch_optimizer import EABatchOptimizer
|
||||
from analyze_results import ResultAnalyzer
|
||||
optimizer = EABatchOptimizer(self.opt_dir.get(), mt5_data_dir=self.data_dir.get())
|
||||
mt5_terminal_dir = self.data_dir.get()
|
||||
mt5_search_dirs = [self.data_dir.get(), os.path.dirname(self.mt5_path.get())]
|
||||
|
||||
self.opt_log.insert('end', "正在分析结果...\n")
|
||||
self.opt_log.update()
|
||||
|
||||
results = []
|
||||
|
||||
if mt5_terminal_dir:
|
||||
if any(mt5_search_dirs):
|
||||
reports_dir = os.path.join(self.opt_dir.get(), 'reports', ea_name)
|
||||
analyzer = ResultAnalyzer(reports_dir)
|
||||
xml_path = analyzer.copy_xml_results_to_reports(mt5_terminal_dir, ea_name, reports_dir)
|
||||
xml_path = analyzer.copy_xml_results_to_reports(mt5_search_dirs, ea_name, reports_dir)
|
||||
if xml_path:
|
||||
self.opt_log.insert('end', f"从MT5复制结果: {xml_path}\n")
|
||||
xml_results = analyzer.parse_mt5_xml_optimization(xml_path)
|
||||
@@ -2311,7 +2341,7 @@ class MT5BacktestGUI:
|
||||
ea_cfg = json.load(f)
|
||||
wf = ea_cfg.get('walk_forward', {})
|
||||
if wf.get('enabled'):
|
||||
wf_result = analyzer.analyze_walk_forward(mt5_terminal_dir, ea_name, reports_dir)
|
||||
wf_result = analyzer.analyze_walk_forward(mt5_search_dirs, ea_name, reports_dir)
|
||||
is_pf = wf_result.get('in_sample', {}).get('profit_factor', 0)
|
||||
oos_pf = wf_result.get('out_of_sample', {}).get('profit_factor', 0)
|
||||
self.opt_log.insert('end', f"In-Sample PF: {is_pf}\n")
|
||||
|
||||
@@ -161,7 +161,7 @@ class ResultAnalyzer:
|
||||
|
||||
return results
|
||||
|
||||
def copy_xml_results_to_reports(self, mt5_terminal_dir: str, ea_name: str, reports_dir: str) -> str:
|
||||
def copy_xml_results_to_reports(self, mt5_terminal_dir, ea_name: str, reports_dir: str) -> str:
|
||||
xml_patterns = [
|
||||
f'{ea_name}_optimization.xml',
|
||||
f'{ea_name}_optimization[1].xml',
|
||||
@@ -172,13 +172,18 @@ class ResultAnalyzer:
|
||||
latest_xml = None
|
||||
latest_time = 0
|
||||
|
||||
for pattern in xml_patterns:
|
||||
xml_path = os.path.join(mt5_terminal_dir, pattern)
|
||||
if os.path.exists(xml_path):
|
||||
mtime = os.path.getmtime(xml_path)
|
||||
if mtime > latest_time:
|
||||
latest_time = mtime
|
||||
latest_xml = xml_path
|
||||
search_dirs = mt5_terminal_dir if isinstance(mt5_terminal_dir, (list, tuple)) else [mt5_terminal_dir]
|
||||
|
||||
for base_dir in search_dirs:
|
||||
if not base_dir or not os.path.isdir(base_dir):
|
||||
continue
|
||||
for pattern in xml_patterns:
|
||||
xml_path = os.path.join(base_dir, pattern)
|
||||
if os.path.exists(xml_path):
|
||||
mtime = os.path.getmtime(xml_path)
|
||||
if mtime > latest_time:
|
||||
latest_time = mtime
|
||||
latest_xml = xml_path
|
||||
|
||||
if latest_xml:
|
||||
dest_path = os.path.join(reports_dir, os.path.basename(latest_xml))
|
||||
@@ -269,7 +274,7 @@ class ResultAnalyzer:
|
||||
|
||||
return output_path
|
||||
|
||||
def analyze_walk_forward(self, mt5_terminal_dir: str, ea_name: str, reports_dir: str) -> Dict:
|
||||
def analyze_walk_forward(self, mt5_terminal_dir, ea_name: str, reports_dir: str) -> Dict:
|
||||
result = {
|
||||
'ea_name': ea_name,
|
||||
'in_sample': {},
|
||||
@@ -286,11 +291,20 @@ class ResultAnalyzer:
|
||||
best_is = sorted(is_results, key=lambda x: x.get('profit_factor', 0), reverse=True)[0]
|
||||
result['in_sample'] = best_is
|
||||
|
||||
forward_xml = os.path.join(mt5_terminal_dir, f'{ea_name}_optimization.forward.xml')
|
||||
if not os.path.exists(forward_xml):
|
||||
forward_xml = os.path.join(mt5_terminal_dir, f'{ea_name}_optimization[1].forward.xml')
|
||||
search_dirs = mt5_terminal_dir if isinstance(mt5_terminal_dir, (list, tuple)) else [mt5_terminal_dir]
|
||||
forward_xml = None
|
||||
for base_dir in search_dirs:
|
||||
if not base_dir or not os.path.isdir(base_dir):
|
||||
continue
|
||||
for candidate in (f'{ea_name}_optimization.forward.xml', f'{ea_name}_optimization[1].forward.xml'):
|
||||
p = os.path.join(base_dir, candidate)
|
||||
if os.path.exists(p):
|
||||
forward_xml = p
|
||||
break
|
||||
if forward_xml:
|
||||
break
|
||||
|
||||
if os.path.exists(forward_xml):
|
||||
if forward_xml and os.path.exists(forward_xml):
|
||||
oos_results = self.parse_mt5_xml_optimization(forward_xml)
|
||||
if oos_results:
|
||||
best_oos = sorted(oos_results, key=lambda x: x.get('profit_factor', 0), reverse=True)[0]
|
||||
@@ -317,4 +331,4 @@ class ResultAnalyzer:
|
||||
if is_dd > 0 and oos_dd > 0:
|
||||
result['dd_increase'] = round((oos_dd / is_dd - 1) * 100, 1) if is_dd > 0 else 0
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user