Market Data Types
Your backend already produces market data in some shape. Translating it is a three-column problem: which kind of data drives which chart surface, which method serves it, and which capability flag must be set before the chart will ask for it at all.
The chart never guesses. A method that exists but whose flag is missing is never called, and a flag set without the method behind it is ignored.
What the chart can render
The full mapping
| Data you have | Method | Capability flag | Renders |
|---|---|---|---|
| Historical OHLCV | loadBars | required | Candles, indicators |
| Live bar or tick stream | subscribeRealTimeBars | supportsRealTime | Developing candle |
| Top of book / NBBO | getQuotes, subscribeQuotes | supportsQuotes | Legend quotes, bid/ask lines, market status |
| Order-book levels | getDepth, subscribeDepth | supportsDepth | Depth ladder, liquidity heatmap |
| Recorded book snapshots | getBookHistory | supportsBookHistory | Heatmap replay over history |
| Trade prints | getTimeAndSales, subscribeTimeAndSales | supportsTimeAndSales | Tape, footprint, market profile |
| Instrument master | resolveSymbol, searchSymbols | supportsSearch | Symbol search and resolution |
| Instrument profile | getInstrumentDetails | supportsDetails | Instrument details panel |
| Trading calendars | resolveSessionInfo, resolveSessionCalendar | supportsSessionInfo, supportsSessionCalendar | Session shading, gap filling |
| Corporate actions, halts, releases | getEvents, subscribeEvents | supportsEvents plus eventCatalog | Timeline event markers |
| Per-bar annotations | getMarks, getTimescaleMarks | supportsMarks, supportsTimescaleMarks | Bar and time-scale marks |
| Headlines | getNews | supportsNews | News panel |
| Option chains | getOptionContracts, getOptionQuotes | supportsOptionContracts, supportsOptionQuotes | Option surfaces |
| Derived or alternative series | getExternalSeries | supportsExternalSeries | Auxiliary plotted series |
| Symbol lists | getWatchlist and the watchlist writers | supportsWatchlist | Watchlist |
| Arbitrary per-bar values | getDataWindow | supportsDataWindow | Data window |
| Venue clock | getServerTime | supportsServerTime | Session and countdown accuracy |
Declare only what you serve:
import type { MarketDataFeedConfig } from '@tradescript/pro/sdk';
export function capabilities(): MarketDataFeedConfig {
return {
supportedIntervals: ['1m', '5m', '15m', '1H', '1D'],
providedIntervals: ['1m', '1D'],
resolutionRebuildPolicy: 'aggregate',
supportsRealTime: true,
supportsSearch: true,
supportsQuotes: true,
supportsDepth: true,
supportsTimeAndSales: true,
};
}
providedIntervals are the intervals your backend can return directly.
supportedIntervals are the ones a user may pick. With
resolutionRebuildPolicy: 'aggregate', the SDK builds the difference by
aggregating a smaller provided interval — so a backend that stores only 1-minute
and daily bars can still offer 5m, 15m, and 1H. See
Resolutions.
Normalization rules
These four rules cause most integration bugs. They apply to every data type.
Time is Unix milliseconds, UTC
Vendors publish seconds, microseconds, nanoseconds, and ISO strings. Convert at
the boundary. Databento timestamps are UTC nanoseconds; FIX SendingTime is a
UTC string; Alpaca uses RFC-3339. The chart accepts exactly one form.
const timeMs = Number(nanoseconds / 1_000_000n);
Bar timestamps are the bar's opening time, not its close.
Prices are floating point in display units
Feeds that carry fixed-precision integers must be scaled before the chart sees them. Databento encodes prices as int64 scaled by 1e-9; many exchange binary protocols use a per-instrument exponent from the security definition. Scale in the normalizer, never in a chart callback.
Bars ascend and never duplicate
loadBars returns bars in ascending time order with no repeated timestamps.
For live updates, a bar whose time matches the current bar replaces it; a
later time appends a new bar. Out-of-order vendor events must be dropped or
reordered before they reach the callback.
Symbol identity is stable across transports
History, stream, depth, and tape must resolve the same instrument to the same
identity. If the vendor uses AAPL for history and NASDAQ:AAPL for streaming,
normalize both through one application-owned map and put the result in
ticker. Use canonicalSymbol when the display ticker must stay human-readable
while the SDK keys on something else, and brokerSymbol for the execution-side
identifier — the SDK never parses or repairs these values.
Serving each type
A bars-plus-quotes feed covers most products. Depth and tape are additive.
import type { MarketDataFeed } from '@tradescript/pro/sdk/core';
import type { Quote, SdkMarketDepth, TimeAndSalesEntry } from '@tradescript/pro/sdk';
interface VendorLevel { px: number; qty: number }
async function api<T>(path: string): Promise<T> {
const response = await fetch(path);
if (!response.ok) throw new Error(`${path} failed: ${response.status}`);
return response.json() as Promise<T>;
}
export const datafeed: MarketDataFeed = {
onReady: () => ({ supportsQuotes: true, supportsDepth: true, supportsTimeAndSales: true }),
async loadBars(symbol, interval, request) {
const query = new URLSearchParams({
symbol: symbol.ticker,
interval,
from: String(request.startTime),
to: String(request.endTime),
});
return api(`/api/bars?${query}`);
},
async getQuotes(request) {
const tickers = request.symbols.map((entry) =>
typeof entry === 'string' ? entry : entry.ticker,
);
return api<Quote[]>(`/api/quotes?symbols=${tickers.join(',')}`);
},
async getDepth(request) {
const book = await api<{ bids: VendorLevel[]; asks: VendorLevel[]; ts: number }>(
`/api/depth?symbol=${request.symbol.ticker}`,
);
const levels = (side: VendorLevel[]) => side.map((l) => ({ price: l.px, size: l.qty }));
const depth: SdkMarketDepth = {
symbol: request.symbol,
bids: levels(book.bids),
asks: levels(book.asks),
timestamp: book.ts,
};
return depth;
},
async getTimeAndSales(request) {
const prints = await api<TimeAndSalesEntry[]>(
`/api/tape?symbol=${request.symbol.ticker}&limit=${request.limit ?? 200}`,
);
return prints;
},
};
Data the chart does not convert
Currency and unit conversion is feed-owned. The SDK never multiplies prices or
volumes after bars arrive. When a user switches the price scale to another
currency, the chart re-enters resolveSymbol with a currencyCode, and your
feed returns a symbol whose subsequent history and stream are already converted.
Advertise the available targets with currencyCodes and units.
Next steps
- Build the data gateway — the transport that carries all of this.
- Historical bars and Realtime bars — exact request and update contracts.
- Order flow — depth, tape, and recorded book history in detail.
- Market events — the event catalog contract.