TradingView charts
What you’re building
Section titled “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
Section titled “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:
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 |
History via fetchOHLCV
Section titled “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:
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
Section titled “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:
{ "op": "auth", "apiKey": "pk_live_…" }{ "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:
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.
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
Section titled “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 before
choosing that fallback.
A live trade tape for free
Section titled “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:
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.