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