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

# TradingView charts for Polymarket

> Render a live TradingView chart of any Polymarket outcome: backfill OHLCV from the candlestick API and stream live bars from the trades room.

The [TradingView Advanced Charts](https://www.tradingview.com/charting-library-docs/) library (the `charting_library` package, not the open-source Lightweight Charts) renders price by calling a **datafeed** object you implement. This guide wires Struct into that datafeed: historical bars from the candlestick API in `getBars`, and live bars from the trades WebSocket room fed into the realtime callback.

| Need                                     | Source                                                                                          |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Historical OHLCV bars for one outcome    | `client.markets.getPositionCandlestick`                                                         |
| Historical OHLCV bars for a whole market | `client.markets.getCandlestick`                                                                 |
| Live trades to build the forming bar     | [`polymarket_trades`](/websockets/rooms/trades) (`trade_stream_update`)                         |
| Pre-aggregated live OHLC (server-side)   | [`polymarket_position_metrics`](/websockets/rooms/position-metrics) (`position_metrics_update`) |

Each Polymarket outcome is an ERC-1155 token identified by a numeric **position ID**. That position ID is the symbol you chart. A binary market has a Yes token and a No token, each with its own price series, so you chart one outcome at a time.

## When to use this

* A candlestick chart of a single market outcome (Yes/No price over time) inside your own app.
* A live trading view that backfills history on mount and extends the last bar on every fill.
* Replacing a polling chart with a pushed one, no reconnect bookkeeping.

If you are charting the crypto spot price behind Up/Down markets instead, use [`getAssetCandlestick`](/guides/crypto-up-down-feed#charting-the-underlying-price), not the market candlestick endpoints below.

## The candlestick endpoints

`getPositionCandlestick` returns OHLCV for a single outcome token; `getCandlestick` returns it for a market by condition ID. Both return the same bar shape.

<CodeGroup>
  ```typescript SDK theme={null}
  import { StructClient } from "@structbuild/sdk";

  const client = new StructClient({ apiKey: "sk_live_xxx" });

  const { data: candles } = await client.markets.getPositionCandlestick({
    position_id: "71321045679252212594626385532706912750332728571942532289631379312455583992563",
    resolution: "60",
    count_back: 500,
  });
  ```

  ```bash cURL theme={null}
  curl "https://api.struct.to/v1/polymarket/market/position/candlestick?position_id=71321045679...&resolution=60&count_back=500" \
    -H "X-API-Key: YOUR_API_KEY"
  ```

  ```python Python theme={null}
  import requests

  candles = requests.get(
      "https://api.struct.to/v1/polymarket/market/position/candlestick",
      headers={"X-API-Key": "YOUR_API_KEY"},
      params={"position_id": "71321045679...", "resolution": "60", "count_back": 500},
  ).json()["data"]
  ```
</CodeGroup>

Each bar is `{ o, h, l, c, v, t, tc }`:

| Field           | Description                                                |
| --------------- | ---------------------------------------------------------- |
| `o` `h` `l` `c` | Open, high, low, close (each `0`–`1`, the outcome's price) |
| `v`             | Volume in the bar                                          |
| `t`             | Bar start time, **Unix milliseconds**                      |
| `tc`            | Trade count in the bar                                     |

| Parameter        | Description                                                                                                                              |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `position_id`    | Outcome token ID (`getCandlestick` takes `condition_id` instead)                                                                         |
| `resolution`     | Candle interval, TradingView naming: `1S`, `5S`, `10S`, `30S` (seconds), `1`, `5`, `15`, `30`, `60`, `240` (minutes), `D` / `1D` (daily) |
| `count_back`     | Number of candles to return (max 2500)                                                                                                   |
| `from` / `to`    | Optional Unix **second** range to bound the window                                                                                       |
| `pagination_key` | Cursor from a previous response to page further back                                                                                     |

<Note>
  Request bounds (`from` / `to`) are in Unix **seconds**, but each returned bar's `t` is in Unix **milliseconds**. TradingView's `Bar.time` wants milliseconds for intraday resolutions, so `t` maps straight across with no conversion.
</Note>

## Step 1: getBars

TradingView calls `getBars` on first load and again as the user pans into older data. `periodParams` gives the window (`from`, `to` in Unix seconds), the bar count (`countBack`), and whether this is the first request. Map the response into the library's `Bar` type and sort ascending; out-of-order bars are rejected.

Page older history with the cursor, not the window. Send `from` / `to` (or just `count_back`) on the **first** request to anchor the visible range, then follow the `pagination_key` from each response on every subsequent call. The `pagination.has_more` flag — not the returned row count — tells you when the series is exhausted, so keep a small `state` object between calls to carry the cursor and the more-data flag.

```typescript theme={null}
import type {
  Bar,
  ResolutionString,
  PeriodParams,
  HistoryCallback,
  DatafeedErrorCallback,
} from "../charting_library";
import type { StructClient } from "@structbuild/sdk";

type ApiResolution =
  | "1S" | "5S" | "10S" | "30S"
  | "1" | "5" | "15" | "30" | "60" | "240" | "1D";

const toApiResolution = (r: ResolutionString): ApiResolution => {
  if (r === "D" || r === "1D") return "1D";
  if (r.endsWith("S")) {
    const seconds = parseInt(r, 10);
    if (seconds <= 1) return "1S";
    if (seconds <= 5) return "5S";
    if (seconds <= 10) return "10S";
    return "30S";
  }
  const val = parseInt(r, 10);
  if (Number.isNaN(val)) return "60";
  if (val <= 1) return "1";
  if (val <= 5) return "5";
  if (val <= 15) return "15";
  if (val <= 30) return "30";
  if (val <= 60) return "60";
  return "240";
};

type GetBarsState = {
  hasMore: boolean;
  paginationKey: string | number | null;
};

async function getBars(
  client: StructClient,
  positionId: string,
  resolution: ResolutionString,
  periodParams: PeriodParams,
  onResult: HistoryCallback,
  onError: DatafeedErrorCallback,
  state: GetBarsState,
) {
  try {
    const { from, to, countBack, firstDataRequest } = periodParams;
    if (firstDataRequest) {
      state.hasMore = true;
      state.paginationKey = null;
    }
    if (!state.hasMore && !firstDataRequest) {
      onResult([], { noData: true });
      return;
    }

    const { data, pagination } = await client.markets.getPositionCandlestick({
      position_id: positionId,
      resolution: toApiResolution(resolution),
      count_back: countBack,
      // First page anchors the window; older pages follow the cursor.
      ...(state.paginationKey != null
        ? { pagination_key: String(state.paginationKey) }
        : { from, to }),
    });

    if (!data || data.length === 0) {
      state.hasMore = false;
      onResult([], { noData: true });
      return;
    }

    const bars: Bar[] = data
      .filter((c) => c.o != null && c.c != null)
      .map((c) => ({
        time: c.t,
        open: c.o ?? 0,
        high: c.h ?? 0,
        low: c.l ?? 0,
        close: c.c ?? 0,
        volume: c.v ?? 0,
      }))
      .sort((a, b) => a.time - b.time);

    // Drive pagination from the cursor, not the returned row count.
    state.hasMore = pagination?.has_more ?? false;
    state.paginationKey = state.hasMore ? pagination?.pagination_key ?? null : null;

    onResult(bars, { noData: bars.length === 0 });
  } catch (err) {
    onError(err instanceof Error ? err.message : "Failed to fetch candlesticks");
  }
}
```

## Step 2: live bars from the trades room

`subscribeBars` should only register the chart's realtime callback. Feed updates from a separate `handleRealtimeTrade` method driven by the [`polymarket_trades`](/websockets/rooms/trades) room, so one socket serves the chart, a trade tape, and anything else.

Each `trade_stream_update` carries `price` (`0`–`1`), `shares_amount`, `side`, and `confirmed_at` (Unix **seconds**). Bucket each trade into its bar, opening a new bar from the previous bar's close so the series stays continuous.

```typescript theme={null}
import type { SubscribeBarsCallback } from "../charting_library";
import type { TradeStreamEvent } from "@structbuild/sdk";

const resolutionToMs = (r: ResolutionString): number => {
  if (r === "D" || r === "1D") return 24 * 60 * 60 * 1000;
  if (r.endsWith("S")) {
    const seconds = parseInt(r, 10);
    return Number.isNaN(seconds) ? 1000 : seconds * 1000;
  }
  const val = parseInt(r, 10);
  return Number.isNaN(val) ? 60 * 60 * 1000 : val * 60 * 1000;
};

type BarSubscription = {
  resolution: ResolutionString;
  onTick: SubscribeBarsCallback;
  lastBar: Bar | null;
};

const subscriptions = new Map<string, BarSubscription>();

function handleRealtimeTrade(positionId: string, trade: TradeStreamEvent) {
  if (trade.position_id !== positionId) return;
  if (trade.price == null || trade.confirmed_at == null) return;

  const price = trade.price;
  const size = trade.shares_amount ?? 0;
  const tsMs = trade.confirmed_at * 1000;

  for (const sub of subscriptions.values()) {
    const barMs = resolutionToMs(sub.resolution);
    const barTime = Math.floor(tsMs / barMs) * barMs;
    let bar: Bar;

    if (sub.lastBar && sub.lastBar.time === barTime) {
      bar = {
        ...sub.lastBar,
        high: Math.max(sub.lastBar.high, price),
        low: Math.min(sub.lastBar.low, price),
        close: price,
        volume: (sub.lastBar.volume ?? 0) + size,
      };
    } else if (sub.lastBar && barTime > sub.lastBar.time) {
      bar = {
        time: barTime,
        open: sub.lastBar.close,
        high: Math.max(sub.lastBar.close, price),
        low: Math.min(sub.lastBar.close, price),
        close: price,
        volume: size,
      };
    } else {
      bar = { time: barTime, open: price, high: price, low: price, close: price, volume: size };
    }

    sub.lastBar = bar;
    sub.onTick(bar);
  }
}
```

The three branches cover every case: extend the current bar, roll into a new one (open at the last close), or start cold. TradingView merges realtime bars by `time`, so the first trade after load lands on the same timestamp as the last historical bar and updates it in place.

<Note>
  By default the trades room sends confirmed on-chain fills. Pass `status: "all"` to also receive mempool trades, which arrive before confirmation and carry `received_at` (Unix milliseconds) instead of `confirmed_at`. Use them for a faster visual tick and reconcile against confirmed data.
</Note>

## Step 3: assemble the datafeed and mount

`onReady` advertises supported resolutions. `resolveSymbol` describes the instrument: prices are `0`–`1`, so `pricescale: 10000` gives four decimals. Then connect the socket, subscribe to the trades room, and pipe each event into `handleRealtimeTrade`.

```typescript theme={null}
import type { IBasicDataFeed, LibrarySymbolInfo, OnReadyCallback, ResolveCallback } from "../charting_library";
import { StructClient, StructWebSocket } from "@structbuild/sdk";

const SUPPORTED_RESOLUTIONS = ["1S", "5S", "10S", "30S", "1", "5", "15", "30", "60", "240", "D"] as ResolutionString[];
const SECONDS_MULTIPLIERS = ["1", "5", "10", "30"];

export function createPredictionDatafeed(positionId: string, client: StructClient) {
  const state: GetBarsState = { hasMore: true, paginationKey: null };

  const datafeed: IBasicDataFeed = {
    onReady: (cb: OnReadyCallback) => {
      setTimeout(() => cb({ supported_resolutions: SUPPORTED_RESOLUTIONS, seconds_multipliers: SECONDS_MULTIPLIERS }), 0);
    },
    searchSymbols: (_input, _exchange, _type, onResult) => onResult([]),
    resolveSymbol: (_name, onResolve: ResolveCallback) => {
      setTimeout(() => {
        onResolve({
          ticker: positionId,
          name: "Polymarket outcome",
          description: "Polymarket outcome",
          type: "index",
          session: "24x7",
          timezone: "Etc/UTC",
          exchange: "Polymarket",
          listed_exchange: "Polymarket",
          format: "price",
          minmov: 1,
          pricescale: 10000,
          has_intraday: true,
          has_seconds: true,
          seconds_multipliers: SECONDS_MULTIPLIERS,
          has_daily: true,
          has_weekly_and_monthly: false,
          supported_resolutions: SUPPORTED_RESOLUTIONS,
          volume_precision: 2,
          data_status: "streaming",
        } as LibrarySymbolInfo);
      }, 0);
    },
    getBars: (_symbolInfo, resolution, periodParams, onResult, onError) =>
      getBars(client, positionId, resolution, periodParams, onResult, onError, state),
    subscribeBars: (_symbolInfo, resolution, onTick, uid) => {
      subscriptions.set(uid, { resolution, onTick, lastBar: null });
    },
    unsubscribeBars: (uid) => subscriptions.delete(uid),
  };

  return datafeed;
}
```

```typescript theme={null}
const client = new StructClient({ apiKey: "pk_jwt_xxx", jwt: userJwt });
const ws = new StructWebSocket({ apiKey: "pk_jwt_xxx", getJwt: () => userJwt });

const datafeed = createPredictionDatafeed(positionId, client);

await ws.connect();
await ws.subscribe("polymarket_trades", { position_ids: [positionId] });
const offTrade = ws.on("trade_stream_update", (trade) => handleRealtimeTrade(positionId, trade));

const widget = new window.TradingView.widget({
  symbol: positionId,
  interval: "60" as ResolutionString,
  container: "tv_chart_container",
  library_path: "/charting_library/",
  datafeed,
  locale: "en",
  autosize: true,
});

// teardown
// offTrade(); ws.unsubscribe("polymarket_trades"); widget.remove();
```

<Note>
  For a browser chart, authenticate with a `pk_jwt_` public key plus the signed-in user's JWT, as above. The `pk_jwt_` key is safe in a frontend bundle because it is useless without a valid JWT from your configured auth provider. On a server, use your `sk_` secret key and drop the `jwt`. See [JWT auth](/sdk/authentication).
</Note>

## Common combinations

| Goal                     | Call                                                                             |
| ------------------------ | -------------------------------------------------------------------------------- |
| Backfill one outcome     | `getPositionCandlestick({ position_id, resolution: "60", count_back: 500 })`     |
| Backfill a whole market  | `getCandlestick({ condition_id, resolution: "60" })`                             |
| Live bars from trades    | `subscribe("polymarket_trades", { position_ids: [id] })` → `trade_stream_update` |
| Live confirmed + pending | `subscribe("polymarket_trades", { position_ids: [id], status: "all" })`          |

## Follow-on

* [Trades room](/websockets/rooms/trades) for every trade filter and the full `trade_stream_update` payload.
* [Position metrics room](/websockets/rooms/position-metrics) for live probability and position-level metrics.
* [SDK REST reference](/sdk/rest-api) for the candlestick method signatures.
* [Crypto Up/Down feed](/guides/crypto-up-down-feed) to chart the crypto spot price behind Up/Down markets.
