wickchart 1.4.0 → 1.5.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.
package/README.md CHANGED
@@ -6,6 +6,9 @@ A TradingView-style financial chart as a single framework-agnostic Web Component
6
6
  One file, zero dependencies, one HTML tag. Canvas-rendered, fast, themeable, and
7
7
  streaming-ready.
8
8
 
9
+ <img width="759" height="390" alt="image" src="https://github.com/user-attachments/assets/f335cecc-d5b3-4d27-a982-600b6bc72d6f" />
10
+
11
+
9
12
  ## Install
10
13
 
11
14
  ```bash
@@ -226,7 +229,7 @@ chart.setData([
226
229
  | ------------- | ---------- | ------------------------------------------------------------------ |
227
230
  | `theme` | `dark` | `dark` or `light` |
228
231
  | `type` | `candles` | `candles`, `line`, `area`, `bars` (OHLC), `hollow` (hollow up-candles), `heikin` (Heikin-Ashi) |
229
- | `indicators` | `volume`* | Space/comma-separated: `sma:20`, `ema:50`, `bb:20`, `rsi:14`, `macd:12/26/9`, `volume`, or any registered indicator |
232
+ | `indicators` | `volume`* | Space/comma-separated: `sma:20`, `ema:50`, `bb:20`, `vwap`, `supertrend:10/3`, `donchian:20`, `keltner:20/2`, `rsi:14`, `macd:12/26/9`, `stoch:14/3`, `atr:14`, `obv`, `cci:20`, `wr:14`, `volume`, or any registered indicator |
230
233
  | `label` | – | Text shown in the legend (e.g. `"BTC · 1h"`) |
231
234
  | `log` | off | Logarithmic price scale |
232
235
  | `auto` | on | Keep the right edge pinned to the latest bar while streaming |
@@ -247,8 +250,17 @@ chart.setData([
247
250
  | `sma` | overlay | `period` (20) | |
248
251
  | `ema` | overlay | `period` (50) | |
249
252
  | `bb` | overlay | `period`, `mult` (20, 2) | Bollinger bands (3 lines) |
253
+ | `vwap` | overlay | – | hlc3 VWAP, resets each UTC day |
254
+ | `supertrend` | overlay | `period`, `mult` (10, 3) | ATR trend line, breaks at flips |
255
+ | `donchian` | overlay | `period` (20) | high/low channel + mid |
256
+ | `keltner` | overlay | `period`, `mult` (20, 2) | EMA ± mult×ATR channel |
250
257
  | `rsi` | pane | `period` (14) | fixed 0–100 scale, 30/70 guides |
251
258
  | `macd` | pane | `fast/slow/signal` (12/26/9) | 2 lines + histogram |
259
+ | `stoch` | pane | `period`, `smooth` (14, 3) | %K + %D, fixed 0–100, 20/80 guides |
260
+ | `atr` | pane | `period` (14) | Wilder ATR |
261
+ | `obv` | pane | – | on-balance volume |
262
+ | `cci` | pane | `period` (20) | ±100 guides |
263
+ | `wr` | pane | `period` (14) | Williams %R, fixed −100–0, −80/−20 guides |
252
264
  | `volume` | overlay | – | histogram at the bottom of the price pane |
253
265
 
254
266
  ### Custom indicators
@@ -257,9 +269,9 @@ Register your own — anything from a one-liner moving average to a multi-line
257
269
  pane:
258
270
 
259
271
  ```js
260
- WickChart.registerIndicator('vwap', {
272
+ WickChart.registerIndicator('cvwap', { // cumulative VWAP over the whole dataset
261
273
  kind: 'overlay', // or 'pane'
262
- params: { period: 20 }, // defaults; set via indicators="vwap:30"
274
+ params: { period: 20 }, // defaults; set via indicators="cvwap:30"
263
275
  compute(bars, params) { // bars: normalized {time,open,high,low,close,volume}
264
276
  const out = new Array(bars.length).fill(null);
265
277
  let pv = 0, vv = 0;
@@ -270,9 +282,9 @@ WickChart.registerIndicator('vwap', {
270
282
  }
271
283
  return out; // single series — or { lines:[{name,values}], histogram }
272
284
  },
273
- // pane-only extras: guides:[30,70], range:[0,100], fmt:'price'|'fixed1'
285
+ // pane-only extras: guides:[30,70], range:[0,100], fmt:'price'|'fixed1'|'compact'
274
286
  });
275
- chart.indicators = 'vwap:20';
287
+ chart.indicators = 'cvwap';
276
288
  ```
277
289
 
278
290
  `import WickChart from 'wickchart'` gives you the class for
@@ -306,6 +318,7 @@ shareable URLs.
306
318
  | `prev(x[,k])` `change(x)` | shifted series / bar-to-bar delta |
307
319
  | `abs(x)` `sqrt(x)` `log(x)` `min(a,b)` `max(a,b)` | element-wise math |
308
320
  | `crossup(a,b)` `crossdown(a,b)` | 1 on a strict cross, else 0 |
321
+ | `vwap()` `obv()` `atr(n)` | bar-level series — callable anywhere, e.g. `crossup(close, vwap())` in alerts |
309
322
 
310
323
  Operators are `+ - * / %` with usual precedence, unary `-`, and parentheses.
311
324
  Values before a window fills are `NaN` (not drawn), division by zero yields
@@ -661,7 +674,7 @@ chart.removeLayer('flags'); // detach by handle or id
661
674
  A claimed gesture delivers `move`/`up` (and `cancel` on Escape) to the layer
662
675
  while the chart suppresses pan/brush/measure. Markers, watermarks, signal
663
676
  badges — or a whole drawing toolkit — plug in without the core growing a
664
- single tool. The main entry is covered by a CI gzip budget (64 KB) so it
677
+ single tool. The main entry is covered by a CI gzip budget (68 KB) so it
665
678
  stays that way.
666
679
 
667
680
  ### Drawings — the `wickchart-draw` plugin
@@ -673,19 +686,26 @@ data that rides zoom & pan, survives reloads, extrapolates into future
673
686
  space, and serializes to JSON. Anchors magnet-snap to bar times and OHLC.
674
687
 
675
688
  ```js
689
+ npm install wickchart wickchart-draw // drawings are a separate opt-in package
690
+
676
691
  import { attachDrawings } from 'wickchart-draw';
677
692
 
678
693
  const draw = attachDrawings(chart);
679
694
  draw.setTool('trendline'); // drag to draw; setTool(null) = select/move mode
680
695
  draw.getDrawings(); // → JSON array (save it); setDrawings(saved)
681
696
  draw.undo(); draw.clear();
697
+ draw.setShare(true); // shared drawings: draw on one tab, appears on all
682
698
  chart.addEventListener('wick:drawings', (e) => save(e.detail.drawings));
683
699
  ```
684
700
 
685
701
  Select mode: click a drawing to select it, drag to move, drag the square
686
702
  handles to re-anchor, `Delete` removes, `Esc` cancels a gesture; clicks on
687
- empty space fall through to the chart. Peer dependency: wickchart 1.4.
688
- See the live playground in the docs (Drawings section).
703
+ empty space fall through to the chart. Placing a note opens an inline editor
704
+ (type + `Enter`); click a selected note again to re-edit. `setShare(true)`
705
+ reuses the chart's `co-view` room (or pass an explicit room name) — last
706
+ writer wins, remote updates never touch the local undo stack. Peer
707
+ dependency: wickchart ≥ 1.4. See the live playground in the docs (Drawings
708
+ section — it shares a room, so open it twice and draw on either chart).
689
709
 
690
710
  ## Methods
691
711
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wickchart",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "<wick-chart> — a modern, dependency-free financial charting web component. Candles, line & area charts, crosshair, zoom/pan, indicators (incl. a safe expression mini-language), live streaming via <wick-feed>, theming.",
5
5
  "type": "module",
6
6
  "main": "src/wick-chart.js",
@@ -57,7 +57,14 @@
57
57
  "custom-element",
58
58
  "canvas",
59
59
  "zero-dependency",
60
- "tradingview"
60
+ "tradingview",
61
+ "vwap",
62
+ "supertrend",
63
+ "bollinger-bands",
64
+ "macd",
65
+ "rsi",
66
+ "stochastic",
67
+ "technical-analysis"
61
68
  ],
62
69
  "license": "MIT",
63
70
  "repository": {
package/src/core.js CHANGED
@@ -462,6 +462,259 @@ export function calcMACD(closes, fast = 12, slow = 26, signal = 9) {
462
462
  return { macd, signal: sig, hist };
463
463
  }
464
464
 
465
+ /** True range: max(h−l, |h−prev close|, |l−prev close|); first bar is h−l.
466
+ * @param {Bar[]} bars
467
+ * @returns {Array<number|null>}
468
+ */
469
+ export function calcTrueRange(bars) {
470
+ const out = new Array(bars.length).fill(null);
471
+ for (let i = 0; i < bars.length; i++) {
472
+ const b = bars[i];
473
+ out[i] =
474
+ i === 0
475
+ ? b.high - b.low
476
+ : Math.max(b.high - b.low, Math.abs(b.high - bars[i - 1].close), Math.abs(b.low - bars[i - 1].close));
477
+ }
478
+ return out;
479
+ }
480
+
481
+ /**
482
+ * Average True Range (Wilder smoothing; seeded with the SMA of the first
483
+ * `period` true ranges).
484
+ * @param {Bar[]} bars
485
+ * @param {number} period
486
+ * @returns {Array<number|null>}
487
+ */
488
+ export function calcATR(bars, period = 14) {
489
+ const out = new Array(bars.length).fill(null);
490
+ if (period < 1 || bars.length < period) return out;
491
+ const tr = calcTrueRange(bars);
492
+ let sum = 0;
493
+ for (let i = 0; i < period; i++) sum += tr[i];
494
+ let prev = sum / period;
495
+ out[period - 1] = prev;
496
+ for (let i = period; i < bars.length; i++) {
497
+ prev = (prev * (period - 1) + tr[i]) / period;
498
+ out[i] = prev;
499
+ }
500
+ return out;
501
+ }
502
+
503
+ /**
504
+ * Volume-weighted average price over the hlc3 typical price, anchored to
505
+ * each UTC day (resets at the session boundary).
506
+ * @param {Bar[]} bars
507
+ * @returns {Array<number|null>}
508
+ */
509
+ export function calcVWAP(bars) {
510
+ const out = new Array(bars.length).fill(null);
511
+ let pv = 0;
512
+ let vv = 0;
513
+ let day = null;
514
+ for (let i = 0; i < bars.length; i++) {
515
+ const b = bars[i];
516
+ const ms = b.time < 1e12 ? b.time * 1000 : b.time;
517
+ const d = Math.floor(ms / DAY);
518
+ if (d !== day) {
519
+ day = d;
520
+ pv = 0;
521
+ vv = 0;
522
+ }
523
+ pv += ((b.high + b.low + b.close) / 3) * b.volume;
524
+ vv += b.volume;
525
+ out[i] = vv > 0 ? pv / vv : null;
526
+ }
527
+ return out;
528
+ }
529
+
530
+ /**
531
+ * On-balance volume: cumulative volume signed by close-to-close direction.
532
+ * @param {Bar[]} bars
533
+ * @returns {Array<number|null>}
534
+ */
535
+ export function calcOBV(bars) {
536
+ const out = new Array(bars.length).fill(null);
537
+ let obv = 0;
538
+ for (let i = 0; i < bars.length; i++) {
539
+ if (i > 0) {
540
+ const d = bars[i].close - bars[i - 1].close;
541
+ obv += d > 0 ? bars[i].volume : d < 0 ? -bars[i].volume : 0;
542
+ }
543
+ out[i] = obv;
544
+ }
545
+ return out;
546
+ }
547
+
548
+ /** Highest-high / lowest-low window ending at `i` (shared by stoch/wr/donchian). */
549
+ function winHL(bars, i, period) {
550
+ let hh = -Infinity;
551
+ let ll = Infinity;
552
+ for (let j = i - period + 1; j <= i; j++) {
553
+ if (bars[j].high > hh) hh = bars[j].high;
554
+ if (bars[j].low < ll) ll = bars[j].low;
555
+ }
556
+ return [hh, ll];
557
+ }
558
+
559
+ /** SMA that tolerates leading nulls (windows over sparse raw series). */
560
+ function smaSparse(values, period) {
561
+ const out = new Array(values.length).fill(null);
562
+ let sum = 0;
563
+ let count = 0;
564
+ for (let i = 0; i < values.length; i++) {
565
+ const v = values[i];
566
+ if (isNum(v)) {
567
+ sum += v;
568
+ count++;
569
+ }
570
+ if (i >= period && isNum(values[i - period])) {
571
+ sum -= values[i - period];
572
+ count--;
573
+ }
574
+ if (count === period) out[i] = sum / period;
575
+ }
576
+ return out;
577
+ }
578
+
579
+ /**
580
+ * Stochastic oscillator (slow): raw %K over `period`, smoothed by `smooth`;
581
+ * %D is the SMA of %K.
582
+ * @param {Bar[]} bars
583
+ * @param {number} period
584
+ * @param {number} smooth
585
+ * @returns {{k:Array<number|null>, d:Array<number|null>}}
586
+ */
587
+ export function calcStoch(bars, period = 14, smooth = 3) {
588
+ const n = bars.length;
589
+ const raw = new Array(n).fill(null);
590
+ for (let i = period - 1; i < n; i++) {
591
+ const [hh, ll] = winHL(bars, i, period);
592
+ const span = hh - ll;
593
+ raw[i] = span > 0 ? ((bars[i].close - ll) / span) * 100 : null;
594
+ }
595
+ const k = smooth > 1 ? smaSparse(raw, smooth) : raw;
596
+ const d = smooth > 1 ? smaSparse(k, smooth) : k;
597
+ return { k, d };
598
+ }
599
+
600
+ /**
601
+ * Commodity Channel Index: typical price vs its SMA, scaled by mean deviation.
602
+ * @param {Bar[]} bars
603
+ * @param {number} period
604
+ * @returns {Array<number|null>}
605
+ */
606
+ export function calcCCI(bars, period = 20) {
607
+ const n = bars.length;
608
+ const out = new Array(n).fill(null);
609
+ if (period < 1 || n < period) return out;
610
+ const tp = bars.map((b) => (b.high + b.low + b.close) / 3);
611
+ const ma = calcSMA(tp, period);
612
+ for (let i = period - 1; i < n; i++) {
613
+ let md = 0;
614
+ for (let j = i - period + 1; j <= i; j++) md += Math.abs(tp[j] - ma[i]);
615
+ md /= period;
616
+ out[i] = md > 0 ? (tp[i] - ma[i]) / (0.015 * md) : 0;
617
+ }
618
+ return out;
619
+ }
620
+
621
+ /**
622
+ * Williams %R: −100 at the period low, 0 at the period high.
623
+ * @param {Bar[]} bars
624
+ * @param {number} period
625
+ * @returns {Array<number|null>}
626
+ */
627
+ export function calcWilliamsR(bars, period = 14) {
628
+ const n = bars.length;
629
+ const out = new Array(n).fill(null);
630
+ for (let i = period - 1; i < n; i++) {
631
+ const [hh, ll] = winHL(bars, i, period);
632
+ const span = hh - ll;
633
+ if (span <= 0) continue;
634
+ const r = ((hh - bars[i].close) / span) * -100;
635
+ out[i] = r === 0 ? 0 : r; // avoid −0 on the axis
636
+ }
637
+ return out;
638
+ }
639
+
640
+ /**
641
+ * Donchian channels: highest high / lowest low over `period`, plus mid.
642
+ * @param {Bar[]} bars
643
+ * @param {number} period
644
+ * @returns {{upper:Array<number|null>, mid:Array<number|null>, lower:Array<number|null>}}
645
+ */
646
+ export function calcDonchian(bars, period = 20) {
647
+ const n = bars.length;
648
+ const upper = new Array(n).fill(null);
649
+ const mid = new Array(n).fill(null);
650
+ const lower = new Array(n).fill(null);
651
+ for (let i = period - 1; i < n; i++) {
652
+ const [hh, ll] = winHL(bars, i, period);
653
+ upper[i] = hh;
654
+ lower[i] = ll;
655
+ mid[i] = (hh + ll) / 2;
656
+ }
657
+ return { upper, mid, lower };
658
+ }
659
+
660
+ /**
661
+ * Keltner channels: EMA mid ± mult × ATR.
662
+ * @param {Bar[]} bars
663
+ * @param {number} period
664
+ * @param {number} [mult]
665
+ * @returns {{upper:Array<number|null>, mid:Array<number|null>, lower:Array<number|null>}}
666
+ */
667
+ export function calcKeltner(bars, period = 20, mult = 2) {
668
+ const mid = calcEMA(bars.map((b) => b.close), period);
669
+ const atr = calcATR(bars, period);
670
+ const band = (f) => mid.map((m, i) => (isNum(m) && isNum(atr[i]) ? f(m, atr[i]) : null));
671
+ return { upper: band((m, a) => m + mult * a), mid, lower: band((m, a) => m - mult * a) };
672
+ }
673
+
674
+ /**
675
+ * SuperTrend: ATR bands that flip with the trend. Returns the trend line
676
+ * (support in uptrends, resistance in downtrends) with a one-bar null gap
677
+ * at flips so the renderer breaks the line.
678
+ * @param {Bar[]} bars
679
+ * @param {number} period
680
+ * @param {number} [mult]
681
+ * @returns {Array<number|null>}
682
+ */
683
+ export function calcSuperTrend(bars, period = 10, mult = 3) {
684
+ const n = bars.length;
685
+ const out = new Array(n).fill(null);
686
+ if (period < 1 || n < period) return out;
687
+ const atr = calcATR(bars, period);
688
+ let dir = 1;
689
+ let fUp = Infinity;
690
+ let fLo = -Infinity;
691
+ let started = false;
692
+ for (let i = 0; i < n; i++) {
693
+ if (!isNum(atr[i])) continue;
694
+ const b = bars[i];
695
+ const hl2 = (b.high + b.low) / 2;
696
+ const bUp = hl2 + mult * atr[i];
697
+ const bLo = hl2 - mult * atr[i];
698
+ if (!started) {
699
+ started = true;
700
+ fUp = bUp;
701
+ fLo = bLo;
702
+ dir = b.close >= hl2 ? 1 : -1;
703
+ out[i] = dir > 0 ? fLo : fUp;
704
+ continue;
705
+ }
706
+ const pc = bars[i - 1].close;
707
+ // carry a band forward unless it tightened, or the previous close broke it
708
+ fUp = bUp < fUp || pc > fUp ? bUp : fUp;
709
+ fLo = bLo > fLo || pc < fLo ? bLo : fLo;
710
+ const prevDir = dir;
711
+ if (b.close > fUp) dir = 1;
712
+ else if (b.close < fLo) dir = -1;
713
+ out[i] = dir === prevDir ? (dir > 0 ? fLo : fUp) : null;
714
+ }
715
+ return out;
716
+ }
717
+
465
718
  /* ------------------------------------------------------------------ *
466
719
  * Data merging & gaps
467
720
  * ------------------------------------------------------------------ */
@@ -823,6 +1076,44 @@ export const BUILTIN_INDICATORS = new Map(
823
1076
  params: { period: 50 },
824
1077
  compute: (bars, p) => calcEMA(closesOf(bars), p.period),
825
1078
  },
1079
+ vwap: {
1080
+ kind: 'overlay',
1081
+ params: {},
1082
+ compute: (bars) => calcVWAP(bars),
1083
+ },
1084
+ supertrend: {
1085
+ kind: 'overlay',
1086
+ params: { period: 10, mult: 3 },
1087
+ compute: (bars, p) => calcSuperTrend(bars, p.period, p.mult),
1088
+ },
1089
+ donchian: {
1090
+ kind: 'overlay',
1091
+ params: { period: 20 },
1092
+ compute: (bars, p) => {
1093
+ const c = calcDonchian(bars, p.period);
1094
+ return {
1095
+ lines: [
1096
+ { name: 'upper', values: c.upper },
1097
+ { name: 'mid', values: c.mid },
1098
+ { name: 'lower', values: c.lower },
1099
+ ],
1100
+ };
1101
+ },
1102
+ },
1103
+ keltner: {
1104
+ kind: 'overlay',
1105
+ params: { period: 20, mult: 2 },
1106
+ compute: (bars, p) => {
1107
+ const c = calcKeltner(bars, p.period, p.mult);
1108
+ return {
1109
+ lines: [
1110
+ { name: 'upper', values: c.upper },
1111
+ { name: 'mid', values: c.mid },
1112
+ { name: 'lower', values: c.lower },
1113
+ ],
1114
+ };
1115
+ },
1116
+ },
826
1117
  bb: {
827
1118
  kind: 'overlay',
828
1119
  params: { period: 20, mult: 2 },
@@ -862,6 +1153,49 @@ export const BUILTIN_INDICATORS = new Map(
862
1153
  };
863
1154
  },
864
1155
  },
1156
+ atr: {
1157
+ kind: 'pane',
1158
+ params: { period: 14 },
1159
+ fmt: 'price',
1160
+ compute: (bars, p) => calcATR(bars, p.period),
1161
+ },
1162
+ stoch: {
1163
+ kind: 'pane',
1164
+ params: { period: 14, smooth: 3 },
1165
+ guides: [20, 80],
1166
+ range: [0, 100],
1167
+ fmt: 'fixed1',
1168
+ compute: (bars, p) => {
1169
+ const r = calcStoch(bars, p.period, p.smooth);
1170
+ return {
1171
+ lines: [
1172
+ { name: 'k', values: r.k },
1173
+ { name: 'd', values: r.d },
1174
+ ],
1175
+ };
1176
+ },
1177
+ },
1178
+ obv: {
1179
+ kind: 'pane',
1180
+ params: {},
1181
+ fmt: 'compact',
1182
+ compute: (bars) => calcOBV(bars),
1183
+ },
1184
+ cci: {
1185
+ kind: 'pane',
1186
+ params: { period: 20 },
1187
+ guides: [-100, 100],
1188
+ fmt: 'fixed1',
1189
+ compute: (bars, p) => calcCCI(bars, p.period),
1190
+ },
1191
+ wr: {
1192
+ kind: 'pane',
1193
+ params: { period: 14 },
1194
+ guides: [-80, -20],
1195
+ range: [-100, 0],
1196
+ fmt: 'fixed1',
1197
+ compute: (bars, p) => calcWilliamsR(bars, p.period),
1198
+ },
865
1199
  })
866
1200
  );
867
1201
 
@@ -977,6 +1311,10 @@ const SCRIPT_FUNCS = {
977
1311
  max: { min: 2, max: 2 },
978
1312
  crossup: { min: 2, max: 2 },
979
1313
  crossdown: { min: 2, max: 2 },
1314
+ // bar-level functions — no leading series argument, they read OHLCV directly
1315
+ vwap: { min: 0, max: 0 },
1316
+ obv: { min: 0, max: 0 },
1317
+ atr: { min: 1, max: 1, scalar: [0] },
980
1318
  };
981
1319
 
982
1320
  const scriptErr = (msg) => new Error('script: ' + msg);
@@ -1219,20 +1557,20 @@ function binOp(op, a, b) {
1219
1557
  return NaN;
1220
1558
  }
1221
1559
 
1222
- function evalScriptNode(node, vars, n) {
1560
+ function evalScriptNode(node, vars, n, bars) {
1223
1561
  switch (node.type) {
1224
1562
  case 'num':
1225
1563
  return node.v;
1226
1564
  case 'var':
1227
1565
  return vars[node.name];
1228
1566
  case 'neg': {
1229
- const e = evalScriptNode(node.e, vars, n);
1567
+ const e = evalScriptNode(node.e, vars, n, bars);
1230
1568
  if (!Array.isArray(e)) return -e;
1231
1569
  return e.map((x) => (x == null ? NaN : -x));
1232
1570
  }
1233
1571
  case 'bin': {
1234
- const l = evalScriptNode(node.l, vars, n);
1235
- const r = evalScriptNode(node.r, vars, n);
1572
+ const l = evalScriptNode(node.l, vars, n, bars);
1573
+ const r = evalScriptNode(node.r, vars, n, bars);
1236
1574
  if (!Array.isArray(l) && !Array.isArray(r)) return binOp(node.op, l, r);
1237
1575
  const a = Array.isArray(l) ? l : new Array(n).fill(l);
1238
1576
  const b = Array.isArray(r) ? r : new Array(n).fill(r);
@@ -1241,14 +1579,18 @@ function evalScriptNode(node, vars, n) {
1241
1579
  return out;
1242
1580
  }
1243
1581
  case 'call':
1244
- return evalScriptCall(node, vars, n);
1582
+ return evalScriptCall(node, vars, n, bars);
1245
1583
  }
1246
1584
  return NaN;
1247
1585
  }
1248
1586
 
1249
- function evalScriptCall(node, vars, n) {
1587
+ function evalScriptCall(node, vars, n, bars) {
1250
1588
  const { name, args } = node;
1251
- const s0 = evalScriptNode(args[0], vars, n);
1589
+ // bar-level functions read several series at once — no leading series argument
1590
+ if (name === 'vwap') return calcVWAP(bars);
1591
+ if (name === 'obv') return calcOBV(bars);
1592
+ if (name === 'atr') return calcATR(bars, args[0].type === 'num' ? args[0].v : 1);
1593
+ const s0 = evalScriptNode(args[0], vars, n, bars);
1252
1594
  const a = Array.isArray(s0) ? s0 : new Array(n).fill(s0);
1253
1595
  // window functions must not read leading nulls as 0 — NaN them so results stay honest
1254
1596
  const clean = a.map((x) => (x == null ? NaN : x));
@@ -1287,13 +1629,13 @@ function evalScriptCall(node, vars, n) {
1287
1629
  case 'log': return clean.map((x) => (x <= 0 ? NaN : Math.log(x)));
1288
1630
  case 'min':
1289
1631
  case 'max': {
1290
- const b0 = evalScriptNode(args[1], vars, n);
1632
+ const b0 = evalScriptNode(args[1], vars, n, bars);
1291
1633
  const b = Array.isArray(b0) ? b0 : new Array(n).fill(b0);
1292
1634
  return a.map((x, i) => (name === 'min' ? Math.min(scriptNum(x), scriptNum(b[i])) : Math.max(scriptNum(x), scriptNum(b[i]))));
1293
1635
  }
1294
1636
  case 'crossup':
1295
1637
  case 'crossdown': {
1296
- const b0 = evalScriptNode(args[1], vars, n);
1638
+ const b0 = evalScriptNode(args[1], vars, n, bars);
1297
1639
  const b = Array.isArray(b0) ? b0 : new Array(n).fill(b0);
1298
1640
  const out = new Array(n).fill(0);
1299
1641
  for (let i = 1; i < n; i++) {
@@ -1331,7 +1673,7 @@ export function evalScript(compiled, bars) {
1331
1673
  hlc3: bars.map((b) => (b.high + b.low + b.close) / 3),
1332
1674
  ohlc4: bars.map((b) => (b.open + b.high + b.low + b.close) / 4),
1333
1675
  };
1334
- const res = evalScriptNode(c.ast, vars, n);
1676
+ const res = evalScriptNode(c.ast, vars, n, bars);
1335
1677
  const arr = Array.isArray(res) ? res : new Array(n).fill(res);
1336
1678
  for (let i = 0; i < n; i++) {
1337
1679
  const v = arr[i];
@@ -2395,7 +2737,7 @@ export const AI_TOOLS = [
2395
2737
  {
2396
2738
  tool: 'set_indicators',
2397
2739
  description:
2398
- 'Replace the indicators. Tokens: sma:20 ema:50 bb:20 vwap rsi:14 macd:12/26/9 volume, @hexcolor suffixes, or WickScript expressions like expr:{close - sma(close,20)} / pexpr:{rsi(close,14)}. Empty string clears all.',
2740
+ 'Replace the indicators. Tokens: sma:20 ema:50 bb:20 vwap supertrend:10/3 donchian:20 keltner:20 rsi:14 macd:12/26/9 stoch:14/3 atr:14 obv cci:20 wr:14 volume, @hexcolor suffixes, or WickScript expressions like expr:{close - sma(close,20)} / pexpr:{rsi(close,14)}. Empty string clears all.',
2399
2741
  args: { indicators: 'string — space/comma-separated tokens' },
2400
2742
  },
2401
2743
  {
package/src/wick-chart.js CHANGED
@@ -2331,6 +2331,8 @@ class WickChart extends HTMLElementBase {
2331
2331
  const fmtV = (v) =>
2332
2332
  entry.def.fmt === 'fixed1'
2333
2333
  ? numberFmt(1).format(v)
2334
+ : entry.def.fmt === 'compact'
2335
+ ? fmtCompact(v)
2334
2336
  : numberFmt(this._prec(scale.rawHi || 1)).format(v);
2335
2337
 
2336
2338
  // pane scale (fixed range or autoscaled from visible values)
@@ -2455,13 +2457,19 @@ class WickChart extends HTMLElementBase {
2455
2457
  ctx.lineWidth = 1;
2456
2458
  ctx.restore();
2457
2459
 
2458
- // right-axis labels for guide levels
2460
+ // right-axis labels: guide levels, or the pane's own min/max when
2461
+ // an autoscaled pane has no guides (atr / obv / pexpr)
2459
2462
  ctx.font = axisFont(400);
2460
2463
  ctx.fillStyle = pal.text;
2461
2464
  ctx.textAlign = 'right';
2462
2465
  ctx.textBaseline = 'middle';
2463
- for (const g of entry.def.guides || []) {
2464
- ctx.fillText(fmtV(g), W - 6, pyOf(g));
2466
+ if ((entry.def.guides || []).length) {
2467
+ for (const g of entry.def.guides) {
2468
+ ctx.fillText(fmtV(g), W - 6, pyOf(g));
2469
+ }
2470
+ } else {
2471
+ ctx.fillText(fmtV(pmax), W - 6, pyOf(pmax) + 6);
2472
+ if (pmax !== pmin) ctx.fillText(fmtV(pmin), W - 6, pyOf(pmin) - 6);
2465
2473
  }
2466
2474
 
2467
2475
  // pane label + live values (script panes show their expression label)
@@ -2580,6 +2588,8 @@ class WickChart extends HTMLElementBase {
2580
2588
  const fmtV =
2581
2589
  paneUnder.entry.def.fmt === 'fixed1'
2582
2590
  ? (v) => v.toFixed(1)
2591
+ : paneUnder.entry.def.fmt === 'compact'
2592
+ ? (v) => fmtCompact(v)
2583
2593
  : (v) => f.format(v);
2584
2594
  this._pill(
2585
2595
  plotRight + 2,
package/types/core.d.ts CHANGED
@@ -294,6 +294,91 @@ export declare function calcMACD(closes: number[], fast?: number, slow?: number,
294
294
  signal: Array<number | null>;
295
295
  hist: Array<number | null>;
296
296
  };
297
+ /** True range: max(h−l, |h−prev close|, |l−prev close|); first bar is h−l.
298
+ * @param {Bar[]} bars
299
+ * @returns {Array<number|null>}
300
+ */
301
+ export declare function calcTrueRange(bars: Bar[]): Array<number | null>;
302
+ /**
303
+ * Average True Range (Wilder smoothing; seeded with the SMA of the first
304
+ * `period` true ranges).
305
+ * @param {Bar[]} bars
306
+ * @param {number} period
307
+ * @returns {Array<number|null>}
308
+ */
309
+ export declare function calcATR(bars: Bar[], period?: number): Array<number | null>;
310
+ /**
311
+ * Volume-weighted average price over the hlc3 typical price, anchored to
312
+ * each UTC day (resets at the session boundary).
313
+ * @param {Bar[]} bars
314
+ * @returns {Array<number|null>}
315
+ */
316
+ export declare function calcVWAP(bars: Bar[]): Array<number | null>;
317
+ /**
318
+ * On-balance volume: cumulative volume signed by close-to-close direction.
319
+ * @param {Bar[]} bars
320
+ * @returns {Array<number|null>}
321
+ */
322
+ export declare function calcOBV(bars: Bar[]): Array<number | null>;
323
+ /**
324
+ * Stochastic oscillator (slow): raw %K over `period`, smoothed by `smooth`;
325
+ * %D is the SMA of %K.
326
+ * @param {Bar[]} bars
327
+ * @param {number} period
328
+ * @param {number} smooth
329
+ * @returns {{k:Array<number|null>, d:Array<number|null>}}
330
+ */
331
+ export declare function calcStoch(bars: Bar[], period?: number, smooth?: number): {
332
+ k: Array<number | null>;
333
+ d: Array<number | null>;
334
+ };
335
+ /**
336
+ * Commodity Channel Index: typical price vs its SMA, scaled by mean deviation.
337
+ * @param {Bar[]} bars
338
+ * @param {number} period
339
+ * @returns {Array<number|null>}
340
+ */
341
+ export declare function calcCCI(bars: Bar[], period?: number): Array<number | null>;
342
+ /**
343
+ * Williams %R: −100 at the period low, 0 at the period high.
344
+ * @param {Bar[]} bars
345
+ * @param {number} period
346
+ * @returns {Array<number|null>}
347
+ */
348
+ export declare function calcWilliamsR(bars: Bar[], period?: number): Array<number | null>;
349
+ /**
350
+ * Donchian channels: highest high / lowest low over `period`, plus mid.
351
+ * @param {Bar[]} bars
352
+ * @param {number} period
353
+ * @returns {{upper:Array<number|null>, mid:Array<number|null>, lower:Array<number|null>}}
354
+ */
355
+ export declare function calcDonchian(bars: Bar[], period?: number): {
356
+ upper: Array<number | null>;
357
+ mid: Array<number | null>;
358
+ lower: Array<number | null>;
359
+ };
360
+ /**
361
+ * Keltner channels: EMA mid ± mult × ATR.
362
+ * @param {Bar[]} bars
363
+ * @param {number} period
364
+ * @param {number} [mult]
365
+ * @returns {{upper:Array<number|null>, mid:Array<number|null>, lower:Array<number|null>}}
366
+ */
367
+ export declare function calcKeltner(bars: Bar[], period?: number, mult?: number): {
368
+ upper: Array<number | null>;
369
+ mid: Array<number | null>;
370
+ lower: Array<number | null>;
371
+ };
372
+ /**
373
+ * SuperTrend: ATR bands that flip with the trend. Returns the trend line
374
+ * (support in uptrends, resistance in downtrends) with a one-bar null gap
375
+ * at flips so the renderer breaks the line.
376
+ * @param {Bar[]} bars
377
+ * @param {number} period
378
+ * @param {number} [mult]
379
+ * @returns {Array<number|null>}
380
+ */
381
+ export declare function calcSuperTrend(bars: Bar[], period?: number, mult?: number): Array<number | null>;
297
382
  /**
298
383
  * Merge older (backfilled) bars in front of `existing`.
299
384
  * Dedupes by time (existing bars win); only strictly older bars are prepended.
@@ -435,11 +520,12 @@ export declare const BUILTIN_INDICATORS: Map<string, {
435
520
  fast?: undefined;
436
521
  slow?: undefined;
437
522
  signal?: undefined;
523
+ smooth?: undefined;
438
524
  };
439
525
  compute: (bars: any, p: any) => number[];
440
- range?: undefined;
441
526
  color?: undefined;
442
527
  guides?: undefined;
528
+ range?: undefined;
443
529
  fmt?: undefined;
444
530
  } | {
445
531
  kind: string;
@@ -449,11 +535,27 @@ export declare const BUILTIN_INDICATORS: Map<string, {
449
535
  fast?: undefined;
450
536
  slow?: undefined;
451
537
  signal?: undefined;
538
+ smooth?: undefined;
452
539
  };
453
540
  compute: (bars: any, p: any) => number[];
541
+ color?: undefined;
542
+ guides?: undefined;
454
543
  range?: undefined;
544
+ fmt?: undefined;
545
+ } | {
546
+ kind: string;
547
+ params: {
548
+ mult?: undefined;
549
+ fast?: undefined;
550
+ slow?: undefined;
551
+ signal?: undefined;
552
+ smooth?: undefined;
553
+ period?: undefined;
554
+ };
555
+ compute: (bars: any) => number[];
455
556
  color?: undefined;
456
557
  guides?: undefined;
558
+ range?: undefined;
457
559
  fmt?: undefined;
458
560
  } | {
459
561
  kind: string;
@@ -463,6 +565,22 @@ export declare const BUILTIN_INDICATORS: Map<string, {
463
565
  fast?: undefined;
464
566
  slow?: undefined;
465
567
  signal?: undefined;
568
+ smooth?: undefined;
569
+ };
570
+ compute: (bars: any, p: any) => number[];
571
+ color?: undefined;
572
+ guides?: undefined;
573
+ range?: undefined;
574
+ fmt?: undefined;
575
+ } | {
576
+ kind: string;
577
+ params: {
578
+ period: number;
579
+ mult?: undefined;
580
+ fast?: undefined;
581
+ slow?: undefined;
582
+ signal?: undefined;
583
+ smooth?: undefined;
466
584
  };
467
585
  compute: (bars: any, p: any) => {
468
586
  lines: {
@@ -470,9 +588,49 @@ export declare const BUILTIN_INDICATORS: Map<string, {
470
588
  values: number[];
471
589
  }[];
472
590
  };
591
+ color?: undefined;
592
+ guides?: undefined;
473
593
  range?: undefined;
594
+ fmt?: undefined;
595
+ } | {
596
+ kind: string;
597
+ params: {
598
+ period: number;
599
+ mult: number;
600
+ fast?: undefined;
601
+ slow?: undefined;
602
+ signal?: undefined;
603
+ smooth?: undefined;
604
+ };
605
+ compute: (bars: any, p: any) => {
606
+ lines: {
607
+ name: string;
608
+ values: number[];
609
+ }[];
610
+ };
474
611
  color?: undefined;
475
612
  guides?: undefined;
613
+ range?: undefined;
614
+ fmt?: undefined;
615
+ } | {
616
+ kind: string;
617
+ params: {
618
+ period: number;
619
+ mult: number;
620
+ fast?: undefined;
621
+ slow?: undefined;
622
+ signal?: undefined;
623
+ smooth?: undefined;
624
+ };
625
+ compute: (bars: any, p: any) => {
626
+ lines: {
627
+ name: string;
628
+ values: number[];
629
+ }[];
630
+ };
631
+ color?: undefined;
632
+ guides?: undefined;
633
+ range?: undefined;
476
634
  fmt?: undefined;
477
635
  } | {
478
636
  kind: string;
@@ -482,6 +640,7 @@ export declare const BUILTIN_INDICATORS: Map<string, {
482
640
  fast?: undefined;
483
641
  slow?: undefined;
484
642
  signal?: undefined;
643
+ smooth?: undefined;
485
644
  };
486
645
  guides: number[];
487
646
  range: number[];
@@ -489,15 +648,15 @@ export declare const BUILTIN_INDICATORS: Map<string, {
489
648
  color: string;
490
649
  compute: (bars: any, p: any) => number[];
491
650
  } | {
492
- range?: undefined;
493
651
  color?: undefined;
494
652
  kind: string;
495
653
  params: {
496
654
  mult?: undefined;
497
- period?: undefined;
498
655
  fast: number;
499
656
  slow: number;
500
657
  signal: number;
658
+ smooth?: undefined;
659
+ period?: undefined;
501
660
  };
502
661
  guides: number[];
503
662
  fmt: string;
@@ -508,6 +667,87 @@ export declare const BUILTIN_INDICATORS: Map<string, {
508
667
  }[];
509
668
  histogram: number[];
510
669
  };
670
+ range?: undefined;
671
+ } | {
672
+ color?: undefined;
673
+ kind: string;
674
+ params: {
675
+ mult?: undefined;
676
+ fast?: undefined;
677
+ slow?: undefined;
678
+ signal?: undefined;
679
+ period: number;
680
+ smooth?: undefined;
681
+ };
682
+ fmt: string;
683
+ compute: (bars: any, p: any) => number[];
684
+ guides?: undefined;
685
+ range?: undefined;
686
+ } | {
687
+ color?: undefined;
688
+ kind: string;
689
+ params: {
690
+ mult?: undefined;
691
+ fast?: undefined;
692
+ slow?: undefined;
693
+ signal?: undefined;
694
+ period: number;
695
+ smooth: number;
696
+ };
697
+ guides: number[];
698
+ range: number[];
699
+ fmt: string;
700
+ compute: (bars: any, p: any) => {
701
+ lines: {
702
+ name: string;
703
+ values: number[];
704
+ }[];
705
+ };
706
+ } | {
707
+ color?: undefined;
708
+ kind: string;
709
+ params: {
710
+ mult?: undefined;
711
+ fast?: undefined;
712
+ slow?: undefined;
713
+ signal?: undefined;
714
+ smooth?: undefined;
715
+ period?: undefined;
716
+ };
717
+ fmt: string;
718
+ compute: (bars: any) => number[];
719
+ guides?: undefined;
720
+ range?: undefined;
721
+ } | {
722
+ color?: undefined;
723
+ kind: string;
724
+ params: {
725
+ mult?: undefined;
726
+ fast?: undefined;
727
+ slow?: undefined;
728
+ signal?: undefined;
729
+ smooth?: undefined;
730
+ period: number;
731
+ };
732
+ guides: number[];
733
+ fmt: string;
734
+ compute: (bars: any, p: any) => number[];
735
+ range?: undefined;
736
+ } | {
737
+ color?: undefined;
738
+ kind: string;
739
+ params: {
740
+ mult?: undefined;
741
+ fast?: undefined;
742
+ slow?: undefined;
743
+ signal?: undefined;
744
+ smooth?: undefined;
745
+ period: number;
746
+ };
747
+ guides: number[];
748
+ range: number[];
749
+ fmt: string;
750
+ compute: (bars: any, p: any) => number[];
511
751
  }>;
512
752
  /**
513
753
  * Parse an `indicators` attribute string against a registry.
@@ -246,11 +246,12 @@ declare class WickChart extends HTMLElementBase {
246
246
  fast?: undefined;
247
247
  slow?: undefined;
248
248
  signal?: undefined;
249
+ smooth?: undefined;
249
250
  };
250
251
  compute: (bars: any, p: any) => number[];
251
- range?: undefined;
252
252
  color?: undefined;
253
253
  guides?: undefined;
254
+ range?: undefined;
254
255
  fmt?: undefined;
255
256
  } | {
256
257
  kind: string;
@@ -260,11 +261,27 @@ declare class WickChart extends HTMLElementBase {
260
261
  fast?: undefined;
261
262
  slow?: undefined;
262
263
  signal?: undefined;
264
+ smooth?: undefined;
263
265
  };
264
266
  compute: (bars: any, p: any) => number[];
267
+ color?: undefined;
268
+ guides?: undefined;
265
269
  range?: undefined;
270
+ fmt?: undefined;
271
+ } | {
272
+ kind: string;
273
+ params: {
274
+ mult?: undefined;
275
+ fast?: undefined;
276
+ slow?: undefined;
277
+ signal?: undefined;
278
+ smooth?: undefined;
279
+ period?: undefined;
280
+ };
281
+ compute: (bars: any) => number[];
266
282
  color?: undefined;
267
283
  guides?: undefined;
284
+ range?: undefined;
268
285
  fmt?: undefined;
269
286
  } | {
270
287
  kind: string;
@@ -274,6 +291,22 @@ declare class WickChart extends HTMLElementBase {
274
291
  fast?: undefined;
275
292
  slow?: undefined;
276
293
  signal?: undefined;
294
+ smooth?: undefined;
295
+ };
296
+ compute: (bars: any, p: any) => number[];
297
+ color?: undefined;
298
+ guides?: undefined;
299
+ range?: undefined;
300
+ fmt?: undefined;
301
+ } | {
302
+ kind: string;
303
+ params: {
304
+ period: number;
305
+ mult?: undefined;
306
+ fast?: undefined;
307
+ slow?: undefined;
308
+ signal?: undefined;
309
+ smooth?: undefined;
277
310
  };
278
311
  compute: (bars: any, p: any) => {
279
312
  lines: {
@@ -281,9 +314,49 @@ declare class WickChart extends HTMLElementBase {
281
314
  values: number[];
282
315
  }[];
283
316
  };
317
+ color?: undefined;
318
+ guides?: undefined;
284
319
  range?: undefined;
320
+ fmt?: undefined;
321
+ } | {
322
+ kind: string;
323
+ params: {
324
+ period: number;
325
+ mult: number;
326
+ fast?: undefined;
327
+ slow?: undefined;
328
+ signal?: undefined;
329
+ smooth?: undefined;
330
+ };
331
+ compute: (bars: any, p: any) => {
332
+ lines: {
333
+ name: string;
334
+ values: number[];
335
+ }[];
336
+ };
337
+ color?: undefined;
338
+ guides?: undefined;
339
+ range?: undefined;
340
+ fmt?: undefined;
341
+ } | {
342
+ kind: string;
343
+ params: {
344
+ period: number;
345
+ mult: number;
346
+ fast?: undefined;
347
+ slow?: undefined;
348
+ signal?: undefined;
349
+ smooth?: undefined;
350
+ };
351
+ compute: (bars: any, p: any) => {
352
+ lines: {
353
+ name: string;
354
+ values: number[];
355
+ }[];
356
+ };
285
357
  color?: undefined;
286
358
  guides?: undefined;
359
+ range?: undefined;
287
360
  fmt?: undefined;
288
361
  } | {
289
362
  kind: string;
@@ -293,6 +366,7 @@ declare class WickChart extends HTMLElementBase {
293
366
  fast?: undefined;
294
367
  slow?: undefined;
295
368
  signal?: undefined;
369
+ smooth?: undefined;
296
370
  };
297
371
  guides: number[];
298
372
  range: number[];
@@ -300,15 +374,15 @@ declare class WickChart extends HTMLElementBase {
300
374
  color: string;
301
375
  compute: (bars: any, p: any) => number[];
302
376
  } | {
303
- range?: undefined;
304
377
  color?: undefined;
305
378
  kind: string;
306
379
  params: {
307
380
  mult?: undefined;
308
- period?: undefined;
309
381
  fast: number;
310
382
  slow: number;
311
383
  signal: number;
384
+ smooth?: undefined;
385
+ period?: undefined;
312
386
  };
313
387
  guides: number[];
314
388
  fmt: string;
@@ -319,6 +393,87 @@ declare class WickChart extends HTMLElementBase {
319
393
  }[];
320
394
  histogram: number[];
321
395
  };
396
+ range?: undefined;
397
+ } | {
398
+ color?: undefined;
399
+ kind: string;
400
+ params: {
401
+ mult?: undefined;
402
+ fast?: undefined;
403
+ slow?: undefined;
404
+ signal?: undefined;
405
+ period: number;
406
+ smooth?: undefined;
407
+ };
408
+ fmt: string;
409
+ compute: (bars: any, p: any) => number[];
410
+ guides?: undefined;
411
+ range?: undefined;
412
+ } | {
413
+ color?: undefined;
414
+ kind: string;
415
+ params: {
416
+ mult?: undefined;
417
+ fast?: undefined;
418
+ slow?: undefined;
419
+ signal?: undefined;
420
+ period: number;
421
+ smooth: number;
422
+ };
423
+ guides: number[];
424
+ range: number[];
425
+ fmt: string;
426
+ compute: (bars: any, p: any) => {
427
+ lines: {
428
+ name: string;
429
+ values: number[];
430
+ }[];
431
+ };
432
+ } | {
433
+ color?: undefined;
434
+ kind: string;
435
+ params: {
436
+ mult?: undefined;
437
+ fast?: undefined;
438
+ slow?: undefined;
439
+ signal?: undefined;
440
+ smooth?: undefined;
441
+ period?: undefined;
442
+ };
443
+ fmt: string;
444
+ compute: (bars: any) => number[];
445
+ guides?: undefined;
446
+ range?: undefined;
447
+ } | {
448
+ color?: undefined;
449
+ kind: string;
450
+ params: {
451
+ mult?: undefined;
452
+ fast?: undefined;
453
+ slow?: undefined;
454
+ signal?: undefined;
455
+ smooth?: undefined;
456
+ period: number;
457
+ };
458
+ guides: number[];
459
+ fmt: string;
460
+ compute: (bars: any, p: any) => number[];
461
+ range?: undefined;
462
+ } | {
463
+ color?: undefined;
464
+ kind: string;
465
+ params: {
466
+ mult?: undefined;
467
+ fast?: undefined;
468
+ slow?: undefined;
469
+ signal?: undefined;
470
+ smooth?: undefined;
471
+ period: number;
472
+ };
473
+ guides: number[];
474
+ range: number[];
475
+ fmt: string;
476
+ compute: (bars: any, p: any) => number[];
322
477
  }>;
323
478
  /**
324
479
  * Register a custom indicator.