Frontend Integration
Wiring TradingView's Charting Library to the Bullhouse NEPSE UDF backend
This guide shows how to render a NEPSE chart in a browser using TradingView's Charting Library and this backend's UDF endpoints.
Prerequisites
- TradingView Charting Library (private download from TradingView — not npm). Place
charting_library/anddatafeeds/udf/under your public assets. - Backend base URL — e.g.
https://api.ranjanyadav.com.np/api/nepse/tradingview. - The backend's CORS policy must allow your origin (configured globally in the router).
The free Lightweight Charts library does NOT consume UDF. Only the full Charting Library (or Advanced Charts) works with this backend. If you need a simpler chart, consume /history directly and feed the OHLCV arrays into a different library.
Minimal mount
<div id="tv-chart" style="height: 600px;"></div>
<script type="text/javascript" src="/charting_library/charting_library.standalone.js"></script>
<script type="text/javascript" src="/datafeeds/udf/dist/bundle.js"></script>
<script>
const datafeed = new Datafeeds.UDFCompatibleDatafeed(
'https://api.ranjanyadav.com.np/api/nepse/tradingview',
);
new TradingView.widget({
container: 'tv-chart',
library_path: '/charting_library/',
datafeed,
symbol: 'NEPSE:NABIL',
interval: 'D',
timezone: 'Asia/Kathmandu',
locale: 'en',
fullscreen: false,
autosize: true,
theme: 'light',
});
</script>That's the complete wiring. The library will:
- Call
/configonce on mount to learn capabilities. - Call
/timeevery few minutes to stay clock-synced. - Call
/symbols?symbol=NABILto resolve metadata (session, pricescale, …). - Call
/historyas the user zooms, pans, or scrolls. - Call
/search?query=…as the user types in the symbol picker.
Charting an index or sub-index
Indices use the same endpoints and mount code; only the symbol string changes. The ^ prefix marks the index namespace and round-trips through full_name:
// NEPSE Index
new TradingView.widget({
container: 'tv-chart',
library_path: '/charting_library/',
datafeed,
symbol: 'NEPSE:^NEPSE',
interval: 'D',
timezone: 'Asia/Kathmandu',
});
// Banking sector sub-index
new TradingView.widget({
container: 'tv-chart',
library_path: '/charting_library/',
datafeed,
symbol: 'NEPSE:^BANKING',
interval: '1W',
timezone: 'Asia/Kathmandu',
});The 17 available indices:
- Top-level:
^NEPSE,^SENSITIVE,^FLOAT,^SFLOAT - Sectors:
^BANKING,^DEVBANK,^FINANCE,^HOTELS,^HYDRO,^INVEST,^LIFEINS,^MFG,^MICROFIN,^MUTUAL,^NONLIFE,^OTHERS,^TRADING
Typing ^ in the Charting Library's symbol picker surfaces every index; typing letters alone keeps stocks ranked first. Both type: "stock" and type: "index" are returned by /search, so the picker's type filter can narrow the list.
Handling the NEPSE: prefix
UDF uses full_name (NEPSE:NABIL) as the canonical identifier. The Charting Library sends it back to the backend on resolution:
- The backend's
/symbolshandler strips theNEPSE:prefix (anyX:Yformat →Y) before the DB lookup. - Your frontend code can use either form — both resolve.
// Both work:
new TradingView.widget({ symbol: 'NEPSE:NABIL', ... });
new TradingView.widget({ symbol: 'NABIL', ... });React example
import { useEffect, useRef } from 'react';
declare global {
interface Window {
TradingView: any;
Datafeeds: any;
}
}
const BASE = process.env.NEXT_PUBLIC_NEPSE_API + '/tradingview';
export function NepseChart({ symbol }: { symbol: string }) {
const ref = useRef<HTMLDivElement>(null);
const widgetRef = useRef<any>(null);
useEffect(() => {
if (!ref.current) return;
const datafeed = new window.Datafeeds.UDFCompatibleDatafeed(BASE);
widgetRef.current = new window.TradingView.widget({
container: ref.current,
library_path: '/charting_library/',
datafeed,
symbol: `NEPSE:${symbol}`,
interval: 'D',
timezone: 'Asia/Kathmandu',
locale: 'en',
autosize: true,
theme: 'light',
});
return () => {
widgetRef.current?.remove?.();
};
}, [symbol]);
return <div ref={ref} style={{ height: 600 }} />;
}Charting Library cannot render server-side. Load it client-only.
'use client';
import dynamic from 'next/dynamic';
const NepseChart = dynamic(() => import('./NepseChart'), { ssr: false });
export default function ChartPage({ params }: { params: { symbol: string } }) {
return <NepseChart symbol={params.symbol} />;
}Load the charting library scripts once in app/layout.tsx:
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Script src="/charting_library/charting_library.standalone.js" strategy="beforeInteractive" />
<Script src="/datafeeds/udf/dist/bundle.js" strategy="beforeInteractive" />
{children}
</body>
</html>
);
}Symbol picker
The picker comes for free with supports_search: true. As the user types, the library hits /search?query=…&limit=… and renders the []SearchResult as a dropdown.
- Results are prefix-boosted: symbol-prefix matches come before name-substring matches.
limitfrom the library is typically 30; the backend clamps to[1, 100].- No extra code required — if you want to customize the picker UI you can intercept via
custom_css_urlor use the library's search callbacks.
Resolutions
Expose the intervals you want in the toolbar via the interval prop or via enabled_features/disabled_features.
| UI label | interval value | Backend resolution |
|---|---|---|
| 1m | 1 | 1 |
| 5m | 5 | 5 |
| 15m | 15 | 15 |
| 30m | 30 | 30 |
| 1h | 60 | 60 |
| 1D | D or 1D | 1D |
| 1W | W or 1W | 1W |
| 1M | M or 1M | 1M |
Intraday bars beyond 3 trading days ago return no_data with a nextTime hint. The library will then reissue its request targeting that hint — this is by design. Don't try to suppress intraday on the frontend; let the backend handle it.
Live subscribeBars
v1 uses polling. UDFCompatibleDatafeed's built-in subscribeBars polls /history every ~10s for the most recent bar — no custom code needed. Bars update at poll cadence, which is fine for candles: a 10s lag is imperceptible on a 1-minute bar and invisible above that. Mount the widget as shown in Minimal mount and live updates just work.
A WebSocket gateway (Phase 6) is specced but deferred. The default polling path absorbs production load easily up to a few hundred concurrent chart viewers thanks to nginx caching. Build the WS path only when measured pain appears: laggy charts, /history throughput ceiling, or >500 concurrent viewers. See docs/TRADINGVIEW_UDF_PHASE6.md for the parked design.
When the WS gateway is eventually wired up, replace the default datafeed with a custom one that overrides subscribeBars / unsubscribeBars to attach the WS subscription. The parked plan keeps the server-side contract simple: ticks go out over WS, the browser aggregates to the current resolution inside subscribeBars. Don't aggregate server-side per subscriber — it wastes CPU and makes the socket stateful.
Timezone and session
Always set timezone: 'Asia/Kathmandu' on the widget. This tells the Charting Library to interpret the session string (1100-1500:23456) in NPT — so the session shaded region, daily-bar placement, and "today" marker all line up with NEPSE hours.
Bar timestamp conventions emitted by the backend:
| Resolution | t timestamp means |
|---|---|
1, 5, 15, 30, 60 | Bucket start (Unix seconds) |
1D | 11:00 NPT of the trading date (market open) |
1W | 00:00 NPT of the Monday starting that week |
1M | 00:00 NPT of the 1st of the month |
Verifying the integration
Open the browser's Network tab and load your chart. You should see:
GET /config— 200, JSON with flatsupports_*booleans.GET /time— 200,text/plaincontent-type, body is digits.GET /symbols?symbol=NABILorNEPSE:NABIL— 200, JSON withsession:"1100-1500:23456".GET /history?symbol=NABIL&resolution=1D&from=…&to=…— 200, JSON withs:"ok"and OHLCV arrays.
If any of those show up as HTML, plain text, or {"message":...,"data":...}, you're hitting the wrong URL — double-check that your base ends in /api/nepse/tradingview with no trailing slash.
Common integration issues
Backend returned 404 {"errmsg":"unknown_symbol"}.
- Check that the ticker exists in
nepse_companieswithstatus='A'. - Casing doesn't matter — the backend upper-cases on resolution.
- If you're using
NEPSE:NABILformat, the backend strips the prefix correctly.
Library's own error, not ours. Means /symbols returned a malformed payload or the library couldn't parse it. Confirm:
- No envelope (
{data, message}) — must be root-level. sessionincludes the day mask (1100-1500:23456, not1100-1500).- All required fields present (see
/symbolsreference).
Chart loads but shows no bars.
- Open Network tab: is
/historyreturning{"s":"ok", "t":[...]}or{"s":"no_data"}? - If
no_data, the range requested is outside your data. Zoom to a known-good range (e.g. last 30 days) or check that daily rows exist innepse_price_history. - If
okbut empty arrays, something is wrong — should never happen.
Daily bars appearing in the wrong day or session shading misaligned.
- Set
timezone: 'Asia/Kathmandu'on the widget — this is not the server's zone, it's the library's interpretation zone. - Check that the response session field is exactly
1100-1500:23456with the day mask.
Symbol search box shows "no matches".
- Check
/confighassupports_search: true. Iffalse, the backend disabled it. - Open Network tab: is
/search?query=…returning a bare array? If it's returning{"results":[...]}ornull, the search UI won't render. - Check CORS — the library's XHR might be blocked.
Production checklist
- Set
timezone: 'Asia/Kathmandu'on all widgets - Prefix symbols as
NEPSE:<ticker>for consistency with TradingView's save-state format - Load
charting_library.standalone.jsanddatafeeds/udf/dist/bundle.js— not the development variants - Ensure the library assets are served with long
Cache-Control: public, max-age=31536000, immutable(they're versioned) - Verify CORS allows your production origin
- Rely on
UDFCompatibleDatafeed's built-in pollingsubscribeBarsfor v1 — revisit when the Phase 6 WebSocket gateway lands - Confirm the backend URL includes
/api/nepse/tradingview(not/api/tradingview)