Shop It Docs
Stock Analysis (Fundamentals)

Fair Value

Two-method estimate (Graham + Earnings-based) with average and delta vs LTP — no verdict, FE renders the under/fair/over cue from deltaPercent

The fair-value endpoint returns two independent valuation methods, their average, and the percent delta vs LTP. The endpoint does not emit a categorical verdict label ("under-valued" / "fair" / "over-valued") — FE renders the indicator from deltaPercent to keep the categorical bands a frontend concern.

MethodPathCache TTL
GET/api/nepse/companies/{symbol}/fundamentals/fair-value10 min

Request

No query parameters.

curl "$BASE/api/nepse/companies/NABIL/fundamentals/fair-value"

Response

{
  "message": "ok",
  "data": {
    "symbol": "NABIL",
    "sector": "Commercial Banks",
    "ltp": 542.5,
    "graham": {
      "value": { "value": 309.23, "source": "computed", "status": "valid" },
      "formula": "sqrt(22.5 * eps * bookValuePerShare)",
      "inputs": {
        "eps":               { "value": 42.5, "source": "mdp",      "status": "valid" },
        "bookValuePerShare": { "value": 100,  "source": "computed", "status": "valid" },
        "sectorCohortN": 0
      }
    },
    "earnings": {
      "value": { "source": "computed", "status": "insufficient_cohort" },
      "formula": "sectorAvgPe * eps",
      "inputs": {
        "eps":         { "value": 42.5, "source": "mdp",      "status": "valid" },
        "sectorAvgPe": {                "source": "computed", "status": "insufficient_cohort" },
        "sectorCohortN": 0
      }
    },
    "average": {
      "value":        { "value": 309.23, "source": "computed", "status": "valid" },
      "deltaPercent": -42.99
    },
    "disclaimer": "Indicative estimate. Not financial advice.",
    "dataAsOf": "2026-05-23T14:55:00+05:45",
    "servedAt": "2026-05-23T14:56:30+05:45"
  }
}

The two methods

Graham

fairValue = sqrt(22.5 × EPS × BookValuePerShare)

Benjamin Graham's classic conservative anchor (22.5 = "P/E of 15 × P/B of 1.5 = defensively priced stock"). The formula is undefined for non-positive inputs — graham.value.status becomes negative_input when either EPS or BVPS is ≤ 0.

Earnings-based

fairValue = sectorAvgPE × EPS

sectorAvgPE is the unweighted mean of valid P/Es across the focal's sector, with the focal symbol excluded. Cohort gating:

  • Skip peers with ltp ≤ 0, eps ≤ 0, or unparseable items_map.
  • Require ≥ 3 valid peers (focal excluded). Below that, sectorAvgPE.status = insufficient_cohort and the earnings-based method collapses too.

inputs.sectorCohortN reports the count of peers that survived gating so FE can tooltip "based on N peers".

Average + delta

if both methods valid:        avg = (graham + earnings) / 2
elif only graham valid:       avg = graham
elif only earnings valid:     avg = earnings
else:                         avg.status = upstream_missing

deltaPercent = (avg - LTP) / LTP × 100   if LTP > 0 else null

deltaPercent is positive when fair value > LTP (under-valued cue) and negative when fair value < LTP (over-valued cue).

The endpoint deliberately ships only a number. The FE chooses the buckets (< -10% = over-valued, > +10% = under-valued, between = fair) so the threshold can evolve without an API change.

Method anatomy

Both graham and earnings follow the same envelope:

type Method = {
  value: MetricFloat64;
  formula: string;     // "sqrt(22.5 * eps * bookValuePerShare)" or "sectorAvgPe * eps"
  inputs: {
    eps?:               MetricFloat64;
    bookValuePerShare?: MetricFloat64;   // Graham only
    sectorAvgPe?:       MetricFloat64;   // Earnings only
    sectorCohortN: number;               // 0 on Graham; cohort count on Earnings
  };
};

Returning the inputs alongside the result lets FE render the breakdown (EPS 42.30 × Sector P/E 11.80 = 502.14) without a second round-trip.

Status precedence

When inputs disagree, the worst input wins:

Graham inputsgraham.value.status
Both valid (positive)valid
Either ≤ 0negative_input
Either upstream_missingupstream_missing
Either insufficient_historyinsufficient_history
Earnings inputsearnings.value.status
Both valid (EPS positive)valid
EPS ≤ 0negative_input
sectorAvgPe.status setthat status (e.g. insufficient_cohort)
EPS upstream_missingupstream_missing

Caching

KeyTTL
nepse:fundamentals:fair-value:{SYMBOL}10 min

10 minutes matches the other taxonomy-driven endpoints since the inputs (LTP + financial reports + sector cohort) refresh on the same cadence.

Errors

CodeReason
200OK (even when both methods collapse to invalid)
404Symbol not in nepse_companies
500DB error during snapshot or cohort reads

dataAsOf

The oldest signal across:

  • The focal's LTP fetched_at and report updated_at timestamps.
  • Every cohort peer's LTP fetched_at and report.updated_at.

A 6-day-old peer IS makes the sector P/E stale — surfacing the peer's staleness on the focal's response is the honest signal.