import logging import os import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from matplotlib.backends.backend_pdf import PdfPages from django.conf import settings logger = logging.getLogger(__name__) def generate_backtest_report(result, execution_id): """백테스트 결과를 PDF 리포트로 생성 Args: result: run_backtest()가 반환한 결과 딕셔너리 execution_id: StrategyExecution ID Returns: MEDIA_ROOT 기준 상대 경로 (예: 'reports/backtest_1.pdf') """ reports_dir = os.path.join(settings.MEDIA_ROOT, 'reports') os.makedirs(reports_dir, exist_ok=True) relative_path = f'reports/backtest_{execution_id}.pdf' file_path = os.path.join(settings.MEDIA_ROOT, relative_path) with PdfPages(file_path) as pdf: _draw_summary_page(pdf, result) _draw_equity_curve_page(pdf, result) logger.info(f'PDF report generated: {file_path}') return relative_path def _draw_summary_page(pdf, result): """페이지 1: 입력 파라미터 + 성과 지표""" fig, (ax_params, ax_metrics) = plt.subplots(2, 1, figsize=(8.27, 11.69)) fig.suptitle(f"Backtest Report: {result.get('strategy', 'N/A')}", fontsize=16, fontweight='bold', y=0.97) # 입력 파라미터 테이블 ax_params.set_title('Input Parameters', fontsize=13, fontweight='bold', loc='left', pad=12) ax_params.axis('off') variant = result.get('variant', '') param_data = [ ['Strategy', result.get('strategy', 'N/A')], ['Variant', variant if variant else 'N/A'], ['Period', f"{result.get('backtest_start_date', '')} ~ {result.get('backtest_end_date', '')}"], ['Initial Capital', f"${result.get('initial_capital', 0):,.0f}"], ['Final Capital', f"${result.get('final_capital', 0):,.0f}"], ] param_table = ax_params.table( cellText=param_data, colLabels=['Parameter', 'Value'], colWidths=[0.35, 0.55], loc='center', cellLoc='left', ) param_table.auto_set_font_size(False) param_table.set_fontsize(10) param_table.scale(1, 1.6) for (row, col), cell in param_table.get_celld().items(): if row == 0: cell.set_facecolor('#4472C4') cell.set_text_props(color='white', fontweight='bold') else: cell.set_facecolor('#F2F2F2' if row % 2 == 0 else 'white') # 성과 지표 테이블 ax_metrics.set_title('Performance Metrics', fontsize=13, fontweight='bold', loc='left', pad=12) ax_metrics.axis('off') metrics = result.get('metrics', {}) metrics_data = [ ['CAGR', f"{metrics.get('cagr', 0):.2f}%"], ['Total Return', f"{metrics.get('total_return', 0):.2f}%"], ['Annual Volatility', f"{metrics.get('annual_volatility', 0):.2f}%"], ['Max Drawdown', f"{metrics.get('max_drawdown', 0):.2f}%"], ['Sharpe Ratio', f"{metrics.get('sharpe_ratio', 0):.2f}"], ['Sortino Ratio', f"{metrics.get('sortino_ratio', 0):.2f}"], ['Calmar Ratio', f"{metrics.get('calmar_ratio', 0):.2f}"], ['Win Rate (Monthly)', f"{metrics.get('win_rate', 0):.2f}%"], ['Best Month', f"{metrics.get('best_month', 0):.2f}%"], ['Worst Month', f"{metrics.get('worst_month', 0):.2f}%"], ] metrics_table = ax_metrics.table( cellText=metrics_data, colLabels=['Metric', 'Value'], colWidths=[0.35, 0.55], loc='center', cellLoc='left', ) metrics_table.auto_set_font_size(False) metrics_table.set_fontsize(10) metrics_table.scale(1, 1.6) for (row, col), cell in metrics_table.get_celld().items(): if row == 0: cell.set_facecolor('#4472C4') cell.set_text_props(color='white', fontweight='bold') else: cell.set_facecolor('#F2F2F2' if row % 2 == 0 else 'white') fig.tight_layout(rect=[0, 0, 1, 0.95]) pdf.savefig(fig) plt.close(fig) def _draw_equity_curve_page(pdf, result): """페이지 2: 에쿼티 커브 차트""" equity_curve = result.get('daily_equity_curve', []) if not equity_curve: return dates = [entry['date'] for entry in equity_curve] values = [entry['value'] for entry in equity_curve] fig, ax = plt.subplots(figsize=(8.27, 11.69 / 2)) fig.suptitle('Equity Curve', fontsize=14, fontweight='bold') ax.plot(range(len(values)), values, color='#4472C4', linewidth=1.2) ax.fill_between(range(len(values)), values, alpha=0.1, color='#4472C4') ax.set_ylabel('Portfolio Value ($)') ax.set_xlabel('Date') ax.grid(True, alpha=0.3) ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}')) # x축 레이블: 적당한 간격으로 날짜 표시 num_labels = min(8, len(dates)) if num_labels > 0: step = max(1, len(dates) // num_labels) tick_positions = list(range(0, len(dates), step)) tick_labels = [dates[i] for i in tick_positions] ax.set_xticks(tick_positions) ax.set_xticklabels(tick_labels, rotation=45, ha='right', fontsize=8) # Drawdown subplot drawdown_series = result.get('drawdown_series', []) if drawdown_series: ax2 = fig.add_axes([0.125, 0.08, 0.775, 0.2]) dd_values = [d['drawdown'] for d in drawdown_series] ax2.fill_between(range(len(dd_values)), dd_values, color='#C0504D', alpha=0.4) ax2.plot(range(len(dd_values)), dd_values, color='#C0504D', linewidth=0.8) ax2.set_ylabel('Drawdown (%)') ax2.grid(True, alpha=0.3) ax2.set_xlim(ax.get_xlim()) ax2.set_xticklabels([]) # 메인 차트 여백 조정 ax.set_position([0.125, 0.35, 0.775, 0.55]) fig.tight_layout(rect=[0, 0.05, 1, 0.95]) pdf.savefig(fig) plt.close(fig)