# -*- coding: utf-8 -*- """ AI Agent MT5 报告分析 API ========================= 专为 AI Agent 设计的结构化接口。解析 MT5 报告,输出 JSON 可序列化结果, 提供设参优化建议。 用法: from mt5_agent import parse_report, analyze_report, compare_reports, get_param_suggestions # 解析单份报告 report = parse_report("path/to/report.htm") print(report.summary) # 汇总指标 print(report.metrics) # 扩展指标(PF、胜率、回撤等) print(report.trades) # 逐笔交易列表 # 单份报告分析 analysis = analyze_report(report) print(analysis.suggestions) # 优化建议列表 # 对比两份报告 comparison = compare_reports(report_a, report_b) print(comparison.diffs) # 关键指标差异 # 批量分析 from mt5_agent import batch_analyze result = batch_analyze(["dir1/", "dir2/"], "*.htm") print(result.rankings) # 按 PF 排名 print(result.anomalies) # 异常报告 # 设参优化建议 suggestions = get_param_suggestions(report, analysis) print(suggestions) # 可写入 .set 的参数修改建议 """ from __future__ import annotations import json import os from typing import Any, Dict, List, Optional, Tuple import numpy as np import pandas as pd # 导入内部模块 import mt5_report_parser as mp from mt5_report_parser import parse_report as _parse_report, MT5Report import run_analysis as ra import walk_forward as wf from param_scan import detect_plateaus, build_pivot, load_scan_results, overfit_score import mae_mfe as mm # ============================================================================= # # 数据模型(JSON 可序列化) # ============================================================================= # class ReportData: """解析后的 MT5 报告数据(AI 友好结构)。""" def __init__(self, rep: MT5Report): self.source_file = rep.source_file self.meta = _to_dict(rep.meta) self.summary = _to_dict(rep.summary_norm) # 数值化后的汇总 self._trades_df = rep.trades # 保持 DataFrame 原类型 self.trades = _trades_to_dict(rep.trades) if rep.trades is not None else [] self.metrics = _compute_metrics(rep.trades) if rep.trades is not None else {} # What-If 需要 open_time 列,部分报告可能缺失 if rep.trades is not None and "open_time" in rep.trades.columns: self.what_if = _what_if_to_dict(ra.whatif_scenarios(rep)) else: self.what_if = [] def to_json(self, indent: int = 2) -> str: """序列化为 JSON 字符串。""" return json.dumps({ "source_file": self.source_file, "meta": self.meta, "summary": self.summary, "metrics": self.metrics, "trades_count": len(self.trades), "what_if_scenarios": self.what_if, }, ensure_ascii=False, indent=indent) class AnalysisResult: """单份报告分析结果。""" def __init__(self, report: ReportData): self.report = report self.walk_forward = _wf_analysis(report) self.anomalies = _detect_anomalies(report) self.suggestions = _generate_suggestions(report, self.walk_forward, self.anomalies) def to_json(self, indent: int = 2) -> str: return json.dumps({ "source_file": self.report.source_file, "summary": self.report.summary, "metrics": self.report.metrics, "walk_forward": self.walk_forward, "anomalies": self.anomalies, "suggestions": self.suggestions, }, ensure_ascii=False, indent=indent) class ComparisonResult: """两份报告对比结果。""" def __init__(self, report_a: ReportData, report_b: ReportData): self.report_a = report_a self.report_b = report_b self.diffs = _compute_diffs(report_a, report_b) self.suggestions = _compare_suggestions(report_a, report_b) def to_json(self, indent: int = 2) -> str: return json.dumps({ "report_a": {"file": self.report_a.source_file, "summary": self.report_a.summary}, "report_b": {"file": self.report_b.source_file, "summary": self.report_b.summary}, "diffs": self.diffs, "suggestions": self.suggestions, }, ensure_ascii=False, indent=indent) class BatchResult: """批量分析结果。""" def __init__(self, reports: List[ReportData]): self.reports = reports self.rankings = _batch_rankings(reports) self.anomalies = [r for r in reports if r.metrics.get("n_trades", 0) < 30] self.best = self.rankings[0] if self.rankings else None def to_json(self, indent: int = 2) -> str: return json.dumps({ "count": len(self.reports), "rankings": self.rankings, "anomalies_count": len(self.anomalies), "best": self.best, }, ensure_ascii=False, indent=indent) # ============================================================================= # # 核心函数 # ============================================================================= # def parse_report(path: str) -> ReportData: """解析 MT5 报告(支持 .xlsx / .htm / .html)。""" rep = _parse_report(path) return ReportData(rep) def analyze_report(report: ReportData) -> AnalysisResult: """对单份报告做深度分析。""" return AnalysisResult(report) def compare_reports(report_a: ReportData, report_b: ReportData) -> ComparisonResult: """对比两份报告。""" return ComparisonResult(report_a, report_b) def batch_analyze(paths: List[str], pattern: str = "*.htm") -> BatchResult: """批量分析多个报告。""" from batch_report import scan_reports, parse_batch, build_summary_df files = scan_reports(paths, pattern) raw_reports = parse_batch(files) report_datas = [] for name, rep in raw_reports: try: rd = ReportData(rep) rd._filename = name report_datas.append(rd) except Exception as e: print("警告: 跳过 %s - %s" % (name, e)) return BatchResult(report_datas) def get_param_suggestions(analysis: AnalysisResult) -> List[Dict[str, Any]]: """ 生成 .set 文件参数修改建议。 返回格式:[{"param_name": "InpStopLossPoints", "suggested_value": "150", "reason": "当前 200,亏损截断场景显示 150 更优"}] """ suggestions = [] report = analysis.report metrics = report.metrics summary = report.summary what_if = report.what_if # 建议 1:止损优化 if metrics.get("avg_loss", 0) != 0: # 找亏损截断场景 cut_scenarios = [s for s in what_if if "亏损截断" in s.get("name", "")] if cut_scenarios: base_pf = summary.get("profit_factor", 0) for s in cut_scenarios: if s.get("pf", 0) > base_pf and s.get("dd", 0) < metrics.get("max_dd", 0): cap = s.get("desc", "") suggestions.append({ "param_name": "InpStopLossPoints", "suggested_value": None, # 需要从场景推算 "reason": f"亏损截断场景 PF={s['pf']:.2f} > 基线 PF={base_pf:.2f},回撤从 {metrics.get('max_dd',0):.0f} 降至 {s['dd']:.0f}", "priority": "high", }) # 建议 2:止盈优化 tp_scenarios = [s for s in what_if if "盈利单放大" in s.get("name", "")] if tp_scenarios: base_pf = summary.get("profit_factor", 0) for s in tp_scenarios: if s.get("pf", 0) > base_pf: suggestions.append({ "param_name": "InpTakeProfitPoints", "suggested_value": None, "reason": f"盈利单放大场景 PF={s['pf']:.2f} > 基线 PF={base_pf:.2f},当前止盈可能太紧", "priority": "medium", }) # 建议 3:仓位优化 halve_scenarios = [s for s in what_if if "仓位减半" in s.get("name", "")] if halve_scenarios: base_dd = metrics.get("max_dd", 0) for s in halve_scenarios: if abs(s.get("dd", 0)) < abs(base_dd * 0.5): suggestions.append({ "param_name": "InpFixedLots", "suggested_value": None, "reason": f"仓位减半后回撤从 {base_dd:.0f} 降至 {s['dd']:.0f},可考虑降低仓位", "priority": "medium", }) # 建议 4:方向性建议 if metrics.get("by_direction"): for direction, stats in metrics["by_direction"].items(): if stats.get("win_rate", 0) < 35 and stats.get("profit_factor", 0) < 0.8: suggestions.append({ "param_name": None, "suggested_value": None, "reason": f"方向 {direction} 胜率仅 {stats['win_rate']:.1f}%,PF={stats['profit_factor']:.2f},考虑单独过滤该方向信号", "priority": "high", }) # 建议 5:时段过滤 if metrics.get("by_hour"): bad_hours = [h for h, stats in metrics["by_hour"].items() if stats.get("sum", 0) < 0 and stats.get("count", 0) > 5] if bad_hours: suggestions.append({ "param_name": "InpFreezeBarCount", "suggested_value": None, "reason": f"时段 {bad_hours[:3]} 净盈利持续为负,建议添加时段过滤", "priority": "low", }) return suggestions # ============================================================================= # # 内部工具函数 # ============================================================================= # def _to_dict(obj: Any) -> Any: """递归将 pandas/numpy 对象转为原生 Python 类型。""" if obj is None: return None if isinstance(obj, dict): return {k: _to_dict(v) for k, v in obj.items()} if isinstance(obj, (list, tuple)): return [_to_dict(v) for v in obj] if isinstance(obj, (np.integer,)): return int(obj) if isinstance(obj, (np.floating,)): if np.isnan(obj) or np.isinf(obj): return None return float(obj) if isinstance(obj, pd.Timestamp): return str(obj) if isinstance(obj, pd.DataFrame): return _to_dict(obj.to_dict(orient="index")) if isinstance(obj, pd.Series): return _to_dict(obj.to_dict()) if isinstance(obj, pd.Index): return _to_dict(list(obj)) if isinstance(obj, (float, int)) and (np.isnan(obj) or np.isinf(obj)): return None if isinstance(obj, (int, float, str, bool)): return obj return str(obj) def _trades_to_dict(trades: pd.DataFrame) -> List[Dict[str, Any]]: """将逐笔交易 DataFrame 转为字典列表。""" if trades is None or trades.empty: return [] result = [] for _, row in trades.iterrows(): item = {} for col in ["open_time", "close_time", "direction", "volume", "open_price", "close_price", "profit", "swap", "commission", "net_profit", "duration_min"]: val = row.get(col) if val is None or (isinstance(val, float) and np.isnan(val)): item[col] = None elif isinstance(val, pd.Timestamp): item[col] = str(val) elif isinstance(val, (np.integer,)): item[col] = int(val) elif isinstance(val, (np.floating,)): item[col] = float(val) else: item[col] = val result.append(item) return result def _compute_metrics(trades: pd.DataFrame) -> Dict[str, Any]: """计算扩展指标(JSON 可序列化)。""" m = ra.extended_metrics(trades) if trades is not None and not trades.empty else {} result = _to_dict(m) # 移除内部字段 for key in ["_equity", "_dd", "_net"]: result.pop(key, None) return result def _what_if_to_dict(scenarios: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """将 What-If 场景转为字典列表。""" return [_to_dict(s) for s in scenarios] def _wf_analysis(report: ReportData) -> Dict[str, Any]: """Walk-Forward 分析。""" trades_df = getattr(report, "_trades_df", None) if trades_df is None or trades_df.empty: return {} # 确保 open_time 全为 Timestamp 类型 trades_df = trades_df.copy() trades_df["open_time"] = pd.to_datetime(trades_df["open_time"], errors="coerce") trades_df = trades_df.dropna(subset=["open_time"]) trades_df["net_profit"] = pd.to_numeric(trades_df["net_profit"], errors="coerce") trades_df = trades_df.dropna(subset=["net_profit"]) if trades_df.empty: return {} windows = wf.walk_forward(trades_df) summary = wf.wf_summary(windows) return _to_dict(summary) def _detect_anomalies(report: ReportData) -> List[Dict[str, Any]]: """检测异常指标。""" anomalies = [] metrics = report.metrics summary = report.summary if metrics.get("n_trades", 0) < 30: anomalies.append({ "type": "样本不足", "detail": f"仅 {metrics['n_trades']} 笔交易", }) if summary.get("profit_factor", 0) > 10: anomalies.append({ "type": "PF异常高", "detail": f"PF={summary['profit_factor']:.2f},可能过拟合", }) if metrics.get("max_dd", 0) < -1000: anomalies.append({ "type": "回撤过大", "detail": f"最大回撤 {metrics['max_dd']:.0f}", }) return anomalies def _generate_suggestions( report: ReportData, wf_result: Dict[str, Any], anomalies: List[Dict[str, Any]], ) -> List[Dict[str, Any]]: """生成优化建议。""" suggestions = [] metrics = report.metrics summary = report.summary # Walk-Forward 建议 if wf_result: wfe = wf_result.get("wfe", 0) if wfe is not None and wfe < 0.3: suggestions.append({ "type": "Walk-Forward", "detail": f"WFE={wfe:.2f},OOS 表现弱,参数可能过拟合 IS 段", "priority": "high", }) # 什么-If 建议 for s in report.what_if: name = s.get("name", "") if "信号反向" in name and s.get("net", 0) > 0 and s.get("pf", 0) > 1: suggestions.append({ "type": "信号方向", "detail": f"反向场景净盈利={s['net']:+.2f}, PF={s['pf']:.2f},可能方向逻辑写反", "priority": "critical", }) return suggestions def _compute_diffs(report_a: ReportData, report_b: ReportData) -> Dict[str, Any]: """计算两份报告的关键指标差异。""" diffs = {} for key in ["profit_factor", "win_rate", "net_profit", "max_dd", "sharpe", "sortino"]: va = report_a.summary.get(key) vb = report_b.summary.get(key) if va is not None and vb is not None: diff = vb - va if (not np.isinf(va) or not np.isinf(vb)) else None pct = ((vb - va) / va * 100) if va != 0 and not np.isinf(va) else None diffs[key] = { "a": va, "b": vb, "diff": diff, "pct_change": pct, } return diffs def _compare_suggestions(report_a: ReportData, report_b: ReportData) -> List[Dict[str, Any]]: """对比分析建议。""" suggestions = [] pf_a = report_a.summary.get("profit_factor", 0) pf_b = report_b.summary.get("profit_factor", 0) if pf_b > pf_a and pf_a > 0: suggestions.append({ "type": "参数优化", "detail": f"报告B PF={pf_b:.2f} 优于报告A PF={pf_a:.2f},差 {pf_b-pf_a:.2f}", "priority": "medium", }) elif pf_b < pf_a and pf_b > 0: suggestions.append({ "type": "参数回退", "detail": f"报告B PF={pf_b:.2f} 差于报告A PF={pf_a:.2f},参数可能过拟合", "priority": "medium", }) return suggestions def _batch_rankings(reports: List[ReportData]) -> List[Dict[str, Any]]: """按 PF 排名。""" valid = [r for r in reports if r.summary.get("profit_factor") is not None and r.metrics.get("n_trades", 0) > 0] sorted_reports = sorted(valid, key=lambda r: r.summary.get("profit_factor", 0), reverse=True) rankings = [] for i, r in enumerate(sorted_reports[:20]): # 只取前 20 rankings.append({ "rank": i + 1, "file": r.source_file, "filename": getattr(r, "_filename", os.path.basename(r.source_file)), "profit_factor": r.summary.get("profit_factor"), "net_profit": r.summary.get("total_net_profit"), "win_rate": r.metrics.get("win_rate"), "n_trades": r.metrics.get("n_trades"), "max_dd": r.metrics.get("max_dd"), }) return rankings # ============================================================================= # # CLI # ============================================================================= # if __name__ == "__main__": import argparse ap = argparse.ArgumentParser(description="AI Agent MT5 报告分析 API") ap.add_argument("path", help="报告文件路径或目录") ap.add_argument("--compare", help="对比的另一份报告路径") ap.add_argument("--batch", action="store_true", help="批量分析目录") ap.add_argument("--suggestions", action="store_true", help="输出设参建议") ap.add_argument("--format", choices=["json", "text"], default="json", help="输出格式") args = ap.parse_args() if args.batch: result = batch_analyze([args.path]) print(result.to_json()) elif args.compare: a = parse_report(args.path) b = parse_report(args.compare) comparison = compare_reports(a, b) if args.format == "json": print(comparison.to_json()) else: print(f"\n报告A: {args.path}") print(f" PF={a.summary.get('profit_factor')} 净盈利={a.summary.get('total_net_profit')}") print(f"报告B: {args.compare}") print(f" PF={b.summary.get('profit_factor')} 净盈利={b.summary.get('total_net_profit')}") print(f"\n关键差异:") for key, diff in comparison.diffs.items(): print(f" {key}: A={diff['a']} B={diff['b']} 差={diff['diff']:.2f}") else: report = parse_report(args.path) analysis = analyze_report(report) if args.suggestions: suggestions = get_param_suggestions(analysis) print(json.dumps(suggestions, ensure_ascii=False, indent=2)) elif args.format == "json": print(analysis.to_json()) else: print(f"\n报告: {args.path}") print(f" PF={report.summary.get('profit_factor')} 净盈利={report.summary.get('total_net_profit')}") print(f" 胜率={report.metrics.get('win_rate')} 笔数={report.metrics.get('n_trades')}") print(f" 回撤={report.metrics.get('max_dd')}") print(f"\n建议 ({len(analysis.suggestions)} 条):") for s in analysis.suggestions: print(f" [{s.get('priority','?')}] {s['type']}: {s['detail']}")