import os import json from datetime import datetime from typing import List, Dict, Optional import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class ReportGenerator: def __init__(self, results: List[Dict] = None): self.results = results or [] def generate_excel(self, output_path: str = "reports/batch_report.xlsx") -> str: try: import openpyxl from openpyxl.styles import Font, Alignment, PatternFill, Border, Side wb = openpyxl.Workbook() ws = wb.active ws.title = "Backtest Results" headers = [ "EA Name", "Symbol", "Period", "Model", "Total Trades", "Winning Trades", "Losing Trades", "Win Rate %", "Gross Profit", "Gross Loss", "Profit Factor", "Expected Payoff", "Initial Deposit", "Final Balance" ] header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid") header_font = Font(bold=True, color="FFFFFF") thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) for col, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col, value=header) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal='center', vertical='center') cell.border = thin_border for row_idx, result in enumerate(self.results, 2): metrics = result.get("metrics", {}) test_info = result.get("test_info", {}) row_data = [ test_info.get("expert", ""), test_info.get("symbol", ""), test_info.get("period", ""), test_info.get("model", ""), metrics.get("total_trades", 0), metrics.get("winning_trades", 0), metrics.get("losing_trades", 0), metrics.get("win_rate", 0), metrics.get("gross_profit", 0), metrics.get("gross_loss", 0), metrics.get("profit_factor", 0), metrics.get("expected_payoff", 0), metrics.get("initial_deposit", 0), metrics.get("final_balance", 0) ] for col, value in enumerate(row_data, 1): cell = ws.cell(row=row_idx, column=col, value=value) cell.border = thin_border if col >= 5: cell.number_format = '0.00' for col in range(1, len(headers) + 1): ws.column_dimensions[openpyxl.utils.get_column_letter(col)].width = 15 ws.auto_filter.ref = f"A1:N{len(self.results) + 1}" os.makedirs(os.path.dirname(output_path), exist_ok=True) wb.save(output_path) logger.info(f"Excel report saved: {output_path}") return output_path except Exception as e: logger.error(f"Error generating Excel report: {e}") raise def generate_csv(self, output_path: str = "reports/batch_report.csv") -> str: try: import csv os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True) headers = [ "EA Name", "Symbol", "Period", "Model", "Total Trades", "Winning Trades", "Losing Trades", "Win Rate %", "Gross Profit", "Gross Loss", "Profit Factor", "Expected Payoff", "Initial Deposit", "Final Balance" ] with open(output_path, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerow(headers) for result in self.results: metrics = result.get("metrics", {}) test_info = result.get("test_info", {}) row_data = [ test_info.get("expert", ""), test_info.get("symbol", ""), test_info.get("period", ""), test_info.get("model", ""), metrics.get("total_trades", 0), metrics.get("winning_trades", 0), metrics.get("losing_trades", 0), metrics.get("win_rate", 0), metrics.get("gross_profit", 0), metrics.get("gross_loss", 0), metrics.get("profit_factor", 0), metrics.get("expected_payoff", 0), metrics.get("initial_deposit", 0), metrics.get("final_balance", 0) ] writer.writerow(row_data) logger.info(f"CSV report saved: {output_path}") return output_path except Exception as e: logger.error(f"Error generating CSV report: {e}") raise def generate_html(self, output_path: str = "reports/batch_report.html") -> str: try: os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True) html_content = f""" MT5 Batch Backtest Report

MT5 Batch Backtest Report

Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

Summary

Total Tests: {len(self.results)}

""" for result in self.results: metrics = result.get("metrics", {}) test_info = result.get("test_info", {}) pf = metrics.get("profit_factor", 0) row_class = "best" if pf >= 2.0 else ("worst" if pf < 1.0 else "") html_content += f""" """ html_content += """
EA Name Symbol Period Total Trades Win Rate % Profit Factor Gross Profit Gross Loss Final Balance
{test_info.get("expert", "")} {test_info.get("symbol", "")} {test_info.get("period", "")} {metrics.get("total_trades", 0)} {metrics.get("win_rate", 0):.2f}% {pf:.2f} {metrics.get("gross_profit", 0):.2f} {metrics.get("gross_loss", 0):.2f} {metrics.get("final_balance", 0):.2f}
""" with open(output_path, 'w', encoding='utf-8') as f: f.write(html_content) logger.info(f"HTML report saved: {output_path}") return output_path except Exception as e: logger.error(f"Error generating HTML report: {e}") raise def generate_markdown(self, output_path: str = "reports/batch_report.md") -> str: try: os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True) md_lines = [ "# MT5 Batch Backtest Report", "", f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "", f"**Total Tests:** {len(self.results)}", "" ] if self.results: best = max(self.results, key=lambda x: x.get("metrics", {}).get("profit_factor", 0)) worst = min(self.results, key=lambda x: x.get("metrics", {}).get("profit_factor", 0)) md_lines.append(f"**Best Test:** {best.get('test_info', {}).get('expert', 'N/A')} " f"(PF: {best.get('metrics', {}).get('profit_factor', 0):.2f})") md_lines.append(f"**Worst Test:** {worst.get('test_info', {}).get('expert', 'N/A')} " f"(PF: {worst.get('metrics', {}).get('profit_factor', 0):.2f})") md_lines.append("") md_lines.append("## Results") md_lines.append("") md_lines.append("| EA Name | Symbol | Period | Total Trades | Win Rate | Profit Factor | " "Gross Profit | Gross Loss | Final Balance |") md_lines.append("|---------|--------|--------|--------------|----------|---------------|" "--------------|------------|---------------|") for result in self.results: metrics = result.get("metrics", {}) test_info = result.get("test_info", {}) md_lines.append( f"| {test_info.get('expert', '')} | " f"{test_info.get('symbol', '')} | " f"{test_info.get('period', '')} | " f"{metrics.get('total_trades', 0)} | " f"{metrics.get('win_rate', 0):.2f}% | " f"{metrics.get('profit_factor', 0):.2f} | " f"{metrics.get('gross_profit', 0):.2f} | " f"{metrics.get('gross_loss', 0):.2f} | " f"{metrics.get('final_balance', 0):.2f} |" ) with open(output_path, 'w', encoding='utf-8') as f: f.write("\n".join(md_lines)) logger.info(f"Markdown report saved: {output_path}") return output_path except Exception as e: logger.error(f"Error generating Markdown report: {e}") raise def main(): import argparse parser = argparse.ArgumentParser(description="MT5 Report Generator") parser.add_argument("--input", "-i", default="reports/parsed_results.json", help="Input JSON file from result_parser") parser.add_argument("--output-dir", "-o", default="reports", help="Output directory for reports") parser.add_argument("--format", "-f", choices=["excel", "csv", "html", "markdown", "all"], default="all", help="Output format") args = parser.parse_args() results = [] if os.path.exists(args.input): with open(args.input, 'r', encoding='utf-8') as f: data = json.load(f) results = data.get("results", []) else: logger.warning(f"Input file not found: {args.input}") logger.info("Use result_parser.py first to generate parsed results") if not results: print("No results to generate report") return generator = ReportGenerator(results) base_name = os.path.join(args.output_dir, "batch_report") if args.format in ["excel", "all"]: generator.generate_excel(f"{base_name}.xlsx") if args.format in ["csv", "all"]: generator.generate_csv(f"{base_name}.csv") if args.format in ["html", "all"]: generator.generate_html(f"{base_name}.html") if args.format in ["markdown", "all"]: generator.generate_markdown(f"{base_name}.md") print(f"\nReports generated in: {args.output_dir}") if __name__ == "__main__": main()