Add export-to-browser utility
- export-report-html.py: Converts MT5 report to clean, styled HTML - export-to-browser.sh: Wrapper script for easy usage - Generates beautiful HTML report viewable in any browser - Includes performance summary, trade stats, and risk metrics
This commit is contained in:
381
export-report-html.py
Executable file
381
export-report-html.py
Executable file
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export MT5 HTML report to a clean, browser-viewable HTML file"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
def parse_report(html_file):
|
||||
"""Parse MT5 HTML report and extract data"""
|
||||
try:
|
||||
with open(html_file, 'rb') as f:
|
||||
content = f.read().decode('utf-16-le', errors='ignore')
|
||||
except Exception as e:
|
||||
print(f"Error reading file: {e}")
|
||||
return None
|
||||
|
||||
# Clean text for parsing
|
||||
text = re.sub('<[^<]+?>', ' ', content)
|
||||
text = text.replace(' ', ' ')
|
||||
|
||||
def extract_value(pattern, text, group=1):
|
||||
match = re.search(pattern, text)
|
||||
return match.group(group).strip() if match else "N/A"
|
||||
|
||||
def parse_num(s):
|
||||
if not s or s == "N/A":
|
||||
return 0
|
||||
try:
|
||||
s = str(s).replace(' ', '').replace(',', '.')
|
||||
parts = s.split('.')
|
||||
if len(parts) > 2:
|
||||
s = ''.join(parts[:-1]) + '.' + parts[-1]
|
||||
return float(s)
|
||||
except:
|
||||
return 0
|
||||
|
||||
data = {
|
||||
'filename': os.path.basename(html_file),
|
||||
'account': extract_value(r'Account:\s*(\d+)', text),
|
||||
'name': extract_value(r'Name:\s*([^\d]+?)(?:\s+Account|\s*$)', text),
|
||||
'company': extract_value(r'Company:\s*([^\d]+?)(?:\s+Date|\s*$)', text),
|
||||
'net_profit': extract_value(r'Total Net Profit[^\d\-]*([\-\d\s,\.]+)', text),
|
||||
'gross_profit': extract_value(r'Gross Profit[^\d]*([\d\s,\.]+)', text),
|
||||
'gross_loss': extract_value(r'Gross Loss[^\d\-]*([\-\d\s,\.]+)', text),
|
||||
'profit_factor': extract_value(r'Profit Factor[^\d]*([\d\.]+)', text),
|
||||
'expected_payoff': extract_value(r'Expected Payoff[^\d]*([\d\s,\.]+)', text),
|
||||
'total_trades': extract_value(r'Total Trades[^\d]*(\d+)', text),
|
||||
'profit_trades': extract_value(r'Profit Trades \(%[^)]*\)[^\d]*(\d+)', text),
|
||||
'loss_trades': extract_value(r'Loss Trades \(%[^)]*\)[^\d]*(\d+)', text),
|
||||
'win_pct': extract_value(r'Profit Trades \(%[^)]*\)[^\d]*\d+[^\d]*\((\d+\.?\d*)%', text),
|
||||
'largest_profit': extract_value(r'Largest profit trade[^\d]*([\d\s,\.]+)', text),
|
||||
'largest_loss': extract_value(r'Largest loss trade[^\d\-]*([\-\d\s,\.]+)', text),
|
||||
'max_drawdown': extract_value(r'Balance Drawdown Maximal[^\d]*([\d\s,\.]+)', text),
|
||||
}
|
||||
|
||||
# Calculate values
|
||||
net = parse_num(data['net_profit'])
|
||||
data['starting_balance'] = 100000.00
|
||||
data['current_balance'] = 100000.00 + net
|
||||
data['return_pct'] = (net / 100000.00) * 100
|
||||
|
||||
return data
|
||||
|
||||
def generate_html(data, output_file):
|
||||
"""Generate clean HTML report"""
|
||||
|
||||
html_content = f'''<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MT5 Trading Report - Account {data['account']}</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
overflow: hidden;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}}
|
||||
.header h1 {{
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
.header p {{
|
||||
opacity: 0.9;
|
||||
font-size: 16px;
|
||||
}}
|
||||
.content {{
|
||||
padding: 30px;
|
||||
}}
|
||||
.section {{
|
||||
margin-bottom: 30px;
|
||||
}}
|
||||
.section-title {{
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}}
|
||||
.info-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
}}
|
||||
.info-item {{
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}}
|
||||
.info-label {{
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
.info-value {{
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}}
|
||||
.profit {{
|
||||
color: #28a745;
|
||||
}}
|
||||
.loss {{
|
||||
color: #dc3545;
|
||||
}}
|
||||
.metric-box {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.metric-value {{
|
||||
font-size: 42px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
.metric-label {{
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}}
|
||||
.stats-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}}
|
||||
.stat-card {{
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
transition: transform 0.2s;
|
||||
}}
|
||||
.stat-card:hover {{
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
|
||||
}}
|
||||
.stat-value {{
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
.stat-label {{
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}}
|
||||
.footer {{
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}}
|
||||
.badge {{
|
||||
display: inline-block;
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-left: 10px;
|
||||
}}
|
||||
.badge-success {{
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}}
|
||||
.badge-warning {{
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}}
|
||||
@media (max-width: 600px) {{
|
||||
.content {{
|
||||
padding: 20px;
|
||||
}}
|
||||
.metric-value {{
|
||||
font-size: 32px;
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📊 MT5 Trading Report</h1>
|
||||
<p>Account {data['account']} | {data['name']} | Generated on {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Performance Summary -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">💰 Performance Summary</h2>
|
||||
<div class="metric-box">
|
||||
<div class="metric-value">{'+' if data['return_pct'] > 0 else ''}{data['return_pct']:.2f}%</div>
|
||||
<div class="metric-label">Total Return</div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Starting Balance</div>
|
||||
<div class="info-value">${data['starting_balance']:,.2f}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Current Balance</div>
|
||||
<div class="info-value">${data['current_balance']:,.2f}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Net Profit</div>
|
||||
<div class="info-value {'profit' if data['return_pct'] > 0 else 'loss'}">${data['net_profit']}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Profit Factor</div>
|
||||
<div class="info-value">{data['profit_factor']}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Gross Profit</div>
|
||||
<div class="info-value profit">${data['gross_profit']}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Gross Loss</div>
|
||||
<div class="info-value loss">${data['gross_loss']}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trade Statistics -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">📈 Trade Statistics</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{data['total_trades']}</div>
|
||||
<div class="stat-label">Total Trades</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #28a745;">{data['profit_trades']}</div>
|
||||
<div class="stat-label">Winning Trades</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #dc3545;">{data['loss_trades']}</div>
|
||||
<div class="stat-label">Losing Trades</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: {'#28a745' if float(data['win_pct'] or 0) > 60 else '#ffc107'}">{data['win_pct']}%</div>
|
||||
<div class="stat-label">Win Rate</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #28a745;">${data['largest_profit']}</div>
|
||||
<div class="stat-label">Largest Win</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #dc3545;">${data['largest_loss']}</div>
|
||||
<div class="stat-label">Largest Loss</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Risk Metrics -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">⚠️ Risk Metrics</h2>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Expected Payoff</div>
|
||||
<div class="info-value">${data['expected_payoff']}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Max Drawdown</div>
|
||||
<div class="info-value">${data['max_drawdown']}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Report generated by MT5 Report Exporter | Strategy: MultiSignal Confluence EA</p>
|
||||
<p>Source: {data['filename']}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
return output_file
|
||||
|
||||
def main():
|
||||
# Find latest report if no argument provided
|
||||
if len(sys.argv) > 1:
|
||||
report_file = sys.argv[1]
|
||||
else:
|
||||
report_dir = os.path.expanduser("~/mt5-docker/config/.wine/drive_c/users/abc/Desktop")
|
||||
import glob
|
||||
reports = glob.glob(f"{report_dir}/ReportHistory-*.html")
|
||||
if not reports:
|
||||
print("❌ No report files found!")
|
||||
print(f"Searched: {report_dir}")
|
||||
sys.exit(1)
|
||||
report_file = max(reports, key=os.path.getmtime)
|
||||
|
||||
if not os.path.exists(report_file):
|
||||
print(f"❌ File not found: {report_file}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"📊 Parsing report: {os.path.basename(report_file)}")
|
||||
|
||||
data = parse_report(report_file)
|
||||
if not data:
|
||||
print("❌ Failed to parse report")
|
||||
sys.exit(1)
|
||||
|
||||
# Generate output filename
|
||||
timestamp = datetime.now().strftime('%Y%m%d-%H%M%S')
|
||||
output_file = os.path.expanduser(f"~/mt5-report-{data['account']}-{timestamp}.html")
|
||||
|
||||
# Generate HTML
|
||||
generate_html(data, output_file)
|
||||
|
||||
print(f"✅ Report exported to: {output_file}")
|
||||
print("")
|
||||
print(f"📊 Summary:")
|
||||
print(f" Account: {data['account']}")
|
||||
print(f" Name: {data['name']}")
|
||||
print(f" Profit: ${data['net_profit']}")
|
||||
print(f" Return: {data['return_pct']:.2f}%")
|
||||
print(f" Win Rate: {data['win_pct']}%")
|
||||
print("")
|
||||
print("🌐 Open in browser:")
|
||||
print(f" file://{output_file}")
|
||||
|
||||
return output_file
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
38
export-to-browser.sh
Executable file
38
export-to-browser.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# export-to-browser.sh - Export MT5 report to browser-viewable HTML
|
||||
# Usage: ./export-to-browser.sh [path/to/report.html]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}=== MT5 Report to Browser Export ===${NC}"
|
||||
echo ""
|
||||
|
||||
# Run the Python exporter
|
||||
if [ -f "${SCRIPT_DIR}/export-report-html.py" ]; then
|
||||
OUTPUT=$(python3 "${SCRIPT_DIR}/export-report-html.py" "$@" 2>&1)
|
||||
echo "$OUTPUT"
|
||||
|
||||
# Extract the output file path
|
||||
OUTPUT_FILE=$(echo "$OUTPUT" | grep "Report exported to:" | sed 's/.*exported to: //')
|
||||
|
||||
if [ -n "$OUTPUT_FILE" ] && [ -f "$OUTPUT_FILE" ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Export complete!${NC}"
|
||||
echo ""
|
||||
echo "To view in browser:"
|
||||
echo " 1. Copy the HTML file to your local machine"
|
||||
echo " 2. Open it in any web browser"
|
||||
echo ""
|
||||
echo "Or on this server with a text browser:"
|
||||
echo " lynx $OUTPUT_FILE"
|
||||
echo " w3m $OUTPUT_FILE"
|
||||
fi
|
||||
else
|
||||
echo "❌ export-report-html.py not found"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user