321 lines
12 KiB
Python
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() |