Skip to content

fix: quality-aware scoring so cash-rich, fairly-priced compounders aren't underrated - #21

Merged
YashvantHange merged 2 commits into
mainfrom
fix/scoring-quality-aware
Jul 18, 2026
Merged

fix: quality-aware scoring so cash-rich, fairly-priced compounders aren't underrated#21
YashvantHange merged 2 commits into
mainfrom
fix/scoring-quality-aware

Conversation

@YashvantHange

Copy link
Copy Markdown
Owner

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:

  • P/E, P/B and EV/EBITDA were scored strictly lower-is-better with hard-zero ceilings (40 / 8 / 25) — a premium compounder floored ~3 of 4 terms and lost almost the whole 15-point bucket, with no way for quality to justify a premium.
  • Ratios carried 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 waysnet_cash / net_cash_to_market_cap are surfaced on Ratios (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 new Ratios fields (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:

Ticker Old New Δ
COFORGE.NS 56.5 58.0 +1.5
TCS.NS 63.5 65.8 +2.3 (→ Strong)
ITC.NS 65.4 67.0 +1.6
TATASTEEL.NS (net debt −36%) 40.7 37.8 −2.9
HDFCBANK.NS (bank) 65.6 66.1 +0.5

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, Ratios net-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.

Note: based on main, independent of the open dividend-yield work. The Balance Sheet rename flows through generically (bucket names are consumed by iteration, not hardcoded).

Research/education only, not investment advice.

YashvantHange and others added 2 commits July 18, 2026 13:38
…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
YashvantHange merged commit 7ad34fd into main Jul 18, 2026
0 of 6 checks passed
@YashvantHange
YashvantHange deleted the fix/scoring-quality-aware branch July 18, 2026 08:14
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant