Skip to content

DefaultDataFeed

DefaultDataFeed is a reference adapter for local development and evaluation. ChartSpire does not provide or guarantee market data. For applications that depend on live or regulated data, implement the [DataFeed](./data-access.md) interface against your own provider and infrastructure.

This page documents the included DefaultDataFeed reference adapter — a way to try ChartSpire against a compatible HTTP and WebSocket API while preserving multi-chart subscription sharing.

For generic DataFeed concepts, see Data Access.
For data shape definitions, see Data Types.

What matters most

  • HTTP is used for searchSymbols, getHistoricalTimeSeries, and getPrice.
  • WebSocket is used for live kline, orderbook, and trades streams.
  • HTTP and WebSocket origins are configured separately so REST and streaming can live on different infrastructure.
  • TLS is the default. http:// and ws:// require security.allowInsecureTransport: true.
  • Multiple charts/components subscribing to the same { dataType, symbol, exchange, interval } share one backend subscription.
  • Stable subscriber IDs are required for subscribe/unsubscribe calls.

Constructor Configuration

typescript
import { DefaultDataFeed, type DefaultDataFeedConfig } from 'chartspire'

const config: DefaultDataFeedConfig = {
  http: {
    baseUrl: 'https://api.example.com',
    endpoints: {
      search: '/v1/market-data/symbols',
      historical: '/v1/market-data/candles',
      price: '/v1/market-data/price'
    },
    headers: async () => ({
      'X-Tenant-Id': tenantId
    }),
    timeoutMs: 15000
  },
  websocket: {
    enabled: true,
    url: 'wss://stream.example.com/v1/market-data',
    reconnect: {
      enabled: true,
      initialDelayMs: 1000,
      maxDelayMs: 30000,
      maxAttempts: 5
    },
    heartbeat: {
      enabled: true,
      intervalMs: 30000,
      timeoutMs: 10000
    }
  },
  auth: {
    type: 'bearer',
    token: async () => getCurrentSessionToken(),
    applyTo: ['http', 'websocket'],
    websocketMode: 'message'
  },
  security: {
    allowedHttpOrigins: ['https://api.example.com'],
    allowedWebSocketOrigins: ['wss://stream.example.com']
  },
  protocol: {
    searchDefaults: {
      default: 'AAPL',
      crypto: 'BTC'
    }
  }
}

const dataFeed = new DefaultDataFeed(config)

Local or private non-TLS deployments

Non-TLS is supported, but it must be explicit:

typescript
const dataFeed = new DefaultDataFeed({
  http: {
    baseUrl: 'http://localhost:3000',
    endpoints: {
      search: '/v1/market-data/symbols',
      historical: '/v1/market-data/candles',
      price: '/v1/market-data/price'
    }
  },
  websocket: {
    enabled: true,
    url: 'ws://localhost:3000/v1/market-data'
  },
  security: {
    allowInsecureTransport: true
  }
})

Do not enable insecure transport on internet-facing deployments.

Config Reference

typescript
interface DefaultDataFeedConfig {
  http: {
    baseUrl: string
    endpoints: {
      search: string
      historical: string
      price?: string
    }
    headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>)
    credentials?: RequestCredentials
    timeoutMs?: number
  }
  websocket?: {
    enabled?: boolean
    url?: string
    reconnect?: {
      enabled?: boolean
      initialDelayMs?: number
      maxDelayMs?: number
      maxAttempts?: number
    }
    heartbeat?: {
      enabled?: boolean
      intervalMs?: number
      timeoutMs?: number
    }
  }
  auth?: {
    type: 'bearer'
    token: string | (() => string | Promise<string>)
    applyTo?: Array<'http' | 'websocket'>
    websocketMode?: 'message' | 'query'
  }
  security?: {
    allowInsecureTransport?: boolean
    allowedHttpOrigins?: string[]
    allowedWebSocketOrigins?: string[]
  }
  protocol?: {
    searchDefaults?: Record<string, string>
  }
  debug?: boolean
  onError?: (error: DefaultDataFeedError) => void
  onStatusChange?: (status: DefaultDataFeedStatus) => void
}

HTTP Contract

All HTTP endpoints are configured as paths under http.baseUrl. Asset type and exchange are query parameters rather than path segments.

GET {http.baseUrl}{endpoints.search}?query={query}&type={type}&exchange={exchange?}&limit={limit?}

Example:

text
GET https://api.example.com/v1/market-data/symbols?query=AAPL&type=stocks&limit=25

Response:

typescript
interface DefaultSearchResponse {
  data: Array<{
    symbol: string
    name?: string
    shortName?: string
    exchange?: string
    market?: string
    type: string
    pricePrecision?: number
    volumePrecision?: number
    priceCurrency?: string
    currency?: string
    logo?: string
  }>
}

If search is empty, DefaultDataFeed uses protocol.searchDefaults[type] or protocol.searchDefaults.default.

2) Historical Candles

GET {http.baseUrl}{endpoints.historical}?symbol={symbol}&type={type}&exchange={exchange?}&interval={interval}&from={fromMs}&to={toMs}

Example:

text
GET https://api.example.com/v1/market-data/candles?symbol=BTCUSDT&type=crypto&exchange=binance&interval=1h&from=1710000000000&to=1710100000000

Response:

typescript
interface DefaultHistoricalResponse {
  data: Array<{
    timestamp: number | string
    open: number | string
    high: number | string
    low: number | string
    close: number | string
    volume?: number | string
    turnover?: number | string
  }>
  meta?: {
    symbol?: string
    exchange?: string
    interval?: string
    nextPageToken?: string
  }
}

Rules:

  • from and to are Unix millisecond timestamps.
  • Return only candles inside the requested window.
  • Return candles sorted ascending by timestamp when possible. The client sorts defensively.
  • Malformed candles are dropped rather than emitted as NaN values.

3) Current Price

GET {http.baseUrl}{endpoints.price}?symbol={symbol}&type={type}&exchange={exchange?}

Response:

typescript
interface DefaultPriceResponse {
  price: number
  symbol?: string
  exchange?: string
  timestamp?: number
}

Interval Behavior

Historical HTTP requests send the selected chart interval using ChartSpire's interval mapping, for example:

  • 1m, 3m, 5m, 15m, 30m
  • 1h, 2h, 4h, 6h, 8h, 12h
  • 1d, 3d, 5d
  • 1w
  • 1M, 3M, 6M
  • 1y, 5y

Live WebSocket chart subscriptions use the selected chart interval (same as historical). The feed must emit OHLCV at that interval; ChartSpire passes bars through without client-side rebucketing.

Note: The reference chartspire-server may not stream all intervals yet. A test backend that matches this contract should emit klines at the subscribed interval.

Custom DataFeed contract

Implement both history and live at the chart period:

MethodBehavior
getHistoricalTimeSeries(symbol, period, from, to)Return candles at period
subscribeSymbol(symbol, period, callback, subscriberUID)Stream updates at period
unsubscribeSymbol(symbol, period, subscriberUID)Same period as subscribe

Feeds that only stream 1m live will not populate 5m/1h charts until the feed (or server) aggregates to the requested interval.

WebSocket Contract

Authentication

When auth.websocketMode is message, DefaultDataFeed sends an auth frame after the socket opens:

json
{
  "event": "authenticate",
  "data": {
    "type": "bearer",
    "token": "jwt-token",
    "clientId": "client_xxx",
    "timestamp": 1710000000000
  }
}

Subscriptions are queued until the server sends:

json
{
  "event": "auth_success",
  "data": {
    "timestamp": 1710000000000
  }
}

On auth_error, the socket closes and subscriptions are not replayed until a new feed/socket lifecycle begins.

Use websocketMode: 'message' whenever possible. websocketMode: 'query' exists for gateways that cannot accept an auth frame, but tokens in URLs may appear in proxy or gateway logs.

Subscribe

json
{
  "event": "subscribe",
  "data": {
    "subscriptionId": "cs_kline%7CBTCUSDT%7Cbinance%7C1m",
    "clientId": "client_xxx",
    "dataType": "kline",
    "symbol": "BTCUSDT",
    "type": "crypto",
    "exchange": "binance",
    "interval": "1m"
  }
}

dataType can be:

  • kline
  • orderbook
  • trades

Unsubscribe

json
{
  "event": "unsubscribe",
  "data": {
    "subscriptionId": "cs_kline%7CBTCUSDT%7Cbinance%7C1m",
    "clientId": "client_xxx"
  }
}

Market Data

json
{
  "event": "market_data",
  "data": {
    "subscriptionId": "cs_kline%7CBTCUSDT%7Cbinance%7C1m",
    "dataType": "kline",
    "symbol": "BTCUSDT",
    "type": "crypto",
    "exchange": "binance",
    "interval": "1m",
    "payload": {
      "timestamp": 1710000000000,
      "open": "100.00",
      "high": "101.00",
      "low": "99.00",
      "close": "100.50",
      "volume": "1234.5"
    }
  }
}

Routing prefers subscriptionId. Symbol/exchange/interval matching is fallback only and does not rely on splitting symbol strings.

Shared Channel Behavior

DefaultDataFeed maintains one backend subscription per { dataType, symbol, exchange, interval } per feed instance.

Example: if four charts subscribe to BTCUSDT on binance for kline 1m, ChartSpire sends one backend subscribe frame and keeps four local handlers. When one chart unsubscribes, the backend subscription remains active. The backend unsubscribe frame is sent only when the last local handler is removed.

Isolation rules:

  • Same symbol on different exchanges uses different channels.
  • Same symbol/exchange with different intervals uses different kline channels.
  • kline, orderbook, and trades are separate channels.

Diagnostics

typescript
dataFeed.debugChannels()
const count = dataFeed.getActiveSubscriptions()
  • debugChannels() logs active channel details when debug is enabled.
  • getActiveSubscriptions() returns the number of active backend channels across kline, order book, and trades.