Skip to content

feat: honest peer resolution and relative-to-industry confidence - #13

Merged
YashvantHange merged 1 commit into
mainfrom
feat/relative-peer-basis
Jul 15, 2026
Merged

feat: honest peer resolution and relative-to-industry confidence#13
YashvantHange merged 1 commit into
mainfrom
feat/relative-peer-basis

Conversation

@YashvantHange

Copy link
Copy Markdown
Owner

Fixes the reported "3. Relative to Industry — Not available. No curated peer group matched, and the relative module returned zero metrics with Low confidence (0.37, 0% field coverage)".

The bug

peers._group_for matches by exact ticker membership. KPIT was in no group — and neither were Tata Elxsi, Tata Technologies, LTTS or Cyient; the whole automotive ER&D cohort was absent from the repo. With no peers, relative.py skipped all 7 metrics, and evidence.py then computed:

0.80 (Yahoo) × (0.4 + 0.6 × 0.0) × 1.0 = 0.32   +0.05 corroboration → 0.37 "Low"
reason: "source: Yahoo Finance, Curated (Investo); 0% field coverage; cross-source agreement"

That reason claims two sources cross-checked each other over zero rows. The 0.37 wasn't a low-confidence answer, it was a confident-looking number manufactured from nothing — and it leaked into both report.py's and thesis.py's aggregates as a plain mean. A plausible number from no data is worse than a zero, because a zero is obviously a zero.

⚠️ This reprices published numbers

Deliberate, and the main thing to review:

Case Before After
Relative section with no peer set 0.37 0.00
Every unknown Buffett criterion (buffett.py:295) 0.37 0.00
Report / thesis headline confidence plain mean coverage-weighted mean

The Buffett path was hit by the identical arithmetic. Report-level confidence will move for affected tickers — upward, since an empty module no longer drags the mean down.

The fix

  • Zero coverage earns zero. The 0.4 floor exists so a couple of gaps don't collapse a score; it no longer applies when nothing was computed. The agreement bonus is gated on there being something to agree on.
  • A resolution ladder — curated → keyword match on Yahoo's industry → Finnhub → none. The PeerBasis travels with the comparison and discounts confidence through a new reliability_factor, so a guessed cohort can't read like a deliberate one, and a rank among five hand-picked names can never reach the High tier (curated caps at 0.90 by construction).
  • auto_erd + auto_components, hospitals_diagnostics, capital_goods_defence.
  • A group reframes the narrative, not just outlook/CAGR — Yahoo calls KPIT "Software - Infrastructure" and Cyient "Conglomerates", which points the whole analysis at the wrong drivers. Yahoo's raw string is preserved alongside; hiding the disagreement would be worse than showing it.
  • 10 metrics (+EV/EBITDA, ROA, P/S), with coverage measured against what the peer set can actually rank on — otherwise every Indian company silently loses confidence for a metric Indian peers don't report. RelativeMetric.unit stops renderers guessing by name and printing EV/EBITDA as 3000%.

Dead tickers found en route

Each one silently dropped a company out of its own peer table — the reported symptom, arriving by a different route. Found by scripts/validate_peers.py, added here because no offline test can catch it:

  • TATAMOTORS.NS — superseded by the TMCV/TMPV demerger; both were already in the group
  • SPICEJET.NSSPICEJET.BO (NSE line gone from the provider)
  • LTIM.NS, AKZOINDIA.NS — unresolvable across .NS and .BO; removed rather than guessed at

Verification

ruff + mypy clean; 142 tests pass (103 → 142). test_peers.py was making a live network call on the unknown-ticker path; it's now monkeypatched.

Live, on the actual reported case:

basis=curated  group='Automotive ER&D'  metrics=10  coverage=1.0
confidence=0.77 Medium   (was 0.37 with 0 metrics)

  Metric                  KPIT  Industry   Standing
  ROE                    19.7%     17.1%   top quartile
  Revenue growth         12.0%      4.6%   top quartile
  P/E                    23.7x     31.3x   top quartile
  EV/EBITDA              10.8x     22.4x   top quartile
  Debt/Equity             0.2x      0.1x   below median

Reliance still resolves to oil_gas_energy and Eicher to auto_oem (both are double-booked across groups; a test now pins the first-match-wins order, since appending vs inserting in peers.yaml silently reframes companies).

Fix a section that reported "Not available ... Low confidence (0.37, 0% field coverage)" for any
company outside a curated peer group -- KPIT and the entire automotive ER&D cohort were in none.
With no peers, relative.py computed nothing and evidence.py then scored 0.80 x (0.4 + 0.6x0) =
0.32 plus a +0.05 cross-source agreement bonus awarded over zero rows, so the module claimed two
sources corroborated each other while holding no data. That 0.37 leaked into the report-level and
thesis-level aggregates as a plain mean. Zero coverage now collapses to 0.00 with a reason that
says why, the agreement bonus is gated on there being something to agree on, and ev.aggregate
blends by a coverage-weighted mean so an empty module carries no weight. The same arithmetic was
scoring 0.37 for every unknown Buffett criterion; those reprice to 0.00 as well.

Add an auto_erd peer group (KPITTECH, TATAELXSI, TATATECH, LTTS, CYIENT) plus auto_components,
hospitals_diagnostics and capital_goods_defence, and a resolution ladder in peers.py: curated
membership, then a keyword match on Yahoo's industry, then Finnhub, then none. The resulting
PeerBasis travels on PeerComparison, RelativeComparison and IndustryIntelligence and discounts
confidence via a new evidence.confidence(reliability_factor=...), so a guessed cohort can never
read like a deliberate one and a rank among five hand-picked names can never reach the High tier.
A matched group now reframes the industry narrative too -- KPIT's sub-domains become SDV, ADAS and
EV powertrain rather than "IT services & outsourcing" -- while Yahoo's raw industry string is kept
verbatim alongside, since hiding the disagreement would be worse than showing it.

Extend the metric set to EV/EBITDA, ROA and P/S (7 -> 10), measuring coverage against the metrics
the peer set can actually rank on so that adding a metric Indian peers rarely report does not
silently mark every Indian company down. RelativeMetric now carries its unit, because both
renderers inferred it from the metric name and would have printed EV/EBITDA as 3000%.

Repair four dead tickers that each silently dropped a company out of its own peer table:
TATAMOTORS.NS (superseded by the TMCV/TMPV demerger, both already listed), SPICEJET.NS (BSE only
now), and LTIM.NS/AKZOINDIA.NS (unresolvable across .NS and .BO, removed rather than guessed at).
No offline test can catch this, so scripts/validate_peers.py checks the file against the provider
on demand. Ships per-group provenance and docs/confidence.md covering the formula's reasoning and
its limits.
@YashvantHange
YashvantHange merged commit 980ee0d into main Jul 15, 2026
6 checks passed
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