Summary & Valuation
GetSummary returns the aggregated headline numbers; GetValuation returns the value/cost-basis time-series for charting
Two read endpoints under the Portfolio Swagger tag drive the dashboard headline and the chart respectively.
| Method | Path | Service method |
|---|---|---|
GET | /portfolios/{id}/summary | core.Service.FindSummary |
GET | /portfolios/{id}/valuation | core.Service.FindValuation |
GET /portfolios/{id}/summary
Returns just the summary block from the full detail call. Cheap for clients that only need headline numbers.
Success (200)
{
"message": "Portfolio summary fetched successfully",
"data": {
"cUnits": 70, "sUnits": 30,
"totalInv": 125461.19, "curInv": 87822.83,
"curVal": 91000.00, "soldVal": 18750.00, "div": 2000.00,
"unrealPnl": 3177.17, "realPnl": 1234.56,
"totalPnl": 6411.73, "totalPnlPct": 5.11,
"dayPnl": 350.00, "dayPnlPct": 0.39,
"recv": 5801.34
}
}Field semantics
| Field | Meaning |
|---|---|
cUnits | Sum of currently-held units across all active holdings. |
sUnits | Sum of historically sold units. |
totalInv | Sum of total_invested — original cost contributed (excludes proceeds-on-sell reduction). |
curInv | Sum of total_cost_current — open-lot residual cost. Decreases with each SELL. |
curVal | Σ LTP × currentUnits over active holdings. |
soldVal | Sum of gross sell proceeds (qty × price). |
div | Sum of net dividends received. |
unrealPnl | curVal - curInv. |
realPnl | Sum of realized_pnl_display — display realised P&L (cost basis matched, excludes CGT). |
totalPnl | unrealPnl + realPnl + div. |
totalPnlPct | totalPnl / totalInv × 100 — null if totalInv is 0. |
dayPnl | Sum of per-company holdings.DayPnL. See Holdings engine. |
dayPnlPct | dayPnl / (curVal - dayPnl) × 100 — relative to yesterday's closing value. Null if denominator is 0. |
recv | Sum of receivable — the T+2 settlement balance from sells not yet settled. |
Suspended holdings (status != 'A') are excluded from summary. They show up only in suspendedSummary on the full detail call. This keeps headline P&L clean even if a portfolio holds a delisted ticker.
Behaviour
- Internally calls
FindDetailand returns just.summary. Same cache key, same TTL, same ownership guard. So callingsummaryand thenGetwithin 15s is two cache reads, not two DB hits. - 404 on missing or not-yours.
cURL
curl -H "Authorization: Bearer $JWT" \
"$BASE/portfolios/$PID/summary"GET /portfolios/{id}/valuation
Returns a time-series of (value, cost_basis) per trading day for charting.
Query params
| Param | Required | Default | Notes |
|---|---|---|---|
range | ✗ | 3M | One of 1M, 3M, 6M, 1Y, YTD, ALL |
Range maps to a start date via valuationRangeStart. The upper bound is always time.Now() captured once at request start.
Success (200)
{
"message": "Portfolio valuation fetched successfully",
"data": {
"range": "3M",
"series": [
{ "d": "2026-01-27", "v": 50250.0, "cb": 50000.0 },
{ "d": "2026-01-28", "v": 50500.0, "cb": 50000.0 },
...
{ "d": "2026-04-25", "v": 91000.0, "cb": 87822.83 }
]
}
}Empty portfolio → {range, series: []} (200, not 404).
Algorithm
The full event-replay pipeline is documented on the Holdings engine page. Quick summary:
- Pull every trade and corporate action for the portfolio (no range filter — events outside the window still affect state).
- Bulk fetch daily prices for every involved company in one query:
FindBulkPriceHistoryForPortfoliowithANY($1::uuid[]). The bound is[range_start, now]. - Walk events chronologically against trading dates in the price set. For each date, value the portfolio at that day's LTP and emit
{d, v, cb}. - Cache as
portfolio:valuation:<id>:<RANGE>.
Behaviour
- Cache TTL: 10 minutes. Stale-but-bounded; busted by trades/actions/delete.
- 6 cache keys per portfolio: one per range. A write op that changes valuation busts all 6 in
Invalidator.Apply. nowcaptured once before the price query and threaded throughbuildValuationto keep the upper bound consistent across midnight boundaries.- Silent empty series — if there are trades but no daily price rows in range (typically a never-listed company or a sparse company), the response is
{range, series: []}with aslog.Warn. The endpoint never errors on missing market data.
Errors
| Status | Cause |
|---|---|
| 400 | range not in whitelist |
| 401 | missing / invalid JWT |
| 404 | not yours, or doesn't exist |
| 500 | DB read failed (any of the three queries) |
cURL
# Default 3M
curl -H "Authorization: Bearer $JWT" "$BASE/portfolios/$PID/valuation"
# Year-to-date
curl -H "Authorization: Bearer $JWT" "$BASE/portfolios/$PID/valuation?range=YTD"
# All time (since 2000-01-01)
curl -H "Authorization: Bearer $JWT" "$BASE/portfolios/$PID/valuation?range=ALL" | jq '.data.series | length'Range start dates
| Range | Start (relative to now) |
|---|---|
1M | now - 1 month |
3M | now - 3 months |
6M | now - 6 months |
1Y | now - 1 year |
YTD | January 1 of now.Year() (NPT calendar) |
ALL | 2000-01-01 |
Edge cases
- Trade outside range — still applies. The state walks from the portfolio's first event; only emitted bars are bounded.
- No trades at all — empty
series, no warn. - One stock that has never traded post-listing — the daily price table is empty for that company;
seriesmay be empty. Warn logged. - Ranges crossing portfolio creation date — for ranges that start before the first event, the series begins empty (units=0) until the first event lands.
cbandvare both 0 on those leading days.
Worked example
Day 1 (1 month ago): BUY 100 NABIL @ 500 (totalAmount = 50000)
Day 2 ... Day 30: no events; daily price drifts 500 → 600
Day 30: query `GET /portfolios/{id}/valuation?range=1M`
series ≈ [
{ d: day-1, v: 50000, cb: 50000 }, // bought today, LTP=500
{ d: day-2, v: 50500, cb: 50000 }, // LTP=505
...
{ d: day-30, v: 60000, cb: 50000 }, // LTP=600
]v tracks market value; cb only changes when a BUY/SELL/RIGHT/IPO/etc. is applied.
Implementation
- Handler:
internal/modules/portfolio/core/handler.go→GetSummary,GetValuation - Service:
internal/modules/portfolio/core/service.go→FindSummary,FindValuation - Algorithm:
internal/modules/portfolio/core/valuation.go→buildValuation,valuationRangeStart - Cache keys:
internal/platform/cache/keys.go→PortfolioValuationKey,TTLPortfolioValuation - sqlc queries:
internal/platform/database/queries/portfolios.sql