@tradecanvas/chart 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +858 -824
  3. package/dist/Chart.d.ts +142 -2
  4. package/dist/Chart.d.ts.map +1 -1
  5. package/dist/DragDropImporter-C5kgfbET.cjs +2 -0
  6. package/dist/DragDropImporter-C5kgfbET.cjs.map +1 -0
  7. package/dist/{DragDropImporter-BcTzFeRP.js → DragDropImporter-Dd9Mud5J.js} +423 -159
  8. package/dist/DragDropImporter-Dd9Mud5J.js.map +1 -0
  9. package/dist/PaneManager.d.ts.map +1 -1
  10. package/dist/charts/ChartTypeStrategy.d.ts +6 -0
  11. package/dist/charts/ChartTypeStrategy.d.ts.map +1 -1
  12. package/dist/grid/ChartGrid.d.ts +1 -0
  13. package/dist/grid/ChartGrid.d.ts.map +1 -1
  14. package/dist/index.cjs +1 -1
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +74 -89
  19. package/dist/index.js.map +1 -1
  20. package/dist/interaction/verticalPanGate.d.ts +22 -0
  21. package/dist/interaction/verticalPanGate.d.ts.map +1 -0
  22. package/dist/widget/ChartWidget.d.ts +71 -7
  23. package/dist/widget/ChartWidget.d.ts.map +1 -1
  24. package/dist/widget/WidgetCommandPalette.d.ts +3 -1
  25. package/dist/widget/WidgetCommandPalette.d.ts.map +1 -1
  26. package/dist/widget/WidgetDrawingSidebar.d.ts +12 -0
  27. package/dist/widget/WidgetDrawingSidebar.d.ts.map +1 -1
  28. package/dist/widget/WidgetGoToDate.d.ts +45 -0
  29. package/dist/widget/WidgetGoToDate.d.ts.map +1 -0
  30. package/dist/widget/WidgetHotkeySheet.d.ts +4 -2
  31. package/dist/widget/WidgetHotkeySheet.d.ts.map +1 -1
  32. package/dist/widget/WidgetIndicatorLegend.d.ts +68 -0
  33. package/dist/widget/WidgetIndicatorLegend.d.ts.map +1 -0
  34. package/dist/widget/WidgetLoadingOverlay.d.ts +3 -1
  35. package/dist/widget/WidgetLoadingOverlay.d.ts.map +1 -1
  36. package/dist/widget/WidgetObjectTree.d.ts +1 -1
  37. package/dist/widget/WidgetReplayBar.d.ts +37 -7
  38. package/dist/widget/WidgetReplayBar.d.ts.map +1 -1
  39. package/dist/widget/WidgetSettings.d.ts +13 -1
  40. package/dist/widget/WidgetSettings.d.ts.map +1 -1
  41. package/dist/widget/WidgetStatusBar.d.ts +16 -1
  42. package/dist/widget/WidgetStatusBar.d.ts.map +1 -1
  43. package/dist/widget/WidgetSymbolSearch.d.ts +3 -1
  44. package/dist/widget/WidgetSymbolSearch.d.ts.map +1 -1
  45. package/dist/widget/WidgetToolbar.d.ts +12 -3
  46. package/dist/widget/WidgetToolbar.d.ts.map +1 -1
  47. package/dist/widget/WidgetTooltip.d.ts +43 -0
  48. package/dist/widget/WidgetTooltip.d.ts.map +1 -0
  49. package/dist/widget/escapeHtml.d.ts +3 -0
  50. package/dist/widget/escapeHtml.d.ts.map +1 -0
  51. package/dist/widget/i18n.d.ts +26 -0
  52. package/dist/widget/i18n.d.ts.map +1 -1
  53. package/dist/widget/iconSet.d.ts +26 -0
  54. package/dist/widget/iconSet.d.ts.map +1 -0
  55. package/dist/widget/icons.d.ts +5 -0
  56. package/dist/widget/icons.d.ts.map +1 -1
  57. package/dist/widget/index.cjs +3035 -2465
  58. package/dist/widget/index.cjs.map +1 -1
  59. package/dist/widget/index.d.ts +3 -0
  60. package/dist/widget/index.d.ts.map +1 -1
  61. package/dist/widget/index.js +1636 -606
  62. package/dist/widget/index.js.map +1 -1
  63. package/dist/widget/legendValues.d.ts +14 -0
  64. package/dist/widget/legendValues.d.ts.map +1 -0
  65. package/dist/widget/types.d.ts +34 -8
  66. package/dist/widget/types.d.ts.map +1 -1
  67. package/dist/widget/widgetConfig.d.ts +1 -5
  68. package/dist/widget/widgetConfig.d.ts.map +1 -1
  69. package/dist/widget/widgetTimeframes.d.ts +20 -0
  70. package/dist/widget/widgetTimeframes.d.ts.map +1 -0
  71. package/package.json +3 -3
  72. package/dist/DragDropImporter-BcTzFeRP.js.map +0 -1
  73. package/dist/DragDropImporter-DZBrretS.cjs +0 -2
  74. package/dist/DragDropImporter-DZBrretS.cjs.map +0 -1
package/README.md CHANGED
@@ -1,824 +1,858 @@
1
- # @tradecanvas/chart
2
-
3
- High-performance canvas trading chart with built-in indicators, drawing tools, and real-time streaming. Zero external dependencies.
4
-
5
- **[Live Demo](https://bonguynvan.github.io/tradecanvas/)** | **[GitHub](https://github.com/bonguynvan/tradecanvas)** | **[npm](https://www.npmjs.com/package/@tradecanvas/chart)**
6
-
7
- ## Why TradeCanvas?
8
-
9
- Most chart libraries make you choose: pretty charts with no trading features, or trading features with an ugly API. TradeCanvas gives you both.
10
-
11
- - **70 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. No separate calculation library needed.
12
- - **40 drawing tools** — Trendlines (info line, trend angle, cross line), Fibonacci (retracement, extension, channel, time zones, speed resistance fan), horizontal/vertical lines, channels, pitchforks (Andrews, Schiff, modified Schiff), Gann fans / boxes, cyclic lines, harmonic patterns (XABCD, ABCD, head and shoulders, Elliott waves), date & price range, Long/Short Position, Volume Profile range. With undo/redo and full serialization.
13
- - **17 chart types** — Candlestick, line, area, bar, hollow candle, baseline, Heikin-Ashi, Renko, Kagi, Line Break, Point & Figure, Range Bars, Volume Candles, **Equivolume**, HLC Area, Step Line, Line+Markers.
14
- - **TradingView-grade interaction** *(new in 0.9)* — drag the price/time axes to scale, double-click to auto-fit, `Shift+drag` to measure (bars × price Δ × %), `Alt+click` to pin a comparison tooltip, axis-following price/time pill labels under the cursor, bar-hover highlight.
15
- - **Trading overlay** — Render open positions with entry line, P&L zone, and SL/TP markers. Orders as dashed lines. Drag SL/TP to modify. Cleanly opt-out via `features.trading: false` for non-trading projects.
16
- - **Real-time streaming** — Built-in Binance adapter. Plug in your own data source with the adapter interface.
17
- - **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.**
18
- - **Replay mode** — `ReplayController` drives historical bars forward at controlled speed with start / pause / step / seek / setSpeed. *(new in 0.9)* The widget now ships a floating bottom scrubber UI (play/pause/step/seek + 0.5×–100× speed) on top of it.
19
- - **Volume Profile** *(new in 0.9)* — optional horizontal histogram of traded volume bucketed by price over the visible range, with point-of-control highlighting.
20
- - **Watchlist sidebar** *(new in 0.9)* — opt-in vertical panel listing symbols with last price, % change, mini sparkline. Click a row to switch chart.
21
- - **CSV / JSON drag-and-drop** *(new in 0.9)* — 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.
22
- - **Saved layouts** *(new in 0.9)* — opt-in per-symbol persistence of chart type + indicator stack + drawings + alerts to localStorage. Switch symbol, switch back, your setup is intact.
23
- - **Multi-chart grid** — `ChartGrid` for synchronized 2×2 / 2×3 layouts with linked crosshairs and shared time axis.
24
- - **Signal markers & trade zones** — render bot/algorithm output (directional arrows, entry→exit rectangles) as a first-class chart layer.
25
- - **Hotkey sheet** *(new in 0.9)* — press `?` in the widget to open a categorized keyboard-shortcut reference.
26
- - **Save/load chart state** — Persist drawings, indicators, theme, and chart type to JSON. Restore with one call.
27
- - **Zero dependencies** — The entire library is self-contained. No `d3`, no `chart.js`, no `fancy-canvas`.
28
-
29
- ## Install
30
-
31
- ```bash
32
- npm install @tradecanvas/chart
33
- # or
34
- pnpm add @tradecanvas/chart
35
- # or
36
- yarn add @tradecanvas/chart
37
- ```
38
-
39
- ## Quick Start
40
-
41
- The fastest path is `ChartWidget` — drop-in component with a full TradingView-like UI (toolbar, drawing sidebar, settings dialog, status bar). Zero framework dependency.
42
-
43
- ```typescript
44
- import { ChartWidget } from '@tradecanvas/chart/widget'
45
- import { BinanceAdapter } from '@tradecanvas/chart'
46
-
47
- const widget = new ChartWidget(document.getElementById('chart')!, {
48
- symbol: 'BTCUSDT',
49
- timeframe: '5m',
50
- theme: 'dark',
51
- adapter: new BinanceAdapter(),
52
- trading: true,
53
- })
54
- ```
55
-
56
- That's it. Live data, all 70 indicators, all 40 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.
57
-
58
- ## Headless Chart
59
-
60
- For projects that want to own the surrounding UI (custom toolbar, framework-specific controls), use the lower-level `Chart` class directly:
61
-
62
- ```typescript
63
- import { Chart, BinanceAdapter } from '@tradecanvas/chart'
64
-
65
- const chart = new Chart(document.getElementById('chart')!, {
66
- theme: 'dark',
67
- autoScale: true,
68
- features: {
69
- drawings: true,
70
- indicators: true,
71
- trading: true, // set false to disable orders/positions entirely
72
- tradingContextMenu: true, // set false to keep overlay but drop right-click menu
73
- volume: true,
74
- },
75
- })
76
-
77
- const adapter = new BinanceAdapter()
78
- chart.connect({ adapter, symbol: 'BTCUSDT', timeframe: '5m', historyLimit: 300 })
79
- ```
80
-
81
- ### Widget Options
82
-
83
- | Option | Type | Default | Description |
84
- |---|---|---|---|
85
- | `symbol` | `string` | `'BTCUSDT'` | Initial trading symbol |
86
- | `timeframe` | `TimeFrame` | `'5m'` | Initial timeframe |
87
- | `theme` | `'dark' \| 'light' \| Theme` | `'dark'` | Chart theme |
88
- | `adapter` | `DataAdapter` | — | Data source adapter |
89
- | `toolbar` | `boolean` | `true` | Show top toolbar |
90
- | `drawingTools` | `boolean` | `true` | Show left drawing sidebar |
91
- | `settings` | `boolean` | `true` | Show settings button |
92
- | `trading` | `boolean` | `true` | Enable trading overlay |
93
- | `statusBar` | `boolean` | `true` | Show bottom status bar |
94
- | `symbols` | `string[]` | BTC/ETH/SOL/BNB | Searchable symbol catalog |
95
- | `timeframes` | `TimeFrame[]` | 1m to 1d | Available timeframes |
96
- | `chartTypes` | `ChartType[]` | 11 types | Available chart types |
97
- | `watchlist` | `boolean` | `false` | Right-side watchlist sidebar |
98
- | `dragDropImport` | `boolean` | `true` | Drop CSV / JSON files onto the chart to load data |
99
- | `persistLayouts` | `boolean \| { keyPrefix, debounceMs }` | `false` | Save per-symbol indicators / drawings / chart type to localStorage |
100
- | `onSymbolChange` | `(symbol) => void` | — | Symbol change callback |
101
- | `onTimeframeChange` | `(tf) => void` | — | Timeframe change callback |
102
- | `onReady` | `(chart) => void` | — | Fired when chart is ready |
103
- | `locale` | `string` | `'en'` | UI chrome language — built-in `'en'` / `'vi'`, see **Widget i18n** below |
104
- | `messages` | `Partial<Record<MessageKey, string>>` | — | Override or add individual UI strings on top of `locale` |
105
-
106
- ### Widget i18n
107
-
108
- `locale` and `messages` translate `ChartWidget`'s own chrome — toolbar, watchlist, indicator-picker section headers (Popular/All, overlay/panel tags), status bar, settings panel (titles/tabs/section headers), and hotkey sheet (title/group headers). Set at construction; not currently hot-swappable at runtime.
109
-
110
- ```ts
111
- new ChartWidget(el, {
112
- locale: 'vi', // built-in Vietnamese table
113
- messages: { 'watchlist.title': 'Theo dõi' }, // override/add individual keys — always wins
114
- chartOptions: { numberLocale: 'vi-VN' }, // separate: number/date formatting (see below)
115
- });
116
- ```
117
-
118
- `locale`/`messages` only cover chrome **text**; they're independent of `chartOptions.numberLocale`, which controls number/date **formatting** (price axis, legend, watchlist prices, current-price tag, session-break dates) via `Intl`/`toLocaleString`.
119
-
120
- Not yet covered by `locale` (still English; PRs welcome, or override via `messages`/your own CSS):
121
- - Indicator and drawing-tool **names** (SMA, Bollinger Bands, Trend Line, …) — these come from `widgetConfig.ts`'s data tables, not the message catalog.
122
- - Individual settings rows beyond the tab/section level (e.g. "Up Body", "Grid Lines").
123
- - Individual hotkey-sheet shortcut labels and key-cap text (group titles are translated).
124
- - Alerts panel, symbol search, command palette, data window, depth ladder, bracket bar, replay bar, drawing-style panel, object tree.
125
-
126
- See `packages/library/src/widget/i18n.ts` for the full key list (`MessageKey`) and the English/Vietnamese tables.
127
-
128
- ### Widget vs Headless
129
-
130
- | | `Chart` (headless) | `ChartWidget` |
131
- |---|---|---|
132
- | Import | `@tradecanvas/chart` | `@tradecanvas/chart/widget` |
133
- | UI included | None — build your own | Complete toolbar, sidebar, settings |
134
- | Bundle impact | ~50 KB gzip | ~65 KB gzip (includes UI) |
135
- | Framework | Any (React, Vue, Svelte, vanilla) | Vanilla JS DOM (works everywhere) |
136
- | Customization | Full control | Toggle sections on/off |
137
- | Advanced access | Direct API | `widget.getChart()` for direct API |
138
-
139
- ### Widget Theming
140
-
141
- `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.
142
-
143
- ```css
144
- /* Dark is the default (no attribute needed); light sets data-tcw-theme="light" */
145
- .my-app .tcw-root:not([data-tcw-theme="light"]) {
146
- --tcw-bg: #0a0a0f;
147
- --tcw-accent: #7c5cff;
148
- --tcw-radius: 0px;
149
- --tcw-radius-lg: 0px;
150
- }
151
- ```
152
-
153
- | Variable | Default (dark) | Purpose |
154
- |---|---|---|
155
- | `--tcw-bg` | `#0d0f17` | Root background |
156
- | `--tcw-bg-surface` | `#131722` | Panel / toolbar surface |
157
- | `--tcw-bg-elevated` | `#1a1e2d` | Popovers, dropdowns, modals |
158
- | `--tcw-bg-overlay` | `#20253480` | Backdrop behind overlays |
159
- | `--tcw-border` | `#2a2e39` | Default border |
160
- | `--tcw-border-strong` | `#363a45` | Emphasized border (focus rings, dividers) |
161
- | `--tcw-text` | `#e9ebf0` | Primary text |
162
- | `--tcw-text-dim` | `#b2b5be` | Secondary text |
163
- | `--tcw-text-muted` | `#6b6f7a` | Tertiary / placeholder text |
164
- | `--tcw-accent` | `#4f88ff` | Primary accent (active tab, focus, links) |
165
- | `--tcw-accent-hover` | `#6b9aff` | Accent hover state |
166
- | `--tcw-accent-soft` | `rgba(79,136,255,.14)` | Accent tint (selected row background) |
167
- | `--tcw-accent-glow` | `rgba(79,136,255,.22)` | Accent glow (focus halo) |
168
- | `--tcw-accent-line` | `rgba(79,136,255,.55)` | Accent border/underline |
169
- | `--tcw-red` / `--tcw-red-soft` | `#f23645` / tint | Down/sell/negative |
170
- | `--tcw-green` / `--tcw-green-soft` | `#26a17b` / tint | Up/buy/positive |
171
- | `--tcw-amber` | `#ff9f43` | Warning |
172
- | `--tcw-hover-bg` | `rgba(255,255,255,.05)` | Row/button hover background |
173
- | `--tcw-active-bg` | `rgba(255,255,255,.08)` | Row/button pressed background |
174
- | `--tcw-divider` | `rgba(255,255,255,.06)` | Hairline dividers |
175
- | `--tcw-ease` / `--tcw-ease-out` | cubic-bezier | Transition easing |
176
- | `--tcw-dur-fast` / `-normal` / `-slow` | `120ms` / `180ms` / `260ms` | Transition durations |
177
- | `--tcw-radius-sm` / `-base` / `-lg` / `-xl` | `4px` / `6px` / `10px` / `14px` | Corner radii — set to `0` for a square look |
178
- | `--tcw-shadow-sm` / `-md` / `-lg` / `-xl` | box-shadow values | Elevation |
179
- | `--tcw-ring` | `0 0 0 2px rgba(79,136,255,.45)` | Focus ring |
180
- | `--tcw-font-mono` | `'JetBrains Mono', …` | Monospace font stack (price ladder, code) |
181
-
182
- 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.
183
-
184
- ## Features
185
-
186
- ### Chart Types
187
-
188
- | Type | Description |
189
- |---|---|
190
- | Candlestick | Standard OHLC candles |
191
- | Hollow Candle | Open/close determines fill |
192
- | Bar (OHLC) | Classic open-high-low-close bars |
193
- | Line | Close price line |
194
- | Area | Filled area below close |
195
- | Baseline | Two-tone area split at a reference price |
196
- | Heikin-Ashi | Smoothed candles for trend identification |
197
- | Renko | Fixed-size bricks that ignore time |
198
- | Kagi | Reversal-based line chart |
199
- | Point & Figure | X/O columns for supply/demand analysis |
200
- | Line Break | Three-line break charts |
201
- | Range Bars | Fixed price-range bars — each bar's high − low equals a configured range |
202
- | Volume Candles | Candlesticks with width proportional to volume |
203
- | Equivolume | Full-range boxes with width proportional to volume share (Richard Arms style) |
204
- | HLC Area | High-low-close area band with close line |
205
- | Step Line | Staircase/step pattern from close prices |
206
- | Line with Markers | Close line with circular markers at each data point |
207
-
208
- ### Multi-Chart Grid
209
-
210
- Display multiple synchronized charts side-by-side with linked crosshairs and time axis:
211
-
212
- ```typescript
213
- import { ChartGrid, BinanceAdapter } from '@tradecanvas/chart'
214
-
215
- const grid = new ChartGrid(document.getElementById('grid')!, {
216
- layout: '2x2',
217
- syncCrosshair: true,
218
- syncTimeAxis: true,
219
- })
220
-
221
- const adapter = new BinanceAdapter()
222
- grid.connectAll(adapter, ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT'], '5m')
223
- ```
224
-
225
- Supported layouts: `'1x1'`, `'1x2'`, `'2x1'`, `'2x2'`, `'1x3'`, `'3x1'`, `'2x3'`, `'3x2'`.
226
-
227
- ### Command Palette
228
-
229
- 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).
230
-
231
- ### Finance Charts
232
-
233
- | Chart | Description |
234
- |---|---|
235
- | SparklineChart | Tiny inline line/area chart from a number array — for dashboards and KPI cards |
236
- | DepthChart | Bid/ask order book visualization with cumulative volume areas |
237
- | EquityCurveChart | Portfolio equity line with drawdown shading and benchmark comparison |
238
- | HeatmapChart | Colored cell grid with treemap layout — for sector/market performance |
239
- | WaterfallChart | Running cumulative bars — P&L attribution, revenue bridge, cash flow |
240
- | GaugeChart | Speedometer-style gauge — KPIs, risk scores, Fear & Greed index |
241
-
242
- ```typescript
243
- import {
244
- SparklineChart, DepthChart, EquityCurveChart, HeatmapChart,
245
- WaterfallChart, GaugeChart,
246
- } from '@tradecanvas/chart'
247
-
248
- // Sparkline in a 120x48 container
249
- new SparklineChart(el, { data: [100, 102, 98, 105, 103], mode: 'area', color: '#26A69A' })
250
-
251
- // Equity curve with drawdown
252
- new EquityCurveChart(el, { data: equityPoints, drawdown: true, benchmark: spyData })
253
-
254
- // Order book depth
255
- new DepthChart(el, { data: { bids, asks }, crosshair: true })
256
-
257
- // Market heatmap (treemap weighted by market cap)
258
- new HeatmapChart(el, { data: cells, weighted: true })
259
-
260
- // P&L waterfall
261
- new WaterfallChart(el, {
262
- data: [
263
- { label: 'Start', value: 10000, type: 'total' },
264
- { label: 'Gain', value: 1850 },
265
- { label: 'Loss', value: -620 },
266
- { label: 'End', value: 11230, type: 'total' },
267
- ],
268
- })
269
-
270
- // Fear & Greed gauge
271
- const gauge = new GaugeChart(el, {
272
- value: 72,
273
- zones: [
274
- { from: 0, to: 25, color: '#ef4444' },
275
- { from: 75, to: 100, color: '#10b981' },
276
- ],
277
- })
278
- gauge.setValue(85) // animates smoothly
279
- ```
280
-
281
- ### Indicators (built-in)
282
-
283
- **Overlay** (drawn on the price chart):
284
- SMA, EMA, Hull MA, Bollinger Bands, Keltner Channel, Donchian Channel, Ichimoku Cloud, Parabolic SAR, Supertrend, VWAP, Anchored VWAP, Pivot Points (Classic), ZigZag, Linear Regression Channel
285
-
286
- **Panel** (separate sub-chart):
287
- RSI, MACD, Stochastic, ATR, ADX, CCI, CMF, MFI, OBV, ROC, TSI, Williams %R, Awesome Oscillator, Chaikin Oscillator, Volume Profile, VROC, Standard Deviation, Accumulation/Distribution, Aroon
288
-
289
- All indicator parameters are validated at runtime — invalid values (NaN, Infinity, non-numeric strings, missing keys) fall back to documented defaults instead of silently propagating to calculations.
290
-
291
- ### Drawing Tools
292
-
293
- 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
294
-
295
- All drawing tools support:
296
- - Click-to-place with magnet snapping to OHLC values
297
- - Undo / redo (Ctrl+Z / Ctrl+Y)
298
- - Serialization for save/load
299
- - Custom styles (color, width, dash pattern)
300
-
301
- ### Trading Overlay
302
-
303
- Render open positions and pending orders directly on the chart, like MT4/MT5.
304
-
305
- ```typescript
306
- import type { TradingPosition, TradingOrder } from '@tradecanvas/chart'
307
-
308
- chart.setPositions([{
309
- id: 'pos-1',
310
- side: 'buy',
311
- entryPrice: 3500,
312
- quantity: 1.5,
313
- closedQuantity: 0.5, // partial close — visualized as a left-edge dim band
314
- stopLoss: 3400,
315
- takeProfit: 3700,
316
- }])
317
-
318
- chart.setOrders([{
319
- id: 'order-1',
320
- side: 'sell',
321
- type: 'limit',
322
- price: 3800,
323
- quantity: 0.5,
324
- label: 'TP',
325
- draggable: true,
326
- }])
327
-
328
- // Customize the position zone color via P&L thresholds
329
- chart.setTradingConfig({
330
- pnlThresholds: [
331
- { pnl: -Infinity, color: '#b91c1c' },
332
- { pnl: 0, color: '#94a3b8' },
333
- { pnl: 50, color: '#16a34a' },
334
- { pnl: 200, color: '#15803d' },
335
- ],
336
- // Custom label template — tokens: {side} {qty} {openQty} {closedQty} {entry} {price} {pnl} {pnlPct} {pnlSign}
337
- positionLabel: '{side} {openQty}/{qty} @ {entry} | {pnlSign}{pnl} ({pnlPct})',
338
- })
339
-
340
- // Listen for user drag-to-modify
341
- chart.on('positionModify', (e) => console.log('SL/TP moved:', e.payload))
342
- chart.on('orderModify', (e) => console.log('Order moved:', e.payload))
343
- ```
344
-
345
- ### Signal Markers
346
-
347
- Visualize buy/sell signals from bots, indicators, or manual analysis.
348
-
349
- ```typescript
350
- chart.addSignalMarker({
351
- time: 1715692800000,
352
- price: 62500,
353
- direction: 'long',
354
- confidence: 0.85,
355
- source: 'ema-crossover',
356
- label: 'EMA Cross',
357
- })
358
-
359
- // Color-code by source
360
- chart.setSignalMarkerStyle({
361
- sourceColors: {
362
- 'ema-crossover': '#2196F3',
363
- 'rsi-divergence': '#FF9800',
364
- 'whale-flow': '#9C27B0',
365
- },
366
- })
367
- ```
368
-
369
- ### Trade Zones
370
-
371
- Render entry→exit rectangles with P&L coloring for executed trades.
372
-
373
- ```typescript
374
- const zoneId = chart.addTradeZone({
375
- entryTime: 1715692800000,
376
- entryPrice: 62500,
377
- exitTime: 1715700000000,
378
- exitPrice: 63200,
379
- direction: 'long',
380
- pnl: 140,
381
- pnlPercent: 1.12,
382
- })
383
-
384
- // Update a live trade when it closes
385
- chart.updateTradeZone(zoneId, {
386
- exitTime: Date.now(),
387
- exitPrice: 63500,
388
- pnl: 200,
389
- })
390
- ```
391
-
392
- ### Real-Time Streaming
393
-
394
- ```typescript
395
- // Built-in Binance adapter (free, no API key)
396
- chart.connect({
397
- adapter: new BinanceAdapter(),
398
- symbol: 'ETHUSDT',
399
- timeframe: '1m',
400
- historyLimit: 500,
401
- })
402
-
403
- // Or manual data feed
404
- chart.setData(historicalBars)
405
- chart.appendBar(newBar)
406
- chart.updateLastBar(updatedBar)
407
- chart.setCurrentPrice(3500.42)
408
- ```
409
-
410
- ### Web Worker indicator pipeline
411
-
412
- Heavy charts (1,000+ bars × 10+ indicators) can stutter when `calculate()` runs on the main thread. `IndicatorWorkerHost` offloads calculation to a worker so the render loop stays smooth.
413
-
414
- ```typescript
415
- import { IndicatorWorkerHost } from '@tradecanvas/core'
416
-
417
- // Bundler-supported worker URL (Vite, webpack 5, esbuild, etc.)
418
- const worker = new Worker(
419
- new URL('@tradecanvas/core/dist/indicator.worker.js', import.meta.url),
420
- { type: 'module' },
421
- )
422
- const host = new IndicatorWorkerHost(worker, { timeoutMs: 30_000 })
423
-
424
- const output = await host.calculate(
425
- 'rsi',
426
- { id: 'rsi', instanceId: 'rsi-1', params: { period: 14 } },
427
- bars,
428
- )
429
-
430
- // Health check / cleanup
431
- await host.ping()
432
- host.terminate()
433
- ```
434
-
435
- No worker available (SSR, tests, or as a safety net)? Pass `null` and register fallback plugins for synchronous calculation:
436
-
437
- ```typescript
438
- import { IndicatorWorkerHost, RSIIndicator } from '@tradecanvas/core'
439
-
440
- const host = new IndicatorWorkerHost(null)
441
- host.registerFallbackPlugin(new RSIIndicator())
442
- const output = await host.calculate('rsi', config, bars) // runs synchronously
443
- ```
444
-
445
- Render still happens on the main thread (it needs `CanvasRenderingContext2D`). Only the heavy compute moves off-thread.
446
-
447
- ### Save / Load
448
-
449
- ```typescript
450
- const json = chart.saveState()
451
- localStorage.setItem('my-chart', json!)
452
-
453
- chart.loadState(localStorage.getItem('my-chart')!)
454
-
455
- // Download / upload files
456
- chart.downloadState('my-chart.json')
457
- await chart.loadStateFromFile()
458
- ```
459
-
460
- ### Themes
461
-
462
- ```typescript
463
- import { DARK_THEME, LIGHT_THEME, DARK_TERMINAL } from '@tradecanvas/chart'
464
-
465
- // Built-in presets: DARK_THEME, LIGHT_THEME, DARK_TERMINAL
466
- chart.setTheme(DARK_TERMINAL) // fintech terminal: #0E0E0E bg, #00FF87/#FF3B4D candles, monospace
467
-
468
- // Or customize any preset
469
- chart.setTheme({
470
- ...DARK_THEME,
471
- candleUp: '#26A69A',
472
- candleDown: '#EF5350',
473
- background: '#0a0a0f',
474
- })
475
- ```
476
-
477
- ### Events
478
-
479
- ```typescript
480
- chart.on('crosshairMove', (e) => { /* { point, bar, barIndex, indicatorValues } */ })
481
- chart.on('barClick', (e) => { /* { bar, barIndex, point } */ })
482
- chart.on('visibleRangeChange', (e) => { /* { from, to } — bar indices, not timestamps */ })
483
- chart.on('priceRangeChange', (e) => { /* { min, max } — visible price bounds */ })
484
- chart.on('zoomChange', (e) => { /* { barWidth } — pixels per bar */ })
485
- chart.on('drawingCreate', (e) => { /* ... */ })
486
- chart.on('orderModify', (e) => { /* ... */ })
487
- chart.on('positionModify', (e) => { /* ... */ })
488
- ```
489
-
490
- `visibleRangeChange`, `priceRangeChange`, and `zoomChange` fire on every pan,
491
- zoom, resize, and data update — but only when that piece of viewport state
492
- actually changed. Resolve a `visibleRangeChange` index to time with
493
- `chart.getData()[e.payload.from].time`.
494
-
495
- ### Replay Mode
496
-
497
- `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).
498
-
499
- ```typescript
500
- import { ReplayController } from '@tradecanvas/chart'
501
-
502
- const replay = new ReplayController({
503
- data: historicalBars,
504
- speed: 10, // bars per second
505
- startIndex: 0,
506
- })
507
-
508
- // Seed the chart with the prefix before replay starts
509
- chart.setData(replay.getPrefix())
510
-
511
- // Each emitted bar drives the chart forward
512
- replay.on('bar', ({ bar }) => chart.appendBar(bar))
513
- replay.on('finished', () => console.log('done'))
514
-
515
- replay.start()
516
- // replay.pause(); replay.resume(); replay.step(5); replay.seek(200); replay.setSpeed(20)
517
- ```
518
-
519
- ### Chart Interaction (TradingView-style)
520
-
521
- Every gesture you'd expect from a desktop trading chart is built in:
522
-
523
- | Gesture | Result |
524
- |---|---|
525
- | Drag chart body left/right | Pan through time |
526
- | Drag chart body up/down | Pan the price scale (freezes auto-scale; double-click price axis to restore) |
527
- | Drag price axis up/down | Compress / expand vertical scale (freezes auto-scale) |
528
- | Drag time axis left/right | Zoom time axis |
529
- | Double-click price axis | Re-enable auto-scale |
530
- | Double-click time axis | Fit all data to viewport |
531
- | Wheel | Zoom around cursor |
532
- | `Shift` + drag | Measure ruler (bars × time × price Δ × %) |
533
- | `Alt` + click | Pin OHLC tooltip; live crosshair shows Δ to pinned bar |
534
- | Hover | Price + time pill labels follow on both axes |
535
- | `Esc` | Unpin tooltip / cancel drawing |
536
- | `?` | Show keyboard-shortcut sheet *(widget)* |
537
- | `Ctrl/⌘ + K` | Command palette *(widget)* |
538
- | `Ctrl/⌘ + P` | Symbol search *(widget)* |
539
- | `Ctrl/⌘ + Z` / `Shift + Z` | Undo / redo drawings |
540
-
541
- ### Data import — drag-and-drop or programmatic
542
-
543
- ```typescript
544
- import { parseOHLCV } from '@tradecanvas/chart'
545
-
546
- const { data, rowCount, skipped } = parseOHLCV(csvText)
547
- chart.setData(data)
548
- ```
549
-
550
- Drop a CSV or JSON file onto the widget and it loads instantly. Auto-detects
551
- delimiter (`,` / `;` / tab / `|`), header vs. headerless, ISO 8601 timestamps,
552
- and array-of-arrays vs. array-of-objects JSON.
553
-
554
- ### Backtesting (`@tradecanvas/analytics`)
555
-
556
- Bar-by-bar strategy backtester with virtual fills, commission/slippage models, and a full risk-metrics report.
557
-
558
- ```typescript
559
- import { Backtester, PercentCommission, PercentSlippage } from '@tradecanvas/analytics'
560
-
561
- const bt = new Backtester({
562
- initialCash: 10_000,
563
- commission: new PercentCommission(0.0005),
564
- slippage: new PercentSlippage(0.0003),
565
- })
566
-
567
- const result = bt.run(historicalBars, (ctx) => {
568
- // Strategy fn runs at close of each bar; orders fill on the NEXT bar.
569
- if (!ctx.position && smaFast > smaSlow) {
570
- ctx.placeOrder({ side: 'long', type: 'market', quantity: 1 })
571
- } else if (ctx.position && smaFast < smaSlow) {
572
- ctx.close()
573
- }
574
- })
575
-
576
- console.log(result.metrics.sharpe) // 1.42
577
- console.log(result.metrics.maxDrawdownPct) // 0.087
578
- console.log(result.equityCurve) // → feed into the chart via EquityCurveRenderer
579
- ```
580
-
581
- 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/).
582
-
583
- #### Strategy library (new in 0.9)
584
-
585
- Four drop-in reference strategies — each returns a `StrategyFn` ready to feed
586
- `Backtester.run()`:
587
-
588
- ```typescript
589
- import {
590
- Backtester,
591
- smaCrossStrategy,
592
- rsiReversionStrategy,
593
- donchianBreakoutStrategy,
594
- bollingerReversionStrategy,
595
- } from '@tradecanvas/analytics'
596
-
597
- const bt = new Backtester({ initialCash: 10_000 })
598
- bt.run(bars, smaCrossStrategy({ fastPeriod: 10, slowPeriod: 30 }))
599
- bt.run(bars, donchianBreakoutStrategy({ entryPeriod: 20, exitPeriod: 10 }))
600
- ```
601
-
602
- #### Monte Carlo path-dependence (new in 0.9)
603
-
604
- Shuffle realised trade order N times to expose whether a strategy depends on
605
- lucky sequencing. Tight P5/P95 band = robust edge; wide band = path-dependent.
606
-
607
- ```typescript
608
- import { runMonteCarlo } from '@tradecanvas/analytics'
609
-
610
- const result = bt.run(bars, smaCrossStrategy())
611
- const mc = runMonteCarlo(10_000, result.trades, { simulations: 1000, seed: 42 })
612
-
613
- mc.equityBands // [{ step, p5, p25, p50, p75, p95 }, …]
614
- mc.finalEquityPercentiles // { p5, p25, p50, p75, p95 }
615
- mc.probabilityProfitable // 0..1
616
- mc.worstMaxDrawdownPct
617
- ```
618
-
619
- ## Comparison
620
-
621
- | Feature | @tradecanvas/chart | lightweight-charts | chart.js | Highcharts Stock |
622
- |---|---|---|---|---|
623
- | Chart types | 17 + 6 finance | 4 | 8 (non-financial) | 10+ |
624
- | Finance charts | Sparkline, Depth, Equity, Heatmap, Waterfall, Gauge | None | None | Some |
625
- | Built-in indicators | 33 | 0 | 0 | ~30 |
626
- | Drawing tools | 24 | 0 | 0 | Some |
627
- | Trading overlay | Full (pos + orders + drag) | None | None | None |
628
- | Real-time streaming | Built-in (Binance) | Manual | Manual | Built-in |
629
- | Save/load state | Yes | No | No | Yes |
630
- | Replay mode | Yes (`ReplayController`) | No | No | No |
631
- | Backtester | Yes (`@tradecanvas/analytics`) | No | No | No |
632
- | Multi-chart grid | Yes (`ChartGrid`) | No | No | Yes |
633
- | Bundle (gzip) | ~56 KB core | ~45 KB | ~70 KB | ~200 KB |
634
- | Dependencies | 0 | 1 | 0 | 0 |
635
- | Widget (complete UI) | Yes (`ChartWidget`) | No | No | No |
636
- | License | MIT | Apache 2.0 | MIT | Commercial |
637
-
638
- ## API Overview
639
-
640
- ### `new Chart(container, options)`
641
-
642
- ```typescript
643
- const chart = new Chart(element, {
644
- chartType: 'candlestick',
645
- theme: DARK_THEME,
646
- autoScale: true,
647
- rightMargin: 5,
648
- numberLocale: 'en-US', // or 'de-DE', 'vi-VN', etc. — BCP 47 locale
649
- crosshair: { mode: 'magnet' },
650
- features: { drawings: true, indicators: true, trading: true, volume: true },
651
- })
652
-
653
- // Change locale at runtime
654
- chart.setNumberLocale('de-DE') // 65.234,00
655
- ```
656
-
657
- ### Key Methods
658
-
659
- | Method | Description |
660
- |---|---|
661
- | `setData(bars)` | Load historical OHLCV data |
662
- | `appendBar(bar)` | Append a new candle |
663
- | `appendBars(bars)` | Bulk append (reconnect catch-up) |
664
- | `updateLastBar(bar)` | Update the in-progress candle |
665
- | `setCurrentPrice(price, pulseColor?)` | Show a live price line |
666
- | `connect(config)` | Connect to a real-time data source |
667
- | `setTimeframe(tf)` | Switch timeframe on active stream |
668
- | `setChartType(type)` | Switch chart type |
669
- | `setTheme(theme)` | Apply a theme (DARK_THEME, LIGHT_THEME, DARK_TERMINAL) |
670
- | `setNumberLocale(locale)` | Set number format locale (en-US, de-DE, vi-VN) |
671
- | `setStatusText(text)` | Show status in legend area ("LIVE · 8ms") |
672
- | `addIndicator(id, params?)` | Add a technical indicator |
673
- | `removeIndicator(instanceId)` | Remove an indicator |
674
- | `setDrawingTool(tool)` | Activate a drawing tool |
675
- | `setPositions(positions)` | Render trading positions |
676
- | `setOrders(orders)` | Render pending orders |
677
- | `setVolumeProfileVisible(v)` | Toggle the horizontal volume-profile overlay |
678
- | `setVolumeProfileConfig({ buckets, widthRatio, opacity, highlightPoC })` | Tune the volume profile |
679
- | `setAutoScale(v)` / `setLogScale(v)` | Lock or change price-scale mode |
680
- | `fitContent()` / `scrollToEnd()` | Fit all data / jump to live edge |
681
- | `saveState(key?)` | Serialize chart state |
682
- | `loadState(json)` | Restore chart state |
683
- | `screenshot()` | Download chart as image |
684
- | `on(event, handler)` | Subscribe to events |
685
- | `destroy()` | Clean up all resources |
686
-
687
- ### Data Format
688
-
689
- ```typescript
690
- interface OHLCBar {
691
- time: number // Unix timestamp (seconds)
692
- open: number
693
- high: number
694
- low: number
695
- close: number
696
- volume: number
697
- }
698
- ```
699
-
700
- ## Examples
701
-
702
- | Example | Description |
703
- |---|---|
704
- | [Live demo](https://bonguynvan.github.io/tradecanvas/) | Feature Lab: drawing tools, indicators, trading, replay, sub-cent prices + Vietnamese UI, 200k bars, slow-network switching — each on a live chart |
705
- | [StackBlitz sandboxes](https://bonguynvan.github.io/tradecanvas/examples/) | One-click, forkable: vanilla `Chart`, `ChartWidget`, React / Vue / Svelte wrappers, finance charts |
706
- | [`@tradecanvas/react`](https://www.npmjs.com/package/@tradecanvas/react) · [`/vue`](https://www.npmjs.com/package/@tradecanvas/vue) · [`/svelte`](https://www.npmjs.com/package/@tradecanvas/svelte) | Framework components — reactive props, typed, zero boilerplate |
707
-
708
- ## Browser Support
709
-
710
- Chrome 80+, Firefox 80+, Safari 14+, Edge 80+
711
-
712
- ## Framework Integration
713
-
714
- TradeCanvas is framework-agnostic. The `Chart` class takes a DOM element and manages its own canvas layers.
715
-
716
- **React:**
717
-
718
- ```tsx
719
- import { useEffect, useRef } from 'react'
720
- import { Chart, BinanceAdapter } from '@tradecanvas/chart'
721
-
722
- function TradingChart() {
723
- const ref = useRef<HTMLDivElement>(null)
724
-
725
- useEffect(() => {
726
- const chart = new Chart(ref.current!, {
727
- theme: 'dark',
728
- features: { indicators: true, drawings: true },
729
- })
730
- chart.connect({
731
- adapter: new BinanceAdapter(),
732
- symbol: 'BTCUSDT',
733
- timeframe: '5m',
734
- })
735
- return () => chart.destroy()
736
- }, [])
737
-
738
- return <div ref={ref} style={{ width: '100%', height: 500 }} />
739
- }
740
- ```
741
-
742
- **Svelte:**
743
-
744
- ```svelte
745
- <script lang="ts">
746
- import { onMount, onDestroy } from 'svelte'
747
- import { Chart, BinanceAdapter, DARK_THEME } from '@tradecanvas/chart'
748
- import type { TimeFrame } from '@tradecanvas/chart'
749
-
750
- interface Props { symbol?: string; timeframe?: TimeFrame }
751
- let { symbol = 'BTCUSDT', timeframe = '5m' }: Props = $props()
752
-
753
- let container: HTMLDivElement
754
- let chart: Chart | null = null
755
-
756
- onMount(() => {
757
- chart = new Chart(container, {
758
- chartType: 'candlestick',
759
- theme: DARK_THEME,
760
- autoScale: true,
761
- features: { indicators: true, drawings: true, volume: true },
762
- })
763
- chart.connect({ adapter: new BinanceAdapter(), symbol, timeframe })
764
- })
765
-
766
- onDestroy(() => chart?.destroy())
767
-
768
- $effect(() => {
769
- if (!chart) return
770
- chart.disconnectStream()
771
- chart.connect({ adapter: new BinanceAdapter(), symbol, timeframe })
772
- })
773
- </script>
774
-
775
- <div bind:this={container} style="width: 100%; height: 600px" />
776
- ```
777
-
778
- **Vue:**
779
-
780
- ```vue
781
- <template>
782
- <div ref="chartContainer" style="width: 100%; height: 600px" />
783
- </template>
784
-
785
- <script setup lang="ts">
786
- import { ref, onMounted, onUnmounted } from 'vue'
787
- import { Chart, BinanceAdapter, DARK_THEME } from '@tradecanvas/chart'
788
-
789
- const chartContainer = ref<HTMLDivElement>()
790
- let chart: Chart | null = null
791
-
792
- onMounted(() => {
793
- if (!chartContainer.value) return
794
- chart = new Chart(chartContainer.value, {
795
- chartType: 'candlestick',
796
- theme: DARK_THEME,
797
- autoScale: true,
798
- features: { indicators: true, drawings: true, volume: true },
799
- })
800
- chart.connect({ adapter: new BinanceAdapter(), symbol: 'BTCUSDT', timeframe: '5m' })
801
- })
802
-
803
- onUnmounted(() => chart?.destroy())
804
- </script>
805
- ```
806
-
807
- ## Architecture
808
-
809
- Multi-layer canvas for optimal rendering — only dirty layers repaint each frame:
810
-
811
- ```
812
- UI Layer (price axis, legend, live price) z=3
813
- Overlay Layer (drawings, trading positions/orders) z=2
814
- Main Layer (candles, indicators, volume) z=1
815
- Background (grid, watermark) z=0
816
- ```
817
-
818
- ## Related projects
819
-
820
- - **[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/)**
821
-
822
- ## License
823
-
824
- [MIT](./LICENSE)
1
+ # @tradecanvas/chart
2
+
3
+ High-performance canvas trading chart with built-in indicators, drawing tools, and real-time streaming. Zero external dependencies.
4
+
5
+ **[Live Demo](https://bonguynvan.github.io/tradecanvas/)** | **[GitHub](https://github.com/bonguynvan/tradecanvas)** | **[npm](https://www.npmjs.com/package/@tradecanvas/chart)**
6
+
7
+ ## Why TradeCanvas?
8
+
9
+ Most chart libraries make you choose: pretty charts with no trading features, or trading features with an ugly API. TradeCanvas gives you both.
10
+
11
+ - **70 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. No separate calculation library needed.
12
+ - **40 drawing tools** — Trendlines (info line, trend angle, cross line), Fibonacci (retracement, extension, channel, time zones, speed resistance fan), horizontal/vertical lines, channels, pitchforks (Andrews, Schiff, modified Schiff), Gann fans / boxes, cyclic lines, harmonic patterns (XABCD, ABCD, head and shoulders, Elliott waves), date & price range, Long/Short Position, Volume Profile range. With undo/redo and full serialization.
13
+ - **17 chart types** — Candlestick, line, area, bar, hollow candle, baseline, Heikin-Ashi, Renko, Kagi, Line Break, Point & Figure, Range Bars, Volume Candles, **Equivolume**, HLC Area, Step Line, Line+Markers.
14
+ - **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.
15
+ - **Trading overlay** — Render open positions with entry line, P&L zone, and SL/TP markers. Orders as dashed lines. Drag SL/TP to modify. Cleanly opt-out via `features.trading: false` for non-trading projects.
16
+ - **Real-time streaming** — Built-in Binance adapter. Plug in your own data source with the adapter interface.
17
+ - **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.**
18
+ - **Replay mode** — `ReplayController` drives historical bars forward at controlled speed with start / pause / step / seek / setSpeed. *(new in 0.9)* The widget now ships a floating bottom scrubber UI (play/pause/step/seek + 0.5×–100× speed) on top of it.
19
+ - **Volume Profile** *(new in 0.9)* — optional horizontal histogram of traded volume bucketed by price over the visible range, with point-of-control highlighting.
20
+ - **Watchlist sidebar** *(new in 0.9)* — opt-in vertical panel listing symbols with last price, % change, mini sparkline. Click a row to switch chart.
21
+ - **CSV / JSON drag-and-drop** *(new in 0.9)* — 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.
22
+ - **Saved layouts** *(new in 0.9)* — opt-in per-symbol persistence of chart type + indicator stack + drawings + alerts to localStorage. Switch symbol, switch back, your setup is intact.
23
+ - **Multi-chart grid** — `ChartGrid` for synchronized 2×2 / 2×3 layouts with linked crosshairs and shared time axis.
24
+ - **Signal markers & trade zones** — render bot/algorithm output (directional arrows, entry→exit rectangles) as a first-class chart layer.
25
+ - **Hotkey sheet** *(new in 0.9)* — press `?` in the widget to open a categorized keyboard-shortcut reference.
26
+ - **Save/load chart state** — Persist drawings, indicators, theme, and chart type to JSON. Restore with one call.
27
+ - **Zero dependencies** — The entire library is self-contained. No `d3`, no `chart.js`, no `fancy-canvas`.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install @tradecanvas/chart
33
+ # or
34
+ pnpm add @tradecanvas/chart
35
+ # or
36
+ yarn add @tradecanvas/chart
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ The fastest path is `ChartWidget` — drop-in component with a full trading UI (toolbar, drawing sidebar, settings dialog, status bar). Zero framework dependency.
42
+
43
+ ```typescript
44
+ import { ChartWidget } from '@tradecanvas/chart/widget'
45
+ import { BinanceAdapter } from '@tradecanvas/chart'
46
+
47
+ const widget = new ChartWidget(document.getElementById('chart')!, {
48
+ symbol: 'BTCUSDT',
49
+ timeframe: '5m',
50
+ theme: 'dark',
51
+ adapter: new BinanceAdapter(),
52
+ trading: true,
53
+ })
54
+ ```
55
+
56
+ That's it. Live data, all 70 indicators, all 40 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.
57
+
58
+ ## Headless Chart
59
+
60
+ For projects that want to own the surrounding UI (custom toolbar, framework-specific controls), use the lower-level `Chart` class directly:
61
+
62
+ ```typescript
63
+ import { Chart, BinanceAdapter } from '@tradecanvas/chart'
64
+
65
+ const chart = new Chart(document.getElementById('chart')!, {
66
+ theme: 'dark',
67
+ autoScale: true,
68
+ features: {
69
+ drawings: true,
70
+ indicators: true,
71
+ trading: true, // set false to disable orders/positions entirely
72
+ tradingContextMenu: true, // opt-in right-click order menu (off by default)
73
+ volume: true,
74
+ },
75
+ })
76
+
77
+ const adapter = new BinanceAdapter()
78
+ chart.connect({ adapter, symbol: 'BTCUSDT', timeframe: '5m', historyLimit: 300 })
79
+ ```
80
+
81
+ ### Widget Options
82
+
83
+ | Option | Type | Default | Description |
84
+ |---|---|---|---|
85
+ | `symbol` | `string` | `'BTCUSDT'` | Initial trading symbol |
86
+ | `timeframe` | `TimeFrame` | `'5m'` | Initial timeframe |
87
+ | `theme` | `'dark' \| 'light' \| Theme` | `'dark'` | Chart theme |
88
+ | `adapter` | `DataAdapter` | — | Data source adapter |
89
+ | `toolbar` | `boolean` | `true` | Show top toolbar |
90
+ | `drawingTools` | `boolean` | `true` | Show left drawing sidebar |
91
+ | `settings` | `boolean` | `true` | Show settings button |
92
+ | `trading` | `boolean` | `true` | Enable trading overlay |
93
+ | `statusBar` | `boolean` | `true` | Show bottom status bar |
94
+ | `rangeBar` | `boolean` | `true` | Range presets (1D … All) and go to date (Alt+G) on the status bar |
95
+ | `indicatorLegend` | `boolean` | `true` | Indicators listed on the chart (under the OHLCV legend and atop their panes) with show / settings / remove |
96
+ | `fullscreen` | `boolean` | `true` | Fullscreen button in the toolbar |
97
+ | `symbols` | `string[]` | BTC/ETH/SOL/BNB | Searchable symbol catalog |
98
+ | `timeframes` | `TimeFrame[]` | 1m to 1M | Timeframes on offer; pin favourites from the ▾ menu |
99
+ | `chartTypes` | `ChartType[]` | 11 types | Available chart types |
100
+ | `watchlist` | `boolean` | `false` | Right-side watchlist sidebar |
101
+ | `dragDropImport` | `boolean` | `true` | Drop CSV / JSON files onto the chart to load data |
102
+ | `persistLayouts` | `boolean \| { keyPrefix, debounceMs }` | `false` | Save per-symbol indicators / drawings / chart type to localStorage |
103
+ | `onSymbolChange` | `(symbol) => void` | — | Symbol change callback |
104
+ | `onTimeframeChange` | `(tf) => void` | — | Timeframe change callback |
105
+ | `onReady` | `(chart) => void` | — | Fired when chart is ready |
106
+ | `locale` | `string` | `'en'` | UI chrome language — built-in `'en'` / `'vi'`, see **Widget i18n** below |
107
+ | `messages` | `Partial<Record<MessageKey, string>>` | — | Override or add individual UI strings on top of `locale` |
108
+
109
+ ### Icons
110
+
111
+ The widget's icon set is exported for your own UI: `createIcon(name)`,
112
+ `createToolIcon(drawingTool)`, `createChartTypeIcon(chartType)` return inline
113
+ SVG strings drawn in `currentColor` (24 px grid, 1.75 px strokes).
114
+
115
+ ```ts
116
+ import { createToolIcon } from '@tradecanvas/chart/widget'
117
+ button.innerHTML = createToolIcon('fibRetracement', 16)
118
+ ```
119
+
120
+ ### Widget i18n
121
+
122
+ `locale` and `messages` translate `ChartWidget`'s own chrome — toolbar, watchlist, indicator-picker section headers (Popular/All, overlay/panel tags), status bar, settings panel (titles/tabs/section headers), and hotkey sheet (title/group headers). Set at construction; not currently hot-swappable at runtime.
123
+
124
+ ```ts
125
+ new ChartWidget(el, {
126
+ locale: 'vi', // built-in Vietnamese table
127
+ messages: { 'watchlist.title': 'Theo dõi' }, // override/add individual keys — always wins
128
+ chartOptions: { numberLocale: 'vi-VN' }, // separate: number/date formatting (see below)
129
+ });
130
+ ```
131
+
132
+ `locale`/`messages` only cover chrome **text**; they're independent of `chartOptions.numberLocale`, which controls number/date **formatting** (price axis, legend, watchlist prices, current-price tag, session-break dates) via `Intl`/`toLocaleString`.
133
+
134
+ Not yet covered by `locale` (still English; PRs welcome, or override via `messages`/your own CSS):
135
+ - Indicator and drawing-tool **names** (SMA, Bollinger Bands, Trend Line, …) — these come from `widgetConfig.ts`'s data tables, not the message catalog.
136
+ - Individual settings rows beyond the tab/section level (e.g. "Up Body", "Grid Lines").
137
+ - Individual hotkey-sheet shortcut labels and key-cap text (group titles are translated).
138
+ - Alerts panel, symbol search, command palette, data window, depth ladder, bracket bar, replay bar, drawing-style panel, object tree.
139
+
140
+ See `packages/library/src/widget/i18n.ts` for the full key list (`MessageKey`) and the English/Vietnamese tables.
141
+
142
+ ### Widget vs Headless
143
+
144
+ | | `Chart` (headless) | `ChartWidget` |
145
+ |---|---|---|
146
+ | Import | `@tradecanvas/chart` | `@tradecanvas/chart/widget` |
147
+ | UI included | None — build your own | Complete toolbar, sidebar, settings |
148
+ | Bundle impact | ~50 KB gzip | ~65 KB gzip (includes UI) |
149
+ | Framework | Any (React, Vue, Svelte, vanilla) | Vanilla JS DOM (works everywhere) |
150
+ | Customization | Full control | Toggle sections on/off |
151
+ | Advanced access | Direct API | `widget.getChart()` for direct API |
152
+
153
+ ### Widget Theming
154
+
155
+ `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.
156
+
157
+ ```css
158
+ /* Dark is the default (no attribute needed); light sets data-tcw-theme="light" */
159
+ .my-app .tcw-root:not([data-tcw-theme="light"]) {
160
+ --tcw-bg: #0a0a0f;
161
+ --tcw-accent: #7c5cff;
162
+ --tcw-radius: 0px;
163
+ --tcw-radius-lg: 0px;
164
+ }
165
+ ```
166
+
167
+ | Variable | Default (dark) | Purpose |
168
+ |---|---|---|
169
+ | `--tcw-bg` | `#080b10` | Root background |
170
+ | `--tcw-bg-surface` | `#0c1016` | Panel / toolbar surface |
171
+ | `--tcw-bg-elevated` | `#141922` | Popovers, dropdowns, modals |
172
+ | `--tcw-bg-overlay` | `rgba(20,25,34,.5)` | Backdrop behind overlays |
173
+ | `--tcw-border` | `#1f2630` | Default border |
174
+ | `--tcw-border-strong` | `#2a323e` | Emphasized border (focus rings, dividers) |
175
+ | `--tcw-text` | `#e7e9ee` | Primary text |
176
+ | `--tcw-text-dim` | `#aab1bd` | Secondary text |
177
+ | `--tcw-text-muted` | `#758091` | Tertiary / placeholder text |
178
+ | `--tcw-accent` | `#f2a93b` | Primary accent (active tab, focus, links) |
179
+ | `--tcw-accent-ink` | `#1a1204` | Text and icons on an accent fill |
180
+ | `--tcw-accent-hover` | `#f5b95c` | Accent hover state |
181
+ | `--tcw-accent-soft` | `rgba(242,169,59,.14)` | Accent tint (selected row background) |
182
+ | `--tcw-accent-glow` | `rgba(242,169,59,.22)` | Accent glow (focus halo) |
183
+ | `--tcw-accent-line` | `rgba(242,169,59,.55)` | Accent border/underline |
184
+ | `--tcw-red` / `--tcw-red-soft` | `#e8505b` / tint | Down/sell/negative |
185
+ | `--tcw-green` / `--tcw-green-soft` | `#1fa874` / tint | Up/buy/positive |
186
+ | `--tcw-amber` | `#ff9f43` | Warning |
187
+ | `--tcw-hover-bg` | `rgba(255,255,255,.05)` | Row/button hover background |
188
+ | `--tcw-active-bg` | `rgba(255,255,255,.08)` | Row/button pressed background |
189
+ | `--tcw-divider` | `rgba(255,255,255,.06)` | Hairline dividers |
190
+ | `--tcw-ease` / `--tcw-ease-out` | cubic-bezier | Transition easing |
191
+ | `--tcw-dur-fast` / `-normal` / `-slow` | `120ms` / `180ms` / `260ms` | Transition durations |
192
+ | `--tcw-radius-sm` / `-base` / `-lg` / `-xl` | `4px` / `6px` / `10px` / `14px` | Corner radii — set to `0` for a square look |
193
+ | `--tcw-shadow-sm` / `-md` / `-lg` / `-xl` | box-shadow values | Elevation |
194
+ | `--tcw-ring` | `0 0 0 2px rgba(242,169,59,.45)` | Focus ring |
195
+ | `--tcw-font-mono` | `'JetBrains Mono', …` | Monospace font stack (price ladder, code) |
196
+
197
+ 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.
198
+
199
+ ## Features
200
+
201
+ ### Chart Types
202
+
203
+ | Type | Description |
204
+ |---|---|
205
+ | Candlestick | Standard OHLC candles |
206
+ | Hollow Candle | Open/close determines fill |
207
+ | Bar (OHLC) | Classic open-high-low-close bars |
208
+ | Line | Close price line |
209
+ | Area | Filled area below close |
210
+ | Baseline | Two-tone area split at a reference price |
211
+ | Heikin-Ashi | Smoothed candles for trend identification |
212
+ | Renko | Fixed-size bricks that ignore time |
213
+ | Kagi | Reversal-based line chart |
214
+ | Point & Figure | X/O columns for supply/demand analysis |
215
+ | Line Break | Three-line break charts |
216
+ | Range Bars | Fixed price-range bars — each bar's high − low equals a configured range |
217
+ | Volume Candles | Candlesticks with width proportional to volume |
218
+ | Equivolume | Full-range boxes with width proportional to volume share (Richard Arms style) |
219
+ | HLC Area | High-low-close area band with close line |
220
+ | Step Line | Staircase/step pattern from close prices |
221
+ | Line with Markers | Close line with circular markers at each data point |
222
+
223
+ ### Multi-Chart Grid
224
+
225
+ Display multiple synchronized charts side-by-side with linked crosshairs and time axis:
226
+
227
+ ```typescript
228
+ import { ChartGrid, BinanceAdapter } from '@tradecanvas/chart'
229
+
230
+ const grid = new ChartGrid(document.getElementById('grid')!, {
231
+ layout: '2x2',
232
+ syncCrosshair: true,
233
+ syncTimeAxis: true,
234
+ })
235
+
236
+ const adapter = new BinanceAdapter()
237
+ grid.connectAll(adapter, ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT'], '5m')
238
+ ```
239
+
240
+ Supported layouts: `'1x1'`, `'1x2'`, `'2x1'`, `'2x2'`, `'1x3'`, `'3x1'`, `'2x3'`, `'3x2'`.
241
+
242
+ ### Command Palette
243
+
244
+ 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).
245
+
246
+ ### Finance Charts
247
+
248
+ | Chart | Description |
249
+ |---|---|
250
+ | SparklineChart | Tiny inline line/area chart from a number array — for dashboards and KPI cards |
251
+ | DepthChart | Bid/ask order book visualization with cumulative volume areas |
252
+ | EquityCurveChart | Portfolio equity line with drawdown shading and benchmark comparison |
253
+ | HeatmapChart | Colored cell grid with treemap layout — for sector/market performance |
254
+ | WaterfallChart | Running cumulative bars — P&L attribution, revenue bridge, cash flow |
255
+ | GaugeChart | Speedometer-style gauge — KPIs, risk scores, Fear & Greed index |
256
+
257
+ ```typescript
258
+ import {
259
+ SparklineChart, DepthChart, EquityCurveChart, HeatmapChart,
260
+ WaterfallChart, GaugeChart,
261
+ } from '@tradecanvas/chart'
262
+
263
+ // Sparkline in a 120x48 container
264
+ new SparklineChart(el, { data: [100, 102, 98, 105, 103], mode: 'area', color: '#1fa874' })
265
+
266
+ // Equity curve with drawdown
267
+ new EquityCurveChart(el, { data: equityPoints, drawdown: true, benchmark: spyData })
268
+
269
+ // Order book depth
270
+ new DepthChart(el, { data: { bids, asks }, crosshair: true })
271
+
272
+ // Market heatmap (treemap weighted by market cap)
273
+ new HeatmapChart(el, { data: cells, weighted: true })
274
+
275
+ // P&L waterfall
276
+ new WaterfallChart(el, {
277
+ data: [
278
+ { label: 'Start', value: 10000, type: 'total' },
279
+ { label: 'Gain', value: 1850 },
280
+ { label: 'Loss', value: -620 },
281
+ { label: 'End', value: 11230, type: 'total' },
282
+ ],
283
+ })
284
+
285
+ // Fear & Greed gauge: zones light up to the value, the label shows the current zone
286
+ const gauge = new GaugeChart(el, {
287
+ value: 72,
288
+ label: 'Fear & Greed',
289
+ zones: [
290
+ { from: 0, to: 25, color: '#e8505b', label: 'Extreme fear' },
291
+ { from: 25, to: 45, color: '#f2a93b', label: 'Fear' },
292
+ { from: 45, to: 55, color: '#8a93a3', label: 'Neutral' },
293
+ { from: 55, to: 75, color: '#62c895', label: 'Greed' },
294
+ { from: 75, to: 100, color: '#1fa874', label: 'Extreme greed' },
295
+ ],
296
+ // pointer: 'needle', // classic needle instead of the ring marker
297
+ })
298
+ gauge.setValue(85) // animates smoothly
299
+ ```
300
+
301
+ ### Indicators (built-in)
302
+
303
+ **Overlay** (drawn on the price chart):
304
+ SMA, EMA, Hull MA, Bollinger Bands, Keltner Channel, Donchian Channel, Ichimoku Cloud, Parabolic SAR, Supertrend, VWAP, Anchored VWAP, Pivot Points (Classic), ZigZag, Linear Regression Channel
305
+
306
+ **Panel** (separate sub-chart):
307
+ RSI, MACD, Stochastic, ATR, ADX, CCI, CMF, MFI, OBV, ROC, TSI, Williams %R, Awesome Oscillator, Chaikin Oscillator, Volume Profile, VROC, Standard Deviation, Accumulation/Distribution, Aroon
308
+
309
+ All indicator parameters are validated at runtime — invalid values (NaN, Infinity, non-numeric strings, missing keys) fall back to documented defaults instead of silently propagating to calculations.
310
+
311
+ ### Drawing Tools
312
+
313
+ 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
314
+
315
+ All drawing tools support:
316
+ - Click-to-place with magnet snapping to OHLC values
317
+ - Undo / redo (Ctrl+Z / Ctrl+Y)
318
+ - Serialization for save/load
319
+ - Custom styles (color, width, dash pattern)
320
+
321
+ ### Trading Overlay
322
+
323
+ Render open positions and pending orders directly on the chart, like MT4/MT5.
324
+
325
+ ```typescript
326
+ import type { TradingPosition, TradingOrder } from '@tradecanvas/chart'
327
+
328
+ chart.setPositions([{
329
+ id: 'pos-1',
330
+ side: 'buy',
331
+ entryPrice: 3500,
332
+ quantity: 1.5,
333
+ closedQuantity: 0.5, // partial close — visualized as a left-edge dim band
334
+ stopLoss: 3400,
335
+ takeProfit: 3700,
336
+ }])
337
+
338
+ chart.setOrders([{
339
+ id: 'order-1',
340
+ side: 'sell',
341
+ type: 'limit',
342
+ price: 3800,
343
+ quantity: 0.5,
344
+ label: 'TP',
345
+ draggable: true,
346
+ }])
347
+
348
+ // Customize the position zone color via P&L thresholds
349
+ chart.setTradingConfig({
350
+ pnlThresholds: [
351
+ { pnl: -Infinity, color: '#b91c1c' },
352
+ { pnl: 0, color: '#94a3b8' },
353
+ { pnl: 50, color: '#16a34a' },
354
+ { pnl: 200, color: '#15803d' },
355
+ ],
356
+ // Custom label template — tokens: {side} {qty} {openQty} {closedQty} {entry} {price} {pnl} {pnlPct} {pnlSign}
357
+ positionLabel: '{side} {openQty}/{qty} @ {entry} | {pnlSign}{pnl} ({pnlPct})',
358
+ })
359
+
360
+ // Listen for user drag-to-modify
361
+ chart.on('positionModify', (e) => console.log('SL/TP moved:', e.payload))
362
+ chart.on('orderModify', (e) => console.log('Order moved:', e.payload))
363
+ ```
364
+
365
+ ### Signal Markers
366
+
367
+ Visualize buy/sell signals from bots, indicators, or manual analysis.
368
+
369
+ ```typescript
370
+ chart.addSignalMarker({
371
+ time: 1715692800000,
372
+ price: 62500,
373
+ direction: 'long',
374
+ confidence: 0.85,
375
+ source: 'ema-crossover',
376
+ label: 'EMA Cross',
377
+ })
378
+
379
+ // Color-code by source
380
+ chart.setSignalMarkerStyle({
381
+ sourceColors: {
382
+ 'ema-crossover': '#4c8dff',
383
+ 'rsi-divergence': '#f2a93b',
384
+ 'whale-flow': '#9C27B0',
385
+ },
386
+ })
387
+ ```
388
+
389
+ ### Trade Zones
390
+
391
+ Render entry→exit rectangles with P&L coloring for executed trades.
392
+
393
+ ```typescript
394
+ const zoneId = chart.addTradeZone({
395
+ entryTime: 1715692800000,
396
+ entryPrice: 62500,
397
+ exitTime: 1715700000000,
398
+ exitPrice: 63200,
399
+ direction: 'long',
400
+ pnl: 140,
401
+ pnlPercent: 1.12,
402
+ })
403
+
404
+ // Update a live trade when it closes
405
+ chart.updateTradeZone(zoneId, {
406
+ exitTime: Date.now(),
407
+ exitPrice: 63500,
408
+ pnl: 200,
409
+ })
410
+ ```
411
+
412
+ ### Real-Time Streaming
413
+
414
+ ```typescript
415
+ // Built-in Binance adapter (free, no API key)
416
+ chart.connect({
417
+ adapter: new BinanceAdapter(),
418
+ symbol: 'ETHUSDT',
419
+ timeframe: '1m',
420
+ historyLimit: 500,
421
+ })
422
+
423
+ // Or manual data feed
424
+ chart.setData(historicalBars)
425
+ chart.appendBar(newBar)
426
+ chart.updateLastBar(updatedBar)
427
+ chart.setCurrentPrice(3500.42)
428
+ ```
429
+
430
+ ### Web Worker indicator pipeline
431
+
432
+ Heavy charts (1,000+ bars × 10+ indicators) can stutter when `calculate()` runs on the main thread. `IndicatorWorkerHost` offloads calculation to a worker so the render loop stays smooth.
433
+
434
+ ```typescript
435
+ import { IndicatorWorkerHost } from '@tradecanvas/core'
436
+
437
+ // Bundler-supported worker URL (Vite, webpack 5, esbuild, etc.)
438
+ const worker = new Worker(
439
+ new URL('@tradecanvas/core/dist/indicator.worker.js', import.meta.url),
440
+ { type: 'module' },
441
+ )
442
+ const host = new IndicatorWorkerHost(worker, { timeoutMs: 30_000 })
443
+
444
+ const output = await host.calculate(
445
+ 'rsi',
446
+ { id: 'rsi', instanceId: 'rsi-1', params: { period: 14 } },
447
+ bars,
448
+ )
449
+
450
+ // Health check / cleanup
451
+ await host.ping()
452
+ host.terminate()
453
+ ```
454
+
455
+ No worker available (SSR, tests, or as a safety net)? Pass `null` and register fallback plugins for synchronous calculation:
456
+
457
+ ```typescript
458
+ import { IndicatorWorkerHost, RSIIndicator } from '@tradecanvas/core'
459
+
460
+ const host = new IndicatorWorkerHost(null)
461
+ host.registerFallbackPlugin(new RSIIndicator())
462
+ const output = await host.calculate('rsi', config, bars) // runs synchronously
463
+ ```
464
+
465
+ Render still happens on the main thread (it needs `CanvasRenderingContext2D`). Only the heavy compute moves off-thread.
466
+
467
+ ### Save / Load
468
+
469
+ ```typescript
470
+ const json = chart.saveState()
471
+ localStorage.setItem('my-chart', json!)
472
+
473
+ chart.loadState(localStorage.getItem('my-chart')!)
474
+
475
+ // Download / upload files
476
+ chart.downloadState('my-chart.json')
477
+ await chart.loadStateFromFile()
478
+
479
+ // Or keep a layout saved as it changes (debounced)
480
+ chart.setAutoSave('my-chart', 1500)
481
+ ```
482
+
483
+ A saved layout holds the chart type, theme, drawings, indicators (inputs, pane,
484
+ colours, visibility) and alerts, including alerts on indicator lines.
485
+
486
+ ### Themes
487
+
488
+ ```typescript
489
+ import { DARK_THEME, LIGHT_THEME, DARK_TERMINAL } from '@tradecanvas/chart'
490
+
491
+ // Built-in presets: DARK_THEME, LIGHT_THEME, DARK_TERMINAL
492
+ chart.setTheme(DARK_TERMINAL) // fintech terminal: #0E0E0E bg, #00FF87/#FF3B4D candles, monospace
493
+
494
+ // Or customize any preset
495
+ chart.setTheme({
496
+ ...DARK_THEME,
497
+ candleUp: '#1fa874',
498
+ candleDown: '#e8505b',
499
+ background: '#0a0a0f',
500
+ })
501
+ ```
502
+
503
+ ### Events
504
+
505
+ ```typescript
506
+ chart.on('crosshairMove', (e) => { /* { point, bar, barIndex, indicatorValues } — also over indicator panes */ })
507
+ chart.on('crosshairLeave', () => { /* the pointer left the plot */ })
508
+ chart.on('drawingToolChange', (e) => { /* { tool } — null once a drawing is finished or cancelled */ })
509
+ chart.on('indicatorUpdate', (e) => { /* { from } — indicator values recomputed from this bar on */ })
510
+ chart.on('paneResize', (e) => { /* { instanceId, size } — an indicator pane was resized */ })
511
+ chart.on('barClick', (e) => { /* { bar, barIndex, point } */ })
512
+ chart.on('visibleRangeChange', (e) => { /* { from, to } — bar indices, not timestamps */ })
513
+ chart.on('priceRangeChange', (e) => { /* { min, max } — visible price bounds */ })
514
+ chart.on('zoomChange', (e) => { /* { barWidth } — pixels per bar */ })
515
+ chart.on('drawingCreate', (e) => { /* ... */ })
516
+ chart.on('orderModify', (e) => { /* ... */ })
517
+ chart.on('positionModify', (e) => { /* ... */ })
518
+ ```
519
+
520
+ `visibleRangeChange`, `priceRangeChange`, and `zoomChange` fire on every pan,
521
+ zoom, resize, and data update — but only when that piece of viewport state
522
+ actually changed. Resolve a `visibleRangeChange` index to time with
523
+ `chart.getData()[e.payload.from].time`.
524
+
525
+ ### Replay Mode
526
+
527
+ `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).
528
+
529
+ ```typescript
530
+ import { ReplayController } from '@tradecanvas/chart'
531
+
532
+ const replay = new ReplayController({
533
+ data: historicalBars,
534
+ speed: 10, // bars per second
535
+ startIndex: 0,
536
+ })
537
+
538
+ // Seed the chart with the prefix before replay starts
539
+ chart.setData(replay.getPrefix())
540
+
541
+ // Each emitted bar drives the chart forward
542
+ replay.on('bar', ({ bar }) => chart.appendBar(bar))
543
+ replay.on('finished', () => console.log('done'))
544
+
545
+ replay.start()
546
+ // replay.pause(); replay.resume(); replay.step(5); replay.seek(200); replay.setSpeed(20)
547
+ ```
548
+
549
+ ### Chart Interaction
550
+
551
+ Every gesture you'd expect from a desktop trading chart is built in:
552
+
553
+ | Gesture | Result |
554
+ |---|---|
555
+ | Drag chart body left/right | Pan through time |
556
+ | Drag chart body up/down | Pan the price scale (freezes auto-scale; double-click price axis to restore) |
557
+ | Drag price axis up/down | Compress / expand vertical scale (freezes auto-scale) |
558
+ | Drag time axis left/right | Zoom time axis |
559
+ | Double-click price axis | Re-enable auto-scale |
560
+ | Double-click time axis | Fit all data to viewport |
561
+ | Wheel | Zoom around cursor |
562
+ | `Shift` + drag | Measure ruler (bars × time × price Δ × %) |
563
+ | `Alt` + click | Pin OHLC tooltip; live crosshair shows Δ to pinned bar |
564
+ | Hover | Price + time pill labels follow on both axes |
565
+ | `Esc` | Unpin tooltip / cancel drawing |
566
+ | `?` | Show keyboard-shortcut sheet *(widget)* |
567
+ | `Ctrl/⌘ + K` | Command palette *(widget)* |
568
+ | `Ctrl/⌘ + P` | Symbol search *(widget)* |
569
+ | `Ctrl/⌘ + Z` / `Shift + Z` | Undo / redo drawings |
570
+
571
+ ### Data import — drag-and-drop or programmatic
572
+
573
+ ```typescript
574
+ import { parseOHLCV } from '@tradecanvas/chart'
575
+
576
+ const { data, rowCount, skipped } = parseOHLCV(csvText)
577
+ chart.setData(data)
578
+ ```
579
+
580
+ Drop a CSV or JSON file onto the widget and it loads instantly. Auto-detects
581
+ delimiter (`,` / `;` / tab / `|`), header vs. headerless, ISO 8601 timestamps,
582
+ and array-of-arrays vs. array-of-objects JSON.
583
+
584
+ ### Backtesting (`@tradecanvas/analytics`)
585
+
586
+ Bar-by-bar strategy backtester with virtual fills, commission/slippage models, and a full risk-metrics report.
587
+
588
+ ```typescript
589
+ import { Backtester, PercentCommission, PercentSlippage } from '@tradecanvas/analytics'
590
+
591
+ const bt = new Backtester({
592
+ initialCash: 10_000,
593
+ commission: new PercentCommission(0.0005),
594
+ slippage: new PercentSlippage(0.0003),
595
+ })
596
+
597
+ const result = bt.run(historicalBars, (ctx) => {
598
+ // Strategy fn runs at close of each bar; orders fill on the NEXT bar.
599
+ if (!ctx.position && smaFast > smaSlow) {
600
+ ctx.placeOrder({ side: 'long', type: 'market', quantity: 1 })
601
+ } else if (ctx.position && smaFast < smaSlow) {
602
+ ctx.close()
603
+ }
604
+ })
605
+
606
+ console.log(result.metrics.sharpe) // 1.42
607
+ console.log(result.metrics.maxDrawdownPct) // 0.087
608
+ console.log(result.equityCurve) // → feed into the chart via EquityCurveRenderer
609
+ ```
610
+
611
+ 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/).
612
+
613
+ #### Strategy library (new in 0.9)
614
+
615
+ Four drop-in reference strategies — each returns a `StrategyFn` ready to feed
616
+ `Backtester.run()`:
617
+
618
+ ```typescript
619
+ import {
620
+ Backtester,
621
+ smaCrossStrategy,
622
+ rsiReversionStrategy,
623
+ donchianBreakoutStrategy,
624
+ bollingerReversionStrategy,
625
+ } from '@tradecanvas/analytics'
626
+
627
+ const bt = new Backtester({ initialCash: 10_000 })
628
+ bt.run(bars, smaCrossStrategy({ fastPeriod: 10, slowPeriod: 30 }))
629
+ bt.run(bars, donchianBreakoutStrategy({ entryPeriod: 20, exitPeriod: 10 }))
630
+ ```
631
+
632
+ #### Monte Carlo path-dependence (new in 0.9)
633
+
634
+ Shuffle realised trade order N times to expose whether a strategy depends on
635
+ lucky sequencing. Tight P5/P95 band = robust edge; wide band = path-dependent.
636
+
637
+ ```typescript
638
+ import { runMonteCarlo } from '@tradecanvas/analytics'
639
+
640
+ const result = bt.run(bars, smaCrossStrategy())
641
+ const mc = runMonteCarlo(10_000, result.trades, { simulations: 1000, seed: 42 })
642
+
643
+ mc.equityBands // [{ step, p5, p25, p50, p75, p95 }, …]
644
+ mc.finalEquityPercentiles // { p5, p25, p50, p75, p95 }
645
+ mc.probabilityProfitable // 0..1
646
+ mc.worstMaxDrawdownPct
647
+ ```
648
+
649
+ ## Comparison
650
+
651
+ | Feature | @tradecanvas/chart | lightweight-charts | chart.js | Highcharts Stock |
652
+ |---|---|---|---|---|
653
+ | Chart types | 17 + 6 finance | 4 | 8 (non-financial) | 10+ |
654
+ | Finance charts | Sparkline, Depth, Equity, Heatmap, Waterfall, Gauge | None | None | Some |
655
+ | Built-in indicators | 33 | 0 | 0 | ~30 |
656
+ | Drawing tools | 24 | 0 | 0 | Some |
657
+ | Trading overlay | Full (pos + orders + drag) | None | None | None |
658
+ | Real-time streaming | Built-in (Binance) | Manual | Manual | Built-in |
659
+ | Save/load state | Yes | No | No | Yes |
660
+ | Replay mode | Yes (`ReplayController`) | No | No | No |
661
+ | Backtester | Yes (`@tradecanvas/analytics`) | No | No | No |
662
+ | Multi-chart grid | Yes (`ChartGrid`) | No | No | Yes |
663
+ | Bundle (gzip) | ~56 KB core | ~45 KB | ~70 KB | ~200 KB |
664
+ | Dependencies | 0 | 1 | 0 | 0 |
665
+ | Widget (complete UI) | Yes (`ChartWidget`) | No | No | No |
666
+ | License | MIT | Apache 2.0 | MIT | Commercial |
667
+
668
+ ## API Overview
669
+
670
+ ### `new Chart(container, options)`
671
+
672
+ ```typescript
673
+ const chart = new Chart(element, {
674
+ chartType: 'candlestick',
675
+ theme: DARK_THEME,
676
+ autoScale: true,
677
+ rightMargin: 5,
678
+ numberLocale: 'en-US', // or 'de-DE', 'vi-VN', etc. — BCP 47 locale
679
+ crosshair: { mode: 'magnet' },
680
+ features: { drawings: true, indicators: true, trading: true, volume: true },
681
+ })
682
+
683
+ // Change locale at runtime
684
+ chart.setNumberLocale('de-DE') // 65.234,00
685
+ ```
686
+
687
+ ### Key Methods
688
+
689
+ | Method | Description |
690
+ |---|---|
691
+ | `setData(bars)` | Load historical OHLCV data |
692
+ | `appendBar(bar)` | Append a new candle |
693
+ | `appendBars(bars)` | Bulk append (reconnect catch-up) |
694
+ | `updateLastBar(bar)` | Update the in-progress candle |
695
+ | `setCurrentPrice(price, pulseColor?)` | Show a live price line |
696
+ | `connect(config)` | Connect to a real-time data source |
697
+ | `setTimeframe(tf)` | Switch timeframe on active stream |
698
+ | `setChartType(type)` | Switch chart type |
699
+ | `setTheme(theme)` | Apply a theme (DARK_THEME, LIGHT_THEME, DARK_TERMINAL) |
700
+ | `setNumberLocale(locale)` | Set number format locale (en-US, de-DE, vi-VN) |
701
+ | `setStatusText(text)` | Show status in legend area ("LIVE · 8ms") |
702
+ | `addIndicator(id, params?)` | Add a technical indicator |
703
+ | `removeIndicator(instanceId)` | Remove an indicator |
704
+ | `setDrawingTool(tool)` | Activate a drawing tool |
705
+ | `setPositions(positions)` | Render trading positions |
706
+ | `setOrders(orders)` | Render pending orders |
707
+ | `setVolumeProfileVisible(v)` | Toggle the horizontal volume-profile overlay |
708
+ | `setVolumeProfileConfig({ buckets, widthRatio, opacity, highlightPoC })` | Tune the volume profile |
709
+ | `setAutoScale(v)` / `setLogScale(v)` | Lock or change price-scale mode |
710
+ | `setInvertScale(v)` | Turn the price scale upside down |
711
+ | `fitContent()` / `scrollToEnd()` | Fit all data / jump to live edge |
712
+ | `setVisibleRangePreset(p)` | Show `1D`, `5D`, `1M`, `3M`, `6M`, `YTD`, `1Y`, `5Y` or `All` |
713
+ | `goToTime(time)` | Centre the bar at a time |
714
+ | `setCrosshairTime(time)` | Mirror another chart's crosshair (vertical line only) |
715
+ | `copyDrawings()` / `pasteDrawings()` | Copy the selection, paste into this or another chart |
716
+ | `setStayInDrawingMode(v)` | Keep the drawing tool after each drawing |
717
+ | `saveState(key?)` | Serialize chart state |
718
+ | `loadState(json)` | Restore chart state |
719
+ | `screenshot()` | Download chart as image |
720
+ | `on(event, handler)` | Subscribe to events |
721
+ | `destroy()` | Clean up all resources |
722
+
723
+ ### Data Format
724
+
725
+ ```typescript
726
+ interface OHLCBar {
727
+ time: number // Unix timestamp (seconds)
728
+ open: number
729
+ high: number
730
+ low: number
731
+ close: number
732
+ volume: number
733
+ }
734
+ ```
735
+
736
+ ## Examples
737
+
738
+ | Example | Description |
739
+ |---|---|
740
+ | [Live demo](https://bonguynvan.github.io/tradecanvas/) | Feature Lab: drawing tools, indicators, trading, replay, sub-cent prices + Vietnamese UI, 200k bars, slow-network switching — each on a live chart |
741
+ | [StackBlitz sandboxes](https://bonguynvan.github.io/tradecanvas/examples/) | One-click, forkable: vanilla `Chart`, `ChartWidget`, React / Vue / Svelte wrappers, finance charts |
742
+ | [`@tradecanvas/react`](https://www.npmjs.com/package/@tradecanvas/react) · [`/vue`](https://www.npmjs.com/package/@tradecanvas/vue) · [`/svelte`](https://www.npmjs.com/package/@tradecanvas/svelte) | Framework components — reactive props, typed, zero boilerplate |
743
+
744
+ ## Browser Support
745
+
746
+ Chrome 80+, Firefox 80+, Safari 14+, Edge 80+
747
+
748
+ ## Framework Integration
749
+
750
+ TradeCanvas is framework-agnostic. The `Chart` class takes a DOM element and manages its own canvas layers.
751
+
752
+ **React:**
753
+
754
+ ```tsx
755
+ import { useEffect, useRef } from 'react'
756
+ import { Chart, BinanceAdapter } from '@tradecanvas/chart'
757
+
758
+ function TradingChart() {
759
+ const ref = useRef<HTMLDivElement>(null)
760
+
761
+ useEffect(() => {
762
+ const chart = new Chart(ref.current!, {
763
+ theme: 'dark',
764
+ features: { indicators: true, drawings: true },
765
+ })
766
+ chart.connect({
767
+ adapter: new BinanceAdapter(),
768
+ symbol: 'BTCUSDT',
769
+ timeframe: '5m',
770
+ })
771
+ return () => chart.destroy()
772
+ }, [])
773
+
774
+ return <div ref={ref} style={{ width: '100%', height: 500 }} />
775
+ }
776
+ ```
777
+
778
+ **Svelte:**
779
+
780
+ ```svelte
781
+ <script lang="ts">
782
+ import { onMount, onDestroy } from 'svelte'
783
+ import { Chart, BinanceAdapter, DARK_THEME } from '@tradecanvas/chart'
784
+ import type { TimeFrame } from '@tradecanvas/chart'
785
+
786
+ interface Props { symbol?: string; timeframe?: TimeFrame }
787
+ let { symbol = 'BTCUSDT', timeframe = '5m' }: Props = $props()
788
+
789
+ let container: HTMLDivElement
790
+ let chart: Chart | null = null
791
+
792
+ onMount(() => {
793
+ chart = new Chart(container, {
794
+ chartType: 'candlestick',
795
+ theme: DARK_THEME,
796
+ autoScale: true,
797
+ features: { indicators: true, drawings: true, volume: true },
798
+ })
799
+ chart.connect({ adapter: new BinanceAdapter(), symbol, timeframe })
800
+ })
801
+
802
+ onDestroy(() => chart?.destroy())
803
+
804
+ $effect(() => {
805
+ if (!chart) return
806
+ chart.disconnectStream()
807
+ chart.connect({ adapter: new BinanceAdapter(), symbol, timeframe })
808
+ })
809
+ </script>
810
+
811
+ <div bind:this={container} style="width: 100%; height: 600px" />
812
+ ```
813
+
814
+ **Vue:**
815
+
816
+ ```vue
817
+ <template>
818
+ <div ref="chartContainer" style="width: 100%; height: 600px" />
819
+ </template>
820
+
821
+ <script setup lang="ts">
822
+ import { ref, onMounted, onUnmounted } from 'vue'
823
+ import { Chart, BinanceAdapter, DARK_THEME } from '@tradecanvas/chart'
824
+
825
+ const chartContainer = ref<HTMLDivElement>()
826
+ let chart: Chart | null = null
827
+
828
+ onMounted(() => {
829
+ if (!chartContainer.value) return
830
+ chart = new Chart(chartContainer.value, {
831
+ chartType: 'candlestick',
832
+ theme: DARK_THEME,
833
+ autoScale: true,
834
+ features: { indicators: true, drawings: true, volume: true },
835
+ })
836
+ chart.connect({ adapter: new BinanceAdapter(), symbol: 'BTCUSDT', timeframe: '5m' })
837
+ })
838
+
839
+ onUnmounted(() => chart?.destroy())
840
+ </script>
841
+ ```
842
+
843
+ ## Architecture
844
+
845
+ Two stacked canvases — a hover repaints only the thin top one:
846
+
847
+ ```
848
+ Top canvas (crosshair + axis pills, legend, countdown, measure) z=1
849
+ Scene canvas (grid, candles, indicators, drawings, orders, axes) z=0
850
+ ```
851
+
852
+ ## Related projects
853
+
854
+ - **[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/)**
855
+
856
+ ## License
857
+
858
+ [MIT](./LICENSE)