diff --git a/predix_strategy_report.py b/predix_strategy_report.py index 803b3ad0..7ec395f8 100644 --- a/predix_strategy_report.py +++ b/predix_strategy_report.py @@ -2,60 +2,58 @@ """ Strategy Performance Report Generator for Predix. -Generates detailed PDF reports with charts for each accepted strategy, -inspired by TPT's performance_report.py but adapted for Predix's -factor-based strategy evaluation with real OHLCV backtests. +Generates detailed PDF reports with charts for each accepted strategy. Features: -- Equity curve -- Drawdown analysis -- Monthly returns heatmap -- Signal distribution -- Trade statistics -- Factor importance +- PDF report with all charts embedded +- Equity curve, drawdown, signal distribution, monthly returns +- Factor correlation matrix +- Full metrics table and strategy code Usage: - python predix_strategy_report.py - python predix_strategy_report.py results/strategies_new/1234567890_MyStrategy.json + python predix_strategy_report.py # All strategies + python predix_strategy_report.py results/strategies_new/123.json # Single strategy """ -import os -import sys -import json -import warnings +import os, sys, json, warnings from pathlib import Path from datetime import datetime import numpy as np import pandas as pd import matplotlib -matplotlib.use('Agg') # Non-interactive backend +matplotlib.use('Agg') import matplotlib.pyplot as plt -import matplotlib.dates as mdates from matplotlib.gridspec import GridSpec import seaborn as sns -# Suppress warnings +from reportlab.lib.pagesizes import A4 +from reportlab.platypus import ( + SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, + Image, PageBreak, HRFlowable +) +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT + warnings.filterwarnings('ignore') -# ============================================================================ -# Configuration -# ============================================================================ +# Config OHLCV_PATH = Path('/home/nico/Predix/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5') REPORTS_DIR = Path('/home/nico/Predix/results/strategy_reports') REPORTS_DIR.mkdir(parents=True, exist_ok=True) -# Dark mode styling +# Colors BG_COLOR = '#1E1E1E' TEXT_COLOR = '#E0E0E0' ACCENT_GREEN = '#4CAF50' ACCENT_RED = '#F44336' ACCENT_BLUE = '#2196F3' -ACCENT_YELLOW = '#FFC107' GRID_COLOR = '#333333' class StrategyPerformanceReporter: - """Generate comprehensive performance report for a single strategy.""" + """Generate comprehensive PDF + PNG report for a strategy.""" def __init__(self, strategy_data: dict, report_dir: Path = None): self.strategy = strategy_data @@ -63,373 +61,284 @@ class StrategyPerformanceReporter: self.report_dir = report_dir or REPORTS_DIR self.plots_dir = self.report_dir / 'plots' self.plots_dir.mkdir(parents=True, exist_ok=True) - - # Metrics from backtest self.bt = strategy_data.get('real_backtest', {}) self.summary = strategy_data.get('summary', {}) self.factors = strategy_data.get('factor_names', []) self.code = strategy_data.get('code', '') self.description = strategy_data.get('description', '') - - # Apply dark mode plt.style.use('dark_background') - def generate_report(self) -> Path: - """Generate full report with all charts.""" - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - report_name = f"{timestamp}_{self.name}" + def generate_report(self) -> dict: + """Generate full report: PNG dashboard + individual charts + PDF + text.""" + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + name = f"{ts}_{self.name}" - # Generate all plots + # PNG charts fig = self._create_dashboard() - report_path = self.plots_dir / f"{report_name}_dashboard.png" - fig.savefig(str(report_path), dpi=150, bbox_inches='tight', facecolor=BG_COLOR) + dash_path = self.plots_dir / f"{name}_dashboard.png" + fig.savefig(str(dash_path), dpi=150, bbox_inches='tight', facecolor=BG_COLOR) plt.close(fig) - # Generate individual charts - self._generate_equity_curve() - self._generate_drawdown() - self._generate_signal_distribution() - self._generate_monthly_returns() - self._generate_factor_correlations() + self._gen_png(self._plot_equity_curve, f"{self.name}_equity.png", (12, 6)) + self._gen_png(self._plot_drawdown, f"{self.name}_drawdown.png", (12, 6)) + self._gen_png(self._plot_signal_dist, f"{self.name}_signals.png", (8, 8)) + self._gen_png(self._plot_monthly_returns, f"{self.name}_monthly_returns.png", (12, 6)) + self._gen_png(self._plot_factor_corr, f"{self.name}_factor_corr.png", (10, 8)) - # Generate text report - txt_path = self.report_dir / f"{report_name}_report.txt" - self._generate_text_report(txt_path) + # Text report + txt_path = self.report_dir / f"{name}_report.txt" + self._gen_text_report(txt_path) - return report_path + # PDF report + pdf_path = self.report_dir / f"{name}_report.pdf" + self._gen_pdf_report(pdf_path) + return {'dashboard': dash_path, 'pdf': pdf_path, 'text': txt_path} + + def _gen_png(self, plot_fn, filename, figsize): + fig, ax = plt.subplots(figsize=figsize, facecolor=BG_COLOR) + plot_fn(ax) + p = self.plots_dir / filename + fig.savefig(str(p), dpi=150, bbox_inches='tight', facecolor=BG_COLOR) + plt.close(fig) + + # ========== Chart methods ========== def _create_dashboard(self): - """Create comprehensive dashboard with all charts.""" fig = plt.figure(figsize=(20, 24), facecolor=BG_COLOR) gs = GridSpec(4, 2, figure=fig, hspace=0.35, wspace=0.3) + fig.suptitle(f"Strategy Report: {self.name}", fontsize=20, fontweight='bold', color=TEXT_COLOR, y=0.98) - # Title - fig.suptitle( - f"Strategy Report: {self.name}", - fontsize=20, fontweight='bold', color=TEXT_COLOR, y=0.98 - ) + self._plot_equity_curve(fig.add_subplot(gs[0, 0])) + self._plot_drawdown(fig.add_subplot(gs[0, 1])) + self._plot_signal_dist(fig.add_subplot(gs[1, 0])) + self._plot_monthly_returns(fig.add_subplot(gs[1, 1])) - # 1. Equity Curve (top-left) - ax1 = fig.add_subplot(gs[0, 0]) - self._plot_equity_curve(ax1) - - # 2. Drawdown (top-right) - ax2 = fig.add_subplot(gs[0, 1]) - self._plot_drawdown(ax2) - - # 3. Signal Distribution (mid-left) - ax3 = fig.add_subplot(gs[1, 0]) - self._plot_signal_dist(ax3) - - # 4. Monthly Returns Heatmap (mid-right) - ax4 = fig.add_subplot(gs[1, 1]) - self._plot_monthly_returns(ax4) - - # 5. Key Metrics (bottom-left) - ax5 = fig.add_subplot(gs[2, 0]) - ax5.axis('off') + ax5 = fig.add_subplot(gs[2, 0]); ax5.axis('off') self._plot_metrics_table(ax5) - - # 6. Strategy Code (bottom-right) - ax6 = fig.add_subplot(gs[2, 1]) - ax6.axis('off') + ax6 = fig.add_subplot(gs[2, 1]); ax6.axis('off') self._plot_strategy_code(ax6) - - # 7. Factor List - ax7 = fig.add_subplot(gs[3, :]) - ax7.axis('off') + ax7 = fig.add_subplot(gs[3, :]); ax7.axis('off') self._plot_factors_list(ax7) - return fig def _plot_equity_curve(self, ax): - """Plot equity curve.""" - n_months = self.summary.get('n_months', 12) - monthly_ret = self.summary.get('monthly_return_pct', 0) / 100 - - # Generate synthetic equity curve from monthly returns - months = pd.date_range(start='2024-01-01', periods=int(max(n_months, 12)), freq='ME') - equity = (1 + monthly_ret) ** np.arange(len(months)) - - ax.fill_between(months, equity, alpha=0.3, color=ACCENT_GREEN) - ax.plot(months, equity, linewidth=2, color=ACCENT_GREEN) + n = max(int(self.summary.get('n_months', 12)), 12) + m = self.summary.get('monthly_return_pct', 0) / 100 + months = pd.date_range(start='2024-01-01', periods=n, freq='ME') + eq = (1 + m) ** np.arange(n) + ax.fill_between(months, eq, alpha=0.3, color=ACCENT_GREEN) + ax.plot(months, eq, linewidth=2, color=ACCENT_GREEN) ax.set_title('Equity Curve (Projected)', fontsize=12, color=TEXT_COLOR) ax.set_ylabel('Equity Multiplier', color=TEXT_COLOR) - ax.grid(True, alpha=0.3, color=GRID_COLOR) - ax.tick_params(colors=TEXT_COLOR) + ax.grid(True, alpha=0.3, color=GRID_COLOR); ax.tick_params(colors=TEXT_COLOR) def _plot_drawdown(self, ax): - """Plot drawdown visualization.""" - max_dd = self.summary.get('max_drawdown', 0) - # Handle negative or invalid values - max_dd_abs = abs(max_dd) if max_dd != 0 else 0.01 - n_months = max(self.summary.get('n_months', 12), 12) - - # Simulated drawdown pattern - months = pd.date_range(start='2024-01-01', periods=int(n_months), freq='ME') - dd = np.linspace(0, -max_dd_abs, len(months)//2) - dd_recovery = np.linspace(-max_dd_abs, 0, len(months) - len(months)//2) - dd_full = np.concatenate([dd, dd_recovery[:len(months)-len(dd)]]) - - ax.fill_between(months[:len(dd_full)], dd_full, alpha=0.5, color=ACCENT_RED) - ax.plot(months[:len(dd_full)], dd_full, linewidth=1.5, color=ACCENT_RED) - ax.set_title(f'Max Drawdown: {max_dd:.2%}', fontsize=12, color=TEXT_COLOR) + mdd = abs(self.summary.get('max_drawdown', 0)) or 0.01 + n = max(int(self.summary.get('n_months', 12)), 12) + months = pd.date_range(start='2024-01-01', periods=n, freq='ME') + dd = np.concatenate([np.linspace(0, -mdd, n//2), np.linspace(-mdd, 0, n-n//2)]) + ax.fill_between(months, dd, alpha=0.5, color=ACCENT_RED) + ax.plot(months, dd, linewidth=1.5, color=ACCENT_RED) + ax.set_title(f'Max Drawdown: {mdd:.2%}', fontsize=12, color=TEXT_COLOR) ax.set_ylabel('Drawdown', color=TEXT_COLOR) - ax.grid(True, alpha=0.3, color=GRID_COLOR) - ax.tick_params(colors=TEXT_COLOR) + ax.grid(True, alpha=0.3, color=GRID_COLOR); ax.tick_params(colors=TEXT_COLOR) ax.axhline(y=0, color=TEXT_COLOR, alpha=0.5, linewidth=0.5) def _plot_signal_dist(self, ax): - """Plot signal distribution pie chart.""" - long = self.bt.get('signal_long', 0) - short = self.bt.get('signal_short', 0) - neutral = self.bt.get('signal_neutral', 0) - total = long + short + neutral - - if total > 0: - labels = [f'LONG ({long:,})', f'SHORT ({short:,})', f'NEUTRAL ({neutral:,})'] - sizes = [long, short, neutral] - colors_plot = [ACCENT_GREEN, ACCENT_RED, '#666666'] - explode = (0.05, 0.05, 0) - - wedges, texts, autotexts = ax.pie( - sizes, explode=explode, labels=labels, colors=colors_plot, - autopct='%1.1f%%', startangle=90, - textprops={'color': TEXT_COLOR} - ) - for t in autotexts: - t.set_color(TEXT_COLOR) - t.set_fontsize(10) - + l, s, n = self.bt.get('signal_long', 0), self.bt.get('signal_short', 0), self.bt.get('signal_neutral', 0) + t = l + s + n + if t > 0: + ax.pie([l, s, n], labels=[f'LONG ({l:,})', f'SHORT ({s:,})', f'NEUTRAL ({n:,})'], + colors=[ACCENT_GREEN, ACCENT_RED, '#666'], autopct='%1.1f%%', startangle=90, + textprops={'color': TEXT_COLOR}) ax.set_title('Signal Distribution', fontsize=12, color=TEXT_COLOR) def _plot_monthly_returns(self, ax): - """Plot monthly returns bar chart.""" - monthly_ret = self.summary.get('monthly_return_pct', 0) - n_months = max(int(self.summary.get('n_months', 12)), 12) - - months = [f'M{i+1}' for i in range(n_months)] - # Add some realistic variation - use absolute value for scale + m = self.summary.get('monthly_return_pct', 0) + n = max(int(self.summary.get('n_months', 12)), 12) np.random.seed(42) - scale = abs(monthly_ret) * 0.3 if monthly_ret != 0 else 1.0 - variation = np.random.normal(0, scale, n_months) - returns = monthly_ret + variation - - colors_plot = [ACCENT_GREEN if r > 0 else ACCENT_RED for r in returns] - ax.bar(months, returns, color=colors_plot, alpha=0.8) + scale = abs(m) * 0.3 if m != 0 else 1.0 + rets = m + np.random.normal(0, scale, n) + cols = [ACCENT_GREEN if r > 0 else ACCENT_RED for r in rets] + ax.bar([f'M{i+1}' for i in range(n)], rets, color=cols, alpha=0.8) ax.axhline(y=0, color=TEXT_COLOR, alpha=0.5, linewidth=0.5) - ax.set_title(f'Monthly Returns (Avg: {monthly_ret:.2f}%)', fontsize=12, color=TEXT_COLOR) - ax.set_ylabel('Return %', color=TEXT_COLOR) - ax.tick_params(colors=TEXT_COLOR) + ax.set_title(f'Monthly Returns (Avg: {m:.2f}%)', fontsize=12, color=TEXT_COLOR) + ax.set_ylabel('Return %', color=TEXT_COLOR); ax.tick_params(colors=TEXT_COLOR) ax.grid(True, alpha=0.2, axis='y', color=GRID_COLOR) def _plot_metrics_table(self, ax): - """Plot key metrics as formatted table.""" - metrics = [ - ('IC', f"{self.bt.get('ic', 0):.4f}"), - ('Sharpe Ratio', f"{self.bt.get('sharpe', 0):.3f}"), - ('Max Drawdown', f"{self.bt.get('max_drawdown', 0):.2%}"), - ('Win Rate', f"{self.bt.get('win_rate', 0):.2%}"), - ('Monthly Return', f"{self.bt.get('monthly_return_pct', 0):.2f}%"), - ('Annual Return', f"{self.bt.get('annual_return_pct', 0):.2f}%"), - ('Total Return', f"{self.bt.get('total_return', 0):.2%}"), - ('Trades', f"{self.bt.get('n_trades', 0):,}"), - ('Bars', f"{self.bt.get('n_bars', 0):,}"), - ] - - y_pos = 0.9 - for label, value in metrics: - color = ACCENT_GREEN if any(x in value and not value.startswith('-') for x in ['%', '.']) else TEXT_COLOR - if value.startswith('-'): - color = ACCENT_RED - - ax.text(0.1, y_pos, label, fontsize=11, fontweight='bold', - color=TEXT_COLOR, transform=ax.transAxes) - ax.text(0.9, y_pos, value, fontsize=11, fontweight='bold', - color=color, transform=ax.transAxes, ha='right') - y_pos -= 0.1 - + metrics = [('IC', f"{self.bt.get('ic', 0):.4f}"), ('Sharpe', f"{self.bt.get('sharpe', 0):.3f}"), + ('Max DD', f"{self.bt.get('max_drawdown', 0):.2%}"), ('Win Rate', f"{self.bt.get('win_rate', 0):.2%}"), + ('Monthly', f"{self.bt.get('monthly_return_pct', 0):.2f}%"), ('Trades', f"{self.bt.get('n_trades', 0):,}")] + y = 0.9 + for lab, val in metrics: + c = ACCENT_GREEN if not val.startswith('-') else ACCENT_RED + ax.text(0.1, y, lab, fontsize=11, fontweight='bold', color=TEXT_COLOR, transform=ax.transAxes) + ax.text(0.9, y, val, fontsize=11, fontweight='bold', color=c, transform=ax.transAxes, ha='right') + y -= 0.15 ax.set_title('Key Metrics', fontsize=14, fontweight='bold', color=TEXT_COLOR) def _plot_strategy_code(self, ax): - """Display strategy code snippet.""" - code = self.code or 'No code available' - # Truncate if too long - if len(code) > 800: - code = code[:800] + '\n\n... (truncated)' - - ax.text(0.05, 0.95, 'Strategy Code:', fontsize=12, fontweight='bold', - color=TEXT_COLOR, transform=ax.transAxes) - ax.text(0.05, 0.88, code, fontsize=8, family='monospace', - color='#A5D6A7', transform=ax.transAxes, va='top', + code = (self.code or 'No code')[:800] + ('\n...(truncated)' if len(self.code or '') > 800 else '') + ax.text(0.05, 0.95, 'Strategy Code:', fontsize=12, fontweight='bold', color=TEXT_COLOR, transform=ax.transAxes) + ax.text(0.05, 0.88, code, fontsize=8, family='monospace', color='#A5D6A7', transform=ax.transAxes, va='top', bbox=dict(boxstyle='round,pad=0.5', facecolor='#2C2C2C', alpha=0.8)) def _plot_factors_list(self, ax): - """Display list of factors used.""" - title = f"Factors Used ({len(self.factors)}):" - ax.text(0.05, 0.9, title, fontsize=14, fontweight='bold', - color=TEXT_COLOR, transform=ax.transAxes) + ax.text(0.05, 0.9, f"Factors Used ({len(self.factors)}):", fontsize=14, fontweight='bold', color=TEXT_COLOR, transform=ax.transAxes) + for i, f in enumerate(self.factors[:15]): + ax.text(0.05, 0.75 - i*0.12, f"• {f}", fontsize=10, color=ACCENT_BLUE, transform=ax.transAxes) - for i, factor in enumerate(self.factors): - y = 0.75 - (i * 0.12) - if y < 0.1: - break - ax.text(0.05, y, f"• {factor}", fontsize=10, - color=ACCENT_BLUE, transform=ax.transAxes) - - def _generate_equity_curve(self): - """Generate standalone equity curve chart.""" - fig, ax = plt.subplots(figsize=(12, 6), facecolor=BG_COLOR) - self._plot_equity_curve(ax) - path = self.plots_dir / f"{self.name}_equity.png" - fig.savefig(str(path), dpi=150, bbox_inches='tight', facecolor=BG_COLOR) - plt.close(fig) - - def _generate_drawdown(self): - """Generate standalone drawdown chart.""" - fig, ax = plt.subplots(figsize=(12, 6), facecolor=BG_COLOR) - self._plot_drawdown(ax) - path = self.plots_dir / f"{self.name}_drawdown.png" - fig.savefig(str(path), dpi=150, bbox_inches='tight', facecolor=BG_COLOR) - plt.close(fig) - - def _generate_signal_distribution(self): - """Generate standalone signal distribution chart.""" - fig, ax = plt.subplots(figsize=(8, 8), facecolor=BG_COLOR) - self._plot_signal_dist(ax) - path = self.plots_dir / f"{self.name}_signals.png" - fig.savefig(str(path), dpi=150, bbox_inches='tight', facecolor=BG_COLOR) - plt.close(fig) - - def _generate_monthly_returns(self): - """Generate standalone monthly returns chart.""" - fig, ax = plt.subplots(figsize=(12, 6), facecolor=BG_COLOR) - self._plot_monthly_returns(ax) - path = self.plots_dir / f"{self.name}_monthly_returns.png" - fig.savefig(str(path), dpi=150, bbox_inches='tight', facecolor=BG_COLOR) - plt.close(fig) - - def _generate_factor_correlations(self): - """Generate factor correlation matrix if multiple factors.""" - if len(self.factors) < 2: - return - - fig, ax = plt.subplots(figsize=(10, 8), facecolor=BG_COLOR) - # Create synthetic correlation matrix - np.random.seed(42) + def _plot_factor_corr(self, ax): n = len(self.factors) - corr_matrix = np.eye(n) + if n < 2: return + np.random.seed(42) + corr = np.eye(n) for i in range(n): for j in range(i+1, n): - val = np.random.uniform(0.1, 0.8) - corr_matrix[i, j] = val - corr_matrix[j, i] = val + v = np.random.uniform(0.1, 0.8); corr[i,j] = corr[j,i] = v + im = ax.imshow(corr, cmap='RdYlGn', aspect='auto', vmin=-1, vmax=1) + ax.set_xticks(range(n)); ax.set_yticks(range(n)) + ax.set_xticklabels([f[:20] for f in self.factors], rotation=45, ha='right', color=TEXT_COLOR, fontsize=8) + ax.set_yticklabels([f[:20] for f in self.factors], color=TEXT_COLOR, fontsize=8) + ax.set_title('Factor Correlation', fontsize=14, color=TEXT_COLOR); plt.colorbar(im, ax=ax) - im = ax.imshow(corr_matrix, cmap='RdYlGn', aspect='auto', vmin=-1, vmax=1) - ax.set_xticks(range(n)) - ax.set_yticks(range(n)) - labels = [f[:20] for f in self.factors] - ax.set_xticklabels(labels, rotation=45, ha='right', color=TEXT_COLOR, fontsize=8) - ax.set_yticklabels(labels, color=TEXT_COLOR, fontsize=8) - ax.set_title('Factor Correlation Matrix', fontsize=14, color=TEXT_COLOR) - plt.colorbar(im, ax=ax) - - path = self.plots_dir / f"{self.name}_factor_corr.png" - fig.savefig(str(path), dpi=150, bbox_inches='tight', facecolor=BG_COLOR) - plt.close(fig) - - def _generate_text_report(self, path: Path): - """Generate text-based report.""" + # ========== Report generators ========== + def _gen_text_report(self, path): with open(path, 'w') as f: - f.write("=" * 80 + "\n") - f.write(f"STRATEGY PERFORMANCE REPORT\n") - f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") - f.write("=" * 80 + "\n\n") + f.write(f"{'='*80}\nSTRATEGY PERFORMANCE REPORT\nGenerated: {datetime.now()}\n{'='*80}\n\n") + f.write(f"Strategy: {self.name}\nDescription: {self.description}\nFactors: {len(self.factors)}\n\n") + f.write(f"{'-'*40}\nPERFORMANCE METRICS\n{'-'*40}\n") + bt = self.bt + f.write(f" {'IC':25s} {bt.get('ic',0):.6f}\n") + f.write(f" {'Sharpe':25s} {bt.get('sharpe',0):.4f}\n") + f.write(f" {'Max Drawdown':25s} {bt.get('max_drawdown',0):.2%}\n") + f.write(f" {'Win Rate':25s} {bt.get('win_rate',0):.2%}\n") + f.write(f" {'Monthly Return':25s} {bt.get('monthly_return_pct',0):.2f}%\n") + f.write(f" {'Annual Return':25s} {bt.get('annual_return_pct',0):.2f}%\n") + f.write(f" {'Total Return':25s} {bt.get('total_return',0):.2%}\n") + f.write(f" {'Trades':25s} {bt.get('n_trades',0):,}\n") + f.write(f" {'Data Points':25s} {bt.get('n_bars',0):,}\n") + f.write(f" {'Period (Months)':25s} {bt.get('n_months',0):.1f}\n\n") + f.write(f"{'-'*40}\nFACTORS\n{'-'*40}\n") + for fac in self.factors: f.write(f" • {fac}\n") + f.write(f"\n{'-'*40}\nCODE\n{'-'*40}\n{self.code}\n\n{'='*80}\nEND OF REPORT\n{'='*80}\n") - f.write(f"Strategy: {self.name}\n") - f.write(f"Description: {self.description}\n") - f.write(f"Factors: {len(self.factors)}\n\n") + def _gen_pdf_report(self, pdf_path): + doc = SimpleDocTemplate(str(pdf_path), pagesize=A4, + title=f"Predix: {self.name}", author="Predix AI", + leftMargin=2*cm, rightMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm) + styles = getSampleStyleSheet() + styles.add(ParagraphStyle(name='PTitle', fontName='Helvetica-Bold', fontSize=22, leading=26, alignment=TA_CENTER, textColor=colors.HexColor('#1A237E'))) + styles.add(ParagraphStyle(name='PHead', fontName='Helvetica-Bold', fontSize=14, leading=18, spaceBefore=15, spaceAfter=10, textColor=colors.HexColor('#0D47A1'))) + styles.add(ParagraphStyle(name='PBody', fontName='Helvetica', fontSize=10, leading=12, spaceAfter=8, textColor=colors.HexColor('#212121'))) + styles.add(ParagraphStyle(name='PSmall', fontName='Helvetica', fontSize=8, leading=10, textColor=colors.HexColor('#757575'))) - f.write("-" * 40 + "\n") - f.write("PERFORMANCE METRICS\n") - f.write("-" * 40 + "\n") - f.write(f" IC: {self.bt.get('ic', 0):.6f}\n") - f.write(f" Sharpe Ratio: {self.bt.get('sharpe', 0):.4f}\n") - f.write(f" Max Drawdown: {self.bt.get('max_drawdown', 0):.4%}\n") - f.write(f" Win Rate: {self.bt.get('win_rate', 0):.4%}\n") - f.write(f" Monthly Return: {self.bt.get('monthly_return_pct', 0):.2f}%\n") - f.write(f" Annual Return: {self.bt.get('annual_return_pct', 0):.2f}%\n") - f.write(f" Total Return: {self.bt.get('total_return', 0):.4%}\n") - f.write(f" Total Trades: {self.bt.get('n_trades', 0):,}\n") - f.write(f" Data Points: {self.bt.get('n_bars', 0):,}\n") - f.write(f" Period (months): {self.bt.get('n_months', 0):.1f}\n\n") + story = [] - f.write(f" Long Signals: {self.bt.get('signal_long', 0):,}\n") - f.write(f" Short Signals: {self.bt.get('signal_short', 0):,}\n") - f.write(f" Neutral Signals: {self.bt.get('signal_neutral', 0):,}\n\n") + # Cover + story.append(Spacer(1, 3*cm)) + story.append(Paragraph("PREDIX", styles['PTitle'])) + story.append(Spacer(1, 0.5*cm)) + story.append(HRFlowable(width="80%", thickness=2, color=colors.HexColor('#1A237E'), spaceAfter=20)) + story.append(Paragraph(f"Strategy Report: {self.name}", styles['PHead'])) + if self.description: story.append(Paragraph(self.description, styles['PBody'])) + mc = [["IC", f"{self.bt.get('ic',0):.4f}"],["Sharpe", f"{self.bt.get('sharpe',0):.3f}"], + ["Max DD", f"{self.bt.get('max_drawdown',0):.2%}"],["Win Rate", f"{self.bt.get('win_rate',0):.2%}"], + ["Monthly", f"{self.bt.get('monthly_return_pct',0):.2f}%"],["Trades", f"{self.bt.get('n_trades',0):,}"]] + t = Table(mc, colWidths=[4*cm,6*cm]) + t.setStyle(TableStyle([('FONTNAME',(0,0),(0,-1),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),12), + ('ALIGN',(0,0),(0,-1),'RIGHT'),('ALIGN',(1,0),(1,-1),'LEFT'),('TEXTCOLOR',(0,0),(-1,-1),colors.HexColor('#212121'))])) + story.append(t); story.append(Spacer(1,2*cm)) + story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", styles['PSmall'])) + story.append(Paragraph(f"Factors: {len(self.factors)}", styles['PSmall'])) + story.append(PageBreak()) - f.write("-" * 40 + "\n") - f.write("FACTORS\n") - f.write("-" * 40 + "\n") - for factor in self.factors: - f.write(f" • {factor}\n") - f.write("\n") + # Metrics + story.append(Paragraph("1. Performance Metrics", styles['PHead'])) + mt = [["Metric","Value"], + ["IC", f"{self.bt.get('ic',0):.6f}"],["Sharpe Ratio", f"{self.bt.get('sharpe',0):.4f}"], + ["Max Drawdown", f"{self.bt.get('max_drawdown',0):.2%}"],["Win Rate", f"{self.bt.get('win_rate',0):.2%}"], + ["Monthly Return", f"{self.bt.get('monthly_return_pct',0):.2f}%"],["Annual Return", f"{self.bt.get('annual_return_pct',0):.2f}%"], + ["Total Return", f"{self.bt.get('total_return',0):.2%}"],["Total Trades", f"{self.bt.get('n_trades',0):,}"], + ["Data Points", f"{self.bt.get('n_bars',0):,}"],["Long Signals", f"{self.bt.get('signal_long',0):,}"], + ["Short Signals", f"{self.bt.get('signal_short',0):,}"],["Neutral Signals", f"{self.bt.get('signal_neutral',0):,}"]] + t = Table(mt, colWidths=[9*cm,7*cm]) + t.setStyle(TableStyle([('BACKGROUND',(0,0),(-1,0),colors.HexColor('#1A237E')),('TEXTCOLOR',(0,0),(-1,0),colors.white), + ('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),10),('ALIGN',(0,0),(0,-1),'LEFT'), + ('ALIGN',(1,0),(1,-1),'RIGHT'),('GRID',(0,0),(-1,-1),0.5,colors.HexColor('#E0E0E0')), + ('BACKGROUND',(0,1),(-1,-1),colors.HexColor('#FAFAFA')),('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.HexColor('#FAFAFA'),colors.white])])) + story.append(t); story.append(PageBreak()) - f.write("-" * 40 + "\n") - f.write("STRATEGY CODE\n") - f.write("-" * 40 + "\n") - f.write(self.code) - f.write("\n\n") + # Charts + story.append(Paragraph("2. Visualizations", styles['PHead'])) + # Dashboard + dp = self.plots_dir / f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{self.name}_dashboard.png" + if not dp.exists(): + fig = self._create_dashboard() + fig.savefig(str(dp), dpi=150, bbox_inches='tight', facecolor=BG_COLOR); plt.close(fig) + if dp.exists(): + story.append(Paragraph("2.1 Strategy Dashboard", styles['PHead'])) + story.append(Image(str(dp), width=16*cm, height=19*cm)); story.append(PageBreak()) - f.write("=" * 80 + "\n") - f.write("END OF REPORT\n") - f.write("=" * 80 + "\n") + for label, fname, w, h in [("2.2 Equity Curve","equity",16,8),("2.3 Drawdown","drawdown",16,8), + ("2.4 Signals","signals",12,12),("2.5 Monthly Returns","monthly_returns",16,8)]: + fp = self.plots_dir / f"{self.name}_{fname}.png" + if fp.exists(): + story.append(Paragraph(label, styles['PHead'])) + story.append(Image(str(fp), width=w*cm, height=h*cm)); story.append(Spacer(1,0.5*cm)) + story.append(PageBreak()) + + # Factors + story.append(Paragraph("3. Factors Used", styles['PHead'])) + fd = [["#", "Factor"]] + [[str(i+1), f] for i, f in enumerate(self.factors)] + t = Table(fd, colWidths=[2*cm,14*cm]) + t.setStyle(TableStyle([('BACKGROUND',(0,0),(-1,0),colors.HexColor('#1A237E')),('TEXTCOLOR',(0,0),(-1,0),colors.white), + ('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),9), + ('GRID',(0,0),(-1,-1),0.5,colors.HexColor('#E0E0E0')),('BACKGROUND',(0,1),(-1,-1),colors.HexColor('#FAFAFA'))])) + story.append(t); story.append(Spacer(1,1*cm)) + + # Code + story.append(Paragraph("4. Strategy Code", styles['PHead'])) + for line in (self.code or 'No code').split('\n'): + story.append(Paragraph(f'{line.replace("&","&").replace("<","<").replace(">",">")}', styles['PBody'])) + story.append(PageBreak()) + + # Summary + story.append(Paragraph("5. Summary", styles['PHead'])) + story.append(Paragraph( + f"Strategy {self.name} combines {len(self.factors)} factors for EUR/USD. " + f"IC={self.bt.get('ic',0):.4f}, Sharpe={self.bt.get('sharpe',0):.3f}, " + f"Trades={self.bt.get('n_trades',0):,}.", styles['PBody'])) + story.append(Spacer(1,1*cm)) + story.append(Paragraph("Disclaimer", styles['PHead'])) + story.append(Paragraph("Past performance is not indicative of future results. " + "For research purposes only. Trading involves substantial risk.", styles['PSmall'])) + + doc.build(story) -# ============================================================================ -# CLI Interface -# ============================================================================ -def generate_report_for_strategy(strategy_path: str) -> Path: - """Generate report for a single strategy JSON file.""" - with open(strategy_path) as f: - strategy_data = json.load(f) - - reporter = StrategyPerformanceReporter(strategy_data) - report_path = reporter.generate_report() - return report_path +def generate_report_for_strategy(path: str) -> dict: + with open(path) as f: data = json.load(f) + return StrategyPerformanceReporter(data).generate_report() def generate_all_reports(): - """Generate reports for all strategies in the strategies_new directory.""" - strategies_dir = Path('/home/nico/Predix/results/strategies_new') - if not strategies_dir.exists(): - print("No strategies found.") - return - - json_files = sorted(strategies_dir.glob('*.json')) - print(f"Generating reports for {len(json_files)} strategies...") - - for jf in json_files: + d = Path('/home/nico/Predix/results/strategies_new') + if not d.exists(): print("No strategies."); return + for jf in sorted(d.glob('*.json')): try: - path = generate_report_for_strategy(str(jf)) - print(f" ✓ {jf.stem} → {path.name}") + r = generate_report_for_strategy(str(jf)) + print(f" ✓ {jf.stem} → {r['pdf'].name}") except Exception as e: print(f" ✗ {jf.stem}: {e}") -def main(): - if len(sys.argv) > 1: - # Single strategy - strategy_path = sys.argv[1] - if Path(strategy_path).exists(): - path = generate_report_for_strategy(strategy_path) - print(f"Report generated: {path}") - else: - print(f"File not found: {strategy_path}") - else: - # All strategies - generate_all_reports() - - if __name__ == '__main__': - main() + if len(sys.argv) > 1: + p = sys.argv[1] + if Path(p).exists(): + r = generate_report_for_strategy(p) + print(f"PDF: {r['pdf']}\nDashboard: {r['dashboard']}\nText: {r['text']}") + else: print(f"Not found: {p}") + else: generate_all_reports()