Skip to main content

Symbol Search

Symbol search is owned by your datafeed. The chart asks for symbols with a typed request object, and your datafeed returns normalized SymbolSearchResult objects. Two methods split the job: searchSymbols fills the picker list, resolveSymbol turns the user's choice into the instrument the chart actually loads.

The user flow

A search row is a selection, not a resolved symbol. Selecting it hands the row's carried descriptor to resolveSymbol — the chart never runs a second text search or substitutes another hit for the row the user chose. The two calls carry different metadata:

  • Selection metadata (returned by searchSymbols) exists to render the picker list and identify the exact row: displayName, description, provider, logos, grouping, and nextCursor for paging. Mixed-provider catalogs set selectionId on the row's symbol so two same-ticker rows stay distinct and the exact row round-trips through resolveSymbol.
  • Resolution metadata (returned by resolveSymbol) is what the chart trades on: the definitive ticker/canonicalSymbol instrument identity, the resolved marketDataSeriesId for feeds serving series variants, plus instrument facts such as supportedIntervals, session data, and formatting. Never assume a search row is complete enough to chart; resolution is the authoritative step, and it runs once per selection and resolve context.

Minimal implementation, empty and error states

const datafeed: MarketDataFeed = {
onReady() {
return { supportsSearch: true };
},

async searchSymbols(request) {
const response = await fetch(
`/symbols/search?q=${encodeURIComponent(request.searchText)}`,
);
if (!response.ok) {
throw new Error(`Symbol search failed: ${response.status}`);
}
const payload = await response.json();
return payload.data.map((item: any) => ({
symbol: { ticker: item.symbol, name: item.name, exchange: item.exchange },
}));
},

async resolveSymbol(symbol) {
return typeof symbol === 'string' ? { ticker: symbol } : symbol;
},
};

Keep the two failure shapes distinct:

  • No matches — resolve with []. The chart shows its empty-results state. An empty array is an answer, not an error.
  • Search failed — throw (or reject), as the !response.ok branch does. Do not translate a transport failure into []; that tells the user their query matched nothing.

Checkpoint: open the symbol search, type a known ticker, and confirm results render; pick one and confirm the chart resolves it and loads bars. Then type nonsense and confirm the empty state (not an error) appears.

Request

export interface SymbolSearchRequest {
searchText: string
exchange?: string
assetType?: AssetType
limit?: number
cursor?: string
searchSource?: 'symbolSearch' | 'watchlist' | 'compare' | 'indicatorInputs'
includeSpreadOperators?: boolean
}

searchText is required. The controller trims it and clamps limit to the supported SDK range before calling your datafeed.

Use exchange, assetType, cursor, and searchSource as server-side query inputs. Do not rediscover those facts from the ticker string or the caller UI. searchSource identifies the initiating surface: main symbol search, watchlist, compare, or indicator inputs.

Results

export interface SymbolSearchGrouping {
key: string
label?: string
root?: string
expiry?: string
}

export interface SymbolInfo {
ticker: string
exchange?: string
listedExchange?: string
canonicalSymbol?: string
brokerSymbol?: string
name?: string
shortName?: string
description?: string
type?: AssetType
logoUrl?: string
logoUrls?: string[]
exchangeLogoUrl?: string
group?: SymbolSearchGrouping
}

export interface SymbolSearchResult {
symbol: SymbolInfo
displayName?: string
description?: string
provider?: string
nextCursor?: string
group?: SymbolSearchGrouping
metadata?: Record<string, unknown>
}

Attach nextCursor to any returned result, usually the last one. The chart uses it to request the next page when the user scrolls or presses load more.

Prediction-market outcomes

Return each tradable outcome as its own search row. Rows from one event may share group.key and group.label for presentation, but each row keeps a different selectionId. Resolution then returns type: 'prediction-contract' and the exact event, market, and outcome facts on SdkSymbolInfo.instrument.

Buying or selling one outcome never selects its opposite. A Yes and No pair, or several candidates in one event, resolves as separate symbols with separate outcomeId values. Do not encode those facts in ticker text or recover them from customFields after selection.

See Prediction-market order ticket for the resolved contract and its trading flow.

Datafeed Config

Return search capability metadata from onReady when you know it:

const datafeed: MarketDataFeed = {
onReady() {
return {
supportsSearch: true,
symbolSearch: {
supportsPagination: true,
supportsSpreadOperators: true,
exchanges: [
{ value: 'NASDAQ', label: 'Nasdaq' },
{ value: 'NYSE', label: 'NYSE' },
],
assetTypes: [
{ value: 'stock', label: 'Stocks' },
{ value: 'crypto', label: 'Crypto' },
],
},
}
},
async searchSymbols(request) {
// Query your symbol service with request.searchText, request.exchange,
// request.assetType, request.limit, request.cursor, and request.searchSource.
return []
},
async resolveSymbol(symbol) {
return typeof symbol === 'string' ? { ticker: symbol } : symbol
},
}

The chart renders configured exchange filters and asset-type categories. If you only expose one asset type, the chart avoids unnecessary category chrome.

Pagination

async function searchSymbols(request: SymbolSearchRequest): Promise<SymbolSearchResult[]> {
const response = await fetch(`/symbols/search?${new URLSearchParams({
q: request.searchText,
limit: String(request.limit ?? 50),
...(request.exchange ? { exchange: request.exchange } : {}),
...(request.assetType ? { asset_type: request.assetType } : {}),
...(request.cursor ? { cursor: request.cursor } : {}),
})}`)

const payload = await response.json()
const results = payload.data.map((item: any) => ({
symbol: {
ticker: item.symbol,
name: item.name,
exchange: item.exchange,
type: item.type,
logoUrls: item.logoUrls,
exchangeLogoUrl: item.exchangeLogoUrl,
group: item.group,
},
}))

if (payload.nextCursor && results.length > 0) {
results[results.length - 1] = {
...results[results.length - 1],
nextCursor: payload.nextCursor,
}
}

return results
}

Next steps

  • Resolutions — narrow the interval selector per resolved symbol.
  • Historical Bars — what the chart requests once a symbol resolves.