acttrader-charts 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.
@@ -0,0 +1,2555 @@
1
+ 'use strict';
2
+
3
+ // src/indicators/sourceField.ts
4
+ function getSource(bar, source) {
5
+ return bar[source];
6
+ }
7
+
8
+ // src/indicators/SMA.ts
9
+ var SMA = class {
10
+ constructor(period, color = "#F5A623", name = "MA", source = "close") {
11
+ this.period = period;
12
+ this.source = source;
13
+ this.name = name;
14
+ this.color = color;
15
+ }
16
+ compute(data) {
17
+ return data.map((_, i) => {
18
+ if (i < this.period - 1) return { index: i, value: null };
19
+ let sum = 0;
20
+ for (let j = i - this.period + 1; j <= i; j++) sum += getSource(data[j], this.source);
21
+ return { index: i, value: sum / this.period };
22
+ });
23
+ }
24
+ render(ctx, results, scale, viewport, chartHeight) {
25
+ const priceRange = getPriceRangeFromCtx(ctx);
26
+ if (!priceRange) return;
27
+ ctx.beginPath();
28
+ ctx.strokeStyle = this.color;
29
+ ctx.lineWidth = 1.5;
30
+ ctx.lineJoin = "round";
31
+ let started = false;
32
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
33
+ for (const r of results) {
34
+ if (r.value === null) {
35
+ started = false;
36
+ continue;
37
+ }
38
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
39
+ const y = scale.yToPixel(r.value, priceRange, chartHeight);
40
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
41
+ }
42
+ ctx.stroke();
43
+ }
44
+ };
45
+ function getPriceRangeFromCtx(ctx) {
46
+ const canvas = ctx.canvas;
47
+ return canvas._priceRange ?? null;
48
+ }
49
+
50
+ // src/indicators/EMA.ts
51
+ var EMA = class {
52
+ constructor(period, color = "#9B59B6", name = "EMA", source = "close") {
53
+ this.period = period;
54
+ this.source = source;
55
+ this.name = name;
56
+ this.color = color;
57
+ }
58
+ compute(data) {
59
+ const k = 2 / (this.period + 1);
60
+ const results = [];
61
+ let ema = null;
62
+ for (let i = 0; i < data.length; i++) {
63
+ if (i < this.period - 1) {
64
+ results.push({ index: i, value: null });
65
+ continue;
66
+ }
67
+ if (ema === null) {
68
+ let sum = 0;
69
+ for (let j = 0; j < this.period; j++) sum += getSource(data[i - j], this.source);
70
+ ema = sum / this.period;
71
+ } else {
72
+ ema = getSource(data[i], this.source) * k + ema * (1 - k);
73
+ }
74
+ results.push({ index: i, value: ema });
75
+ }
76
+ return results;
77
+ }
78
+ render(ctx, results, scale, viewport, chartHeight) {
79
+ const canvas = ctx.canvas;
80
+ const priceRange = canvas._priceRange;
81
+ if (!priceRange) return;
82
+ ctx.beginPath();
83
+ ctx.strokeStyle = this.color;
84
+ ctx.lineWidth = 1.5;
85
+ ctx.lineJoin = "round";
86
+ let started = false;
87
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
88
+ for (const r of results) {
89
+ if (r.value === null) {
90
+ started = false;
91
+ continue;
92
+ }
93
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
94
+ const y = scale.yToPixel(r.value, priceRange, chartHeight);
95
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
96
+ }
97
+ ctx.stroke();
98
+ }
99
+ };
100
+
101
+ // src/indicators/WMA.ts
102
+ var WMA = class {
103
+ constructor(period = 20, color = "#e91e63", name = "WMA", source = "close") {
104
+ this.period = period;
105
+ this.source = source;
106
+ this.name = name;
107
+ this.color = color;
108
+ }
109
+ compute(data) {
110
+ const denominator = this.period * (this.period + 1) / 2;
111
+ const results = [];
112
+ for (let i = 0; i < data.length; i++) {
113
+ if (i < this.period - 1) {
114
+ results.push({ index: i, value: null });
115
+ continue;
116
+ }
117
+ let wma = 0;
118
+ for (let j = 0; j < this.period; j++) {
119
+ wma += getSource(data[i - (this.period - 1 - j)], this.source) * (j + 1);
120
+ }
121
+ results.push({ index: i, value: wma / denominator });
122
+ }
123
+ return results;
124
+ }
125
+ render(ctx, results, scale, viewport, chartHeight) {
126
+ const canvas = ctx.canvas;
127
+ const priceRange = canvas._priceRange;
128
+ if (!priceRange) return;
129
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
130
+ ctx.beginPath();
131
+ ctx.strokeStyle = this.color;
132
+ ctx.lineWidth = 1.5;
133
+ ctx.lineJoin = "round";
134
+ let started = false;
135
+ for (const r of results) {
136
+ if (r.value === null) {
137
+ started = false;
138
+ continue;
139
+ }
140
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
141
+ const y = scale.yToPixel(r.value, priceRange, chartHeight);
142
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
143
+ }
144
+ ctx.stroke();
145
+ }
146
+ };
147
+
148
+ // src/indicators/HMA.ts
149
+ function computeWMA(data, period) {
150
+ const denominator = period * (period + 1) / 2;
151
+ const results = [];
152
+ for (let i = 0; i < data.length; i++) {
153
+ if (i < period - 1) {
154
+ results.push(null);
155
+ continue;
156
+ }
157
+ let wma = 0;
158
+ for (let j = 0; j < period; j++) {
159
+ wma += data[i - (period - 1 - j)] * (j + 1);
160
+ }
161
+ results.push(wma / denominator);
162
+ }
163
+ return results;
164
+ }
165
+ var HMA = class {
166
+ constructor(period = 20, color = "#00bcd4", name = "HMA", source = "close") {
167
+ this.period = period;
168
+ this.source = source;
169
+ this.name = name;
170
+ this.color = color;
171
+ }
172
+ compute(data) {
173
+ const closes = data.map((b) => getSource(b, this.source));
174
+ const halfPeriod = Math.floor(this.period / 2);
175
+ const sqrtPeriod = Math.floor(Math.sqrt(this.period));
176
+ const wmaHalf = computeWMA(closes, halfPeriod);
177
+ const wmaFull = computeWMA(closes, this.period);
178
+ const diff = wmaHalf.map((half, i) => {
179
+ const full = wmaFull[i];
180
+ if (half === null || full === null) return null;
181
+ return 2 * half - full;
182
+ });
183
+ const diffForWma = diff.map((v) => v === null ? 0 : v);
184
+ const hmaRaw = computeWMA(diffForWma, sqrtPeriod);
185
+ return data.map((_, i) => {
186
+ const windowStart = i - sqrtPeriod + 1;
187
+ if (windowStart < 0) return { index: i, value: null };
188
+ const allDiffValid = diff[windowStart] !== null && diff[i] !== null;
189
+ if (!allDiffValid || hmaRaw[i] === null) return { index: i, value: null };
190
+ return { index: i, value: hmaRaw[i] };
191
+ });
192
+ }
193
+ render(ctx, results, scale, viewport, chartHeight) {
194
+ const canvas = ctx.canvas;
195
+ const priceRange = canvas._priceRange;
196
+ if (!priceRange) return;
197
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
198
+ ctx.beginPath();
199
+ ctx.strokeStyle = this.color;
200
+ ctx.lineWidth = 1.5;
201
+ ctx.lineJoin = "round";
202
+ let started = false;
203
+ for (const r of results) {
204
+ if (r.value === null) {
205
+ started = false;
206
+ continue;
207
+ }
208
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
209
+ const y = scale.yToPixel(r.value, priceRange, chartHeight);
210
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
211
+ }
212
+ ctx.stroke();
213
+ }
214
+ };
215
+
216
+ // src/indicators/BollingerBands.ts
217
+ var BollingerBands = class {
218
+ constructor(period = 20, stdDevMultiplier = 2, color = "#3B82F6", name = "BB", source = "close") {
219
+ this.period = period;
220
+ this.stdDevMultiplier = stdDevMultiplier;
221
+ this.source = source;
222
+ this.name = name;
223
+ this.color = color;
224
+ }
225
+ compute(data) {
226
+ return data.map((_, i) => {
227
+ if (i < this.period - 1) return { index: i, value: null, upper: null, lower: null };
228
+ const slice = data.slice(i - this.period + 1, i + 1).map((b) => getSource(b, this.source));
229
+ const mean = slice.reduce((s, v) => s + v, 0) / this.period;
230
+ const variance = slice.reduce((s, v) => s + (v - mean) ** 2, 0) / this.period;
231
+ const std = Math.sqrt(variance);
232
+ return {
233
+ index: i,
234
+ value: mean,
235
+ upper: mean + std * this.stdDevMultiplier,
236
+ lower: mean - std * this.stdDevMultiplier
237
+ };
238
+ });
239
+ }
240
+ render(ctx, results, scale, viewport, chartHeight) {
241
+ const canvas = ctx.canvas;
242
+ const priceRange = canvas._priceRange;
243
+ if (!priceRange) return;
244
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
245
+ ctx.lineWidth = 1;
246
+ ctx.lineJoin = "round";
247
+ const drawLine = (getValue, dash = false) => {
248
+ if (dash) ctx.setLineDash([4, 3]);
249
+ ctx.beginPath();
250
+ let started = false;
251
+ for (const r of results) {
252
+ const val = getValue(r);
253
+ if (val == null) {
254
+ started = false;
255
+ continue;
256
+ }
257
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
258
+ const y = scale.yToPixel(val, priceRange, chartHeight);
259
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
260
+ }
261
+ ctx.stroke();
262
+ if (dash) ctx.setLineDash([]);
263
+ };
264
+ ctx.strokeStyle = this.color;
265
+ drawLine((r) => r.value);
266
+ ctx.strokeStyle = `${this.color}99`;
267
+ drawLine((r) => r.upper, true);
268
+ drawLine((r) => r.lower, true);
269
+ const upperPoints = [];
270
+ const lowerPoints = [];
271
+ for (const r of results) {
272
+ if (r.upper == null || r.lower == null) continue;
273
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
274
+ upperPoints.push([x, scale.yToPixel(r.upper, priceRange, chartHeight)]);
275
+ lowerPoints.push([x, scale.yToPixel(r.lower, priceRange, chartHeight)]);
276
+ }
277
+ if (upperPoints.length < 2) return;
278
+ ctx.beginPath();
279
+ upperPoints.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
280
+ lowerPoints.reverse().forEach(([x, y]) => ctx.lineTo(x, y));
281
+ ctx.closePath();
282
+ ctx.fillStyle = `${this.color}12`;
283
+ ctx.fill();
284
+ }
285
+ };
286
+
287
+ // src/indicators/KeltnerChannels.ts
288
+ var KeltnerChannels = class {
289
+ constructor(period = 20, mult = 2, atrPeriod = 10, color = "#4caf50", name = "KC") {
290
+ this.period = period;
291
+ this.mult = mult;
292
+ this.atrPeriod = atrPeriod;
293
+ this.name = name;
294
+ this.color = color;
295
+ }
296
+ compute(data) {
297
+ const results = [];
298
+ const emaK = 2 / (this.period + 1);
299
+ let ema = null;
300
+ let atr = null;
301
+ for (let i = 0; i < data.length; i++) {
302
+ const bar = data[i];
303
+ if (i < this.period - 1) {
304
+ results.push({ index: i, value: null, upper: null, lower: null });
305
+ continue;
306
+ }
307
+ if (ema === null) {
308
+ let sum = 0;
309
+ for (let j = 0; j < this.period; j++) sum += data[i - j].close;
310
+ ema = sum / this.period;
311
+ } else {
312
+ ema = bar.close * emaK + ema * (1 - emaK);
313
+ }
314
+ const tr = i === 0 ? bar.high - bar.low : Math.max(
315
+ bar.high - bar.low,
316
+ Math.abs(bar.high - data[i - 1].close),
317
+ Math.abs(bar.low - data[i - 1].close)
318
+ );
319
+ if (atr === null) {
320
+ if (i < this.atrPeriod) {
321
+ results.pop();
322
+ results.push({ index: i, value: null, upper: null, lower: null });
323
+ if (i === this.atrPeriod - 1) {
324
+ let trSum = 0;
325
+ for (let j = 0; j < this.atrPeriod; j++) {
326
+ const b = data[i - j];
327
+ const prev = data[i - j - 1];
328
+ const trj = j === i ? b.high - b.low : Math.max(
329
+ b.high - b.low,
330
+ Math.abs(b.high - prev.close),
331
+ Math.abs(b.low - prev.close)
332
+ );
333
+ trSum += trj;
334
+ }
335
+ atr = trSum / this.atrPeriod;
336
+ }
337
+ continue;
338
+ }
339
+ } else {
340
+ atr = (atr * (this.atrPeriod - 1) + tr) / this.atrPeriod;
341
+ }
342
+ if (atr === null) {
343
+ results.push({ index: i, value: null, upper: null, lower: null });
344
+ continue;
345
+ }
346
+ const upper = ema + this.mult * atr;
347
+ const lower = ema - this.mult * atr;
348
+ results.push({ index: i, value: ema, upper, lower });
349
+ }
350
+ return results;
351
+ }
352
+ render(ctx, results, scale, viewport, chartHeight) {
353
+ const canvas = ctx.canvas;
354
+ const priceRange = canvas._priceRange;
355
+ if (!priceRange) return;
356
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
357
+ ctx.lineWidth = 1;
358
+ ctx.lineJoin = "round";
359
+ const drawLine = (getValue, dash = false) => {
360
+ if (dash) ctx.setLineDash([4, 3]);
361
+ ctx.beginPath();
362
+ let started = false;
363
+ for (const r of results) {
364
+ const val = getValue(r);
365
+ if (val == null) {
366
+ started = false;
367
+ continue;
368
+ }
369
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
370
+ const y = scale.yToPixel(val, priceRange, chartHeight);
371
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
372
+ }
373
+ ctx.stroke();
374
+ if (dash) ctx.setLineDash([]);
375
+ };
376
+ ctx.strokeStyle = this.color;
377
+ drawLine((r) => r.value);
378
+ ctx.strokeStyle = `${this.color}99`;
379
+ drawLine((r) => r.upper, true);
380
+ drawLine((r) => r.lower, true);
381
+ const upperPoints = [];
382
+ const lowerPoints = [];
383
+ for (const r of results) {
384
+ if (r.upper == null || r.lower == null) continue;
385
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
386
+ upperPoints.push([x, scale.yToPixel(r.upper, priceRange, chartHeight)]);
387
+ lowerPoints.push([x, scale.yToPixel(r.lower, priceRange, chartHeight)]);
388
+ }
389
+ if (upperPoints.length < 2) return;
390
+ ctx.beginPath();
391
+ upperPoints.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
392
+ lowerPoints.reverse().forEach(([x, y]) => ctx.lineTo(x, y));
393
+ ctx.closePath();
394
+ ctx.fillStyle = `${this.color}12`;
395
+ ctx.fill();
396
+ }
397
+ };
398
+
399
+ // src/indicators/ATR.ts
400
+ var ATR = class {
401
+ constructor(period = 14, color = "#ff5722", name = "ATR") {
402
+ this.period = period;
403
+ this.pane = "sub";
404
+ this.paneHeight = 80;
405
+ this.name = name;
406
+ this.color = color;
407
+ }
408
+ compute(data) {
409
+ const results = [];
410
+ if (data.length === 0) return results;
411
+ const tr = [];
412
+ for (let i = 0; i < data.length; i++) {
413
+ if (i === 0) {
414
+ tr.push(data[i].high - data[i].low);
415
+ } else {
416
+ const prevClose = data[i - 1].close;
417
+ tr.push(Math.max(
418
+ data[i].high - data[i].low,
419
+ Math.abs(data[i].high - prevClose),
420
+ Math.abs(data[i].low - prevClose)
421
+ ));
422
+ }
423
+ }
424
+ if (data.length < this.period) {
425
+ return data.map((_, i) => ({ index: i, value: null }));
426
+ }
427
+ let atr = 0;
428
+ for (let i = 0; i < this.period; i++) {
429
+ atr += tr[i];
430
+ }
431
+ atr /= this.period;
432
+ for (let i = 0; i < this.period - 1; i++) {
433
+ results.push({ index: i, value: null });
434
+ }
435
+ results.push({ index: this.period - 1, value: atr });
436
+ for (let i = this.period; i < data.length; i++) {
437
+ atr = (atr * (this.period - 1) + tr[i]) / this.period;
438
+ results.push({ index: i, value: atr });
439
+ }
440
+ return results;
441
+ }
442
+ getValueRange(results) {
443
+ let maxVal = 1e-4;
444
+ for (const r of results) {
445
+ if (r.value != null) maxVal = Math.max(maxVal, r.value);
446
+ }
447
+ return { min: 0, max: maxVal * 1.2 };
448
+ }
449
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
450
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
451
+ let maxVal = 1e-4;
452
+ for (const r of results) {
453
+ if (r.value != null) maxVal = Math.max(maxVal, r.value);
454
+ }
455
+ const range = rangeOverride ?? { min: 0, max: maxVal * 1.2 };
456
+ const points = [];
457
+ for (const r of results) {
458
+ if (r.value == null) continue;
459
+ points.push({
460
+ x: scale.xToPixel(r.index, viewport, chartWidth),
461
+ y: scale.yToPixel(r.value, range, paneHeight)
462
+ });
463
+ }
464
+ if (points.length === 0) return;
465
+ const baseY = scale.yToPixel(0, range, paneHeight);
466
+ ctx.beginPath();
467
+ ctx.moveTo(points[0].x, baseY);
468
+ for (const p of points) ctx.lineTo(p.x, p.y);
469
+ ctx.lineTo(points[points.length - 1].x, baseY);
470
+ ctx.closePath();
471
+ ctx.fillStyle = this.color + "33";
472
+ ctx.fill();
473
+ ctx.beginPath();
474
+ ctx.strokeStyle = this.color;
475
+ ctx.lineWidth = 1.5;
476
+ ctx.lineJoin = "round";
477
+ let started = false;
478
+ for (const r of results) {
479
+ if (r.value == null) {
480
+ started = false;
481
+ continue;
482
+ }
483
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
484
+ const y = scale.yToPixel(r.value, range, paneHeight);
485
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
486
+ }
487
+ ctx.stroke();
488
+ }
489
+ };
490
+
491
+ // src/indicators/RSI.ts
492
+ var RSI = class {
493
+ constructor(period = 14, color = "#e8b84b", name = "RSI", source = "close") {
494
+ this.period = period;
495
+ this.source = source;
496
+ this.pane = "sub";
497
+ this.paneHeight = 80;
498
+ this.name = name;
499
+ this.color = color;
500
+ }
501
+ compute(data) {
502
+ const results = [];
503
+ if (data.length < this.period + 1) {
504
+ return data.map((_, i) => ({ index: i, value: null }));
505
+ }
506
+ let avgGain = 0;
507
+ let avgLoss = 0;
508
+ for (let i = 1; i <= this.period; i++) {
509
+ const delta = getSource(data[i], this.source) - getSource(data[i - 1], this.source);
510
+ if (delta > 0) avgGain += delta;
511
+ else avgLoss += Math.abs(delta);
512
+ }
513
+ avgGain /= this.period;
514
+ avgLoss /= this.period;
515
+ for (let i = 0; i < this.period; i++) {
516
+ results.push({ index: i, value: null });
517
+ }
518
+ const rs0 = avgLoss === 0 ? Infinity : avgGain / avgLoss;
519
+ results.push({ index: this.period, value: avgLoss === 0 ? 100 : 100 - 100 / (1 + rs0) });
520
+ for (let i = this.period + 1; i < data.length; i++) {
521
+ const delta = getSource(data[i], this.source) - getSource(data[i - 1], this.source);
522
+ const gain = delta > 0 ? delta : 0;
523
+ const loss = delta < 0 ? Math.abs(delta) : 0;
524
+ avgGain = (avgGain * (this.period - 1) + gain) / this.period;
525
+ avgLoss = (avgLoss * (this.period - 1) + loss) / this.period;
526
+ const rs = avgLoss === 0 ? Infinity : avgGain / avgLoss;
527
+ results.push({ index: i, value: avgLoss === 0 ? 100 : 100 - 100 / (1 + rs) });
528
+ }
529
+ return results;
530
+ }
531
+ getValueRange(_results) {
532
+ return { min: 0, max: 100 };
533
+ }
534
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
535
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
536
+ const range = rangeOverride ?? { min: 0, max: 100 };
537
+ const y70 = scale.yToPixel(70, range, paneHeight);
538
+ const y30 = scale.yToPixel(30, range, paneHeight);
539
+ ctx.fillStyle = "rgba(239,83,80,0.06)";
540
+ ctx.fillRect(0, 0, chartWidth, y70);
541
+ ctx.fillStyle = "rgba(38,166,154,0.06)";
542
+ ctx.fillRect(0, y30, chartWidth, paneHeight - y30);
543
+ ctx.save();
544
+ ctx.setLineDash([4, 4]);
545
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
546
+ ctx.lineWidth = 1;
547
+ for (const level of [70, 50, 30]) {
548
+ const y = scale.yToPixel(level, range, paneHeight);
549
+ ctx.beginPath();
550
+ ctx.moveTo(0, y);
551
+ ctx.lineTo(chartWidth, y);
552
+ ctx.stroke();
553
+ }
554
+ ctx.setLineDash([]);
555
+ ctx.restore();
556
+ ctx.beginPath();
557
+ ctx.strokeStyle = this.color;
558
+ ctx.lineWidth = 1.5;
559
+ ctx.lineJoin = "round";
560
+ let started = false;
561
+ for (const r of results) {
562
+ if (r.value === null) {
563
+ started = false;
564
+ continue;
565
+ }
566
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
567
+ const y = scale.yToPixel(r.value, range, paneHeight);
568
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
569
+ }
570
+ ctx.stroke();
571
+ }
572
+ };
573
+
574
+ // src/indicators/VWAP.ts
575
+ var VWAP = class {
576
+ constructor(color = "#9c27b0") {
577
+ this.name = "VWAP";
578
+ this.color = color;
579
+ }
580
+ compute(data) {
581
+ const results = [];
582
+ let cumTPV = 0;
583
+ let cumVol = 0;
584
+ for (let i = 0; i < data.length; i++) {
585
+ const bar = data[i];
586
+ const typicalPrice = (bar.high + bar.low + bar.close) / 3;
587
+ cumTPV += typicalPrice * (bar.volume ?? 0);
588
+ cumVol += bar.volume ?? 0;
589
+ results.push({
590
+ index: i,
591
+ value: cumVol === 0 ? null : cumTPV / cumVol
592
+ });
593
+ }
594
+ return results;
595
+ }
596
+ render(ctx, results, scale, viewport, chartHeight) {
597
+ const canvas = ctx.canvas;
598
+ const priceRange = canvas._priceRange;
599
+ if (!priceRange) return;
600
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
601
+ ctx.beginPath();
602
+ ctx.strokeStyle = this.color;
603
+ ctx.lineWidth = 1.5;
604
+ ctx.lineJoin = "round";
605
+ ctx.setLineDash([4, 3]);
606
+ let started = false;
607
+ for (const r of results) {
608
+ if (r.value === null) {
609
+ started = false;
610
+ continue;
611
+ }
612
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
613
+ const y = scale.yToPixel(r.value, priceRange, chartHeight);
614
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
615
+ }
616
+ ctx.stroke();
617
+ ctx.setLineDash([]);
618
+ }
619
+ };
620
+
621
+ // src/indicators/MACD.ts
622
+ function computeEMA(data, period) {
623
+ const k = 2 / (period + 1);
624
+ const results = [];
625
+ let ema = null;
626
+ for (let i = 0; i < data.length; i++) {
627
+ if (i < period - 1) {
628
+ results.push(null);
629
+ continue;
630
+ }
631
+ if (ema === null) {
632
+ let sum = 0;
633
+ for (let j = 0; j < period; j++) sum += data[i - j];
634
+ ema = sum / period;
635
+ } else {
636
+ ema = data[i] * k + ema * (1 - k);
637
+ }
638
+ results.push(ema);
639
+ }
640
+ return results;
641
+ }
642
+ var MACD = class {
643
+ constructor(fast = 12, slow = 26, signal = 9, color = "#2196f3", name = "MACD", source = "close") {
644
+ this.fast = fast;
645
+ this.slow = slow;
646
+ this.signal = signal;
647
+ this.source = source;
648
+ this.pane = "sub";
649
+ this.paneHeight = 80;
650
+ this.name = name;
651
+ this.color = color;
652
+ }
653
+ compute(data) {
654
+ const closes = data.map((b) => getSource(b, this.source));
655
+ const fastEMA = computeEMA(closes, this.fast);
656
+ const slowEMA = computeEMA(closes, this.slow);
657
+ const macdLine = closes.map((_, i) => {
658
+ if (fastEMA[i] === null || slowEMA[i] === null) return null;
659
+ return fastEMA[i] - slowEMA[i];
660
+ });
661
+ const macdValues = macdLine.map((v) => v ?? 0);
662
+ const rawSignal = computeEMA(macdValues, this.signal);
663
+ return data.map((_, i) => {
664
+ const macd = macdLine[i];
665
+ if (macd === null) return { index: i, value: null, upper: null, lower: null, histogram: null };
666
+ const signalStart = this.slow - 1 + this.signal - 1;
667
+ const sig = i >= signalStart ? rawSignal[i] : null;
668
+ const hist = macd !== null && sig !== null ? macd - sig : null;
669
+ return {
670
+ index: i,
671
+ value: macd,
672
+ // MACD line
673
+ upper: sig,
674
+ // Signal line
675
+ lower: hist
676
+ // Histogram
677
+ };
678
+ });
679
+ }
680
+ getValueRange(results) {
681
+ let maxAbs = 1e-4;
682
+ for (const r of results) {
683
+ if (r.value != null) maxAbs = Math.max(maxAbs, Math.abs(r.value));
684
+ if (r.upper != null) maxAbs = Math.max(maxAbs, Math.abs(r.upper));
685
+ if (r.lower != null) maxAbs = Math.max(maxAbs, Math.abs(r.lower));
686
+ }
687
+ return { min: -maxAbs * 1.3, max: maxAbs * 1.3 };
688
+ }
689
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
690
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
691
+ let maxAbs = 1e-4;
692
+ for (const r of results) {
693
+ if (r.value != null) maxAbs = Math.max(maxAbs, Math.abs(r.value));
694
+ if (r.upper != null) maxAbs = Math.max(maxAbs, Math.abs(r.upper));
695
+ if (r.lower != null) maxAbs = Math.max(maxAbs, Math.abs(r.lower));
696
+ }
697
+ const range = rangeOverride ?? { min: -maxAbs * 1.3, max: maxAbs * 1.3 };
698
+ const zeroY = scale.yToPixel(0, range, paneHeight);
699
+ ctx.strokeStyle = "rgba(150,150,150,0.3)";
700
+ ctx.lineWidth = 1;
701
+ ctx.beginPath();
702
+ ctx.moveTo(0, zeroY);
703
+ ctx.lineTo(chartWidth, zeroY);
704
+ ctx.stroke();
705
+ const bodyW = Math.max(scale.candleBodyWidth(viewport, chartWidth), 1);
706
+ for (const r of results) {
707
+ if (r.lower == null) continue;
708
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
709
+ const barY = scale.yToPixel(r.lower, range, paneHeight);
710
+ const h = Math.abs(barY - zeroY);
711
+ const top = Math.min(barY, zeroY);
712
+ ctx.fillStyle = r.lower >= 0 ? "rgba(38,166,154,0.55)" : "rgba(239,83,80,0.55)";
713
+ ctx.fillRect(x - bodyW / 2, top, bodyW, h);
714
+ }
715
+ ctx.beginPath();
716
+ ctx.strokeStyle = this.color;
717
+ ctx.lineWidth = 1.5;
718
+ ctx.lineJoin = "round";
719
+ let started = false;
720
+ for (const r of results) {
721
+ if (r.value == null) {
722
+ started = false;
723
+ continue;
724
+ }
725
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
726
+ const y = scale.yToPixel(r.value, range, paneHeight);
727
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
728
+ }
729
+ ctx.stroke();
730
+ ctx.beginPath();
731
+ ctx.strokeStyle = "#ff6b35";
732
+ ctx.lineWidth = 1;
733
+ ctx.lineJoin = "round";
734
+ started = false;
735
+ for (const r of results) {
736
+ if (r.upper == null) {
737
+ started = false;
738
+ continue;
739
+ }
740
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
741
+ const y = scale.yToPixel(r.upper, range, paneHeight);
742
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
743
+ }
744
+ ctx.stroke();
745
+ }
746
+ };
747
+
748
+ // src/indicators/Stochastic.ts
749
+ var Stochastic = class {
750
+ constructor(kPeriod = 14, dPeriod = 3, color = "#e040fb") {
751
+ this.kPeriod = kPeriod;
752
+ this.dPeriod = dPeriod;
753
+ this.name = "Stoch";
754
+ this.pane = "sub";
755
+ this.paneHeight = 80;
756
+ this.color = color;
757
+ }
758
+ compute(data) {
759
+ const results = [];
760
+ const kValues = [];
761
+ for (let i = 0; i < data.length; i++) {
762
+ if (i < this.kPeriod - 1) {
763
+ kValues.push(null);
764
+ results.push({ index: i, value: null, upper: null });
765
+ continue;
766
+ }
767
+ let lowestLow = Infinity;
768
+ let highestHigh = -Infinity;
769
+ for (let j = i - this.kPeriod + 1; j <= i; j++) {
770
+ if (data[j].low < lowestLow) lowestLow = data[j].low;
771
+ if (data[j].high > highestHigh) highestHigh = data[j].high;
772
+ }
773
+ const denom = highestHigh - lowestLow;
774
+ const k = denom === 0 ? 50 : (data[i].close - lowestLow) / denom * 100;
775
+ kValues.push(k);
776
+ results.push({ index: i, value: k, upper: null });
777
+ }
778
+ for (let i = 0; i < data.length; i++) {
779
+ const kVal = kValues[i];
780
+ if (kVal === null) continue;
781
+ let count = 0;
782
+ let sum = 0;
783
+ for (let j = i; j >= 0 && count < this.dPeriod; j--) {
784
+ if (kValues[j] === null) break;
785
+ sum += kValues[j];
786
+ count++;
787
+ }
788
+ if (count === this.dPeriod) {
789
+ results[i].upper = sum / this.dPeriod;
790
+ }
791
+ }
792
+ return results;
793
+ }
794
+ getValueRange(_results) {
795
+ return { min: 0, max: 100 };
796
+ }
797
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
798
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
799
+ const range = rangeOverride ?? { min: 0, max: 100 };
800
+ const y80 = scale.yToPixel(80, range, paneHeight);
801
+ const y20 = scale.yToPixel(20, range, paneHeight);
802
+ ctx.fillStyle = "rgba(239,83,80,0.06)";
803
+ ctx.fillRect(0, 0, chartWidth, y80);
804
+ ctx.fillStyle = "rgba(38,166,154,0.06)";
805
+ ctx.fillRect(0, y20, chartWidth, paneHeight - y20);
806
+ ctx.save();
807
+ ctx.setLineDash([4, 4]);
808
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
809
+ ctx.lineWidth = 1;
810
+ for (const level of [80, 50, 20]) {
811
+ const y = scale.yToPixel(level, range, paneHeight);
812
+ ctx.beginPath();
813
+ ctx.moveTo(0, y);
814
+ ctx.lineTo(chartWidth, y);
815
+ ctx.stroke();
816
+ }
817
+ ctx.setLineDash([]);
818
+ ctx.restore();
819
+ ctx.beginPath();
820
+ ctx.strokeStyle = this.color;
821
+ ctx.lineWidth = 1.5;
822
+ ctx.lineJoin = "round";
823
+ let started = false;
824
+ for (const r of results) {
825
+ if (r.value === null) {
826
+ started = false;
827
+ continue;
828
+ }
829
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
830
+ const y = scale.yToPixel(r.value, range, paneHeight);
831
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
832
+ }
833
+ ctx.stroke();
834
+ ctx.beginPath();
835
+ ctx.strokeStyle = "#ff9800";
836
+ ctx.lineWidth = 1;
837
+ ctx.lineJoin = "round";
838
+ started = false;
839
+ for (const r of results) {
840
+ if (r.upper == null) {
841
+ started = false;
842
+ continue;
843
+ }
844
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
845
+ const y = scale.yToPixel(r.upper, range, paneHeight);
846
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
847
+ }
848
+ ctx.stroke();
849
+ }
850
+ };
851
+
852
+ // src/indicators/StochRSI.ts
853
+ function computeSMA(values, period) {
854
+ const result = [];
855
+ for (let i = 0; i < values.length; i++) {
856
+ if (i < period - 1) {
857
+ result.push(null);
858
+ continue;
859
+ }
860
+ let sum = 0;
861
+ let valid = true;
862
+ for (let j = i - period + 1; j <= i; j++) {
863
+ if (values[j] === null) {
864
+ valid = false;
865
+ break;
866
+ }
867
+ sum += values[j];
868
+ }
869
+ result.push(valid ? sum / period : null);
870
+ }
871
+ return result;
872
+ }
873
+ var StochRSI = class {
874
+ constructor(rsiPeriod = 14, stochPeriod = 14, kPeriod = 3, dPeriod = 3, color = "#00e5ff") {
875
+ this.rsiPeriod = rsiPeriod;
876
+ this.stochPeriod = stochPeriod;
877
+ this.kPeriod = kPeriod;
878
+ this.dPeriod = dPeriod;
879
+ this.name = "StochRSI";
880
+ this.pane = "sub";
881
+ this.paneHeight = 80;
882
+ this.color = color;
883
+ }
884
+ compute(data) {
885
+ const rsiValues = [];
886
+ if (data.length < this.rsiPeriod + 1) {
887
+ return data.map((_, i) => ({ index: i, value: null, upper: null }));
888
+ }
889
+ let avgGain = 0;
890
+ let avgLoss = 0;
891
+ for (let i = 1; i <= this.rsiPeriod; i++) {
892
+ const delta = data[i].close - data[i - 1].close;
893
+ if (delta > 0) avgGain += delta;
894
+ else avgLoss += Math.abs(delta);
895
+ }
896
+ avgGain /= this.rsiPeriod;
897
+ avgLoss /= this.rsiPeriod;
898
+ for (let i = 0; i < this.rsiPeriod; i++) rsiValues.push(null);
899
+ const rs0 = avgLoss === 0 ? Infinity : avgGain / avgLoss;
900
+ rsiValues.push(avgLoss === 0 ? 100 : 100 - 100 / (1 + rs0));
901
+ for (let i = this.rsiPeriod + 1; i < data.length; i++) {
902
+ const delta = data[i].close - data[i - 1].close;
903
+ const gain = delta > 0 ? delta : 0;
904
+ const loss = delta < 0 ? Math.abs(delta) : 0;
905
+ avgGain = (avgGain * (this.rsiPeriod - 1) + gain) / this.rsiPeriod;
906
+ avgLoss = (avgLoss * (this.rsiPeriod - 1) + loss) / this.rsiPeriod;
907
+ const rs = avgLoss === 0 ? Infinity : avgGain / avgLoss;
908
+ rsiValues.push(avgLoss === 0 ? 100 : 100 - 100 / (1 + rs));
909
+ }
910
+ const stochRsiValues = [];
911
+ for (let i = 0; i < data.length; i++) {
912
+ if (rsiValues[i] === null) {
913
+ stochRsiValues.push(null);
914
+ continue;
915
+ }
916
+ if (i < this.rsiPeriod + this.stochPeriod - 1) {
917
+ stochRsiValues.push(null);
918
+ continue;
919
+ }
920
+ let minRsi = Infinity;
921
+ let maxRsi = -Infinity;
922
+ let valid = true;
923
+ for (let j = i - this.stochPeriod + 1; j <= i; j++) {
924
+ if (rsiValues[j] === null) {
925
+ valid = false;
926
+ break;
927
+ }
928
+ const v = rsiValues[j];
929
+ if (v < minRsi) minRsi = v;
930
+ if (v > maxRsi) maxRsi = v;
931
+ }
932
+ if (!valid) {
933
+ stochRsiValues.push(null);
934
+ continue;
935
+ }
936
+ const denom = maxRsi - minRsi;
937
+ const stoch = denom === 0 ? 50 : (rsiValues[i] - minRsi) / denom * 100;
938
+ stochRsiValues.push(stoch);
939
+ }
940
+ const kLine = computeSMA(stochRsiValues, this.kPeriod);
941
+ const dLine = computeSMA(kLine, this.dPeriod);
942
+ return data.map((_, i) => ({
943
+ index: i,
944
+ value: kLine[i] ?? null,
945
+ upper: dLine[i] ?? null
946
+ }));
947
+ }
948
+ getValueRange(_results) {
949
+ return { min: 0, max: 100 };
950
+ }
951
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
952
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
953
+ const range = rangeOverride ?? { min: 0, max: 100 };
954
+ const y80 = scale.yToPixel(80, range, paneHeight);
955
+ const y20 = scale.yToPixel(20, range, paneHeight);
956
+ ctx.fillStyle = "rgba(239,83,80,0.06)";
957
+ ctx.fillRect(0, 0, chartWidth, y80);
958
+ ctx.fillStyle = "rgba(38,166,154,0.06)";
959
+ ctx.fillRect(0, y20, chartWidth, paneHeight - y20);
960
+ ctx.save();
961
+ ctx.setLineDash([4, 4]);
962
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
963
+ ctx.lineWidth = 1;
964
+ for (const level of [80, 50, 20]) {
965
+ const y = scale.yToPixel(level, range, paneHeight);
966
+ ctx.beginPath();
967
+ ctx.moveTo(0, y);
968
+ ctx.lineTo(chartWidth, y);
969
+ ctx.stroke();
970
+ }
971
+ ctx.setLineDash([]);
972
+ ctx.restore();
973
+ ctx.beginPath();
974
+ ctx.strokeStyle = this.color;
975
+ ctx.lineWidth = 1.5;
976
+ ctx.lineJoin = "round";
977
+ let started = false;
978
+ for (const r of results) {
979
+ if (r.value === null) {
980
+ started = false;
981
+ continue;
982
+ }
983
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
984
+ const y = scale.yToPixel(r.value, range, paneHeight);
985
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
986
+ }
987
+ ctx.stroke();
988
+ ctx.beginPath();
989
+ ctx.strokeStyle = "#ff9800";
990
+ ctx.lineWidth = 1;
991
+ ctx.lineJoin = "round";
992
+ started = false;
993
+ for (const r of results) {
994
+ if (r.upper == null) {
995
+ started = false;
996
+ continue;
997
+ }
998
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
999
+ const y = scale.yToPixel(r.upper, range, paneHeight);
1000
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1001
+ }
1002
+ ctx.stroke();
1003
+ }
1004
+ };
1005
+
1006
+ // src/indicators/CCI.ts
1007
+ var CCI = class {
1008
+ constructor(period = 20, color = "#4db6ac") {
1009
+ this.period = period;
1010
+ this.name = "CCI";
1011
+ this.pane = "sub";
1012
+ this.paneHeight = 80;
1013
+ this.color = color;
1014
+ }
1015
+ compute(data) {
1016
+ return data.map((_, i) => {
1017
+ if (i < this.period - 1) return { index: i, value: null };
1018
+ const slice = data.slice(i - this.period + 1, i + 1);
1019
+ const tpSlice = slice.map((b) => (b.high + b.low + b.close) / 3);
1020
+ const tp = tpSlice[tpSlice.length - 1];
1021
+ const sma = tpSlice.reduce((s, v) => s + v, 0) / this.period;
1022
+ const meanDev = tpSlice.reduce((s, v) => s + Math.abs(v - sma), 0) / this.period;
1023
+ if (meanDev === 0) return { index: i, value: 0 };
1024
+ return { index: i, value: (tp - sma) / (0.015 * meanDev) };
1025
+ });
1026
+ }
1027
+ getValueRange(results) {
1028
+ let maxAbs = 1e-4;
1029
+ for (const r of results) {
1030
+ if (r.value != null) maxAbs = Math.max(maxAbs, Math.abs(r.value));
1031
+ }
1032
+ return { min: -maxAbs * 1.3, max: maxAbs * 1.3 };
1033
+ }
1034
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
1035
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
1036
+ let maxAbs = 1e-4;
1037
+ for (const r of results) {
1038
+ if (r.value != null) maxAbs = Math.max(maxAbs, Math.abs(r.value));
1039
+ }
1040
+ const range = rangeOverride ?? { min: -maxAbs * 1.3, max: maxAbs * 1.3 };
1041
+ const y100 = scale.yToPixel(100, range, paneHeight);
1042
+ const yNeg100 = scale.yToPixel(-100, range, paneHeight);
1043
+ ctx.fillStyle = "rgba(239,83,80,0.10)";
1044
+ ctx.fillRect(0, 0, chartWidth, y100);
1045
+ ctx.fillStyle = "rgba(38,166,154,0.10)";
1046
+ ctx.fillRect(0, yNeg100, chartWidth, paneHeight - yNeg100);
1047
+ ctx.save();
1048
+ ctx.setLineDash([4, 4]);
1049
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
1050
+ ctx.lineWidth = 1;
1051
+ for (const level of [100, 0, -100]) {
1052
+ const y = scale.yToPixel(level, range, paneHeight);
1053
+ ctx.beginPath();
1054
+ ctx.moveTo(0, y);
1055
+ ctx.lineTo(chartWidth, y);
1056
+ ctx.stroke();
1057
+ }
1058
+ ctx.setLineDash([]);
1059
+ ctx.restore();
1060
+ ctx.beginPath();
1061
+ ctx.strokeStyle = this.color;
1062
+ ctx.lineWidth = 1.5;
1063
+ ctx.lineJoin = "round";
1064
+ let started = false;
1065
+ for (const r of results) {
1066
+ if (r.value === null) {
1067
+ started = false;
1068
+ continue;
1069
+ }
1070
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1071
+ const y = scale.yToPixel(r.value, range, paneHeight);
1072
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1073
+ }
1074
+ ctx.stroke();
1075
+ }
1076
+ };
1077
+
1078
+ // src/indicators/WilliamsR.ts
1079
+ var WilliamsR = class {
1080
+ constructor(period = 14, color = "#ff7043") {
1081
+ this.period = period;
1082
+ this.name = "%R";
1083
+ this.pane = "sub";
1084
+ this.paneHeight = 80;
1085
+ this.color = color;
1086
+ }
1087
+ compute(data) {
1088
+ return data.map((_, i) => {
1089
+ if (i < this.period - 1) return { index: i, value: null };
1090
+ let highestHigh = -Infinity;
1091
+ let lowestLow = Infinity;
1092
+ for (let j = i - this.period + 1; j <= i; j++) {
1093
+ if (data[j].high > highestHigh) highestHigh = data[j].high;
1094
+ if (data[j].low < lowestLow) lowestLow = data[j].low;
1095
+ }
1096
+ const denom = highestHigh - lowestLow;
1097
+ const r = denom === 0 ? -50 : (highestHigh - data[i].close) / denom * -100;
1098
+ return { index: i, value: r };
1099
+ });
1100
+ }
1101
+ getValueRange(_results) {
1102
+ return { min: -100, max: 0 };
1103
+ }
1104
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
1105
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
1106
+ const range = rangeOverride ?? { min: -100, max: 0 };
1107
+ const yNeg20 = scale.yToPixel(-20, range, paneHeight);
1108
+ const yNeg80 = scale.yToPixel(-80, range, paneHeight);
1109
+ ctx.fillStyle = "rgba(239,83,80,0.06)";
1110
+ ctx.fillRect(0, 0, chartWidth, yNeg20);
1111
+ ctx.fillStyle = "rgba(38,166,154,0.06)";
1112
+ ctx.fillRect(0, yNeg80, chartWidth, paneHeight - yNeg80);
1113
+ ctx.save();
1114
+ ctx.setLineDash([4, 4]);
1115
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
1116
+ ctx.lineWidth = 1;
1117
+ for (const level of [-20, -50, -80]) {
1118
+ const y = scale.yToPixel(level, range, paneHeight);
1119
+ ctx.beginPath();
1120
+ ctx.moveTo(0, y);
1121
+ ctx.lineTo(chartWidth, y);
1122
+ ctx.stroke();
1123
+ }
1124
+ ctx.setLineDash([]);
1125
+ ctx.restore();
1126
+ ctx.beginPath();
1127
+ ctx.strokeStyle = this.color;
1128
+ ctx.lineWidth = 1.5;
1129
+ ctx.lineJoin = "round";
1130
+ let started = false;
1131
+ for (const r of results) {
1132
+ if (r.value === null) {
1133
+ started = false;
1134
+ continue;
1135
+ }
1136
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1137
+ const y = scale.yToPixel(r.value, range, paneHeight);
1138
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1139
+ }
1140
+ ctx.stroke();
1141
+ }
1142
+ };
1143
+
1144
+ // src/indicators/ROC.ts
1145
+ var ROC = class {
1146
+ constructor(period = 12, color = "#80cbc4", source = "close") {
1147
+ this.period = period;
1148
+ this.source = source;
1149
+ this.pane = "sub";
1150
+ this.paneHeight = 80;
1151
+ this.name = "ROC";
1152
+ this.color = color;
1153
+ }
1154
+ compute(data) {
1155
+ return data.map((_, i) => {
1156
+ if (i < this.period) return { index: i, value: null };
1157
+ const prev = getSource(data[i - this.period], this.source);
1158
+ if (prev === 0) return { index: i, value: null };
1159
+ return { index: i, value: (getSource(data[i], this.source) - prev) / prev * 100 };
1160
+ });
1161
+ }
1162
+ getValueRange(results) {
1163
+ let maxAbs = 1e-4;
1164
+ for (const r of results) {
1165
+ if (r.value != null) maxAbs = Math.max(maxAbs, Math.abs(r.value));
1166
+ }
1167
+ return { min: -maxAbs * 1.3, max: maxAbs * 1.3 };
1168
+ }
1169
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
1170
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
1171
+ let maxAbs = 1e-4;
1172
+ for (const r of results) {
1173
+ if (r.value != null) maxAbs = Math.max(maxAbs, Math.abs(r.value));
1174
+ }
1175
+ const range = rangeOverride ?? { min: -maxAbs * 1.3, max: maxAbs * 1.3 };
1176
+ const zeroY = scale.yToPixel(0, range, paneHeight);
1177
+ const points = [];
1178
+ for (const r of results) {
1179
+ if (r.value == null) continue;
1180
+ points.push({
1181
+ x: scale.xToPixel(r.index, viewport, chartWidth),
1182
+ y: scale.yToPixel(r.value, range, paneHeight),
1183
+ value: r.value
1184
+ });
1185
+ }
1186
+ if (points.length >= 2) {
1187
+ ctx.beginPath();
1188
+ ctx.moveTo(points[0].x, zeroY);
1189
+ for (const p of points) {
1190
+ ctx.lineTo(p.x, Math.min(p.y, zeroY));
1191
+ }
1192
+ ctx.lineTo(points[points.length - 1].x, zeroY);
1193
+ ctx.closePath();
1194
+ ctx.fillStyle = "rgba(38,166,154,0.05)";
1195
+ ctx.fill();
1196
+ ctx.beginPath();
1197
+ ctx.moveTo(points[0].x, zeroY);
1198
+ for (const p of points) {
1199
+ ctx.lineTo(p.x, Math.max(p.y, zeroY));
1200
+ }
1201
+ ctx.lineTo(points[points.length - 1].x, zeroY);
1202
+ ctx.closePath();
1203
+ ctx.fillStyle = "rgba(239,83,80,0.05)";
1204
+ ctx.fill();
1205
+ }
1206
+ ctx.save();
1207
+ ctx.setLineDash([4, 4]);
1208
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
1209
+ ctx.lineWidth = 1;
1210
+ ctx.beginPath();
1211
+ ctx.moveTo(0, zeroY);
1212
+ ctx.lineTo(chartWidth, zeroY);
1213
+ ctx.stroke();
1214
+ ctx.setLineDash([]);
1215
+ ctx.restore();
1216
+ ctx.beginPath();
1217
+ ctx.strokeStyle = this.color;
1218
+ ctx.lineWidth = 1.5;
1219
+ ctx.lineJoin = "round";
1220
+ let started = false;
1221
+ for (const r of results) {
1222
+ if (r.value === null) {
1223
+ started = false;
1224
+ continue;
1225
+ }
1226
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1227
+ const y = scale.yToPixel(r.value, range, paneHeight);
1228
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1229
+ }
1230
+ ctx.stroke();
1231
+ }
1232
+ };
1233
+
1234
+ // src/indicators/OBV.ts
1235
+ var OBV = class {
1236
+ constructor(color = "#aed581") {
1237
+ this.name = "OBV";
1238
+ this.pane = "sub";
1239
+ this.paneHeight = 80;
1240
+ this.color = color;
1241
+ }
1242
+ compute(data) {
1243
+ const results = [];
1244
+ let obv = 0;
1245
+ for (let i = 0; i < data.length; i++) {
1246
+ if (i === 0) {
1247
+ obv = data[i].volume ?? 0;
1248
+ } else {
1249
+ if (data[i].close > data[i - 1].close) {
1250
+ obv += data[i].volume ?? 0;
1251
+ } else if (data[i].close < data[i - 1].close) {
1252
+ obv -= data[i].volume ?? 0;
1253
+ }
1254
+ }
1255
+ results.push({ index: i, value: obv });
1256
+ }
1257
+ return results;
1258
+ }
1259
+ getValueRange(results) {
1260
+ let minVal = Infinity;
1261
+ let maxVal = -Infinity;
1262
+ for (const r of results) {
1263
+ if (r.value != null) {
1264
+ if (r.value < minVal) minVal = r.value;
1265
+ if (r.value > maxVal) maxVal = r.value;
1266
+ }
1267
+ }
1268
+ if (minVal === Infinity) return { min: 0, max: 1 };
1269
+ const padding = (maxVal - minVal) * 0.1 || 1;
1270
+ return { min: minVal - padding, max: maxVal + padding };
1271
+ }
1272
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
1273
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
1274
+ let minVal = Infinity;
1275
+ let maxVal = -Infinity;
1276
+ for (const r of results) {
1277
+ if (r.value != null) {
1278
+ if (r.value < minVal) minVal = r.value;
1279
+ if (r.value > maxVal) maxVal = r.value;
1280
+ }
1281
+ }
1282
+ if (minVal === Infinity) return;
1283
+ const padding = (maxVal - minVal) * 0.1 || 1;
1284
+ const range = rangeOverride ?? { min: minVal - padding, max: maxVal + padding };
1285
+ const zeroY = scale.yToPixel(0, range, paneHeight);
1286
+ const points = [];
1287
+ for (const r of results) {
1288
+ if (r.value == null) continue;
1289
+ points.push({
1290
+ x: scale.xToPixel(r.index, viewport, chartWidth),
1291
+ y: scale.yToPixel(r.value, range, paneHeight),
1292
+ value: r.value
1293
+ });
1294
+ }
1295
+ if (points.length === 0) return;
1296
+ ctx.save();
1297
+ ctx.setLineDash([4, 4]);
1298
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
1299
+ ctx.lineWidth = 1;
1300
+ ctx.beginPath();
1301
+ ctx.moveTo(0, zeroY);
1302
+ ctx.lineTo(chartWidth, zeroY);
1303
+ ctx.stroke();
1304
+ ctx.setLineDash([]);
1305
+ ctx.restore();
1306
+ if (points.length >= 2) {
1307
+ ctx.beginPath();
1308
+ ctx.moveTo(points[0].x, zeroY);
1309
+ for (const p of points) ctx.lineTo(p.x, p.y);
1310
+ ctx.lineTo(points[points.length - 1].x, zeroY);
1311
+ ctx.closePath();
1312
+ const lastVal = points[points.length - 1].value;
1313
+ ctx.fillStyle = lastVal >= 0 ? "rgba(38,166,154,0.12)" : "rgba(239,83,80,0.12)";
1314
+ ctx.fill();
1315
+ }
1316
+ ctx.beginPath();
1317
+ ctx.strokeStyle = this.color;
1318
+ ctx.lineWidth = 1.5;
1319
+ ctx.lineJoin = "round";
1320
+ let started = false;
1321
+ for (const r of results) {
1322
+ if (r.value == null) {
1323
+ started = false;
1324
+ continue;
1325
+ }
1326
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1327
+ const y = scale.yToPixel(r.value, range, paneHeight);
1328
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1329
+ }
1330
+ ctx.stroke();
1331
+ }
1332
+ };
1333
+
1334
+ // src/indicators/AccDistribution.ts
1335
+ var AccDistribution = class {
1336
+ constructor(color = "#ce93d8") {
1337
+ this.name = "A/D";
1338
+ this.pane = "sub";
1339
+ this.paneHeight = 80;
1340
+ this.color = color;
1341
+ }
1342
+ compute(data) {
1343
+ const results = [];
1344
+ let ad = 0;
1345
+ for (let i = 0; i < data.length; i++) {
1346
+ const { high, low, close, volume } = data[i];
1347
+ const denom = high - low;
1348
+ const mfm = denom === 0 ? 0 : (close - low - (high - close)) / denom;
1349
+ ad += mfm * (volume ?? 0);
1350
+ results.push({ index: i, value: ad });
1351
+ }
1352
+ return results;
1353
+ }
1354
+ getValueRange(results) {
1355
+ let minVal = Infinity;
1356
+ let maxVal = -Infinity;
1357
+ for (const r of results) {
1358
+ if (r.value != null) {
1359
+ if (r.value < minVal) minVal = r.value;
1360
+ if (r.value > maxVal) maxVal = r.value;
1361
+ }
1362
+ }
1363
+ if (minVal === Infinity) return { min: 0, max: 1 };
1364
+ const padding = (maxVal - minVal) * 0.1 || 1;
1365
+ return { min: minVal - padding, max: maxVal + padding };
1366
+ }
1367
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
1368
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
1369
+ let minVal = Infinity;
1370
+ let maxVal = -Infinity;
1371
+ for (const r of results) {
1372
+ if (r.value != null) {
1373
+ if (r.value < minVal) minVal = r.value;
1374
+ if (r.value > maxVal) maxVal = r.value;
1375
+ }
1376
+ }
1377
+ if (minVal === Infinity) return;
1378
+ const padding = (maxVal - minVal) * 0.1 || 1;
1379
+ const range = rangeOverride ?? { min: minVal - padding, max: maxVal + padding };
1380
+ const points = [];
1381
+ for (const r of results) {
1382
+ if (r.value == null) continue;
1383
+ points.push({
1384
+ x: scale.xToPixel(r.index, viewport, chartWidth),
1385
+ y: scale.yToPixel(r.value, range, paneHeight)
1386
+ });
1387
+ }
1388
+ if (points.length === 0) return;
1389
+ if (points.length >= 2) {
1390
+ const baseY = paneHeight;
1391
+ ctx.beginPath();
1392
+ ctx.moveTo(points[0].x, baseY);
1393
+ for (const p of points) ctx.lineTo(p.x, p.y);
1394
+ ctx.lineTo(points[points.length - 1].x, baseY);
1395
+ ctx.closePath();
1396
+ ctx.fillStyle = this.color + "20";
1397
+ ctx.fill();
1398
+ }
1399
+ ctx.beginPath();
1400
+ ctx.strokeStyle = this.color;
1401
+ ctx.lineWidth = 1.5;
1402
+ ctx.lineJoin = "round";
1403
+ let started = false;
1404
+ for (const r of results) {
1405
+ if (r.value == null) {
1406
+ started = false;
1407
+ continue;
1408
+ }
1409
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1410
+ const y = scale.yToPixel(r.value, range, paneHeight);
1411
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1412
+ }
1413
+ ctx.stroke();
1414
+ }
1415
+ };
1416
+
1417
+ // src/indicators/MFI.ts
1418
+ var MFI = class {
1419
+ constructor(period = 14, color = "#ffab40") {
1420
+ this.period = period;
1421
+ this.pane = "sub";
1422
+ this.paneHeight = 80;
1423
+ this.name = "MFI";
1424
+ this.color = color;
1425
+ }
1426
+ compute(data) {
1427
+ const results = [];
1428
+ const tp = data.map((b) => (b.high + b.low + b.close) / 3);
1429
+ const rawMF = data.map((b, i) => tp[i] * (b.volume ?? 0));
1430
+ for (let i = 0; i < data.length; i++) {
1431
+ if (i < this.period) {
1432
+ results.push({ index: i, value: null });
1433
+ continue;
1434
+ }
1435
+ let posSum = 0;
1436
+ let negSum = 0;
1437
+ for (let j = i - this.period + 1; j <= i; j++) {
1438
+ if (tp[j] > tp[j - 1]) {
1439
+ posSum += rawMF[j];
1440
+ } else if (tp[j] < tp[j - 1]) {
1441
+ negSum += rawMF[j];
1442
+ }
1443
+ }
1444
+ if (negSum === 0) {
1445
+ results.push({ index: i, value: 100 });
1446
+ } else {
1447
+ const mfRatio = posSum / negSum;
1448
+ results.push({ index: i, value: 100 - 100 / (1 + mfRatio) });
1449
+ }
1450
+ }
1451
+ return results;
1452
+ }
1453
+ getValueRange(_results) {
1454
+ return { min: 0, max: 100 };
1455
+ }
1456
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
1457
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
1458
+ const range = rangeOverride ?? { min: 0, max: 100 };
1459
+ const y80 = scale.yToPixel(80, range, paneHeight);
1460
+ const y20 = scale.yToPixel(20, range, paneHeight);
1461
+ ctx.fillStyle = "rgba(239,83,80,0.06)";
1462
+ ctx.fillRect(0, 0, chartWidth, y80);
1463
+ ctx.fillStyle = "rgba(38,166,154,0.06)";
1464
+ ctx.fillRect(0, y20, chartWidth, paneHeight - y20);
1465
+ ctx.save();
1466
+ ctx.setLineDash([4, 4]);
1467
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
1468
+ ctx.lineWidth = 1;
1469
+ for (const level of [80, 50, 20]) {
1470
+ const y = scale.yToPixel(level, range, paneHeight);
1471
+ ctx.beginPath();
1472
+ ctx.moveTo(0, y);
1473
+ ctx.lineTo(chartWidth, y);
1474
+ ctx.stroke();
1475
+ }
1476
+ ctx.setLineDash([]);
1477
+ ctx.restore();
1478
+ ctx.beginPath();
1479
+ ctx.strokeStyle = this.color;
1480
+ ctx.lineWidth = 1.5;
1481
+ ctx.lineJoin = "round";
1482
+ let started = false;
1483
+ for (const r of results) {
1484
+ if (r.value === null) {
1485
+ started = false;
1486
+ continue;
1487
+ }
1488
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1489
+ const y = scale.yToPixel(r.value, range, paneHeight);
1490
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1491
+ }
1492
+ ctx.stroke();
1493
+ }
1494
+ };
1495
+
1496
+ // src/indicators/CMF.ts
1497
+ var CMF = class {
1498
+ constructor(period = 20, color = "#80deea") {
1499
+ this.period = period;
1500
+ this.pane = "sub";
1501
+ this.paneHeight = 80;
1502
+ this.name = "CMF";
1503
+ this.color = color;
1504
+ }
1505
+ compute(data) {
1506
+ const mfv = data.map((b) => {
1507
+ const denom = b.high - b.low;
1508
+ const mfm = denom === 0 ? 0 : (b.close - b.low - (b.high - b.close)) / denom;
1509
+ return mfm * (b.volume ?? 0);
1510
+ });
1511
+ return data.map((b, i) => {
1512
+ if (i < this.period - 1) return { index: i, value: null };
1513
+ let sumMFV = 0;
1514
+ let sumVol = 0;
1515
+ for (let j = i - this.period + 1; j <= i; j++) {
1516
+ sumMFV += mfv[j];
1517
+ sumVol += data[j].volume ?? 0;
1518
+ }
1519
+ const cmf = sumVol === 0 ? 0 : sumMFV / sumVol;
1520
+ return { index: i, value: cmf };
1521
+ });
1522
+ }
1523
+ getValueRange(_results) {
1524
+ return { min: -1, max: 1 };
1525
+ }
1526
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
1527
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
1528
+ const range = rangeOverride ?? { min: -1, max: 1 };
1529
+ const zeroY = scale.yToPixel(0, range, paneHeight);
1530
+ const y02 = scale.yToPixel(0.2, range, paneHeight);
1531
+ const yN02 = scale.yToPixel(-0.2, range, paneHeight);
1532
+ ctx.fillStyle = "rgba(38,166,154,0.08)";
1533
+ ctx.fillRect(0, 0, chartWidth, y02);
1534
+ ctx.fillStyle = "rgba(239,83,80,0.08)";
1535
+ ctx.fillRect(0, yN02, chartWidth, paneHeight - yN02);
1536
+ ctx.save();
1537
+ ctx.setLineDash([4, 4]);
1538
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
1539
+ ctx.lineWidth = 1;
1540
+ for (const level of [0.2, 0, -0.2]) {
1541
+ const y = scale.yToPixel(level, range, paneHeight);
1542
+ ctx.beginPath();
1543
+ ctx.moveTo(0, y);
1544
+ ctx.lineTo(chartWidth, y);
1545
+ ctx.stroke();
1546
+ }
1547
+ ctx.setLineDash([]);
1548
+ ctx.restore();
1549
+ const bodyW = Math.max(1, chartWidth / Math.max(results.length, 1) * 0.6);
1550
+ for (const r of results) {
1551
+ if (r.value == null) continue;
1552
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1553
+ const barY = scale.yToPixel(r.value, range, paneHeight);
1554
+ const h = Math.abs(barY - zeroY);
1555
+ const top = Math.min(barY, zeroY);
1556
+ ctx.fillStyle = r.value >= 0 ? "rgba(38,166,154,0.45)" : "rgba(239,83,80,0.45)";
1557
+ ctx.fillRect(x - bodyW / 2, top, bodyW, h);
1558
+ }
1559
+ ctx.beginPath();
1560
+ ctx.strokeStyle = this.color;
1561
+ ctx.lineWidth = 1.5;
1562
+ ctx.lineJoin = "round";
1563
+ let started = false;
1564
+ for (const r of results) {
1565
+ if (r.value === null) {
1566
+ started = false;
1567
+ continue;
1568
+ }
1569
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1570
+ const y = scale.yToPixel(r.value, range, paneHeight);
1571
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1572
+ }
1573
+ ctx.stroke();
1574
+ }
1575
+ };
1576
+
1577
+ // src/indicators/ADX.ts
1578
+ var ADX = class {
1579
+ constructor(period = 14, color = "#fff176") {
1580
+ this.period = period;
1581
+ this.pane = "sub";
1582
+ this.paneHeight = 80;
1583
+ this.name = "ADX";
1584
+ this.color = color;
1585
+ }
1586
+ compute(data) {
1587
+ const n = data.length;
1588
+ if (n < this.period + 1) {
1589
+ return data.map((_, i) => ({ index: i, value: null, upper: null, lower: null }));
1590
+ }
1591
+ const tr = new Array(n).fill(0);
1592
+ const dmP = new Array(n).fill(0);
1593
+ const dmM = new Array(n).fill(0);
1594
+ tr[0] = data[0].high - data[0].low;
1595
+ dmP[0] = 0;
1596
+ dmM[0] = 0;
1597
+ for (let i = 1; i < n; i++) {
1598
+ const prevClose = data[i - 1].close;
1599
+ const prevHigh = data[i - 1].high;
1600
+ const prevLow = data[i - 1].low;
1601
+ const currHigh = data[i].high;
1602
+ const currLow = data[i].low;
1603
+ tr[i] = Math.max(
1604
+ currHigh - currLow,
1605
+ Math.abs(currHigh - prevClose),
1606
+ Math.abs(currLow - prevClose)
1607
+ );
1608
+ const upMove = currHigh - prevHigh;
1609
+ const downMove = prevLow - currLow;
1610
+ dmP[i] = upMove > downMove && upMove > 0 ? upMove : 0;
1611
+ dmM[i] = downMove > upMove && downMove > 0 ? downMove : 0;
1612
+ }
1613
+ let smoothTR = 0;
1614
+ let smoothDP = 0;
1615
+ let smoothDM = 0;
1616
+ for (let i = 0; i < this.period; i++) {
1617
+ smoothTR += tr[i];
1618
+ smoothDP += dmP[i];
1619
+ smoothDM += dmM[i];
1620
+ }
1621
+ const results = [];
1622
+ for (let i = 0; i < this.period; i++) {
1623
+ results.push({ index: i, value: null, upper: null, lower: null });
1624
+ }
1625
+ const diPArr = new Array(n).fill(0);
1626
+ const diMArr = new Array(n).fill(0);
1627
+ const dxArr = new Array(n).fill(0);
1628
+ const diP0 = smoothTR === 0 ? 0 : 100 * smoothDP / smoothTR;
1629
+ const diM0 = smoothTR === 0 ? 0 : 100 * smoothDM / smoothTR;
1630
+ const dx0 = diP0 + diM0 === 0 ? 0 : 100 * Math.abs(diP0 - diM0) / (diP0 + diM0);
1631
+ diPArr[this.period - 1] = diP0;
1632
+ diMArr[this.period - 1] = diM0;
1633
+ dxArr[this.period - 1] = dx0;
1634
+ results[this.period - 1] = {
1635
+ index: this.period - 1,
1636
+ value: null,
1637
+ // ADX not ready yet
1638
+ upper: diP0,
1639
+ lower: diM0
1640
+ };
1641
+ for (let i = this.period; i < n; i++) {
1642
+ smoothTR = smoothTR - smoothTR / this.period + tr[i];
1643
+ smoothDP = smoothDP - smoothDP / this.period + dmP[i];
1644
+ smoothDM = smoothDM - smoothDM / this.period + dmM[i];
1645
+ const diP = smoothTR === 0 ? 0 : 100 * smoothDP / smoothTR;
1646
+ const diM = smoothTR === 0 ? 0 : 100 * smoothDM / smoothTR;
1647
+ const dx = diP + diM === 0 ? 0 : 100 * Math.abs(diP - diM) / (diP + diM);
1648
+ diPArr[i] = diP;
1649
+ diMArr[i] = diM;
1650
+ dxArr[i] = dx;
1651
+ results.push({ index: i, value: null, upper: diP, lower: diM });
1652
+ }
1653
+ const adxStart = 2 * this.period - 2;
1654
+ if (adxStart >= n) return results;
1655
+ let adx = 0;
1656
+ for (let i = this.period - 1; i <= adxStart; i++) {
1657
+ adx += dxArr[i];
1658
+ }
1659
+ adx /= this.period;
1660
+ results[adxStart].value = adx;
1661
+ for (let i = adxStart + 1; i < n; i++) {
1662
+ adx = (adx * (this.period - 1) + dxArr[i]) / this.period;
1663
+ results[i].value = adx;
1664
+ }
1665
+ return results;
1666
+ }
1667
+ getValueRange(_results) {
1668
+ return { min: 0, max: 100 };
1669
+ }
1670
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
1671
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
1672
+ const range = rangeOverride ?? { min: 0, max: 100 };
1673
+ ctx.save();
1674
+ ctx.setLineDash([4, 4]);
1675
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
1676
+ ctx.lineWidth = 1;
1677
+ const y25 = scale.yToPixel(25, range, paneHeight);
1678
+ ctx.beginPath();
1679
+ ctx.moveTo(0, y25);
1680
+ ctx.lineTo(chartWidth, y25);
1681
+ ctx.stroke();
1682
+ ctx.setLineDash([]);
1683
+ ctx.restore();
1684
+ const drawLine = (getValue, strokeColor, lineWidth) => {
1685
+ ctx.beginPath();
1686
+ ctx.strokeStyle = strokeColor;
1687
+ ctx.lineWidth = lineWidth;
1688
+ ctx.lineJoin = "round";
1689
+ let started = false;
1690
+ for (const r of results) {
1691
+ const val = getValue(r);
1692
+ if (val == null) {
1693
+ started = false;
1694
+ continue;
1695
+ }
1696
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1697
+ const y = scale.yToPixel(val, range, paneHeight);
1698
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1699
+ }
1700
+ ctx.stroke();
1701
+ };
1702
+ drawLine((r) => r.upper, "#26a69a", 1);
1703
+ drawLine((r) => r.lower, "#ef5350", 1);
1704
+ drawLine((r) => r.value, this.color, 1.5);
1705
+ }
1706
+ };
1707
+
1708
+ // src/indicators/AwesomeOscillator.ts
1709
+ var AwesomeOscillator = class {
1710
+ constructor(fastPeriod = 5, slowPeriod = 34, color = "#b0bec5") {
1711
+ this.fastPeriod = fastPeriod;
1712
+ this.slowPeriod = slowPeriod;
1713
+ this.name = "AO";
1714
+ this.pane = "sub";
1715
+ this.paneHeight = 80;
1716
+ this.color = color;
1717
+ }
1718
+ compute(data) {
1719
+ const mid = data.map((b) => (b.high + b.low) / 2);
1720
+ const sma = (arr, end, period) => {
1721
+ if (end < period - 1) return null;
1722
+ let sum = 0;
1723
+ for (let j = end - period + 1; j <= end; j++) sum += arr[j];
1724
+ return sum / period;
1725
+ };
1726
+ return data.map((_, i) => {
1727
+ const fast = sma(mid, i, this.fastPeriod);
1728
+ const slow = sma(mid, i, this.slowPeriod);
1729
+ if (fast === null || slow === null) return { index: i, value: null };
1730
+ return { index: i, value: fast - slow };
1731
+ });
1732
+ }
1733
+ getValueRange(results) {
1734
+ let maxAbs = 1e-4;
1735
+ for (const r of results) {
1736
+ if (r.value != null) maxAbs = Math.max(maxAbs, Math.abs(r.value));
1737
+ }
1738
+ return { min: -maxAbs * 1.2, max: maxAbs * 1.2 };
1739
+ }
1740
+ render(ctx, results, scale, viewport, paneHeight, rangeOverride) {
1741
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
1742
+ let maxAbs = 1e-4;
1743
+ for (const r of results) {
1744
+ if (r.value != null) maxAbs = Math.max(maxAbs, Math.abs(r.value));
1745
+ }
1746
+ const range = rangeOverride ?? { min: -maxAbs * 1.2, max: maxAbs * 1.2 };
1747
+ const zeroY = scale.yToPixel(0, range, paneHeight);
1748
+ ctx.save();
1749
+ ctx.setLineDash([4, 4]);
1750
+ ctx.strokeStyle = "rgba(150,150,150,0.35)";
1751
+ ctx.lineWidth = 1;
1752
+ ctx.beginPath();
1753
+ ctx.moveTo(0, zeroY);
1754
+ ctx.lineTo(chartWidth, zeroY);
1755
+ ctx.stroke();
1756
+ ctx.setLineDash([]);
1757
+ ctx.restore();
1758
+ const bodyW = Math.max(scale.candleBodyWidth(viewport, chartWidth), 1);
1759
+ let prevValue = null;
1760
+ for (const r of results) {
1761
+ if (r.value == null) {
1762
+ prevValue = null;
1763
+ continue;
1764
+ }
1765
+ const isGreen = prevValue === null ? true : r.value >= prevValue;
1766
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1767
+ const barY = scale.yToPixel(r.value, range, paneHeight);
1768
+ const h = Math.abs(barY - zeroY);
1769
+ const top = Math.min(barY, zeroY);
1770
+ ctx.fillStyle = isGreen ? "rgba(38,166,154,0.70)" : "rgba(239,83,80,0.70)";
1771
+ ctx.fillRect(x - bodyW / 2, top, bodyW, h);
1772
+ prevValue = r.value;
1773
+ }
1774
+ }
1775
+ };
1776
+
1777
+ // src/indicators/MARibbon.ts
1778
+ var RIBBON_PERIODS = [5, 10, 20, 50, 100];
1779
+ var RIBBON_COLORS = ["#ff6b6b", "#ffd93d", "#6bcb77", "#4d96ff", "#c77dff"];
1780
+ var MARibbon = class {
1781
+ constructor(color = "#ffcc80", source = "close") {
1782
+ this.source = source;
1783
+ this.name = "MARibbon";
1784
+ this.color = color;
1785
+ }
1786
+ compute(data) {
1787
+ const sma = (endIndex, period) => {
1788
+ if (endIndex < period - 1) return null;
1789
+ let sum = 0;
1790
+ for (let j = endIndex - period + 1; j <= endIndex; j++) sum += getSource(data[j], this.source);
1791
+ return sum / period;
1792
+ };
1793
+ return data.map((_, i) => ({
1794
+ index: i,
1795
+ value: sma(i, 5),
1796
+ // SMA5
1797
+ upper: sma(i, 10),
1798
+ // SMA10
1799
+ lower: sma(i, 20),
1800
+ // SMA20
1801
+ signal: sma(i, 50),
1802
+ // SMA50
1803
+ histogram: sma(i, 100)
1804
+ // SMA100
1805
+ }));
1806
+ }
1807
+ render(ctx, results, scale, viewport, chartHeight) {
1808
+ const canvas = ctx.canvas;
1809
+ const priceRange = canvas._priceRange;
1810
+ if (!priceRange) return;
1811
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
1812
+ ctx.lineWidth = 1;
1813
+ ctx.lineJoin = "round";
1814
+ const keys = ["value", "upper", "lower", "signal", "histogram"];
1815
+ for (let li = 0; li < RIBBON_PERIODS.length; li++) {
1816
+ const key = keys[li];
1817
+ const lineColor = RIBBON_COLORS[li];
1818
+ ctx.beginPath();
1819
+ ctx.strokeStyle = lineColor;
1820
+ let started = false;
1821
+ for (const r of results) {
1822
+ const val = r[key];
1823
+ if (val == null) {
1824
+ started = false;
1825
+ continue;
1826
+ }
1827
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1828
+ const y = scale.yToPixel(val, priceRange, chartHeight);
1829
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1830
+ }
1831
+ ctx.stroke();
1832
+ }
1833
+ }
1834
+ };
1835
+
1836
+ // src/indicators/DonchianChannels.ts
1837
+ var DonchianChannels = class {
1838
+ constructor(period = 20, color = "#26c6da") {
1839
+ this.period = period;
1840
+ this.name = "DC";
1841
+ this.color = color;
1842
+ }
1843
+ compute(data) {
1844
+ return data.map((_, i) => {
1845
+ if (i < this.period - 1) return { index: i, value: null, upper: null, lower: null };
1846
+ let highest = -Infinity;
1847
+ let lowest = Infinity;
1848
+ for (let j = i - this.period + 1; j <= i; j++) {
1849
+ if (data[j].high > highest) highest = data[j].high;
1850
+ if (data[j].low < lowest) lowest = data[j].low;
1851
+ }
1852
+ const middle = (highest + lowest) / 2;
1853
+ return { index: i, value: middle, upper: highest, lower: lowest };
1854
+ });
1855
+ }
1856
+ render(ctx, results, scale, viewport, chartHeight) {
1857
+ const canvas = ctx.canvas;
1858
+ const priceRange = canvas._priceRange;
1859
+ if (!priceRange) return;
1860
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
1861
+ ctx.lineWidth = 1;
1862
+ ctx.lineJoin = "round";
1863
+ const drawLine = (getValue, dash = false) => {
1864
+ if (dash) ctx.setLineDash([4, 3]);
1865
+ ctx.beginPath();
1866
+ let started = false;
1867
+ for (const r of results) {
1868
+ const val = getValue(r);
1869
+ if (val == null) {
1870
+ started = false;
1871
+ continue;
1872
+ }
1873
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1874
+ const y = scale.yToPixel(val, priceRange, chartHeight);
1875
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
1876
+ }
1877
+ ctx.stroke();
1878
+ if (dash) ctx.setLineDash([]);
1879
+ };
1880
+ ctx.strokeStyle = this.color;
1881
+ drawLine((r) => r.value, true);
1882
+ ctx.strokeStyle = `${this.color}99`;
1883
+ drawLine((r) => r.upper);
1884
+ drawLine((r) => r.lower);
1885
+ const upperPoints = [];
1886
+ const lowerPoints = [];
1887
+ for (const r of results) {
1888
+ if (r.upper == null || r.lower == null) continue;
1889
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1890
+ upperPoints.push([x, scale.yToPixel(r.upper, priceRange, chartHeight)]);
1891
+ lowerPoints.push([x, scale.yToPixel(r.lower, priceRange, chartHeight)]);
1892
+ }
1893
+ if (upperPoints.length < 2) return;
1894
+ ctx.beginPath();
1895
+ upperPoints.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
1896
+ lowerPoints.slice().reverse().forEach(([x, y]) => ctx.lineTo(x, y));
1897
+ ctx.closePath();
1898
+ ctx.fillStyle = `${this.color}14`;
1899
+ ctx.fill();
1900
+ }
1901
+ };
1902
+
1903
+ // src/indicators/Supertrend.ts
1904
+ var Supertrend = class {
1905
+ constructor(atrPeriod = 10, multiplier = 3, color = "#26a69a") {
1906
+ this.atrPeriod = atrPeriod;
1907
+ this.multiplier = multiplier;
1908
+ this.name = "Supertrend";
1909
+ this.color = color;
1910
+ }
1911
+ compute(data) {
1912
+ const n = data.length;
1913
+ const results = [];
1914
+ if (n === 0) return results;
1915
+ const tr = new Array(n).fill(0);
1916
+ for (let i = 0; i < n; i++) {
1917
+ const high = data[i].high;
1918
+ const low = data[i].low;
1919
+ const prevClose = i > 0 ? data[i - 1].close : data[i].close;
1920
+ tr[i] = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose));
1921
+ }
1922
+ const atr = new Array(n).fill(0);
1923
+ if (n < this.atrPeriod) {
1924
+ for (let i = 0; i < n; i++) results.push({ index: i, value: null, upper: null });
1925
+ return results;
1926
+ }
1927
+ let atrSeed = 0;
1928
+ for (let i = 0; i < this.atrPeriod; i++) atrSeed += tr[i];
1929
+ atr[this.atrPeriod - 1] = atrSeed / this.atrPeriod;
1930
+ for (let i = this.atrPeriod; i < n; i++) {
1931
+ atr[i] = (atr[i - 1] * (this.atrPeriod - 1) + tr[i]) / this.atrPeriod;
1932
+ }
1933
+ const finalUpperBand = new Array(n).fill(0);
1934
+ const finalLowerBand = new Array(n).fill(0);
1935
+ const direction = new Array(n).fill(1);
1936
+ for (let i = 0; i < n; i++) {
1937
+ if (i < this.atrPeriod - 1) {
1938
+ results.push({ index: i, value: null, upper: null });
1939
+ continue;
1940
+ }
1941
+ const hl2 = (data[i].high + data[i].low) / 2;
1942
+ const basicUpper = hl2 + this.multiplier * atr[i];
1943
+ const basicLower = hl2 - this.multiplier * atr[i];
1944
+ if (i === this.atrPeriod - 1) {
1945
+ finalUpperBand[i] = basicUpper;
1946
+ finalLowerBand[i] = basicLower;
1947
+ direction[i] = 1;
1948
+ results.push({ index: i, value: finalLowerBand[i], upper: 1 });
1949
+ continue;
1950
+ }
1951
+ const prevClose = data[i - 1].close;
1952
+ finalUpperBand[i] = basicUpper < finalUpperBand[i - 1] || prevClose > finalUpperBand[i - 1] ? basicUpper : finalUpperBand[i - 1];
1953
+ finalLowerBand[i] = basicLower > finalLowerBand[i - 1] || prevClose < finalLowerBand[i - 1] ? basicLower : finalLowerBand[i - 1];
1954
+ if (direction[i - 1] === -1) {
1955
+ direction[i] = data[i].close > finalUpperBand[i] ? 1 : -1;
1956
+ } else {
1957
+ direction[i] = data[i].close < finalLowerBand[i] ? -1 : 1;
1958
+ }
1959
+ const supertrendVal = direction[i] === 1 ? finalLowerBand[i] : finalUpperBand[i];
1960
+ results.push({ index: i, value: supertrendVal, upper: direction[i] });
1961
+ }
1962
+ return results;
1963
+ }
1964
+ render(ctx, results, scale, viewport, chartHeight) {
1965
+ const canvas = ctx.canvas;
1966
+ const priceRange = canvas._priceRange;
1967
+ if (!priceRange) return;
1968
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
1969
+ ctx.lineWidth = 2;
1970
+ ctx.lineJoin = "round";
1971
+ const BULLISH_COLOR = "#26a69a";
1972
+ const BEARISH_COLOR = "#ef5350";
1973
+ let prevDirection = null;
1974
+ ctx.beginPath();
1975
+ ctx.strokeStyle = BULLISH_COLOR;
1976
+ let startedBull = false;
1977
+ for (const r of results) {
1978
+ if (r.value == null || r.upper == null) {
1979
+ startedBull = false;
1980
+ continue;
1981
+ }
1982
+ const isBullish = r.upper >= 0;
1983
+ if (!isBullish) {
1984
+ startedBull = false;
1985
+ continue;
1986
+ }
1987
+ if (prevDirection !== 1) startedBull = false;
1988
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
1989
+ const y = scale.yToPixel(r.value, priceRange, chartHeight);
1990
+ startedBull ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), startedBull = true);
1991
+ prevDirection = 1;
1992
+ }
1993
+ ctx.stroke();
1994
+ ctx.beginPath();
1995
+ ctx.strokeStyle = BEARISH_COLOR;
1996
+ let startedBear = false;
1997
+ prevDirection = null;
1998
+ for (const r of results) {
1999
+ if (r.value == null || r.upper == null) {
2000
+ startedBear = false;
2001
+ continue;
2002
+ }
2003
+ const isBearish = r.upper < 0;
2004
+ if (!isBearish) {
2005
+ startedBear = false;
2006
+ continue;
2007
+ }
2008
+ if (prevDirection !== -1) startedBear = false;
2009
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
2010
+ const y = scale.yToPixel(r.value, priceRange, chartHeight);
2011
+ startedBear ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), startedBear = true);
2012
+ prevDirection = -1;
2013
+ }
2014
+ ctx.stroke();
2015
+ }
2016
+ };
2017
+
2018
+ // src/indicators/ParabolicSAR.ts
2019
+ var ParabolicSAR = class {
2020
+ constructor(step = 0.02, maxAF = 0.2, color = "#ff6b9d") {
2021
+ this.step = step;
2022
+ this.maxAF = maxAF;
2023
+ this.name = "PSAR";
2024
+ this.color = color;
2025
+ }
2026
+ compute(data) {
2027
+ const n = data.length;
2028
+ const results = [];
2029
+ if (n < 2) {
2030
+ for (let i = 0; i < n; i++) results.push({ index: i, value: null, upper: null });
2031
+ return results;
2032
+ }
2033
+ let bullish = true;
2034
+ let af = this.step;
2035
+ let ep = data[0].high;
2036
+ let sar = data[0].low;
2037
+ results.push({ index: 0, value: null, upper: null });
2038
+ for (let i = 1; i < n; i++) {
2039
+ let newSar = sar + af * (ep - sar);
2040
+ if (bullish) {
2041
+ const prevLow = data[i - 1].low;
2042
+ const prevPrevLow = i >= 2 ? data[i - 2].low : data[i - 1].low;
2043
+ newSar = Math.min(newSar, prevLow, prevPrevLow);
2044
+ if (newSar > data[i].low) {
2045
+ bullish = false;
2046
+ newSar = ep;
2047
+ ep = data[i].low;
2048
+ af = this.step;
2049
+ } else {
2050
+ if (data[i].high > ep) {
2051
+ ep = data[i].high;
2052
+ af = Math.min(af + this.step, this.maxAF);
2053
+ }
2054
+ }
2055
+ } else {
2056
+ const prevHigh = data[i - 1].high;
2057
+ const prevPrevHigh = i >= 2 ? data[i - 2].high : data[i - 1].high;
2058
+ newSar = Math.max(newSar, prevHigh, prevPrevHigh);
2059
+ if (newSar < data[i].high) {
2060
+ bullish = true;
2061
+ newSar = ep;
2062
+ ep = data[i].high;
2063
+ af = this.step;
2064
+ } else {
2065
+ if (data[i].low < ep) {
2066
+ ep = data[i].low;
2067
+ af = Math.min(af + this.step, this.maxAF);
2068
+ }
2069
+ }
2070
+ }
2071
+ sar = newSar;
2072
+ results.push({ index: i, value: sar, upper: bullish ? 1 : -1 });
2073
+ }
2074
+ return results;
2075
+ }
2076
+ render(ctx, results, scale, viewport, chartHeight) {
2077
+ const canvas = ctx.canvas;
2078
+ const priceRange = canvas._priceRange;
2079
+ if (!priceRange) return;
2080
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
2081
+ const BULLISH_COLOR = "#26a69a";
2082
+ const BEARISH_COLOR = "#ef5350";
2083
+ for (const r of results) {
2084
+ if (r.value == null || r.upper == null) continue;
2085
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
2086
+ const y = scale.yToPixel(r.value, priceRange, chartHeight);
2087
+ ctx.beginPath();
2088
+ ctx.arc(x, y, 2, 0, Math.PI * 2);
2089
+ ctx.fillStyle = r.upper >= 0 ? BULLISH_COLOR : BEARISH_COLOR;
2090
+ ctx.fill();
2091
+ }
2092
+ }
2093
+ };
2094
+
2095
+ // src/indicators/IchimokuCloud.ts
2096
+ function periodHighLow(data, index, period) {
2097
+ if (index < period - 1) return null;
2098
+ let high = -Infinity;
2099
+ let low = Infinity;
2100
+ for (let j = index - period + 1; j <= index; j++) {
2101
+ if (data[j].high > high) high = data[j].high;
2102
+ if (data[j].low < low) low = data[j].low;
2103
+ }
2104
+ return { high, low };
2105
+ }
2106
+ var IchimokuCloud = class {
2107
+ constructor(tenkanPeriod = 9, kijunPeriod = 26, senkouBPeriod = 52, color = "#b0bec5") {
2108
+ this.tenkanPeriod = tenkanPeriod;
2109
+ this.kijunPeriod = kijunPeriod;
2110
+ this.senkouBPeriod = senkouBPeriod;
2111
+ this.name = "Ichimoku";
2112
+ this.color = color;
2113
+ }
2114
+ compute(data) {
2115
+ return data.map((bar, i) => {
2116
+ const tenkanHL = periodHighLow(data, i, this.tenkanPeriod);
2117
+ const tenkan = tenkanHL !== null ? (tenkanHL.high + tenkanHL.low) / 2 : null;
2118
+ const kijunHL = periodHighLow(data, i, this.kijunPeriod);
2119
+ const kijun = kijunHL !== null ? (kijunHL.high + kijunHL.low) / 2 : null;
2120
+ const spanA = tenkan !== null && kijun !== null ? (tenkan + kijun) / 2 : null;
2121
+ const spanBHL = periodHighLow(data, i, this.senkouBPeriod);
2122
+ const spanB = spanBHL !== null ? (spanBHL.high + spanBHL.low) / 2 : null;
2123
+ const chikou = bar.close;
2124
+ return {
2125
+ index: i,
2126
+ value: tenkan,
2127
+ upper: kijun,
2128
+ lower: spanA,
2129
+ signal: spanB,
2130
+ histogram: chikou
2131
+ };
2132
+ });
2133
+ }
2134
+ render(ctx, results, scale, viewport, chartHeight) {
2135
+ const canvas = ctx.canvas;
2136
+ const priceRange = canvas._priceRange;
2137
+ if (!priceRange) return;
2138
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
2139
+ ctx.lineWidth = 1;
2140
+ ctx.lineJoin = "round";
2141
+ const drawLine = (getValue, color, dash = false) => {
2142
+ ctx.strokeStyle = color;
2143
+ if (dash) ctx.setLineDash([4, 3]);
2144
+ ctx.beginPath();
2145
+ let started2 = false;
2146
+ for (const r of results) {
2147
+ const val = getValue(r);
2148
+ if (val == null) {
2149
+ started2 = false;
2150
+ continue;
2151
+ }
2152
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
2153
+ const y = scale.yToPixel(val, priceRange, chartHeight);
2154
+ started2 ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started2 = true);
2155
+ }
2156
+ ctx.stroke();
2157
+ if (dash) ctx.setLineDash([]);
2158
+ };
2159
+ const spanAPoints = [];
2160
+ const spanBPoints = [];
2161
+ for (const r of results) {
2162
+ if (r.lower == null || r.signal == null) continue;
2163
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
2164
+ spanAPoints.push([x, scale.yToPixel(r.lower, priceRange, chartHeight)]);
2165
+ spanBPoints.push([x, scale.yToPixel(r.signal, priceRange, chartHeight)]);
2166
+ }
2167
+ for (let i = 0; i + 1 < spanAPoints.length; i++) {
2168
+ const [x0, a0] = spanAPoints[i];
2169
+ const [x1, a1] = spanAPoints[i + 1];
2170
+ const [, b0] = spanBPoints[i];
2171
+ const [, b1] = spanBPoints[i + 1];
2172
+ const spanAAbove = a0 < b0;
2173
+ ctx.beginPath();
2174
+ ctx.moveTo(x0, a0);
2175
+ ctx.lineTo(x1, a1);
2176
+ ctx.lineTo(x1, b1);
2177
+ ctx.lineTo(x0, b0);
2178
+ ctx.closePath();
2179
+ ctx.fillStyle = spanAAbove ? "rgba(38, 166, 154, 0.15)" : "rgba(239, 83, 80, 0.15)";
2180
+ ctx.fill();
2181
+ }
2182
+ drawLine((r) => r.lower, "#26a69a", true);
2183
+ drawLine((r) => r.signal, "#ef5350", true);
2184
+ drawLine((r) => r.value, "#ef9a9a");
2185
+ drawLine((r) => r.upper, "#90caf9");
2186
+ ctx.strokeStyle = "#b0bec5";
2187
+ ctx.beginPath();
2188
+ let started = false;
2189
+ for (const r of results) {
2190
+ if (r.histogram == null) {
2191
+ started = false;
2192
+ continue;
2193
+ }
2194
+ const shiftedIndex = r.index - this.kijunPeriod;
2195
+ if (shiftedIndex < 0) {
2196
+ started = false;
2197
+ continue;
2198
+ }
2199
+ const x = scale.xToPixel(shiftedIndex, viewport, chartWidth);
2200
+ const y = scale.yToPixel(r.histogram, priceRange, chartHeight);
2201
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
2202
+ }
2203
+ ctx.stroke();
2204
+ }
2205
+ };
2206
+
2207
+ // src/indicators/PivotPoints.ts
2208
+ var PivotPoints = class {
2209
+ constructor(color = "#ffd54f") {
2210
+ this.name = "Pivot";
2211
+ this.color = color;
2212
+ }
2213
+ compute(data) {
2214
+ return data.map((_, i) => {
2215
+ if (i === 0) return { index: i, value: null, upper: null, lower: null, signal: null, histogram: null };
2216
+ const prev = data[i - 1];
2217
+ const pp = (prev.high + prev.low + prev.close) / 3;
2218
+ const r1 = 2 * pp - prev.low;
2219
+ const s1 = 2 * pp - prev.high;
2220
+ const r2 = pp + (prev.high - prev.low);
2221
+ const s2 = pp - (prev.high - prev.low);
2222
+ return {
2223
+ index: i,
2224
+ value: pp,
2225
+ // PP
2226
+ upper: r1,
2227
+ // R1
2228
+ lower: s1,
2229
+ // S1
2230
+ signal: r2,
2231
+ // R2
2232
+ histogram: s2
2233
+ // S2
2234
+ };
2235
+ });
2236
+ }
2237
+ render(ctx, results, scale, viewport, chartHeight) {
2238
+ const canvas = ctx.canvas;
2239
+ const priceRange = canvas._priceRange;
2240
+ if (!priceRange) return;
2241
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
2242
+ let lastResult = null;
2243
+ for (let i = results.length - 1; i >= 0; i--) {
2244
+ if (results[i].value !== null) {
2245
+ lastResult = results[i];
2246
+ break;
2247
+ }
2248
+ }
2249
+ if (lastResult === null) return;
2250
+ const levels = [
2251
+ { value: lastResult.signal, label: "R2", color: "#b71c1c" },
2252
+ { value: lastResult.upper, label: "R1", color: "#ef5350" },
2253
+ { value: lastResult.value, label: "PP", color: "#ffd54f" },
2254
+ { value: lastResult.lower, label: "S1", color: "#26a69a" },
2255
+ { value: lastResult.histogram, label: "S2", color: "#1b5e20" }
2256
+ ];
2257
+ ctx.lineWidth = 1;
2258
+ ctx.font = "10px sans-serif";
2259
+ ctx.textBaseline = "middle";
2260
+ for (const level of levels) {
2261
+ if (level.value == null) continue;
2262
+ const y = scale.yToPixel(level.value, priceRange, chartHeight);
2263
+ ctx.beginPath();
2264
+ ctx.setLineDash([6, 4]);
2265
+ ctx.strokeStyle = level.color;
2266
+ ctx.moveTo(0, y);
2267
+ ctx.lineTo(chartWidth, y);
2268
+ ctx.stroke();
2269
+ ctx.setLineDash([]);
2270
+ ctx.fillStyle = level.color;
2271
+ ctx.fillText(level.label, chartWidth - 28, y - 6);
2272
+ }
2273
+ }
2274
+ };
2275
+
2276
+ // src/indicators/FibRetracementIndicator.ts
2277
+ var FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
2278
+ var FIB_LABELS = ["0%", "23.6%", "38.2%", "50%", "61.8%", "78.6%", "100%"];
2279
+ var FIB_COLORS = ["#90a4ae", "#4d96ff", "#6bcb77", "#ffd93d", "#ff9800", "#ef5350", "#90a4ae"];
2280
+ var FibRetracementIndicator = class {
2281
+ constructor(lookback = 50, color = "#e1bee7") {
2282
+ this.lookback = lookback;
2283
+ this.name = "Fib";
2284
+ this.color = color;
2285
+ }
2286
+ compute(data) {
2287
+ return data.map((_, i) => {
2288
+ const start = Math.max(0, i - this.lookback + 1);
2289
+ let high = -Infinity;
2290
+ let low = Infinity;
2291
+ for (let j = start; j <= i; j++) {
2292
+ if (data[j].high > high) high = data[j].high;
2293
+ if (data[j].low < low) low = data[j].low;
2294
+ }
2295
+ const range = high - low;
2296
+ return {
2297
+ index: i,
2298
+ value: high,
2299
+ // 100% (top of range)
2300
+ upper: low + 0.618 * range,
2301
+ // 61.8%
2302
+ lower: low + 0.382 * range,
2303
+ // 38.2%
2304
+ signal: low + 0.5 * range,
2305
+ // 50%
2306
+ histogram: low
2307
+ // 0% (bottom of range)
2308
+ };
2309
+ });
2310
+ }
2311
+ render(ctx, results, scale, viewport, chartHeight) {
2312
+ const canvas = ctx.canvas;
2313
+ const priceRange = canvas._priceRange;
2314
+ if (!priceRange) return;
2315
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
2316
+ let lastResult = null;
2317
+ for (let i = results.length - 1; i >= 0; i--) {
2318
+ if (results[i].value !== null) {
2319
+ lastResult = results[i];
2320
+ break;
2321
+ }
2322
+ }
2323
+ if (lastResult === null) return;
2324
+ const high = lastResult.value;
2325
+ const low = lastResult.histogram;
2326
+ if (high == null || low == null) return;
2327
+ const range = high - low;
2328
+ const fibValues = FIB_RATIOS.map((ratio) => low + ratio * range);
2329
+ ctx.lineWidth = 1;
2330
+ ctx.font = "10px sans-serif";
2331
+ ctx.textBaseline = "middle";
2332
+ for (let fi = 0; fi < FIB_RATIOS.length; fi++) {
2333
+ const price = fibValues[fi];
2334
+ const y = scale.yToPixel(price, priceRange, chartHeight);
2335
+ const color = FIB_COLORS[fi];
2336
+ ctx.beginPath();
2337
+ ctx.setLineDash([6, 4]);
2338
+ ctx.strokeStyle = color;
2339
+ ctx.moveTo(0, y);
2340
+ ctx.lineTo(chartWidth, y);
2341
+ ctx.stroke();
2342
+ ctx.setLineDash([]);
2343
+ ctx.fillStyle = color;
2344
+ ctx.fillText(FIB_LABELS[fi], chartWidth - 38, y - 6);
2345
+ }
2346
+ }
2347
+ };
2348
+
2349
+ // src/indicators/HeikinAshi.ts
2350
+ var HeikinAshi = class {
2351
+ constructor(color = "#90a4ae", upColor = "#26a69a", downColor = "#ef5350") {
2352
+ this.name = "HA";
2353
+ this.color = color;
2354
+ this.upColor = upColor;
2355
+ this.downColor = downColor;
2356
+ }
2357
+ compute(data) {
2358
+ const results = [];
2359
+ let prevHAOpen = 0;
2360
+ let prevHAClose = 0;
2361
+ for (let i = 0; i < data.length; i++) {
2362
+ const bar = data[i];
2363
+ const haClose = (bar.open + bar.high + bar.low + bar.close) / 4;
2364
+ let haOpen;
2365
+ if (i === 0) {
2366
+ haOpen = (bar.open + bar.close) / 2;
2367
+ } else {
2368
+ haOpen = (prevHAOpen + prevHAClose) / 2;
2369
+ }
2370
+ const haHigh = Math.max(bar.high, haOpen, haClose);
2371
+ const haLow = Math.min(bar.low, haOpen, haClose);
2372
+ prevHAOpen = haOpen;
2373
+ prevHAClose = haClose;
2374
+ results.push({
2375
+ index: i,
2376
+ value: haClose,
2377
+ // HA-Close
2378
+ upper: haHigh,
2379
+ // HA-High
2380
+ lower: haLow,
2381
+ // HA-Low
2382
+ signal: haOpen
2383
+ // HA-Open
2384
+ });
2385
+ }
2386
+ return results;
2387
+ }
2388
+ render(ctx, results, scale, viewport, chartHeight) {
2389
+ const canvas = ctx.canvas;
2390
+ const priceRange = canvas._priceRange;
2391
+ if (!priceRange) return;
2392
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
2393
+ const candleWidth = scale.candleBodyWidth(viewport, chartWidth);
2394
+ const halfBody = candleWidth / 2;
2395
+ for (const r of results) {
2396
+ if (r.value == null || r.upper == null || r.lower == null || r.signal == null) continue;
2397
+ const haClose = r.value;
2398
+ const haHigh = r.upper;
2399
+ const haLow = r.lower;
2400
+ const haOpen = r.signal;
2401
+ const isBullish = haClose >= haOpen;
2402
+ const color = isBullish ? this.upColor : this.downColor;
2403
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
2404
+ const yHigh = scale.yToPixel(haHigh, priceRange, chartHeight);
2405
+ const yLow = scale.yToPixel(haLow, priceRange, chartHeight);
2406
+ const yOpen = scale.yToPixel(haOpen, priceRange, chartHeight);
2407
+ const yClose = scale.yToPixel(haClose, priceRange, chartHeight);
2408
+ const bodyTop = Math.min(yOpen, yClose);
2409
+ const bodyBottom = Math.max(yOpen, yClose);
2410
+ const bodyHeight = Math.max(1, bodyBottom - bodyTop);
2411
+ ctx.beginPath();
2412
+ ctx.strokeStyle = color;
2413
+ ctx.lineWidth = 1;
2414
+ ctx.moveTo(x, yHigh);
2415
+ ctx.lineTo(x, yLow);
2416
+ ctx.stroke();
2417
+ ctx.fillStyle = color;
2418
+ ctx.strokeStyle = color;
2419
+ ctx.lineWidth = 1;
2420
+ ctx.fillRect(x - halfBody, bodyTop, candleWidth, bodyHeight);
2421
+ ctx.strokeRect(x - halfBody, bodyTop, candleWidth, bodyHeight);
2422
+ }
2423
+ }
2424
+ };
2425
+
2426
+ // src/indicators/VOL.ts
2427
+ var VOL = class {
2428
+ constructor(upColor = "#26a69a", downColor = "#ef5350") {
2429
+ this.name = "VOL";
2430
+ this.pane = "sub";
2431
+ this.paneHeight = 80;
2432
+ this.upColor = upColor;
2433
+ this.downColor = downColor;
2434
+ this.color = upColor;
2435
+ }
2436
+ compute(data) {
2437
+ return data.map((bar, i) => ({
2438
+ index: i,
2439
+ value: bar.volume ?? 0,
2440
+ // encode direction in signal field for render phase
2441
+ signal: bar.close >= bar.open ? 1 : 0
2442
+ }));
2443
+ }
2444
+ render(ctx, results, scale, viewport, paneHeight) {
2445
+ const chartWidth = ctx.canvas._chartWidth ?? ctx.canvas.width;
2446
+ let maxVol = 0;
2447
+ for (const r of results) {
2448
+ if (r.value != null && r.value > maxVol) maxVol = r.value;
2449
+ }
2450
+ if (maxVol === 0) return;
2451
+ const range = { min: 0, max: maxVol * 1.1 };
2452
+ const baseY = scale.yToPixel(0, range, paneHeight);
2453
+ const bodyW = Math.max(scale.candleBodyWidth(viewport, chartWidth), 1);
2454
+ for (const r of results) {
2455
+ if (r.value == null || r.value === 0) continue;
2456
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
2457
+ const topY = scale.yToPixel(r.value, range, paneHeight);
2458
+ const h = Math.abs(baseY - topY);
2459
+ ctx.fillStyle = r.signal === 1 ? `${this.upColor}99` : `${this.downColor}99`;
2460
+ ctx.fillRect(x - bodyW / 2, topY, bodyW, h);
2461
+ }
2462
+ ctx.fillStyle = this.upColor;
2463
+ ctx.font = "10px 'Inter', system-ui, sans-serif";
2464
+ ctx.fillText(this.name, 4, 11);
2465
+ }
2466
+ };
2467
+
2468
+ // src/indicators/LinearRegression.ts
2469
+ var LinearRegression = class {
2470
+ constructor(period = 14, color = "#ff9800") {
2471
+ this.period = period;
2472
+ this.name = "LinReg";
2473
+ this.color = color;
2474
+ }
2475
+ compute(data) {
2476
+ const n = this.period;
2477
+ const results = [];
2478
+ const sumX = n * (n - 1) / 2;
2479
+ const sumX2 = n * (n - 1) * (2 * n - 1) / 6;
2480
+ const denom = n * sumX2 - sumX * sumX;
2481
+ for (let i = 0; i < data.length; i++) {
2482
+ if (i < n - 1) {
2483
+ results.push({ index: i, value: null });
2484
+ continue;
2485
+ }
2486
+ let sumY = 0;
2487
+ let sumXY = 0;
2488
+ for (let j = 0; j < n; j++) {
2489
+ const close = data[i - (n - 1 - j)].close;
2490
+ sumY += close;
2491
+ sumXY += j * close;
2492
+ }
2493
+ const slope = (n * sumXY - sumX * sumY) / denom;
2494
+ const intercept = (sumY - slope * sumX) / n;
2495
+ const value = intercept + slope * (n - 1);
2496
+ results.push({ index: i, value });
2497
+ }
2498
+ return results;
2499
+ }
2500
+ render(ctx, results, scale, viewport, chartHeight) {
2501
+ const canvas = ctx.canvas;
2502
+ const priceRange = canvas._priceRange;
2503
+ if (!priceRange) return;
2504
+ const chartWidth = canvas._chartWidth ?? ctx.canvas.width;
2505
+ ctx.beginPath();
2506
+ ctx.strokeStyle = this.color;
2507
+ ctx.lineWidth = 1.5;
2508
+ ctx.lineJoin = "round";
2509
+ let started = false;
2510
+ for (const r of results) {
2511
+ if (r.value === null) {
2512
+ started = false;
2513
+ continue;
2514
+ }
2515
+ const x = scale.xToPixel(r.index, viewport, chartWidth);
2516
+ const y = scale.yToPixel(r.value, priceRange, chartHeight);
2517
+ started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
2518
+ }
2519
+ ctx.stroke();
2520
+ }
2521
+ };
2522
+
2523
+ exports.ADX = ADX;
2524
+ exports.ATR = ATR;
2525
+ exports.AccDistribution = AccDistribution;
2526
+ exports.AwesomeOscillator = AwesomeOscillator;
2527
+ exports.BollingerBands = BollingerBands;
2528
+ exports.CCI = CCI;
2529
+ exports.CMF = CMF;
2530
+ exports.DonchianChannels = DonchianChannels;
2531
+ exports.EMA = EMA;
2532
+ exports.FibRetracementIndicator = FibRetracementIndicator;
2533
+ exports.HMA = HMA;
2534
+ exports.HeikinAshi = HeikinAshi;
2535
+ exports.IchimokuCloud = IchimokuCloud;
2536
+ exports.KeltnerChannels = KeltnerChannels;
2537
+ exports.LinearRegression = LinearRegression;
2538
+ exports.MACD = MACD;
2539
+ exports.MARibbon = MARibbon;
2540
+ exports.MFI = MFI;
2541
+ exports.OBV = OBV;
2542
+ exports.ParabolicSAR = ParabolicSAR;
2543
+ exports.PivotPoints = PivotPoints;
2544
+ exports.ROC = ROC;
2545
+ exports.RSI = RSI;
2546
+ exports.SMA = SMA;
2547
+ exports.StochRSI = StochRSI;
2548
+ exports.Stochastic = Stochastic;
2549
+ exports.Supertrend = Supertrend;
2550
+ exports.VOL = VOL;
2551
+ exports.VWAP = VWAP;
2552
+ exports.WMA = WMA;
2553
+ exports.WilliamsR = WilliamsR;
2554
+ //# sourceMappingURL=index.cjs.map
2555
+ //# sourceMappingURL=index.cjs.map