wickchart 1.4.0 → 1.6.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 +216 -9
- package/package.json +11 -4
- package/src/core.js +353 -11
- package/src/wick-chart.js +39 -5
- package/types/core.d.ts +243 -3
- package/types/wick-chart.d.ts +175 -4
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
|
{
|
package/src/wick-chart.js
CHANGED
|
@@ -1598,8 +1598,9 @@ class WickChart extends HTMLElementBase {
|
|
|
1598
1598
|
);
|
|
1599
1599
|
}
|
|
1600
1600
|
const timeH = 26;
|
|
1601
|
+
const dock = this._dockInset();
|
|
1601
1602
|
const plotRight = Math.max(30, W - priceW);
|
|
1602
|
-
const plotBottom = H - timeH;
|
|
1603
|
+
const plotBottom = H - timeH - dock;
|
|
1603
1604
|
const paneList = this._ind.panes;
|
|
1604
1605
|
const paneArea = paneList.length
|
|
1605
1606
|
? Math.min(
|
|
@@ -1622,6 +1623,7 @@ class WickChart extends HTMLElementBase {
|
|
|
1622
1623
|
plotBottom,
|
|
1623
1624
|
main: { y0: 0, y1: mainH, h: mainH },
|
|
1624
1625
|
panes,
|
|
1626
|
+
dock: dock > 0 ? { y0: H - dock, h: dock } : null,
|
|
1625
1627
|
});
|
|
1626
1628
|
|
|
1627
1629
|
/* background */
|
|
@@ -2331,6 +2333,8 @@ class WickChart extends HTMLElementBase {
|
|
|
2331
2333
|
const fmtV = (v) =>
|
|
2332
2334
|
entry.def.fmt === 'fixed1'
|
|
2333
2335
|
? numberFmt(1).format(v)
|
|
2336
|
+
: entry.def.fmt === 'compact'
|
|
2337
|
+
? fmtCompact(v)
|
|
2334
2338
|
: numberFmt(this._prec(scale.rawHi || 1)).format(v);
|
|
2335
2339
|
|
|
2336
2340
|
// pane scale (fixed range or autoscaled from visible values)
|
|
@@ -2455,13 +2459,19 @@ class WickChart extends HTMLElementBase {
|
|
|
2455
2459
|
ctx.lineWidth = 1;
|
|
2456
2460
|
ctx.restore();
|
|
2457
2461
|
|
|
2458
|
-
// right-axis labels
|
|
2462
|
+
// right-axis labels: guide levels, or the pane's own min/max when
|
|
2463
|
+
// an autoscaled pane has no guides (atr / obv / pexpr)
|
|
2459
2464
|
ctx.font = axisFont(400);
|
|
2460
2465
|
ctx.fillStyle = pal.text;
|
|
2461
2466
|
ctx.textAlign = 'right';
|
|
2462
2467
|
ctx.textBaseline = 'middle';
|
|
2463
|
-
|
|
2464
|
-
|
|
2468
|
+
if ((entry.def.guides || []).length) {
|
|
2469
|
+
for (const g of entry.def.guides) {
|
|
2470
|
+
ctx.fillText(fmtV(g), W - 6, pyOf(g));
|
|
2471
|
+
}
|
|
2472
|
+
} else {
|
|
2473
|
+
ctx.fillText(fmtV(pmax), W - 6, pyOf(pmax) + 6);
|
|
2474
|
+
if (pmax !== pmin) ctx.fillText(fmtV(pmin), W - 6, pyOf(pmin) - 6);
|
|
2465
2475
|
}
|
|
2466
2476
|
|
|
2467
2477
|
// pane label + live values (script panes show their expression label)
|
|
@@ -2580,6 +2590,8 @@ class WickChart extends HTMLElementBase {
|
|
|
2580
2590
|
const fmtV =
|
|
2581
2591
|
paneUnder.entry.def.fmt === 'fixed1'
|
|
2582
2592
|
? (v) => v.toFixed(1)
|
|
2593
|
+
: paneUnder.entry.def.fmt === 'compact'
|
|
2594
|
+
? (v) => fmtCompact(v)
|
|
2583
2595
|
: (v) => f.format(v);
|
|
2584
2596
|
this._pill(
|
|
2585
2597
|
plotRight + 2,
|
|
@@ -2932,7 +2944,13 @@ class WickChart extends HTMLElementBase {
|
|
|
2932
2944
|
* layer then receives that pointer's move/up/cancel events (plus a
|
|
2933
2945
|
* 'cancel' on Escape) and the chart suppresses its own pan/measure/brush
|
|
2934
2946
|
* for the duration.
|
|
2935
|
-
*
|
|
2947
|
+
*
|
|
2948
|
+
* A layer may also declare `insetBottom` (px, 0..160): the largest
|
|
2949
|
+
* declared inset reserves a docked strip at the very bottom of the
|
|
2950
|
+
* canvas — all chart content (panes + time axis) shrinks above it and
|
|
2951
|
+
* the strip is handed to layers as `api.layout.dock = { y0, h }`
|
|
2952
|
+
* (used by the wickchart-navigator plugin).
|
|
2953
|
+
* @param {{id?: string, draw: Function, onPointer?: Function, insetBottom?: number}} layer
|
|
2936
2954
|
* @returns {object|null} the normalized layer handle (with `id`), or null
|
|
2937
2955
|
* if the layer was rejected (no draw fn, or 16 layers already added)
|
|
2938
2956
|
*/
|
|
@@ -2946,6 +2964,10 @@ class WickChart extends HTMLElementBase {
|
|
|
2946
2964
|
const entry = {
|
|
2947
2965
|
id,
|
|
2948
2966
|
draw: layer.draw,
|
|
2967
|
+
insetBottom:
|
|
2968
|
+
typeof layer.insetBottom === 'number' && Number.isFinite(layer.insetBottom)
|
|
2969
|
+
? Math.max(0, Math.min(160, Math.round(layer.insetBottom)))
|
|
2970
|
+
: 0,
|
|
2949
2971
|
onPointer: typeof layer.onPointer === 'function' ? layer.onPointer : null,
|
|
2950
2972
|
};
|
|
2951
2973
|
const at = this._layers.findIndex((l) => l.id === id);
|
|
@@ -2997,6 +3019,18 @@ class WickChart extends HTMLElementBase {
|
|
|
2997
3019
|
}
|
|
2998
3020
|
}
|
|
2999
3021
|
|
|
3022
|
+
/**
|
|
3023
|
+
* Bottom space reserved by plugin layers: the largest declared
|
|
3024
|
+
* `insetBottom` (px, clamped 0..160 at addLayer time), or 0.
|
|
3025
|
+
*/
|
|
3026
|
+
_dockInset() {
|
|
3027
|
+
let dock = 0;
|
|
3028
|
+
for (const l of this._layers) {
|
|
3029
|
+
if (l.insetBottom > dock) dock = l.insetBottom;
|
|
3030
|
+
}
|
|
3031
|
+
return dock;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3000
3034
|
/** Ask layers, in order, whether one claims this pointerdown. */
|
|
3001
3035
|
_layerHit(e, pt) {
|
|
3002
3036
|
for (const layer of this._layers) {
|