wickchart 0.3.0 → 1.0.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 +175 -40
- package/package.json +12 -12
- package/src/core.js +734 -7
- package/src/feeds.js +1 -1
- package/src/{hab-chart.js → wick-chart.js} +211 -94
- package/src/{hab-feed.js → wick-feed.js} +34 -19
- package/types/core.d.ts +108 -1
- package/types/{hab-chart.d.ts → wick-chart.d.ts} +106 -7
- package/types/{hab-feed.d.ts → wick-feed.d.ts} +8 -4
package/src/feeds.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
*
|
|
2
|
+
* WickChart feeds — data-source helpers shared by <wick-feed> and the demo app.
|
|
3
3
|
* Browser module (uses fetch/WebSocket inside functions); importable in Node
|
|
4
4
|
* for unit-testing the pure generators.
|
|
5
5
|
* MIT License.
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
* <
|
|
2
|
+
* <wick-chart> — a modern, dependency-free financial charting web component.
|
|
3
3
|
*
|
|
4
|
-
* <
|
|
5
|
-
* </
|
|
4
|
+
* <wick-chart label="BTC · 1h" type="candles" indicators="sma:20 volume">
|
|
5
|
+
* </wick-chart>
|
|
6
6
|
* <script type="module">
|
|
7
|
-
* const chart = document.querySelector('
|
|
7
|
+
* const chart = document.querySelector('wick-chart');
|
|
8
8
|
* chart.setData(bars); // [{ time, open, high, low, close, volume }]
|
|
9
9
|
* chart.update(bar); // streaming update / append
|
|
10
10
|
* </script>
|
|
11
11
|
*
|
|
12
12
|
* Zero dependencies. Canvas-rendered. Framework-agnostic (works in React,
|
|
13
|
-
* Vue,
|
|
13
|
+
* Vue, plain HTML). Themeable with --wick-* CSS custom properties.
|
|
14
|
+
*
|
|
15
|
+
* The 0.x names (<hab-chart>, --hab-*, hab:* events) still work as
|
|
16
|
+
* deprecated aliases — see the README migration notes.
|
|
14
17
|
*
|
|
15
18
|
* MIT License.
|
|
16
19
|
* ========================================================================== */
|
|
@@ -24,19 +27,25 @@ import {
|
|
|
24
27
|
positionPnl, checkAlertCross, computeStats, safeColor,
|
|
25
28
|
SERIES_TYPES, calcHeikinAshi, buildColumns, computeVolumeProfile,
|
|
26
29
|
calcRSI, detectAnnotations, priceToFreq,
|
|
30
|
+
calcRealizedVol, volRegimeBands, percentileOfSorted, parseVolShading,
|
|
31
|
+
windowSummary,
|
|
27
32
|
} from './core.js';
|
|
28
33
|
|
|
29
34
|
/* ------------------------------------------------------------------ *
|
|
30
|
-
* <
|
|
35
|
+
* <wick-chart>
|
|
31
36
|
* ------------------------------------------------------------------ */
|
|
32
37
|
|
|
38
|
+
/** Indicator registry — module scope so registrations are shared by
|
|
39
|
+
* <wick-chart> and the deprecated <hab-chart> alias element. */
|
|
40
|
+
const REGISTRY = new Map(BUILTIN_INDICATORS);
|
|
41
|
+
|
|
33
42
|
/* SSR safety: importing this module under Node (Next.js/Nuxt server render)
|
|
34
43
|
* must not throw — the element simply registers only in browsers. */
|
|
35
44
|
const HTMLElementBase = typeof HTMLElement !== 'undefined' ? HTMLElement : class {};
|
|
36
45
|
|
|
37
|
-
class
|
|
46
|
+
class WickChart extends HTMLElementBase {
|
|
38
47
|
static get observedAttributes() {
|
|
39
|
-
return ['theme', 'type', 'log', 'auto', 'indicators', 'precision', 'label', 'stats', 'profile', 'annotations', 'co-view', 'sonify'];
|
|
48
|
+
return ['theme', 'type', 'log', 'auto', 'indicators', 'precision', 'label', 'stats', 'profile', 'annotations', 'volshading', 'co-view', 'sonify'];
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
constructor() {
|
|
@@ -53,7 +62,7 @@ class HabChart extends HTMLElementBase {
|
|
|
53
62
|
contain: content;
|
|
54
63
|
}
|
|
55
64
|
:host(:focus-visible) {
|
|
56
|
-
outline: 2px solid var(--hab-accent, #4c8dff);
|
|
65
|
+
outline: 2px solid var(--wick-accent, var(--hab-accent, #4c8dff));
|
|
57
66
|
outline-offset: -2px;
|
|
58
67
|
}
|
|
59
68
|
.wrap { position: absolute; inset: 0; overflow: hidden; }
|
|
@@ -76,26 +85,26 @@ class HabChart extends HTMLElementBase {
|
|
|
76
85
|
}
|
|
77
86
|
.legend .row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
|
|
78
87
|
.legend .sym {
|
|
79
|
-
color: var(--hab-text-strong, #e6edf3);
|
|
88
|
+
color: var(--wick-text-strong, var(--hab-text-strong, #e6edf3));
|
|
80
89
|
font-weight: 700;
|
|
81
90
|
font-size: 13px;
|
|
82
91
|
letter-spacing: 0.02em;
|
|
83
92
|
}
|
|
84
93
|
.legend .kv { display: inline-flex; gap: 5px; align-items: baseline; white-space: nowrap; }
|
|
85
|
-
.legend .k { color: var(--hab-text, #8b949e); font-size: 11px; }
|
|
86
|
-
.legend .v { color: var(--hab-text-strong, #e6edf3); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
94
|
+
.legend .k { color: var(--wick-text, var(--hab-text, #8b949e)); font-size: 11px; }
|
|
95
|
+
.legend .v { color: var(--wick-text-strong, var(--hab-text-strong, #e6edf3)); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
87
96
|
.legend .pct { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
88
|
-
.legend .up { color: var(--hab-up, #16c784); }
|
|
89
|
-
.legend .dn { color: var(--hab-down, #ea3943); }
|
|
97
|
+
.legend .up { color: var(--wick-up, var(--hab-up, #16c784)); }
|
|
98
|
+
.legend .dn { color: var(--wick-down, var(--hab-down, #ea3943)); }
|
|
90
99
|
.legend .ind {
|
|
91
100
|
display: inline-flex; align-items: center; gap: 6px;
|
|
92
|
-
color: var(--hab-text, #8b949e); font-size: 11.5px; white-space: nowrap;
|
|
101
|
+
color: var(--wick-text, var(--hab-text, #8b949e)); font-size: 11.5px; white-space: nowrap;
|
|
93
102
|
}
|
|
94
103
|
.legend .ind i { width: 8px; height: 2.5px; border-radius: 2px; display: inline-block; }
|
|
95
104
|
.legend .ind .v { font-size: 12px; }
|
|
96
105
|
.legend .insight {
|
|
97
|
-
color: var(--hab-accent, #4c8dff);
|
|
98
|
-
background: var(--hab-chip, rgba(127, 137, 153, 0.12));
|
|
106
|
+
color: var(--wick-accent, var(--hab-accent, #4c8dff));
|
|
107
|
+
background: var(--wick-chip, var(--hab-chip, rgba(127, 137, 153, 0.12)));
|
|
99
108
|
border-radius: 6px;
|
|
100
109
|
padding: 1px 8px;
|
|
101
110
|
font-size: 11.5px;
|
|
@@ -104,7 +113,7 @@ class HabChart extends HTMLElementBase {
|
|
|
104
113
|
.nodata {
|
|
105
114
|
position: absolute; inset: 0;
|
|
106
115
|
display: flex; align-items: center; justify-content: center;
|
|
107
|
-
color: var(--hab-text, #8b949e);
|
|
116
|
+
color: var(--wick-text, var(--hab-text, #8b949e));
|
|
108
117
|
font: 500 13px ${FONT_STACK};
|
|
109
118
|
pointer-events: none;
|
|
110
119
|
}
|
|
@@ -117,25 +126,25 @@ class HabChart extends HTMLElementBase {
|
|
|
117
126
|
}
|
|
118
127
|
.hud .pos {
|
|
119
128
|
display: inline-flex; gap: 9px; align-items: baseline; white-space: nowrap;
|
|
120
|
-
background: var(--hab-chip, rgba(127, 137, 153, 0.12));
|
|
121
|
-
border: 1px solid var(--hab-border, rgba(148, 163, 184, 0.2));
|
|
129
|
+
background: var(--wick-chip, var(--hab-chip, rgba(127, 137, 153, 0.12)));
|
|
130
|
+
border: 1px solid var(--wick-border, var(--hab-border, rgba(148, 163, 184, 0.2)));
|
|
122
131
|
border-radius: 7px;
|
|
123
132
|
padding: 3px 9px;
|
|
124
133
|
}
|
|
125
134
|
.hud .statsrow {
|
|
126
135
|
display: inline-flex; gap: 12px; white-space: nowrap;
|
|
127
|
-
background: var(--hab-chip, rgba(127, 137, 153, 0.12));
|
|
128
|
-
border: 1px solid var(--hab-border, rgba(148, 163, 184, 0.2));
|
|
136
|
+
background: var(--wick-chip, var(--hab-chip, rgba(127, 137, 153, 0.12)));
|
|
137
|
+
border: 1px solid var(--wick-border, var(--hab-border, rgba(148, 163, 184, 0.2)));
|
|
129
138
|
border-radius: 7px;
|
|
130
139
|
padding: 3px 10px;
|
|
131
|
-
color: var(--hab-text, #8b949e);
|
|
140
|
+
color: var(--wick-text, var(--hab-text, #8b949e));
|
|
132
141
|
font-variant-numeric: tabular-nums;
|
|
133
142
|
}
|
|
134
|
-
.hud .statsrow b { color: var(--hab-text-strong, #e6edf3); font-weight: 600; }
|
|
135
|
-
.hud .k { color: var(--hab-text, #8b949e); font-weight: 500; }
|
|
136
|
-
.hud .v { color: var(--hab-text-strong, #e6edf3); font-variant-numeric: tabular-nums; }
|
|
137
|
-
.hud .up { color: var(--hab-up, #16c784); }
|
|
138
|
-
.hud .dn { color: var(--hab-down, #ea3943); }
|
|
143
|
+
.hud .statsrow b { color: var(--wick-text-strong, var(--hab-text-strong, #e6edf3)); font-weight: 600; }
|
|
144
|
+
.hud .k { color: var(--wick-text, var(--hab-text, #8b949e)); font-weight: 500; }
|
|
145
|
+
.hud .v { color: var(--wick-text-strong, var(--hab-text-strong, #e6edf3)); font-variant-numeric: tabular-nums; }
|
|
146
|
+
.hud .up { color: var(--wick-up, var(--hab-up, #16c784)); }
|
|
147
|
+
.hud .dn { color: var(--wick-down, var(--hab-down, #ea3943)); }
|
|
139
148
|
</style>
|
|
140
149
|
<div class="wrap" part="wrap">
|
|
141
150
|
<canvas part="canvas" role="img"></canvas>
|
|
@@ -184,6 +193,7 @@ class HabChart extends HTMLElementBase {
|
|
|
184
193
|
this._annotations = false;
|
|
185
194
|
this._annoKey = '';
|
|
186
195
|
this._annoList = null;
|
|
196
|
+
this._volshade = null;
|
|
187
197
|
|
|
188
198
|
// cross-tab co-view state
|
|
189
199
|
this._coviewName = null;
|
|
@@ -302,7 +312,7 @@ class HabChart extends HTMLElementBase {
|
|
|
302
312
|
this._label = val || '';
|
|
303
313
|
break;
|
|
304
314
|
case 'indicators':
|
|
305
|
-
this._ind = parseIndicators(val,
|
|
315
|
+
this._ind = parseIndicators(val, WickChart._registry());
|
|
306
316
|
break;
|
|
307
317
|
case 'stats':
|
|
308
318
|
this._stats = val != null && val !== 'false';
|
|
@@ -316,6 +326,10 @@ class HabChart extends HTMLElementBase {
|
|
|
316
326
|
this._annotations = val != null && val !== 'false';
|
|
317
327
|
this._annoKey = '';
|
|
318
328
|
break;
|
|
329
|
+
case 'volshading':
|
|
330
|
+
this._volshade = val != null && val !== 'false' ? parseVolShading(val) : null;
|
|
331
|
+
this._legendKey = '';
|
|
332
|
+
break;
|
|
319
333
|
case 'co-view':
|
|
320
334
|
this._coviewName = val || null;
|
|
321
335
|
this._setupCoView();
|
|
@@ -332,20 +346,15 @@ class HabChart extends HTMLElementBase {
|
|
|
332
346
|
* Indicator registry
|
|
333
347
|
* ------------------------------------------------------------ */
|
|
334
348
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
/** Lazily-built registry, seeded with the built-in indicators. */
|
|
349
|
+
/** Indicator registry (module scope — shared with the legacy alias tag). */
|
|
338
350
|
static _registry() {
|
|
339
|
-
|
|
340
|
-
HabChart._registryMap = new Map(BUILTIN_INDICATORS);
|
|
341
|
-
}
|
|
342
|
-
return HabChart._registryMap;
|
|
351
|
+
return REGISTRY;
|
|
343
352
|
}
|
|
344
353
|
|
|
345
354
|
/**
|
|
346
355
|
* Register a custom indicator.
|
|
347
356
|
*
|
|
348
|
-
*
|
|
357
|
+
* WickChart.registerIndicator('vwap', {
|
|
349
358
|
* kind: 'overlay', // or 'pane'
|
|
350
359
|
* params: { period: 20 }, // defaults; settable via name:period
|
|
351
360
|
* compute(bars, params) { // bars: normalized {time,o,h,l,c,v}
|
|
@@ -366,7 +375,7 @@ class HabChart extends HTMLElementBase {
|
|
|
366
375
|
if (!def || typeof def.compute !== 'function') {
|
|
367
376
|
throw new Error('registerIndicator: def.compute must be a function');
|
|
368
377
|
}
|
|
369
|
-
|
|
378
|
+
REGISTRY.set(name.toLowerCase(), {
|
|
370
379
|
kind: def.kind === 'pane' ? 'pane' : 'overlay',
|
|
371
380
|
...def,
|
|
372
381
|
});
|
|
@@ -396,7 +405,7 @@ class HabChart extends HTMLElementBase {
|
|
|
396
405
|
}
|
|
397
406
|
const norm = [];
|
|
398
407
|
for (const b of bars) {
|
|
399
|
-
const nb =
|
|
408
|
+
const nb = WickChart._normBar(b);
|
|
400
409
|
if (nb) norm.push(nb);
|
|
401
410
|
}
|
|
402
411
|
// skip the O(n log n) sort when already ascending (typical for feeds)
|
|
@@ -425,7 +434,7 @@ class HabChart extends HTMLElementBase {
|
|
|
425
434
|
* @param {import('./core.js').Bar} bar
|
|
426
435
|
*/
|
|
427
436
|
update(bar) {
|
|
428
|
-
const b =
|
|
437
|
+
const b = WickChart._normBar(bar);
|
|
429
438
|
if (!b) return;
|
|
430
439
|
const d = this._data;
|
|
431
440
|
const last = d[d.length - 1];
|
|
@@ -485,7 +494,7 @@ class HabChart extends HTMLElementBase {
|
|
|
485
494
|
}
|
|
486
495
|
const older = [];
|
|
487
496
|
for (const b of bars) {
|
|
488
|
-
const nb =
|
|
497
|
+
const nb = WickChart._normBar(b);
|
|
489
498
|
if (nb) older.push(nb);
|
|
490
499
|
}
|
|
491
500
|
const { bars: merged, added } = mergeOlderData(this._data, older);
|
|
@@ -533,14 +542,14 @@ class HabChart extends HTMLElementBase {
|
|
|
533
542
|
setVisibleRange(range) {
|
|
534
543
|
const d = this._data;
|
|
535
544
|
if (!d.length || !range || !this._ly) return;
|
|
536
|
-
const from =
|
|
537
|
-
const to =
|
|
538
|
-
let i0 =
|
|
539
|
-
let i1 =
|
|
545
|
+
const from = WickChart._timeToMs(range.from);
|
|
546
|
+
const to = WickChart._timeToMs(range.to);
|
|
547
|
+
let i0 = WickChart._indexForTime(d, from);
|
|
548
|
+
let i1 = WickChart._indexForTime(d, to);
|
|
540
549
|
if (i0 > i1) [i0, i1] = [i1, i0];
|
|
541
550
|
if (i1 - i0 < 2) return;
|
|
542
551
|
const { plotRight } = this._ly;
|
|
543
|
-
this._view.spacing = clamp(plotRight / (i1 - i0), this._minSpacing(),
|
|
552
|
+
this._view.spacing = clamp(plotRight / (i1 - i0), this._minSpacing(), WickChart._MAX_SP);
|
|
544
553
|
this._view.rightIndex = i1;
|
|
545
554
|
this._auto = false;
|
|
546
555
|
this._clampView();
|
|
@@ -553,6 +562,41 @@ class HabChart extends HTMLElementBase {
|
|
|
553
562
|
return this._canvas.toDataURL('image/png');
|
|
554
563
|
}
|
|
555
564
|
|
|
565
|
+
/**
|
|
566
|
+
* AI-ready summary of the visible window: structured fields plus a
|
|
567
|
+
* ready-to-paste markdown rendering (`text`). Computed locally —
|
|
568
|
+
* nothing leaves the page until the user copies it somewhere.
|
|
569
|
+
* @returns {object|null}
|
|
570
|
+
*/
|
|
571
|
+
getDataWindow() {
|
|
572
|
+
const d = this._data;
|
|
573
|
+
if (!d.length || !this._ly) return null;
|
|
574
|
+
const { plotRight } = this._ly;
|
|
575
|
+
const { rightIndex, spacing } = this._view;
|
|
576
|
+
const i0 = Math.max(0, Math.round(rightIndex - plotRight / spacing));
|
|
577
|
+
const i1 = clamp(Math.round(rightIndex), 0, d.length - 1);
|
|
578
|
+
const s = windowSummary(d, i0, i1, { dtMs: this._dt, label: this._label });
|
|
579
|
+
if (!s) return null;
|
|
580
|
+
// snapshot active indicator values at the right edge (scripts show their expression)
|
|
581
|
+
const f = numberFmt(this._prec(d[i1].close));
|
|
582
|
+
const snap = [];
|
|
583
|
+
for (const entry of this._ind.overlays.concat(this._ind.panes)) {
|
|
584
|
+
if (entry.name === 'volume') continue;
|
|
585
|
+
const res = this._indicatorSeries(entry);
|
|
586
|
+
for (const ln of res.lines) {
|
|
587
|
+
const v = ln.values[i1];
|
|
588
|
+
if (!isNum(v)) continue;
|
|
589
|
+
const isScript = entry.name === 'expr' || entry.name === 'pexpr';
|
|
590
|
+
const name = isScript
|
|
591
|
+
? ln.name || entry.name
|
|
592
|
+
: entry.name + (Object.keys(entry.params).length ? ':' + Object.values(entry.params).join('/') : '');
|
|
593
|
+
snap.push(`${name} = ${f.format(v)}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (snap.length) s.text += `\n- Indicators: ${snap.join('; ')}.`;
|
|
597
|
+
return s;
|
|
598
|
+
}
|
|
599
|
+
|
|
556
600
|
/* ------------------------------------------------------------ *
|
|
557
601
|
* State serialization
|
|
558
602
|
* ------------------------------------------------------------ */
|
|
@@ -575,6 +619,9 @@ class HabChart extends HTMLElementBase {
|
|
|
575
619
|
stats: this._stats,
|
|
576
620
|
profile: this._profile,
|
|
577
621
|
annotations: this._annotations,
|
|
622
|
+
volshading: this._volshade
|
|
623
|
+
? `${this._volshade.p1}/${this._volshade.p2}`
|
|
624
|
+
: false,
|
|
578
625
|
indicators: ind.join(' '),
|
|
579
626
|
view: range ? { from: range.from, to: range.to } : null,
|
|
580
627
|
positions: this._positions.map((p) => ({
|
|
@@ -599,6 +646,8 @@ class HabChart extends HTMLElementBase {
|
|
|
599
646
|
if (typeof state.stats === 'boolean') this.setAttribute('stats', String(state.stats));
|
|
600
647
|
if (typeof state.profile === 'boolean') this.setAttribute('profile', String(state.profile));
|
|
601
648
|
if (typeof state.annotations === 'boolean') this.setAttribute('annotations', String(state.annotations));
|
|
649
|
+
if (state.volshading === true) this.setAttribute('volshading', 'true');
|
|
650
|
+
else if (typeof state.volshading === 'string' && state.volshading) this.setAttribute('volshading', state.volshading);
|
|
602
651
|
if (typeof state.label === 'string') this.setAttribute('label', state.label);
|
|
603
652
|
if (typeof state.indicators === 'string') {
|
|
604
653
|
this.setAttribute('indicators', state.indicators);
|
|
@@ -678,7 +727,8 @@ class HabChart extends HTMLElementBase {
|
|
|
678
727
|
}
|
|
679
728
|
|
|
680
729
|
/**
|
|
681
|
-
* Price alert. Fires `
|
|
730
|
+
* Price alert. Fires `wick:alert` ({id, price, bar}) on an edge crossing
|
|
731
|
+
* (plus the deprecated `hab:alert` alias)
|
|
682
732
|
* during streaming updates.
|
|
683
733
|
* @param {{id?: string, price: number, direction?: 'above'|'below'|'cross',
|
|
684
734
|
* once?: boolean}} alert
|
|
@@ -717,9 +767,7 @@ class HabChart extends HTMLElementBase {
|
|
|
717
767
|
if (a.fired) continue;
|
|
718
768
|
if (checkAlertCross(a, prevClose, bar.close)) {
|
|
719
769
|
a.fired = true;
|
|
720
|
-
this.
|
|
721
|
-
new CustomEvent('hab:alert', { detail: { id: a.id, price: a.price, bar } })
|
|
722
|
-
);
|
|
770
|
+
this._fire('alert', { id: a.id, price: a.price, bar });
|
|
723
771
|
if (a.once) this._alerts = this._alerts.filter((x) => x !== a);
|
|
724
772
|
}
|
|
725
773
|
}
|
|
@@ -761,7 +809,7 @@ class HabChart extends HTMLElementBase {
|
|
|
761
809
|
if (!b) return null;
|
|
762
810
|
const t = b.time != null ? b.time : b.t;
|
|
763
811
|
if (!isNum(t)) return null;
|
|
764
|
-
const time =
|
|
812
|
+
const time = WickChart._timeToMs(t);
|
|
765
813
|
const close = isNum(b.close) ? b.close : isNum(b.value) ? b.value : NaN;
|
|
766
814
|
if (!isNum(close)) return null;
|
|
767
815
|
const open = isNum(b.open) ? b.open : close;
|
|
@@ -840,8 +888,9 @@ class HabChart extends HTMLElementBase {
|
|
|
840
888
|
if (this._pal && this._palKey === this._theme) return this._pal;
|
|
841
889
|
const base = THEMES[this._theme] || THEMES.dark;
|
|
842
890
|
const cs = getComputedStyle(this);
|
|
891
|
+
// --wick-* is canonical; --hab-* still honored as the 0.x fallback
|
|
843
892
|
const get = (name, fallback) => {
|
|
844
|
-
const v = cs.getPropertyValue('--hab-' + name).trim();
|
|
893
|
+
const v = cs.getPropertyValue('--wick-' + name).trim() || cs.getPropertyValue('--hab-' + name).trim();
|
|
845
894
|
return v || fallback;
|
|
846
895
|
};
|
|
847
896
|
const pal = {};
|
|
@@ -852,7 +901,8 @@ class HabChart extends HTMLElementBase {
|
|
|
852
901
|
o.push(get('overlay-' + i, base.overlay[i]));
|
|
853
902
|
}
|
|
854
903
|
// allow single overlay color
|
|
855
|
-
const single =
|
|
904
|
+
const single =
|
|
905
|
+
cs.getPropertyValue('--wick-overlay').trim() || cs.getPropertyValue('--hab-overlay').trim();
|
|
856
906
|
pal.overlay = single ? base.overlay.map(() => single) : o;
|
|
857
907
|
} else {
|
|
858
908
|
pal[k] = get(k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()), base[k]);
|
|
@@ -889,6 +939,23 @@ class HabChart extends HTMLElementBase {
|
|
|
889
939
|
return this._cache.map.__rsi14;
|
|
890
940
|
}
|
|
891
941
|
|
|
942
|
+
/** Volatility-regime data (realized vol + percentile bands), cached per data version. */
|
|
943
|
+
_volShadeCache() {
|
|
944
|
+
if (!this._volshade) return null;
|
|
945
|
+
if (this._cache.v !== this._version) {
|
|
946
|
+
this._cache = { v: this._version, map: {} };
|
|
947
|
+
}
|
|
948
|
+
if (!this._cache.map.__volshade) {
|
|
949
|
+
const closes = this._data.map((b) => b.close);
|
|
950
|
+
const vol = calcRealizedVol(closes, this._volshade.period);
|
|
951
|
+
this._cache.map.__volshade = {
|
|
952
|
+
vol,
|
|
953
|
+
...volRegimeBands(vol, this._volshade.p1, this._volshade.p2),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
return this._cache.map.__volshade;
|
|
957
|
+
}
|
|
958
|
+
|
|
892
959
|
/** Compute (and cache per data version) an indicator entry's series. */
|
|
893
960
|
_indicatorSeries(entry) {
|
|
894
961
|
if (this._cache.v !== this._version) {
|
|
@@ -954,7 +1021,7 @@ class HabChart extends HTMLElementBase {
|
|
|
954
1021
|
if (!d.length || !this._ly) return;
|
|
955
1022
|
const { plotRight } = this._ly;
|
|
956
1023
|
const target = Math.min(d.length, 150);
|
|
957
|
-
this._view.spacing = clamp(plotRight / target, this._minSpacing(),
|
|
1024
|
+
this._view.spacing = clamp(plotRight / target, this._minSpacing(), WickChart._MAX_SP);
|
|
958
1025
|
this._view.rightIndex = d.length - 1 + this._rightMargin();
|
|
959
1026
|
}
|
|
960
1027
|
|
|
@@ -963,7 +1030,7 @@ class HabChart extends HTMLElementBase {
|
|
|
963
1030
|
const ly = this._ly;
|
|
964
1031
|
if (!d.length || !ly) return;
|
|
965
1032
|
const v = this._view;
|
|
966
|
-
v.spacing = clamp(v.spacing, this._minSpacing(),
|
|
1033
|
+
v.spacing = clamp(v.spacing, this._minSpacing(), WickChart._MAX_SP);
|
|
967
1034
|
const visible = ly.plotRight / v.spacing;
|
|
968
1035
|
const maxRight = d.length - 1 + Math.max(6, visible * 0.5);
|
|
969
1036
|
const minRight = Math.min(2, d.length - 1);
|
|
@@ -1288,6 +1355,32 @@ class HabChart extends HTMLElementBase {
|
|
|
1288
1355
|
}
|
|
1289
1356
|
ctx.stroke();
|
|
1290
1357
|
|
|
1358
|
+
/* volatility-regime shading (behind everything but the grid) */
|
|
1359
|
+
if (this._volshade) {
|
|
1360
|
+
const vs = this._volShadeCache();
|
|
1361
|
+
if (vs) {
|
|
1362
|
+
const flushRun = (val, a, b) => {
|
|
1363
|
+
if (val !== 0 && val !== 2) return;
|
|
1364
|
+
const xa = clamp(this._xFor(a) - sp * 0.5, 0, plotRight);
|
|
1365
|
+
const xb = clamp(this._xFor(b) + sp * 0.5, 0, plotRight);
|
|
1366
|
+
if (xb <= xa) return;
|
|
1367
|
+
ctx.fillStyle = val === 0 ? hexToRgba(pal.accent, 0.05) : hexToRgba(pal.down, 0.07);
|
|
1368
|
+
ctx.fillRect(xa, main.y0, xb - xa, main.h);
|
|
1369
|
+
};
|
|
1370
|
+
let runVal = -2;
|
|
1371
|
+
let runA = i0;
|
|
1372
|
+
for (let i = i0; i <= i1; i++) {
|
|
1373
|
+
const rv = vs.regimes[i] == null ? -1 : vs.regimes[i];
|
|
1374
|
+
if (rv !== runVal) {
|
|
1375
|
+
flushRun(runVal, runA, i - 1);
|
|
1376
|
+
runVal = rv;
|
|
1377
|
+
runA = i;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
flushRun(runVal, runA, i1);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1291
1384
|
/* position zones (under series) */
|
|
1292
1385
|
for (const pos of this._positions) {
|
|
1293
1386
|
const yE = clamp(yOf(pos.entry), main.y0, main.y1);
|
|
@@ -1367,9 +1460,7 @@ class HabChart extends HTMLElementBase {
|
|
|
1367
1460
|
this._annoList = detectAnnotations(this._data, i0, i1, this._cachedRSI14());
|
|
1368
1461
|
this._annoKey = akey;
|
|
1369
1462
|
this._legendKey = ''; // legend may now show insights at the hovered bar
|
|
1370
|
-
this.
|
|
1371
|
-
new CustomEvent('hab:annotations', { detail: { annotations: this._annoList } })
|
|
1372
|
-
);
|
|
1463
|
+
this._fire('annotations', { annotations: this._annoList });
|
|
1373
1464
|
}
|
|
1374
1465
|
const A = this._annoList;
|
|
1375
1466
|
if (A.length) {
|
|
@@ -1825,21 +1916,21 @@ class HabChart extends HTMLElementBase {
|
|
|
1825
1916
|
ctx.fillText(fmtV(g), W - 6, pyOf(g));
|
|
1826
1917
|
}
|
|
1827
1918
|
|
|
1828
|
-
// pane label + live values
|
|
1919
|
+
// pane label + live values (script panes show their expression label)
|
|
1829
1920
|
const hi = this._hover ? clamp(this._hover.index, 0, d.length - 1) : d.length - 1;
|
|
1830
1921
|
const vals = res.lines
|
|
1831
1922
|
.map((ln) => (isNum(ln.values[hi]) ? fmtV(ln.values[hi]) : '—'))
|
|
1832
1923
|
.join(' ');
|
|
1924
|
+
const paneLabel =
|
|
1925
|
+
(entry.name === 'expr' || entry.name === 'pexpr') && res.lines[0] && res.lines[0].name
|
|
1926
|
+
? res.lines[0].name
|
|
1927
|
+
: `${entry.name.toUpperCase()} ${Object.values(entry.params).join(' ')}`;
|
|
1833
1928
|
ctx.font = axisFont(600);
|
|
1834
1929
|
ctx.fillStyle = pal.text;
|
|
1835
1930
|
ctx.textAlign = 'left';
|
|
1836
1931
|
ctx.textBaseline = 'top';
|
|
1837
1932
|
ctx.globalAlpha = 0.9;
|
|
1838
|
-
ctx.fillText(
|
|
1839
|
-
`${entry.name.toUpperCase()} ${Object.values(entry.params).join(' ')}${vals ? ' ' + vals : ''}`,
|
|
1840
|
-
8,
|
|
1841
|
-
pr.y0 + 5
|
|
1842
|
-
);
|
|
1933
|
+
ctx.fillText(`${paneLabel}${vals ? ' ' + vals : ''}`, 8, pr.y0 + 5);
|
|
1843
1934
|
ctx.globalAlpha = 1;
|
|
1844
1935
|
}
|
|
1845
1936
|
|
|
@@ -2103,7 +2194,8 @@ class HabChart extends HTMLElementBase {
|
|
|
2103
2194
|
const idx = clamp(hoverIdx, 0, d.length - 1);
|
|
2104
2195
|
const key = [
|
|
2105
2196
|
idx, this._version, this._type, this._label, this._theme,
|
|
2106
|
-
this.getAttribute('indicators'), this.
|
|
2197
|
+
this.getAttribute('indicators'), this.getAttribute('volshading'),
|
|
2198
|
+
this._positions.length, this._posVersion || 0,
|
|
2107
2199
|
].join('|');
|
|
2108
2200
|
if (key === this._legendKey) return;
|
|
2109
2201
|
this._legendKey = key;
|
|
@@ -2143,12 +2235,29 @@ class HabChart extends HTMLElementBase {
|
|
|
2143
2235
|
const vals = res.lines
|
|
2144
2236
|
.map((ln) => (isNum(ln.values[idx]) ? f.format(ln.values[idx]) : '—'))
|
|
2145
2237
|
.join(' ');
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2238
|
+
// script indicators carry their expression in the line name; built-ins show name+params
|
|
2239
|
+
const label =
|
|
2240
|
+
(entry.name === 'expr' || entry.name === 'pexpr') && res.lines[0].name
|
|
2241
|
+
? esc(res.lines[0].name)
|
|
2242
|
+
: `${entry.name.toUpperCase()} ${Object.values(entry.params).join(' ')}`;
|
|
2243
|
+
html += `<div class="row"><span class="ind"><i style="background:${dotColor}"></i>${label}</span><span class="v">${vals}</span></div>`;
|
|
2150
2244
|
});
|
|
2151
2245
|
|
|
2246
|
+
if (this._volshade) {
|
|
2247
|
+
const vs = this._volShadeCache();
|
|
2248
|
+
const rv = vs ? vs.regimes[idx] : -1;
|
|
2249
|
+
if (vs && rv >= 0) {
|
|
2250
|
+
const pct = percentileOfSorted(vs.sorted, vs.vol[idx]);
|
|
2251
|
+
const name = rv === 0 ? 'calm' : rv === 2 ? 'hot' : 'normal';
|
|
2252
|
+
const dot = rv === 0 ? palNow.accent : rv === 2 ? palNow.down : palNow.text;
|
|
2253
|
+
html +=
|
|
2254
|
+
`<div class="row"><span class="ind">` +
|
|
2255
|
+
`<i style="background:${dot}"></i>VOL ${this._volshade.p1}/${this._volshade.p2} · ${name}` +
|
|
2256
|
+
`${isNum(pct) ? ` · ${pct.toFixed(0)}%ile` : ''}` +
|
|
2257
|
+
`</span></div>`;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2152
2261
|
if (this._annotations && this._annoList) {
|
|
2153
2262
|
const notes = this._annoList.filter((a) => a.i === idx).map((a) => a.note);
|
|
2154
2263
|
if (notes.length) {
|
|
@@ -2245,7 +2354,7 @@ class HabChart extends HTMLElementBase {
|
|
|
2245
2354
|
const s = clamp(
|
|
2246
2355
|
(this._pinch.spacing * dist) / this._pinch.dist,
|
|
2247
2356
|
this._minSpacing(),
|
|
2248
|
-
|
|
2357
|
+
WickChart._MAX_SP
|
|
2249
2358
|
);
|
|
2250
2359
|
this._view.spacing = s;
|
|
2251
2360
|
this._view.rightIndex = this._pinch.idxAtMid + (ly.plotRight - mid.x) / s;
|
|
@@ -2298,15 +2407,11 @@ class HabChart extends HTMLElementBase {
|
|
|
2298
2407
|
const d = this._data;
|
|
2299
2408
|
const barA = d[clamp(m.iA, 0, d.length - 1)];
|
|
2300
2409
|
const barB = d[clamp(m.iB, 0, d.length - 1)];
|
|
2301
|
-
this.
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
bars: Math.abs(m.iB - m.iA),
|
|
2307
|
-
},
|
|
2308
|
-
})
|
|
2309
|
-
);
|
|
2410
|
+
this._fire('measure', {
|
|
2411
|
+
from: { index: m.iA, time: barA.time, price: m.pA },
|
|
2412
|
+
to: { index: m.iB, time: barB.time, price: m.pB },
|
|
2413
|
+
bars: Math.abs(m.iB - m.iA),
|
|
2414
|
+
});
|
|
2310
2415
|
}
|
|
2311
2416
|
} else if (this._pan && had && !this._pan.moved && this._data.length) {
|
|
2312
2417
|
if (this._measure) {
|
|
@@ -2318,11 +2423,7 @@ class HabChart extends HTMLElementBase {
|
|
|
2318
2423
|
const pt = this._localPoint(e);
|
|
2319
2424
|
const idx = clamp(Math.round(this._indexForX(pt.x)), 0, this._data.length - 1);
|
|
2320
2425
|
const price = this._yToPrice(pt.y);
|
|
2321
|
-
this.
|
|
2322
|
-
new CustomEvent('hab:select', {
|
|
2323
|
-
detail: { index: idx, bar: this._data[idx], price },
|
|
2324
|
-
})
|
|
2325
|
-
);
|
|
2426
|
+
this._fire('select', { index: idx, bar: this._data[idx], price });
|
|
2326
2427
|
}
|
|
2327
2428
|
}
|
|
2328
2429
|
this._pan = null;
|
|
@@ -2359,7 +2460,7 @@ class HabChart extends HTMLElementBase {
|
|
|
2359
2460
|
|
|
2360
2461
|
const factor = Math.exp(-dy * (e.ctrlKey ? 0.008 : 0.0016));
|
|
2361
2462
|
const oldSp = this._view.spacing;
|
|
2362
|
-
const newSp = clamp(oldSp * factor, this._minSpacing(),
|
|
2463
|
+
const newSp = clamp(oldSp * factor, this._minSpacing(), WickChart._MAX_SP);
|
|
2363
2464
|
if (newSp === oldSp) return;
|
|
2364
2465
|
const idxAtCursor = this._indexForX(pt.x);
|
|
2365
2466
|
this._view.spacing = newSp;
|
|
@@ -2396,12 +2497,12 @@ class HabChart extends HTMLElementBase {
|
|
|
2396
2497
|
this._invalidate();
|
|
2397
2498
|
this._emitRange();
|
|
2398
2499
|
} else if (key === '+' || key === '=') {
|
|
2399
|
-
this._view.spacing = clamp(this._view.spacing * 1.25, this._minSpacing(),
|
|
2500
|
+
this._view.spacing = clamp(this._view.spacing * 1.25, this._minSpacing(), WickChart._MAX_SP);
|
|
2400
2501
|
this._clampView();
|
|
2401
2502
|
this._invalidate();
|
|
2402
2503
|
this._emitRange();
|
|
2403
2504
|
} else if (key === '-' || key === '_') {
|
|
2404
|
-
this._view.spacing = clamp(this._view.spacing / 1.25, this._minSpacing(),
|
|
2505
|
+
this._view.spacing = clamp(this._view.spacing / 1.25, this._minSpacing(), WickChart._MAX_SP);
|
|
2405
2506
|
this._clampView();
|
|
2406
2507
|
this._invalidate();
|
|
2407
2508
|
this._emitRange();
|
|
@@ -2528,7 +2629,7 @@ class HabChart extends HTMLElementBase {
|
|
|
2528
2629
|
price: this._yToPrice(hover.y),
|
|
2529
2630
|
};
|
|
2530
2631
|
}
|
|
2531
|
-
this.
|
|
2632
|
+
this._fire('crosshair', detail);
|
|
2532
2633
|
|
|
2533
2634
|
// co-view: share the pointer with peer charts (leave events bypass throttle)
|
|
2534
2635
|
if (this._coviewCh) {
|
|
@@ -2570,7 +2671,7 @@ class HabChart extends HTMLElementBase {
|
|
|
2570
2671
|
if (!name || !this._connected || typeof BroadcastChannel === 'undefined') return;
|
|
2571
2672
|
if (!this._coviewPeer) this._coviewPeer = 'p' + Math.random().toString(36).slice(2, 8);
|
|
2572
2673
|
try {
|
|
2573
|
-
const ch = new BroadcastChannel('
|
|
2674
|
+
const ch = new BroadcastChannel('wick-co-view:' + name);
|
|
2574
2675
|
ch.onmessage = (ev) => this._onCoMessage(ev.data);
|
|
2575
2676
|
this._coviewCh = ch;
|
|
2576
2677
|
} catch (_) {}
|
|
@@ -2595,7 +2696,7 @@ class HabChart extends HTMLElementBase {
|
|
|
2595
2696
|
}
|
|
2596
2697
|
if (!isNum(m.time) || !this._data.length) return;
|
|
2597
2698
|
this._ghost = {
|
|
2598
|
-
index:
|
|
2699
|
+
index: WickChart._indexForTime(this._data, m.time),
|
|
2599
2700
|
yFrac: isNum(m.yFrac) ? clamp(m.yFrac, 0, 1) : null,
|
|
2600
2701
|
at: Date.now(),
|
|
2601
2702
|
};
|
|
@@ -2607,15 +2708,31 @@ class HabChart extends HTMLElementBase {
|
|
|
2607
2708
|
this._invalidate();
|
|
2608
2709
|
}
|
|
2609
2710
|
|
|
2711
|
+
/** Dispatch `wick:name` (canonical) plus the deprecated `hab:name` alias,
|
|
2712
|
+
* so 0.x listeners keep working until 2.0. */
|
|
2713
|
+
_fire(name, detail) {
|
|
2714
|
+
this.dispatchEvent(new CustomEvent('wick:' + name, { detail }));
|
|
2715
|
+
this.dispatchEvent(new CustomEvent('hab:' + name, { detail }));
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2610
2718
|
_emitRange() {
|
|
2611
2719
|
const r = this.getVisibleRange();
|
|
2612
2720
|
if (!r) return;
|
|
2613
|
-
this.
|
|
2721
|
+
this._fire('range', r);
|
|
2614
2722
|
}
|
|
2615
2723
|
}
|
|
2616
2724
|
|
|
2617
|
-
if (typeof customElements !== 'undefined'
|
|
2618
|
-
customElements.
|
|
2725
|
+
if (typeof customElements !== 'undefined') {
|
|
2726
|
+
if (!customElements.get('wick-chart')) {
|
|
2727
|
+
customElements.define('wick-chart', WickChart);
|
|
2728
|
+
}
|
|
2729
|
+
// 0.x alias: same element under its old tag name (deprecated, removed in 2.0)
|
|
2730
|
+
if (!customElements.get('hab-chart')) {
|
|
2731
|
+
/** @deprecated use <wick-chart> */
|
|
2732
|
+
class HabChart extends WickChart {}
|
|
2733
|
+
customElements.define('hab-chart', HabChart);
|
|
2734
|
+
}
|
|
2619
2735
|
}
|
|
2620
2736
|
|
|
2621
|
-
export default
|
|
2737
|
+
export default WickChart;
|
|
2738
|
+
export { WickChart };
|