')
# Data rows
data_rows = []
for year in years:
row_cells = [f'
{year}
']
year_data = monthly_returns.get(year, {})
for month_num in range(1, 13):
value = year_data.get(str(month_num), Decimal("0"))
val_float = float(value)
# Color based on value
if val_float > 5:
color = "#15803d" # Dark green
elif val_float > 2:
color = "#22c55e" # Green
elif val_float > 0:
color = "#86efac" # Light green
elif val_float > -2:
color = "#fca5a5" # Light red
elif val_float > -5:
color = "#ef4444" # Red
else:
color = "#b91c1c" # Dark red
cell_text = f"{val_float:+.1f}%" if value != ZERO else "-"
row_cells.append(
f'
"""
def _generate_svg_chart(self, chart: ChartData) -> str:
"""Generate SVG chart from chart data."""
width = self.config.chart_width
height = self.config.chart_height
padding = 60
# Calculate data bounds
y_values = [float(y) for y in chart.y_data if y is not None]
if not y_values:
return '
No data available
'
y_min = min(y_values)
y_max = max(y_values)
y_range = y_max - y_min if y_max != y_min else 1
# Scale data to SVG coordinates
plot_width = width - 2 * padding
plot_height = height - 2 * padding
points = []
for i, y in enumerate(y_values):
x = padding + (i / max(len(y_values) - 1, 1)) * plot_width
y_scaled = height - padding - ((y - y_min) / y_range) * plot_height
points.append(f"{x:.1f},{y_scaled:.1f}")
polyline_points = " ".join(points)
color = chart.colors[0] if chart.colors else self.config.color_scheme["primary"]
# Generate SVG
svg = f"""
"""
return svg
# ========================================================================
# PDF Generation
# ========================================================================
def _html_to_pdf(self, html_content: str, output_path: Path) -> bool:
"""Convert HTML to PDF.
Returns True if successful, False if PDF libraries not available.
"""
try:
# Try weasyprint first
from weasyprint import HTML
HTML(string=html_content).write_pdf(output_path)
return True
except ImportError:
pass
try:
# Try pdfkit as fallback
import pdfkit
pdfkit.from_string(html_content, str(output_path))
return True
except ImportError:
pass
logger.warning("No PDF library available (weasyprint or pdfkit)")
return False
# ========================================================================
# JSON Generation
# ========================================================================
def _build_json_data(
self,
result: BacktestResult,
analysis: AnalysisResult,
) -> dict[str, Any]:
"""Build JSON data structure from backtest results."""
return {
"report": {
"title": self.config.title,
"generated_at": datetime.now().isoformat(),
"author": self.config.author,
},
"backtest": {
"start_date": result.start_date.isoformat() if result.start_date else None,
"end_date": result.end_date.isoformat() if result.end_date else None,
"initial_capital": str(result.initial_capital),
"final_capital": str(result.final_value),
},
"performance": {
"total_return": str(result.total_return),
"total_return_pct": str(result.total_return),
"win_rate": str(result.win_rate),
"profit_factor": str(result.profit_factor),
"max_drawdown": str(result.max_drawdown),
"max_drawdown_pct": str(result.max_drawdown),
"sharpe_ratio": str(result.sharpe_ratio),
"sortino_ratio": str(result.sortino_ratio),
},
"trades": {
"total": result.total_trades,
"winning": result.winning_trades,
"losing": result.losing_trades,
"total_commission": str(result.total_commission),
"total_slippage": str(result.total_slippage),
},
"trade_statistics": {
"avg_win": str(analysis.trade_statistics.avg_win),
"avg_loss": str(analysis.trade_statistics.avg_loss),
"largest_win": str(analysis.trade_statistics.max_win),
"largest_loss": str(analysis.trade_statistics.max_loss),
"expectancy": str(analysis.trade_statistics.expectancy),
"max_consecutive_wins": analysis.trade_statistics.max_consecutive_wins,
"max_consecutive_losses": analysis.trade_statistics.max_consecutive_losses,
},
"risk_metrics": {
"sharpe_ratio": str(analysis.risk_metrics.sharpe_ratio),
"sortino_ratio": str(analysis.risk_metrics.sortino_ratio),
"calmar_ratio": str(analysis.risk_metrics.calmar_ratio),
"max_drawdown": str(analysis.risk_metrics.max_drawdown),
"var_95": str(analysis.risk_metrics.var_95),
"cvar_95": str(analysis.risk_metrics.cvar_95),
"ulcer_index": str(analysis.risk_metrics.ulcer_index),
},
"equity_curve": [
{
"timestamp": s.timestamp.isoformat() if s.timestamp else None,
"equity": str(s.total_value),
}
for s in result.snapshots
],
"trade_list": [
{
"timestamp": t.timestamp.isoformat() if t.timestamp else None,
"symbol": t.symbol,
"side": t.side.value if t.side else None,
"quantity": str(t.quantity),
"price": str(t.price),
"pnl": str(t.pnl),
}
for t in result.trades[:self.config.max_trades_shown]
],
}
# ========================================================================
# Markdown Generation
# ========================================================================
def _build_markdown(
self,
result: BacktestResult,
analysis: AnalysisResult,
) -> str:
"""Build Markdown report from backtest results."""
dp = self.config.decimal_places
lines = []
# Title
lines.append(f"# {self.config.title}")
lines.append("")
lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if self.config.author:
lines.append(f"**Author:** {self.config.author}")
lines.append("")
# Summary
lines.append("## Summary")
lines.append("")
lines.append(f"| Metric | Value |")
lines.append(f"|--------|-------|")
lines.append(f"| Initial Capital | ${float(result.initial_capital):,.{dp}f} |")
lines.append(f"| Final Capital | ${float(result.final_value):,.{dp}f} |")
lines.append(f"| Total Return | {float(result.total_return):,.{dp}f}% |")
lines.append(f"| Total Trades | {result.total_trades} |")
lines.append(f"| Win Rate | {float(result.win_rate):,.{dp}f}% |")
lines.append(f"| Profit Factor | {float(result.profit_factor):,.{dp}f} |")
lines.append(f"| Max Drawdown | {float(result.max_drawdown):,.{dp}f}% |")
lines.append(f"| Sharpe Ratio | {float(result.sharpe_ratio):,.{dp}f} |")
lines.append("")
# Trade Statistics
lines.append("## Trade Statistics")
lines.append("")
stats = analysis.trade_statistics
lines.append(f"| Metric | Value |")
lines.append(f"|--------|-------|")
lines.append(f"| Winning Trades | {stats.winning_trades} |")
lines.append(f"| Losing Trades | {stats.losing_trades} |")
lines.append(f"| Average Win | ${float(stats.avg_win):,.{dp}f} |")
lines.append(f"| Average Loss | ${float(stats.avg_loss):,.{dp}f} |")
lines.append(f"| Largest Win | ${float(stats.max_win):,.{dp}f} |")
lines.append(f"| Largest Loss | ${float(stats.max_loss):,.{dp}f} |")
lines.append(f"| Max Consecutive Wins | {stats.max_consecutive_wins} |")
lines.append(f"| Max Consecutive Losses | {stats.max_consecutive_losses} |")
lines.append(f"| Expectancy | ${float(stats.expectancy):,.{dp}f} |")
lines.append("")
# Risk Metrics
lines.append("## Risk Metrics")
lines.append("")
risk = analysis.risk_metrics
lines.append(f"| Metric | Value |")
lines.append(f"|--------|-------|")
lines.append(f"| Sharpe Ratio | {float(risk.sharpe_ratio):,.{dp}f} |")
lines.append(f"| Sortino Ratio | {float(risk.sortino_ratio):,.{dp}f} |")
lines.append(f"| Calmar Ratio | {float(risk.calmar_ratio):,.{dp}f} |")
lines.append(f"| Max Drawdown | {float(risk.max_drawdown):,.{dp}f}% |")
lines.append(f"| VaR (95%) | ${float(risk.var_95):,.{dp}f} |")
lines.append(f"| CVaR (95%) | ${float(risk.cvar_95):,.{dp}f} |")
lines.append(f"| Ulcer Index | {float(risk.ulcer_index):,.{dp}f} |")
lines.append("")
# Recent Trades
if result.trades:
lines.append("## Recent Trades")
lines.append("")
lines.append("| # | Symbol | Side | Quantity | Price | P&L |")
lines.append("|---|--------|------|----------|-------|-----|")
for i, trade in enumerate(result.trades[:20]):
pnl = float(trade.pnl)
pnl_str = f"${pnl:,.{dp}f}" if pnl >= 0 else f"-${abs(pnl):,.{dp}f}"
lines.append(
f"| {i + 1} | {trade.symbol} | {trade.side.value if trade.side else ''} | "
f"{float(trade.quantity):,.2f} | ${float(trade.price):,.{dp}f} | {pnl_str} |"
)
lines.append("")
# Footer
lines.append("---")
lines.append(f"*{self.config.footer_text}*")
return "\n".join(lines)
# ============================================================================
# Factory Functions
# ============================================================================
def create_report_generator(
title: str = "Backtest Report",
author: str = "",
include_trade_list: bool = True,
max_trades: int = 100,
) -> ReportGenerator:
"""Create a configured report generator.
Args:
title: Report title
author: Author name
include_trade_list: Whether to include trade list
max_trades: Maximum trades to show
Returns:
Configured ReportGenerator
"""
config = ReportConfig(
title=title,
author=author,
include_trade_list=include_trade_list,
max_trades_shown=max_trades,
)
return ReportGenerator(config=config)