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.
| Method | Path | Cache TTL |
|---|---|---|
GET | /api/nepse/companies/{symbol}/fundamentals/peers | 10 min |
Request
curl "$BASE/api/nepse/companies/NABIL/fundamentals/peers?symbols=NIBL,NICA,GBIME,KBL"Query parameters
| Param | Default | Notes |
|---|---|---|
symbols | — | Comma-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: duplicateso 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 giveninverted. 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 cohortA 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
reason | Meaning |
|---|---|
different_sector | Peer's sector doesn't match the focal symbol |
unknown_symbol | Peer not present in nepse_companies |
duplicate | Peer 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
| Key | TTL |
|---|---|
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
| Code | Reason |
|---|---|
200 | OK (including the no-peer-symbols case) |
404 | Focal symbol not in nepse_companies |
500 | DB error |
Peer symbols that don't exist or sit in a different sector are silently routed to excludedPeers — they never produce a 4xx.