GET /history
OHLCV bars in TradingView UDF format with intraday, daily, weekly, monthly routing and nextTime scroll-back hints
/history is the core endpoint — it returns OHLCV bars for a symbol in a time range. The Charting Library calls it on mount, on every zoom/pan, and as the user scrolls back.
- Method:
GET - Path:
/api/nepse/tradingview/history - Content-Type:
application/json
Query parameters
| Param | Required | Type | Notes |
|---|---|---|---|
symbol | ✓ | string | Ticker. NEPSE:NABIL → NABIL (prefix stripped), upper-cased. Prefix with ^ for indices (^NEPSE) and sector sub-indices (^BANKING) — same endpoints, same response shape. |
resolution | ✓ | string | One of 1, 5, 15, 30, 60, 1D, 1W, 1M. Whitelist-checked. |
from | ✓ | int | Unix seconds, inclusive. Must be ≥ 946684800 (2000-01-01 UTC). |
to | ✓ | int | Unix seconds, inclusive. Must be ≤ now + 48h. |
countback | ✗ | int | Accepted for UDF compatibility; currently unused. |
Validation happens in the handler before the service runs — see Errors.
Success response
{
"s": "ok",
"t": [1735689600, 1735776000, 1735862400],
"o": [500.0, 510.0, 515.0],
"h": [515.0, 520.0, 522.0],
"l": [498.0, 508.0, 513.0],
"c": [510.0, 515.0, 520.0],
"v": [12345, 6789, 9876]
}Note v is emitted as JSON integers (no decimal point) — consistent with volume_precision: 0.
{ "s": "no_data" }No older data exists (or we can't look for it — e.g. the symbol was unknown on the daily path). The Charting Library marks the bar slot empty and stops scrolling back.
{
"s": "no_data",
"nextTime": 1776230100
}No bars in the requested range, but older bars exist. nextTime is the Unix second of the newest bar strictly before from. The library jumps the next fetch to start around there instead of requesting ever-older ranges forever.
type HistoryResponse struct {
S string `json:"s"` // "ok" | "no_data" | "error"
Errmsg string `json:"errmsg,omitempty"` // only when s="error"
T []int64 `json:"t,omitempty"` // bar timestamps (unix s)
O []float64 `json:"o,omitempty"`
H []float64 `json:"h,omitempty"`
L []float64 `json:"l,omitempty"`
C []float64 `json:"c,omitempty"`
V []int64 `json:"v,omitempty"` // int64 — whole shares
NextTime *int64 `json:"nextTime,omitempty"` // only when s="no_data"
}NextTime is a pointer + omitempty so 0 (a valid Unix epoch) is distinguishable from "not set". OHLCV arrays are omitted when s != "ok".
Resolution routing
The service first resolve()s the symbol into a kindStock | kindIndex | kindSubIndex, then picks a data source based on resolution:
Kind picks the table family; resolution picks the aggregation shape. The OHLCV marshal loop is shared across kinds via generic helpers (bucketedToResponse, intradayRowsToResponse), so bar semantics stay identical across stocks and indices.
Intraday (1, 5, 15, 30, 60)
- Tables (by kind):
nepse_intraday_prices(stock),nepse_intraday_indices(index),nepse_intraday_sub_indices(sub-index) — all under the rolling 3-trading-day retention window. - Aggregation: SQL-side via
GetIntradayStockCandles/GetIntradayIndexCandles/GetIntradaySubIndexCandleswith theinterval_secsparameter. For resolution15,interval_secs = 900. Open = first polled_at'sopen; high = MAX; low = MIN; close = last polled_at'sclose; volume = SUM. - Retention guard: if
fromis outside the 3-trading-day window (timezone.IsWithinNTradingDays), the service skips the DB query entirely and returnsintradayNoData(resolved, from)— which attempts to populatenextTimefrom the most recent surviving intraday tick using the kind-appropriateGetNext*IntradayBarBefore. - Bucket timestamp (
t[i]) = bucket-start polled_at as Unix seconds.
Daily (1D)
- Tables:
nepse_price_history/nepse_index_history/nepse_sub_index_history. - Query:
GetStockPriceHistory(company_id, ...),GetIndexHistory(index_id, ...),GetSubIndexHistory(sub_index_id, ...)— return rows as-is. - Bucket timestamp convention: 11:00 NPT of the trading date. Same across kinds so a 1D bar on
NABILand^NEPSElands at the same instant for the same date.
Weekly (1W)
- Tables: same three daily tables, aggregated on the fly by
GetStockWeeklyHistory,GetIndexWeeklyHistory, orGetSubIndexWeeklyHistory. - Bucketing:
date_trunc('week', trade_date)/date_trunc('week', trading_date)— Postgres' ISO week is Monday-start, which matches NEPSE's Mon–Fri trading week. - OHLCV aggregation:
(array_agg(open ORDER BY <date> ASC))[1]for open, MAX for high, MIN for low,(array_agg(close ORDER BY <date> DESC))[1]for close, SUM for volume. - Bucket timestamp: 00:00 NPT of the Monday starting the week.
Monthly (1M)
- Identical to weekly but with
date_trunc('month', ...)against the same three tables. - Bucket timestamp: 00:00 NPT of the 1st of the month.
The nextTime scroll-back contract
When the library asks for [from, to] and gets no_data, it doesn't know whether:
- There's older data it just hasn't asked for yet (keep scrolling back), or
- There's literally no data (stop).
UDF's nextTime field answers that question: if populated, it's a timestamp in the past where data exists. The library then issues its next request with a to near nextTime — skipping huge empty ranges efficiently.
When nextTime is populated
| Kind | Daily/Weekly/Monthly helper | Intraday helper |
|---|---|---|
| Stock | GetNextDailyBarBefore(company_id, from) | GetNextIntradayBarBefore(symbol, from) |
| Index | GetNextIndexDailyBarBefore(index_id, from) | GetNextIndexIntradayBarBefore(index_id, from) |
| Sub-index | GetNextSubIndexDailyBarBefore(sub_index_id, from) | GetNextSubIndexIntradayBarBefore(sub_index_id, from) |
Daily/weekly/monthly nextTime is returned as 11:00 NPT of the next-earlier trading date. Intraday nextTime is returned as the raw polled_at epoch. The dispatch lives in (*Service).dailyNoData and (*Service).intradayNoData, which both switch on resolvedSymbol.kind.
When nextTime is not populated
- Symbol unknown (404-class resolve) —
GetHistoryreturns{s: "no_data"}without even attempting the next-before lookup. - No daily/intraday rows exist before
fromat all (truly out-of-range query). - The "next-before" query errors out — degraded, but still returns a clean
no_data.
Examples
Valid daily range
curl -s "$BASE/history?symbol=NABIL&resolution=1D&from=1735689600&to=1736294400" | jq .{
"s": "ok",
"t": [1735689600, ...],
"o": [506.5, ...],
"h": [517, ...],
"l": [502, ...],
"c": [514, ...],
"v": [29808, ...]
}Empty daily with nextTime
Range entirely after NABIL's last bar (2026-04-15):
curl -s "$BASE/history?symbol=NABIL&resolution=1D&from=1776470400&to=1776707761" | jq .{
"s": "no_data",
"nextTime": 1776230100
}1776230100 decodes to 2026-04-15 11:00 NPT — NABIL's last daily bar, reported using the daily 11:00-NPT convention.
Weekly bucketing demo
# Same range, 1D → 21 bars, 1W → 5 bars
curl -s "$BASE/history?symbol=NABIL&resolution=1D&from=..." | jq '.t | length'
# 21
curl -s "$BASE/history?symbol=NABIL&resolution=1W&from=..." | jq '.t | length'
# 5Each weekly t lands on a Monday at 00:00 NPT:
1773598500 → 2026-03-16 00:00 Mon
1774203300 → 2026-03-23 00:00 Mon
1774808100 → 2026-03-30 00:00 Mon
1775412900 → 2026-04-06 00:00 Mon
1776017700 → 2026-04-13 00:00 MonWeekly OHLCV correctness
For 2026-03-16..22 (one NEPSE week):
| Field | Daily aggregation (manual) | Weekly bar from /history?resolution=1W |
|---|---|---|
| Open | 530 (Monday's open) | 530 |
| High | max across days = 545.9 | 545.9 |
| Low | min across days = 523 | 523 |
| Close | Friday's close = 542.1 | 542.1 |
| Volume | sum of 5 days = 445053 | 445053 |
Error responses
All errors follow UDF shape {"s":"error","errmsg":"..."}. See Errors for full validation rules.
| Condition | Status | errmsg |
|---|---|---|
Missing symbol | 400 | symbol is required |
resolution not in whitelist | 400 | invalid resolution |
from/to missing | 400 | from and to are required |
from/to not an integer | 400 | from must be a unix timestamp / to must be a unix timestamp |
from > to | 400 | from must be <= to |
from < 2000-01-01 or to > now+48h | 400 | timestamps out of range |
| Request exceeded 5s timeout | 504 | timeout |
| DB error after validation | 500 | internal_error |
Performance
- Rate limit: 60 rpm per IP (shared with other TV endpoints).
- Request budget: 5s per request via
context.WithTimeout. On timeout → 504 +timeoutUDF error. - gzip: applied to the TV group; typical 225-bar daily response shrinks from 8.4KB → 3KB (~2.75×).
- Nginx proxy_cache: recommended 5-minute TTL for general /history, 10-second TTL for intraday-with-recent-
to. See Caching.
Edge cases
Market hours: incomplete intraday bars
The currently-forming intraday bar is not returned by /history until the poller flushes it. The poller runs every ~2s during market hours, so /history trails live by at most one poll cycle. TV's default UDFCompatibleDatafeed polls /history every ~10s via its built-in subscribeBars, so the chart's last bar visibly updates within seconds without any extra integration. A tick-level WebSocket path (Phase 6) is specced but deferred — see docs/TRADINGVIEW_UDF_PHASE6.md.
Unknown symbol (any resolution)
resolve() returns a 404-class error → GetHistory short-circuits to {"s":"no_data"} with no nextTime. Without a resolved ID we can't run the kind-specific "next-before" query. This is a signal, not an error — the chart treats it like any other empty response. Applies to unknown stocks (XXX) and unknown ^-prefixed symbols (^XXX) alike.
to > now
Allowed (up to now + 48h) because the Charting Library often sends slightly-in-the-future to for the currently-forming bar. The server simply returns data up to the last persisted bar.
Weekly request within a week that isn't complete yet
If today is Wednesday and the client asks for this week's weekly bar, the aggregation runs on the partial week's daily rows (Mon, Tue, Wed). The resulting bar updates as new daily rows land. This is expected UDF behavior for real-time weekly charts.
Implementation
- Handler:
internal/modules/nepse/tradingview/handler.go→(*Handler).History - Service routing:
internal/modules/nepse/tradingview/service.go→(*Service).GetHistory→resolve+get{Daily,Weekly,Monthly,Intraday}HistoryForKind - Stock helpers:
getStockDailyHistoryByID,getStockWeeklyHistoryByID,getStockMonthlyHistoryByID,getStockIntradayHistory - Index helpers:
getIndexDailyHistory,getIndexWeeklyHistory,getIndexMonthlyHistory,getIndexIntradayHistory - Sub-index helpers:
getSubIndexDailyHistory,getSubIndexWeeklyHistory,getSubIndexMonthlyHistory,getSubIndexIntradayHistory - No-data helpers:
dailyNoData/intradayNoData(kind-aware) - sqlc queries in
internal/platform/database/queries/charts.sql:- Stocks:
GetStockPriceHistory,GetStockWeeklyHistory,GetStockMonthlyHistory,GetIntradayStockCandles,GetNextDailyBarBefore,GetNextIntradayBarBefore - Indices:
GetIndexHistory,GetIndexWeeklyHistory,GetIndexMonthlyHistory,GetIntradayIndexCandles,GetNextIndexDailyBarBefore,GetNextIndexIntradayBarBefore - Sub-indices:
GetSubIndexHistory,GetSubIndexWeeklyHistory,GetSubIndexMonthlyHistory,GetIntradaySubIndexCandles,GetNextSubIndexDailyBarBefore,GetNextSubIndexIntradayBarBefore
- Stocks: