fix: quality-aware scoring so cash-rich, fairly-priced compounders aren't underrated - #21
Merged
Merged
Conversation
…en't underrated The 0-100 rating treated valuation as a pure cheapness screen and ignored a company's cash, so high-quality names trading at a fair (not cheap) premium were dragged into "Fair"/"Weak" despite strong economics. - Valuation is now quality-aware: P/E, P/B and EV/EBITDA ceilings widen quadratically with a four-signal quality factor (ROE, ROCE, operating margin, revenue+EPS-CAGR growth), so a proven compounder isn't floored for a premium while a low-quality expensive name still is. - Net cash is surfaced on Ratios (FX-normalized against market cap) and rewarded twice: it lowers the effective equity multiple (dampened ex-cash P/E and P/B) and lifts the renamed "Balance Sheet" bucket via a signed term that also drags net-debt names below debt-free. - DCF held to a 30% cross-check within valuation and recentred so fair value scores neutral instead of low. - Weights rebalanced toward durable quality (Valuation 15->10, Profitability ->17, Competitive Moat ->12, Balance Sheet ->11). - Shared net_debt helper extracted into finutils, reused by DCF and Ratios. Tests: quality-premium, per-signal monotonicity, net-cash/net-debt, ratios plumbing (incl. an FX-mismatch case), plus a Spearman rank-preservation guard (0.99 on a synthetic basket) and an archetype-direction table. Full suite green.
YashvantHange
added a commit
that referenced
this pull request
Jul 18, 2026
Follow-up to the quality-aware scoring change (#21), which shipped without user-facing docs. - CHANGELOG: add an [Unreleased] Changed entry covering the quality factor, the net-cash reward, the Debt -> Balance Sheet rename, the reweighting, and the preserved rank order. - README: update the rating-buckets table to the shipped weights (15/17/10/11/10/12/10/5/5/5, +5 ESG) with the Balance Sheet bucket, and add a short note explaining quality-aware valuation and the net-cash effect.
YashvantHange
added a commit
that referenced
this pull request
Jul 18, 2026
* docs: document quality-aware scoring in CHANGELOG and README Follow-up to the quality-aware scoring change (#21), which shipped without user-facing docs. - CHANGELOG: add an [Unreleased] Changed entry covering the quality factor, the net-cash reward, the Debt -> Balance Sheet rename, the reweighting, and the preserved rank order. - README: update the rating-buckets table to the shipped weights (15/17/10/11/10/12/10/5/5/5, +5 ESG) with the Balance Sheet bucket, and add a short note explaining quality-aware valuation and the net-cash effect. * feat: auto-generate HTML report and modernise the research-grade report Deliver the analysis as a document automatically and make it read as professional equity research in both HTML and PDF. Auto-HTML - `investo analyze` writes a self-contained HTML note by default (--no-html to skip; announced on stderr so --json stays pipeable). - The MCP `analyze_company` tool writes one too, returning html_report_path (plus generated_at, investo_version, html_bytes); emit_html=false opts out. The tool is now correctly marked as a file writer. Modern redesign (render/css.py, render/html.py) - Design-token system: spacing scale, radii, shadows and a semantic status palette in :root, consumed everywhere. - New furniture: masthead rating block, KPI cards, colour status badges from one shared mapping, card-framed zebra tables, a table of contents, a colophon, <meta description> and accessible table captions. - Removes the heavy letter-spacing that made PDF text render and copy as "I N V E S T O". Research-grade PDF (print CSS + export.py) - Per-page running header/footer via a repeating thead/tfoot group, replacing fixed-position furniture that rendered inverted and clipped body text on full pages. - Tables and charts fit the page, table headers repeat, colours print, cover page break. Tests updated and added (auto-HTML on CLI/MCP, badges/cards/TOC, captions, a no-BUY/SELL guard); all 284 pass. * fix: satisfy CI lint and type gates - mypy: annotate the KPI growth-signal local as str | None (the two branches assign different Literal types, which mypy rejected as an implicit narrow). - ruff: pre-existing nits the newer CI linter now flags — sort the finutils import members and rewrite a test-helper dict() call as a literal.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The 0–100 rating treated valuation as a pure cheapness screen and was blind to a company's cash, so high-quality companies that hold cash and trade at a fair — not cheap — price (e.g. Coforge, TCS) were dragged into "Fair"/"Weak" despite strong economics:
Ratioscarried no cash field, so a fortress net-cash balance sheet earned nothing beyond the debt bucket's caps.What changed
Quality-aware valuation — a reusable
_quality_factor(r)(ROE, ROCE, operating margin, and a revenue+EPS-CAGR growth blend) widens the acceptable multiple ceilings quadratically, so only genuinely exceptional quality unlocks premium headroom while a low-quality expensive name still floors. DCF held to a 30% cross-check and recentred so fair value scores neutral instead of low.Net cash rewarded, both ways —
net_cash/net_cash_to_market_capare surfaced onRatios(FX-normalized against market cap, handling the Infosys USD-statements/INR-price case). Net cash lowers the effective equity multiple (dampened ex-cash P/E and P/B) and lifts the renamed Balance Sheet bucket via a signed term that also drags net-debt names below debt-free.Weights rebalanced toward durable quality: Valuation 15→10, Profitability 15→17, Competitive Moat 10→12, Balance Sheet (was Debt) 10→11.
Refactor — the net-debt/cash computation is extracted into a shared
finutils.net_debt(fin, info)used by both the DCF equity bridge and the newRatiosfields (one source of truth).Impact (targeted, not blanket inflation)
Old-vs-new on a 42-name synthetic basket: Spearman rank correlation = 0.99, mean total change ≈ 0. Real-data spot checks:
Quality compounders up modestly, leveraged/net-debt names nudged down, banks flat.
Tests
New coverage: quality-premium (premium is fine when quality justifies it, still floors when it doesn't), per-signal monotonicity (raising ROE / operating margin / EPS CAGR / net cash never lowers the valuation bucket), net-cash lift and net-debt drag,
Ratiosnet-cash plumbing incl. an FX-mismatch case, a Spearman rank-preservation guard (≥ 0.90), an archetype-direction table, and a no-blanket-inflation check. Full suite green.Research/education only, not investment advice.