Shop It Docs
TradingView UDF

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/ and datafeeds/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:

  1. Call /config once on mount to learn capabilities.
  2. Call /time every few minutes to stay clock-synced.
  3. Call /symbols?symbol=NABIL to resolve metadata (session, pricescale, …).
  4. Call /history as the user zooms, pans, or scrolls.
  5. 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 /symbols handler strips the NEPSE: prefix (any X:Y format → 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.
  • limit from 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_url or 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 labelinterval valueBackend resolution
1m11
5m55
15m1515
30m3030
1h6060
1DD or 1D1D
1WW or 1W1W
1MM or 1M1M

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:

Resolutiont timestamp means
1, 5, 15, 30, 60Bucket start (Unix seconds)
1D11:00 NPT of the trading date (market open)
1W00:00 NPT of the Monday starting that week
1M00:00 NPT of the 1st of the month

Verifying the integration

Open the browser's Network tab and load your chart. You should see:

  1. GET /config — 200, JSON with flat supports_* booleans.
  2. GET /time — 200, text/plain content-type, body is digits.
  3. GET /symbols?symbol=NABIL or NEPSE:NABIL — 200, JSON with session:"1100-1500:23456".
  4. GET /history?symbol=NABIL&resolution=1D&from=…&to=… — 200, JSON with s:"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_companies with status='A'.
  • Casing doesn't matter — the backend upper-cases on resolution.
  • If you're using NEPSE:NABIL format, 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.
  • session includes the day mask (1100-1500:23456, not 1100-1500).
  • All required fields present (see /symbols reference).

Chart loads but shows no bars.

  • Open Network tab: is /history returning {"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 in nepse_price_history.
  • If ok but 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:23456 with the day mask.

Symbol search box shows "no matches".

  • Check /config has supports_search: true. If false, the backend disabled it.
  • Open Network tab: is /search?query=… returning a bare array? If it's returning {"results":[...]} or null, 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.js and datafeeds/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 polling subscribeBars for v1 — revisit when the Phase 6 WebSocket gateway lands
  • Confirm the backend URL includes /api/nepse/tradingview (not /api/tradingview)