此頁面尚未翻譯為繁體中文,因此以英文顯示。

Realtime & replay

Stream live data, aggregate ticks into bars, and replay historical data at controlled speed.

Built-in adapters

All free, no API key — connect any of them the same way:

import { BinanceAdapter, CoinbaseAdapter, BybitAdapter, KrakenAdapter } from '@tradecanvas/chart'

chart.connect({ adapter: new BinanceAdapter(),  symbol: 'BTCUSDT', timeframe: '1m' })
chart.connect({ adapter: new BybitAdapter(),    symbol: 'BTCUSDT', timeframe: '1m' })
chart.connect({ adapter: new KrakenAdapter(),   symbol: 'BTC/USD', timeframe: '5m' })
chart.connect({ adapter: new CoinbaseAdapter(), symbol: 'BTC-USD', timeframe: '15m' })

DataAdapter interface

interface DataAdapter {
  name: string
  connect(config): void
  disconnect(): void
  getConnectionState(): ConnectionState
  fetchHistory(symbol, timeframe, limit?): Promise<OHLCBar[]>
  on(event, listener): void   // 'bar' | 'tick' | 'snapshot' | 'connectionChange' | 'error'
  off(event, listener): void
  dispose(): void

  // Optional
  fetchHistoryBefore?(symbol, timeframe, before, limit): Promise<OHLCBar[]>
  searchSymbols?(query, options?): Promise<SymbolInfo[]>
  resolveSymbol?(symbol): Promise<SymbolInfo | null>
  subscribeQuotes?(symbols, onQuotes): () => void      // a watchlist's rows
  fetchNews?(symbol, limit?): Promise<NewsItem[]>       // the symbol info panel
  fetchTrades?(symbol, limit?): Promise<Trade[]>        // tick charts, with
  subscribeTrades?(symbol, onTrades): () => void        // subscribeTrades
}

Any feed in ~20 lines

Extend WebSocketAdapter (live + REST history) or PollingAdapter (REST-only feeds). The base handles the connection lifecycle, reconnect, decoding, and event emission — you supply a URL and a parse function.

import { WebSocketAdapter } from '@tradecanvas/chart'

const myAdapter = new WebSocketAdapter({
  name: 'myexchange',
  wsUrl: (c) => 'wss://api.myexchange.com/ws/' + c.symbol + '@kline_' + c.timeframe,
  fetchHistory: (symbol, tf, limit) => fetch('/candles?...').then((r) => r.json()),
  parseMessage: (raw) => ({ bar: toBar(raw), closed: raw.k.x }),
})

Tick aggregation

Roll raw ticks into OHLC bars per timeframe:

import { TickAggregator } from '@tradecanvas/chart'

const agg = new TickAggregator('1m')
agg.processTick({ time, price, volume })

const current = agg.getCurrentBar()         // forming bar
const closed = agg.flushClosedBars()        // bars that have rolled over

Quotes for many symbols

A feed with subscribeQuotes sends quotes (last price, the day's change, high, low, volume, bid and ask) for many symbols at once until you stop it. BinanceAdapter sends a 24 h snapshot, then its mini-ticker stream; MockAdapter makes quotes up. A widget's watchlist fills its rows from it.

const stop = new BinanceAdapter().subscribeQuotes(['BTCUSDT', 'ETHUSDT'], (quotes) => {
  for (const q of quotes) console.log(q.symbol, q.last, q.changePercent)
})
stop()

readQuote({ symbol: 'AAPL', last: 190, prevClose: 188 })   // → change 2, changePercent 1.06

Tick charts

A tick timeframe — '100T' — draws a bar per 100 trades. It is built from a feed's trades: its recent ones for the history (fetchTrades), then its live ones (subscribeTrades). BinanceAdapter streams aggregate trades. A tick chart has no countdown and pages in no older history; a feed without trades refuses tick timeframes.

chart.connect({ adapter: new BinanceAdapter(), symbol: 'BTCUSDT', timeframe: '100T' })
await chart.setTimeframe('500T')

// Your own trades into tick bars
const builder = new TickBarBuilder(100)
builder.push([{ time, price, volume }])   // → [{ bar, closed }]

Market status and news

marketStatus(info, now) tells whether a symbol's market is open at a moment, from its hours in its exchange's zone (clock changes included), and when that changes. Sessions can name the weekdays they open on. A feed's fetchNews gives headlines for the widget's symbol info panel; readNews keeps only items with a title and a time, and links to web pages.

const info = {
  symbol: 'AAPL',
  timezone: 'America/New_York',
  sessions: [{ start: '09:30', end: '16:00', days: [1, 2, 3, 4, 5] }],
}
marketStatus(info, Date.now())   // { state: 'closed', next: <next open, ms> } · 'open' · 'always' (24/7)

Reconnect

ReconnectManager handles exponential backoff with a cap and a final give-up.

import { ReconnectManager } from '@tradecanvas/chart'

const rm = new ReconnectManager({ maxRetries: 6, baseDelay: 500, maxDelay: 30_000 })

rm.start()
rm.schedule(() => connectWebsocket())   // call on disconnect; backs off automatically
rm.onConnected()                        // call on a successful connect to reset

Replay mode

Drive a historical DataSeries forward at controlled speed. Decoupled from Chart so it can power both UI replay and headless backtests.

import { ReplayController } from '@tradecanvas/chart'

const replay = new ReplayController({
  data: historicalBars,
  speed: 10,      // bars per second
  startIndex: 0,
})

// Seed the chart with everything before startIndex
chart.setData(replay.getPrefix())

// Wire each emitted bar into the chart
replay.on('bar', ({ bar }) => chart.appendBar(bar))
replay.on('finished', () => console.log('done'))

replay.start()
// replay.pause(); replay.resume(); replay.step(5); replay.seek(200); replay.setSpeed(20)

In the widget's replay mode, click any revealed bar to jump the replay cursor there. More generally, the chart now emits click and barClick events for plain left-clicks (press and release without dragging):

chart.on('barClick', (e) => {
  const { bar, barIndex, point } = e.payload
})

Replay on the chart, in finer steps

chart.replayStart() replays the chart's own series. Give it finer bars as steps (5-minute bars under an hourly chart) and each step grows the forming bar from them, as it grew in the market; closed bars show as they are in the series. startIndex and replaySeekToBar count the chart's bars. In ChartWidget, the replay bar's Step menu offers the finer intervals the feed has (or that the bars you loaded can build).

const steps = await adapter.fetchHistory('BTCUSDT', '5m', 2000)
chart.replayStart({ steps, startIndex: 120, paused: true, speed: 5 })
chart.replayResume()
chart.getReplayBarIndex()      // the chart bar forming now
chart.replaySeekToBar(150)     // to the end of bar 150
chart.replaySeekToTime(t)      // to the bars that opened before t
chart.on('replayStep', (e) => e.payload)    // { barIndex, time, until }
chart.on('replayState', (e) => e.payload)   // { state: 'playing' | 'paused' | 'stopped' }
chart.replayStop()             // back to the live series

Paper trading in a replay

An execution adapter with setMarkPrice(price, time) (as PaperExecutionAdapter has) trades on the replay: during a replay the chart hands it each replayed price and time, so orders and stops fill as the replay passes them and fills land on the replayed bars. Each step's low and high count, and a jump ahead passes every step on the way. It only goes forward: after a seek back it waits until the replay is past the furthest point it has seen. The live price doesn't move it meanwhile; when the replay ends it goes back to the live price, and fills to the clock. Price alerts keep watching the live market throughout; alerts on indicator lines wait for the replay to end.

chart.connectExecution(new PaperExecutionAdapter())
chart.replayStart({ startIndex: 300, paused: true })
// place orders from the chart or the order ticket, then play

API

MethodPurpose
start()Begin emitting from current index.
pause() / resume()Pause/continue timer.
step(n)Emit N bars synchronously without the timer.
seek(index)Jump without emitting in-between bars.
setSpeed(bps)Bars per second (clamped to >= 0.01).
destroy()Clear timer + listeners.

See Analytics for using replay alongside the backtester.