Shop It Docs
Stock Analysis (Fundamentals)

Peers

Side-by-side ratio comparison + cohort-normalized radar polygon for the focal symbol against up to four sector peers

The peers endpoint composes a focal symbol with up to four peer symbols into a transposed comparison table and a normalized radar polygon. Cross-sector peers are silently filtered into excludedPeers — the comparison only ever shows apples-to-apples sector matches.

MethodPathCache TTL
GET/api/nepse/companies/{symbol}/fundamentals/peers10 min

Request

curl "$BASE/api/nepse/companies/NABIL/fundamentals/peers?symbols=NIBL,NICA,GBIME,KBL"

Query parameters

ParamDefaultNotes
symbolsComma-separated peer symbols, max 4

Symbol parsing rules

All silent — no 4xx for shaping issues:

  • Case-folded to upper, whitespace trimmed.
  • Empty entries dropped.
  • The focal symbol is removed if it appears in the peer list (self-compare is meaningless).
  • Duplicates are dropped with reason: duplicate so FE can hint.
  • Overflow past the cap (4) is silently dropped without polluting excludedPeers.

Response

{
  "message": "ok",
  "data": {
    "symbol": "NABIL",
    "sector": "Commercial Banks",
    "companies": [
      { "symbol": "NABIL", "name": "Nabil Bank Limited", "sector": "Commercial Banks", "isFocal": true },
      { "symbol": "NIBL",  "name": "Nepal Investment Bank Limited", "sector": "Commercial Banks", "isFocal": false }
    ],
    "rows": [
      {
        "key": "pe_ratio",
        "label": "P/E Ratio",
        "unit": "x",
        "inverted": true,
        "formula": "LTP / EPS",
        "values": {
          "NABIL": { "value": 12.76, "source": "computed", "status": "valid" },
          "NIBL":  { "value": 15.40, "source": "computed", "status": "valid" }
        },
        "bestOf": "NABIL"
      }
      /* ... more rows ... */
    ],
    "radar": [
      {
        "symbol": "NABIL",
        "dimensions": {
          "pe_ratio": 1.0,
          "pb_ratio": 0.8,
          "dividend_yield": 0.5,
          "roe": 1.0
        }
      },
      {
        "symbol": "NIBL",
        "dimensions": {
          "pe_ratio": 0.0,
          "pb_ratio": 0.2,
          "dividend_yield": 0.5,
          "roe": 0.0
        }
      }
    ],
    "excludedPeers": [
      { "symbol": "NTC", "reason": "different_sector" }
    ],
    "note": "Sector benchmarks deferred until multi-quarter coverage exists; radar uses cohort min-max only.",
    "dataAsOf": "2026-05-23T14:55:00+05:45",
    "servedAt": "2026-05-23T14:56:30+05:45"
  }
}

Comparison rows

rows[] is one row per metric (the union of valuation + profitability + solvency from the Ratios endpoint — growth is intentionally skipped under v4.5 because every row would be insufficient_history and clutter the polygon with empty axes).

Each row carries:

  • values — symbol → MetricFloat64, with every cohort member keyed in (even when the value is invalid).
  • bestOf — the symbol with the best value given inverted. Empty when fewer than two symbols carry a valid value (a 1-symbol "win" would be misleading).

The metric order is the focal symbol's tab output — so the FE table renders the same row order regardless of peer composition.

Radar polygon

radar[] is one entry per symbol with a dimensions map keyed by metric. Each value is min-max normalized within the cohort:

score = (v - min) / (max - min)         # higher = better
if inverted:  score = 1 - score
if min == max: score = 0.5              # degenerate cohort

A dimension only lands on the polygon when every symbol has a valid value for that metric. A single missing value collapses the axis cohort-wide rather than distorting the polygon shape.

For a 1-symbol cohort (no peers in the same sector) every dimension scores 0.5 — the polygon renders a regular shape rather than collapsing to a point.

Excluded peers

reasonMeaning
different_sectorPeer's sector doesn't match the focal symbol
unknown_symbolPeer not present in nepse_companies
duplicatePeer appears more than once in the query string

excludedPeers is always present (possibly empty) so the FE banner can render unconditionally.

Composition over Ratios

The service reuses the canonical ratio engine from the ratios package:

import "internal/modules/nepse/fundamentals/stock_analysis/ratios"

inputs := ratios.Inputs{
    Registry:    s.taxonomyRegistry(),
    Sector:      sector,
    LTP:         …,
    Outstanding: ratios.OutstandingSharesPtr(core),
    MarketCap:   compute.ComputeMarketCap(core),
    BSItems:     compute.ItemsFromReports(reports, "Balance Sheet"),
    ISItems:     compute.ItemsFromReports(reports, "Income Statement"),
    Dividends:   dividends,
}

rows := ratios.ComputeTab(ratios.TabValuation, inputs)

If the /ratios endpoint returns a value for a symbol, /peers returns the same value for that symbol — there's no recomputation drift.

Caching

KeyTTL
nepse:fundamentals:peers:{FOCAL}:{sortedPeersJoinedByComma}10 min

Peer symbols are sorted before forming the cache key so ?symbols=A,B,C and ?symbols=C,B,A hit the same entry.

Errors

CodeReason
200OK (including the no-peer-symbols case)
404Focal symbol not in nepse_companies
500DB error

Peer symbols that don't exist or sit in a different sector are silently routed to excludedPeers — they never produce a 4xx.