backtrader.reports.reporter 源代码

#!/usr/bin/env python
"""
Main report generator.

Generates backtest reports in HTML, PDF, and JSON formats.
"""

import json
import os
from datetime import datetime

from .charts import ReportChart
from .performance import PerformanceCalculator

# Try to import Jinja2
try:
    from jinja2 import BaseLoader, Environment, FileSystemLoader

    JINJA2_AVAILABLE = True
except ImportError:
    JINJA2_AVAILABLE = False

# Try to import weasyprint (PDF generation)
try:
    from weasyprint import HTML as WeasyHTML

    WEASYPRINT_AVAILABLE = True
except ImportError:
    WEASYPRINT_AVAILABLE = False


# Default HTML template
DEFAULT_TEMPLATE = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Backtrader Report - {{ strategy_name }}</title>
<style>
    @page {
        size: A4;
        margin: 12mm 15mm;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { 
        font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
        font-size: 10pt;
        line-height: 1.4;
        color: #2c3e50;
        background: white;
    }
    
    /* Header - Compact */
    .header {
        background: #1a365d;
        color: white;
        padding: 15px 20px;
        margin-bottom: 12px;
        border-bottom: 3px solid #3182ce;
    }
    .header h1 { 
        font-size: 18pt; 
        font-weight: 600;
        margin-bottom: 3px;
    }
    .header .subtitle {
        font-size: 11pt;
        font-weight: 400;
        margin-bottom: 10px;
        opacity: 0.9;
    }
    .header-info { 
        font-size: 9pt;
        line-height: 1.6;
    }
    .header-info span {
        margin-right: 20px;
    }
    .header-info b { color: #90cdf4; }
    
    /* Sections - Minimal spacing */
    .section { 
        margin-bottom: 8px;
    }
    .section h2 { 
        padding: 6px 12px;
        margin-bottom: 8px;
        background: #edf2f7;
        border-left: 3px solid #3182ce;
        font-size: 12pt;
        font-weight: 600;
        color: #1a365d;
    }
    
    /* Notes */
    .notes {
        background: #fffbeb;
        border: 1px solid #f6e05e;
        padding: 8px 12px;
        margin: 0 12px 10px 12px;
        font-size: 9pt;
        color: #744210;
    }
    
    /* Charts - New Page */
    .section.charts-page {
        page-break-before: always;
    }
    .charts { 
        padding: 0 12px;
    }
    .charts img { 
        width: 100%;
        height: auto;
        margin-bottom: 12px;
        border: 1px solid #e2e8f0;
    }
    
    /* Params - New Page */
    .section.params-page {
        page-break-before: always;
    }
    
    /* Metrics Table - Compact */
    .metrics-container {
        padding: 0 12px;
    }
    .metrics-table { 
        width: 100%;
        border-collapse: collapse;
        margin-bottom: 10px;
        font-size: 9pt;
    }
    .metrics-table td {
        padding: 5px 8px;
        border-bottom: 1px solid #e2e8f0;
    }
    .metrics-table .group-header td {
        background: #3182ce;
        color: white;
        font-weight: 600;
        font-size: 9pt;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        padding: 6px 8px;
    }
    .metrics-table .label {
        color: #4a5568;
        width: 22%;
    }
    .metrics-table .value {
        font-weight: 600;
        color: #2d3748;
        text-align: right;
        width: 28%;
    }
    .metrics-table .value.positive { color: #276749; }
    .metrics-table .value.negative { color: #c53030; }
    
    /* Parameters - Compact */
    .params-table {
        width: 60%;
        border-collapse: collapse;
        font-size: 9pt;
        margin: 0 12px;
    }
    .params-table td {
        padding: 4px 8px;
        border: 1px solid #e2e8f0;
    }
    .params-table .param-name {
        background: #f7fafc;
        font-weight: 500;
        width: 40%;
    }
    
    /* Footer - Minimal */
    .footer { 
        text-align: center; 
        color: #718096; 
        font-size: 8pt;
        padding: 10px;
        border-top: 1px solid #e2e8f0;
        margin-top: 15px;
    }
    .footer p { margin: 2px 0; }
</style>
</head>
<body>

<div class="header">
    <h1>{{ strategy_name }}</h1>
    <div class="subtitle">Backtest Performance Report</div>
    <div class="header-info">
        <span><b>Data:</b> {{ data_name }}</span>
        <span><b>Period:</b> {{ start_date }} ~ {{ end_date }}</span>
        <span><b>Bars:</b> {{ bars }}</span>
        {% if user %}<span><b>Analyst:</b> {{ user }}</span>{% endif %}
        <span><b>Generated:</b> {{ report_date }}</span>
    </div>
</div>

{% if memo %}
<div class="section">
    <h2>Notes</h2>
    <div class="notes">{{ memo }}</div>
</div>
{% endif %}

<div class="section">
    <h2>Performance Summary</h2>
    <div class="metrics-container">
        <table class="metrics-table">
            <tr class="group-header"><td colspan="4">Profit & Loss</td></tr>
            <tr>
                <td class="label">Start Capital</td>
                <td class="value">{{ "${:,.2f}".format(start_cash) if start_cash else 'N/A' }}</td>
                <td class="label">End Value</td>
                <td class="value">{{ "${:,.2f}".format(end_value) if end_value else 'N/A' }}</td>
            </tr>
            <tr>
                <td class="label">Net Profit</td>
                <td class="value {{ 'positive' if rpl and rpl > 0 else 'negative' if rpl and rpl < 0 else '' }}">{{ "${:,.2f}".format(rpl) if rpl else 'N/A' }}</td>
                <td class="label">Total Return</td>
                <td class="value {{ 'positive' if total_return and total_return > 0 else 'negative' if total_return and total_return < 0 else '' }}">{{ "{:.2f}%".format(total_return) if total_return else 'N/A' }}</td>
            </tr>
            <tr>
                <td class="label">Annual Return</td>
                <td class="value {{ 'positive' if annual_return and annual_return > 0 else 'negative' if annual_return and annual_return < 0 else '' }}">{{ "{:.2f}%".format(annual_return) if annual_return else 'N/A' }}</td>
                <td class="label">Profit Factor</td>
                <td class="value">{{ "{:.2f}".format(profit_factor) if profit_factor else 'N/A' }}</td>
            </tr>
            
            <tr class="group-header"><td colspan="4">Risk Metrics</td></tr>
            <tr>
                <td class="label">Max Drawdown ($)</td>
                <td class="value negative">{{ "${:,.2f}".format(max_money_drawdown) if max_money_drawdown else 'N/A' }}</td>
                <td class="label">Max Drawdown (%)</td>
                <td class="value negative">{{ "{:.2f}%".format(max_pct_drawdown) if max_pct_drawdown else 'N/A' }}</td>
            </tr>
            <tr>
                <td class="label">Sharpe Ratio</td>
                <td class="value">{{ "{:.2f}".format(sharpe_ratio) if sharpe_ratio else 'N/A' }}</td>
                <td class="label">Calmar Ratio</td>
                <td class="value">{{ "{:.2f}".format(calmar_ratio) if calmar_ratio else 'N/A' }}</td>
            </tr>
            <tr>
                <td class="label">SQN Score</td>
                <td class="value">{{ "{:.2f}".format(sqn_score) if sqn_score else 'N/A' }}</td>
                <td class="label">SQN Rating</td>
                <td class="value">{{ sqn_human if sqn_human else 'N/A' }}</td>
            </tr>
            
            <tr class="group-header"><td colspan="4">Trade Statistics</td></tr>
            <tr>
                <td class="label">Total Trades</td>
                <td class="value">{{ total_number_trades }}</td>
                <td class="label">Closed Trades</td>
                <td class="value">{{ trades_closed }}</td>
            </tr>
            <tr>
                <td class="label">Win Rate</td>
                <td class="value">{{ "{:.2f}%".format(pct_winning) if pct_winning else 'N/A' }}</td>
                <td class="label">Avg Win</td>
                <td class="value positive">{{ "${:,.2f}".format(avg_money_winning) if avg_money_winning else 'N/A' }}</td>
            </tr>
            <tr>
                <td class="label">Avg Loss</td>
                <td class="value negative">{{ "${:,.2f}".format(avg_money_losing) if avg_money_losing else 'N/A' }}</td>
                <td class="label">Best Trade</td>
                <td class="value positive">{{ "${:,.2f}".format(best_winning_trade) if best_winning_trade else 'N/A' }}</td>
            </tr>
            <tr>
                <td class="label">Worst Trade</td>
                <td class="value negative">{{ "${:,.2f}".format(worst_losing_trade) if worst_losing_trade else 'N/A' }}</td>
                <td class="label"></td>
                <td class="value"></td>
            </tr>
        </table>
    </div>
</div>

<div class="section charts-page">
    <h2>Performance Charts</h2>
    <div class="charts">
        {% if equity_curve_img %}<img src="{{ equity_curve_img }}" alt="Equity Curve">{% endif %}
        {% if return_bars_img %}<img src="{{ return_bars_img }}" alt="Return Bars">{% endif %}
        {% if drawdown_img %}<img src="{{ drawdown_img }}" alt="Drawdown">{% endif %}
    </div>
</div>

{% if params %}
<div class="section params-page">
    <h2>Strategy Parameters</h2>
    <div class="metrics-container">
        <table class="params-table">
            {% for key, value in params.items() %}
            <tr>
                <td class="param-name">{{ key }}</td>
                <td>{{ value }}</td>
            </tr>
            {% endfor %}
        </table>
    </div>
</div>
{% endif %}

<div class="footer">
    <p>Generated by <strong>Backtrader Reports Module</strong></p>
    <p>{{ report_date }}</p>
</div>

</body>
</html>
"""


[文档] class ReportGenerator: """Main report generator. Generates backtest reports in HTML, PDF, and JSON formats. Attributes: strategy: Strategy instance calculator: Performance calculator charts: Chart generator Usage example: report = ReportGenerator(strategy) report.generate_html('report.html') report.generate_pdf('report.pdf') report.generate_json('report.json') """
[文档] def __init__(self, strategy, template="default"): """Initialize the report generator. Args: strategy: backtrader strategy instance template: Template name or template string """ self.strategy = strategy self.calculator = PerformanceCalculator(strategy) self.charts = ReportChart() self.template = template # User information self._user = None self._memo = None
[文档] def generate_html(self, output_path, user=None, memo=None, **kwargs): """Generate HTML report. Args: output_path: Output file path user: Username memo: Notes **kwargs: Additional template variables Returns: str: Output file path """ if not JINJA2_AVAILABLE: raise ImportError( "jinja2 is required for HTML report generation. " "Install it with: pip install jinja2" ) self._user = user self._memo = memo # Collect all data context = self._build_context(**kwargs) # Render template html_content = self._render_template(context) # Write to file with open(output_path, "w", encoding="utf-8") as f: f.write(html_content) # Clean up charts self.charts.close_all() return output_path
[文档] def generate_pdf(self, output_path, user=None, memo=None, **kwargs): """Generate PDF report. Args: output_path: Output file path user: Username memo: Notes **kwargs: Additional template variables Returns: str: Output file path """ if not WEASYPRINT_AVAILABLE: raise ImportError( "weasyprint is required for PDF report generation. " "Install it with: pip install weasyprint" ) self._user = user self._memo = memo # Collect all data context = self._build_context(**kwargs) # Render template html_content = self._render_template(context) # Convert to PDF WeasyHTML(string=html_content).write_pdf(output_path) # Clean up charts self.charts.close_all() return output_path
[文档] def generate_json(self, output_path, indent=2, **kwargs): """Generate JSON report. Args: output_path: Output file path indent: JSON indentation **kwargs: Additional data Returns: str: Output file path """ # Get all metrics metrics = self.calculator.get_all_metrics() strategy_info = self.calculator.get_strategy_info() data_info = self.calculator.get_data_info() # Build JSON structure report_data = { "generated_at": datetime.now().isoformat(), "strategy": strategy_info, "data": { "name": data_info.get("data_name"), "start_date": ( str(data_info.get("start_date")) if data_info.get("start_date") else None ), "end_date": str(data_info.get("end_date")) if data_info.get("end_date") else None, "bars": data_info.get("bars"), }, "metrics": { "pnl": { "start_cash": metrics.get("start_cash"), "end_value": metrics.get("end_value"), "net_profit": metrics.get("rpl"), "total_return": metrics.get("total_return"), "annual_return": metrics.get("annual_return"), "profit_factor": metrics.get("profit_factor"), }, "risk": { "max_drawdown_money": metrics.get("max_money_drawdown"), "max_drawdown_pct": metrics.get("max_pct_drawdown"), "sharpe_ratio": metrics.get("sharpe_ratio"), "calmar_ratio": metrics.get("calmar_ratio"), "sqn_score": metrics.get("sqn_score"), "sqn_rating": metrics.get("sqn_human"), }, "trades": { "total": metrics.get("total_number_trades"), "closed": metrics.get("trades_closed"), "won": metrics.get("trades_won"), "lost": metrics.get("trades_lost"), "win_rate": metrics.get("pct_winning"), "avg_win": metrics.get("avg_money_winning"), "avg_loss": metrics.get("avg_money_losing"), "best_trade": metrics.get("best_winning_trade"), "worst_trade": metrics.get("worst_losing_trade"), }, }, **kwargs, } # Handle non-serializable values report_data = self._make_json_serializable(report_data) # Write to file with open(output_path, "w", encoding="utf-8") as f: json.dump(report_data, f, indent=indent, ensure_ascii=False) return output_path
def _build_context(self, **kwargs): """Build template context. Returns: dict: Template variables dictionary """ # Get metrics metrics = self.calculator.get_all_metrics() strategy_info = self.calculator.get_strategy_info() data_info = self.calculator.get_data_info() # Generate charts dates, values = self.calculator.get_equity_curve() benchmark_dates, benchmark_values = self.calculator.get_buynhold_curve() equity_curve_img = "" return_bars_img = "" drawdown_img = "" if dates and values: # Equity curve fig_equity = self.charts.plot_equity_curve( dates, values, benchmark_dates, benchmark_values ) if fig_equity: equity_curve_img = self.charts.to_base64(fig_equity) # Return bars chart fig_returns = self.charts.plot_return_bars(dates, values) if fig_returns: return_bars_img = self.charts.to_base64(fig_returns) # Drawdown chart fig_drawdown = self.charts.plot_drawdown(dates, values) if fig_drawdown: drawdown_img = self.charts.to_base64(fig_drawdown) # Build context context = { # Strategy information "strategy_name": strategy_info.get("strategy_name", "Strategy"), "params": strategy_info.get("params", {}), # Data information "data_name": data_info.get("data_name", "Data"), "start_date": ( str(data_info.get("start_date", ""))[:10] if data_info.get("start_date") else "N/A" ), "end_date": ( str(data_info.get("end_date", ""))[:10] if data_info.get("end_date") else "N/A" ), "bars": data_info.get("bars", 0), # User information "user": self._user, "memo": self._memo, "report_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # Charts "equity_curve_img": equity_curve_img, "return_bars_img": return_bars_img, "drawdown_img": drawdown_img, # Metrics **metrics, **kwargs, } return context def _render_template(self, context): """Render template. Args: context: Template variables dictionary Returns: str: Rendered HTML """ if self.template == "default": # Use default template env = Environment(loader=BaseLoader()) template = env.from_string(DEFAULT_TEMPLATE) else: # Try to load as file path if os.path.isfile(self.template): template_dir = os.path.dirname(self.template) template_name = os.path.basename(self.template) env = Environment(loader=FileSystemLoader(template_dir)) template = env.get_template(template_name) else: # Handle as template string env = Environment(loader=BaseLoader()) template = env.from_string(self.template) return template.render(**context) def _make_json_serializable(self, obj): """Make object JSON serializable. Args: obj: Object to process Returns: Serializable object """ import math if isinstance(obj, dict): return {k: self._make_json_serializable(v) for k, v in obj.items()} elif isinstance(obj, (list, tuple)): return [self._make_json_serializable(v) for v in obj] elif isinstance(obj, float): if math.isnan(obj) or math.isinf(obj): return None return obj elif hasattr(obj, "isoformat") and callable(getattr(obj, "isoformat", None)): return obj.isoformat() elif hasattr(obj, "__dict__"): return str(obj) else: return obj
[文档] def get_metrics(self): """Get all performance metrics. Returns: dict: Performance metrics dictionary """ return self.calculator.get_all_metrics()
[文档] def print_summary(self): """Print performance summary to console.""" metrics = self.calculator.get_all_metrics() strategy_info = self.calculator.get_strategy_info() print("\n" + "=" * 60) print(f"Strategy: {strategy_info.get('strategy_name', 'Strategy')}") print("=" * 60) print("\n*** PnL ***") print( f"Start Capital : {metrics.get('start_cash', 'N/A'):,.2f}" if metrics.get("start_cash") else "Start Capital : N/A" ) print( f"Net Profit : {metrics.get('rpl', 'N/A'):,.2f}" if metrics.get("rpl") else "Net Profit : N/A" ) print( f"Total Return : {metrics.get('total_return', 'N/A'):.2f}%" if metrics.get("total_return") else "Total Return : N/A" ) print( f"Annual Return : {metrics.get('annual_return', 'N/A'):.2f}%" if metrics.get("annual_return") else "Annual Return : N/A" ) print("\n*** Risk ***") print( f"Max Drawdown ($) : {metrics.get('max_money_drawdown', 'N/A'):,.2f}" if metrics.get("max_money_drawdown") else "Max Drawdown ($) : N/A" ) print( f"Max Drawdown (%) : {metrics.get('max_pct_drawdown', 'N/A'):.2f}%" if metrics.get("max_pct_drawdown") else "Max Drawdown (%) : N/A" ) print( f"Sharpe Ratio : {metrics.get('sharpe_ratio', 'N/A'):.2f}" if metrics.get("sharpe_ratio") else "Sharpe Ratio : N/A" ) print("\n*** Trades ***") print(f"Total Trades : {metrics.get('total_number_trades', 0)}") print( f"Win Rate : {metrics.get('pct_winning', 'N/A'):.2f}%" if metrics.get("pct_winning") else "Win Rate : N/A" ) print( f"SQN Score : {metrics.get('sqn_score', 'N/A'):.2f}" if metrics.get("sqn_score") else "SQN Score : N/A" ) print(f"SQN Rating : {metrics.get('sqn_human', 'N/A')}") print("\n" + "=" * 60)