# TradeCanvas > Canvas2D trading chart library for the web (TypeScript, no runtime dependencies): candlestick, Heikin-Ashi, Renko and other chart types, 95 indicators, drawing tools, live exchange feeds, orders and positions on the chart, alerts, replay and backtesting. npm: @tradecanvas/chart (Chart and the ChartWidget UI), @tradecanvas/react, @tradecanvas/vue, @tradecanvas/svelte, @tradecanvas/analytics. Use `ChartWidget` (`@tradecanvas/chart/widget`) for a complete trading UI, or `Chart` (`@tradecanvas/chart`) to build your own around it. Both need a sized DOM element and run in the browser only. ## Docs - [Getting started](https://bonguynvan.github.io/tradecanvas/docs/getting-started): install, the widget, the headless chart - [API reference](https://bonguynvan.github.io/tradecanvas/docs/api): Chart and ChartWidget methods, options and events - [Indicators](https://bonguynvan.github.io/tradecanvas/docs/indicators): every built-in indicator with its id, inputs and lines - [Styling](https://bonguynvan.github.io/tradecanvas/docs/styling): the widget's look as tokens: the Studio, Terminal and Capsule presets, your own theme, CSS variables - [Chart types](https://bonguynvan.github.io/tradecanvas/docs/chart-types): candlestick, Heikin-Ashi, Renko, Kagi, point & figure and more - [Drawing tools](https://bonguynvan.github.io/tradecanvas/docs/drawing-tools): trend lines, Fibonacci, Gann, patterns, text - [Realtime and replay](https://bonguynvan.github.io/tradecanvas/docs/realtime): data adapters (Binance, Coinbase, Bybit, Kraken, WebSocket, polling) and replay - [Trading](https://bonguynvan.github.io/tradecanvas/docs/trading): orders, positions, brackets and execution adapters on the chart - [Frameworks](https://bonguynvan.github.io/tradecanvas/docs/frameworks): React, Vue and Svelte components - [Plugins](https://bonguynvan.github.io/tradecanvas/docs/plugins): custom indicators, drawings and chart types - [Analytics](https://bonguynvan.github.io/tradecanvas/docs/analytics): strategy backtester and risk metrics ## Agent skill - [SKILL.md](https://github.com/bonguynvan/tradecanvas/blob/main/skills/tradecanvas/SKILL.md): how to build with TradeCanvas, the rules that avoid most bugs, worked examples - [Indicator reference](https://github.com/bonguynvan/tradecanvas/blob/main/skills/tradecanvas/references/indicators.md): ids, default params, lines and levels - [Recipes](https://github.com/bonguynvan/tradecanvas/blob/main/skills/tradecanvas/references/recipes.md): live data, custom adapters, indicators on indicators, custom indicators, layouts ## Optional - [Everything in one file](https://bonguynvan.github.io/tradecanvas/llms-full.txt): the library README, the skill and its references - [Source](https://github.com/bonguynvan/tradecanvas) ============================================================================== skills/tradecanvas/SKILL.md ============================================================================== --- name: tradecanvas description: Build trading and financial charts with TradeCanvas (@tradecanvas/chart) — candlesticks and 16 other chart types, 85 indicators, drawing tools, live exchange data, orders and positions on the chart, alerts, replay. Use when a project imports @tradecanvas/*, or when asked to add a price, candlestick or trading chart to a web app (vanilla TS/JS, React, Vue, Svelte). --- # TradeCanvas TradeCanvas draws trading charts on two stacked Canvas2D layers, in TypeScript, with no runtime dependencies. One package, `@tradecanvas/chart`, holds both the chart engine and a complete trading UI around it. ## Pick the entry point | You need | Use | |---|---| | A full trading UI: toolbar, drawing sidebar, indicator legend, settings, status bar | `ChartWidget` from `@tradecanvas/chart/widget` | | A chart inside your own UI | `Chart` from `@tradecanvas/chart` | | React, Vue or Svelte components | `@tradecanvas/react`, `@tradecanvas/vue`, `@tradecanvas/svelte` | | Strategy backtests and risk metrics | `@tradecanvas/analytics` | `widget.getChart()` returns the `Chart` inside a widget, so everything below also works on a widget. ## Rules that avoid most bugs 1. **Give the container a size** (width and height) before creating the chart. The chart follows later size changes by itself. 2. **Browser only.** In SSR frameworks (Next, Nuxt, SvelteKit) create the chart in `useEffect` / `onMounted` / `onMount`, importing it there with `await import('@tradecanvas/chart')` if the module is also loaded on the server. 3. **Call `destroy()`** when the element goes away (component unmount). 4. **Bars** are `{ time, open, high, low, close, volume }`, ascending by time. `time` may be milliseconds or seconds (values up to 1e12 are read as seconds). Keep one unit per series. 5. **Indicator ids are short** — `'rsi'`, `'bb'`, `'psar'`, `'stochastic'` — see [references/indicators.md](references/indicators.md). `addIndicator` returns an instance id (`null` if the feature or id is turned off); later calls take that instance id, not the indicator id. 6. **Live updates:** `appendBar` for a new bar, `updateLastBar` for the forming one. Never `setData` on every tick: it resets the view and recomputes everything. 7. Read values with `chart.getIndicatorOutput(instanceId)?.series[i]` (an object keyed by the indicator's line keys), not by guessing from the drawing. ## Quick start: the widget with live data ```ts import { ChartWidget } from '@tradecanvas/chart/widget'; import { BinanceAdapter } from '@tradecanvas/chart'; const widget = new ChartWidget(document.getElementById('chart')!, { symbol: 'BTCUSDT', timeframe: '5m', theme: 'dark', adapter: new BinanceAdapter(), locale: 'en', // or 'vi' onReady: (chart) => { chart.addIndicator('ema', { period: 50 }); chart.addIndicator('rsi', { period: 14 }); }, }); // Later, when the page goes away: widget.destroy(); ``` ## Quick start: a chart with your own data ```ts import { Chart, type OHLCBar } from '@tradecanvas/chart'; const chart = new Chart(document.getElementById('chart')!, { chartType: 'candlestick', theme: 'dark', features: { indicators: true, drawings: true, volume: true }, }); const bars: OHLCBar[] = await fetch('/api/bars?symbol=AAPL&tf=1d').then((r) => r.json()); chart.setData(bars); // A new bar opens, then the forming bar ticks: const last = bars[bars.length - 1]; chart.appendBar({ time: last.time + 86_400_000, open: last.close, high: last.close, low: last.close, close: last.close, volume: 0 }); chart.updateLastBar({ ...chart.getData()[chart.getData().length - 1], close: last.close + 1.25 }); ``` For a live feed, give the chart an adapter instead and it handles history, streaming and reconnects: `chart.connect({ adapter, symbol, timeframe })`. Built-in adapters: `BinanceAdapter`, `CoinbaseAdapter`, `BybitAdapter`, `KrakenAdapter`, `MockAdapter` (random data for demos), and two bases for your own feeds — `WebSocketAdapter` and `PollingAdapter` (see [references/recipes.md](references/recipes.md)). ## Indicators ```ts import { Chart, indicatorSource } from '@tradecanvas/chart'; declare const chart: Chart; const ema = chart.addIndicator('ema', { period: 21 })!; // on the price pane const rsi = chart.addIndicator('rsi', { period: 14 }, 'bottom')!; // a pane of its own chart.updateIndicator(ema, { period: 34 }); // change inputs chart.updateIndicator(ema, { source: 'hlc3' }); // compute from another price chart.setIndicatorLevels(rsi, [20, 50, 80]); // RSI's reference lines (null = defaults) chart.updateIndicatorStyle(rsi, { colors: ['#f2a93b'], lineWidths: [2] }); chart.setIndicatorVisible(ema, false); // An indicator on another indicator's line: an SMA of RSI is drawn in RSI's pane. const smoothed = chart.addIndicator('sma', { period: 9, source: indicatorSource(rsi, 'value') }); // Values at the latest bar, by line key: const series = chart.getIndicatorOutput(rsi)?.series ?? []; const latestRsi = series[series.length - 1]?.value; chart.removeIndicator(rsi); // also removes `smoothed`, which reads from it ``` - Panel indicators may share a pane: `addIndicator('stochastic', {}, 'bottom', { pane: rsi })`. - Each line's latest value is tagged on its axis; turn that off with `features.indicatorValueLabels: false`. - Custom indicators: extend `IndicatorBase`, declare `plots`, and register with `chart.registerIndicator(...)` — see [references/recipes.md](references/recipes.md). ## Drawings, alerts and orders ```ts import type { Chart } from '@tradecanvas/chart'; declare const chart: Chart; declare const t1: number, t2: number; // bar times chart.setDrawingTool('fibRetracement'); // the user clicks the points; null cancels const line = chart.addDrawing({ type: 'trendLine', anchors: [{ time: t1, price: 61_800 }, { time: t2, price: 63_400 }], options: { extendRight: true }, // each tool's own settings: chart.getDrawingOptionDefs(type) }); if (line && chart.canAddDrawingAlert(line)) chart.addDrawingAlert(line, { condition: 'crossingDown' }); chart.addAlert(65_000, 'crossingUp', 'BTC above 65k'); chart.setPositions([{ id: 'p1', side: 'buy', entryPrice: 62_500, quantity: 0.5, stopLoss: 61_000, takeProfit: 66_000 }]); chart.on('positionModify', (e) => console.log('stop or target dragged', e.payload)); ``` 69 drawing tools; each keeps its own settings (`setDrawingOptions`, `updateDrawing`), and drawings can be grouped (`groupDrawings`), reordered (`moveDrawing`) and erased (`setEraserMode`). `riskReward` is a Long/Short position sized from `accountSize` and `risk`. ## Saving layouts ```ts import type { Chart } from '@tradecanvas/chart'; declare const chart: Chart; const json = chart.saveState(); // chart type, theme, drawings, indicators, alerts if (json) localStorage.setItem('layout', json); chart.loadState(localStorage.getItem('layout') ?? '{}'); chart.setAutoSave('layout', 1500); // or keep it saved as it changes ``` Saved indicators keep their inputs, sources, panes, colours, visibility and levels. ## Events `chart.on(event, (e) => …)` — the payload is `e.payload`. | Event | Payload | |---|---| | `crosshairMove` | `{ point, bar, barIndex }` (also over indicator panes) | | `crosshairLeave` | — | | `barClick` | `{ bar, barIndex, point }` | | `visibleRangeChange` | `{ from, to }` bar indices | | `dataUpdate` | the series | | `indicatorAdd` / `indicatorRemove` | `{ instanceId, id }` | | `indicatorChange` | `{ instanceId, change }`: `'visible' \| 'style' \| 'levels' \| 'params' \| 'pane'` | | `indicatorUpdate` | `{ from }`: values recomputed from that bar on (once per update) | | `drawingCreate` | `{ id, type, drawing }` (also when an undo brings a drawing back) | | `drawingUpdate`, `drawingRemove`, `drawingDoubleClick` | `{ id }` | | `drawingContextMenu` | `{ id, x, y }`: a drawing was right-clicked | | `toolModeChange` | `{ eraser }` or `{ zoomArea }` | | `orderModify`, `positionModify` | the object | ## Theming `theme: 'dark' | 'light'` or a `Theme` object (`{ ...DARK_THEME, candleUp: '#1fa874' }`); `chart.setTheme(...)` at runtime. Numbers follow `numberLocale` (`'en-US'`, `'vi-VN'` …). ## When something looks wrong | Symptom | Check | |---|---| | Blank chart | The container has no height (check its computed size) | | Garbled candles, wrong dates on the axis | Bars not ascending by time: `setData` does not sort them | | Dates in 1970 | `time` in seconds treated as ms (or the reverse) mixed in one series | | `addIndicator` returns `null` | `features.indicators` is off, or `features.indicatorIds` leaves it out | | Indicator line missing at the start | Warm-up: no value until its period has enough bars | | Chart stutters on a fast feed | `setData` called per tick instead of `updateLastBar` / `appendBar` | ## References - [references/indicators.md](references/indicators.md) — every built-in indicator: id, default params, lines, levels, source support (generated from the code). - [references/recipes.md](references/recipes.md) — worked examples: custom data feeds, frameworks, indicators on indicators, a custom indicator, multi-chart layouts. - Docs site: https://bonguynvan.github.io/tradecanvas/ · API: https://bonguynvan.github.io/tradecanvas/docs/api ============================================================================== skills/tradecanvas/references/recipes.md ============================================================================== # TradeCanvas recipes Worked examples. Every `ts` block here is type-checked against the library in CI (`pnpm docs:check`), so the calls are real. ## Your own data feed over REST `PollingAdapter` turns any "give me the last N bars" endpoint into a live feed: it loads history, polls, updates the forming bar and closes it on rollover. ```ts import { Chart, PollingAdapter, type OHLCBar, type TimeFrame } from '@tradecanvas/chart'; const feed = new PollingAdapter({ name: 'my-api', intervalMs: 5_000, fetchBars: async (symbol: string, timeframe: TimeFrame, limit: number): Promise => { const res = await fetch(`/api/candles?symbol=${symbol}&tf=${timeframe}&limit=${limit}`); const rows: [number, number, number, number, number, number][] = await res.json(); return rows.map(([time, open, high, low, close, volume]) => ({ time, open, high, low, close, volume })); }, }); const chart = new Chart(document.getElementById('chart')!, { theme: 'dark' }); await chart.connect({ adapter: feed, symbol: 'AAPL', timeframe: '1h', historyLimit: 500 }); ``` ## Your own data feed over WebSocket `WebSocketAdapter` handles connecting, reconnecting and the history request; you describe the URL, the subscribe message and how to read a frame. ```ts import { Chart, WebSocketAdapter, type OHLCBar } from '@tradecanvas/chart'; interface KlineFrame { t: number; o: string; h: string; l: string; c: string; v: string; final: boolean } const feed = new WebSocketAdapter({ name: 'my-exchange', wsUrl: () => 'wss://stream.example.com/ws', subscribeMessage: ({ symbol, timeframe }) => ({ op: 'subscribe', channel: `kline.${timeframe}.${symbol}` }), fetchHistory: async (symbol, timeframe, limit): Promise => { const res = await fetch(`https://api.example.com/klines?symbol=${symbol}&interval=${timeframe}&limit=${limit}`); return res.json(); }, parseMessage: (raw) => { const k = (raw as { kline?: KlineFrame }).kline; if (!k) return null; const bar = { time: k.t, open: +k.o, high: +k.h, low: +k.l, close: +k.c, volume: +k.v }; return { bar, closed: k.final }; }, }); const chart = new Chart(document.getElementById('chart')!, {}); await chart.connect({ adapter: feed, symbol: 'BTCUSDT', timeframe: '1m' }); ``` ## React, Vue, Svelte The wrapper packages give a component with reactive props: ```tsx import { TradeCanvas } from '@tradecanvas/react'; import { BinanceAdapter } from '@tradecanvas/chart'; const adapter = new BinanceAdapter(); export function PriceChart({ symbol }: { symbol: string }) { return ( chart.setIndicatorValueLabelsVisible(true)} style={{ height: 480 }} /> ); } ``` `@tradecanvas/vue` and `@tradecanvas/svelte` take the same data props (Vue emits `ready` and `crosshairMove` instead of the callback props). To drive the widget or a bare `Chart` from a framework, create it in the mount hook on a sized element and call `destroy()` in the unmount hook. ## A signal from an indicator on an indicator An SMA of RSI, drawn in RSI's pane, and a signal when RSI closes above it. It looks at the two last *closed* bars (the forming one still moves) and fires once per bar: ```ts import { Chart, BinanceAdapter, indicatorSource } from '@tradecanvas/chart'; const chart = new Chart(document.getElementById('chart')!, { theme: 'dark' }); await chart.connect({ adapter: new BinanceAdapter(), symbol: 'ETHUSDT', timeframe: '15m' }); const rsi = chart.addIndicator('rsi', { period: 14 })!; const signal = chart.addIndicator('sma', { period: 9, source: indicatorSource(rsi, 'value') })!; let lastSignalBar = -1; chart.on('indicatorUpdate', () => { const r = chart.getIndicatorOutput(rsi)?.series ?? []; const s = chart.getIndicatorOutput(signal)?.series ?? []; const i = r.length - 2; // the bar that closed last if (i <= lastSignalBar) return; const [r0, r1, s0, s1] = [r[i - 1]?.value, r[i]?.value, s[i - 1]?.value, s[i]?.value]; if (r0 === undefined || r1 === undefined || s0 === undefined || s1 === undefined) return; if (r0 <= s0 && r1 > s1) { lastSignalBar = i; console.log('RSI closed above its average'); } }); ``` ## Alerts beyond a price level A line crossing another line, a fast move, a close past a level, and an end date. They check on every price the feed sends, with the lines they watch taken at the same moment: ```ts import { Chart, BinanceAdapter } from '@tradecanvas/chart'; const chart = new Chart(document.getElementById('chart')!, { theme: 'dark' }); await chart.connect({ adapter: new BinanceAdapter(), symbol: 'BTCUSDT', timeframe: '1h' }); const ema = chart.addIndicator('ema', { period: 50 })!; const rsi = chart.addIndicator('rsi', { period: 14 })!; chart.addAlert(NaN, 'crossingUp', 'above the 50 EMA', 'price', undefined, { target: `${ema}:value` }); chart.addAlert(NaN, 'movesDown', 'dump', 'price', undefined, { percent: 4, bars: 6 }); // bars: 2–500 chart.addAlert(70, 'greaterThan', 'RSI closed above 70', `${rsi}:value`, 'RSI', { onBarClose: true }); chart.addAlert(64_000, 'crossing', 'today only', 'price', undefined, { expiresAt: Date.now() + 86_400_000 }); chart.on('alertTriggered', (e) => console.log(e.payload.message, e.payload.channel, e.payload.target)); chart.on('alertExpired', (e) => console.log('expired', e.payload.id)); ``` ## Practising on a replay with a paper account Replay an hourly chart in 5-minute steps and trade it on paper: orders fill on the replayed prices and fills land on the replayed bars. Alerts keep watching the live market meanwhile. ```ts import { Chart, BinanceAdapter, PaperExecutionAdapter } from '@tradecanvas/chart'; const adapter = new BinanceAdapter(); const chart = new Chart(document.getElementById('chart')!, { theme: 'dark' }); await chart.connect({ adapter, symbol: 'BTCUSDT', timeframe: '1h' }); chart.connectExecution(new PaperExecutionAdapter()); const steps = await adapter.fetchHistory('BTCUSDT', '5m', 2000); chart.replayStart({ steps, startIndex: 300, paused: true, speed: 5 }); chart.replayResume(); // … place orders from the chart; chart.replaySeekToBar(i) jumps, chart.replayStop() goes back to live ``` ## Another symbol on its own scale, and the ratio of the two ETH beside BTC on a price scale of its own, and BTC ÷ ETH in a pane. The indicators ask for the bars of the symbol they read; fetch them again when the interval changes: ```ts import { Chart, BinanceAdapter } from '@tradecanvas/chart'; const adapter = new BinanceAdapter(); const chart = new Chart(document.getElementById('chart')!, { theme: 'dark' }); await chart.connect({ adapter, symbol: 'BTCUSDT', timeframe: '1h' }); const load = async (symbol: string) => chart.setSymbolSeries(symbol, await adapter.fetchHistory(symbol, '1h', 1000)); chart.on('symbolSeriesRequest', (e) => void load(e.payload.symbol)); chart.addIndicator('compareSymbol', { symbol: 'ETHUSDT' }, 'bottom', { scale: 'left' }); const ratio = chart.addIndicator('spread', { symbol: 'ETHUSDT', mode: 'ratio' })!; chart.setPaneScale(ratio, { percent: true }); ``` ## Prices in 32nds A bond future quoted in 32nds and half 32nds, on High-Low bars, regular session only: ```ts import { Chart } from '@tradecanvas/chart'; const chart = new Chart(document.getElementById('chart')!, { chartType: 'hiLo', priceFormat: { denominator: 32, subDenominator: 2 }, // 110'165 = 110 and 16.5/32 extendedHours: false, }); chart.setSymbolInfo({ symbol: 'ZN', timezone: 'America/Chicago', sessions: [{ start: '07:20', end: '14:00' }] }); ``` ## A custom indicator Extend `IndicatorBase` and declare what it draws (`plots`), its pane scale and levels; the chart then draws it, scales its pane, labels its values and lists it in the widget legend and settings with no rendering code of yours. ```ts import { Chart, IndicatorBase, IndicatorValueMap } from '@tradecanvas/chart'; import type { DataSeries, IndicatorConfig, IndicatorDescriptor, IndicatorOutput, IndicatorValue } from '@tradecanvas/chart'; /** Where the close sits in the last `period` bars' range, 0–100. */ class RangePosition extends IndicatorBase { descriptor: IndicatorDescriptor = { id: 'rangePosition', name: 'Range Position', shortName: 'RP', placement: 'panel', defaultConfig: { period: 20 }, inputs: { period: { min: 2, max: 500 } }, plots: [{ key: 'value', title: 'RP', color: 0 }], scale: { min: 0, max: 100 }, levels: [20, 80], }; calculate(data: DataSeries, config: IndicatorConfig): IndicatorOutput { const period = Math.max(2, Number(config.params.period) || 20); const values = new IndicatorValueMap(); const series: (IndicatorValue | null)[] = new Array(data.length).fill(null); for (let i = period - 1; i < data.length; i++) { let hi = -Infinity; let lo = Infinity; for (let j = i - period + 1; j <= i; j++) { hi = Math.max(hi, data[j].high); lo = Math.min(lo, data[j].low); } const point = { value: hi > lo ? ((data[i].close - lo) / (hi - lo)) * 100 : 50 }; values.set(data[i].time, point); series[i] = point; } return { values, series }; } } const chart = new Chart(document.getElementById('chart')!, {}); chart.registerIndicator(new RangePosition()); chart.addIndicator('rangePosition', { period: 30 }); ``` Declare `inputs: { source: { source: true } }` and read `data[i].close` to let users run it on another price or another indicator's line. ## Several charts in a grid ```ts import { ChartGrid, BinanceAdapter } from '@tradecanvas/chart'; const grid = new ChartGrid(document.getElementById('grid')!, { layout: '2x2', syncCrosshair: true, syncTimeAxis: true, }); // An adapter keeps one stream: each chart needs its own. await grid.connectAll(() => new BinanceAdapter(), ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT'], '5m'); grid.getChart(0)?.addIndicator('ema', { period: 50 }); ``` With the full widget on each chart, `ChartWidgetGrid` adds a bar to pick the arrangement and the sync, and saves the whole grid as a named layout: ```ts import { ChartWidgetGrid } from '@tradecanvas/chart/widget'; import { BinanceAdapter } from '@tradecanvas/chart'; const workspace = new ChartWidgetGrid(document.getElementById('grid')!, { layout: '1x2', adapter: () => new BinanceAdapter(), // one per chart cells: [{ symbol: 'BTCUSDT' }, { symbol: 'ETHUSDT', timeframe: '1h' }], sync: { crosshair: true, interval: true }, }); workspace.getActiveWidget().getChart().addIndicator('rsi', { period: 14 }); ``` ## Named layouts on your own server ```ts import { ChartWidget, type LayoutStorage, type SavedLayout } from '@tradecanvas/chart/widget'; const server: LayoutStorage = { list: async () => (await fetch('/api/layouts')).json(), load: async (id) => { const res = await fetch(`/api/layouts/${encodeURIComponent(id)}`); return res.ok ? ((await res.json()) as SavedLayout) : null; }, save: async (layout) => { await fetch(`/api/layouts/${encodeURIComponent(layout.id)}`, { method: 'PUT', body: JSON.stringify(layout) }); }, remove: async (id) => { await fetch(`/api/layouts/${encodeURIComponent(id)}`, { method: 'DELETE' }); }, }; const widget = new ChartWidget(document.getElementById('chart')!, { symbol: 'BTCUSDT', layouts: { storage: server, openLast: true }, // Ctrl/Cmd+S saves, the open layout auto-saves }); await widget.getLayoutSession()?.saveAs('Swing BTC'); ``` Treat what the server returns as untrusted: the widget refuses content it cannot read, but your API should still check who owns a layout. ## The widget, saved per symbol and in Vietnamese ```ts import { ChartWidget } from '@tradecanvas/chart/widget'; import { BinanceAdapter } from '@tradecanvas/chart'; new ChartWidget(document.getElementById('chart')!, { adapter: new BinanceAdapter(), symbol: 'BTCUSDT', symbols: ['BTCUSDT', 'ETHUSDT', 'SOLUSDT'], persistLayouts: true, // indicators, drawings and chart type per symbol, in localStorage locale: 'vi', chartOptions: { numberLocale: 'vi-VN' }, }); ``` ## The widget in a look of your own ```ts import { ChartWidget, WIDGET_UI_PRESETS } from '@tradecanvas/chart/widget'; import { BinanceAdapter } from '@tradecanvas/chart'; // 'studio' (the default), 'terminal' (dense, square) or 'capsule' (pills, floating bars) const widget = new ChartWidget(document.getElementById('chart')!, { adapter: new BinanceAdapter(), symbol: 'BTCUSDT', ui: 'terminal', }); // Your theme over a preset: what you leave out stays the preset's. widget.setUI({ preset: 'studio', radius: { md: 10, lg: 14 }, // buttons and fields take md, menus and panels lg density: 'compact', font: { family: "'Manrope', system-ui, sans-serif", labelCase: 'uppercase' }, toolbar: 'floating', active: 'solid', // tint · solid · underline tagRadius: 999, // pill price tags on the chart }); console.log(widget.getUI().sizes.control, WIDGET_UI_PRESETS.capsule.components.control); ``` The look changes shapes and sizes only; colours stay with `theme`. The widget loads no fonts, so load the families a look names. Without `ui` the look's CSS variables (`--tcw-radius`, `--tcw-control-h`, …) stay the stylesheet's and your CSS can set them; with it the widget writes them inline. ## Watchlists with live quotes, and a tick chart ```ts import { ChartWidget } from '@tradecanvas/chart/widget'; import { BinanceAdapter, marketStatus } from '@tradecanvas/chart'; const widget = new ChartWidget(document.getElementById('chart')!, { adapter: new BinanceAdapter(), // its subscribeQuotes fills the rows symbol: 'BTCUSDT', watchlist: { lists: [ { id: 'majors', name: 'Majors', symbols: ['BTCUSDT', 'ETHUSDT'] }, { id: 'alts', name: 'Alts', symbols: ['SOLUSDT', 'ADAUSDT'] }, ], persist: true, }, }); widget.addToWatchlist('BNBUSDT', 'alts'); widget.toggleSymbolInfo(true); // price, market status, the day, hours, news await widget.setTimeframe('100T'); // a bar per 100 trades, from Binance's trades // Your own feed's quotes instead widget.setQuotes([{ symbol: 'AAPL', last: 190.2, prevClose: 188.1 }]); console.log(marketStatus({ symbol: 'AAPL', timezone: 'America/New_York', sessions: [{ start: '09:30', end: '16:00', days: [1, 2, 3, 4, 5] }] }, Date.now()).state); ``` Quotes come from a feed's `subscribeQuotes` (or `watchlist.quotes`, or `setQuotes`); tick timeframes need a feed with `fetchTrades` and `subscribeTrades`. ============================================================================== skills/tradecanvas/references/indicators.md ============================================================================== # Built-in indicators 95 indicators. Add one with `chart.addIndicator(id, params)`; params you leave out take the defaults below. An indicator's values at bar `i` are `chart.getIndicatorOutput(instanceId).series[i]`, an object keyed by the line keys listed here. "Source ✓" means it has a `source` input: a price (`close`, `open`, `high`, `low`, `hl2`, `hlc3`, `ohlc4`, `hlcc4`) or another indicator's line (`indicatorSource(instanceId, key)`). ## On the price pane (35) Drawn over the candles, on the price scale. | id | Name | Default params | Lines (key: title) | Levels | Source | |---|---|---|---|---|---| | `sma` | Simple Moving Average | period: 20, source: "close" | value: SMA | — | ✓ | | `ema` | Exponential Moving Average | period: 20, source: "close" | value: EMA | — | ✓ | | `bb` | Bollinger Bands | period: 20, stdDev: 2, source: "close" | upper: Upper, middle: Basis, lower: Lower | — | ✓ | | `vwap` | Volume Weighted Average Price | — | value: VWAP | — | | | `ichimoku` | Ichimoku Cloud | tenkan: 9, kijun: 26, senkou: 52, displacement: 26 | tenkan: Conversion, kijun: Base, senkouA: Lead A, senkouB: Lead B | — | | | `psar` | Parabolic SAR | step: 0.02, max: 0.2 | value: SAR | — | | | `supertrend` | Supertrend | period: 10, multiplier: 3 | value: Supertrend | — | | | `keltner` | Keltner Channel | emaPeriod: 20, atrPeriod: 10, multiplier: 1.5 | upper: Upper, middle: Basis, lower: Lower | — | | | `donchian` | Donchian Channel | period: 20 | upper: Upper, middle: Basis, lower: Lower | — | | | `pivots` | Pivot Points (Classic) | lookback: 24 | r3: R3, r2: R2, r1: R1, pp: P, s1: S1, s2: S2, s3: S3 | — | | | `avwap` | Anchored VWAP | anchorTime: 0 | value: AVWAP | — | | | `zigzag` | ZigZag | deviation: 5 | pivot: Pivot | — | | | `lrc` | Linear Regression Channel | period: 100, stdDev: 2, source: "close" | upper: Upper, middle: Basis, lower: Lower | — | ✓ | | `hma` | Hull Moving Average | period: 21, source: "close" | value: HMA | — | ✓ | | `mtfma` | MTF Moving Average | period: 50, timeframe: "1d", source: "close" | value: MA | — | ✓ | | `svwap` | Session VWAP | bands: 1 | value: VWAP, u1: +1σ, l1: −1σ, u2: +2σ, l2: −2σ, u3: +3σ, l3: −3σ | — | | | `chandelier` | Chandelier Exit | period: 22, multiplier: 3 | long: Long, short: Short | — | | | `alligator` | Alligator | jaw: 13, teeth: 8, lips: 5, jawShift: 8, teethShift: 5, lipsShift: 3 | jaw: Jaw, teeth: Teeth, lips: Lips | — | | | `vwma` | Volume Weighted Moving Average | period: 20, source: "close" | value: VWMA | — | ✓ | | `wma` | Weighted Moving Average | period: 20, source: "close" | value: WMA | — | ✓ | | `envelope` | Moving Average Envelope | period: 20, percent: 2.5, source: "close" | upper: Upper, basis: Basis, lower: Lower | — | ✓ | | `tema` | Triple EMA | period: 20, source: "close" | value: TEMA | — | ✓ | | `volumeProfile` | Volume Profile | rows: 24 | — | — | | | `dema` | Double Exponential Moving Average | period: 20, source: "close" | value: DEMA | — | ✓ | | `smma` | Smoothed Moving Average | period: 14, source: "close" | value: SMMA | — | ✓ | | `alma` | Arnaud Legoux Moving Average | period: 9, offset: 0.85, sigma: 6, source: "close" | value: ALMA | — | ✓ | | `kama` | Kaufman Adaptive Moving Average | period: 10, fast: 2, slow: 30, source: "close" | value: KAMA | — | ✓ | | `lsma` | Least Squares Moving Average | period: 25, offset: 0, source: "close" | value: LSMA | — | ✓ | | `mcginley` | McGinley Dynamic | period: 14, source: "close" | value: McGinley | — | ✓ | | `macross` | MA Cross | fast: 9, slow: 21, type: "sma", source: "close" | fast: Fast, slow: Slow | — | ✓ | | `fractals` | Williams Fractals | period: 2 | up: Up, down: Down | — | | | `cks` | Chande Kroll Stop | p: 10, x: 1, q: 9 | long: Stop Long, short: Stop Short | — | | | `seb` | Standard Error Bands | period: 21, mult: 2, smooth: 3 | upper: Upper, middle: Middle, lower: Lower | — | | | `gmma` | Guppy Multiple Moving Average | — | s3: EMA 3, s5: EMA 5, s8: EMA 8, s10: EMA 10, s12: EMA 12, s15: EMA 15, l30: EMA 30, l35: EMA 35, l40: EMA 40, l45: EMA 45, l50: EMA 50, l60: EMA 60 | — | | | `maribbon` | Moving Average Ribbon | ma1: 20, ma2: 50, ma3: 100, ma4: 200, exponential: 0 | ma1: MA 1, ma2: MA 2, ma3: MA 3, ma4: MA 4 | — | | ## In a pane of their own (60) Drawn in a pane under the chart (`addIndicator(id, params, 'bottom' | 'top')`), with their own scale and levels. | id | Name | Default params | Lines (key: title) | Levels | Source | |---|---|---|---|---|---| | `rsi` | Relative Strength Index | period: 14, source: "close" | value: RSI | 30, 70 | ✓ | | `macd` | MACD | fast: 12, slow: 26, signal: 9, source: "close" | histogram: Histogram, macd: MACD, signal: Signal | — | ✓ | | `stochastic` | Stochastic Oscillator | kPeriod: 14, dPeriod: 3, smooth: 3 | k: %K, d: %D | 20, 80 | | | `atr` | Average True Range | period: 14 | value: ATR | — | | | `adx` | Average Directional Index | period: 14 | adx: ADX, plusDI: +DI, minusDI: −DI | — | | | `obv` | On Balance Volume | — | value: OBV | — | | | `williamsR` | Williams %R | period: 14 | value: %R | -80, -20 | | | `cci` | Commodity Channel Index | period: 20 | value: CCI | -100, 0, 100 | | | `mfi` | Money Flow Index | period: 14 | value: MFI | 20, 80 | | | `aroon` | Aroon | period: 25 | up: Up, down: Down | — | | | `roc` | Rate of Change | period: 12, source: "close" | value: ROC | 0 | ✓ | | `tsi` | True Strength Index | longPeriod: 25, shortPeriod: 13, signalPeriod: 7, source: "close" | tsi: TSI, signal: Signal | 0 | ✓ | | `cmf` | Chaikin Money Flow | period: 20 | value: CMF | — | | | `stddev` | Standard Deviation | period: 20, source: "close" | value: StdDev | — | ✓ | | `ad` | Accumulation/Distribution | — | value: A/D | — | | | `vroc` | Volume Rate of Change | period: 14 | value: VROC | 0 | | | `ao` | Awesome Oscillator | fast: 5, slow: 34 | value: AO | — | | | `chaikinOsc` | Chaikin Oscillator | fast: 3, slow: 10 | value: Chaikin | — | | | `voldelta` | Volume Delta | mode: 0 | value: Delta | — | | | `vortex` | Vortex Indicator | period: 14 | viPlus: VI+, viMinus: VI− | 1 | | | `chop` | Choppiness Index | period: 14 | value: CHOP | 38.2, 61.8 | | | `uo` | Ultimate Oscillator | fast: 7, mid: 14, slow: 28 | value: UO | 30, 70 | | | `fi` | Force Index | period: 13 | value: EFI | 0 | | | `crsi` | Connors RSI | rsiPeriod: 3, streakPeriod: 2, rankPeriod: 100, source: "close" | value: CRSI | 10, 90 | ✓ | | `coppock` | Coppock Curve | longRoc: 14, shortRoc: 11, wma: 10, source: "close" | value: Coppock | 0 | ✓ | | `kst` | Know Sure Thing | roc1: 10, roc2: 15, roc3: 20, roc4: 30, sma1: 10, sma2: 10, sma3: 10, sma4: 15, signal: 9, source: "close" | value: KST, signal: Signal | 0 | ✓ | | `elderray` | Elder Ray | period: 13 | bull: Bull, bear: Bear | — | | | `stc` | Schaff Trend Cycle | fast: 23, slow: 50, cycle: 10, source: "close" | value: STC | 25, 75 | ✓ | | `kvo` | Klinger Oscillator | fast: 34, slow: 55, signal: 13 | value: KVO, signal: Signal | 0 | | | `fisher` | Fisher Transform | period: 9 | value: Fisher, trigger: Trigger | 0 | | | `dpo` | Detrended Price Oscillator | period: 20, source: "close" | value: DPO | — | ✓ | | `bop` | Balance of Power | smooth: 14 | value: BOP | 0 | | | `massindex` | Mass Index | ema: 9, sum: 25 | value: Mass | 26.5, 27 | | | `cmo` | Chande Momentum | period: 9, source: "close" | value: CMO | -50, 0, 50 | ✓ | | `trix` | TRIX | period: 15, signal: 9, source: "close" | value: TRIX, signal: Signal | 0 | ✓ | | `emv` | Ease of Movement | period: 14 | value: EOM | 0 | | | `pvt` | Price Volume Trend | — | value: PVT | — | | | `wad` | Williams A/D | — | value: WAD | — | | | `chaikinvol` | Chaikin Volatility | ema: 10, roc: 10 | value: CV | 0 | | | `rvi` | Relative Vigor Index | period: 10 | value: RVGI, signal: Signal | 0 | | | `stochrsi` | Stochastic RSI | rsiPeriod: 14, stochPeriod: 14, k: 3, d: 3, source: "close" | k: %K, d: %D | 20, 80 | ✓ | | `ppo` | Percentage Price Oscillator | fast: 12, slow: 26, signal: 9, source: "close" | hist: Histogram, value: PPO, signal: Signal | 0 | ✓ | | `ac` | Accelerator Oscillator | — | value: AC | — | | | `rmi` | Relative Momentum Index | period: 20, momentum: 5, source: "close" | value: RMI | 30, 70 | ✓ | | `disparity` | Disparity Index | period: 14, source: "close" | value: DI | 0 | ✓ | | `qstick` | Qstick | period: 8 | value: Qstick | 0 | | | `pgo` | Pretty Good Oscillator | period: 14 | value: PGO | -3, 0, 3 | | | `bbpb` | Bollinger Bands %B | period: 20, stdDev: 2, source: "close" | value: %B | 0, 0.5, 1 | ✓ | | `bbw` | Bollinger BandWidth | period: 20, stdDev: 2, source: "close" | value: BBW | — | ✓ | | `mom` | Momentum | period: 10, source: "close" | value: MOM | 0 | ✓ | | `hv` | Historical Volatility | period: 10, annual: 365, source: "close" | value: HV | — | ✓ | | `vo` | Volume Oscillator | short: 5, long: 10 | value: Vol Osc | 0 | | | `ulcer` | Ulcer Index | period: 14, source: "close" | value: Ulcer | — | ✓ | | `smi` | Stochastic Momentum Index | period: 10, smooth: 3, signal: 3 | smi: SMI, signal: Signal | 40, -40 | | | `rvix` | Relative Volatility Index | period: 10, smooth: 14 | value: RVI | 80, 50, 20 | | | `trendstrength` | Trend Strength Index | period: 14 | value: Trend | 0 | | | `lrslope` | Linear Regression Slope | period: 14 | value: Slope | 0 | | | `stderror` | Standard Error | period: 14 | value: StdErr | — | | | `adr` | Average Day Range | period: 14 | value: ADR | — | | | `netvolume` | Net Volume | — | value: Net | 0 | | ============================================================================== packages/library/README.md ==============================================================================

TradeCanvas, the chart engine for trading apps

npm version npm downloads CI status No third-party dependencies TypeScript MIT license GitHub stars

Live demo · Docs · Examples · Playground · Changelog

English · Tiếng Việt · 简体中文 · 日本語 · 한국어 · Español

**A complete trading chart for the web.** Candlesticks to Renko, 95 indicators, 69 drawing tools, live exchange feeds and orders on the chart, drawn on Canvas2D with zero dependencies. Drop in the full `ChartWidget`, or build your own UI on the headless `Chart`, in plain TypeScript, React, Vue or Svelte.

ChartWidget with live BTCUSDT from Binance: EMA 21 and 55, RSI, a trend line, a long position and a watchlist with live quotes

## Quick Start ```bash npm install @tradecanvas/chart # or: pnpm add / yarn add ``` `ChartWidget` is the whole trading UI in one component: toolbar, drawing sidebar, settings dialog and status bar. ```typescript import { ChartWidget } from '@tradecanvas/chart/widget' import { BinanceAdapter } from '@tradecanvas/chart' const widget = new ChartWidget(document.getElementById('chart')!, { symbol: 'BTCUSDT', timeframe: '5m', theme: 'dark', adapter: new BinanceAdapter(), // live data, no API key trading: true, }) ``` That's it. Live data, all 95 indicators, all 69 drawing tools, command palette (`Ctrl+K`), symbol search (`Ctrl+P`), hotkey sheet (`?`), shift-drag measure, alt-click tooltip pin, and drag-drop CSV/JSON loading. Using a framework? [`@tradecanvas/react`](https://github.com/bonguynvan/tradecanvas/tree/main/packages/react/), [`@tradecanvas/vue`](https://github.com/bonguynvan/tradecanvas/tree/main/packages/vue/) and [`@tradecanvas/svelte`](https://github.com/bonguynvan/tradecanvas/tree/main/packages/svelte/) wrap the headless `Chart` as a component, and the widget above mounts the same way in any framework; see [Framework Integration](#framework-integration). Or fork a [StackBlitz sandbox](https://bonguynvan.github.io/tradecanvas/examples/) and start from there. ## A Look Around
Drawing tools: a Fibonacci retracement with a note, a trend line, an Elliott impulse wave and a long position
69 drawing tools
Fibonacci, Gann, pitchforks, Elliott waves, harmonic patterns, notes and brushes, and a Long/Short position that sizes the trade. Alerts on trend lines, groups, undo and redo.
Trading on the chart: a long position with stop-loss and take-profit, a buy stop and a sell limit, and the account panel
Trading on the chart
Positions with live P&L, orders you drag to a new price, SL and TP, reverse and close buttons, an order ticket and an account panel. A paper broker is built in; connect your own.
A two-by-two workspace of BTC, ETH, SOL and BNB charts on mixed intervals
Multi-chart workspace
Up to six full charts side by side, linked by symbol, interval, crosshair, time or drawings, and saved as one layout.
One chart in three looks: studio, terminal, and capsule on the light theme
Your own look
Three presets (studio, terminal, capsule) or your own corner radius, density, fonts, toolbar and price tags, on light and dark themes.
## Why TradeCanvas? Most chart libraries make you choose: pretty charts with no trading features, or trading features with an ugly API. TradeCanvas gives you both. - **95 built-in indicators** — SMA, EMA, TEMA, VWMA, Hull MA, RSI, MACD, Bollinger, Envelope, Ichimoku, Pivot Points, Anchored VWAP, ZigZag, Linear Regression Channel, Awesome / Chaikin Oscillator, and more. Any indicator can read another one's line (an SMA of RSI). No separate calculation library needed. - **69 drawing tools** — Trendlines (info line, trend angle, cross line), Fibonacci (retracement, extension, channel, time zones, speed resistance fan and arcs, circles, spiral, wedge), horizontal/vertical lines, channels, pitchforks and pitchfan, Gann fan / box / square, cycles, harmonic patterns (XABCD, cypher, ABCD, three drives, head and shoulders), Elliott waves, notes, callouts and marks, brush and path, forecast and projection, Long/Short Position with position sizing, Volume Profile range. Each with its own settings, alerts on trend lines, groups and layers, undo/redo and full serialization. - **18 chart types** — Candlestick, line, area, bar, hollow candle, baseline, High-Low, Heikin-Ashi, Renko, Kagi, Line Break, Point & Figure, Range Bars, Volume Candles, **Equivolume**, HLC Area, Step Line, Line+Markers. Renko's box, Kagi's reversal and the like are yours to set. - **Pro-grade interaction** — pan freely past the last bar into empty future space (drawings can go there too), drag the price/time axes to scale, double-click to auto-fit, `Ctrl/⌘+drag` to select several drawings (then move, restyle or delete them together), `Shift+drag` to measure (bars × price Δ × %), `Alt+click` to pin a comparison tooltip, context cursors (crosshair, grabbing hand, resize arrows), axis-following price/time pill labels under the cursor, bar-hover highlight. - **Trading overlay** — Render open positions with entry line, P&L zone, and SL/TP markers. Orders as dashed lines. Drag SL/TP to modify, cancel / close / reverse from the buttons on each line, and see every fill marked on its bar. ChartWidget adds an order ticket that checks the order as you fill it in, and an account panel with positions, working orders and history. Cleanly opt-out via `features.trading: false` for non-trading projects. - **Real-time streaming** — Built-in Binance, Coinbase, Bybit, and Kraken adapters, plus generic `WebSocketAdapter` / `PollingAdapter` bases so any feed plugs in with ~20 lines. Older bars load as you scroll back, any interval (`7m`, `90m`, `2d`) is built from the feed's own, tick charts (`100T`) from its trades, and symbol search and quotes come from the feed. - **Time zones** — any IANA zone with daylight saving time (`'America/New_York'`), a fixed offset, or the exchange's own zone, for the axis, crosshair, day breaks and session hours. - **16 languages** — `ChartWidget` in English, Vietnamese, Simplified and Traditional Chinese, Japanese, Korean, Spanish, Portuguese, French, German, Russian, Turkish, Indonesian, Thai, Arabic and Hebrew; Arabic and Hebrew mirror right to left. - **Accessible** — keyboard navigation, zoom and scroll buttons over the chart, and a screen-reader summary that reads the view and the bars one at a time. - **Live execution** — connect an `ExecutionAdapter` to turn the trading overlay into a real trading surface, drag on the chart to create orders, and reconcile fills. Ships a `PaperExecutionAdapter` sandbox. - **Plugin SDK** — register custom indicators, drawing tools, chart types, and overlays — globally or per-chart. - **Strategy backtester** — `@tradecanvas/analytics` ships a bar-by-bar `Backtester` with virtual fills, commission/slippage models, portfolio tracking, and risk metrics (Sharpe, Sortino, Calmar, max drawdown). **Now with 4 ready-to-use reference strategies + Monte Carlo path-dependence analysis.** - **Replay mode** — replay the chart's own bars from any point, in finer steps if you like (an hourly chart forming from 5-minute bars), with play / pause / step / seek / speed, and paper-trade on the replayed prices. The widget has a replay bar for it; `ReplayController` drives bars headless too. - **Alerts** — on a price level, an indicator line, a drawing, or one line crossing another; on a move of some percent within some bars; only on closed bars; with an expiry. The widget's alerts panel sets all of them. - **Compare and spread** — other symbols in percent on the price scale, on a scale or pane of their own, or as a spread or ratio, lined up with the chart by time. - **Price formats** — prices in your own format or in fractions of a point (a bond in 32nds: 110'165) on every label; times your way; extended hours on or off; data export with the indicator lines. - **Volume Profile** — optional horizontal histogram of traded volume bucketed by price over the visible range, with point-of-control highlighting. - **Watchlists and symbol info** — lists of symbols to switch between, edit and reorder, with live quotes; a symbol panel with price, the market's status, the day's numbers, hours and news. - **CSV / JSON drag-and-drop** — drop a file onto the chart, it parses and loads instantly. Detects header layouts, ISO/unix-s/unix-ms timestamps, and array-vs-object JSON shapes. - **Named layouts** — save the chart under a name (symbol, interval, scale, indicators, drawings, alerts), open, rename, delete, auto-save the open one, `Ctrl/⌘+S`. Kept in the browser, or on your server through a four-call `LayoutStorage`. Per-symbol auto-persistence (`persistLayouts`) is there too. - **Multi-chart** — `ChartWidgetGrid` puts up to six full widgets side by side, linked by symbol, interval, crosshair, time or drawings as you choose, and saves them as one layout. `ChartGrid` does the same for bare charts. - **Signal markers & trade zones** — render bot/algorithm output (directional arrows, entry→exit rectangles) as a first-class chart layer. - **Hotkey sheet** — press `?` in the widget to open a categorized keyboard-shortcut reference. - **Extensible widget** — add your own toolbar buttons and right-click menu entries (`addToolbarButton`, `chartMenuItems`). - **Save/load chart state** — Persist drawings, indicators, theme, and chart type to JSON. Restore with one call. - **Zero dependencies** — The entire library is self-contained. No `d3`, no `chart.js`, no `fancy-canvas`. ## Headless Chart For projects that want to own the surrounding UI (custom toolbar, framework-specific controls), use the lower-level `Chart` class directly: ```typescript import { Chart, BinanceAdapter } from '@tradecanvas/chart' const chart = new Chart(document.getElementById('chart')!, { theme: 'dark', autoScale: true, features: { drawings: true, indicators: true, trading: true, // set false to disable orders/positions entirely tradingContextMenu: true, // opt-in right-click order menu (off by default) volume: true, }, }) const adapter = new BinanceAdapter() chart.connect({ adapter, symbol: 'BTCUSDT', timeframe: '5m', historyLimit: 300 }) ``` ### Widget Options | Option | Type | Default | Description | |---|---|---|---| | `symbol` | `string` | `'BTCUSDT'` | Initial trading symbol | | `timeframe` | `TimeFrame` | `'5m'` | Initial timeframe | | `theme` | `'dark' \| 'light' \| Theme` | `'dark'` | Chart theme | | `adapter` | `DataAdapter` | — | Data source adapter | | `toolbar` | `boolean` | `true` | Show top toolbar | | `drawingTools` | `boolean` | `true` | Show left drawing sidebar | | `settings` | `boolean` | `true` | Show settings button | | `trading` | `boolean` | `true` | Enable trading overlay | | `statusBar` | `boolean` | `true` | Show bottom status bar | | `rangeBar` | `boolean` | `true` | Range presets (1D … All) and go to date (Alt+G) on the status bar | | `indicatorLegend` | `boolean` | `true` | Indicators listed on the chart (under the OHLCV legend and atop their panes) with show / settings / remove | | `fullscreen` | `boolean` | `true` | Fullscreen button in the toolbar | | `symbols` | `string[]` | BTC/ETH/SOL/BNB | Searchable symbol catalog | | `timeframes` | `TimeFrame[]` | 1m to 1M | Timeframes on offer; pin favourites from the ▾ menu | | `chartTypes` | `ChartType[]` | 18 types | Available chart types | | `watchlist` | `boolean` | `false` | Right-side watchlist sidebar | | `dragDropImport` | `boolean` | `true` | Drop CSV / JSON files onto the chart to load data | | `persistLayouts` | `boolean \| { keyPrefix, debounceMs }` | `false` | Save per-symbol indicators / drawings / chart type to localStorage | | `onSymbolChange` | `(symbol) => void` | — | Symbol change callback | | `onTimeframeChange` | `(tf) => void` | — | Timeframe change callback | | `onReady` | `(chart) => void` | — | Fired when chart is ready | | `locale` | `string` | `'en'` | UI language — `'en'` and `'vi'` built in, 12 more from the locales entry, see **Widget i18n** below | | `messages` | `Partial>` | — | Override or add individual UI strings on top of `locale` | ### Icons The widget's icon set is exported for your own UI: `createIcon(name)`, `createToolIcon(drawingTool)`, `createChartTypeIcon(chartType)` return inline SVG strings drawn in `currentColor` (24 px grid, 1.75 px strokes). ```ts import { createToolIcon } from '@tradecanvas/chart/widget' button.innerHTML = createToolIcon('fibRetracement', 16) ``` ### Widget i18n `ChartWidget` speaks 16 languages: English, Vietnamese, Simplified and Traditional Chinese, Japanese, Korean, Spanish, Portuguese, French, German, Russian, Turkish, Indonesian, Thai, Arabic and Hebrew (the last two right to left; `dir` sets the direction yourself). Every string it shows is translated: toolbar, settings, drawing tools, alerts, dialogs, the command palette, the hotkey sheet and notices. Indicator names (SMA, RSI…) stay as they are. Set at construction. English and Vietnamese are built in. The others load from `@tradecanvas/chart/widget/locales`, so a page ships only the languages it imports: ```ts import { ja } from '@tradecanvas/chart/widget/locales' new ChartWidget(el, { locale: 'ja', messages: ja, // or registerWidgetLocales() for all of them chartOptions: { numberLocale: 'ja-JP' }, // separate: number/date formatting (see below) }); ``` `messages` also overrides single keys on top of `locale` (`{ 'watchlist.title': 'Theo dõi' }`). A locale with a region falls back to its language (`ja-JP` → `ja`; `zh-TW` → Traditional Chinese). `locale`/`messages` cover the **text**; `chartOptions.numberLocale` controls number and date **formatting** (price axis, legend, watchlist prices, current-price tag, session-break dates) via `Intl`. See `packages/library/src/widget/locales/en.ts` for the full key list (`MessageKey`). ### Widget vs Headless | | `Chart` (headless) | `ChartWidget` | |---|---|---| | Import | `@tradecanvas/chart` | `@tradecanvas/chart/widget` | | UI included | None — build your own | Complete toolbar, sidebar, settings | | Bundle impact | ~50 KB gzip | ~65 KB gzip (includes UI) | | Framework | Any (React, Vue, Svelte, vanilla) | Vanilla JS DOM (works everywhere) | | Customization | Full control | Toggle sections on/off | | Advanced access | Direct API | `widget.getChart()` for direct API | ### Widget Look The widget's shapes and sizes — corners, control heights, type, borders, shadows, how a chosen button shows, bars docked or floating — are a look of their own, apart from the colours. Three presets: **Studio** (the default), **Terminal** (dense and square) and **Capsule** (pills, floating bars). Start from one and change what you like: ```ts const widget = new ChartWidget(host, { ui: 'terminal' }); widget.setUI({ preset: 'studio', radius: { md: 10 }, density: 'compact', toolbar: 'floating', active: 'solid' }); ``` The chart's price tags take the same corners (`tagRadius`; on a bare `Chart`, `chart.setShapes({ tagRadius })`). The widget loads no fonts: load the ones a look names. See [Styling](https://bonguynvan.github.io/tradecanvas/docs/styling). ### Widget Theming `ChartWidget`'s own chrome (toolbar, sidebars, settings panel, watchlist — everything *outside* the canvas) is styled entirely through CSS custom properties on `.tcw-root`, the widget's own root element. These are a **stable, documented contract**: additive-only across minor/patch releases — a property is never renamed or removed without a major version bump. Override them from the host page; no build step or theme object needed. ```css /* Dark is the default (no attribute needed); light sets data-tcw-theme="light" */ .my-app .tcw-root:not([data-tcw-theme="light"]) { --tcw-bg: #0a0a0f; --tcw-accent: #7c5cff; --tcw-radius: 0px; --tcw-radius-lg: 0px; } ``` | Variable | Default (dark) | Purpose | |---|---|---| | `--tcw-bg` | `#080b10` | Root background | | `--tcw-bg-surface` | `#0c1016` | Panel / toolbar surface | | `--tcw-bg-elevated` | `#141922` | Popovers, dropdowns, modals | | `--tcw-bg-overlay` | `rgba(20,25,34,.5)` | Backdrop behind overlays | | `--tcw-border` | `#1f2630` | Default border | | `--tcw-border-strong` | `#2a323e` | Emphasized border (focus rings, dividers) | | `--tcw-text` | `#e7e9ee` | Primary text | | `--tcw-text-dim` | `#aab1bd` | Secondary text | | `--tcw-text-muted` | `#758091` | Tertiary / placeholder text | | `--tcw-accent` | `#f2a93b` | Primary accent (active tab, focus, links) | | `--tcw-accent-ink` | `#1a1204` | Text and icons on an accent fill | | `--tcw-accent-hover` | `#f5b95c` | Accent hover state | | `--tcw-accent-soft` | `rgba(242,169,59,.14)` | Accent tint (selected row background) | | `--tcw-accent-glow` | `rgba(242,169,59,.22)` | Accent glow (focus halo) | | `--tcw-accent-line` | `rgba(242,169,59,.55)` | Accent border/underline | | `--tcw-red` / `--tcw-red-soft` | `#e8505b` / tint | Down/sell/negative | | `--tcw-green` / `--tcw-green-soft` | `#1fa874` / tint | Up/buy/positive | | `--tcw-amber` | `#ff9f43` | Warning | | `--tcw-hover-bg` | `rgba(255,255,255,.05)` | Row/button hover background | | `--tcw-active-bg` | `rgba(255,255,255,.08)` | Row/button pressed background | | `--tcw-divider` | `rgba(255,255,255,.06)` | Hairline dividers | | `--tcw-ease` / `--tcw-ease-out` | cubic-bezier | Transition easing | | `--tcw-dur-fast` / `-normal` / `-slow` | `120ms` / `180ms` / `260ms` | Transition durations | | `--tcw-radius-xs` / `-sm` / `--tcw-radius` / `-lg` / `-xl` | `3px` / `5px` / `7px` / `11px` / `16px` | The corner scale — set to `0` for a square look | | `--tcw-control-radius` / `--tcw-input-radius` / `--tcw-menu-radius` / `--tcw-dialog-radius` / `--tcw-panel-radius` / `--tcw-tooltip-radius` / `--tcw-tag-radius` / `--tcw-toast-radius` | from the scale | Each kind of part's corners | | `--tcw-toolbar-h` / `--tcw-control-h` / `--tcw-control-h-sm` / `--tcw-icon` / `--tcw-sidebar-w` / `--tcw-menu-item-h` | `46px` / `30px` / `24px` / `18px` / `48px` / `30px` | Sizes | | `--tcw-font` / `--tcw-font-size` / `--tcw-weight` / `--tcw-weight-strong` | `'Manrope', 'Inter', …` / `13px` / `500` / `600` | Type | | `--tcw-label-case` / `--tcw-label-tracking` | `none` / `0em` | Small labels (section titles) | | `--tcw-border-w` / `--tcw-sep-w` | `1px` / `0px` | Border width; rules between toolbar groups | | `--tcw-menu-shadow` / `--tcw-dialog-shadow` / `--tcw-tooltip-shadow` | the elevation shadows | Shadows of menus, dialogs, tooltips | | `--tcw-blur` / `--tcw-surface-opacity` | `0px` / `100%` | Frosted menus | | `--tcw-shadow-sm` / `-md` / `-lg` / `-xl` | box-shadow values | Elevation | | `--tcw-ring` | `0 0 0 2px rgba(242,169,59,.45)` | Focus ring | | `--tcw-font-mono` | `'JetBrains Mono', …` | Monospace font stack (price ladder, code) | Light theme (`[data-tcw-theme="light"]`) redefines the color group (`--tcw-bg*`, `--tcw-border*`, `--tcw-text*`, `--tcw-accent*`, `--tcw-hover-bg`, `--tcw-active-bg`, `--tcw-divider`, `--tcw-shadow*`) with its own defaults — override both selectors if you support both themes. With the `ui` option set, the widget writes its look's variables on the element, so they win over your CSS; without it they stay yours to set. ## Features ### Chart Types | Type | Description | |---|---| | Candlestick | Standard OHLC candles | | Hollow Candle | Open/close determines fill | | Bar (OHLC) | Classic open-high-low-close bars | | Line | Close price line | | Area | Filled area below close | | Baseline | Two-tone area split at a reference price | | Heikin-Ashi | Smoothed candles for trend identification | | Renko | Fixed-size bricks that ignore time | | Kagi | Reversal-based line chart | | Point & Figure | X/O columns for supply/demand analysis | | Line Break | Three-line break charts | | Range Bars | Fixed price-range bars — each bar's high − low equals a configured range | | Volume Candles | Candlesticks with width proportional to volume | | Equivolume | Full-range boxes with width proportional to volume share (Richard Arms style) | | HLC Area | High-low-close area band with close line | | Step Line | Staircase/step pattern from close prices | | Line with Markers | Close line with circular markers at each data point | ### Multi-Chart Grid Display multiple synchronized charts side-by-side with linked crosshairs and time axis: ```typescript import { ChartGrid, BinanceAdapter } from '@tradecanvas/chart' const grid = new ChartGrid(document.getElementById('grid')!, { layout: '2x2', syncCrosshair: true, syncTimeAxis: true, }) // An adapter keeps one stream: give each chart its own grid.connectAll(() => new BinanceAdapter(), ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT'], '5m') ``` With the full widget on each chart, a bar to pick the arrangement and sync, and the whole grid saved as a named layout: ```typescript import { ChartWidgetGrid } from '@tradecanvas/chart/widget' const workspace = new ChartWidgetGrid(document.getElementById('grid')!, { layout: '1x2', adapter: () => new BinanceAdapter(), cells: [{ symbol: 'BTCUSDT' }, { symbol: 'ETHUSDT', timeframe: '1h' }], sync: { crosshair: true, interval: false, symbol: false, time: false, drawings: false }, }) workspace.setSync({ time: true }) ``` Supported layouts: `'1x1'`, `'1x2'`, `'2x1'`, `'2x2'`, `'1x3'`, `'3x1'`, `'2x3'`, `'3x2'`. ### Command Palette Press `Ctrl+K` (or `Cmd+K`) inside ChartWidget to open a searchable command palette. Quickly find and toggle indicators, change chart types, activate drawing tools, switch timeframes, or trigger actions (screenshot, theme toggle, settings). ### Finance Charts | Chart | Description | |---|---| | SparklineChart | Tiny inline line/area chart from a number array — for dashboards and KPI cards | | DepthChart | Bid/ask order book visualization with cumulative volume areas | | EquityCurveChart | Portfolio equity line with drawdown shading and benchmark comparison | | HeatmapChart | Colored cell grid with treemap layout — for sector/market performance | | WaterfallChart | Running cumulative bars — P&L attribution, revenue bridge, cash flow | | GaugeChart | Speedometer-style gauge — KPIs, risk scores, Fear & Greed index | ```typescript import { SparklineChart, DepthChart, EquityCurveChart, HeatmapChart, WaterfallChart, GaugeChart, } from '@tradecanvas/chart' // Sparkline in a 120x48 container new SparklineChart(el, { data: [100, 102, 98, 105, 103], mode: 'area', color: '#1fa874' }) // Equity curve with drawdown new EquityCurveChart(el, { data: equityPoints, drawdown: true, benchmark: spyData }) // Order book depth new DepthChart(el, { data: { bids, asks }, crosshair: true }) // Market heatmap (treemap weighted by market cap) new HeatmapChart(el, { data: cells, weighted: true }) // P&L waterfall new WaterfallChart(el, { data: [ { label: 'Start', value: 10000, type: 'total' }, { label: 'Gain', value: 1850 }, { label: 'Loss', value: -620 }, { label: 'End', value: 11230, type: 'total' }, ], }) // Fear & Greed gauge: zones light up to the value, the label shows the current zone const gauge = new GaugeChart(el, { value: 72, label: 'Fear & Greed', zones: [ { from: 0, to: 25, color: '#e8505b', label: 'Extreme fear' }, { from: 25, to: 45, color: '#f2a93b', label: 'Fear' }, { from: 45, to: 55, color: '#8a93a3', label: 'Neutral' }, { from: 55, to: 75, color: '#62c895', label: 'Greed' }, { from: 75, to: 100, color: '#1fa874', label: 'Extreme greed' }, ], // pointer: 'needle', // classic needle instead of the ring marker }) gauge.setValue(85) // animates smoothly ``` ### Indicators (built-in) 95 indicators — moving averages (SMA, EMA, WMA, Hull, DEMA, TEMA, ALMA, KAMA, LSMA, McGinley, SMMA, MA Cross, MTF MA), bands and channels (Bollinger, Keltner, Donchian, Envelope, Linear Regression), trend and stops (Ichimoku, Supertrend, Parabolic SAR, Chandelier, Chande Kroll Stop, Alligator, ZigZag, Fractals, Pivot Points), VWAPs and Volume Profile on the price pane; RSI, MACD, Stochastic, ATR, ADX, CCI, OBV, MFI, Bollinger %B and BandWidth, Historical Volatility, Ulcer Index and 40 more oscillators, volume and volatility indicators in panes. The [indicator catalog](https://bonguynvan.github.io/tradecanvas/docs/indicators) lists every id with its inputs, lines and levels. ```typescript import { indicatorSource } from '@tradecanvas/chart' const rsi = chart.addIndicator('rsi', { period: 14 })! chart.addIndicator('ema', { period: 21, source: 'hlc3' }) // another price chart.addIndicator('sma', { period: 9, source: indicatorSource(rsi, 'value') }) // RSI's own average, in RSI's pane chart.setIndicatorLevels(rsi, [20, 50, 80]) ``` - **Sources**: close, open, high, low, hl2, hlc3, ohlc4, hlcc4, or another indicator's line. - **Panes**: one value scale per pane for lines, levels, axis and crosshair; move an indicator into another pane, a new one or back to the price pane; fold, maximise and reorder panes (`moveIndicatorToPane`, `setPaneCollapsed`, `setMaximizedPane`, `movePane`). - **Undo and templates**: Ctrl/Cmd+Z undoes indicator changes, in the same history as drawings; ChartWidget saves the indicators as named templates (`getIndicatorSetup` / `applyIndicatorSetup`). - **Levels**: editable per instance (RSI 30/70, CCI ±100 …), kept in saved layouts. - **Value tags**: each line's latest value on its axis, in its colour. - **Custom indicators** declare their lines (`plots`), scale, levels and inputs; the chart draws and labels them. Invalid parameters (NaN, Infinity, non-numeric strings, missing keys) fall back to the defaults instead of reaching the calculations. ### Drawing Tools Trendline, Horizontal Line, Vertical Line, Ray, Extended Line, Parallel Channel, Fibonacci Retracement, Fibonacci Extension, **Fibonacci Time Zones**, Rectangle, Ellipse, Triangle, Arrow, Pitchfork, Gann Fan, Gann Box, Elliott Wave, Regression Channel, Date Range, Price Range, Measure, Anchored VWAP, Volume Profile Range, Text Annotation All drawing tools support: - Click-to-place with magnet snapping to OHLC values - Undo / redo (Ctrl+Z / Ctrl+Y) - Serialization for save/load - Custom styles (color, width, dash pattern) ### Trading Overlay Render open positions and pending orders directly on the chart, like MT4/MT5. ```typescript import type { TradingPosition, TradingOrder } from '@tradecanvas/chart' chart.setPositions([{ id: 'pos-1', side: 'buy', entryPrice: 3500, quantity: 1.5, closedQuantity: 0.5, // partial close — visualized as a left-edge dim band stopLoss: 3400, takeProfit: 3700, }]) chart.setOrders([{ id: 'order-1', side: 'sell', type: 'limit', price: 3800, quantity: 0.5, label: 'TP', draggable: true, }]) // Customize the position zone color via P&L thresholds chart.setTradingConfig({ pnlThresholds: [ { pnl: -Infinity, color: '#b91c1c' }, { pnl: 0, color: '#94a3b8' }, { pnl: 50, color: '#16a34a' }, { pnl: 200, color: '#15803d' }, ], // Custom label template — tokens: {side} {qty} {openQty} {closedQty} {entry} {price} {pnl} {pnlPct} {pnlSign} positionLabel: '{side} {openQty}/{qty} @ {entry} | {pnlSign}{pnl} ({pnlPct})', }) // Listen for user drag-to-modify chart.on('positionModify', (e) => console.log('SL/TP moved:', e.payload)) chart.on('orderModify', (e) => console.log('Order moved:', e.payload)) // The × and ⇅ buttons on the lines raise these; so can your own UI chart.cancelOrderIntent('order-1') chart.reversePositionIntent('pos-1') chart.on('executionFill', (e) => console.log(e.payload.reason, e.payload.pnl)) ``` ### Signal Markers Visualize buy/sell signals from bots, indicators, or manual analysis. ```typescript chart.addSignalMarker({ time: 1715692800000, price: 62500, direction: 'long', confidence: 0.85, source: 'ema-crossover', label: 'EMA Cross', }) // Color-code by source chart.setSignalMarkerStyle({ sourceColors: { 'ema-crossover': '#4c8dff', 'rsi-divergence': '#f2a93b', 'whale-flow': '#9C27B0', }, }) ``` ### Trade Zones Render entry→exit rectangles with P&L coloring for executed trades. ```typescript const zoneId = chart.addTradeZone({ entryTime: 1715692800000, entryPrice: 62500, exitTime: 1715700000000, exitPrice: 63200, direction: 'long', pnl: 140, pnlPercent: 1.12, }) // Update a live trade when it closes chart.updateTradeZone(zoneId, { exitTime: Date.now(), exitPrice: 63500, pnl: 200, }) ``` ### Real-Time Streaming ```typescript // Built-in Binance adapter (free, no API key) chart.connect({ adapter: new BinanceAdapter(), symbol: 'ETHUSDT', timeframe: '1m', historyLimit: 500, }) // Or manual data feed chart.setData(historicalBars) chart.appendBar(newBar) chart.updateLastBar(updatedBar) chart.setCurrentPrice(3500.42) ``` **Built-in adapters** (all free, no API key): `BinanceAdapter`, `CoinbaseAdapter`, `BybitAdapter`, `KrakenAdapter`, plus `MockAdapter` for offline/testing. ```typescript import { BybitAdapter, KrakenAdapter, CoinbaseAdapter } from '@tradecanvas/chart' 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' }) ``` **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: ```typescript 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 }), }) ``` ### Live Execution Connect an `ExecutionAdapter` to turn the display-only trading overlay into a real trading surface. The chart routes its order/position intents into the adapter, and renders the authoritative `orders` / `positions` the adapter emits back — the **adapter is the single source of truth**. With no adapter connected, those intents stay plain events (backward-compatible). ```typescript import { PaperExecutionAdapter } from '@tradecanvas/chart' chart.connectExecution(new PaperExecutionAdapter({ markPrice: 64000 })) // Drag-to-create an order, then confirm: chart.startOrderDraft('buy') // draggable line at the latest close chart.confirmOrderDraft() // emits orderPlace → adapter fills → chart renders the position // chart.cancelOrderDraft() // One channel for failures (adapter-reported or a failed command): chart.on('executionError', (e) => toast(e.payload.message)) ``` Implement `ExecutionAdapter` (it mirrors `DataAdapter`) to wire a real broker / OMS: `placeOrder`, `modifyOrder`, `cancelOrder`, `modifyPosition`, `closePosition`, plus `orders` / `positions` / `fill` / `error` events. `PaperExecutionAdapter` is a virtual-fill sandbox for demos and tests. The order type of a drag-to-create order (limit vs stop) is inferred from where you drop the line relative to the current price. ### Plugins — extend the chart Register custom **indicators**, **drawing tools**, **chart types**, and **overlays** — globally (every chart created afterward inherits) or per-chart. ```typescript import { Chart, registerPlugin, IndicatorBase } from '@tradecanvas/chart' class MyIndicator extends IndicatorBase { /* descriptor, calculate(), render() */ } // 1) Global — available to every chart created afterward: registerPlugin({ kind: 'indicator', plugin: new MyIndicator() }) // 2) Per-chart at construction: const chart = new Chart(el, { plugins: [{ kind: 'overlay', plugin: myHeatmap }] }) // 3) Imperative on an instance: chart.plugins.register({ kind: 'chartType', plugin: myCustomCandles }) chart.setChartType('my-custom-candles') // custom chart types render via the plugin ``` | Plugin kind | Contract | |---|---| | `indicator` | `IndicatorPlugin` — `calculate()` + `render()` | | `drawing` | `DrawingPlugin` — `render()` + `hitTest()` | | `chartType` | `ChartTypePlugin` — `createRenderer()` + optional `transform()` | | `overlay` | `OverlayPlugin` — `render(ctx, { viewport, data, theme })` on the `main` / `overlay` / `ui` layer | ### Indicators outside the chart `IndicatorWorkerHost` computes an indicator from bars with the same messages a Web Worker would use. The worker script is not part of the published packages yet; pass `null` and register the plugins to compute in place (SSR, tests, scripts): ```typescript import { IndicatorWorkerHost, RSIIndicator } from '@tradecanvas/core' const host = new IndicatorWorkerHost(null) host.registerFallbackPlugin(new RSIIndicator()) const output = await host.calculate('rsi', { id: 'rsi', instanceId: 'rsi-1', params: { period: 14 } }, bars) ``` ### Save / Load ```typescript const json = chart.saveState() localStorage.setItem('my-chart', json!) chart.loadState(localStorage.getItem('my-chart')!) // Download / upload files chart.downloadState('my-chart.json') await chart.loadStateFromFile() // Or keep a layout saved as it changes (debounced) chart.setAutoSave('my-chart', 1500) ``` A saved layout holds the chart type, theme, drawings, indicators (inputs, pane, colours, visibility) and alerts, including alerts on indicator lines. ### Themes ```typescript import { DARK_THEME, LIGHT_THEME, DARK_TERMINAL, volumeColor } from '@tradecanvas/chart' // Built-in presets: DARK_THEME, LIGHT_THEME, DARK_TERMINAL chart.setTheme(DARK_TERMINAL) // fintech terminal: #0E0E0E bg, #00FF87/#FF3B4D candles, monospace // Or customize any preset chart.setTheme({ ...DARK_THEME, candleUp: '#1fa874', candleDown: '#e8505b', volumeUp: volumeColor('#1fa874'), // volume bars: the candle colour, see-through volumeDown: volumeColor('#e8505b'), background: '#0a0a0f', }) ``` ### Events ```typescript chart.on('crosshairMove', (e) => { /* { point, bar, barIndex, indicatorValues } — also over indicator panes */ }) chart.on('crosshairLeave', () => { /* the pointer left the plot */ }) chart.on('drawingToolChange', (e) => { /* { tool } — null once a drawing is finished or cancelled */ }) chart.on('indicatorUpdate', (e) => { /* { from } — indicator values recomputed from this bar on */ }) chart.on('paneResize', (e) => { /* { instanceId, size } — an indicator pane was resized */ }) chart.on('indicatorChange', (e) => { /* { instanceId, change } — shown/hidden, restyled, levels, inputs or pane changed */ }) chart.on('barClick', (e) => { /* { bar, barIndex, point } */ }) chart.on('visibleRangeChange', (e) => { /* { from, to } — bar indices, not timestamps */ }) chart.on('priceRangeChange', (e) => { /* { min, max } — visible price bounds */ }) chart.on('zoomChange', (e) => { /* { barWidth } — pixels per bar */ }) chart.on('drawingCreate', (e) => { /* ... */ }) chart.on('orderModify', (e) => { /* ... */ }) chart.on('positionModify', (e) => { /* ... */ }) ``` `visibleRangeChange`, `priceRangeChange`, and `zoomChange` fire on every pan, zoom, resize, and data update — but only when that piece of viewport state actually changed. Resolve a `visibleRangeChange` index to time with `chart.getData()[e.payload.from].time`. ### Replay Mode `ReplayController` plays a historical `DataSeries` forward at controlled speed. Decoupled from `Chart` — wire it into any sink (chart for UI playback, or a strategy fn for headless backtests). ```typescript import { ReplayController } from '@tradecanvas/chart' const replay = new ReplayController({ data: historicalBars, speed: 10, // bars per second startIndex: 0, }) // Seed the chart with the prefix before replay starts chart.setData(replay.getPrefix()) // Each emitted bar drives the chart forward 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) ``` ### Chart Interaction Every gesture you'd expect from a desktop trading chart is built in: | Gesture | Result | |---|---| | Drag chart body left/right | Pan through time | | Drag chart body up/down | Pan the price scale (freezes auto-scale; double-click price axis to restore) | | Drag price axis up/down | Compress / expand vertical scale (freezes auto-scale) | | Drag time axis left/right | Zoom time axis | | Double-click price axis | Re-enable auto-scale | | Double-click time axis | Fit all data to viewport | | Wheel | Zoom around cursor | | Drag a pane divider | Resize the indicator pane (`ns-resize` cursor on hover) | | `Shift` + drag | Measure ruler (bars × time × price Δ × %) | | `Alt` + click | Pin OHLC tooltip; live crosshair shows Δ to pinned bar | | Hover | Price + time pill labels follow on both axes | | `Esc` | Unpin tooltip / cancel drawing | | `?` | Show keyboard-shortcut sheet *(widget)* | | `Ctrl/⌘ + K` | Command palette *(widget)* | | `Ctrl/⌘ + P` | Symbol search *(widget)* | | `Ctrl/⌘ + Z` / `Shift + Z` | Undo / redo drawings | ### Data import — drag-and-drop or programmatic ```typescript import { parseOHLCV } from '@tradecanvas/chart' const { data, rowCount, skipped } = parseOHLCV(csvText) chart.setData(data) ``` Drop a CSV or JSON file onto the widget and it loads instantly. Auto-detects delimiter (`,` / `;` / tab / `|`), header vs. headerless, ISO 8601 timestamps, and array-of-arrays vs. array-of-objects JSON. ### Backtesting (`@tradecanvas/analytics`) Bar-by-bar strategy backtester with virtual fills, commission/slippage models, and a full risk-metrics report. ```typescript import { Backtester, PercentCommission, PercentSlippage } from '@tradecanvas/analytics' const bt = new Backtester({ initialCash: 10_000, commission: new PercentCommission(0.0005), slippage: new PercentSlippage(0.0003), }) const result = bt.run(historicalBars, (ctx) => { // Strategy fn runs at close of each bar; orders fill on the NEXT bar. if (!ctx.position && smaFast > smaSlow) { ctx.placeOrder({ side: 'long', type: 'market', quantity: 1 }) } else if (ctx.position && smaFast < smaSlow) { ctx.close() } }) console.log(result.metrics.sharpe) // 1.42 console.log(result.metrics.maxDrawdownPct) // 0.087 console.log(result.equityCurve) // → feed into the chart via EquityCurveRenderer ``` Returns: `fills`, closed `trades`, `equityCurve`, `metrics` (Sharpe, Sortino, Calmar, CAGR, max drawdown, win rate, profit factor, expectancy). See the [live backtest demo](https://bonguynvan.github.io/tradecanvas/docs/analytics/). #### Strategy library Four drop-in reference strategies — each returns a `StrategyFn` ready to feed `Backtester.run()`: ```typescript import { Backtester, smaCrossStrategy, rsiReversionStrategy, donchianBreakoutStrategy, bollingerReversionStrategy, } from '@tradecanvas/analytics' const bt = new Backtester({ initialCash: 10_000 }) bt.run(bars, smaCrossStrategy({ fastPeriod: 10, slowPeriod: 30 })) bt.run(bars, donchianBreakoutStrategy({ entryPeriod: 20, exitPeriod: 10 })) ``` #### Monte Carlo path-dependence Shuffle realised trade order N times to expose whether a strategy depends on lucky sequencing. Tight P5/P95 band = robust edge; wide band = path-dependent. ```typescript import { runMonteCarlo } from '@tradecanvas/analytics' const result = bt.run(bars, smaCrossStrategy()) const mc = runMonteCarlo(10_000, result.trades, { simulations: 1000, seed: 42 }) mc.equityBands // [{ step, p5, p25, p50, p75, p95 }, …] mc.finalEquityPercentiles // { p5, p25, p50, p75, p95 } mc.probabilityProfitable // 0..1 mc.worstMaxDrawdownPct ``` ## Comparison | Feature | @tradecanvas/chart | lightweight-charts | chart.js | Highcharts Stock | |---|---|---|---|---| | Chart types | 18 + 6 finance | 4 | 8 (non-financial) | 10+ | | Finance charts | Sparkline, Depth, Equity, Heatmap, Waterfall, Gauge | None | None | Some | | Built-in indicators | 95 | 0 | 0 | ~30 | | Drawing tools | 69 | 0 | 0 | Some | | Trading overlay | Full (pos + orders + drag) | None | None | None | | Real-time streaming | Built-in (Binance) | Manual | Manual | Built-in | | Save/load state | Yes | No | No | Yes | | Replay mode | Yes (`ReplayController`) | No | No | No | | Backtester | Yes (`@tradecanvas/analytics`) | No | No | No | | Multi-chart grid | Yes (`ChartGrid`) | No | No | Yes | | Bundle (gzip) | ~100 KB core | ~45 KB | ~70 KB | ~200 KB | | Dependencies | 0 | 1 | 0 | 0 | | Widget (complete UI) | Yes (`ChartWidget`) | No | No | No | | License | MIT | Apache 2.0 | MIT | Commercial | ## API Overview ### `new Chart(container, options)` ```typescript const chart = new Chart(element, { chartType: 'candlestick', theme: DARK_THEME, autoScale: true, rightMargin: 5, numberLocale: 'en-US', // or 'de-DE', 'vi-VN', etc. — BCP 47 locale crosshair: { mode: 'magnet' }, features: { drawings: true, indicators: true, trading: true, volume: true }, }) // Change locale at runtime chart.setNumberLocale('de-DE') // 65.234,00 ``` ### Key Methods | Method | Description | |---|---| | `setData(bars)` | Load historical OHLCV data | | `appendBar(bar)` | Append a new candle | | `appendBars(bars)` | Bulk append (reconnect catch-up) | | `updateLastBar(bar)` | Update the in-progress candle | | `setCurrentPrice(price, pulseColor?)` | Show a live price line | | `connect(config)` | Connect to a real-time data source | | `setTimeframe(tf)` | Switch timeframe on active stream | | `setChartType(type)` | Switch chart type | | `setTheme(theme)` | Apply a theme (DARK_THEME, LIGHT_THEME, DARK_TERMINAL) | | `setNumberLocale(locale)` | Set number format locale (en-US, de-DE, vi-VN) | | `setStatusText(text)` | Show status in legend area ("LIVE · 8ms") | | `addIndicator(id, params?)` | Add a technical indicator | | `removeIndicator(instanceId)` | Remove an indicator | | `setDrawingTool(tool)` | Activate a drawing tool | | `setPositions(positions)` | Render trading positions | | `setOrders(orders)` | Render pending orders | | `setVolumeProfileVisible(v)` | Toggle the horizontal volume-profile overlay | | `setVolumeProfileConfig({ buckets, widthRatio, opacity, highlightPoC })` | Tune the volume profile | | `setAutoScale(v)` / `setLogScale(v)` | Lock or change price-scale mode | | `setInvertScale(v)` | Turn the price scale upside down | | `fitContent()` / `scrollToEnd()` | Fit all data / jump to live edge | | `setVisibleRangePreset(p)` | Show `1D`, `5D`, `1M`, `3M`, `6M`, `YTD`, `1Y`, `5Y` or `All` | | `goToTime(time)` | Centre the bar at a time | | `setCrosshairTime(time)` | Mirror another chart's crosshair (vertical line only) | | `copyDrawings()` / `pasteDrawings()` | Copy the selection, paste into this or another chart | | `setStayInDrawingMode(v)` | Keep the drawing tool after each drawing | | `saveState(key?)` | Serialize chart state | | `loadState(json)` | Restore chart state | | `screenshot()` | Download chart as image | | `setRenderer(mode)` | Draw with `'canvas'` (default), `'webgl'` or `'auto'`; resolves to what draws now | | `getRenderer()` | `'canvas'` or `'webgl'` | | `on(event, handler)` | Subscribe to events | | `destroy()` | Clean up all resources | ### Data Format ```typescript interface OHLCBar { time: number // Unix time in ms or seconds (up to 1e12 is read as seconds); ascending open: number high: number low: number close: number volume: number } ``` ## Examples | Example | Description | |---|---| | [Live demo](https://bonguynvan.github.io/tradecanvas/) | Feature Lab: drawing tools, indicators, trading, ranges, paged history, replay, 16 languages with sub-cent prices, watchlists with live quotes, 200k bars, slow-network switching — each on a live chart. The site and docs are also in Vietnamese, Chinese, Japanese, Korean and Spanish | | [StackBlitz sandboxes](https://bonguynvan.github.io/tradecanvas/examples/) | One-click, forkable: vanilla `Chart`, `ChartWidget`, React / Vue / Svelte wrappers, finance charts | | [`@tradecanvas/react`](https://github.com/bonguynvan/tradecanvas/tree/main/packages/react/) · [`/vue`](https://github.com/bonguynvan/tradecanvas/tree/main/packages/vue/) · [`/svelte`](https://github.com/bonguynvan/tradecanvas/tree/main/packages/svelte/) | Framework components — reactive props, typed, zero boilerplate | ## AI coding tools - [`llms.txt`](https://bonguynvan.github.io/tradecanvas/llms.txt) and [`llms-full.txt`](https://bonguynvan.github.io/tradecanvas/llms-full.txt) give assistants the docs in one place. - An agent skill, [`skills/tradecanvas`](https://github.com/bonguynvan/tradecanvas/blob/main/skills/tradecanvas/SKILL.md), teaches coding agents to build with TradeCanvas: the entry points, the rules that avoid most bugs, and worked examples that CI type-checks against the library. Copy the folder into your project's `.claude/skills/` (or your agent's skills folder) to use it. ## Browser Support Chrome 80+, Firefox 80+, Safari 14+, Edge 80+ ## Framework Integration Official wrapper components — reactive props, refs, zero boilerplate. Published at `1.x` alongside the core: ```bash npm install @tradecanvas/react # or @tradecanvas/vue · @tradecanvas/svelte ``` ```tsx import { TradeCanvas } from '@tradecanvas/react' ``` All three share the same prop surface and hand you the underlying `Chart` (for drawings, trading, execution, plugins) via `onReady` / ref / `bind:chart`. See the [frameworks docs](https://bonguynvan.github.io/tradecanvas/docs/frameworks). ### Headless (own the lifecycle) The `Chart` class also takes a DOM element directly — framework-agnostic: **React:** ```tsx import { useEffect, useRef } from 'react' import { Chart, BinanceAdapter } from '@tradecanvas/chart' function TradingChart() { const ref = useRef(null) useEffect(() => { const chart = new Chart(ref.current!, { theme: 'dark', features: { indicators: true, drawings: true }, }) chart.connect({ adapter: new BinanceAdapter(), symbol: 'BTCUSDT', timeframe: '5m', }) return () => chart.destroy() }, []) return
} ``` **Svelte:** ```svelte
``` **Vue:** ```vue ``` ## Performance A two-canvas Canvas2D pipeline: a hover repaints only the thin top canvas, never the scene. Five things keep large data fast: - **LTTB downsampling** — line / area charts automatically downsample the visible range to ~2 points per pixel using Largest-Triangle-Three-Buckets when there are far more bars than pixels. The line stays visually identical while drawing dozens of times fewer points; a no-op at normal zoom. The `lttbDownsample` utility is exported for your own use. - **Indicators zoomed out** — below a pixel per bar, indicator lines, bands and histograms draw one span per pixel column instead of a stroke through thousands of points: much the same look for a fraction of the raster work (about 54 → 21 ms a frame zoomed out on 200,000 bars with four indicators, on integrated graphics). `node scripts/bench-render.mjs` measures it on your machine. - **Visible-range rendering** — every renderer iterates only the bars in view, never the whole series. Hover and pan frame cost stays flat from 500 to 100,000 loaded bars. - **Incremental indicators on live ticks** — a tick only changes the forming bar, so built-in indicators that implement `update()` (SMA, EMA, WMA, VWMA, Bollinger, Envelope, RSI, MACD, ATR, OBV, Stochastic) recompute just that bar instead of the whole history. Others fall back to a full recalculation. Custom plugins can opt in via `IndicatorPlugin.update`. - **Cheap full loads** — a symbol/timeframe switch recomputes every indicator once. Their per-bar `values` lookup is an `IndicatorValueMap` (array-backed while bars arrive in time order, ~3x cheaper to build than a `Map` keyed by timestamps), and `setData` reuses already well-formed bars instead of copying each one. BB + EMA + RSI + MACD (`pnpm bench`, single core): | History | Full recalculation (switch / `setData`) | Incremental `update()` (live tick) | |---|---|---| | 20,000 bars | ~5 ms | ~0.0005 ms | | 100,000 bars | ~27 ms | ~0.001 ms | Downsampling throughput (`pnpm bench`, single core): | Visible points → 1600 | Time / frame | Throughput | |---|---|---| | 10,000 | ~0.025 ms | 39,600 / s | | 100,000 | ~0.32 ms | 3,100 / s | | 1,000,000 | ~2.6 ms | 380 / s | A 100k-bar line chart downsamples in ~0.3 ms — well inside a 16.6 ms frame budget — then draws ~62× fewer points (100k → 1600). ### WebGL renderer (preview) `renderer: 'webgl'` draws the plot and indicator panes with WebGL 2, on a canvas under the 2D scene: the grid, sessions, candles and volume directly, and the Canvas 2D drawing of indicators, compare lines and most chart types recorded as GPU strokes, fills and rectangles, antialiased at their edges as Canvas 2D is. Text, drawings, orders, axes and the crosshair stay on Canvas 2D, as does anything the GPU wouldn't draw the same (left to Canvas 2D in order, so the stacking doesn't change); custom indicator plugins come along without changes. Candles match Canvas 2D to within 2/255; lines and fills differ only on a few antialiased edge pixels. The WebGL code is a chunk of its own (about 17 KB gzipped), loaded on first use; where WebGL 2 is missing, or its context is lost, the chart carries on with Canvas 2D. ```typescript const chart = new Chart(el, { renderer: 'webgl' }) // or 'auto': WebGL on a hardware GPU only chart.on('rendererChange', (e) => console.log(e.payload)) // { renderer: 'webgl' }, or { renderer: 'canvas', reason: 'unsupported' | 'contextLost' } await chart.setRenderer('canvas') // resolves to what draws now ``` Frame time while panning, on integrated graphics (Intel UHD; 16.7 ms is 60 fps): | Chart | Pixel ratio | Canvas 2D | WebGL | |---|---|---|---| | 1600×900, 500 candles + 4 indicators | 2 | 27.4 ms | 19.6 ms | | 1600×900, zoomed out on 200,000 bars + 4 indicators | 2 | 34.5 ms | 20.2 ms | | Six charts, 500 candles and two indicators each | 2 | 23.5 ms | 17.2 ms | | 2560×1400, 2,000 candles + 4 indicators | 1 | 41.6 ms | 17.7 ms | | 2560×1400, 2,000 candles + 4 indicators | 1.5 | 70.8 ms | 17.6 ms | | 2560×1400, 2,000 candles + 4 indicators | 2 | 114.5 ms | 29.1 ms | | 2560×1400, 2,000 candles | 2 | 33.1 ms | 20.9 ms | Most WebGL frames above land on 16.7 ms; the averages carry a few longer ones. At a pixel ratio of 2 on a 2560×1400 chart, the browser's compositing of the full-size layers alone takes about 23 ms on this GPU. `node scripts/bench-render.mjs --renderer=webgl` runs these numbers on your own machine. ## Architecture Two stacked canvases — a hover repaints only the thin top one: ``` Top canvas (crosshair + axis pills, legend, countdown, measure) z=1 Scene canvas (grid, candles, indicators, drawings, orders, axes) z=0 ``` ## Related projects - **[bo-grid](https://github.com/bonguynvan/bo-grid)** — tiny, fast **Svelte 5** data grid for fintech UIs: canvas sparklines, batched realtime cell updates, virtual scrolling, grouping / pivot / tree data, and Excel export, with a core that gzips to ~32 KB. The table half of the same toolkit — pair it with TradeCanvas for a full trading desk. **[Live demo](https://bonguynvan.github.io/bo-grid/)** ## Contributing Bug reports, ideas and pull requests are welcome. [CONTRIBUTING.md](https://github.com/bonguynvan/tradecanvas/blob/main/CONTRIBUTING.md) covers the setup (`pnpm install && pnpm build && pnpm test`), how the repo is laid out and what a pull request needs. Found a security issue? Please follow [SECURITY.md](https://github.com/bonguynvan/tradecanvas/blob/main/SECURITY.md) instead of opening an issue. If TradeCanvas saves you time, a star on GitHub helps other developers find it. ## License [MIT](https://github.com/bonguynvan/tradecanvas/blob/main/LICENSE)