1 ---
2 name: stock-analyzer
3 description: Analyze stocks with fundamental analysis, technical indicators, and PDF reports.
4 ---
5
6 # Stock & Investment Analyzer
7
8 Analyze stocks, companies, and investment opportunities using financial market data. Provide company profiles, technical analysis, fundamental analysis, and portfolio insights.
9
10 Primary deliverable: A professional PDF research report. The Excel model and interactive web app are optional extras — only build them if the user explicitly requests them.
11
12 When to Use
13
14 User wants to analyze a specific stock or company
15
16 User asks about financial metrics, earnings, or valuations
17
18 User wants to compare investment options
19
20 User needs portfolio analysis or allocation advice
21
22 User asks about market trends or sector performance
23
24 When NOT to Use
25
26 Tax-specific questions (use tax-reviewer skill)
27
28 Personal budgeting (use budget-planner skill)
29
30 Insurance coverage (use insurance-optimizer skill)
31
32 IMPORTANT: Disclaimer & Compliance
33
34 All output from this skill is strictly informational. It does not constitute investment advice, a recommendation or solicitation to buy or sell any security, or a substitute for professional financial counsel.
35
36 Every report, PDF, Excel model, or web dashboard produced by this skill must:
37
38 State clearly on the cover page (or first visible section): "This report is for informational purposes only and does not constitute investment advice. It is not a recommendation to buy, sell, or hold any security."
39
40 Include a full disclaimer on the final page (see "Limitations & Disclaimer" section below for the required text).
41
42 Never use imperative language that implies a directive — say "may outperform" or "could be worth investigating," not "you should buy" or "add this to your portfolio."
43
44 Always recommend consulting a licensed financial advisor before making any investment decision.
45
46 This applies to all outputs regardless of whether an investor profile has been collected.
47
48 Optional Investor Profile
49
50 Before starting any analysis, offer the user the option to share their investment profile. This step is entirely optional — if the user declines or wants to skip it, proceed immediately with a general-purpose analysis using the default assumptions below.
51
52 How to offer it: At the start of the analysis, ask something like: "I can tailor this analysis to your investment style if you'd like to answer a few quick questions. Otherwise, I'll provide a general market perspective. Which do you prefer?"
53
54 Profiling Dimensions (ask all 5 if the user opts in)
55
56 Dimension
57
58 Question
59
60 Options
61
62 Risk tolerance
63
64 How would you describe your comfort with investment risk?
65
66 Conservative (preserve capital, minimize losses) / Moderate (balanced growth with some downside protection) / Aggressive (maximize returns, comfortable with volatility)
67
68 Time horizon
69
70 How long do you plan to hold these investments?
71
72 Short-term (under 1 year) / Medium-term (1-5 years) / Long-term (5+ years)
73
74 Current allocation
75
76 Roughly, what is your current portfolio split?
77
78 Approximate % in equities, bonds/fixed income, cash, alternatives (real estate, commodities, crypto)
79
80 Income needs
81
82 Do you need your investments to generate regular income?
83
84 Yes, dividend/income-focused / No, purely growth-focused / Mixed
85
86 Experience level
87
88 How would you describe your investing experience?
89
90 Beginner (started recently, still learning) / Intermediate (a few years, understands basic concepts) / Advanced (experienced, comfortable with complex strategies)
91
92 Default Assumptions (when user skips profiling)
93
94 When the user declines profiling, use these neutral defaults and do not assume anything about their personal situation:
95
96 Risk tolerance: Moderate
97
98 Time horizon: Medium-term (1-5 years)
99
100 Allocation: Not assumed — present analysis without allocation recommendations
101
102 Income needs: Not assumed — cover both growth and income angles
103
104 Experience level: Intermediate — use clear language but don't oversimplify
105
106 How Profiling Shapes the Report
107
108 When profile data is available, adjust the report in these ways:
109
110 Cover page: Add an "Investor Profile" summary box showing the user's stated risk tolerance, time horizon, and income preference
111
112 Stock/sector picks: Add a suitability tag to each recommendation (e.g., "Suitable for: moderate risk, 3-5yr horizon" or "Caution: high volatility — may not suit conservative investors")
113
114 Allocation section: Add a "Suggested Allocation" section at the end of the report, tailored to their risk/timeline. Frame it as illustrative, not prescriptive (e.g., "A moderate-risk investor with a 3-5 year horizon might consider an allocation along these lines...")
115
116 Language calibration: For beginners, use simpler explanations and fewer technical terms. For advanced investors, include more granular data and nuanced analysis.
117
118 Picks that don't fit: Still include all relevant picks, but flag any that conflict with the user's stated profile (e.g., "Note: This is a high-volatility pick that may not align with your conservative risk preference")
119
120 When profile data is NOT available, omit the Investor Profile box, omit the Suggested Allocation section, and present all picks without suitability tags. The core analysis remains identical.
121
122 Data Sources (Use These — Don't Guess)
123
124 Python libs (run directly, no API key):
125
126 import yfinance as yf
127
128 t = yf.Ticker("AAPL")
129
130 t.info \# P/E, market cap, beta, 52w range, margins
131
132 t.financials \# income statement (4yr)
133
134 t.balance_sheet \# debt, cash, equity
135
136 t.cashflow \# FCF, capex
137
138 t.history(period="1y") \# OHLCV for technicals
139
140 t.institutional_holders \# 13F ownership
141
142 Sector screening: Use yfinance sector ETFs (XLE, XLK, XLF, XLV, XLI, XLP, XLU, XLRE, XLB, XLC, XLY) for reliable sector-level data. These are more stable than third-party screener libraries. For individual stock screening, pull yfinance data for each ticker directly.
143
144 Note on finvizfinance: This library frequently breaks due to website changes. Avoid relying on it. Use yfinance sector ETFs and direct ticker lookups instead.
145
146 Primary filings: Start from the EDGAR filing index: webFetch("<https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK={ticker}&type=10-K>"). This returns a list of filings — find the most recent 10-K and webFetch its "Documents" link to reach the actual filing. Read Item 1A (Risk Factors) and Item 7 (MD&A) — this is where management admits problems.
147
148 Insider activity: webFetch("<http://openinsider.com/screener?s={ticker}>") — look for cluster buys (multiple execs buying same week) and P-code open-market purchases (insider paid cash at market price — strongest signal). Ignore option exercises (M-code) and 10b5-1 scheduled sales.
149
150 Short interest: webSearch "{ticker} short interest fintel" — >20% of float = crowded short, squeeze risk either direction.
151
152 Data Source Fallbacks
153
154 yfinance occasionally returns empty data, tickers change, or rate limits kick in. Use this fallback hierarchy:
155
156 Primary: yfinance — try up to 2 retries with a short delay
157
158 Fallback: Web search for the specific metric (e.g., webSearch("[ticker] market cap 2026"))
159
160 Last resort: Flag the metric as "data unavailable" — never fabricate numbers
161
162 Research First — Mandatory Before Any Output
163
164 Never show financials, tables, or a report to the user without thoroughly researching first. Before generating the PDF, you must:
165
166 Load the deep-research skill for comprehensive web research. This is not optional — every stock analysis must use deep research to gather real data before producing any deliverable.
167
168 Pull actual financials from yfinance AND cross-reference with SEC EDGAR filings (10-K, 10-Q). Do not rely on a single source.
169
170 Search for every company mentioned — if the user's request involves multiple companies or peers, pull financials on ALL of them, not just the primary ticker.
171
172 Bias towards tables and numbers from actual public filings. Every financial figure in the report must be traceable to a real source (SEC filing, earnings release, or yfinance data pull). Do not estimate or round when real numbers are available.
173
174 If you cannot verify a financial figure from at least one real source, flag it explicitly as unverified. Never present guessed or hallucinated numbers as fact.
175
176 Scoping Deep Research
177
178 For a single-stock analysis, launch 2-3 parallel research agents: one for fundamentals/financials, one for industry/competitive landscape, one for recent news/catalysts.
179
180 For a multi-sector or thematic analysis, launch 4-6 parallel research agents organized by theme (e.g., macro environment, sector rotation, geopolitics, specific sector deep-dives). More than 6 agents rarely adds value and wastes time.
181
182 Methodology
183
184 Step 1: Pull the Data (Python)
185
186 Run yfinance to get fundamentals + 1yr price history. Compute 50/200 SMA, RSI(14), and current price vs 52w high. Takes 10 lines of pandas.
187
188 Step 2: Fundamental Analysis
189
190 Valuation (compare to sector median, not S&P):
191
192 P/E — meaningless alone; flag if >2x sector median
193
194 PEG — <1.0 = growth at reasonable price; >2.0 = priced for perfection
195
196 EV/EBITDA — better than P/E for capital-intensive or leveraged cos
197
198 P/S — only metric for unprofitable growth; >20x = needs hypergrowth to justify
199
200 FCF yield (FCF/market cap) — >5% = genuinely cheap; negative = burning cash
201
202 Quality red lines (practitioner heuristics):
203
204 Revenue growing but FCF shrinking -> earnings quality problem, dig into receivables
205
206 Debt/EBITDA >4x -> one bad year from covenant breach
207
208 Gross margin compressing 3+ quarters -> losing pricing power
209
210 Stock-based comp >15% of revenue -> dilution machine (common in SaaS)
211
212 Goodwill >50% of assets -> acquisition-heavy, writedown risk
213
214 Step 3: Technical Context (Not Prediction)
215
216 Compute in pandas — don't just describe:
217
218 Price vs 50/200 SMA: below both = downtrend, don't catch knives
219
220 Golden cross (50 crosses above 200) = trend confirmation, not entry signal
221
222 RSI(14): >70 overbought / <30 oversold — only useful at extremes + divergence
223
224 Volume: moves on 2x+ avg volume are real; low-volume moves fade
225
226 % off 52w high: >30% drawdown in an uptrending market = something broke
227
228 Step 4: The Retail Edge — Signals Institutions Ignore
229
230 Insider cluster buys (OpenInsider): 3+ insiders open-market buying within 2 weeks is the single highest-conviction public signal. Research shows insider buys outperform; sells mean nothing (taxes/divorces/yachts).
231
232 Buying the dip: insider P-code purchase after >10% drop = management disagrees with the market
233
234 Short squeeze setup: short interest >20% + days-to-cover >5 + any positive catalyst
235
236 Unusual options: webSearch "{ticker} unusual options activity" — large OTM call sweeps before earnings sometimes leak info
237
238 Step 5: Comparative Table
239
240 Build a pandas DataFrame with peers side-by-side: P/E, PEG, rev growth, gross margin, FCF yield, debt/EBITDA. The outlier in either direction is your thesis. Pull yfinance data for every peer company — do not leave cells blank or use estimates when real data is available. Every company in the comparison must have actual financials pulled and verified.
241
242 Step 6: Web Research — Find Existing Analyst Reports and News
243
244 Use web search aggressively via the deep-research skill. Before writing the report, gather real external research to cite:
245
246 webSearch("[ticker] analyst report 2026")
247
248 webSearch("[ticker] earnings analysis site:seekingalpha.com")
249
250 webSearch("[ticker] bull case bear case site:seekingalpha.com OR site:fool.com")
251
252 webSearch("[company] investor presentation 2026 filetype:pdf")
253
254 webSearch("[ticker] price target consensus")
255
256 webSearch("[ticker] industry outlook [sector]")
257
258 webSearch("[company] competitive landscape")
259
260 webSearch("[ticker] short interest thesis")
261
262 Source hierarchy (cite all of these in the report):
263
264 Source
265
266 What you get
267
268 How to cite
269
270 SEC EDGAR (10-K, 10-Q, 8-K)
271
272 Primary financials, risk factors, MD&A
273
274 "Source: [Company] 10-K FY2025, Item 7"
275
276 Earnings call transcripts
277
278 Management commentary, guidance
279
280 "Source: Q4 2025 Earnings Call, CEO remarks"
281
282 Sell-side research (via SeekingAlpha, TipRanks)
283
284 Price targets, consensus estimates
285
286 "Source: TipRanks consensus, 12 analysts"
287
288 Industry reports
289
290 TAM, growth rates, competitive dynamics
291
292 "Source: [Firm] [Industry] Report, [Date]"
293
294 Company investor presentations
295
296 Management's own bull case, KPIs
297
298 "Source: [Company] Investor Day 2025"
299
300 News (Reuters, Bloomberg, CNBC)
301
302 Catalysts, M&A, regulatory
303
304 "Source: Reuters, [Date]"
305
306 Use webFetch to pull actual content from SeekingAlpha articles, earnings transcripts, and investor presentations. Extract specific data points, quotes, and estimates to cite in the report.
307
308 Project Architecture — Reusable Modules
309
310 The reports/ directory contains shared modules that all ticker-specific scripts import from. Never duplicate the ResearchReport class, chart functions, or Excel styling helpers — always import from these modules.
311
312 reports/report_base.py — Shared PDF Report Class:
313
314 - sanitize_text(text): Cleans Unicode characters for fpdf2's latin-1 encoding. Apply to ALL text before passing to pdf.cell() or pdf.multi_cell().
315 - ResearchReport(company_name, ticker): Extends FPDF with all report methods. Constructor sets alias_nb_pages() and auto_page_break(margin=15).
316
317 - Methods: header(), footer(), ensure_space(needed_mm), section_title(), sub_title(), body_text(), bullet_point(), key_metric(), table_header(), table_row(), data_table(), callout_box(), cover_page(data_dict), disclaimer_page(sources_text)
318 - cover_page(data) expects a dict with keys: exchange, sector, rating, price_target, current_price, market_cap_str, range_52w, analyst_consensus
319
320 - Usage: from report_base import ResearchReport, sanitize_text
321 - IMPORTANT fpdf2 gotcha: bullet_point() uses cell(5, 4, chr(149)) then multi_cell(185, 4, text). You MUST pass an explicit width (185) to multi_cell after cell(), otherwise multi_cell gets 0 remaining width and throws "Not enough horizontal space" error. Never use multi_cell(0, ...) after a cell() on the same line.
322
323 reports/chart_utils.py — Shared Chart Generation:
324
325 - price_chart(df, ticker, company_name): 1-year price history with 50/200 SMAs. Returns saved path.
326 - revenue_margin_chart(years, revenues, gross_margins, op_margins, net_margins, ticker): Bar+line combo chart. Returns saved path.
327
328 - segment_chart(segments_dict, ticker): Grouped bar chart from {year: {segment: value}} dict. Returns saved path.
329 - peer_valuation_chart(peers_dict, ticker): 3-panel bar chart (Fwd P/E, EV/EBITDA, P/S). peers_dict = {ticker: {fwd_pe, ev_ebitda, ps}}. Subject ticker highlighted in red. Returns saved path.
330
331 - All charts saved to reports/charts/ at 150 DPI.
332 - Usage: from chart_utils import price_chart, revenue_margin_chart, segment_chart, peer_valuation_chart
333
334 reports/excel_base.py — Shared Excel Styling:
335
336 - Style constants: NAVY_FILL, WHITE_FONT, HEADER_FONT, INPUT_FILL, LIGHT_GRAY, GREEN_FILL, RED_FILL, THIN_BORDER, INPUT_BORDER, THICK_BORDER
337 - style_header_row(ws, row, max_col): Navy header with white bold text
338
339 - style_data_cell(ws, row, col, fmt, is_input, shaded): Applies borders, fills, number format
340 - auto_width(ws): Auto-sizes all columns
341
342 - write_section_header(ws, row, col, text): Section label in navy bold
343 - write_table(ws, start_row, headers, rows, col_formats, highlight_row): Complete table with header + data rows
344
345 - Usage: from excel_base import *
346
347 reports/data_utils.py — Data Pulling & Validation:
348
349 - pull_stock_data(ticker, retries=2): Pulls all yfinance data with retry logic. Returns dict with all metrics, formatted strings (market_cap_str, range_52w, analyst_consensus), and raw DataFrames (financials, balance_sheet, cashflow, quarterly_financials, history_1y).
350 - pull_peer_data(tickers): Pulls key metrics for a list of peer tickers. Returns dict of dicts.
351
352 - validate_data(data): Checks for common data issues (zero price, negative P/E, missing history). Returns list of warning strings.
353 - fmt_val(val, fmt_type, fallback): Format a value for display. Types: "pct", "pct_change", "ratio", "dollar", "dollar_b", "market_cap".
354
355 - Usage: from data_utils import pull_stock_data, pull_peer_data, validate_data, fmt_val
356
357 reports/generate_report.py — Single Entry Point CLI:
358
359 - Usage: python reports/generate_report.py TICKER --peers PEER1,PEER2,PEER3
360 - Runs the data pipeline: pulls subject + peer data, validates, generates price + peer charts
361
362 - Outputs a data summary and tells you which ticker-specific scripts to create/run next
363 - Revenue/margin and segment charts require manual historical data (yfinance only provides trailing 4 years, segment data requires 10-K parsing)
364
365 Workflow for Adding a New Ticker:
366
367 1. Run: python reports/generate_report.py TICKER --peers PEER1,PEER2
368 2. Create reports/generate_{ticker}_pdf.py — imports ResearchReport from report_base, chart paths from chart_utils
369
370 3. Create reports/generate_{ticker}_dcf.py — imports excel_base helpers, uses Excel formulas (not hardcoded values)
371 4. Run both scripts to generate the PDF and Excel model
372
373 5. Present both files to the user
374
375 Data Validation Rules:
376
377 - Always call validate_data() after pulling data and print warnings
378 - Negative trailing P/E means the company is unprofitable — flag it, don't hide it
379
380 - If yfinance returns None for a metric, display "N/A" in the report, never 0 or a guess
381 - Cross-reference key financials (revenue, net income) between yfinance and SEC filings when possible
382
383 - If a peer ticker fails to pull data, include it in the table with "N/A" values rather than silently dropping it
384
385 Peer Data — Always Pull from yfinance:
386
387 - Never hardcode peer metrics — always pull from yfinance via pull_peer_data() so data is current
388 - Pull at least 4 peers in the same sector/industry
389
390 - Common peer sets: Tech (AAPL/MSFT/GOOG/AMZN/META), Semis (NVDA/AMD/AVGO/QCOM/INTC), Auto (TSLA/GM/F/TM/RIVN)
391 - For the comparable companies Excel sheet, use formulas for the median row: =MEDIAN(C5:C10)
392
393 Build Order — PDF Report + DCF Excel Model (Built in Parallel)
394
395 Both the PDF research report and the DCF Excel model are primary deliverables. Build them in parallel from the same research data.
396
397 Step 1: Generate charts (Python + matplotlib)
398
399 Generate all charts first — these will be embedded in the PDF. Save as PNG at 150+ DPI to reports/charts/. Generate at least 4 charts: price history with SMAs, revenue/margin trends, peer valuation comparison, and one more relevant to the thesis.
400
401 import matplotlib.pyplot as plt
402
403 import yfinance as yf
404
405 df = yf.Ticker("AAPL").history(period="1y")
406
407 fig, ax = plt.subplots(figsize=(10, 5))
408
409 ax.plot(df.index, df['Close'], label='Price', color='#1a1a2e')
410
411 ax.plot(df.index, df['Close'].rolling(50).mean(), label='50 SMA', color='#e94560', linestyle='--')
412
413 ax.plot(df.index, df['Close'].rolling(200).mean(), label='200 SMA', color='#0f3460', linestyle='--')
414
415 ax.set_title("AAPL - 1 Year Price History")
416
417 ax.legend()
418
419 ax.grid(alpha=0.3)
420
421 fig.savefig("reports/charts/price_chart.png", dpi=150, bbox_inches='tight')
422
423 Step 2: Generate the PDF research report (PRIMARY DELIVERABLE \#1)
424
425 Write a Python generation script (reports/generate_pdf.py) using fpdf2 to produce a polished, multi-page equity research PDF. Do not output a markdown summary as a substitute. Do not skip the PDF.
426
427 Why fpdf2, not jsPDF: Python is already required for yfinance and matplotlib. fpdf2 installs instantly (pip install fpdf2), runs reliably, and handles image embedding natively. jsPDF (Node) has heavy dependencies, frequent install timeouts in constrained environments, and is designed for browser-side generation — not server-side report building.
428
429 Present the PDF to the user in chat immediately after generating it.
430
431 Step 3: Build the DCF Excel Model (PRIMARY DELIVERABLE \#2 — built in parallel with the PDF)
432
433 Always build the DCF Excel model alongside the PDF report. Do not wait for user request — this is a standard deliverable for every stock analysis.
434
435 Use openpyxl in Python to generate the .xlsx file (reports/{TICKER}_DCF_Model.xlsx):
436
437 from openpyxl import Workbook
438
439 from openpyxl.styles import Font, PatternFill, Alignment, Border, Side, numbers
440
441 from openpyxl.utils import get_column_letter
442
443 The Excel model must include the following sheets:
444
445 Sheet 1 — DCF Model:
446
447 - Revenue projections (5-year forecast with growth assumptions)
448 - EBITDA / operating income projections (margin assumptions)
449
450 - Free cash flow build: EBITDA -> subtract taxes, capex, change in working capital -> unlevered FCF
451 - Terminal value calculation (perpetuity growth method AND exit multiple method)
452
453 - WACC calculation with clearly labeled inputs: risk-free rate, equity risk premium, beta, cost of debt, tax rate, debt/equity ratio
454 - Enterprise value -> equity value -> implied share price
455
456 - Upside/downside % vs current price
457
458 Sheet 2 — Sensitivity Analysis:
459
460 - Two-variable data table: WACC (rows) vs terminal growth rate (columns) showing implied share price at each combination
461 - Highlight the base case cell
462
463 - Use conditional formatting: green for upside scenarios, red for downside
464
465 Sheet 3 — Scenario Analysis:
466
467 - Bull / Base / Bear cases with different revenue growth, margin, and multiple assumptions
468 - Probability-weighted price target (e.g., 25% Bull, 50% Base, 25% Bear)
469
470 - Show each scenario's implied share price and upside/downside
471
472 Sheet 4 — Comparable Companies:
473
474 - Peer comparison table: ticker, market cap, P/E, EV/EBITDA, P/S, revenue growth, gross margin, FCF yield
475 - Pull real data from yfinance for 4-6 peers
476
477 - Highlight the subject company row
478
479 Sheet 5 — Financial Summary:
480
481 - Historical income statement (3-4 years from yfinance)
482 - Historical balance sheet highlights (cash, debt, equity)
483
484 - Key ratios: ROE, ROIC, debt/EBITDA, current ratio
485
486 CRITICAL — Use Excel Formulas, Not Hardcoded Values:
487
488 - Every calculated cell MUST use an Excel formula, not a pre-computed Python value. The entire point of a financial model is that users can change assumptions and see results update automatically.
489 - Input cells (yellow): hardcoded values the user can edit (growth rates, margins, WACC inputs, tax rate, etc.)
490
491 - Calculated cells: MUST be formulas referencing input cells or other calculated cells. Examples:
492 - Cost of Equity: =B7+B9*B8 (risk-free + beta \* ERP)
493
494 - WACC: =B10*(1-B13)+B11*(1-B12)*B13 (formula using CoE, CoD, tax, D/TC)
495 - Revenue projections: =B21*(1+C22) (prior year \* (1 + growth rate))
496
497 - Gross Profit: =C21*C24 (revenue \* gross margin)
498 - Terminal Value: =B42*(1+B43)/(B44-B43) (FCF*(1+g)/(WACC-g))
499
500 - PV factors: =1/(1+B44)^n
501 - Implied Share Price: =B55/B56 (equity value / shares)
502
503 - Sensitivity table: Each cell should be a formula computing implied price for that row's WACC and column's terminal growth rate, referencing the DCF Model sheet for PV of FCFs, terminal FCF, debt, cash, and shares.
504 - Scenario Analysis: Implied prices should be formulas (e.g., revenue *EBITDA margin* EV/EBITDA multiple / (1+WACC)^5, adjusted for net cash). Probability-weighted target = SUMPRODUCT of probabilities and prices.
505
506 - Financial Summary: Derived metrics (gross profit, margins, net cash, FCF, ROE) should be formulas referencing the raw data cells, not hardcoded.
507 - Cross-sheet references use the format: ='DCF Model'!B42
508
509 - Test: Open the Excel file, change WACC from 12.5% to 10%, and verify the implied share price, sensitivity table, and scenario prices all update. If anything stays static, it's broken.
510
511 Formatting standards:
512
513 - Professional header row with navy (#003366) background and white text
514 - Alternating row shading for data tables
515
516 - Number formatting: currency with commas ($#,##0), percentages (0.0%), large numbers in millions ($#,##0M)
517 - Input cells (assumptions) highlighted in light yellow (#FFFFCC) with medium borders to visually distinguish from calculated cells
518
519 - Freeze panes on header rows
520 - Column widths auto-sized for readability
521
522 Present the Excel model to the user in chat alongside the PDF.
523
524 Step 4 (OPTIONAL): Build the web app
525
526 Only build if the user requests an interactive dashboard or web view. When building the web app:
527
528 Design it as a native web dashboard — responsive, interactive charts (Recharts), scrollable sections. Do NOT make it a literal PDF replica with fixed-size page containers.
529
530 Hardcode data in a centralized data file (src/data/report-data.ts) — no backend API needed for static analysis.
531
532 Use the react-vite skill and generateFrontend() for scaffolding.
533
534 PDF Report — Professional Research Report (Sell-Side Format)
535
536 The PDF should look like a sell-side initiation note from Goldman, Morgan Stanley, or JP Morgan. This is the primary deliverable — invest the most effort here.
537
538 Report Structure
539
540 Page 1 — Cover / Executive Summary:
541
542 Disclaimer line (required): "This report is for informational purposes only and does not constitute investment advice. It is not a recommendation to buy, sell, or hold any security."
543
544 Company name, ticker, exchange, current price, market cap
545
546 Rating: Buy / Hold / Sell with price target and upside/downside %
547
548 Investment thesis in 3-4 bullet points (the "elevator pitch")
549
550 Key metrics snapshot: P/E, EV/EBITDA, revenue growth, FCF yield
551
552 A 1-year price chart (generated via matplotlib, embedded as image)
553
554 Investor Profile box (only if profile was collected): Show risk tolerance, time horizon, and income preference in a small summary box
555
556 Pages 2-3 — Investment Thesis:
557
558 Bull case (with probability weighting if possible)
559
560 Bear case (required — what kills this trade?)
561
562 Key catalysts with expected timeline
563
564 Competitive positioning / moat analysis
565
566 Pages 3-4 — Financial Analysis:
567
568 Revenue breakdown by segment (with a stacked bar chart)
569
570 Margin trends over 4+ quarters (with a line chart)
571
572 FCF bridge / waterfall
573
574 Balance sheet health (debt maturity, liquidity)
575
576 Peer comparison table (pulled from yfinance for 3-5 peers)
577
578 Page 5 — Valuation:
579
580 DCF model summary (show assumptions: WACC, terminal growth, revenue CAGR)
581
582 Comparable company analysis table
583
584 Historical valuation range (P/E or EV/EBITDA band chart)
585
586 Price target derivation
587
588 Page 6 — Technical Analysis:
589
590 Price chart with 50/200 SMA overlay (generated via matplotlib)
591
592 Volume analysis
593
594 Key support/resistance levels
595
596 RSI chart
597
598 Page 7 — Risks:
599
600 Ranked by probability x impact
601
602 Regulatory, competitive, execution, macro risks
603
604 Specific to this company, not generic boilerplate
605
606 Final Page — Sources & Disclaimer:
607
608 Full citation list with dates for every external source referenced
609
610 Full disclaimer (required — use the exact text from the "Limitations & Disclaimer" section below)
611
612 If investor profile was collected, include a closing note: "This analysis was tailored to a [risk tolerance] risk profile with a [time horizon] time horizon. Your actual circumstances may differ. Consult a licensed financial advisor before making investment decisions."
613
614 PDF Generation with fpdf2
615
616 Use fpdf2 (Python) to generate the PDF. Install: pip install fpdf2.
617
618 from fpdf import FPDF
619
620 class ResearchReport(FPDF):
621
622 def header(self):
623
624 if self.page_no() == 1:
625
626 return
627
628 self.set_font("Helvetica", "B", 8)
629
630 self.set_text_color(0, 51, 102)
631
632 self.cell(0, 6, "Company (TICKER) | Equity Research Report", align="L")
633
634 self.cell(0, 6, "Report Date", align="R", new_x="LMARGIN", new_y="NEXT")
635
636 self.set_draw_color(0, 51, 102)
637
638 self.line(10, self.get_y(), 200, self.get_y())
639
640 self.ln(2)
641
642 def footer(self):
643
644 self.set_y(-15)
645
646 self.set_font("Helvetica", "I", 7)
647
648 self.cell(0, 10, f"Page {self.page_no()}/{{nb}}", align="C")
649
650 def ensure_space(self, needed_mm):
651
652 """Check if needed_mm of vertical space remains on the current page.
653
654 If not, insert a page break. Call this before any section that has a
655
656 title + table/chart combo to prevent them from splitting across pages."""
657
658 if self.get_y() + needed_mm > self.h - self.b_margin:
659
660 self.add_page()
661
662 pdf = ResearchReport()
663
664 pdf.alias_nb_pages()
665
666 pdf.set_auto_page_break(auto=True, margin=15)
667
668 pdf.add_page()
669
670 ## Title
671
672 pdf.set_font("Helvetica", "B", 24)
673
674 pdf.cell(0, 12, "Company Analysis", ln=True)
675
676 ## Embed chart (w=160, NOT 190 — see chart sizing notes below)
677
678 pdf.image("reports/charts/price_chart.png", x=15, w=160)
679
680 pdf.output("reports/Research_Report.pdf")
681
682 Critical: Text Encoding for fpdf2
683
684 fpdf2 uses latin-1 encoding by default. Special Unicode characters will cause errors or render as garbage. Before writing any text to the PDF, sanitize it:
685
686 def sanitize_text(text):
687
688 replacements = {
689
690 "\u2014": "-", \# em-dash
691
692 "\u2013": "-", \# en-dash
693
694 "\u2018": "'", \# left single quote
695
696 "\u2019": "'", \# right single quote
697
698 "\u201c": '"', \# left double quote
699
700 "\u201d": '"', \# right double quote
701
702 "\u2022": "*", \# bullet
703
704 "\u2026": "...", \# ellipsis
705
706 "\u00a0": " ", \# non-breaking space
707
708 "\u2212": "-", \# minus sign
709
710 "\u00b7": "*", \# middle dot
711
712 }
713
714 for char, replacement in replacements.items():
715
716 text = text.replace(char, replacement)
717
718 return text.encode("latin-1", errors="replace").decode("latin-1")
719
720 Apply sanitize_text() to every string before passing it to pdf.cell(), pdf.multi_cell(), or pdf.write(). This is not optional — text from web scraping, yfinance, and deep research will contain Unicode characters that break the PDF.
721
722 Avoiding Common fpdf2 Issues
723
724 Chart sizing — DO NOT use w=190 (full page width): Charts generated by matplotlib at 150 DPI are typically 1500-1700px wide and 800-1000px tall. At w=190mm, fpdf2 scales them proportionally, producing chart heights of 90-130mm — nearly half an A4 page. This pushes the chart to the next page, leaving a large white gap below the section title. Use w=160 (centered with x=15) as the default chart width. This keeps charts at ~75-110mm tall, which fits comfortably on a page alongside a title and surrounding content.
725
726 Section splitting across pages: fpdf2's auto page break only triggers when content would overflow — it does NOT keep a section title together with its table or chart. A subtitle at y=270 will render on the current page, but the table below it starts on the next page, leaving the title orphaned. To prevent this, implement an ensure_space(needed_mm) method (see sample code above) and call it before every subtitle+table or subtitle+chart combo. Estimate the needed space: title (~7mm) + table header (~6mm) + rows (5mm each) + body text if any. For charts, use ~80mm. For tables with 5-8 rows, use ~50-55mm.
727
728 Spacing discipline: Use tight spacing throughout. Recommended values: ln(0.5) after subtitles, ln(1) between sections, ln(1.5) between major sections. Avoid ln(3) or higher — it adds up fast across a multi-page report. Body text line height should be 4mm, bullet line height 4mm, table rows 5mm, table headers 6mm. These small values compound: a report with 20 sections saves 40-60mm of vertical space vs. generous spacing, which is nearly an entire extra page of content.
729
730 Auto page break margin: Use margin=15, not margin=20. The default 20mm wastes 5mm per page (45mm across a 9-page report). 15mm provides enough room for the footer without wasting space.
731
732 Long text overflow: Use multi_cell() for paragraphs, not cell(). cell() truncates at one line.
733
734 Table column overflow: cell() does NOT clip or wrap text — it renders beyond the cell boundary, causing text to overflow into adjacent columns or off the page. For tables with a wide text column (like "Details" in a risk matrix), allocate most of the width to that column. Example: [45, 24, 24, 97] instead of [55, 27, 27, 81]. Also shorten text strings to fit: abbreviate "Medium" to "Med", truncate long names ("Valuation Compression" -> "Valuation Compress."), and tighten phrasing. At 8pt Helvetica, a 97mm column fits roughly 50-55 characters. Test visually and shorten any string that overflows.
735
736 Table alignment: Right-align numbers, left-align text. Use cell() with explicit widths for table columns.
737
738 Font availability: Stick to built-in fonts: Helvetica, Times, Courier. These require no font file embedding. If you need bold/italic, use the style parameter: set_font("Helvetica", "B", 12).
739
740 Styling Guidelines
741
742 Header bar on each page (except cover) with company name, ticker, report date, and page number
743
744 Data tables with alternating row shading, right-aligned numbers, 8pt font, 5mm row height
745
746 Charts at w=160 (not full width) centered with x=15, with clear titles and axis labels
747
748 Callout boxes for key insights ("Management guided 15% revenue growth in Q4 call")
749
750 Source citations as footnotes or inline parenthetical references
751
752 Professional typography: 9pt body text (4mm line height), 8pt table text (5mm row height), 10pt subtitles, 13pt section headers. Keep spacing tight — ln(0.5) to ln(1.5) between elements, never ln(3) or higher.
753
754 Color palette: Navy (#003366) for headers, dark gray (#333333) for body text, green (#228B22) for positive metrics, red (#CC0000) for negative metrics
755
756 Page density: Aim for dense, information-rich pages with minimal white space. Every page should feel full. If a page has more than 40mm of unused space at the bottom, either pull the next section up (by removing the forced add_page()) or increase chart/table sizes to fill the space.
757
758 Avoid forced page breaks between sections: Do NOT use add_page() between every section. This creates large white gaps when the previous section ends partway down the page. Instead, let content flow continuously and use ensure_space(needed_mm) to trigger a page break only when there isn't enough room for the next section's title + content. Only use add_page() for truly standalone sections (e.g., cover page, disclaimer page). For everything else — financial tables, charts, risk matrices, catalysts — let them flow naturally with ensure_space() guarding against orphaned titles.
759
760 Best Practices
761
762 Timestamp everything — state data pull date; yfinance prices are ~15min delayed
763
764 Sector-relative only — a 30 P/E is cheap in software, expensive in utilities
765
766 Label facts vs thesis — "FCF yield is 6%" (fact) vs "undervalued" (opinion)
767
768 Bear case required — every analysis must include: what kills this trade?
769
770 Position sizing reality — no single stock >5% for most retail portfolios; if conviction demands 20%, the conviction is the problem
771
772 Limitations & Disclaimer
773
774 Include the following disclaimer text (or substantially similar language) on the final page of every PDF report and at the bottom of every web dashboard:
775
776 IMPORTANT DISCLAIMER: This report is produced for informational and educational purposes only. It does not constitute investment advice, a recommendation, or a solicitation to buy, sell, or hold any security or financial instrument. The information contained herein is based on publicly available data sources that may be incomplete, delayed, or inaccurate. No representation or warranty, express or implied, is made as to the accuracy, completeness, or reliability of the information provided.
777
778 Any opinions, estimates, or projections expressed are those of an automated analysis system and do not reflect the views of any licensed financial institution, registered investment advisor, or broker-dealer. Past performance is not indicative of future results. All investments carry risk, including the potential loss of principal.
779
780 Before making any investment decision, you should consult with a qualified, licensed financial advisor who can assess your individual circumstances, risk tolerance, and financial goals. The authors and publishers of this report accept no liability for any loss or damage arising from reliance on the information contained herein.
781
782 Technical limitations:
783
784 yfinance scrapes Yahoo Finance — occasionally breaks, data may lag filings by days or weeks
785
786 Cannot access Bloomberg, FactSet, Refinitiv, or real-time Level 2 market data
787
788 Cannot execute trades, manage portfolios, or provide personalized financial planning
789
790 Data is delayed (typically 15-20 minutes for prices; financials may lag quarterly filings)
791
792 All equities can decline to zero; sector and thematic analysis reflects a point-in-time snapshot that may not reflect rapid market changes
793
794 Closing Prompt
795
796 After presenting the PDF report and DCF Excel model to the user, always end the reply with:
797
798 "Let me know if you'd like me to build an interactive web page for these findings as well."