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, andnextCursorfor paging. Mixed-provider catalogs setselectionIdon the row's symbol so two same-ticker rows stay distinct and the exact row round-trips throughresolveSymbol. - Resolution metadata (returned by
resolveSymbol) is what the chart trades on: the definitiveticker/canonicalSymbolinstrument identity, the resolvedmarketDataSeriesIdfor feeds serving series variants, plus instrument facts such assupportedIntervals, 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.okbranch 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.