> ## Documentation index
> Fetch the complete documentation index at: https://docs.predictefy.com/llms.txt
> Use it to discover every available page before exploring further.

# TradingView charts

> Build a capability-qualified TradingView Advanced Charts datafeed on Predictefy from fetchOHLCV history and supported live streams.

:::note[Advanced Charts is licensed separately]
TradingView licenses Advanced Charts and gives you access to `charting_library`
directly. Predictefy ships the data only — no charting library, chart component, or
hosted chart UI.
:::

## What you're building

An Advanced Charts datafeed has two data paths: historical bars from
`fetchOHLCV`, then updates to the forming bar from the `trades` WebSocket channel.
The chart integration stays the same across venues, but live-trade availability does
not. Switch `venue` and supply that venue's native `outcomeId` / `marketId` to change
the market source, then check the per-venue table below.

This guide wires one configured outcome into the five datafeed methods Advanced Charts
calls: `onReady`, `resolveSymbol`, `getBars`, `subscribeBars`, and `unsubscribeBars`.

## Datafeed contract

Advanced Charts names these resolutions `1S`, `5S`, `10S`, `30S`, `1`, `5`, `15`, `30`,
`60`, `240`, `360`, and `1D`. Predictefy's matching server resolutions are `1s`, `5s`,
`10s`, `30s`, `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `6h`, and `1d`:

```ts
const SERVER_RESOLUTION = {
  '1S': '1s',
  '5S': '5s',
  '10S': '10s',
  '30S': '30s',
  '1': '1m',
  '5': '5m',
  '15': '15m',
  '30': '30m',
  '60': '1h',
  '240': '4h',
  '360': '6h',
  '1D': '1d',
} as const;

const BAR_MS = {
  '1S': 1_000,
  '5S': 5_000,
  '10S': 10_000,
  '30S': 30_000,
  '1': 60_000,
  '5': 300_000,
  '15': 900_000,
  '30': 1_800_000,
  '60': 60 * 60_000,
  '240': 14_400_000,
  '360': 21_600_000,
  '1D': 24 * 60 * 60_000,
} as const;

const DATAFEED_CONFIGURATION = {
  supported_resolutions: ['1S', '5S', '10S', '30S', '1', '5', '15', '30', '60', '240', '360', '1D'],
};

function createPredictefyDatafeed(options) {
  const getBars = createGetBars(options);
  const { subscribeBars, unsubscribeBars } = createLiveBars(options);

  return {
    onReady(callback) {
      setTimeout(() => callback(DATAFEED_CONFIGURATION), 0);
    },
    resolveSymbol(_symbolName, onResolved) {
      setTimeout(() => onResolved(options.symbolInfo), 0);
    },
    getBars,
    subscribeBars,
    unsubscribeBars,
  };
}
```

Pass `symbolInfo` in the shape required by your licensed Advanced Charts build, with the
same `supported_resolutions`. This factory represents one outcome, so every symbol lookup
resolves to that configured object.

| Venue       | Sub-minute outcomeId format                | Example               |
| ----------- | ------------------------------------------ | --------------------- |
| polymarket  | CLOB asset id                              | `11198861…`           |
| kalshi      | ticker (YES view) / `ticker-NO` (NO view)  | `KXBTC15M-…-00`       |
| hyperliquid | full HIP-4 asset id (venue coin uses `#N`) | `100001730` (`#1730`) |
| sxbet       | `marketHash#outcomeIndex`                  | `0x3f…9a#0`           |
| myriad      | `networkId:marketId:outcomeIdx`            | `42220:1320:0`        |

:::caution[Resolution floor]
Sub-minute history is forward-only from the start of each venue's capture coverage.
Requests before that point return empty data rather than fabricated bars. 1m/1h/1d
are stored; 5m/15m/30m/4h/6h are aggregated server-side. Live trades update the
forming bar at the selected resolution. Capture and storage availability do not imply
continuous freshness: gaps can exist, and temporarily unavailable history
returns retryable `503 HISTORY_UNAVAILABLE`.
:::

## History via fetchOHLCV

The REST endpoint is `GET {PREDICTEFY_API_URL}/api/{venue}/fetchOHLCV`. Your
developer dashboard shows the API origin. The endpoint accepts the
venue-native `outcomeId`, `resolution`, optional `start` / `end` as ISO timestamps or
epoch milliseconds, and an optional `limit` of up to 5,000 candles. Send the same
`pk_live_…` key used for streaming as a bearer token.

`periodParams.from` and `periodParams.to` are converted from seconds to epoch
milliseconds. `countBack` becomes `limit`, capped at 5,000:

```ts
type HistoryCandle = {
  timestamp: number;
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number | null;
  source: 'official' | 'onchain' | 'write-forward' | 'derived';
  sourceType: 'true-candle' | 'point-derived' | 'trade-derived' | 'book-derived' | 'rollup';
  quality: 'ok' | 'partial' | 'suspect' | 'mixed';
  isTrueCandle: boolean;
};

type HistoryResponse = {
  data: HistoryCandle[];
  meta?: unknown;
};

function createGetBars({ apiBaseUrl, apiKey, venue, outcomeId, onHistoryMeta = () => undefined }) {
  return async function getBars(
    _symbolInfo,
    resolution,
    periodParams,
    onHistoryCallback,
    onErrorCallback,
  ) {
    const serverResolution = SERVER_RESOLUTION[resolution];
    if (!serverResolution) {
      onErrorCallback(`Unsupported resolution: ${resolution}`);
      return;
    }

    const params = new URLSearchParams({
      outcomeId,
      resolution: serverResolution,
      start: String(periodParams.from * 1000),
      end: String(periodParams.to * 1000),
      limit: String(Math.max(1, Math.min(periodParams.countBack, 5000))),
    });

    try {
      const response = await fetch(`${apiBaseUrl}/api/${venue}/fetchOHLCV?${params.toString()}`, {
        headers: { Authorization: `Bearer ${apiKey}` },
      });
      if (!response.ok) throw new Error(`History request failed: HTTP ${response.status}`);

      const payload = (await response.json()) as HistoryResponse;
      if (!Array.isArray(payload.data)) throw new Error('History response has no data array');
      onHistoryMeta(payload.meta);

      const bars = payload.data
        .map((row) => ({
          time: row.timestamp,
          open: row.open,
          high: row.high,
          low: row.low,
          close: row.close,
          volume: row.volume ?? 0,
        }))
        .sort((a, b) => a.time - b.time);

      onHistoryCallback(bars, { noData: bars.length === 0 });
    } catch (error) {
      onErrorCallback(error instanceof Error ? error.message : 'History request failed');
    }
  };
}
```

The response `meta` carries request-level provenance. Each row also keeps its own
`source`, `sourceType`, `quality`, and `isTrueCandle` fields, even though Advanced Charts
only needs the OHLCV fields above. Page a longer range with `start` / `end`; one call never
returns more than 5,000 candles.

## Live bars from the trades channel

Browser clients connect to the WebSocket origin shown in the dashboard, using the
`/v1/stream` path. The first frame authenticates; the second subscribes to the
venue-native market id:

```json
{ "op": "auth", "apiKey": "pk_live_…" }
```

```json
{ "op": "subscribe", "channel": "trades", "venue": "polymarket", "marketId": "<asset_id>" }
```

The server's trade frame is `{ type, venue, marketId, data, ts }`, where `data` is a
`MarketTrade`:

```ts
type MarketTrade = {
  id: string;
  time: string;
  timestamp: number;
  type: 'Buy' | 'Sell';
  usd: number;
  outcome: string;
  outcomeIndex?: number | null;
  shares: number;
  price: number;
  maker: 'polymarket' | 'kalshi' | 'gemini' | 'limitless' | 'myriad' | 'hyperliquid' | 'sxbet';
  transactionHash: string;
  wallet?: string | null;
  counterparty?: string | null;
};

type TradeFrame = {
  type: 'trade';
  venue: string;
  marketId: string;
  data: MarketTrade;
  ts: number;
};
```

This implementation has the three required bar transitions: cold-start from the first
trade, extend the current bucket, or roll into a new bucket whose open is the prior close.
Trade `shares` accumulate into the forming bar's volume.

```ts
function createLiveBars({ streamUrl, apiKey, venue, marketId }) {
  const sockets = new Map<string, WebSocket>();

  function subscribeBars(
    _symbolInfo,
    resolution,
    onRealtimeCallback,
    subscriberUID,
    _onResetCacheNeededCallback,
  ) {
    const bucketMs = BAR_MS[resolution];
    if (!bucketMs) throw new Error(`Unsupported resolution: ${resolution}`);

    sockets.get(subscriberUID)?.close();
    const socket = new WebSocket(streamUrl);
    sockets.set(subscriberUID, socket);
    let currentBar;

    socket.addEventListener('open', () => {
      socket.send(JSON.stringify({ op: 'auth', apiKey }));
      socket.send(JSON.stringify({ op: 'subscribe', channel: 'trades', venue, marketId }));
    });

    socket.addEventListener('message', (event) => {
      let frame: Partial<TradeFrame>;
      try {
        frame = JSON.parse(String(event.data));
      } catch {
        return;
      }

      if (
        frame.type !== 'trade' ||
        frame.venue !== venue ||
        frame.marketId !== marketId ||
        !frame.data
      ) {
        return;
      }

      const { timestamp, price, shares } = frame.data;
      const bucketStart = Math.floor(timestamp / bucketMs) * bucketMs;

      if (!currentBar) {
        currentBar = {
          time: bucketStart,
          open: price,
          high: price,
          low: price,
          close: price,
          volume: shares,
        };
      } else if (bucketStart === currentBar.time) {
        currentBar = {
          ...currentBar,
          high: Math.max(currentBar.high, price),
          low: Math.min(currentBar.low, price),
          close: price,
          volume: currentBar.volume + shares,
        };
      } else if (bucketStart > currentBar.time) {
        const open = currentBar.close;
        currentBar = {
          time: bucketStart,
          open,
          high: Math.max(open, price),
          low: Math.min(open, price),
          close: price,
          volume: shares,
        };
      } else {
        return;
      }

      onRealtimeCallback({ ...currentBar });
    });
  }

  function unsubscribeBars(subscriberUID) {
    sockets.get(subscriberUID)?.close();
    sockets.delete(subscriberUID);
  }

  return { subscribeBars, unsubscribeBars };
}
```

The browser handshake must be the first frame and arrive within 10 seconds. Never put the
API key in the URL.

## Per-venue availability

Native `trades` streams are available today for:

| Venue         | Live-bar source |
| ------------- | --------------- |
| `polymarket`  | Native trades   |
| `kalshi`      | Native trades   |
| `hyperliquid` | Native trades   |
| `sxbet`       | Native trades   |
| `myriad`      | Native trades   |

Other venues answer a non-fatal `NOT_SUPPORTED` error and leave the WebSocket open. For
those venues, the fallback pattern is the `orderbook` channel: turn each full `snapshot`
or `update` into a mid-price tick from the best `bids` and `asks`. That is a book-derived
mid, not a trade, and it has no trade volume. See the
[Streaming capability notes](/guides/streaming/#wire-protocol-json-text-frames) before
choosing that fallback.

## A live trade tape for free

The same subscription already carries the full `MarketTrade`. Add another message handler
beside the bar builder to keep the newest `N` trades for a tape:

```ts
const MAX_TRADES = 50;
const tape: MarketTrade[] = [];

socket.addEventListener('message', (event) => {
  const frame = JSON.parse(String(event.data)) as Partial<TradeFrame>;
  if (frame.type !== 'trade' || !frame.data) return;
  tape.unshift(frame.data);
  if (tape.length > MAX_TRADES) tape.length = MAX_TRADES;
  renderTradeTape(tape);
});
```

Polymarket's native channel does not expose a wallet or transaction hash. Those fields
degrade honestly to `wallet: null` and `transactionHash: ''`; prices, shares, side, and
timestamps still drive the bars and tape.
