> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobula.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Setup Trading View Charting Library JS for Crypto

> Learn how to setup a Trading View candle chart with crypto real time price updates, and historical candle chart.

Mobula pair data API allows you to fetch real-time and historical price data for any on-chain pair. You can use this data to feed Trading View charting library.

<Tip>
  **Live Demo**: See charts in action on [mtt.gg](https://mtt.gg) - try any token or pair page!

  **Full Source Code**: The complete TradingView implementation is open-source at [github.com/MobulaFi/MTT](https://github.com/MobulaFi/MTT)
</Tip>

### What you'll need

1. Access to trading view codebase repo (need manual approval from Trading View)
2. An API key from the [Dashboard](https://admin.mobula.io) (only for production use, you can use the API without an API key in development mode)
3. A react app (or any other framework) to display the chart

***

## Open Source Reference Implementation

Before diving into the code, check out the production-ready implementation in the MTT (Mobula Trader Terminal) codebase:

| File                                                                                                                | Description                                             |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| [`datafeed.tsx`](https://github.com/MobulaFi/MTT/blob/main/src/components/charts/datafeed.tsx)                      | Complete datafeed with REST + WebSocket OHLCV streaming |
| [`index.tsx`](https://github.com/MobulaFi/MTT/blob/main/src/components/charts/index.tsx)                            | TradingView chart component with theme support          |
| [`token/page.tsx`](https://github.com/MobulaFi/MTT/blob/main/src/app/token/%5Bblockchain%5D/%5Baddress%5D/page.tsx) | Token page with chart integration                       |
| [`pair/page.tsx`](https://github.com/MobulaFi/MTT/blob/main/src/app/pair/%5Bblockchain%5D/%5Baddress%5D/page.tsx)   | Pair page with chart integration                        |

**Live Examples**:

* Token page: [mtt.gg/token/solana:solana/So11111111111111111111111111111111111111112](https://mtt.gg/token/solana:solana/So11111111111111111111111111111111111111112)
* Pair page: [mtt.gg/pair/solana:solana/YOUR\_PAIR\_ADDRESS](https://mtt.gg/pair/solana:solana/YOUR_PAIR_ADDRESS)

***

### Walkthrough

<Steps>
  <Step title="Identify your asset">
    Pick your asset symbol, name or address. If it is a symbol/name, make sure to check case sensitivity and to respect the asset name as listed on Mobula curated token list (explorable [here](https://mobula.io)). If it is an address, make sure to check the blockchain supported (check [here](/rest-api-reference/endpoint/blockchains) for the full list) and to respect the blockchain ID format.

    You can also use pair address directly (if using asset, we will route to the largest liquidity pool for this asset).
  </Step>

  <Step title="Setup Chart Component">
    Let's first setup the main chart component - you must host the trading view lib files inside the public folder of your app, in our
    example, we host them at static/charting\_library.

    Here's a simplified version. For the full production implementation with theme support, loading states, and tool persistence, see the [MTT chart component](https://github.com/MobulaFi/MTT/blob/main/src/components/charts/index.tsx).

    ```typescript theme={null}
    const ChartBox = ({
    baseAsset,
    mobile = false,
    custom_css_url = "../themed.css",
    isPair = false,
    isUsd = true,
    }: ChartBoxProps) => {
    const ref = useRef<HTMLDivElement>(null);
    const { resolvedTheme } = useTheme();
    const isWhiteMode = resolvedTheme === "light";

    useEffect(() => {
    if (!baseAsset) return () => {};

    let freshWidget: IChartingLibraryWidget | null = null;
    import("../../../../../../../../public/static/charting_library").then(
      ({ widget: Widget }) => {
        if (!ref.current) return;

        // Build symbol for display
        const symbol = isPair
          ? `${baseAsset.base?.symbol ?? baseAsset.symbol}/USD`
          : `${baseAsset.symbol}/USD`;

        freshWidget = new Widget({
          // Datafeed with pair/token mode support
          datafeed: Datafeed(baseAsset, isUsd),
          symbol,
          interval: "60" as ResolutionString,

          // Settings
          container: ref.current,
          container_id: ref.current.id,
          library_path: "/static/charting_library/",

          // UI & Behavior
          locale: "en",
          fullscreen: false,
          disabled_features: [
            "header_saveload",
            "header_symbol_search",
            ...(mobile ? ["left_toolbar"] : []),
          ],
          timezone: Intl.DateTimeFormat().resolvedOptions()
            .timeZone as Timezone,
          autosize: true,

          // Theme
          theme: isWhiteMode ? "Light" : "Dark",
          overrides: overrides(isWhiteMode),
          custom_css_url,
        });
      }
    );

    return () => {
      freshWidget?.remove();
    };
    }, [baseAsset, custom_css_url, mobile, isPair, isUsd]);

    return <div ref={ref}></div>;
    };
    ```
  </Step>

  <Step title="Setup the Datafeed with Token & Pair Support">
    The datafeed handles both **token-based** and **pair-based** charts. This is the key difference in the MTT implementation:

    * **Token mode** (`isPair: false`): Uses `asset` parameter to route to the highest liquidity pool
    * **Pair mode** (`isPair: true`): Uses `address` parameter for a specific pool

    ```typescript theme={null}
    export const supportedResolutions = ['1s', '5s', '15s', '30s', '1', '5', '15', '30', '60', '240', '1D', '1W', '1M'];

    const lastBarsCache = new Map();
    const activeSubscriptions = new Map<string, string>();

    type BaseAsset = {
      asset?: string;      // Token address (for token mode)
      address?: string;    // Pair/pool address (for pair mode)
      chainId: string;
      symbol?: string;
      priceUSD?: number;
      isPair?: boolean;
    };

    export const Datafeed = (initialBaseAsset: BaseAsset, isUsd = false) => {
      let baseAsset = initialBaseAsset;

      return {
        // Allow updating the asset without recreating the datafeed
        updateBaseAsset: (newAsset: BaseAsset) => {
          baseAsset = newAsset;
        },

        onReady: (callback: Function) => {
          setTimeout(() => {
            callback({
              supported_resolutions: supportedResolutions,
              supports_time: true,
              supports_marks: false,
            });
          }, 0);
        },

        resolveSymbol: (symbolName: string, onResolve: Function) => {
          setTimeout(() => {
            const price = baseAsset.priceUSD ?? 1;
            onResolve({
              name: symbolName,
              type: 'crypto',
              session: '24x7',
              timezone: 'Etc/UTC',
              minmov: 1,
              pricescale: Math.min(10 ** String(Math.round(10000 / price)).length, 1e16),
              has_intraday: true,
              has_seconds: true,
              supported_resolution: supportedResolutions,
              data_status: 'streaming',
            });
          }, 0);
        },
        // ... getBars and subscribeBars below
      };
    };
    ```
  </Step>

  <Step title="Fetch Historical Data (getBars)">
    The `getBars` function fetches historical OHLCV data. The key is handling both token and pair modes:

    ```typescript theme={null}
    getBars: async (
      _info: any,
      resolution: string,
      params: any,
      onResult: (bars: any[], meta: { noData: boolean }) => void
    ) => {
      const assetId = baseAsset.isPair ? baseAsset.address : baseAsset.asset;
      const normalizedResolution = normalizeResolution(resolution);
      const cacheKey = `${assetId}-${normalizedResolution}`;

      // TradingView provides timestamps in seconds, convert to ms
      const fromMs = params.from * 1000;
      const toMs = params.to * 1000;

      try {
        const client = getMobulaClient();
        
        // Build request params based on mode (token vs pair)
        const requestParams: any = {
          from: fromMs,
          to: toMs,
          amount: params.countBack,
          usd: `${isUsd}`,
          period: normalizedResolution,
          blockchain: baseAsset.chainId
        };

        if (baseAsset.isPair) {
          // Pair mode: use pool address directly
          requestParams.address = baseAsset.address;
          requestParams.mode = 'pool';
        } else {
          // Token mode: use asset address, routes to highest liquidity pool
          requestParams.asset = baseAsset.asset;
          requestParams.mode = 'asset';
        }

        const response = await client.fetchMarketHistoricalPairData(requestParams);
        const bars = response.data || [];
        
        onResult(bars, { noData: !bars.length });

        // Cache last bar for real-time updates
        if (bars.length > 0) {
          lastBarsCache.set(cacheKey, bars[bars.length - 1]);
        }
      } catch (err) {
        console.error('Error fetching bars:', err);
        onResult([], { noData: true });
      }
    }
    ```
  </Step>

  <Step title="Subscribe to Real-Time OHLCV Stream">
    The MTT implementation uses Mobula's WebSocket `ohlcv` stream for real-time candle updates:

    ```typescript theme={null}
    subscribeBars: async (
      _info: any,
      resolution: string,
      onRealtime: (bar: any) => void,
      subscriberUID: string
    ) => {
      const client = getMobulaClient();
      if (!client) return;

      const assetId = baseAsset.isPair ? baseAsset.address : baseAsset.asset;
      const key = `${assetId}-${subscriberUID}`;
      const normalizedResolution = normalizeResolution(resolution);
      const cacheKey = `${assetId}-${normalizedResolution}`;

      // Unsubscribe from existing subscription if any
      if (activeSubscriptions.has(key)) {
        try {
          await client.streams.unsubscribe('ohlcv', activeSubscriptions.get(key)!);
        } catch {}
        activeSubscriptions.delete(key);
      }

      try {
        // Build subscription params based on mode
        const subscribeParams: any = {
          period: normalizedResolution,
          chainId: baseAsset.chainId,
        };

        if (baseAsset.isPair) {
          subscribeParams.address = baseAsset.address;
        } else {
          subscribeParams.asset = baseAsset.asset;
        }

        // Subscribe to OHLCV stream
        const subId = client.streams.subscribe(
          'ohlcv',
          subscribeParams,
          (candle: any) => {
            if (!candle?.time) return;

            // Handle candle timestamp format
            const normalizedCandle = {
              ...candle,
              time: candle.time > 10000000000 ? candle.time / 1000 : candle.time
            };

            onRealtime(normalizedCandle);
            lastBarsCache.set(cacheKey, normalizedCandle);
          }
        );
        
        activeSubscriptions.set(key, subId);
      } catch (err) {
        console.error('Error subscribing to OHLCV stream', err);
      }
    },

    unsubscribeBars: async (subscriberUID: string) => {
      const client = getMobulaClient();
      const assetId = baseAsset.isPair ? baseAsset.address : baseAsset.asset;
      const key = `${assetId}-${subscriberUID}`;
      const subId = activeSubscriptions.get(key);

      if (subId && client) {
        try {
          await client.streams.unsubscribe('ohlcv', subId);
        } catch (err) {
          console.error('Unsubscribe error:', err);
        }
        activeSubscriptions.delete(key);
      }
    }
    ```
  </Step>

  <Step title="Resolution Normalization">
    The MTT datafeed supports sub-second resolutions. Here's the normalization function:

    ```typescript theme={null}
    const normalizeResolution = (resolution: string): string => {
      switch (resolution) {
        case '1S':
        case '1s':
          return '1s';
        case '5S':
        case '5s':
          return '5s';
        case '15S':
        case '15s':
          return '15s';
        case '30S':
        case '30s':
          return '30s';
        case '1':
        case '1m':
          return '1m';
        case '5':
        case '5m':
          return '5m';
        case '15':
        case '15m':
          return '15m';
        case '60':
        case '1h':
          return '1h';
        case '240':
        case '4h':
          return '4h';
        case '1D':
        case '1d':
          return '1d';
        case '1W':
        case '1w':
          return '1w';
        case '1M':
        case '1month':
          return '1M';
        default:
          return resolution;
      }
    };
    ```
  </Step>
</Steps>

***

## Token Page vs Pair Page

The MTT terminal has two types of pages that use charts differently:

### Token Page (`/token/[blockchain]/[address]`)

Uses the **asset mode** which automatically routes to the highest liquidity pool:

```typescript theme={null}
// Token page passes isPair: false
<TradingViewChart
  baseAsset={{
    address: tokenData.address,  // Token contract address
    blockchain: blockchain,
    symbol: tokenData.symbol,
    priceUSD: tokenData.priceUSD,
  }}
  isPair={false}  // Asset mode
  isUsd={true}
/>
```

### Pair Page (`/pair/[blockchain]/[address]`)

Uses the **pair mode** for a specific pool/pair:

```typescript theme={null}
// Pair page passes isPair: true
<TradingViewChart
  baseAsset={{
    address: pairData.address,  // Pool/pair address
    blockchain: blockchain,
    symbol: pairData.base?.symbol,
    priceUSD: pairData.base?.priceUSD,
    base: pairData.base,
    quote: pairData.quote,
  }}
  isPair={true}  // Pair mode
  isUsd={true}
/>
```

***

## Supported Resolutions

The MTT implementation supports these resolutions:

| Resolution   | Period     | Description          |
| ------------ | ---------- | -------------------- |
| `1s`         | 1 second   | Sub-second precision |
| `5s`         | 5 seconds  |                      |
| `15s`        | 15 seconds |                      |
| `30s`        | 30 seconds |                      |
| `1` / `1m`   | 1 minute   |                      |
| `5` / `5m`   | 5 minutes  |                      |
| `15` / `15m` | 15 minutes |                      |
| `60` / `1h`  | 1 hour     |                      |
| `1D` / `1d`  | 1 day      |                      |
| `1W` / `1w`  | 1 week     |                      |
| `1M`         | 1 month    |                      |

***

## Full Example: Complete Datafeed

For the complete production-ready datafeed with all edge cases handled, see:

**[src/components/charts/datafeed.tsx](https://github.com/MobulaFi/MTT/blob/main/src/components/charts/datafeed.tsx)**

Key features in the production implementation:

* Request deduplication with `pendingRequests` map
* Last bar caching for smooth real-time updates
* First candle gap handling (fills gaps between last historical bar and first streaming candle)
* Proper subscription cleanup
* Support for both token and pair modes

***

### Need help?

Can't find what you're looking for? Reach out to us, response times \< 1h.

<CardGroup>
  <Card title="Support" icon="Telegram" href="https://t.me/mobuladevelopers">
    Telegram
  </Card>

  <Card title="Support" icon="Slack" href="https://join.slack.com/t/mobulaapi/shared_invite/zt-29zrrpjnl-I0tyD73sy7zKy8q~KLL3Ug">
    Slack
  </Card>

  <Card title="Support" icon="Discord" href="https://discord.gg/JVT7xKm3AD">
    Discord
  </Card>

  <Card title="Need help?" icon="envelope" href="mailto:contact@mobulalabs.org">
    Email
  </Card>
</CardGroup>
