Skip to main content

Quotes and Watchlists

Quotes power three surfaces from one contract: watchlist rows, the legend's quote fields, and the extended-hours price line. Watchlist methods power the native watchlist panel — its lists, rows, and selection are read from and written to your backend, so a user's watchlist follows them across devices. Both are optional capabilities on the same MarketDataFeed that already serves your bars.

Feed methods

CapabilityImplementAdvertise in onReady()
Quote snapshotsgetQuotessupportsQuotes: true
Streaming quotessubscribeQuotessupportsQuotes: true
Watchlist panelgetWatchlist, listWatchlistssupportsWatchlist: true
Watchlist editingcreateWatchlist, renameWatchlist, updateWatchlist, deleteWatchlist, setActiveWatchlistsupportsWatchlist: true
Live watchlist syncsubscribeWatchlistsupportsWatchlist: true

Reference implementation

The methods below extend the feed from Mixed-Asset Catalogs; spread them into that object and add the two flags to its onReady(). Editing methods not shown (createWatchlist, renameWatchlist, deleteWatchlist, setActiveWatchlist) follow the same fetch-and-map pattern against the same resource.

quotesAndWatchlists.ts
import type { MarketDataFeed, Quote, SdkSymbolInfo } from '@tradescript/pro/sdk';

const API = 'https://api.yourco.example';

/** One row of your quotes response. */
interface QuoteRow {
instrument: string;
last?: number;
bid?: number;
ask?: number;
change?: number;
changePercent?: number;
volume?: number;
t?: number;
}

// The chart requests quotes for symbols it already holds; echo each request
// symbol back on its quote so the SDK needs no second resolution.
function toQuote(requested: Array<string | SdkSymbolInfo>, row: QuoteRow): Quote {
const match = requested.find((entry) => (
typeof entry === 'string' ? entry === row.instrument : entry.canonicalSymbol === row.instrument
));
const symbol: SdkSymbolInfo = typeof match === 'string' || match === undefined
? { ticker: row.instrument }
: match;
return {
symbol,
last: row.last,
bid: row.bid,
ask: row.ask,
change: row.change,
changePercent: row.changePercent,
volume: row.volume,
timestamp: row.t,
};
}

function instrumentIds(symbols: Array<string | SdkSymbolInfo>): string {
return symbols
.map((entry) => (typeof entry === 'string' ? entry : entry.canonicalSymbol ?? entry.ticker))
.join(',');
}

export const quotesAndWatchlists: Pick<
MarketDataFeed,
'getQuotes' | 'subscribeQuotes' | 'getWatchlist' | 'listWatchlists' | 'updateWatchlist'
> = {
async getQuotes(request) {
const response = await fetch(`${API}/quotes?instruments=${encodeURIComponent(instrumentIds(request.symbols))}`);
if (!response.ok) throw new Error(`Quotes request failed: ${response.status}`);
const rows: QuoteRow[] = await response.json();
return rows.map((row) => toQuote(request.symbols, row));
},

subscribeQuotes(subscription, push) {
const socket = new WebSocket(
`${API.replace('https', 'wss')}/quotes/stream?instruments=${encodeURIComponent(instrumentIds(subscription.symbols))}`,
);
socket.onmessage = (message) => {
const rows: QuoteRow[] = JSON.parse(message.data as string);
push(rows.map((row) => toQuote(subscription.symbols, row)));
};
return () => socket.close();
},

async getWatchlist(request) {
const id = request?.id ?? 'active';
const response = await fetch(`${API}/watchlists/${encodeURIComponent(id)}`);
if (!response.ok) throw new Error(`Watchlist request failed: ${response.status}`);
return response.json();
},

async listWatchlists() {
const response = await fetch(`${API}/watchlists`);
if (!response.ok) throw new Error(`Watchlist listing failed: ${response.status}`);
return response.json();
},

async updateWatchlist(request) {
const response = await fetch(`${API}/watchlists/${encodeURIComponent(request.id)}`, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(request),
});
if (!response.ok) throw new Error(`Watchlist update failed: ${response.status}`);
return response.json();
},
};

Backend contract

Quote snapshots

GET /quotes?instruments=binance%3Abtcusd,nasdaq%3Aaapl
[
{ "instrument": "binance:btcusd", "last": 62519.8, "bid": 62519.5, "ask": 62520.1, "change": 128.4, "changePercent": 0.21, "volume": 18234.5, "t": 1722644105000 },
{ "instrument": "nasdaq:aapl", "last": 226.41, "bid": 226.4, "ask": 226.43, "change": -1.12, "changePercent": -0.49, "volume": 41230011, "t": 1722644104000 }
]

Every field except instrument is optional — surfaces render what the quote carries and leave the rest blank. t is a UTC epoch-ms timestamp. The stream endpoint (WSS /quotes/stream?instruments=...) sends the same row shape in batches, one JSON array per message; only changed instruments need to be included.

Watchlist listing

GET /watchlists
[
{ "id": "wl-main", "name": "Main", "active": true, "symbolCount": 2 },
{ "id": "wl-fx", "name": "FX majors", "active": false, "symbolCount": 6 }
]

One watchlist

GET /watchlists/wl-main
{
"id": "wl-main",
"name": "Main",
"symbols": [
{
"symbol": { "ticker": "BTCUSD", "selectionId": "binance:btcusd", "canonicalSymbol": "binance:btcusd", "name": "Bitcoin / USD", "exchange": "BINANCE", "type": "crypto" }
},
{
"symbol": { "ticker": "AAPL", "selectionId": "nasdaq:aapl", "canonicalSymbol": "nasdaq:aapl", "name": "Apple Inc.", "exchange": "NASDAQ", "type": "stock" }
}
],
"activeSymbol": { "ticker": "BTCUSD", "selectionId": "binance:btcusd", "canonicalSymbol": "binance:btcusd" }
}

Each row's symbol is a chart selection, so store the identity fields with it: selectionId and canonicalSymbol make a watchlist click load the exact instrument even when two rows share a ticker. Rows may also carry label, group (for sectioned lists), and metadata.

Updating a watchlist

PUT /watchlists/wl-main

The request body is the update — id plus any of name, symbols, activeSymbol, metadata — and the response is the full stored watchlist in the same shape as the read, which the SDK treats as the new truth.

Checkpoint: open the watchlist panel and your lists appear with live quote columns ticking; click a row and the chart loads that exact instrument; add the chart's symbol to the list and reloading the page shows it persisted.

Next steps