Files
2026-07-06 03:32:12 +08:00

321 lines
12 KiB
Python

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"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MT5 Batch Backtest Report</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
h1 {{ color: #366092; }}
.summary {{ background: #f0f4f8; padding: 15px; border-radius: 5px; margin: 20px 0; }}
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
th {{ background: #366092; color: white; padding: 12px; text-align: left; }}
td {{ border: 1px solid #ddd; padding: 10px; }}
tr:nth-child(even) {{ background: #f9f9f9; }}
.best {{ color: green; font-weight: bold; }}
.worst {{ color: red; }}
</style>
</head>
<body>
<h1>MT5 Batch Backtest Report</h1>
<p>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<div class="summary">
<h2>Summary</h2>
<p>Total Tests: {len(self.results)}</p>
</div>
<table>
<tr>
<th>EA Name</th>
<th>Symbol</th>
<th>Period</th>
<th>Total Trades</th>
<th>Win Rate %</th>
<th>Profit Factor</th>
<th>Gross Profit</th>
<th>Gross Loss</th>
<th>Final Balance</th>
</tr>
"""
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"""
<tr class="{row_class}">
<td>{test_info.get("expert", "")}</td>
<td>{test_info.get("symbol", "")}</td>
<td>{test_info.get("period", "")}</td>
<td>{metrics.get("total_trades", 0)}</td>
<td>{metrics.get("win_rate", 0):.2f}%</td>
<td>{pf:.2f}</td>
<td>{metrics.get("gross_profit", 0):.2f}</td>
<td>{metrics.get("gross_loss", 0):.2f}</td>
<td>{metrics.get("final_balance", 0):.2f}</td>
</tr>
"""
html_content += """
</table>
</body>
</html>
"""
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()