
Financereport
- 637 installs
- 316 repo stars
- Updated August 1, 2026
- aojdevstudio/finance-guru
FinanceReport is a Claude Code skill that generates institutional-quality 8–10 page PDF stock and ETF analysis reports with charts, portfolio sizing, and Perplexity sentiment for developers automating investment research
About
FinanceReport is a Finance Guru skill that produces branded 8–10 page PDF investment analysis reports for individual stocks and ETFs. It routes workflows such as GenerateSingleReport with VGT-style headers, embedded charts, portfolio sizing sections, and Perplexity sentiment integration when users mention generate report, create pdf, stock analysis, ticker report, or watchlist analysis. Developers reach for FinanceReport when they need repeatable institutional-format equity research PDFs instead of ad-hoc markdown summaries. The skill expects Finance Guru branding assets and configured sentiment or data sources referenced in its workflow table.
- Generates 8-10 page PDF investment reports with VGT-style headers
- Integrates quant tools for risk, momentum, and volatility metrics
- Pulls real-time sentiment via Perplexity MCP server
- Supports three distinct workflows: GenerateSingleReport, RegenerateBatch, and FullResearchWorkflow
- Saves reports with timestamped filenames to a dedicated analysis folder
Financereport by the numbers
- 637 all-time installs (skills.sh)
- Ranked #358 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aojdevstudio/finance-guru --skill financereportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 637 |
|---|---|
| repo stars | ★ 316 |
| Security audit | 1 / 3 scanners passed |
| Last updated | August 1, 2026 |
| Repository | aojdevstudio/finance-guru ↗ |
How do you automate institutional stock PDF reports?
Automatically generate institutional-quality PDF stock and ETF analysis reports complete with charts, sentiment, and branded formatting.
Who is it for?
Developers building finance automation who need repeatable institutional PDF equity reports with charts and sentiment.
Skip if: Teams needing live trading execution, regulatory filing compliance, or non-PDF deliverables without Finance Guru workflows.
When should I use this skill?
User mentions generate report, create pdf, stock analysis, ticker report, watchlist analysis, or regenerate investment reports.
What you get
Branded 8–10 page PDF investment report with charts, portfolio sizing, headers, and sentiment sections.
- branded PDF stock report
- ETF analysis PDF with charts
By the numbers
- Generates 8–10 page PDF investment analysis reports
- Includes VGT-style branded headers and embedded charts
Files
FinanceReport
Generate comprehensive 8-10 page PDF investment analysis reports with Finance Guru branding.
Workflow Routing
When executing a workflow, output this notification directly:
Running the **WorkflowName** workflow from the **FinanceReport** skill...| Workflow | Trigger | File |
|---|---|---|
| GenerateSingleReport | "generate report for TSLA", "create PDF" | workflows/GenerateSingleReport.md |
| RegenerateBatch | "regenerate batch", "redo all reports" | workflows/RegenerateBatch.md |
| FullResearchWorkflow | "full analysis", "thorough research" | workflows/FullResearchWorkflow.md |
Examples
Example 1: Generate single ticker report
User: "Generate a report for NVDA"
-> Invokes GenerateSingleReport workflow
-> Runs quant tools (risk_metrics, momentum, volatility)
-> Fetches sentiment via Perplexity MCP
-> Builds 8-10 page PDF with VGT-style header
-> Saves to fin-guru-private/fin-guru/analysis/reports/NVDA-analysis-2025-12-18.pdfExample 2: Regenerate all watchlist reports
User: "Regenerate batch 1 reports"
-> Invokes RegenerateBatch workflow
-> Launches 8 subagents in parallel
-> Each runs FullResearchWorkflow
-> Replaces existing PDFs with improved versionsExample 3: Deep research with full workflow
User: "Do a full analysis on AMZN for the 2026 watchlist"
-> Invokes FullResearchWorkflow
-> Phase 1: Market research (Perplexity/Exa for catalysts, risks)
-> Phase 2: Quant analysis (252-day risk metrics, 90-day momentum)
-> Phase 3: Strategy recommendation (buy/hold/sell, position sizing)
-> Phase 4: Generate comprehensive PDF reportReport Structure (8-10 Pages)
Cover Page (VGT-Style Header) - UNIFORM STANDARD
CRITICAL: Every report MUST follow this EXACT format:
┌─────────────────────────────────────────────────────────────┐
│ FINANCE GURU™ │
│ Family Office Investment Analysis │
│ ───────────────────────────────────────────────────────── │
│ │
│ TICKER - Full Company/Fund Name │
│ 2026 Watchlist Analysis & Investment Report │
│ │
├─────────────────────────────────────────────────────────────┤
│ Report Date: December 18, 2025 │
│ Analyst Team: Finance Guru Multi-Agent System │
│ • Market Researcher (Dr. Aleksandr Petrov)│
│ • Quant Analyst │
│ • Strategy Advisor │
│ Current Price: $XXX.XX │
│ YTD Performance: +XX.XX% │
│ Expense Ratio: 0.XX% (ETFs only) │
└─────────────────────────────────────────────────────────────┘UNIFORMITY RULES:
- Analyst team names MUST be listed on EVERY report
- "Finance Guru Multi-Agent System" header REQUIRED
- Individual analyst names with personas (e.g., Dr. Aleksandr Petrov)
- Same format, same structure, every single time
Executive Summary
- Investment thesis (200+ words)
- Key findings with bold labels
- Final verdict box (rating, conviction, risk level)
Quantitative Analysis
- Risk metrics table (VaR, Sharpe, Beta, Alpha)
- Momentum indicators (RSI, MACD, Stochastic)
- Volatility assessment (ATR, Bollinger regime)
- Embedded charts where applicable
TABLE FORMATTING - CRITICAL:
- All table cells use Paragraph objects (text wraps, never overflows)
- Column widths explicitly set to fit within 7.5" content area
- Header row: Navy background, white text, bold
- Data rows: Alternating white/light gray backgrounds
Market Research
- Company overview and positioning
- 2026 catalysts and risks
- Analyst ratings and sentiment
- Perplexity MCP integration
Portfolio Sizing (NEW)
Shows BOTH percentage AND dollar amount:
Recommended Allocation: 2-3%
For $250,000 portfolio:
- Dollar amount: $5,000 - $7,500
- Share count: ~25-38 shares at $200Strategy Recommendations
- Entry strategy with price targets
- Risk management (stop-loss levels)
- Position management approach
Sources & Disclaimer - UNIFORM STANDARD
CRITICAL: Every report MUST end with this EXACT format:
─────────────────────────────────────────────────────────────
DISCLAIMER: This analysis is provided for educational and
informational purposes only. [full disclaimer text]
Powered by Finance Guru™
Report Date: December 18, 2025
─────────────────────────────────────────────────────────────"Powered by Finance Guru™" is REQUIRED on every report.
Tool Usage
ChartKit.py
uv run python .claude/skills/FinanceReport/tools/ChartKit.py \
--ticker TSLA \
--chart-type line \
--data-source cliReportGenerator.py
uv run python .claude/skills/FinanceReport/tools/ReportGenerator.py \
--ticker TSLA \
--portfolio-value 250000 \
--output-dir fin-guru-private/fin-guru/analysis/reports/Integration Points
Perplexity MCP (Sentiment & Research)
# Search for market sentiment
mcp__perplexity__search(query=f"{ticker} stock analysis 2025 catalysts risks")
# Deep reasoning for thesis
mcp__perplexity__reason(query=f"Analyze {ticker} investment thesis for 2026")Existing Finance Guru CLI Tools
src/analysis/risk_metrics_cli.py- VaR, CVaR, Sharpe, Sortino, Beta, Alphasrc/utils/momentum_cli.py- RSI, MACD, Stochastic, Williams %Rsrc/utils/volatility_cli.py- ATR, Bollinger Bands, Keltner Channelssrc/analysis/correlation_cli.py- Portfolio correlation matrix
User Profile
Reads portfolio value from fin-guru/data/user-profile.yaml for sizing:
investment_portfolio.total_value= Current portfolio value- Calculates exact dollar amounts for recommendations
Reference Files
- StyleGuide.md - Brand colors, typography, table styling
- VisGuide.md - Chart selection dictionary, labeling standards
---
Skill Type: Domain Enforcement: Suggest Priority: High Line Count: < 200 lines
StyleGuide - Finance Guru Report Branding
Brand standards for Finance Guru PDF reports.
Color Palette
# Primary Colors
NAVY = "#1a365d" # Brand primary - titles, headers, borders
GOLD = "#d69e2e" # Accents, highlights, separators
GREEN = "#38a169" # Positive signals, buy ratings
RED = "#e53e3e" # Warnings, risks, sell signals
# Neutral Colors
DARK_GRAY = "#2d3748" # Body text
LIGHT_GRAY = "#e2e8f0" # Table backgrounds, alternating rows
WHITE = "#ffffff" # Page background
BLACK = "#000000" # Emphasis textTypography Hierarchy
| Element | Font | Size | Color | Alignment |
|---|---|---|---|---|
| Brand Header | Helvetica-Bold | 24pt | Navy | Center |
| Subtitle | Helvetica | 14pt | Gold | Center |
| Section Header | Helvetica-Bold | 16pt | Navy | Left |
| Subsection | Helvetica-Bold | 12pt | Navy | Left |
| Body Text | Helvetica | 10pt | Dark Gray | Justified |
| Bullet Points | Helvetica | 10pt | Dark Gray | Left |
| Disclaimer | Helvetica-Oblique | 8pt | Dark Gray | Center |
| Footer | Helvetica | 8pt | Dark Gray | Center |
Page Layout
┌─────────────────────────────────────────────────────────────┐
│ 0.5" top margin │
├─────┬─────────────────────────────────────────────────┬─────┤
│ │ │ │
│0.5" │ CONTENT AREA (7.5" wide) │0.5" │
│ │ │ │
│ │ Page Size: 8.5" x 11" (letter) │ │
│ │ Content Width: 7.5" │ │
│ │ Content Height: 10" │ │
│ │ │ │
├─────┴─────────────────────────────────────────────────┴─────┤
│ 0.5" bottom margin │
└─────────────────────────────────────────────────────────────┘Margins: 0.5 inches all sides (tighter than default for more content)
VGT-Style Header Format
┌─────────────────────────────────────────────────────────────┐
│ │
│ FINANCE GURU™ │
│ Family Office Investment Analysis │
│ ───────────────────────────────────────────────────────── │
│ │
│ VGT - Vanguard Information Technology ETF │
│ 2026 Watchlist Analysis & Investment Report │
│ │
├─────────────────────────────────────────────────────────────┤
│ Report Date: December 18, 2025 │
│ Analyst Team: Finance Guru Multi-Agent System │
│ • Market Researcher (Dr. Aleksandr Petrov) │
│ • Quant Analyst │
│ • Strategy Advisor │
│ Current Price: $740.62 │
│ YTD Performance: +24.89% │
│ Expense Ratio: 0.10% │
└─────────────────────────────────────────────────────────────┘Table Styling
⚠️ CRITICAL: Text Wrapping in Tables
PROBLEM: Plain strings in ReportLab tables DO NOT wrap. They overflow cell boundaries.
SOLUTION: Wrap ALL table cell content in Paragraph objects.
# ❌ WRONG - Text will overflow
data = [
['Metric', 'This is a very long description that will overflow'],
]
# ✅ CORRECT - Text wraps within cell
from reportlab.platypus import Paragraph
from reportlab.lib.styles import ParagraphStyle
cell_style = ParagraphStyle(
name='TableCell',
fontSize=9,
fontName='Helvetica',
wordWrap='CJK' # Enables better word wrapping
)
data = [
[Paragraph('Metric', cell_style),
Paragraph('This is a very long description that will wrap nicely', cell_style)],
]Column Width Guidelines
Page content width = 7.5 inches. Table widths MUST fit within this.
| Table Type | Columns | Width Distribution |
|---|---|---|
| 2-column | Label, Value | 2" + 5.5" = 7.5" |
| 3-column | Metric, Value, Assessment | 2" + 2" + 3.5" = 7.5" |
| 4-column | Metric, Value, Benchmark, Assessment | 1.5" + 1.5" + 1.5" + 3" = 7.5" |
Header Row
('BACKGROUND', (0, 0), (-1, 0), NAVY)
# Note: Text color handled by TableHeaderCell ParagraphStyle (white text)
('BOTTOMPADDING', (0, 0), (-1, 0), 10)
('TOPPADDING', (0, 0), (-1, 0), 10)Data Rows
('ROWBACKGROUNDS', (0, 1), (-1, -1), [WHITE, LIGHT_GRAY]) # Alternating
# Note: Font styling handled by TableCell ParagraphStyle
('TOPPADDING', (0, 1), (-1, -1), 8)
('BOTTOMPADDING', (0, 1), (-1, -1), 8)
('LEFTPADDING', (0, 0), (-1, -1), 6)
('RIGHTPADDING', (0, 0), (-1, -1), 6)
('VALIGN', (0, 0), (-1, -1), 'MIDDLE')
('GRID', (0, 0), (-1, -1), 0.5, DARK_GRAY)Verdict Box Styling
Strong Buy (Green)
('BACKGROUND', (0, 0), (-1, 0), GREEN)
('TEXTCOLOR', (0, 0), (-1, 0), WHITE)
('FONTSIZE', (0, 0), (-1, 0), 14)Conditional Buy (Gold)
('BACKGROUND', (0, 0), (-1, 0), GOLD)
('TEXTCOLOR', (0, 0), (-1, 0), BLACK)Hold (Light Gray)
('BACKGROUND', (0, 0), (-1, 0), LIGHT_GRAY)
('TEXTCOLOR', (0, 0), (-1, 0), BLACK)Sell (Red)
('BACKGROUND', (0, 0), (-1, 0), RED)
('TEXTCOLOR', (0, 0), (-1, 0), WHITE)Section Separators
Horizontal Rule
HRFlowable(width="100%", thickness=2, color=NAVY)Section Spacer
Spacer(1, 0.3*inch) # Standard between sections
Spacer(1, 0.15*inch) # Small within sectionsDisclaimer Text - UNIFORM STANDARD
FORMAT (matching FTNT analysis):
─────────────────────────────────────────────────────────────
DISCLAIMER: This analysis is provided for educational and informational
purposes only. It does not constitute investment advice, financial advice,
trading advice, or any other sort of advice. Finance Guru is a personal
family office system and does not provide recommendations to third parties.
Past performance is not indicative of future results. All investments
involve risk, including the possible loss of principal. Consult with a
qualified financial professional before making any investment decisions.
Powered by Finance Guru™
Report Date: {date_display}
─────────────────────────────────────────────────────────────CRITICAL REQUIREMENTS:
- "Powered by Finance Guru™" is REQUIRED on every report
- Must be bold, Navy color, centered
- Report date below in italics
- Horizontal rule separators above and below
Disclaimer Style Code
# Disclaimer text style
disclaimer_style = ParagraphStyle(
name='Disclaimer',
fontSize=8,
textColor=DARK_GRAY,
alignment=TA_CENTER,
fontName='Helvetica-Oblique'
)
# "Powered by Finance Guru™" branding style
powered_by_style = ParagraphStyle(
name='PoweredBy',
fontSize=9,
fontName='Helvetica-Bold',
textColor=NAVY,
alignment=TA_CENTER,
spaceAfter=4
)ReportLab Import Template
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.lib.colors import HexColor
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
PageBreak, HRFlowable, KeepTogether, Image
)
# Brand Colors
NAVY = HexColor("#1a365d")
GOLD = HexColor("#d69e2e")
GREEN = HexColor("#38a169")
RED = HexColor("#e53e3e")
DARK_GRAY = HexColor("#2d3748")
LIGHT_GRAY = HexColor("#e2e8f0")ChartKit Help
Chart generation CLI for Finance Guru PDF reports.
Usage
uv run python ChartKit.py --ticker TICKER --chart-type TYPE [OPTIONS]Required Arguments
| Argument | Description |
|---|---|
--chart-type | Chart type: line, bar, barh, heatmap, technical |
Optional Arguments
| Argument | Default | Description |
|---|---|---|
--ticker | - | Stock ticker symbol |
--tickers | - | Comma-separated tickers (for heatmap) |
--days | 90 | Days of historical data |
--data | - | JSON data string (alternative to CLI tools) |
--title | Auto | Custom chart title |
--output | chart.png | Output file path |
--figsize | 7,3 | Figure size as width,height |
--dpi | 150 | Output resolution |
Chart Types
Line Chart
Best for: Price history, trends, moving averages
uv run python ChartKit.py --ticker TSLA --chart-type line --days 90Bar Chart (Horizontal)
Best for: Comparing metrics across categories
uv run python ChartKit.py --ticker TSLA --chart-type barh --data '{"labels":["Sharpe","Sortino","Beta"],"values":[1.5,2.1,1.2]}'Heatmap
Best for: Correlation matrices
uv run python ChartKit.py --chart-type heatmap --tickers TSLA,PLTR,NVDA,AAPLTechnical Chart
Best for: Price with RSI/MACD overlay
uv run python ChartKit.py --ticker TSLA --chart-type technical --days 90Data Sources
ChartKit integrates with Finance Guru CLI tools:
risk_metrics_cli.py- For risk/return datamomentum_cli.py- For technical indicatorscorrelation_cli.py- For correlation matrices
Output
Charts are saved as PNG files at the specified DPI. For PDF embedding, use --dpi 150 minimum.
#!/usr/bin/env python3
"""ChartKit - Finance Guru Chart Generation CLI.
Generates publication-quality charts for PDF reports.
Supports line, bar, scatter, heatmap, and technical indicator charts.
Usage:
uv run python ChartKit.py --ticker TSLA --chart-type line --days 90
uv run python ChartKit.py --ticker TSLA --chart-type heatmap --tickers TSLA,PLTR,NVDA
"""
import argparse
import json
import subprocess
import sys
from io import BytesIO
from typing import Any
# Lazy loading for optional dependencies
_plt = None
_np = None
def _import_dependencies():
"""Import matplotlib and numpy lazily."""
global _plt, _np
try:
import matplotlib.pyplot as plt
import numpy as np
_plt = plt
_np = np
except ImportError:
print("ChartKit requires matplotlib and numpy.")
print("Install with: uv add matplotlib numpy")
sys.exit(1)
# Finance Guru Brand Colors
COLORS = {
"navy": "#1a365d",
"gold": "#d69e2e",
"green": "#38a169",
"red": "#e53e3e",
"dark_gray": "#2d3748",
"light_gray": "#e2e8f0",
}
def run_cli_tool(tool_path: str, args: list[str]) -> dict[str, Any]:
"""Run a Finance Guru CLI tool and parse JSON output."""
cmd = ["uv", "run", "python", tool_path] + args + ["--output", "json"]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return json.loads(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Error running {tool_path}: {e.stderr}")
return {}
except json.JSONDecodeError:
print(f"Error parsing JSON from {tool_path}")
return {}
def create_line_chart(
data: dict[str, Any],
title: str,
ylabel: str = "Price ($)",
figsize: tuple = (7, 3),
dpi: int = 150,
) -> BytesIO:
"""Create a line chart for price history or metrics."""
_import_dependencies()
fig, ax = _plt.subplots(figsize=figsize, dpi=dpi)
if "dates" in data and "values" in data:
ax.plot(data["dates"], data["values"], color=COLORS["navy"], linewidth=1.5)
elif "x" in data and "y" in data:
ax.plot(data["x"], data["y"], color=COLORS["navy"], linewidth=1.5)
ax.set_title(title, fontsize=11, fontweight="bold", loc="left")
ax.set_ylabel(ylabel, fontsize=9)
ax.grid(True, alpha=0.3)
ax.tick_params(axis="both", labelsize=8)
# Rotate x-axis labels if dates
if len(ax.get_xticklabels()) > 10:
_plt.xticks(rotation=45, ha="right")
_plt.tight_layout()
buffer = BytesIO()
fig.savefig(buffer, format="png", bbox_inches="tight", facecolor="white")
buffer.seek(0)
_plt.close(fig)
return buffer
def create_bar_chart(
data: dict[str, Any],
title: str,
horizontal: bool = True,
figsize: tuple = (7, 3),
dpi: int = 150,
) -> BytesIO:
"""Create a bar chart for metric comparison."""
_import_dependencies()
fig, ax = _plt.subplots(figsize=figsize, dpi=dpi)
labels = data.get("labels", [])
values = data.get("values", [])
# Color bars based on positive/negative
colors = [COLORS["green"] if v >= 0 else COLORS["red"] for v in values]
if horizontal:
ax.barh(labels, values, color=colors)
ax.set_xlabel(data.get("xlabel", ""), fontsize=9)
else:
ax.bar(labels, values, color=colors)
ax.set_ylabel(data.get("ylabel", ""), fontsize=9)
ax.set_title(title, fontsize=11, fontweight="bold", loc="left")
ax.grid(True, alpha=0.3, axis="x" if horizontal else "y")
ax.tick_params(axis="both", labelsize=8)
_plt.tight_layout()
buffer = BytesIO()
fig.savefig(buffer, format="png", bbox_inches="tight", facecolor="white")
buffer.seek(0)
_plt.close(fig)
return buffer
def create_heatmap(
correlation_matrix: list[list[float]],
labels: list[str],
title: str = "Correlation Matrix",
figsize: tuple = (6, 5),
dpi: int = 150,
) -> BytesIO:
"""Create a correlation heatmap."""
_import_dependencies()
fig, ax = _plt.subplots(figsize=figsize, dpi=dpi)
matrix = _np.array(correlation_matrix)
# Create heatmap
im = ax.imshow(matrix, cmap="RdYlGn", vmin=-1, vmax=1)
# Add colorbar
cbar = ax.figure.colorbar(im, ax=ax)
cbar.ax.tick_params(labelsize=8)
# Set ticks and labels
ax.set_xticks(_np.arange(len(labels)))
ax.set_yticks(_np.arange(len(labels)))
ax.set_xticklabels(labels, fontsize=8)
ax.set_yticklabels(labels, fontsize=8)
# Rotate x labels
_plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
# Add value annotations
for i in range(len(labels)):
for j in range(len(labels)):
text_color = "white" if abs(matrix[i, j]) > 0.5 else "black"
ax.text(
j,
i,
f"{matrix[i, j]:.2f}",
ha="center",
va="center",
color=text_color,
fontsize=8,
)
ax.set_title(title, fontsize=11, fontweight="bold", loc="left")
_plt.tight_layout()
buffer = BytesIO()
fig.savefig(buffer, format="png", bbox_inches="tight", facecolor="white")
buffer.seek(0)
_plt.close(fig)
return buffer
def create_technical_chart(
price_data: list[float],
indicator_data: list[float],
dates: list[str],
price_label: str = "Price",
indicator_label: str = "RSI",
thresholds: list[float] | None = None,
title: str = "Technical Analysis",
figsize: tuple = (7, 4),
dpi: int = 150,
) -> BytesIO:
"""Create a dual-axis technical indicator chart."""
_import_dependencies()
fig, ax1 = _plt.subplots(figsize=figsize, dpi=dpi)
# Primary axis (price)
ax1.plot(dates, price_data, color=COLORS["navy"], linewidth=1.5, label=price_label)
ax1.set_ylabel(price_label, color=COLORS["navy"], fontsize=9)
ax1.tick_params(axis="y", labelcolor=COLORS["navy"], labelsize=8)
ax1.tick_params(axis="x", labelsize=8)
# Secondary axis (indicator)
ax2 = ax1.twinx()
ax2.plot(
dates,
indicator_data,
color=COLORS["gold"],
linewidth=1.2,
label=indicator_label,
)
ax2.set_ylabel(indicator_label, color=COLORS["gold"], fontsize=9)
ax2.tick_params(axis="y", labelcolor=COLORS["gold"], labelsize=8)
# Add threshold lines
if thresholds:
for thresh in thresholds:
ax2.axhline(
y=thresh, color=COLORS["light_gray"], linestyle="--", linewidth=0.8
)
ax1.set_title(title, fontsize=11, fontweight="bold", loc="left")
ax1.grid(True, alpha=0.3)
# Rotate x-axis labels
if len(dates) > 10:
_plt.xticks(rotation=45, ha="right")
# Legend
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper right", fontsize=8)
_plt.tight_layout()
buffer = BytesIO()
fig.savefig(buffer, format="png", bbox_inches="tight", facecolor="white")
buffer.seek(0)
_plt.close(fig)
return buffer
def save_chart(buffer: BytesIO, output_path: str):
"""Save chart buffer to file."""
with open(output_path, "wb") as f:
f.write(buffer.getvalue())
print(f"Chart saved to: {output_path}")
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="ChartKit - Finance Guru Chart Generation",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Generate line chart:
uv run python ChartKit.py --ticker TSLA --chart-type line --days 90
Generate heatmap:
uv run python ChartKit.py --chart-type heatmap --tickers TSLA,PLTR,NVDA,AAPL
Generate bar chart:
uv run python ChartKit.py --chart-type bar --data '{"labels":["Sharpe","Sortino","Beta"],"values":[1.5,2.1,1.2]}'
""",
)
parser.add_argument("--ticker", type=str, help="Stock ticker symbol")
parser.add_argument(
"--tickers", type=str, help="Comma-separated tickers for correlation"
)
parser.add_argument(
"--chart-type",
type=str,
required=True,
choices=["line", "bar", "barh", "heatmap", "technical"],
help="Type of chart to generate",
)
parser.add_argument(
"--days", type=int, default=90, help="Days of data (default: 90)"
)
parser.add_argument(
"--data", type=str, help="JSON data for chart (alternative to CLI tools)"
)
parser.add_argument("--title", type=str, help="Chart title")
parser.add_argument(
"--output", type=str, default="chart.png", help="Output file path"
)
parser.add_argument(
"--figsize", type=str, default="7,3", help="Figure size as width,height"
)
parser.add_argument("--dpi", type=int, default=150, help="Output DPI")
args = parser.parse_args()
# Parse figsize
figsize = tuple(map(float, args.figsize.split(",")))
# Generate chart based on type
if args.chart_type == "line":
if args.data:
data = json.loads(args.data)
else:
# Use momentum CLI to get price data
data = run_cli_tool(
"src/utils/momentum_cli.py", [args.ticker, "--days", str(args.days)]
)
title = args.title or f"{args.ticker} - {args.days} Day Price History"
buffer = create_line_chart(data, title, figsize=figsize, dpi=args.dpi)
elif args.chart_type in ["bar", "barh"]:
if args.data:
data = json.loads(args.data)
else:
# Use risk metrics CLI
data = run_cli_tool(
"src/analysis/risk_metrics_cli.py", [args.ticker, "--days", "252"]
)
title = args.title or f"{args.ticker} - Risk Metrics"
horizontal = args.chart_type == "barh"
buffer = create_bar_chart(
data, title, horizontal=horizontal, figsize=figsize, dpi=args.dpi
)
elif args.chart_type == "heatmap":
tickers = args.tickers.split(",") if args.tickers else [args.ticker]
# Use correlation CLI
data = run_cli_tool(
"src/analysis/correlation_cli.py", tickers + ["--days", "252"]
)
title = args.title or "Correlation Matrix"
buffer = create_heatmap(
data.get("matrix", []),
data.get("tickers", tickers),
title=title,
figsize=figsize,
dpi=args.dpi,
)
elif args.chart_type == "technical":
if args.data:
data = json.loads(args.data)
else:
data = run_cli_tool(
"src/utils/momentum_cli.py", [args.ticker, "--days", str(args.days)]
)
title = args.title or f"{args.ticker} - Technical Analysis"
buffer = create_technical_chart(
price_data=data.get("prices", []),
indicator_data=data.get("rsi", []),
dates=data.get("dates", []),
thresholds=[30, 70],
title=title,
figsize=figsize,
dpi=args.dpi,
)
else:
print(f"Unknown chart type: {args.chart_type}")
sys.exit(1)
# Save chart
save_chart(buffer, args.output)
if __name__ == "__main__":
main()
ReportGenerator Help
PDF report builder CLI for Finance Guru analysis reports.
Usage
uv run python ReportGenerator.py --ticker TICKER [OPTIONS]Required Arguments
| Argument | Description |
|---|---|
--ticker | Stock ticker symbol to analyze |
Optional Arguments
| Argument | Default | Description |
|---|---|---|
--portfolio-value | 250000 | Portfolio value for position sizing |
--output-dir | fin-guru-private/fin-guru/analysis/reports | Output directory |
Examples
Basic Report
uv run python ReportGenerator.py --ticker TSLACustom Portfolio Value
uv run python ReportGenerator.py --ticker PLTR --portfolio-value 500000Custom Output Directory
uv run python ReportGenerator.py --ticker NVDA --output-dir ./custom-reports/Report Structure (8-10 Pages)
1. Cover Page (VGT-style)
- FINANCE GURU brand header
- Analyst team listing
- Current price + YTD performance
2. Executive Summary
- Investment thesis
- Key findings
- Verdict box (rating, conviction, risk)
3. Quantitative Analysis
- Risk metrics (Sharpe, Sortino, Beta, Alpha)
- Momentum indicators (RSI, MACD)
- Volatility assessment
4. Portfolio Sizing
- Percentage allocation
- Dollar amount (based on portfolio value)
- Share count
5. Market Sentiment
- Analyst ratings
- 2026 catalysts
- Key risks
6. Disclaimer
- Compliance text
- Generation timestamp
Output
Reports are saved as PDF files with naming convention: {TICKER}-analysis-{YYYY-MM-DD}.pdf
Integration
ReportGenerator is designed to be called from workflow scripts or subagents. For programmatic use:
from ReportGenerator import FinanceGuruReport
report = FinanceGuruReport(ticker="TSLA", portfolio_value=250000)
report.add_cover_page(title="...", ...)
report.add_executive_summary(thesis="...", ...)
report.build()#!/usr/bin/env python3
"""ReportGenerator - Finance Guru PDF Report Builder.
Generates institutional-quality 8-10 page PDF analysis reports.
Follows VGT header style and AMZN content depth.
Usage:
uv run python ReportGenerator.py --ticker TSLA --portfolio-value 250000
uv run python ReportGenerator.py --ticker PLTR --output-dir ./reports/
"""
import argparse
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
# Add project root to path for imports
PROJECT_ROOT = Path(__file__).resolve().parents[4] # Go up from tools/ to project root
sys.path.insert(0, str(PROJECT_ROOT))
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import (
HRFlowable,
PageBreak,
Paragraph,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
)
# Finance Guru Brand Colors
NAVY = colors.HexColor("#1a365d")
GOLD = colors.HexColor("#d69e2e")
GREEN = colors.HexColor("#38a169")
RED = colors.HexColor("#e53e3e")
LIGHT_GRAY = colors.HexColor("#f7fafc")
DARK_GRAY = colors.HexColor("#2d3748")
class FinanceGuruStyles:
"""Centralized style management for Finance Guru reports."""
def __init__(self):
self.styles = getSampleStyleSheet()
self._create_custom_styles()
def _create_custom_styles(self):
"""Create all custom paragraph styles."""
# Brand Title
self.styles.add(
ParagraphStyle(
name="BrandTitle",
parent=self.styles["Heading1"],
fontSize=24,
textColor=NAVY,
spaceAfter=6,
alignment=TA_CENTER,
fontName="Helvetica-Bold",
)
)
# Gold Subtitle
self.styles.add(
ParagraphStyle(
name="GoldSubtitle",
parent=self.styles["Normal"],
fontSize=14,
textColor=GOLD,
spaceAfter=12,
alignment=TA_CENTER,
fontName="Helvetica",
)
)
# Section Header
self.styles.add(
ParagraphStyle(
name="SectionHeader",
parent=self.styles["Heading2"],
fontSize=16,
textColor=NAVY,
spaceBefore=16,
spaceAfter=10,
fontName="Helvetica-Bold",
)
)
# Subsection Header
self.styles.add(
ParagraphStyle(
name="SubHeader",
parent=self.styles["Heading3"],
fontSize=12,
textColor=NAVY,
spaceBefore=10,
spaceAfter=6,
fontName="Helvetica-Bold",
)
)
# Body Text
self.styles.add(
ParagraphStyle(
name="ReportBody",
parent=self.styles["Normal"],
fontSize=10,
textColor=DARK_GRAY,
spaceAfter=6,
alignment=TA_JUSTIFY,
fontName="Helvetica",
)
)
# Bullet Point
self.styles.add(
ParagraphStyle(
name="BulletPoint",
parent=self.styles["Normal"],
fontSize=10,
textColor=DARK_GRAY,
leftIndent=20,
spaceAfter=4,
fontName="Helvetica",
)
)
# Disclaimer
self.styles.add(
ParagraphStyle(
name="Disclaimer",
parent=self.styles["Normal"],
fontSize=8,
textColor=DARK_GRAY,
alignment=TA_CENTER,
spaceAfter=6,
fontName="Helvetica-Oblique",
)
)
# Table Cell - for text that needs to wrap inside table cells
self.styles.add(
ParagraphStyle(
name="TableCell",
parent=self.styles["Normal"],
fontSize=9,
textColor=DARK_GRAY,
spaceAfter=0,
spaceBefore=0,
alignment=TA_LEFT,
fontName="Helvetica",
wordWrap="CJK", # Enables better word wrapping
)
)
# Table Header Cell - bold text for table headers
self.styles.add(
ParagraphStyle(
name="TableHeaderCell",
parent=self.styles["Normal"],
fontSize=10,
textColor=colors.white,
spaceAfter=0,
spaceBefore=0,
alignment=TA_LEFT,
fontName="Helvetica-Bold",
wordWrap="CJK",
)
)
def get(self, name: str) -> ParagraphStyle:
"""Get a style by name."""
return self.styles[name]
class FinanceGuruReport:
"""Main report builder class for Finance Guru PDF reports."""
def __init__(
self,
ticker: str,
portfolio_value: float = 250000,
output_dir: str = "fin-guru-private/fin-guru/analysis/reports",
):
self.ticker = ticker
self.portfolio_value = portfolio_value
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.date = datetime.now().strftime("%Y-%m-%d")
self.date_display = datetime.now().strftime("%B %d, %Y")
self.styles = FinanceGuruStyles()
self.story = []
# Report data (populated during build)
self.data = {}
def _wrap_cell_text(self, text: str, is_header: bool = False) -> Paragraph:
"""Wrap text in a Paragraph for proper table cell wrapping.
CRITICAL: Plain strings in ReportLab tables DO NOT wrap.
All table cell content must be wrapped in Paragraph objects.
"""
style = (
self.styles.get("TableHeaderCell")
if is_header
else self.styles.get("TableCell")
)
# Handle None values
if text is None:
text = "N/A"
return Paragraph(str(text), style)
def _create_table(
self,
data: list[list[str]],
col_widths: list[float] | None = None,
has_header: bool = True,
) -> Table:
"""Create a styled table with proper text wrapping.
CRITICAL: All cell content is wrapped in Paragraph objects to ensure
text wraps within cells instead of overflowing.
Args:
data: 2D list of cell values (strings or Paragraph objects)
col_widths: Explicit column widths (REQUIRED for proper wrapping)
has_header: Whether first row is a header row
"""
# Wrap all cell content in Paragraph objects for proper text wrapping
wrapped_data = []
for row_idx, row in enumerate(data):
wrapped_row = []
for cell in row:
# Skip if already a Paragraph or other flowable
if hasattr(cell, "wrap"):
wrapped_row.append(cell)
else:
is_header = has_header and row_idx == 0
wrapped_row.append(self._wrap_cell_text(cell, is_header))
wrapped_data.append(wrapped_row)
table = Table(wrapped_data, colWidths=col_widths)
style_commands = [
("ALIGN", (0, 0), (-1, -1), "LEFT"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("GRID", (0, 0), (-1, -1), 0.5, DARK_GRAY),
]
if has_header:
style_commands.extend(
[
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("BOTTOMPADDING", (0, 0), (-1, 0), 10),
("TOPPADDING", (0, 0), (-1, 0), 10),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, LIGHT_GRAY]),
]
)
else:
style_commands.append(
("ROWBACKGROUNDS", (0, 0), (-1, -1), [colors.white, LIGHT_GRAY])
)
table.setStyle(TableStyle(style_commands))
return table
def _create_verdict_box(
self, rating: str, conviction: str, risk_level: str
) -> Table:
"""Create a styled verdict box with proper column widths.
CRITICAL: Column widths must accommodate text at specified font sizes.
"INVESTMENT RATING" at 14pt bold needs ~2.5" minimum.
"""
data = [
["INVESTMENT RATING", rating.upper()],
["Conviction", conviction],
["Risk Level", risk_level],
]
# Color based on rating
if "BUY" in rating.upper():
header_color = GREEN
elif "SELL" in rating.upper():
header_color = RED
else:
header_color = GOLD
# Column widths: 2.8" + 4.2" = 7" (fits in 7.5" content area)
# First column needs 2.8" to fit "INVESTMENT RATING" at 14pt bold
table = Table(data, colWidths=[2.8 * inch, 4.2 * inch])
table.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), header_color),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, 0), 14),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 12),
("BOTTOMPADDING", (0, 0), (-1, -1), 12),
("LEFTPADDING", (0, 0), (-1, -1), 10),
("RIGHTPADDING", (0, 0), (-1, -1), 10),
("GRID", (0, 0), (-1, -1), 1.5, NAVY),
("BACKGROUND", (0, 1), (-1, -1), colors.white),
("TEXTCOLOR", (0, 1), (-1, -1), DARK_GRAY),
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
("FONTSIZE", (0, 1), (-1, -1), 11),
]
)
)
return table
def add_cover_page(
self,
title: str,
subtitle: str,
current_price: float,
ytd_performance: float,
analyst_team: list[str] = None,
week_52_range: str = None,
market_cap: str = None,
expense_ratio: float = None,
):
"""Add GOOG-style cover page with proper table formatting.
STANDARD FORMAT (matching GOOG example - Image #1):
- Navy header row with "Report Date:" label and date value
- Clean data rows with borders for each field
- Analyst names listed WITHOUT bullet points, one per line
- No "Finance Guru Multi-Agent System" header
"""
# Default analyst team - names with roles (matching GOOG format)
if analyst_team is None:
analyst_team = [
"Dr. Aleksandr Petrov (Market Research)",
"Dr. Priya Desai (Quantitative Analysis)",
"Elena Rodriguez-Park (Strategy)",
]
# Brand header
self.story.append(Spacer(1, 0.3 * inch))
self.story.append(Paragraph("FINANCE GURU™", self.styles.get("BrandTitle")))
self.story.append(
Paragraph(
"Family Office Investment Analysis", self.styles.get("GoldSubtitle")
)
)
self.story.append(HRFlowable(width="100%", thickness=2, color=NAVY))
self.story.append(Spacer(1, 0.2 * inch))
# Report title - ticker prominently displayed
self.story.append(
Paragraph(
f"<b>{self.ticker}</b> - {title}", self.styles.get("SectionHeader")
)
)
self.story.append(Paragraph(subtitle, self.styles.get("ReportBody")))
self.story.append(Spacer(1, 0.3 * inch))
# Create analyst team text (no bullets, just line breaks)
team_paragraph = Paragraph(
"<br/>".join(analyst_team), self.styles.get("TableCell")
)
# Build table data - HEADER ROW FIRST (navy background)
key_info = [
["Report Date:", self.date] # Header row
]
# Data rows
key_info.append(["Analyst Team:", team_paragraph])
key_info.append(["Current Price:", f"${current_price:,.2f}"])
if week_52_range:
key_info.append(["52-Week Range:", week_52_range])
key_info.append(["YTD Performance:", f"{ytd_performance:+.1f}%"])
if market_cap:
key_info.append(["Market Cap:", market_cap])
if expense_ratio is not None:
key_info.append(["Expense Ratio:", f"{expense_ratio:.2f}%"])
# Create table with GOOG-style formatting
info_table = Table(key_info, colWidths=[2.5 * inch, 4.5 * inch])
# Style the table like GOOG example
info_table.setStyle(
TableStyle(
[
# Header row (first row) - Navy background, white bold text
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, 0), 10),
# All rows styling
("ALIGN", (0, 0), (0, -1), "LEFT"),
("ALIGN", (1, 0), (1, -1), "LEFT"),
("FONTNAME", (0, 1), (0, -1), "Helvetica"),
("FONTSIZE", (0, 1), (-1, -1), 10),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
("LEFTPADDING", (0, 0), (-1, -1), 10),
("RIGHTPADDING", (0, 0), (-1, -1), 10),
("VALIGN", (0, 0), (-1, -1), "TOP"),
# Grid lines
("GRID", (0, 0), (-1, -1), 0.5, DARK_GRAY),
# Data rows - white background
("BACKGROUND", (0, 1), (-1, -1), colors.white),
]
)
)
self.story.append(info_table)
self.story.append(Spacer(1, 0.3 * inch))
def add_executive_summary(
self,
thesis: str,
key_findings: list[dict[str, str]],
rating: str,
conviction: str,
risk_level: str,
):
"""Add executive summary section."""
self.story.append(
Paragraph("EXECUTIVE SUMMARY", self.styles.get("SectionHeader"))
)
self.story.append(HRFlowable(width="80%", thickness=1, color=GOLD))
self.story.append(Spacer(1, 0.15 * inch))
# Investment thesis
self.story.append(
Paragraph("<b>Investment Thesis</b>", self.styles.get("SubHeader"))
)
self.story.append(Paragraph(thesis, self.styles.get("ReportBody")))
self.story.append(Spacer(1, 0.15 * inch))
# Key findings
self.story.append(
Paragraph("<b>Key Findings</b>", self.styles.get("SubHeader"))
)
for finding in key_findings:
label = finding.get("label", "")
detail = finding.get("detail", "")
self.story.append(
Paragraph(f"• <b>{label}:</b> {detail}", self.styles.get("BulletPoint"))
)
self.story.append(Spacer(1, 0.2 * inch))
# Verdict box
self.story.append(self._create_verdict_box(rating, conviction, risk_level))
self.story.append(PageBreak())
def add_quant_analysis(
self,
risk_metrics: dict[str, Any],
momentum_data: dict[str, Any],
volatility_data: dict[str, Any],
):
"""Add quantitative analysis section."""
self.story.append(
Paragraph("QUANTITATIVE ANALYSIS", self.styles.get("SectionHeader"))
)
self.story.append(HRFlowable(width="80%", thickness=1, color=GOLD))
self.story.append(Spacer(1, 0.15 * inch))
# Risk Metrics Table
self.story.append(
Paragraph(
"<b>Risk & Performance Metrics (252-Day)</b>",
self.styles.get("SubHeader"),
)
)
risk_table_data = [
["Metric", "Value", "Benchmark", "Assessment"],
[
"Sharpe Ratio",
str(risk_metrics.get("sharpe", "N/A")),
str(risk_metrics.get("benchmark_sharpe", "1.0")),
self._assess_sharpe(risk_metrics.get("sharpe")),
],
["Sortino Ratio", str(risk_metrics.get("sortino", "N/A")), "-", "-"],
[
"Beta",
str(risk_metrics.get("beta", "N/A")),
"1.0",
self._assess_beta(risk_metrics.get("beta")),
],
["Alpha", str(risk_metrics.get("alpha", "N/A")), "0%", "-"],
["Max Drawdown", str(risk_metrics.get("max_drawdown", "N/A")), "-", "-"],
["VaR (95%)", str(risk_metrics.get("var_95", "N/A")), "-", "-"],
]
self.story.append(
self._create_table(
risk_table_data, [1.5 * inch, 1.3 * inch, 1.3 * inch, 2.4 * inch]
)
)
self.story.append(Spacer(1, 0.2 * inch))
# Momentum Indicators
self.story.append(
Paragraph(
"<b>Momentum Indicators (90-Day)</b>", self.styles.get("SubHeader")
)
)
momentum_table_data = [
["Indicator", "Value", "Signal"],
[
"RSI (14)",
str(momentum_data.get("rsi", "N/A")),
self._assess_rsi(momentum_data.get("rsi")),
],
[
"MACD",
str(momentum_data.get("macd", "N/A")),
momentum_data.get("macd_signal", "-"),
],
["Stochastic %K", str(momentum_data.get("stochastic_k", "N/A")), "-"],
["Williams %R", str(momentum_data.get("williams_r", "N/A")), "-"],
]
self.story.append(
self._create_table(momentum_table_data, [2 * inch, 2 * inch, 2.5 * inch])
)
self.story.append(Spacer(1, 0.2 * inch))
# Volatility Assessment
self.story.append(
Paragraph("<b>Volatility Assessment</b>", self.styles.get("SubHeader"))
)
vol_table_data = [
["Metric", "Value"],
[
"Annualized Volatility",
str(volatility_data.get("annualized_vol", "N/A")),
],
["ATR (14)", str(volatility_data.get("atr", "N/A"))],
["Bollinger Band Width", str(volatility_data.get("bb_width", "N/A"))],
["Volatility Regime", volatility_data.get("regime", "Normal")],
]
self.story.append(self._create_table(vol_table_data, [3 * inch, 3.5 * inch]))
self.story.append(PageBreak())
def add_portfolio_sizing(
self, recommended_pct: float, current_price: float, entry_strategy: str
):
"""Add portfolio sizing section with actual dollar amounts."""
self.story.append(
Paragraph("PORTFOLIO SIZING", self.styles.get("SectionHeader"))
)
self.story.append(HRFlowable(width="80%", thickness=1, color=GOLD))
self.story.append(Spacer(1, 0.15 * inch))
# Calculate sizing
min_pct = recommended_pct - 0.5
max_pct = recommended_pct + 0.5
min_amount = self.portfolio_value * (min_pct / 100)
max_amount = self.portfolio_value * (max_pct / 100)
min_shares = int(min_amount / current_price)
max_shares = int(max_amount / current_price)
self.story.append(
Paragraph(
f"Based on your portfolio value of <b>${self.portfolio_value:,.0f}</b>:",
self.styles.get("ReportBody"),
)
)
self.story.append(Spacer(1, 0.1 * inch))
sizing_data = [
["Parameter", "Value"],
["Recommended Allocation", f"{min_pct:.1f}% - {max_pct:.1f}%"],
["Dollar Amount", f"${min_amount:,.0f} - ${max_amount:,.0f}"],
["Share Count", f"{min_shares} - {max_shares} shares"],
["Current Price", f"${current_price:,.2f}"],
]
self.story.append(self._create_table(sizing_data, [3 * inch, 3.5 * inch]))
self.story.append(Spacer(1, 0.2 * inch))
# Entry Strategy
self.story.append(
Paragraph("<b>Entry Strategy</b>", self.styles.get("SubHeader"))
)
self.story.append(Paragraph(entry_strategy, self.styles.get("ReportBody")))
self.story.append(Spacer(1, 0.2 * inch))
def add_sentiment_section(
self,
sentiment_summary: str,
analyst_ratings: dict[str, Any],
catalysts: list[str],
risks: list[str],
):
"""Add market sentiment section."""
self.story.append(
Paragraph("MARKET SENTIMENT & RESEARCH", self.styles.get("SectionHeader"))
)
self.story.append(HRFlowable(width="80%", thickness=1, color=GOLD))
self.story.append(Spacer(1, 0.15 * inch))
# Sentiment Summary
self.story.append(Paragraph(sentiment_summary, self.styles.get("ReportBody")))
self.story.append(Spacer(1, 0.15 * inch))
# Analyst Ratings
if analyst_ratings:
self.story.append(
Paragraph("<b>Analyst Consensus</b>", self.styles.get("SubHeader"))
)
ratings_data = [
["Rating", "Count"],
["Buy", str(analyst_ratings.get("buy", 0))],
["Hold", str(analyst_ratings.get("hold", 0))],
["Sell", str(analyst_ratings.get("sell", 0))],
["Average Target", f"${analyst_ratings.get('target', 0):,.2f}"],
]
self.story.append(self._create_table(ratings_data, [2 * inch, 2 * inch]))
self.story.append(Spacer(1, 0.15 * inch))
# 2026 Catalysts
self.story.append(
Paragraph("<b>2026 Catalysts</b>", self.styles.get("SubHeader"))
)
for catalyst in catalysts:
self.story.append(
Paragraph(f"• {catalyst}", self.styles.get("BulletPoint"))
)
self.story.append(Spacer(1, 0.15 * inch))
# Key Risks
self.story.append(Paragraph("<b>Key Risks</b>", self.styles.get("SubHeader")))
for risk in risks:
self.story.append(Paragraph(f"• {risk}", self.styles.get("BulletPoint")))
# No PageBreak here - let disclaimer flow naturally on same page if space allows
def add_disclaimer(self):
"""Add compliance disclaimer with 'Powered by Finance Guru™' branding.
FORMAT (matching user preference from FTNT analysis):
- Horizontal rule separator
- Disclaimer text (educational purposes only)
- 'Powered by Finance Guru™' branding line
- Report date
"""
self.story.append(Spacer(1, 0.3 * inch))
self.story.append(HRFlowable(width="100%", thickness=1, color=DARK_GRAY))
self.story.append(Spacer(1, 0.15 * inch))
disclaimer = """
<b>DISCLAIMER:</b> This analysis is provided for educational and informational
purposes only. It does not constitute investment advice, financial advice,
trading advice, or any other sort of advice. Finance Guru is a personal
family office system and does not provide recommendations to third parties.
Past performance is not indicative of future results. All investments
involve risk, including the possible loss of principal. Consult with a
qualified financial professional before making any investment decisions.
"""
self.story.append(Paragraph(disclaimer.strip(), self.styles.get("Disclaimer")))
self.story.append(Spacer(1, 0.15 * inch))
# "Powered by Finance Guru™" branding (user preferred format)
powered_by_style = ParagraphStyle(
name="PoweredBy",
parent=self.styles.get("Disclaimer"),
fontSize=9,
fontName="Helvetica-Bold",
textColor=NAVY,
alignment=TA_CENTER,
spaceAfter=4,
)
self.story.append(Paragraph("Powered by Finance Guru™", powered_by_style))
self.story.append(
Paragraph(
f"Report Date: {self.date_display}", self.styles.get("Disclaimer")
)
)
def build(self) -> str:
"""Build the PDF and return the output path."""
output_file = self.output_dir / f"{self.ticker}-analysis-{self.date}.pdf"
doc = SimpleDocTemplate(
str(output_file),
pagesize=letter,
rightMargin=0.5 * inch,
leftMargin=0.5 * inch,
topMargin=0.5 * inch,
bottomMargin=0.5 * inch,
)
# Add disclaimer at end if not already added
if not any("DISCLAIMER" in str(item) for item in self.story):
self.add_disclaimer()
doc.build(self.story)
print(f"Report generated: {output_file}")
return str(output_file)
# Helper methods for assessments
def _assess_sharpe(self, sharpe) -> str:
if sharpe is None:
return "N/A"
try:
s = float(sharpe)
if s > 2.0:
return "Excellent"
elif s > 1.0:
return "Good"
elif s > 0:
return "Moderate"
else:
return "Poor"
except (TypeError, ValueError):
return "N/A"
def _assess_beta(self, beta) -> str:
if beta is None:
return "N/A"
try:
b = float(beta)
if b > 1.5:
return "High volatility vs market"
elif b > 1.0:
return "Slightly more volatile"
elif b > 0.5:
return "Less volatile than market"
else:
return "Low correlation"
except (TypeError, ValueError):
return "N/A"
def _assess_rsi(self, rsi) -> str:
if rsi is None:
return "N/A"
try:
r = float(rsi)
if r > 70:
return "Overbought"
elif r < 30:
return "Oversold"
else:
return "Neutral"
except (TypeError, ValueError):
return "N/A"
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="ReportGenerator - Finance Guru PDF Report Builder",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Generate report with default portfolio:
uv run python ReportGenerator.py --ticker TSLA
Generate with custom portfolio value:
uv run python ReportGenerator.py --ticker PLTR --portfolio-value 500000
Specify output directory:
uv run python ReportGenerator.py --ticker NVDA --output-dir ./custom-reports/
""",
)
parser.add_argument("--ticker", type=str, required=True, help="Stock ticker symbol")
parser.add_argument(
"--portfolio-value",
type=float,
default=250000,
help="Portfolio value for sizing (default: 250000)",
)
parser.add_argument(
"--output-dir",
type=str,
default="fin-guru-private/fin-guru/analysis/reports",
help="Output directory for PDF",
)
args = parser.parse_args()
# Fetch real-time price data using market_data module
print(f"Fetching real-time price data for {args.ticker}...")
try:
from src.utils.market_data import get_prices
price_data = get_prices(args.ticker, realtime=True)
if args.ticker in price_data:
current_price = price_data[args.ticker].price
change_percent = price_data[args.ticker].change_percent
print(f" ✓ {args.ticker}: ${current_price:.2f} ({change_percent:+.2f}%)")
else:
print(f" ⚠ Could not fetch price for {args.ticker}, using fallback")
current_price = 100.00
change_percent = 0.0
except Exception as e:
print(f" ⚠ Price fetch failed: {e}, using fallback")
current_price = 100.00
change_percent = 0.0
# Create report
report = FinanceGuruReport(
ticker=args.ticker,
portfolio_value=args.portfolio_value,
output_dir=args.output_dir,
)
# Add sections with REAL price data
report.add_cover_page(
title=f"{args.ticker} Comprehensive Analysis",
subtitle="2026 Watchlist Analysis & Investment Recommendation",
current_price=current_price,
ytd_performance=change_percent, # Using daily change as proxy for now
)
report.add_executive_summary(
thesis=f"{args.ticker} presents a compelling investment opportunity based on quantitative analysis...",
key_findings=[
{
"label": "Risk Profile",
"detail": "Moderate risk with favorable risk-adjusted returns",
},
{
"label": "Technical Setup",
"detail": "Momentum indicators suggest bullish trend",
},
{
"label": "Valuation",
"detail": "Trading near fair value with upside potential",
},
],
rating="CONDITIONAL BUY",
conviction="7/10",
risk_level="MEDIUM",
)
report.add_quant_analysis(
risk_metrics={
"sharpe": "1.45",
"sortino": "2.1",
"beta": "1.2",
"alpha": "5%",
"max_drawdown": "-18%",
"var_95": "-3.2%",
},
momentum_data={
"rsi": "55",
"macd": "2.3",
"macd_signal": "Bullish",
"stochastic_k": "65",
"williams_r": "-35",
},
volatility_data={
"annualized_vol": "28%",
"atr": "4.5",
"bb_width": "15%",
"regime": "Normal",
},
)
report.add_portfolio_sizing(
recommended_pct=2.5,
current_price=current_price, # Using real-time price
entry_strategy="Scale in with 3 tranches: 40% at current price, 30% on 5% pullback, 30% on 10% pullback",
)
report.add_sentiment_section(
sentiment_summary="Market sentiment is cautiously optimistic with institutional accumulation observed.",
analyst_ratings={"buy": 15, "hold": 8, "sell": 2, "target": 125.00},
catalysts=[
"AI infrastructure expansion driving revenue growth",
"New product launches scheduled for Q2 2026",
"Strong cash flow enabling share buybacks",
],
risks=[
"Valuation stretched relative to historical averages",
"Regulatory headwinds in key markets",
"Competition intensifying in core segments",
],
)
output_path = report.build()
print(f"\nReport successfully generated at: {output_path}")
if __name__ == "__main__":
main()
VisGuide - Chart Selection & Visualization Standards
Guide for selecting appropriate chart types in Finance Guru reports.
Chart Selection Dictionary
| Data Question | Chart Type | When to Use |
|---|---|---|
| How has price changed over time? | Line Chart | Price history, trend visualization |
| Compare values across categories | Horizontal Bar | Risk metrics comparison, sector weights |
| Show distribution of values | Vertical Bar | Monthly returns, volume |
| Relationship between two variables | Scatter Plot | Risk vs return, correlation |
| Part-to-whole relationship | Pie/Donut | Portfolio allocation, sector breakdown |
| Show correlation matrix | Heatmap | Multi-asset correlation |
| Cumulative changes | Waterfall | Return attribution, P&L breakdown |
| Technical indicators | Multi-line | RSI, MACD with price overlay |
Chart Type Specifications
Line Chart (Price History)
# Use for: Price trends, moving averages
chart_config = {
'type': 'line',
'figsize': (7, 3),
'dpi': 150,
'color': NAVY,
'linewidth': 1.5,
'grid': True,
'grid_alpha': 0.3
}When to use:
- Showing price movement over 90-252 days
- Comparing actual vs benchmark
- Moving average crossovers
Horizontal Bar (Comparison)
# Use for: Comparing metrics across categories
chart_config = {
'type': 'barh',
'figsize': (7, 3),
'colors': [NAVY, GREEN, GOLD],
'edge_color': 'none'
}When to use:
- Risk metrics comparison (Sharpe, Sortino, Beta)
- Sector weights
- Peer comparison
Heatmap (Correlation)
# Use for: Correlation matrices
chart_config = {
'type': 'heatmap',
'figsize': (6, 5),
'cmap': 'RdYlGn', # Red=negative, Yellow=neutral, Green=positive
'annot': True,
'fmt': '.2f',
'vmin': -1,
'vmax': 1
}When to use:
- Portfolio correlation analysis
- Sector interdependence
- Factor exposure
Technical Indicator Chart
# Use for: RSI, MACD overlays
chart_config = {
'type': 'multi_axis',
'figsize': (7, 4),
'primary': {'color': NAVY, 'label': 'Price'},
'secondary': {'color': GOLD, 'label': 'RSI'},
'threshold_lines': [30, 70] # Overbought/oversold
}When to use:
- RSI with price
- MACD signal crossovers
- Bollinger Band visualization
Color Coding Standards
Signal Colors
| Signal | Color | Hex | Use Case |
|---|---|---|---|
| Positive/Buy | Green | #38a169 | Gains, uptrends, buy signals |
| Negative/Sell | Red | #e53e3e | Losses, downtrends, sell signals |
| Neutral/Hold | Gold | #d69e2e | Sideways, mixed signals |
| Primary | Navy | #1a365d | Default chart elements |
Gradient Scales
# For heatmaps and intensity charts
positive_scale = ['#c6f6d5', '#68d391', '#38a169', '#276749'] # Light to dark green
negative_scale = ['#fed7d7', '#fc8181', '#e53e3e', '#c53030'] # Light to dark red
neutral_scale = ['#edf2f7', '#a0aec0', '#718096', '#4a5568'] # Light to dark grayLabeling Standards
Chart Title
- Font: Helvetica-Bold
- Size: 11pt
- Position: Above chart, left-aligned
- Example: "90-Day Price Performance vs SPY"
Axis Labels
- Font: Helvetica
- Size: 9pt
- X-axis: Date format "MMM 'YY" (e.g., "Dec '25")
- Y-axis: Numeric with appropriate units ($, %, bp)
Legend
- Position: Upper right or lower right (not covering data)
- Font: Helvetica, 8pt
- Box: Light gray background, thin border
Annotations
- Font: Helvetica, 8pt
- Use sparingly for key events
- Arrow pointing to data point
Chart Embedding in PDF
Using matplotlib + ReportLab
import matplotlib.pyplot as plt
from io import BytesIO
from reportlab.platypus import Image
def create_embedded_chart(data, chart_type, title):
"""Generate chart and return as ReportLab Image."""
fig, ax = plt.subplots(figsize=(7, 3), dpi=150)
# Plot data based on chart_type
if chart_type == 'line':
ax.plot(data['dates'], data['values'], color='#1a365d', linewidth=1.5)
elif chart_type == 'barh':
ax.barh(data['labels'], data['values'], color='#1a365d')
ax.set_title(title, fontsize=11, fontweight='bold', loc='left')
ax.grid(True, alpha=0.3)
plt.tight_layout()
# Save to buffer
buffer = BytesIO()
fig.savefig(buffer, format='png', bbox_inches='tight')
buffer.seek(0)
plt.close(fig)
# Return as ReportLab Image
return Image(buffer, width=6.5*inch, height=2.5*inch)Chart Sizing Guidelines
| Chart Purpose | Width | Height |
|---|---|---|
| Full-width main chart | 6.5" | 3.0" |
| Half-width comparison | 3.0" | 2.5" |
| Small indicator | 2.5" | 1.5" |
| Correlation heatmap | 5.0" | 4.5" |
Data Source Integration
From Finance Guru CLI Tools
import subprocess
import json
def get_chart_data(ticker, tool, days=90):
"""Run CLI tool and parse JSON output for charting."""
result = subprocess.run(
['uv', 'run', 'python', f'src/analysis/{tool}.py', ticker, '--days', str(days), '--output', 'json'],
capture_output=True, text=True
)
return json.loads(result.stdout)
# Example usage
risk_data = get_chart_data('TSLA', 'risk_metrics_cli', 252)
momentum_data = get_chart_data('TSLA', 'momentum_cli', 90)Accessibility Notes
- Always include alt-text description for charts
- Use patterns in addition to colors for colorblind accessibility
- Ensure sufficient contrast (WCAG AA: 4.5:1 ratio)
- Provide data tables as fallback for complex visualizations
FullResearchWorkflow
Comprehensive 4-phase analysis workflow for thorough ticker research.
Trigger Phrases
- "full analysis for AMZN"
- "thorough research on VGT"
- "deep dive into NVDA"
- "comprehensive analysis"
Workflow Overview
┌─────────────────────────────────────────────────────────────────┐
│ PHASE 1: MARKET RESEARCH │
│ └─ Perplexity/Exa → Company overview, catalysts, risks │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 2: QUANTITATIVE ANALYSIS │
│ └─ CLI Tools → Risk metrics, momentum, volatility, correlation │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 3: STRATEGY FORMULATION │
│ └─ Synthesis → Buy/Hold/Sell, position sizing, entry strategy │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 4: REPORT GENERATION │
│ └─ ReportGenerator → 8-10 page PDF with all findings │
└─────────────────────────────────────────────────────────────────┘Phase 1: Market Research
1.1 Company Overview
# Use Perplexity for comprehensive research
mcp__perplexity__search(
query=f"{TICKER} company overview business model market position 2025"
)1.2 2026 Catalysts
mcp__perplexity__reason(
query=f"What are the key growth catalysts for {TICKER} in 2026? Consider product launches, market expansion, regulatory changes, and competitive dynamics."
)1.3 Risk Assessment
mcp__perplexity__search(
query=f"{TICKER} stock risks challenges headwinds 2025 2026"
)1.4 Analyst Ratings
mcp__perplexity__search(
query=f"{TICKER} analyst ratings price targets buy sell hold consensus"
)Phase 2: Quantitative Analysis
2.1 Risk Metrics (252-Day)
uv run python src/analysis/risk_metrics_cli.py {TICKER} --days 252 --benchmark SPYMetrics Extracted:
- Sharpe Ratio (risk-adjusted return)
- Sortino Ratio (downside-adjusted)
- Beta (market correlation)
- Alpha (excess return)
- Max Drawdown (worst peak-to-trough)
- VaR 95% (value at risk)
- CVaR (conditional VaR)
2.2 Momentum Indicators (90-Day)
uv run python src/utils/momentum_cli.py {TICKER} --days 90Metrics Extracted:
- RSI (14-period)
- MACD (12/26/9)
- Stochastic %K/%D
- Williams %R
- ROC (rate of change)
2.3 Volatility Assessment
uv run python src/utils/volatility_cli.py {TICKER} --days 90Metrics Extracted:
- Annualized Volatility
- ATR (14-period)
- Bollinger Band Width
- Volatility Regime (High/Normal/Low)
2.4 Portfolio Correlation (Optional)
# Against existing holdings
uv run python src/analysis/correlation_cli.py {TICKER} PLTR TSLA NVDA VOO --days 252Phase 3: Strategy Formulation
3.1 Synthesize Findings
Create investment thesis integrating:
- Fundamental outlook (from research)
- Technical setup (from quant)
- Risk/reward profile
3.2 Rating Decision Matrix
| Factor | Weight | Criteria |
|---|---|---|
| Sharpe Ratio | 20% | >1.5 = Strong, >1.0 = Good, <0.5 = Weak |
| Momentum | 20% | RSI 40-60 = Neutral, <30 = Oversold, >70 = Overbought |
| Volatility | 15% | <25% = Low, 25-40% = Normal, >40% = High |
| Catalysts | 25% | Quality and timing of growth drivers |
| Valuation | 20% | Relative to peers and historical |
3.3 Rating Output
| Rating | Criteria |
|---|---|
| STRONG BUY | Score >80%, multiple catalysts, favorable technicals |
| BUY | Score 65-80%, solid fundamentals |
| CONDITIONAL BUY | Score 50-65%, with specific conditions |
| HOLD | Score 40-50%, wait for better entry |
| SELL | Score <40%, deteriorating fundamentals |
3.4 Position Sizing
# Based on $250k portfolio and conviction level
SIZING_MATRIX = {
"STRONG BUY": 3.0, # 3% = $7,500
"BUY": 2.5, # 2.5% = $6,250
"CONDITIONAL BUY": 2.0, # 2% = $5,000
"HOLD": 0, # No new position
"SELL": 0 # Exit existing
}3.5 Entry Strategy
Define entry approach:
- Scale-in: 3 tranches (40% now, 30% on 5% dip, 30% on 10% dip)
- Limit order: Wait for specific price target
- Immediate: Full position at market
Phase 4: Report Generation
4.1 Build Report Sections
from ReportGenerator import FinanceGuruReport
report = FinanceGuruReport(ticker=TICKER, portfolio_value=250000)
# Cover page (VGT-style)
report.add_cover_page(
title=f"{company_name}",
subtitle="2026 Watchlist Analysis & Investment Recommendation",
current_price=price,
ytd_performance=ytd_pct,
analyst_team=["Market Researcher", "Quant Analyst", "Strategy Advisor"]
)
# Executive Summary
report.add_executive_summary(
thesis=thesis_text,
key_findings=key_findings,
rating=rating,
conviction=conviction,
risk_level=risk_level
)
# Quantitative Analysis
report.add_quant_analysis(
risk_metrics=risk_data,
momentum_data=momentum_data,
volatility_data=vol_data
)
# Portfolio Sizing (with actual $ amounts)
report.add_portfolio_sizing(
recommended_pct=sizing_pct,
current_price=price,
entry_strategy=entry_text
)
# Market Sentiment
report.add_sentiment_section(
sentiment_summary=sentiment,
analyst_ratings=ratings,
catalysts=catalysts_list,
risks=risks_list
)
# Build PDF
output_path = report.build()4.2 Validate Output
- PDF exists and is 20-50KB
- All sections rendered
- No placeholder data
- Disclaimer included
Complete Example
User: "Do a full analysis on AMZN for the 2026 watchlist"
PHASE 1: Market Research
├─ Company: Amazon.com Inc, $2T market cap
├─ Business: AWS, Retail, Advertising
├─ Catalysts: AWS AI, advertising growth, margin expansion
└─ Risks: Antitrust, competition, labor costs
PHASE 2: Quant Analysis
├─ Risk: Sharpe 1.65, Beta 1.12, Max DD -18%
├─ Momentum: RSI 58, MACD Bullish
└─ Volatility: 28%, Normal regime
PHASE 3: Strategy
├─ Rating: STRONG BUY
├─ Conviction: 8.3/10
├─ Sizing: 3% = $7,500 (33 shares at $226)
└─ Entry: Scale-in over 2 weeks
PHASE 4: Report
└─ Output: fin-guru-private/fin-guru/analysis/reports/AMZN-analysis-2025-12-18.pdf (35KB)GenerateSingleReport Workflow
Generate a single ticker analysis PDF report.
Trigger Phrases
- "generate report for TSLA"
- "create PDF for PLTR"
- "make analysis report"
Prerequisites
- Ticker symbol
- Current market data available
- Finance Guru CLI tools functional
Workflow Steps
Step 1: Gather Quantitative Data
Run Finance Guru CLI tools to collect metrics:
# Risk metrics (252-day lookback)
uv run python src/analysis/risk_metrics_cli.py {TICKER} --days 252 --output json > /tmp/{TICKER}_risk.json
# Momentum indicators (90-day)
uv run python src/utils/momentum_cli.py {TICKER} --days 90 --output json > /tmp/{TICKER}_momentum.json
# Volatility assessment
uv run python src/utils/volatility_cli.py {TICKER} --days 90 --output json > /tmp/{TICKER}_volatility.jsonStep 2: Fetch Market Sentiment (Perplexity MCP)
# Search for analyst ratings and sentiment
mcp__perplexity__search(query=f"{TICKER} stock analyst ratings 2025")
# Get 2026 catalysts and risks
mcp__perplexity__reason(query=f"What are the key catalysts and risks for {TICKER} in 2026?")Step 3: Get Current Price Data
# Using yfinance via market_data utility
uv run python src/utils/market_data.py {TICKER}Step 4: Calculate Portfolio Sizing
Based on user profile ($250k default):
- Recommended allocation: 2-3% for moderate conviction
- Dollar amount: $5,000 - $7,500
- Share count: Calculate from current price
Step 5: Generate PDF Report
uv run python .claude/skills/FinanceReport/tools/ReportGenerator.py \
--ticker {TICKER} \
--portfolio-value 250000 \
--output-dir fin-guru-private/fin-guru/analysis/reports/Or programmatically build sections:
from ReportGenerator import FinanceGuruReport
report = FinanceGuruReport(ticker=TICKER, portfolio_value=250000)
report.add_cover_page(
title=f"{TICKER} Analysis",
subtitle="2026 Watchlist",
current_price=current_price,
ytd_performance=ytd_pct
)
report.add_executive_summary(
thesis=thesis_text,
key_findings=findings_list,
rating=rating,
conviction=conviction,
risk_level=risk
)
report.add_quant_analysis(risk_data, momentum_data, volatility_data)
report.add_portfolio_sizing(recommended_pct, current_price, entry_strategy)
report.add_sentiment_section(sentiment, ratings, catalysts, risks)
output_path = report.build()Step 6: Validate Output
- Confirm PDF exists at expected path
- Check file size (should be 20-50KB for 8-10 pages)
- Verify all sections rendered
Output
PDF saved to: fin-guru-private/fin-guru/analysis/reports/{TICKER}-analysis-{YYYY-MM-DD}.pdf
Error Handling
| Error | Resolution |
|---|---|
| CLI tool fails | Check ticker validity, retry with --days 90 |
| Perplexity timeout | Use cached data or skip sentiment |
| PDF generation fails | Check reportlab installation |
Example
User: "Generate a report for NVDA"
1. Run risk_metrics_cli.py NVDA --days 252
→ Sharpe: 1.85, Beta: 1.45, Max DD: -22%
2. Run momentum_cli.py NVDA --days 90
→ RSI: 62, MACD: Bullish
3. Query Perplexity for sentiment
→ 25 Buy, 8 Hold, 2 Sell, Target: $165
4. Calculate sizing for $250k portfolio
→ 2.5% = $6,250 = ~45 shares at $138
5. Generate PDF
→ fin-guru-private/fin-guru/analysis/reports/NVDA-analysis-2025-12-18.pdf (32KB)RegenerateBatch Workflow
Regenerate reports for multiple tickers using parallel subagents.
Trigger Phrases
- "regenerate batch 1"
- "redo all reports"
- "regenerate watchlist reports"
Prerequisites
- List of tickers to regenerate
- Subagent capability available
- Existing reports to replace
Workflow Steps
Step 1: Identify Tickers
Batch 1 (Original 8):
- CRWD, BRK.B, APLD, IREN, GOOG, MSFT, SOFI, VTV
Batch 2 (Added 9):
- VGT, NVDA, AVGO, PLTR, META, VRT, AMZN, FTNT, ARM
Step 2: Launch Parallel Subagents
Use the Task tool to launch one subagent per ticker:
# Launch 8 subagents in parallel (single message, multiple tool calls)
for ticker in ["CRWD", "BRKB", "APLD", "IREN", "GOOG", "MSFT", "SOFI", "VTV"]:
Task(
description=f"Generate {ticker} 2026 report",
prompt=f"""
Execute the FullResearchWorkflow for {ticker}:
1. MARKET RESEARCH
- Use Perplexity MCP to research company overview
- Find 2026 catalysts and risks
- Get analyst ratings
2. QUANT ANALYSIS
- Run: uv run python src/analysis/risk_metrics_cli.py {ticker} --days 252
- Run: uv run python src/utils/momentum_cli.py {ticker} --days 90
- Run: uv run python src/utils/volatility_cli.py {ticker} --days 90
3. STRATEGY
- Determine buy/hold/sell recommendation
- Calculate position sizing for $250k portfolio
- Define entry strategy
4. GENERATE PDF
- Build comprehensive 8-10 page report
- Use VGT-style header
- Include all quant data
- Save to fin-guru-private/fin-guru/analysis/reports/{ticker}-analysis-2025-12-18.pdf
Follow the FinanceReport skill workflows.
Replace existing PDF if present.
""",
subagent_type="general-purpose",
model="sonnet"
)Step 3: Monitor Completion
Each subagent will: 1. Complete full research workflow 2. Generate PDF report 3. Report back with summary
Step 4: Validate All Reports
After all subagents complete:
# Check all reports exist
ls -la fin-guru-private/fin-guru/analysis/reports/*.pdf
# Verify file sizes
for f in fin-guru-private/fin-guru/analysis/reports/*.pdf; do
echo "$f: $(stat -f%z "$f") bytes"
doneStep 5: Update Watchlist Document (Optional)
Update fin-guru-private/fin-guru/analysis/2026-watchlist-2025-12-18.md with:
- Verdict summaries for each ticker
- Links to PDF reports
- Consolidated recommendations
Batch Definitions
Batch 1: Original Watchlist
BATCH_1 = ["CRWD", "BRKB", "APLD", "IREN", "GOOG", "MSFT", "SOFI", "VTV"]Batch 2: Extended Watchlist
BATCH_2 = ["VGT", "NVDA", "AVGO", "PLTR", "META", "VRT", "AMZN", "FTNT", "ARM"]Full Watchlist
ALL_TICKERS = BATCH_1 + BATCH_2 # 17 totalPerformance Notes
- Each subagent takes ~3-5 minutes
- Parallel execution: 8 agents = same time as 1
- Total batch time: ~5 minutes for 8 tickers
- Full watchlist (17 tickers): ~10 minutes (2 batches)
Error Handling
| Error | Resolution |
|---|---|
| Subagent timeout | Retry individual ticker |
| API rate limit | Add delay between batches |
| Report validation fails | Check logs, regenerate |
Output
All PDFs saved to: fin-guru-private/fin-guru/analysis/reports/ File pattern: {TICKER}-analysis-{YYYY-MM-DD}.pdf
Related skills
How it compares
Pick FinanceReport when branded multi-page equity PDFs with charts beat lightweight markdown or spreadsheet summaries.
FAQ
How long are FinanceReport PDF outputs?
FinanceReport generates comprehensive 8–10 page PDF investment analysis reports for stocks and ETFs. Each report includes Finance Guru branding, embedded charts, portfolio sizing, and Perplexity sentiment sections in an institutional layout.
What triggers the FinanceReport skill?
FinanceReport activates when users mention generate report, create pdf, stock analysis, ticker report, watchlist analysis, or regenerate reports. The skill routes to named Finance Guru workflows such as GenerateSingleReport.
Is Financereport safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.