wickchart 1.6.0 → 1.7.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 +198 -5
- package/package.json +17 -4
- package/src/core.js +3012 -2882
- package/src/feeds.js +333 -1
- package/src/react-core.js +7 -1
- package/src/report.js +309 -0
- package/src/wick-chart.js +553 -52
- package/src/wick-feed.js +213 -2
- package/src/worker-core.js +89 -0
- package/src/worker.js +128 -0
- package/types/core.d.ts +60 -17
- package/types/feeds.d.ts +165 -0
- package/types/report.d.ts +114 -0
- package/types/wick-chart.d.ts +108 -5
- package/types/wick-feed.d.ts +19 -0
- package/types/worker-core.d.ts +11 -0
- package/types/worker.d.ts +36 -0
package/src/wick-feed.js
CHANGED
|
@@ -30,6 +30,15 @@ import {
|
|
|
30
30
|
openBinanceSocket,
|
|
31
31
|
tfToSeconds,
|
|
32
32
|
BASE_PRICES,
|
|
33
|
+
parseAggregate,
|
|
34
|
+
aggregateTrades,
|
|
35
|
+
genSyntheticTrades,
|
|
36
|
+
makeSynthTradeStream,
|
|
37
|
+
synthTradesPerBar,
|
|
38
|
+
fetchBinanceAggTrades,
|
|
39
|
+
fetchBinanceAggTradesSince,
|
|
40
|
+
openBinanceTradeSocket,
|
|
41
|
+
normalizeTrades,
|
|
33
42
|
} from './feeds.js';
|
|
34
43
|
|
|
35
44
|
const LIVE_TICK_MS = 650;
|
|
@@ -38,7 +47,7 @@ const HTMLElementBase = typeof HTMLElement !== 'undefined' ? HTMLElement : class
|
|
|
38
47
|
|
|
39
48
|
class WickFeed extends HTMLElementBase {
|
|
40
49
|
static get observedAttributes() {
|
|
41
|
-
return ['for', 'binance', 'demo', 'url', 'tf', 'limit', 'poll', 'live'];
|
|
50
|
+
return ['for', 'binance', 'demo', 'url', 'tf', 'limit', 'poll', 'live', 'aggregate'];
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
constructor() {
|
|
@@ -127,9 +136,19 @@ class WickFeed extends HTMLElementBase {
|
|
|
127
136
|
});
|
|
128
137
|
return;
|
|
129
138
|
}
|
|
139
|
+
// aggregate="tick:200|volume:50|dollar:25000" switches the feed from
|
|
140
|
+
// time bars to information-based bars built client-side from raw trades
|
|
141
|
+
const agg = parseAggregate(this.getAttribute('aggregate'));
|
|
142
|
+
if (this.hasAttribute('aggregate') && !agg) {
|
|
143
|
+
this._setStatus('error', { message: 'aggregate must be tick|volume|dollar[:N]' });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
130
146
|
if (!chart.hasAttribute('label')) {
|
|
131
147
|
const sym = this.getAttribute('binance') || this.getAttribute('demo');
|
|
132
|
-
if (sym)
|
|
148
|
+
if (sym) {
|
|
149
|
+
const sub = agg ? `${agg.kind}:${String(+(+agg.threshold).toFixed(6))} bars` : (this.getAttribute('tf') || '1h');
|
|
150
|
+
chart.setAttribute('label', `${String(sym).toUpperCase()} · ${sub}`);
|
|
151
|
+
}
|
|
133
152
|
}
|
|
134
153
|
|
|
135
154
|
const gen = this._gen;
|
|
@@ -140,6 +159,14 @@ class WickFeed extends HTMLElementBase {
|
|
|
140
159
|
const url = this.getAttribute('url');
|
|
141
160
|
const demo = this.getAttribute('demo');
|
|
142
161
|
|
|
162
|
+
if (agg) {
|
|
163
|
+
if (sym) this._binanceTrades(gen, chart, String(sym).toUpperCase(), agg, limit, live);
|
|
164
|
+
else if (url) this._restTrades(gen, chart, url, agg, limit, live);
|
|
165
|
+
else if (demo != null) {
|
|
166
|
+
this._syntheticTrades(gen, chart, demo === '' ? 'DEMO' : demo, agg, limit, live);
|
|
167
|
+
} else this._setStatus('idle');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
143
170
|
if (sym) this._binance(gen, chart, String(sym).toUpperCase(), tfId, limit, live);
|
|
144
171
|
else if (url) this._rest(gen, chart, url, limit, live);
|
|
145
172
|
else if (demo != null) {
|
|
@@ -220,6 +247,180 @@ class WickFeed extends HTMLElementBase {
|
|
|
220
247
|
this._synthetic(gen, chart, sym, tfId, limit, live, 'fallback');
|
|
221
248
|
}
|
|
222
249
|
|
|
250
|
+
/* ---------------- aggregate sources (information-based bars) ------------- */
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Wrap chart.update for aggregated bars: the chart keys bars by timestamp,
|
|
254
|
+
* and two groups can close inside the same millisecond, so emitted times
|
|
255
|
+
* are nudged +1ms to stay strictly increasing (keeps the one-bar-per-
|
|
256
|
+
* timestamp integrity invariant; display-only, values are untouched).
|
|
257
|
+
*/
|
|
258
|
+
_aggEmit(gen, chart) {
|
|
259
|
+
let last = -Infinity;
|
|
260
|
+
return (bar) => {
|
|
261
|
+
if (!bar || this._gen !== gen || !this.isConnected) return;
|
|
262
|
+
if (bar.time <= last) bar = { ...bar, time: last + 1 };
|
|
263
|
+
last = bar.time;
|
|
264
|
+
chart.update(bar);
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Seed + stream one aggregator onto the chart (shared by all sources). */
|
|
269
|
+
_aggAttach(gen, chart, res, bars, limit, status) {
|
|
270
|
+
chart.setData([...bars.slice(-limit), ...(res.pending ? [res.pending] : [])].slice(-limit));
|
|
271
|
+
this._setStatus(status);
|
|
272
|
+
return {
|
|
273
|
+
emit: this._aggEmit(gen, chart),
|
|
274
|
+
aggregator: res.aggregator,
|
|
275
|
+
/** Push one print through; emits the closed bar, then the forming one. */
|
|
276
|
+
push(t) {
|
|
277
|
+
const closed = res.aggregator.add(t);
|
|
278
|
+
if (closed) this.emit(closed);
|
|
279
|
+
this.emit(res.aggregator.current());
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_syntheticTrades(gen, chart, key, agg, limit, live, status = 'live') {
|
|
285
|
+
const base = BASE_PRICES[key.toUpperCase()] || 100;
|
|
286
|
+
const perBar = synthTradesPerBar(agg, base);
|
|
287
|
+
const seed = Math.max(2000, Math.min(48000, Math.ceil(limit * perBar) * 2));
|
|
288
|
+
const full = aggregateTrades(genSyntheticTrades(`${key}:agg`, seed, base), agg.kind, agg.threshold);
|
|
289
|
+
const bars = full.bars;
|
|
290
|
+
chart.onloadmore = (fromTime) => bars.filter((b) => b.time < fromTime).slice(-limit);
|
|
291
|
+
const handle = this._aggAttach(gen, chart, full, bars, limit, status);
|
|
292
|
+
if (!live) return;
|
|
293
|
+
const lastPrice = (full.pending || bars[bars.length - 1] || { close: base }).close;
|
|
294
|
+
const next = makeSynthTradeStream(lastPrice);
|
|
295
|
+
const timer = setInterval(() => {
|
|
296
|
+
if (this._gen !== gen || !this.isConnected) return;
|
|
297
|
+
const n = 2 + ((Math.random() * 6) | 0); // a burst of prints per tick
|
|
298
|
+
for (let i = 0; i < n; i++) handle.push(next());
|
|
299
|
+
}, LIVE_TICK_MS);
|
|
300
|
+
this._closers.push(() => clearInterval(timer));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async _binanceTrades(gen, chart, sym, agg, limit, live) {
|
|
304
|
+
this._setStatus('loading');
|
|
305
|
+
try {
|
|
306
|
+
// prints-per-bar is instrument-specific — size the seed from a first page
|
|
307
|
+
const first = await fetchBinanceAggTrades(sym, 1000, 1);
|
|
308
|
+
let perBar = agg.kind === 'tick' ? agg.threshold : 40;
|
|
309
|
+
if (agg.kind !== 'tick' && first.trades.length) {
|
|
310
|
+
let vol = 0;
|
|
311
|
+
let notl = 0;
|
|
312
|
+
for (const t of first.trades) {
|
|
313
|
+
vol += t.size;
|
|
314
|
+
notl += t.price * t.size;
|
|
315
|
+
}
|
|
316
|
+
const n = first.trades.length;
|
|
317
|
+
perBar = agg.kind === 'volume'
|
|
318
|
+
? agg.threshold / Math.max(1e-12, vol / n)
|
|
319
|
+
: agg.threshold / Math.max(1e-12, notl / n);
|
|
320
|
+
}
|
|
321
|
+
let trades = first.trades;
|
|
322
|
+
let oldestId = first.oldestId;
|
|
323
|
+
const target = Math.max(1000, Math.min(25000, Math.ceil(limit * perBar)));
|
|
324
|
+
if (trades.length < target) {
|
|
325
|
+
const older = await fetchBinanceAggTrades(sym, target - trades.length, 25, oldestId);
|
|
326
|
+
trades = older.trades.concat(trades);
|
|
327
|
+
if (older.oldestId != null) oldestId = older.oldestId;
|
|
328
|
+
}
|
|
329
|
+
if (this._gen !== gen || !this.isConnected) return;
|
|
330
|
+
const res = aggregateTrades(trades, agg.kind, agg.threshold);
|
|
331
|
+
const handle = this._aggAttach(gen, chart, res, res.bars, limit, 'loaded');
|
|
332
|
+
let lastId = trades.length ? trades[trades.length - 1].id : 0;
|
|
333
|
+
chart.onloadmore = async (fromTime) => {
|
|
334
|
+
const older = await fetchBinanceAggTrades(sym, Math.ceil(limit * perBar), 10, oldestId);
|
|
335
|
+
if (older.oldestId != null) oldestId = older.oldestId;
|
|
336
|
+
return aggregateTrades(older.trades, agg.kind, agg.threshold).bars
|
|
337
|
+
.filter((b) => b.time < fromTime)
|
|
338
|
+
.slice(-limit);
|
|
339
|
+
};
|
|
340
|
+
if (!live) return;
|
|
341
|
+
const ws = openBinanceTradeSocket(
|
|
342
|
+
sym,
|
|
343
|
+
(t) => {
|
|
344
|
+
if (this._gen !== gen || !this.isConnected) return;
|
|
345
|
+
if (!(t.id > lastId)) return; // seed/WS overlap
|
|
346
|
+
lastId = t.id;
|
|
347
|
+
handle.push(t);
|
|
348
|
+
this._setStatus('live');
|
|
349
|
+
},
|
|
350
|
+
() => {
|
|
351
|
+
if (this._gen !== gen || !this.isConnected) return;
|
|
352
|
+
this._pollBinanceTrades(gen, chart, sym, handle, lastId, agg, limit, live);
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
this._closers.push(() => ws.close());
|
|
356
|
+
} catch (err) {
|
|
357
|
+
if (this._gen !== gen || !this.isConnected) return;
|
|
358
|
+
this._fire('fallback', { reason: err && err.message });
|
|
359
|
+
this._syntheticTrades(gen, chart, sym, agg, limit, live, 'fallback');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_pollBinanceTrades(gen, chart, sym, handle, fromId, agg, limit, live) {
|
|
364
|
+
this._setStatus('polling');
|
|
365
|
+
let lastId = fromId;
|
|
366
|
+
const timer = setInterval(async () => {
|
|
367
|
+
if (this._gen !== gen || !this.isConnected) return;
|
|
368
|
+
try {
|
|
369
|
+
const { trades, latestId } = await fetchBinanceAggTradesSince(sym, lastId + 1);
|
|
370
|
+
for (const t of trades) {
|
|
371
|
+
if (!(t.id > lastId)) continue;
|
|
372
|
+
lastId = t.id;
|
|
373
|
+
handle.push(t);
|
|
374
|
+
}
|
|
375
|
+
if (latestId > lastId) lastId = latestId;
|
|
376
|
+
} catch (_) {
|
|
377
|
+
clearInterval(timer);
|
|
378
|
+
this._fire('fallback', { reason: 'trade poll failed' });
|
|
379
|
+
this._syntheticTrades(gen, chart, sym, agg, limit, live, 'fallback');
|
|
380
|
+
}
|
|
381
|
+
}, 10000);
|
|
382
|
+
this._closers.push(() => clearInterval(timer));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async _restTrades(gen, chart, url, agg, limit, live) {
|
|
386
|
+
this._setStatus('loading');
|
|
387
|
+
const pull = async () => {
|
|
388
|
+
const res = await fetch(url);
|
|
389
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
390
|
+
const body = await res.json();
|
|
391
|
+
return normalizeTrades(Array.isArray(body) ? body : body.trades || body.bars);
|
|
392
|
+
};
|
|
393
|
+
try {
|
|
394
|
+
const trades = await pull();
|
|
395
|
+
if (this._gen !== gen || !this.isConnected) return;
|
|
396
|
+
const res = aggregateTrades(trades, agg.kind, agg.threshold);
|
|
397
|
+
this._aggAttach(gen, chart, res, res.bars, limit, 'loaded');
|
|
398
|
+
if (!live) return;
|
|
399
|
+
const pollSec = Math.max(1, parseInt(this.getAttribute('poll') || '0', 10) || 0);
|
|
400
|
+
if (!pollSec) return;
|
|
401
|
+
// dedupe by print time: a same-ms reprint would double-count notional
|
|
402
|
+
let lastTime = trades.length ? trades[trades.length - 1].time : 0;
|
|
403
|
+
const emit = this._aggEmit(gen, chart);
|
|
404
|
+
const timer = setInterval(async () => {
|
|
405
|
+
if (this._gen !== gen || !this.isConnected) return;
|
|
406
|
+
try {
|
|
407
|
+
for (const t of await pull()) {
|
|
408
|
+
if (t.time <= lastTime) continue;
|
|
409
|
+
lastTime = t.time;
|
|
410
|
+
const closed = res.aggregator.add(t);
|
|
411
|
+
if (closed) emit(closed);
|
|
412
|
+
}
|
|
413
|
+
emit(res.aggregator.current());
|
|
414
|
+
this._setStatus('polling');
|
|
415
|
+
} catch (_) {}
|
|
416
|
+
}, pollSec * 1000);
|
|
417
|
+
this._closers.push(() => clearInterval(timer));
|
|
418
|
+
} catch (err) {
|
|
419
|
+
if (this._gen !== gen || !this.isConnected) return;
|
|
420
|
+
this._setStatus('error', { message: err && err.message });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
223
424
|
/* ---------------- generic REST source ---------------- */
|
|
224
425
|
|
|
225
426
|
async _rest(gen, chart, url, limit, live) {
|
|
@@ -268,3 +469,13 @@ if (typeof customElements !== 'undefined') {
|
|
|
268
469
|
|
|
269
470
|
export default WickFeed;
|
|
270
471
|
export { WickFeed };
|
|
472
|
+
// pure aggregation helpers — exported so apps can pipe their own trade
|
|
473
|
+
// streams through the same machinery `aggregate=` uses
|
|
474
|
+
export {
|
|
475
|
+
parseAggregate,
|
|
476
|
+
TickBarAggregator,
|
|
477
|
+
aggregateTrades,
|
|
478
|
+
normalizeTrades,
|
|
479
|
+
genSyntheticTrades,
|
|
480
|
+
makeSynthTradeStream,
|
|
481
|
+
} from './feeds.js';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
* worker-core — the worker side of the worker compute path.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside a module Worker spawned by src/worker.js. Tasks arrive with a
|
|
5
|
+
* correlation id; replies carry { id, ok, ... }. The chart sends its dataset
|
|
6
|
+
* once per data epoch as six Float64Arrays (transferred, not cloned — a
|
|
7
|
+
* structured clone of a million bar objects costs ~1 s, the columnar fill
|
|
8
|
+
* ~25 ms), and indicator tasks afterwards reference that data by
|
|
9
|
+
* (sid, epoch). Sessions are keyed per chart so one shared worker can serve
|
|
10
|
+
* several charts; the two most recent are kept (1M bars of columns plus the
|
|
11
|
+
* rebuilt object tape is ~50 MB each).
|
|
12
|
+
*
|
|
13
|
+
* handle() is exported (and pure with respect to its state object) so the
|
|
14
|
+
* dispatch is unit-testable in Node with no Worker at all.
|
|
15
|
+
* ========================================================================== */
|
|
16
|
+
|
|
17
|
+
import { BUILTIN_INDICATORS } from './core.js';
|
|
18
|
+
|
|
19
|
+
/** Rebuild the bar-object tape from a columnar snapshot (worker-side only —
|
|
20
|
+
* this cost never touches the main thread). */
|
|
21
|
+
export function barsFromCols(cols) {
|
|
22
|
+
const n = cols.time.length;
|
|
23
|
+
const out = new Array(n);
|
|
24
|
+
for (let i = 0; i < n; i++) {
|
|
25
|
+
out[i] = {
|
|
26
|
+
time: cols.time[i],
|
|
27
|
+
open: cols.open[i],
|
|
28
|
+
high: cols.high[i],
|
|
29
|
+
low: cols.low[i],
|
|
30
|
+
close: cols.close[i],
|
|
31
|
+
volume: cols.volume[i],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a worker-core state + dispatcher.
|
|
39
|
+
* @returns {{sessions: Map, handle(message: object): object}} the reply object
|
|
40
|
+
*/
|
|
41
|
+
export function createWorkerCore() {
|
|
42
|
+
const sessions = new Map(); // sid → { epoch, cols, bars: Array|null }
|
|
43
|
+
return {
|
|
44
|
+
sessions,
|
|
45
|
+
|
|
46
|
+
handle(msg) {
|
|
47
|
+
const { id, type, sid } = msg || {};
|
|
48
|
+
if (type === 'epoch') {
|
|
49
|
+
// keep at most two chart sessions resident
|
|
50
|
+
while (sessions.size >= 2) sessions.delete(sessions.keys().next().value);
|
|
51
|
+
sessions.set(sid, { epoch: msg.epoch, cols: msg.cols, bars: null });
|
|
52
|
+
return { id, ok: true };
|
|
53
|
+
}
|
|
54
|
+
if (type === 'indicator') {
|
|
55
|
+
const s = sessions.get(sid);
|
|
56
|
+
if (!s || s.epoch !== msg.epoch) {
|
|
57
|
+
// the worker no longer holds this epoch's data (evicted, or the
|
|
58
|
+
// chart is ahead) — the chart resends and retries
|
|
59
|
+
return { id, ok: false, stale: true };
|
|
60
|
+
}
|
|
61
|
+
const def = BUILTIN_INDICATORS.get(msg.name);
|
|
62
|
+
if (!def) return { id, ok: false, error: 'unknown indicator: ' + msg.name };
|
|
63
|
+
let res = null;
|
|
64
|
+
try {
|
|
65
|
+
if (!s.bars) s.bars = barsFromCols(s.cols); // once per epoch
|
|
66
|
+
res = def.compute(s.bars, msg.params || {});
|
|
67
|
+
} catch (_) {
|
|
68
|
+
res = null; // same contract as the sync path: a failed compute draws nothing
|
|
69
|
+
}
|
|
70
|
+
return { id, ok: true, res };
|
|
71
|
+
}
|
|
72
|
+
return { id, ok: false, error: 'unknown task type: ' + type };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ---------------- worker bootstrap (real Worker scope only) ---------------- */
|
|
78
|
+
|
|
79
|
+
const inWorker =
|
|
80
|
+
typeof WorkerGlobalScope !== 'undefined' &&
|
|
81
|
+
typeof self !== 'undefined' &&
|
|
82
|
+
self instanceof WorkerGlobalScope;
|
|
83
|
+
|
|
84
|
+
if (inWorker) {
|
|
85
|
+
const core = createWorkerCore();
|
|
86
|
+
self.onmessage = (e) => {
|
|
87
|
+
self.postMessage(core.handle(e.data));
|
|
88
|
+
};
|
|
89
|
+
}
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
* <wick-chart> worker compute path — the main-thread side.
|
|
3
|
+
*
|
|
4
|
+
* import 'wickchart/worker'; // once, anywhere
|
|
5
|
+
* <wick-chart worker indicators="sma:20 bb:20 rsi:14"></wick-chart>
|
|
6
|
+
*
|
|
7
|
+
* Importing this module wires a shared ChartWorkerPool into the chart class;
|
|
8
|
+
* from then on, any chart with the `worker` attribute computes its built-in
|
|
9
|
+
* indicators off the main thread once the dataset crosses WORKER_MIN_BARS
|
|
10
|
+
* (50k bars). Custom/scripted indicators are closures and stay sync; so does
|
|
11
|
+
* everything below the threshold. Data crosses once per bulk load as six
|
|
12
|
+
* transferable Float64Arrays — never as cloned objects.
|
|
13
|
+
*
|
|
14
|
+
* import { ChartWorkerPool, getSharedPool, setChartWorkerPool } from 'wickchart/worker';
|
|
15
|
+
* const pool = new ChartWorkerPool(); // a private pool (e.g. one per tab view)
|
|
16
|
+
* setChartWorkerPool(pool);
|
|
17
|
+
*
|
|
18
|
+
* No Worker available (old browsers, non-HTTP contexts where module workers
|
|
19
|
+
* fail, Node)? pool.available is false and every chart silently stays on the
|
|
20
|
+
* synchronous path — the attribute is an optimization, never a dependency.
|
|
21
|
+
* ========================================================================== */
|
|
22
|
+
|
|
23
|
+
import { WickChart } from './wick-chart.js';
|
|
24
|
+
|
|
25
|
+
export { WickChart };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A single-Worker task runner. The factory is injectable so tests can drive
|
|
29
|
+
* the protocol with a fake Worker; the default spawns the module worker
|
|
30
|
+
* sitting next to this file.
|
|
31
|
+
*/
|
|
32
|
+
export class ChartWorkerPool {
|
|
33
|
+
/** @param {() => Worker} [factory] */
|
|
34
|
+
constructor(factory) {
|
|
35
|
+
this._factory =
|
|
36
|
+
typeof factory === 'function'
|
|
37
|
+
? factory
|
|
38
|
+
: typeof Worker === 'function'
|
|
39
|
+
? () => new Worker(new URL('./worker-core.js', import.meta.url), { type: 'module' })
|
|
40
|
+
: null;
|
|
41
|
+
this.available = this._factory != null;
|
|
42
|
+
this._worker = null;
|
|
43
|
+
this._seq = 0;
|
|
44
|
+
this._pending = new Map(); // id → { resolve, reject }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Post a task; resolves with the reply's `res`, rejects on failure or
|
|
49
|
+
* worker death (a `stale` rejection means: resend the epoch data first).
|
|
50
|
+
* @param {object} msg task message without the id
|
|
51
|
+
* @returns {Promise<any>}
|
|
52
|
+
*/
|
|
53
|
+
run(msg) {
|
|
54
|
+
if (!this.available) return Promise.reject(new Error('worker unavailable'));
|
|
55
|
+
if (!this._worker) this._spawn();
|
|
56
|
+
const id = ++this._seq;
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
this._pending.set(id, { resolve, reject });
|
|
59
|
+
try {
|
|
60
|
+
this._worker.postMessage({ ...msg, id });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
this._pending.delete(id);
|
|
63
|
+
reject(err);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_spawn() {
|
|
69
|
+
try {
|
|
70
|
+
this._worker = this._factory();
|
|
71
|
+
} catch (err) {
|
|
72
|
+
this.available = false;
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
this._worker.onmessage = (e) => {
|
|
76
|
+
const reply = e.data || {};
|
|
77
|
+
const p = this._pending.get(reply.id);
|
|
78
|
+
if (!p) return;
|
|
79
|
+
this._pending.delete(reply.id);
|
|
80
|
+
if (reply.stale) p.reject(Object.assign(new Error('stale epoch'), { stale: true }));
|
|
81
|
+
else if (reply.ok) p.resolve(reply.res);
|
|
82
|
+
else p.reject(new Error(reply.error || 'worker task failed'));
|
|
83
|
+
};
|
|
84
|
+
this._worker.onerror = () => this._die();
|
|
85
|
+
this._worker.onmessageerror = () => this._die();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Reject everything in flight and drop the worker; the next run() respawns
|
|
89
|
+
* — a transient worker crash must not permanently kill the mode. */
|
|
90
|
+
_die() {
|
|
91
|
+
for (const p of this._pending.values()) p.reject(new Error('worker died'));
|
|
92
|
+
this._pending.clear();
|
|
93
|
+
if (this._worker) {
|
|
94
|
+
try {
|
|
95
|
+
this._worker.terminate();
|
|
96
|
+
} catch (_) {}
|
|
97
|
+
}
|
|
98
|
+
this._worker = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Shut the pool down for good (in-flight tasks reject). */
|
|
102
|
+
terminate() {
|
|
103
|
+
this._die();
|
|
104
|
+
this.available = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let shared = null;
|
|
109
|
+
|
|
110
|
+
/** The page-wide pool (created on first use). */
|
|
111
|
+
export function getSharedPool() {
|
|
112
|
+
return shared || (shared = new ChartWorkerPool());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Point the chart class at a pool (or null to disable the worker path).
|
|
117
|
+
* @param {ChartWorkerPool|null} pool
|
|
118
|
+
*/
|
|
119
|
+
export function setChartWorkerPool(pool) {
|
|
120
|
+
WickChart._workerPool = pool;
|
|
121
|
+
return pool;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// importing 'wickchart/worker' is the whole setup: wire the shared pool into
|
|
125
|
+
// the chart class so <wick-chart worker> just works afterwards
|
|
126
|
+
if (typeof Worker === 'function') {
|
|
127
|
+
setChartWorkerPool(getSharedPool());
|
|
128
|
+
}
|
package/types/core.d.ts
CHANGED
|
@@ -190,6 +190,27 @@ export declare const SEC = 1000;
|
|
|
190
190
|
export declare const MIN: number;
|
|
191
191
|
export declare const HOUR: number;
|
|
192
192
|
export declare const DAY: number;
|
|
193
|
+
/**
|
|
194
|
+
* Interpret a timestamp as milliseconds. Numbers may be seconds or ms, so
|
|
195
|
+
* some threshold is unavoidable; pass a `Date` for anything before 1973,
|
|
196
|
+
* which is unambiguous. Single source of truth — everything that reads a
|
|
197
|
+
* caller-supplied time goes through here.
|
|
198
|
+
* @param {number|Date} t
|
|
199
|
+
* @returns {number} milliseconds
|
|
200
|
+
*/
|
|
201
|
+
export declare const toMs: (t: number | Date) => number;
|
|
202
|
+
/**
|
|
203
|
+
* Milliseconds east of UTC in `zone` at the instant `at`.
|
|
204
|
+
* 'utc' → 0
|
|
205
|
+
* 'local' / null → the browser's zone (DST-correct via Date)
|
|
206
|
+
* number → a fixed offset in ms (exchange sessions)
|
|
207
|
+
* IANA name → DST-correct via Intl
|
|
208
|
+
* Never throws: an unusable zone reads as UTC.
|
|
209
|
+
* @param {number} at epoch ms
|
|
210
|
+
* @param {string|number|null} [zone]
|
|
211
|
+
* @returns {number} offset in ms
|
|
212
|
+
*/
|
|
213
|
+
export declare function zoneOffset(at: number, zone?: string | number | null): number;
|
|
193
214
|
export declare const TIME_STEPS: {
|
|
194
215
|
ms: number;
|
|
195
216
|
label: string;
|
|
@@ -308,12 +329,22 @@ export declare function calcTrueRange(bars: Bar[]): Array<number | null>;
|
|
|
308
329
|
*/
|
|
309
330
|
export declare function calcATR(bars: Bar[], period?: number): Array<number | null>;
|
|
310
331
|
/**
|
|
311
|
-
* Volume-weighted average price over the hlc3 typical price,
|
|
312
|
-
* each
|
|
332
|
+
* Volume-weighted average price over the hlc3 typical price, resetting at
|
|
333
|
+
* each session boundary.
|
|
334
|
+
*
|
|
335
|
+
* The anchor defaults to the UTC day — the crypto convention, and what this
|
|
336
|
+
* has always done. Equities, futures and FX rarely open at UTC midnight, so
|
|
337
|
+
* pass the session's zone (or a fixed offset) to move the reset. Note this is
|
|
338
|
+
* deliberately independent of the chart's `timezone`, which only governs how
|
|
339
|
+
* times are displayed: changing the axis to Stockholm should not silently
|
|
340
|
+
* re-anchor a BTC chart's VWAP.
|
|
341
|
+
*
|
|
313
342
|
* @param {Bar[]} bars
|
|
343
|
+
* @param {string|number} [anchor='utc'] 'utc' | 'local' | IANA zone | fixed
|
|
344
|
+
* offset in ms — see zoneOffset()
|
|
314
345
|
* @returns {Array<number|null>}
|
|
315
346
|
*/
|
|
316
|
-
export declare function calcVWAP(bars: Bar[]): Array<number | null>;
|
|
347
|
+
export declare function calcVWAP(bars: Bar[], anchor?: string | number): Array<number | null>;
|
|
317
348
|
/**
|
|
318
349
|
* On-balance volume: cumulative volume signed by close-to-close direction.
|
|
319
350
|
* @param {Bar[]} bars
|
|
@@ -523,10 +554,10 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
523
554
|
smooth?: undefined;
|
|
524
555
|
};
|
|
525
556
|
compute: (bars: any, p: any) => number[];
|
|
526
|
-
color?: undefined;
|
|
527
557
|
guides?: undefined;
|
|
528
558
|
range?: undefined;
|
|
529
559
|
fmt?: undefined;
|
|
560
|
+
color?: undefined;
|
|
530
561
|
} | {
|
|
531
562
|
kind: string;
|
|
532
563
|
params: {
|
|
@@ -538,10 +569,10 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
538
569
|
smooth?: undefined;
|
|
539
570
|
};
|
|
540
571
|
compute: (bars: any, p: any) => number[];
|
|
541
|
-
color?: undefined;
|
|
542
572
|
guides?: undefined;
|
|
543
573
|
range?: undefined;
|
|
544
574
|
fmt?: undefined;
|
|
575
|
+
color?: undefined;
|
|
545
576
|
} | {
|
|
546
577
|
kind: string;
|
|
547
578
|
params: {
|
|
@@ -552,11 +583,11 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
552
583
|
smooth?: undefined;
|
|
553
584
|
period?: undefined;
|
|
554
585
|
};
|
|
555
|
-
compute: (bars: any) => number[];
|
|
556
|
-
color?: undefined;
|
|
586
|
+
compute: (bars: any, p: any) => number[];
|
|
557
587
|
guides?: undefined;
|
|
558
588
|
range?: undefined;
|
|
559
589
|
fmt?: undefined;
|
|
590
|
+
color?: undefined;
|
|
560
591
|
} | {
|
|
561
592
|
kind: string;
|
|
562
593
|
params: {
|
|
@@ -568,10 +599,10 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
568
599
|
smooth?: undefined;
|
|
569
600
|
};
|
|
570
601
|
compute: (bars: any, p: any) => number[];
|
|
571
|
-
color?: undefined;
|
|
572
602
|
guides?: undefined;
|
|
573
603
|
range?: undefined;
|
|
574
604
|
fmt?: undefined;
|
|
605
|
+
color?: undefined;
|
|
575
606
|
} | {
|
|
576
607
|
kind: string;
|
|
577
608
|
params: {
|
|
@@ -588,10 +619,10 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
588
619
|
values: number[];
|
|
589
620
|
}[];
|
|
590
621
|
};
|
|
591
|
-
color?: undefined;
|
|
592
622
|
guides?: undefined;
|
|
593
623
|
range?: undefined;
|
|
594
624
|
fmt?: undefined;
|
|
625
|
+
color?: undefined;
|
|
595
626
|
} | {
|
|
596
627
|
kind: string;
|
|
597
628
|
params: {
|
|
@@ -608,10 +639,10 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
608
639
|
values: number[];
|
|
609
640
|
}[];
|
|
610
641
|
};
|
|
611
|
-
color?: undefined;
|
|
612
642
|
guides?: undefined;
|
|
613
643
|
range?: undefined;
|
|
614
644
|
fmt?: undefined;
|
|
645
|
+
color?: undefined;
|
|
615
646
|
} | {
|
|
616
647
|
kind: string;
|
|
617
648
|
params: {
|
|
@@ -628,10 +659,10 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
628
659
|
values: number[];
|
|
629
660
|
}[];
|
|
630
661
|
};
|
|
631
|
-
color?: undefined;
|
|
632
662
|
guides?: undefined;
|
|
633
663
|
range?: undefined;
|
|
634
664
|
fmt?: undefined;
|
|
665
|
+
color?: undefined;
|
|
635
666
|
} | {
|
|
636
667
|
kind: string;
|
|
637
668
|
params: {
|
|
@@ -648,7 +679,6 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
648
679
|
color: string;
|
|
649
680
|
compute: (bars: any, p: any) => number[];
|
|
650
681
|
} | {
|
|
651
|
-
color?: undefined;
|
|
652
682
|
kind: string;
|
|
653
683
|
params: {
|
|
654
684
|
mult?: undefined;
|
|
@@ -668,8 +698,8 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
668
698
|
histogram: number[];
|
|
669
699
|
};
|
|
670
700
|
range?: undefined;
|
|
671
|
-
} | {
|
|
672
701
|
color?: undefined;
|
|
702
|
+
} | {
|
|
673
703
|
kind: string;
|
|
674
704
|
params: {
|
|
675
705
|
mult?: undefined;
|
|
@@ -683,8 +713,8 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
683
713
|
compute: (bars: any, p: any) => number[];
|
|
684
714
|
guides?: undefined;
|
|
685
715
|
range?: undefined;
|
|
686
|
-
} | {
|
|
687
716
|
color?: undefined;
|
|
717
|
+
} | {
|
|
688
718
|
kind: string;
|
|
689
719
|
params: {
|
|
690
720
|
mult?: undefined;
|
|
@@ -703,8 +733,8 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
703
733
|
values: number[];
|
|
704
734
|
}[];
|
|
705
735
|
};
|
|
706
|
-
} | {
|
|
707
736
|
color?: undefined;
|
|
737
|
+
} | {
|
|
708
738
|
kind: string;
|
|
709
739
|
params: {
|
|
710
740
|
mult?: undefined;
|
|
@@ -718,8 +748,8 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
718
748
|
compute: (bars: any) => number[];
|
|
719
749
|
guides?: undefined;
|
|
720
750
|
range?: undefined;
|
|
721
|
-
} | {
|
|
722
751
|
color?: undefined;
|
|
752
|
+
} | {
|
|
723
753
|
kind: string;
|
|
724
754
|
params: {
|
|
725
755
|
mult?: undefined;
|
|
@@ -733,8 +763,8 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
733
763
|
fmt: string;
|
|
734
764
|
compute: (bars: any, p: any) => number[];
|
|
735
765
|
range?: undefined;
|
|
736
|
-
} | {
|
|
737
766
|
color?: undefined;
|
|
767
|
+
} | {
|
|
738
768
|
kind: string;
|
|
739
769
|
params: {
|
|
740
770
|
mult?: undefined;
|
|
@@ -748,6 +778,7 @@ export declare const BUILTIN_INDICATORS: Map<string, {
|
|
|
748
778
|
range: number[];
|
|
749
779
|
fmt: string;
|
|
750
780
|
compute: (bars: any, p: any) => number[];
|
|
781
|
+
color?: undefined;
|
|
751
782
|
}>;
|
|
752
783
|
/**
|
|
753
784
|
* Parse an `indicators` attribute string against a registry.
|
|
@@ -814,6 +845,18 @@ export declare function positionPnl(pos: {
|
|
|
814
845
|
entry: number;
|
|
815
846
|
qty?: number;
|
|
816
847
|
}, price: number): number;
|
|
848
|
+
/**
|
|
849
|
+
* Percent return of a position at `price` — the move per unit, so it does
|
|
850
|
+
* NOT scale with `qty` the way positionPnl() does. Deriving this by dividing
|
|
851
|
+
* positionPnl() by the entry price reports qty × the true return.
|
|
852
|
+
* @param {{side?: 'long'|'short', entry: number}} pos
|
|
853
|
+
* @param {number} price
|
|
854
|
+
* @returns {number} percent (10 means +10%)
|
|
855
|
+
*/
|
|
856
|
+
export declare function positionPnlPct(pos: {
|
|
857
|
+
side?: 'long' | 'short';
|
|
858
|
+
entry: number;
|
|
859
|
+
}, price: number): number;
|
|
817
860
|
/**
|
|
818
861
|
* Edge-triggered alert crossing test between two consecutive prices.
|
|
819
862
|
* @param {{price: number, direction?: 'above'|'below'|'cross'}} alert
|