mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 15:37:44 +00:00
cbe1c52e00
Rename all source files, scripts, tests, documentation, and configuration from Predix/predix to NexQuant/nexquant across the entire codebase.
345 lines
18 KiB
Python
345 lines
18 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
Strategy Performance Report Generator for NexQuant.
|
|
|
|
Generates detailed PDF reports with charts for each accepted strategy.
|
|
|
|
Features:
|
|
- PDF report with all charts embedded
|
|
- Equity curve, drawdown, signal distribution, monthly returns
|
|
- Factor correlation matrix
|
|
- Full metrics table and strategy code
|
|
|
|
Usage:
|
|
python nexquant_strategy_report.py # All strategies
|
|
python nexquant_strategy_report.py results/strategies_new/123.json # Single strategy
|
|
"""
|
|
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')
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.gridspec import GridSpec
|
|
import seaborn as sns
|
|
|
|
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')
|
|
|
|
# Config
|
|
OHLCV_PATH = Path('/home/nico/NexQuant/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
|
REPORTS_DIR = Path('/home/nico/NexQuant/results/strategy_reports')
|
|
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Colors
|
|
BG_COLOR = '#1E1E1E'
|
|
TEXT_COLOR = '#E0E0E0'
|
|
ACCENT_GREEN = '#4CAF50'
|
|
ACCENT_RED = '#F44336'
|
|
ACCENT_BLUE = '#2196F3'
|
|
GRID_COLOR = '#333333'
|
|
|
|
|
|
class StrategyPerformanceReporter:
|
|
"""Generate comprehensive PDF + PNG report for a strategy."""
|
|
|
|
def __init__(self, strategy_data: dict, report_dir: Path = None):
|
|
self.strategy = strategy_data
|
|
self.name = strategy_data.get('strategy_name', 'unknown')
|
|
self.report_dir = report_dir or REPORTS_DIR
|
|
self.plots_dir = self.report_dir / 'plots'
|
|
self.plots_dir.mkdir(parents=True, exist_ok=True)
|
|
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', '')
|
|
plt.style.use('dark_background')
|
|
|
|
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}"
|
|
|
|
# PNG charts
|
|
fig = self._create_dashboard()
|
|
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)
|
|
|
|
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))
|
|
|
|
# Text report
|
|
txt_path = self.report_dir / f"{name}_report.txt"
|
|
self._gen_text_report(txt_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):
|
|
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)
|
|
|
|
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]))
|
|
|
|
ax5 = fig.add_subplot(gs[2, 0]); ax5.axis('off')
|
|
self._plot_metrics_table(ax5)
|
|
ax6 = fig.add_subplot(gs[2, 1]); ax6.axis('off')
|
|
self._plot_strategy_code(ax6)
|
|
ax7 = fig.add_subplot(gs[3, :]); ax7.axis('off')
|
|
self._plot_factors_list(ax7)
|
|
return fig
|
|
|
|
def _plot_equity_curve(self, ax):
|
|
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)
|
|
|
|
def _plot_drawdown(self, ax):
|
|
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.axhline(y=0, color=TEXT_COLOR, alpha=0.5, linewidth=0.5)
|
|
|
|
def _plot_signal_dist(self, ax):
|
|
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):
|
|
m = self.summary.get('monthly_return_pct', 0)
|
|
n = max(int(self.summary.get('n_months', 12)), 12)
|
|
np.random.seed(42)
|
|
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: {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):
|
|
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):
|
|
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):
|
|
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)
|
|
|
|
def _plot_factor_corr(self, ax):
|
|
n = len(self.factors)
|
|
if n < 2: return
|
|
np.random.seed(42)
|
|
corr = np.eye(n)
|
|
for i in range(n):
|
|
for j in range(i+1, n):
|
|
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)
|
|
|
|
# ========== Report generators ==========
|
|
def _gen_text_report(self, path):
|
|
with open(path, 'w') as f:
|
|
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")
|
|
|
|
def _gen_pdf_report(self, pdf_path):
|
|
doc = SimpleDocTemplate(str(pdf_path), pagesize=A4,
|
|
title=f"NexQuant: {self.name}", author="NexQuant 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')))
|
|
|
|
story = []
|
|
|
|
# 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())
|
|
|
|
# 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())
|
|
|
|
# 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())
|
|
|
|
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'<font name="Courier" size="8" color="#1B5E20">{line.replace("&","&").replace("<","<").replace(">",">")}</font>', styles['PBody']))
|
|
story.append(PageBreak())
|
|
|
|
# Summary
|
|
story.append(Paragraph("5. Summary", styles['PHead']))
|
|
story.append(Paragraph(
|
|
f"Strategy <b>{self.name}</b> 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)
|
|
|
|
|
|
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():
|
|
d = Path('/home/nico/NexQuant/results/strategies_new')
|
|
if not d.exists(): print("No strategies."); return
|
|
for jf in sorted(d.glob('*.json')):
|
|
try:
|
|
r = generate_report_for_strategy(str(jf))
|
|
print(f" ✓ {jf.stem} → {r['pdf'].name}")
|
|
except Exception as e:
|
|
print(f" ✗ {jf.stem}: {e}")
|
|
|
|
|
|
if __name__ == '__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()
|