wickchart 1.3.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 +83 -5
- package/package.json +11 -4
- package/src/core.js +353 -11
- package/src/wick-chart.js +257 -3
- package/types/core.d.ts +243 -3
- package/types/wick-chart.d.ts +233 -3
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('
|
|
272
|
+
WickChart.registerIndicator('cvwap', { // cumulative VWAP over the whole dataset
|
|
261
273
|
kind: 'overlay', // or 'pane'
|
|
262
|
-
params: { period: 20 }, // defaults; set via indicators="
|
|
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 = '
|
|
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
|
|
@@ -633,6 +646,67 @@ WickChart.registerIndicator('vwap', {
|
|
|
633
646
|
chart.indicators = 'vwap';
|
|
634
647
|
```
|
|
635
648
|
|
|
649
|
+
### Plugin layers — extend without forking
|
|
650
|
+
|
|
651
|
+
`addLayer()` is the whole extension surface: an external draw hook that paints
|
|
652
|
+
into the render pipeline (above chart content, under the crosshair) and can
|
|
653
|
+
claim pointer gestures so drags reach your code instead of panning the chart.
|
|
654
|
+
Four public coordinate transforms — `timeToX`/`xToTime` (extrapolating past
|
|
655
|
+
the last bar into future space) and `priceToY`/`yToPrice` — anchor your
|
|
656
|
+
content in data space so it rides along with zoom and pan:
|
|
657
|
+
|
|
658
|
+
```js
|
|
659
|
+
chart.addLayer({
|
|
660
|
+
id: 'flags',
|
|
661
|
+
draw(api) {
|
|
662
|
+
const x = api.timeToX(t), y = api.priceToY(p); // anchors, not pixels
|
|
663
|
+
api.ctx.fillStyle = api.palette.accent;
|
|
664
|
+
// …paint in CSS pixels
|
|
665
|
+
},
|
|
666
|
+
onPointer(ev) {
|
|
667
|
+
if (ev.type === 'down' && hitsMyContent(ev)) return true; // claim the drag
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
chart.requestDraw(); // repaint hook for interactive layers
|
|
671
|
+
chart.removeLayer('flags'); // detach by handle or id
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
A claimed gesture delivers `move`/`up` (and `cancel` on Escape) to the layer
|
|
675
|
+
while the chart suppresses pan/brush/measure. Markers, watermarks, signal
|
|
676
|
+
badges — or a whole drawing toolkit — plug in without the core growing a
|
|
677
|
+
single tool. The main entry is covered by a CI gzip budget (68 KB) so it
|
|
678
|
+
stays that way.
|
|
679
|
+
|
|
680
|
+
### Drawings — the `wickchart-draw` plugin
|
|
681
|
+
|
|
682
|
+
The first official plugin: TradingView-style drawing tools as opt-in bytes
|
|
683
|
+
(~8 KB gz, own CI budget). Trendlines (segment/ray), horizontal levels,
|
|
684
|
+
rectangles, fibonacci retracements and text — all plain `{ time, price }`
|
|
685
|
+
data that rides zoom & pan, survives reloads, extrapolates into future
|
|
686
|
+
space, and serializes to JSON. Anchors magnet-snap to bar times and OHLC.
|
|
687
|
+
|
|
688
|
+
```js
|
|
689
|
+
npm install wickchart wickchart-draw // drawings are a separate opt-in package
|
|
690
|
+
|
|
691
|
+
import { attachDrawings } from 'wickchart-draw';
|
|
692
|
+
|
|
693
|
+
const draw = attachDrawings(chart);
|
|
694
|
+
draw.setTool('trendline'); // drag to draw; setTool(null) = select/move mode
|
|
695
|
+
draw.getDrawings(); // → JSON array (save it); setDrawings(saved)
|
|
696
|
+
draw.undo(); draw.clear();
|
|
697
|
+
draw.setShare(true); // shared drawings: draw on one tab, appears on all
|
|
698
|
+
chart.addEventListener('wick:drawings', (e) => save(e.detail.drawings));
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Select mode: click a drawing to select it, drag to move, drag the square
|
|
702
|
+
handles to re-anchor, `Delete` removes, `Esc` cancels a gesture; clicks on
|
|
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).
|
|
709
|
+
|
|
636
710
|
## Methods
|
|
637
711
|
|
|
638
712
|
| Method | Description |
|
|
@@ -647,6 +721,10 @@ chart.indicators = 'vwap';
|
|
|
647
721
|
| `getDataWindow()` | → AI-ready summary of the visible window (see below) |
|
|
648
722
|
| `getState()` | → serializable snapshot (type, indicators, view, positions, alerts) |
|
|
649
723
|
| `setState(state)` | Apply a snapshot; a pending view applies after the next `setData()` |
|
|
724
|
+
| `addLayer(layer)` / `removeLayer(idOrHandle)` | Register/detach a plugin layer (draw hook + optional pointer claim) |
|
|
725
|
+
| `requestDraw()` | Repaint on the next frame (interactive layers) |
|
|
726
|
+
| `timeToX(t)` / `xToTime(x)` | Bar time ⇄ x-pixel; extrapolates into future space |
|
|
727
|
+
| `priceToY(p)` / `yToPrice(y)` | Price ⇄ y-pixel in the main pane (log-aware) |
|
|
650
728
|
|
|
651
729
|
### Infinite history (`loadMore`)
|
|
652
730
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wickchart",
|
|
3
|
-
"version": "1.
|
|
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",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"dev": "npx --yes serve . -l 5173",
|
|
45
|
-
"test": "node --test \"tests/*.test.mjs\"",
|
|
45
|
+
"test": "node --test \"tests/*.test.mjs\" \"plugins/draw/tests/*.test.mjs\"",
|
|
46
46
|
"build:types": "node -e \"require('fs').rmSync('types', { recursive: true, force: true });\" && tsc -p tsconfig.json",
|
|
47
47
|
"prepack": "npm run build:types",
|
|
48
|
-
"ci": "npm run build:types && npm test && node --check src/wick-chart.js && node --check src/wick-feed.js && node --check src/core.js && node --check src/react.js && node --check src/react-core.js && node --check demo/app.js"
|
|
48
|
+
"ci": "npm run build:types && npm test && node --check src/wick-chart.js && node --check src/wick-feed.js && node --check src/core.js && node --check src/react.js && node --check src/react-core.js && node --check demo/app.js && node --check plugins/draw/core.mjs && node --check plugins/draw/draw.mjs"
|
|
49
49
|
},
|
|
50
50
|
"keywords": [
|
|
51
51
|
"chart",
|
|
@@ -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
|
-
|
|
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
|
{
|