@adrienhobbs/candlekit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3903 @@
1
+ import { useRef, useState, useEffect, useMemo, useCallback } from 'react';
2
+ import { createChart, ColorType, CandlestickSeries, HistogramSeries, createSeriesMarkers, LineSeries, AreaSeries, LineStyle } from 'lightweight-charts';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+ import { X, Search, Plus } from 'lucide-react';
5
+ import { z } from 'zod';
6
+ import { stochastic, obv, mfi, forceindex, atr, adx, macd, psar, cci, williamsr, keltnerchannel, supertrend, ichimokucloud, donchianchannels, roc, stochasticrsi, wma } from 'fast-technical-indicators';
7
+
8
+ var __defProp = Object.defineProperty;
9
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
10
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
11
+
12
+ // src/indicators/core/registry.ts
13
+ var IndicatorRegistry = class {
14
+ constructor() {
15
+ __publicField(this, "indicators", /* @__PURE__ */ new Map());
16
+ __publicField(this, "instanceCounters", /* @__PURE__ */ new Map());
17
+ }
18
+ register(definition) {
19
+ if (this.indicators.has(definition.metadata.id)) {
20
+ console.warn(
21
+ `Indicator with id "${definition.metadata.id}" is already registered. Overwriting.`
22
+ );
23
+ }
24
+ this.indicators.set(definition.metadata.id, definition);
25
+ }
26
+ unregister(id) {
27
+ return this.indicators.delete(id);
28
+ }
29
+ get(id) {
30
+ return this.indicators.get(id);
31
+ }
32
+ getAll() {
33
+ return Array.from(this.indicators.values());
34
+ }
35
+ getByCategory(category) {
36
+ return this.getAll().filter(
37
+ (indicator) => indicator.metadata.category === category
38
+ );
39
+ }
40
+ search(query) {
41
+ const lowerQuery = query.toLowerCase();
42
+ return this.getAll().filter(
43
+ (indicator) => indicator.metadata.name.toLowerCase().includes(lowerQuery) || indicator.metadata.description.toLowerCase().includes(lowerQuery)
44
+ );
45
+ }
46
+ createInstance(definitionId, customSettings) {
47
+ const definition = this.get(definitionId);
48
+ if (!definition) {
49
+ throw new Error(`Indicator definition "${definitionId}" not found`);
50
+ }
51
+ const currentCount = this.instanceCounters.get(definitionId) || 0;
52
+ const newCount = currentCount + 1;
53
+ this.instanceCounters.set(definitionId, newCount);
54
+ const instanceId = `${definitionId}-${newCount}`;
55
+ const defaultSettings = {};
56
+ Object.entries(definition.settings).forEach(([key, field]) => {
57
+ defaultSettings[key] = field.defaultValue;
58
+ });
59
+ const settings = { ...defaultSettings, ...customSettings };
60
+ return {
61
+ id: instanceId,
62
+ definitionId,
63
+ name: `${definition.metadata.name}(${newCount})`,
64
+ settings
65
+ };
66
+ }
67
+ validateSettings(definitionId, settings) {
68
+ const definition = this.get(definitionId);
69
+ if (!definition) {
70
+ return false;
71
+ }
72
+ for (const [key, field] of Object.entries(definition.settings)) {
73
+ const value = settings[key];
74
+ if (value === void 0) {
75
+ return false;
76
+ }
77
+ if (field.type === "number") {
78
+ if (typeof value !== "number") return false;
79
+ if (field.min !== void 0 && value < field.min) return false;
80
+ if (field.max !== void 0 && value > field.max) return false;
81
+ } else if (field.type === "boolean") {
82
+ if (typeof value !== "boolean") return false;
83
+ } else if (field.type === "color" || field.type === "lineStyle") {
84
+ if (typeof value !== "string") return false;
85
+ } else if (field.type === "select") {
86
+ if (typeof value !== "string") return false;
87
+ if (field.options && !field.options.some((opt) => opt.value === value)) {
88
+ return false;
89
+ }
90
+ }
91
+ }
92
+ return true;
93
+ }
94
+ getMetadataList() {
95
+ return this.getAll().map((indicator) => indicator.metadata);
96
+ }
97
+ clear() {
98
+ this.indicators.clear();
99
+ this.instanceCounters.clear();
100
+ }
101
+ };
102
+ var indicatorRegistry = new IndicatorRegistry();
103
+
104
+ // src/indicators/core/calculator.ts
105
+ var IndicatorCalculator = class {
106
+ constructor() {
107
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
108
+ }
109
+ hashSettings(settings) {
110
+ return JSON.stringify(settings);
111
+ }
112
+ calculate(instance, bars) {
113
+ const definition = indicatorRegistry.get(instance.definitionId);
114
+ if (!definition) {
115
+ console.error(
116
+ `Indicator definition "${instance.definitionId}" not found`
117
+ );
118
+ return [];
119
+ }
120
+ const settingsHash = this.hashSettings(instance.settings);
121
+ const cacheKey = instance.id;
122
+ const cached = this.cache.get(cacheKey);
123
+ if (cached && cached.settingsHash === settingsHash && cached.dataLength === bars.length) {
124
+ return cached.data;
125
+ }
126
+ try {
127
+ const data = definition.calculate(bars, instance.settings);
128
+ this.cache.set(cacheKey, {
129
+ data,
130
+ settingsHash,
131
+ dataLength: bars.length
132
+ });
133
+ return data;
134
+ } catch (error) {
135
+ console.error(
136
+ `Error calculating indicator "${instance.definitionId}":`,
137
+ error
138
+ );
139
+ return [];
140
+ }
141
+ }
142
+ invalidateCache(instanceId) {
143
+ if (instanceId) {
144
+ this.cache.delete(instanceId);
145
+ } else {
146
+ this.cache.clear();
147
+ }
148
+ }
149
+ getCacheSize() {
150
+ return this.cache.size;
151
+ }
152
+ };
153
+ var indicatorCalculator = new IndicatorCalculator();
154
+
155
+ // src/indicators/primitives/BandsPrimitive.ts
156
+ var AreaBetweenLinesPaneView = class {
157
+ constructor(source) {
158
+ __publicField(this, "_source");
159
+ __publicField(this, "_data", null);
160
+ this._source = source;
161
+ }
162
+ update() {
163
+ const series1 = this._source._upperSeries;
164
+ const series2 = this._source._lowerSeries;
165
+ const chart = this._source._chart;
166
+ if (!series1 || !series2 || !chart) return;
167
+ const timeScale = chart.timeScale();
168
+ const data1 = series1.data();
169
+ const data2 = series2.data();
170
+ const points = [];
171
+ let i = 0;
172
+ let j = 0;
173
+ while (i < data1.length && j < data2.length) {
174
+ const d1 = data1[i];
175
+ const d2 = data2[j];
176
+ if (d1.time < d2.time) {
177
+ i++;
178
+ } else if (d1.time > d2.time) {
179
+ j++;
180
+ } else {
181
+ const x = timeScale.timeToCoordinate(d1.time);
182
+ const y1 = series1.priceToCoordinate(d1.value);
183
+ const y2 = series2.priceToCoordinate(d2.value);
184
+ if (x !== null && y1 !== null && y2 !== null) {
185
+ points.push({ x, y1, y2 });
186
+ }
187
+ i++;
188
+ j++;
189
+ }
190
+ }
191
+ this._data = {
192
+ points,
193
+ color: this._source._options.fillColor || "rgba(59, 130, 246, 0.1)"
194
+ };
195
+ }
196
+ renderer() {
197
+ return new AreaBetweenLinesRenderer(this._data);
198
+ }
199
+ };
200
+ var AreaBetweenLinesRenderer = class {
201
+ constructor(data) {
202
+ __publicField(this, "_data");
203
+ this._data = data;
204
+ }
205
+ draw(target) {
206
+ target.useBitmapCoordinateSpace((scope) => {
207
+ if (!this._data || this._data.points.length === 0) return;
208
+ const ctx = scope.context;
209
+ ctx.save();
210
+ ctx.scale(scope.horizontalPixelRatio, scope.verticalPixelRatio);
211
+ const points = this._data.points;
212
+ ctx.beginPath();
213
+ ctx.moveTo(points[0].x, points[0].y1);
214
+ for (let i = 1; i < points.length; i++) {
215
+ ctx.lineTo(points[i].x, points[i].y1);
216
+ }
217
+ for (let i = points.length - 1; i >= 0; i--) {
218
+ ctx.lineTo(points[i].x, points[i].y2);
219
+ }
220
+ ctx.closePath();
221
+ ctx.fillStyle = this._data.color;
222
+ ctx.fill();
223
+ ctx.restore();
224
+ });
225
+ }
226
+ drawBackground(target) {
227
+ this.draw(target);
228
+ }
229
+ };
230
+ var BandsPrimitive = class {
231
+ constructor(options) {
232
+ __publicField(this, "_options");
233
+ __publicField(this, "_upperSeries");
234
+ __publicField(this, "_lowerSeries");
235
+ __publicField(this, "_chart", null);
236
+ __publicField(this, "_series", null);
237
+ __publicField(this, "_paneViews");
238
+ this._options = options;
239
+ this._upperSeries = options.upperSeries;
240
+ this._lowerSeries = options.lowerSeries;
241
+ this._paneViews = [new AreaBetweenLinesPaneView(this)];
242
+ }
243
+ attached(param) {
244
+ this._chart = param.chart;
245
+ this._series = param.series;
246
+ }
247
+ detached() {
248
+ this._chart = null;
249
+ this._series = null;
250
+ }
251
+ paneViews() {
252
+ return this._paneViews;
253
+ }
254
+ updateAllViews() {
255
+ this._paneViews.forEach((pw) => pw.update());
256
+ }
257
+ };
258
+ function ChartComponent({
259
+ bars,
260
+ onLoadMoreData,
261
+ indicators = [],
262
+ lines = [],
263
+ onBarUpdate,
264
+ onNewBar,
265
+ onDeleteLine,
266
+ onAddLine,
267
+ onClearAllLines,
268
+ enableBarSelection = true,
269
+ onBarClick
270
+ }) {
271
+ const chartContainerRef = useRef(null);
272
+ const chartRef = useRef(null);
273
+ const candlestickSeriesRef = useRef(null);
274
+ const volumeSeriesRef = useRef(null);
275
+ const indicatorSeriesRef = useRef(/* @__PURE__ */ new Map());
276
+ const indicatorPaneIndexRef = useRef(/* @__PURE__ */ new Map());
277
+ const nextPaneIndexRef = useRef(2);
278
+ const priceLineRefs = useRef(/* @__PURE__ */ new Map());
279
+ const seriesMarkersRef = useRef(null);
280
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
281
+ const [contextMenu, setContextMenu] = useState(null);
282
+ const [linePositions, setLinePositions] = useState(/* @__PURE__ */ new Map());
283
+ const [selectedBar, setSelectedBar] = useState(null);
284
+ const selectedBarRef = useRef(null);
285
+ const [spotlightPosition, setSpotlightPosition] = useState(null);
286
+ const previousBarsRef = useRef([]);
287
+ const previousBarsLengthRef = useRef(0);
288
+ const isLoadingRef = useRef(false);
289
+ const barsRef = useRef(bars);
290
+ const isDraggingRef = useRef(false);
291
+ const mouseDownPosRef = useRef(null);
292
+ useEffect(() => {
293
+ if (!chartContainerRef.current) return;
294
+ const chart = createChart(chartContainerRef.current, {
295
+ layout: {
296
+ background: { type: ColorType.Solid, color: "#0f172a" },
297
+ textColor: "#94a3b8",
298
+ panes: {
299
+ enableResize: true,
300
+ separatorColor: "#1e293b",
301
+ separatorHoverColor: "rgba(148,163,184,0.5)"
302
+ }
303
+ },
304
+ grid: {
305
+ vertLines: { color: "#1e293b" },
306
+ horzLines: { color: "#1e293b" }
307
+ },
308
+ width: chartContainerRef.current.clientWidth,
309
+ height: 600,
310
+ timeScale: {
311
+ timeVisible: true,
312
+ secondsVisible: true,
313
+ borderColor: "#334155"
314
+ },
315
+ rightPriceScale: {
316
+ borderColor: "#334155"
317
+ },
318
+ crosshair: {
319
+ mode: 0,
320
+ vertLine: {
321
+ width: 1,
322
+ color: "#475569",
323
+ style: 3
324
+ },
325
+ horzLine: {
326
+ width: 1,
327
+ color: "#475569",
328
+ style: 3
329
+ }
330
+ }
331
+ });
332
+ const candlestickSeries = chart.addSeries(CandlestickSeries, {
333
+ upColor: "#10b981",
334
+ downColor: "#ef4444",
335
+ borderVisible: false,
336
+ wickUpColor: "#10b981",
337
+ wickDownColor: "#ef4444"
338
+ }, 0);
339
+ const volumeSeries = chart.addSeries(HistogramSeries, {
340
+ color: "#64748b",
341
+ priceFormat: {
342
+ type: "volume"
343
+ },
344
+ priceScaleId: ""
345
+ }, 1);
346
+ chartRef.current = chart;
347
+ candlestickSeriesRef.current = candlestickSeries;
348
+ volumeSeriesRef.current = volumeSeries;
349
+ seriesMarkersRef.current = createSeriesMarkers(candlestickSeries, []);
350
+ chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
351
+ if (logicalRange && logicalRange.from < 5 && !isLoadingRef.current && onLoadMoreData) {
352
+ console.log("Triggering load more, logicalRange.from:", logicalRange.from);
353
+ isLoadingRef.current = true;
354
+ setIsLoadingMore(true);
355
+ previousBarsLengthRef.current = barsRef.current.length;
356
+ console.log("Set previousBarsLengthRef to:", previousBarsLengthRef.current);
357
+ const sortedBars = [...barsRef.current].sort((a, b) => a.timestamp - b.timestamp);
358
+ const oldestTimestamp = sortedBars[0]?.timestamp;
359
+ if (oldestTimestamp) {
360
+ onLoadMoreData(oldestTimestamp);
361
+ }
362
+ }
363
+ });
364
+ const handleResize = () => {
365
+ if (chartContainerRef.current && chart) {
366
+ chart.applyOptions({ width: chartContainerRef.current.clientWidth });
367
+ }
368
+ };
369
+ const handleContextMenu = (e) => {
370
+ console.log("Context menu event triggered!", e);
371
+ e.preventDefault();
372
+ e.stopPropagation();
373
+ if (!candlestickSeriesRef.current) {
374
+ console.log("No candlestick series yet");
375
+ return;
376
+ }
377
+ const rect = chartContainerRef.current.getBoundingClientRect();
378
+ const y = e.clientY - rect.top;
379
+ console.log("Y position:", y, "Rect:", rect);
380
+ const price = candlestickSeriesRef.current.coordinateToPrice(y);
381
+ console.log("Calculated price:", price);
382
+ if (price !== null) {
383
+ setContextMenu({
384
+ x: e.clientX - rect.left,
385
+ y,
386
+ price
387
+ });
388
+ console.log("Context menu set:", { x: e.clientX - rect.left, y, price });
389
+ }
390
+ };
391
+ const handleMouseDown = (e) => {
392
+ mouseDownPosRef.current = { x: e.clientX, y: e.clientY };
393
+ isDraggingRef.current = false;
394
+ };
395
+ const handleMouseMove = (e) => {
396
+ if (mouseDownPosRef.current) {
397
+ const dx = Math.abs(e.clientX - mouseDownPosRef.current.x);
398
+ const dy = Math.abs(e.clientY - mouseDownPosRef.current.y);
399
+ if (dx > 5 || dy > 5) {
400
+ isDraggingRef.current = true;
401
+ }
402
+ }
403
+ };
404
+ const handleMouseUp = () => {
405
+ mouseDownPosRef.current = null;
406
+ };
407
+ const handleClick = (e) => {
408
+ console.log("=== CLICK DEBUG START ===");
409
+ console.log("Click event - closing context menu");
410
+ setContextMenu(null);
411
+ if (isDraggingRef.current) {
412
+ console.log("Ignoring click - user was dragging");
413
+ isDraggingRef.current = false;
414
+ return;
415
+ }
416
+ console.log("enableBarSelection prop:", enableBarSelection);
417
+ console.log("chartRef.current:", !!chartRef.current);
418
+ console.log("candlestickSeriesRef.current:", !!candlestickSeriesRef.current);
419
+ console.log("bars.length:", bars.length);
420
+ if (!enableBarSelection) {
421
+ console.log("Bar selection is DISABLED - exiting");
422
+ return;
423
+ }
424
+ if (!chartRef.current) {
425
+ console.log("Chart ref is NULL - exiting");
426
+ return;
427
+ }
428
+ if (!candlestickSeriesRef.current) {
429
+ console.log("Candlestick series ref is NULL - exiting");
430
+ return;
431
+ }
432
+ console.log("All checks passed - proceeding with bar selection");
433
+ const rect = chartContainerRef.current.getBoundingClientRect();
434
+ const x = e.clientX - rect.left;
435
+ console.log("Click X coordinate:", x);
436
+ const timeScale = chartRef.current.timeScale();
437
+ const coordinate = timeScale.coordinateToTime(x);
438
+ console.log("Coordinate from timeScale:", coordinate);
439
+ if (!coordinate) {
440
+ console.log("No coordinate from timeScale - exiting");
441
+ return;
442
+ }
443
+ const clickedTime = coordinate;
444
+ console.log("Clicked time:", clickedTime);
445
+ console.log("Sample bar timestamps (first 3):", barsRef.current.slice(0, 3).map((b) => b.timestamp / 1e3));
446
+ const clickedBar = barsRef.current.find((bar) => {
447
+ const diff = Math.abs(bar.timestamp / 1e3 - clickedTime);
448
+ return diff < 300;
449
+ });
450
+ console.log("Clicked bar found:", !!clickedBar);
451
+ if (clickedBar) {
452
+ console.log("Clicked bar details:", clickedBar);
453
+ console.log("Current selectedBarRef.current:", selectedBarRef.current);
454
+ if (selectedBarRef.current && selectedBarRef.current.timestamp === clickedBar.timestamp) {
455
+ console.log("Deselecting bar (same bar clicked)");
456
+ selectedBarRef.current = null;
457
+ setSelectedBar(null);
458
+ setSpotlightPosition(null);
459
+ if (onBarClick) {
460
+ onBarClick(null);
461
+ }
462
+ } else {
463
+ console.log("Selecting new bar");
464
+ selectedBarRef.current = clickedBar;
465
+ setSelectedBar(clickedBar);
466
+ const barX = timeScale.timeToCoordinate(clickedBar.timestamp / 1e3);
467
+ console.log("Bar X position:", barX);
468
+ if (barX !== null) {
469
+ const barWidth = Math.max(8, rect.width / barsRef.current.length);
470
+ console.log("Bar width:", barWidth);
471
+ setSpotlightPosition({ x: barX - barWidth / 2, width: barWidth });
472
+ }
473
+ if (onBarClick) {
474
+ onBarClick(clickedBar);
475
+ }
476
+ }
477
+ } else {
478
+ console.log("No bar found at clicked position");
479
+ }
480
+ console.log("=== CLICK DEBUG END ===");
481
+ };
482
+ window.addEventListener("resize", handleResize);
483
+ chartContainerRef.current.addEventListener("contextmenu", handleContextMenu, true);
484
+ chartContainerRef.current.addEventListener("mousedown", handleMouseDown, true);
485
+ chartContainerRef.current.addEventListener("mousemove", handleMouseMove, true);
486
+ chartContainerRef.current.addEventListener("mouseup", handleMouseUp, true);
487
+ chartContainerRef.current.addEventListener("click", handleClick, true);
488
+ const container = chartContainerRef.current;
489
+ return () => {
490
+ window.removeEventListener("resize", handleResize);
491
+ container.removeEventListener("contextmenu", handleContextMenu, true);
492
+ container.removeEventListener("mousedown", handleMouseDown, true);
493
+ container.removeEventListener("mousemove", handleMouseMove, true);
494
+ container.removeEventListener("mouseup", handleMouseUp, true);
495
+ container.removeEventListener("click", handleClick, true);
496
+ chart.remove();
497
+ };
498
+ }, []);
499
+ useEffect(() => {
500
+ barsRef.current = bars;
501
+ }, [bars]);
502
+ useEffect(() => {
503
+ if (!candlestickSeriesRef.current || !volumeSeriesRef.current) return;
504
+ const sortedBars = [...bars].sort((a, b) => a.timestamp - b.timestamp);
505
+ const uniqueBars = sortedBars.reduce((acc, bar) => {
506
+ const exists = acc.find((b) => b.timestamp === bar.timestamp);
507
+ if (!exists) {
508
+ acc.push(bar);
509
+ }
510
+ return acc;
511
+ }, []);
512
+ console.log("Debug - bars.length:", bars.length, "uniqueBars.length:", uniqueBars.length, "isLoadingRef:", isLoadingRef.current, "previousBarsLength:", previousBarsLengthRef.current);
513
+ const previousBars = previousBarsRef.current;
514
+ const isInitialLoad = previousBars.length === 0;
515
+ const hasNewBars = uniqueBars.length > previousBars.length;
516
+ const lastBarChanged = uniqueBars.length > 0 && previousBars.length > 0 && uniqueBars[uniqueBars.length - 1].timestamp === previousBars[previousBars.length - 1].timestamp;
517
+ console.log("isInitialLoad:", isInitialLoad, "hasNewBars:", hasNewBars, "lastBarChanged:", lastBarChanged);
518
+ console.log("previousBars.length:", previousBars.length, "uniqueBars.length:", uniqueBars.length);
519
+ if (isLoadingRef.current && uniqueBars.length > previousBarsLengthRef.current) {
520
+ console.log("Hiding loading indicator");
521
+ isLoadingRef.current = false;
522
+ setIsLoadingMore(false);
523
+ }
524
+ const firstBarChanged = uniqueBars.length > 0 && previousBars.length > 0 && uniqueBars[0].timestamp !== previousBars[0].timestamp;
525
+ if (isInitialLoad || hasNewBars) {
526
+ console.log("Setting data with", uniqueBars.length, "bars", "firstBarChanged:", firstBarChanged);
527
+ const candleData = uniqueBars.map((bar) => ({
528
+ time: bar.timestamp / 1e3,
529
+ open: bar.open,
530
+ high: bar.high,
531
+ low: bar.low,
532
+ close: bar.close
533
+ }));
534
+ const volumeData = uniqueBars.map((bar) => ({
535
+ time: bar.timestamp / 1e3,
536
+ value: bar.volume,
537
+ color: bar.close >= bar.open ? "#10b98180" : "#ef444480"
538
+ }));
539
+ console.log("candleData sample (first 3):", candleData.slice(0, 3));
540
+ console.log("candleData sample (last 3):", candleData.slice(-3));
541
+ candlestickSeriesRef.current.setData(candleData);
542
+ volumeSeriesRef.current.setData(volumeData);
543
+ } else if (lastBarChanged && uniqueBars.length > 0 && !hasNewBars) {
544
+ const lastBar = uniqueBars[uniqueBars.length - 1];
545
+ const candleUpdate = {
546
+ time: lastBar.timestamp / 1e3,
547
+ open: lastBar.open,
548
+ high: lastBar.high,
549
+ low: lastBar.low,
550
+ close: lastBar.close
551
+ };
552
+ const volumeUpdate = {
553
+ time: lastBar.timestamp / 1e3,
554
+ value: lastBar.volume,
555
+ color: lastBar.close >= lastBar.open ? "#10b98180" : "#ef444480"
556
+ };
557
+ candlestickSeriesRef.current.update(candleUpdate);
558
+ volumeSeriesRef.current.update(volumeUpdate);
559
+ }
560
+ previousBarsRef.current = uniqueBars;
561
+ }, [bars]);
562
+ useEffect(() => {
563
+ if (!chartRef.current || !candlestickSeriesRef.current) return;
564
+ priceLineRefs.current.forEach((priceLine) => {
565
+ candlestickSeriesRef.current?.removePriceLine(priceLine);
566
+ });
567
+ priceLineRefs.current.clear();
568
+ const newPositions = /* @__PURE__ */ new Map();
569
+ lines.forEach((line) => {
570
+ const priceLine = candlestickSeriesRef.current?.createPriceLine({
571
+ price: line.price,
572
+ color: line.color,
573
+ lineWidth: line.lineWidth || 2,
574
+ lineStyle: getLineStyle(line.lineStyle),
575
+ axisLabelVisible: true,
576
+ title: line.title || ""
577
+ });
578
+ if (priceLine) {
579
+ priceLineRefs.current.set(line.id, priceLine);
580
+ }
581
+ const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
582
+ if (y !== null && y !== void 0) {
583
+ newPositions.set(line.id, y);
584
+ }
585
+ });
586
+ setLinePositions(newPositions);
587
+ }, [lines]);
588
+ useEffect(() => {
589
+ if (!chartRef.current || !candlestickSeriesRef.current) return;
590
+ const updatePositions = () => {
591
+ const newPositions = /* @__PURE__ */ new Map();
592
+ lines.forEach((line) => {
593
+ const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
594
+ if (y !== null && y !== void 0) {
595
+ newPositions.set(line.id, y);
596
+ }
597
+ });
598
+ setLinePositions(newPositions);
599
+ };
600
+ chartRef.current.timeScale().subscribeVisibleLogicalRangeChange(updatePositions);
601
+ return () => {
602
+ chartRef.current?.timeScale().unsubscribeVisibleLogicalRangeChange(updatePositions);
603
+ };
604
+ }, [lines]);
605
+ useEffect(() => {
606
+ if (!chartRef.current) return;
607
+ indicatorSeriesRef.current.forEach((series) => {
608
+ chartRef.current?.removeSeries(series);
609
+ });
610
+ indicatorSeriesRef.current.clear();
611
+ indicatorPaneIndexRef.current.clear();
612
+ nextPaneIndexRef.current = 2;
613
+ indicators.forEach((indicator) => {
614
+ const definition = indicatorRegistry.get(indicator.definitionId);
615
+ if (!definition) return;
616
+ const data = indicatorCalculator.calculate(indicator, barsRef.current);
617
+ if (!data || data.length === 0) return;
618
+ const seriesType = definition.renderConfig.seriesType;
619
+ const isOverlay = definition.renderConfig.overlay !== false;
620
+ const paneIndex = isOverlay ? 0 : nextPaneIndexRef.current;
621
+ if (!isOverlay) {
622
+ indicatorPaneIndexRef.current.set(indicator.id, paneIndex);
623
+ nextPaneIndexRef.current++;
624
+ }
625
+ if (seriesType === "line") {
626
+ if (definition.renderConfig.outputCount === 1) {
627
+ const series = chartRef.current.addSeries(LineSeries, {
628
+ color: indicator.settings.color || "#3b82f6",
629
+ lineWidth: indicator.settings.lineWidth || 2,
630
+ title: indicator.name
631
+ }, paneIndex);
632
+ const lineData = data.map((d) => ({ time: d.time, value: d.value }));
633
+ series.setData(lineData);
634
+ indicatorSeriesRef.current.set(indicator.id, series);
635
+ } else if (definition.renderConfig.hasBandFill && definition.renderConfig.fillBands) {
636
+ const { upper: upperField, lower: lowerField } = definition.renderConfig.fillBands;
637
+ const upperSeries = chartRef.current.addSeries(LineSeries, {
638
+ color: indicator.settings.upperColor || "#ef4444",
639
+ lineWidth: indicator.settings.lineWidth || 2,
640
+ title: `${indicator.name} Upper`
641
+ }, paneIndex);
642
+ const middleSeries = chartRef.current.addSeries(LineSeries, {
643
+ color: indicator.settings.middleColor || indicator.settings.vwapColor || "#3b82f6",
644
+ lineWidth: indicator.settings.lineWidth || 2,
645
+ title: `${indicator.name} Middle`
646
+ }, paneIndex);
647
+ const lowerSeries = chartRef.current.addSeries(LineSeries, {
648
+ color: indicator.settings.lowerColor || "#10b981",
649
+ lineWidth: indicator.settings.lineWidth || 2,
650
+ title: `${indicator.name} Lower`
651
+ }, paneIndex);
652
+ const upperData = data.map((d) => ({ time: d.time, value: d[upperField] })).filter((d) => !isNaN(d.value));
653
+ const middleData = data.map((d) => ({ time: d.time, value: d.value })).filter((d) => !isNaN(d.value));
654
+ const lowerData = data.map((d) => ({ time: d.time, value: d[lowerField] })).filter((d) => !isNaN(d.value));
655
+ upperSeries.setData(upperData);
656
+ middleSeries.setData(middleData);
657
+ lowerSeries.setData(lowerData);
658
+ if (indicator.settings.showFill !== false) {
659
+ const bandsPrimitive = new BandsPrimitive({
660
+ upperSeries,
661
+ lowerSeries,
662
+ fillColor: indicator.settings.fillColor || "rgba(59, 130, 246, 0.1)"
663
+ });
664
+ upperSeries.attachPrimitive(bandsPrimitive);
665
+ }
666
+ indicatorSeriesRef.current.set(`${indicator.id}-upper`, upperSeries);
667
+ indicatorSeriesRef.current.set(`${indicator.id}-middle`, middleSeries);
668
+ indicatorSeriesRef.current.set(`${indicator.id}-lower`, lowerSeries);
669
+ }
670
+ } else if (seriesType === "histogram") {
671
+ const series = chartRef.current.addSeries(HistogramSeries, {
672
+ color: indicator.settings.color || "#8b5cf6",
673
+ title: indicator.name
674
+ }, paneIndex);
675
+ const histData = data.map((d) => ({ time: d.time, value: d.value }));
676
+ series.setData(histData);
677
+ indicatorSeriesRef.current.set(indicator.id, series);
678
+ } else if (seriesType === "area") {
679
+ const series = chartRef.current.addSeries(AreaSeries, {
680
+ topColor: indicator.settings.topColor || "#3b82f680",
681
+ bottomColor: indicator.settings.bottomColor || "#3b82f600",
682
+ lineColor: indicator.settings.lineColor || "#3b82f6",
683
+ lineWidth: indicator.settings.lineWidth || 2,
684
+ title: indicator.name
685
+ }, paneIndex);
686
+ const areaData = data.map((d) => ({ time: d.time, value: d.value }));
687
+ series.setData(areaData);
688
+ indicatorSeriesRef.current.set(indicator.id, series);
689
+ }
690
+ });
691
+ }, [indicators]);
692
+ useEffect(() => {
693
+ if (!chartRef.current || indicators.length === 0) return;
694
+ indicators.forEach((indicator) => {
695
+ const definition = indicatorRegistry.get(indicator.definitionId);
696
+ if (!definition) return;
697
+ const data = indicatorCalculator.calculate(indicator, barsRef.current);
698
+ if (!data || data.length === 0) return;
699
+ if (definition.renderConfig.outputCount === 1) {
700
+ const series = indicatorSeriesRef.current.get(indicator.id);
701
+ if (series) {
702
+ const lineData = data.map((d) => ({ time: d.time, value: d.value }));
703
+ series.setData(lineData);
704
+ }
705
+ } else if (definition.renderConfig.hasBandFill && definition.renderConfig.fillBands) {
706
+ const { upper: upperField, lower: lowerField } = definition.renderConfig.fillBands;
707
+ const upperSeries = indicatorSeriesRef.current.get(`${indicator.id}-upper`);
708
+ const middleSeries = indicatorSeriesRef.current.get(`${indicator.id}-middle`);
709
+ const lowerSeries = indicatorSeriesRef.current.get(`${indicator.id}-lower`);
710
+ if (upperSeries && middleSeries && lowerSeries) {
711
+ const upperData = data.map((d) => ({ time: d.time, value: d[upperField] })).filter((d) => !isNaN(d.value));
712
+ const middleData = data.map((d) => ({ time: d.time, value: d.value })).filter((d) => !isNaN(d.value));
713
+ const lowerData = data.map((d) => ({ time: d.time, value: d[lowerField] })).filter((d) => !isNaN(d.value));
714
+ upperSeries.setData(upperData);
715
+ middleSeries.setData(middleData);
716
+ lowerSeries.setData(lowerData);
717
+ }
718
+ }
719
+ });
720
+ }, [bars, indicators]);
721
+ useEffect(() => {
722
+ console.log("Marker effect running, selectedBar:", selectedBar);
723
+ if (!seriesMarkersRef.current) {
724
+ console.log("Marker API not initialized yet.");
725
+ return;
726
+ }
727
+ if (!enableBarSelection) {
728
+ console.log("Marker effect: clearing markers, selection disabled");
729
+ seriesMarkersRef.current.setMarkers([]);
730
+ return;
731
+ }
732
+ if (!selectedBar) {
733
+ console.log("Marker effect: clearing markers, no bar selected");
734
+ seriesMarkersRef.current.setMarkers([]);
735
+ return;
736
+ }
737
+ console.log("Marker effect: setting marker for bar:", selectedBar.timestamp);
738
+ const marker = {
739
+ time: selectedBar.timestamp / 1e3,
740
+ position: "aboveBar",
741
+ color: "#3b82f6",
742
+ shape: "circle",
743
+ text: "Selected"
744
+ };
745
+ seriesMarkersRef.current.setMarkers([marker]);
746
+ }, [selectedBar, enableBarSelection]);
747
+ useEffect(() => {
748
+ if (!chartRef.current || !selectedBar || !enableBarSelection) {
749
+ setSpotlightPosition(null);
750
+ return;
751
+ }
752
+ const updateSpotlightPosition = () => {
753
+ if (!chartRef.current || !selectedBar || !chartContainerRef.current) return;
754
+ const timeScale2 = chartRef.current.timeScale();
755
+ const barX = timeScale2.timeToCoordinate(selectedBar.timestamp / 1e3);
756
+ if (barX !== null) {
757
+ const rect = chartContainerRef.current.getBoundingClientRect();
758
+ const barWidth = Math.max(8, rect.width / bars.length);
759
+ setSpotlightPosition({ x: barX - barWidth / 2, width: barWidth });
760
+ }
761
+ };
762
+ updateSpotlightPosition();
763
+ const timeScale = chartRef.current.timeScale();
764
+ timeScale.subscribeVisibleLogicalRangeChange(updateSpotlightPosition);
765
+ return () => {
766
+ if (chartRef.current) {
767
+ chartRef.current.timeScale().unsubscribeVisibleLogicalRangeChange(updateSpotlightPosition);
768
+ }
769
+ };
770
+ }, [selectedBar, enableBarSelection, bars.length]);
771
+ const handleDeleteLine = (lineId) => {
772
+ if (onDeleteLine) {
773
+ onDeleteLine(lineId);
774
+ }
775
+ };
776
+ const handleAddLineFromContext = (type) => {
777
+ if (onAddLine && contextMenu) {
778
+ onAddLine(type, contextMenu.price);
779
+ setContextMenu(null);
780
+ }
781
+ };
782
+ const handleClearAllLines = () => {
783
+ if (onClearAllLines) {
784
+ onClearAllLines();
785
+ setContextMenu(null);
786
+ }
787
+ };
788
+ return /* @__PURE__ */ jsxs("div", { className: "relative space-y-4", children: [
789
+ isLoadingMore && /* @__PURE__ */ jsx("div", { className: "absolute top-4 left-1/2 transform -translate-x-1/2 bg-slate-800 text-slate-200 px-4 py-2 rounded-lg shadow-lg z-10", children: "Loading more data..." }),
790
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
791
+ /* @__PURE__ */ jsx("div", { ref: chartContainerRef, className: "w-full" }),
792
+ enableBarSelection && spotlightPosition && /* @__PURE__ */ jsx(
793
+ "div",
794
+ {
795
+ className: "absolute top-0 bottom-0 pointer-events-none z-5",
796
+ style: {
797
+ left: `${spotlightPosition.x}px`,
798
+ width: `${spotlightPosition.width}px`,
799
+ background: "rgba(59, 130, 246, 0.15)",
800
+ borderLeft: "1px solid rgba(59, 130, 246, 0.4)",
801
+ borderRight: "1px solid rgba(59, 130, 246, 0.4)"
802
+ }
803
+ }
804
+ ),
805
+ lines.map((line) => {
806
+ const y = linePositions.get(line.id);
807
+ if (y === null || y === void 0) return null;
808
+ return /* @__PURE__ */ jsx(
809
+ "button",
810
+ {
811
+ onClick: () => handleDeleteLine(line.id),
812
+ className: "absolute bg-red-500/70 hover:bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center text-xs font-bold transition-colors z-20 shadow-md",
813
+ style: {
814
+ right: "68px",
815
+ top: `${y - 8}px`
816
+ },
817
+ title: "Delete line",
818
+ children: "\xD7"
819
+ },
820
+ line.id
821
+ );
822
+ })
823
+ ] }),
824
+ contextMenu && /* @__PURE__ */ jsxs(
825
+ "div",
826
+ {
827
+ className: "absolute bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-30 py-1 min-w-[180px]",
828
+ style: {
829
+ left: `${contextMenu.x}px`,
830
+ top: `${contextMenu.y}px`
831
+ },
832
+ children: [
833
+ /* @__PURE__ */ jsxs("div", { className: "px-3 py-1 text-xs text-slate-400 border-b border-slate-600", children: [
834
+ "Price: ",
835
+ contextMenu.price.toFixed(3)
836
+ ] }),
837
+ /* @__PURE__ */ jsx(
838
+ "button",
839
+ {
840
+ onClick: () => handleAddLineFromContext("entry"),
841
+ className: "w-full px-3 py-2 text-left text-sm text-slate-200 hover:bg-slate-700 transition-colors",
842
+ children: "Add Entry Line"
843
+ }
844
+ ),
845
+ /* @__PURE__ */ jsx(
846
+ "button",
847
+ {
848
+ onClick: () => handleAddLineFromContext("stopLoss"),
849
+ className: "w-full px-3 py-2 text-left text-sm text-slate-200 hover:bg-slate-700 transition-colors",
850
+ children: "Add Stop Loss"
851
+ }
852
+ ),
853
+ /* @__PURE__ */ jsx(
854
+ "button",
855
+ {
856
+ onClick: () => handleAddLineFromContext("takeProfit"),
857
+ className: "w-full px-3 py-2 text-left text-sm text-slate-200 hover:bg-slate-700 transition-colors",
858
+ children: "Add Take Profit"
859
+ }
860
+ ),
861
+ /* @__PURE__ */ jsx("div", { className: "border-t border-slate-600 my-1" }),
862
+ /* @__PURE__ */ jsx(
863
+ "button",
864
+ {
865
+ onClick: () => handleAddLineFromContext("support"),
866
+ className: "w-full px-3 py-2 text-left text-sm text-slate-200 hover:bg-slate-700 transition-colors",
867
+ children: "Add Support Line"
868
+ }
869
+ ),
870
+ /* @__PURE__ */ jsx(
871
+ "button",
872
+ {
873
+ onClick: () => handleAddLineFromContext("resistance"),
874
+ className: "w-full px-3 py-2 text-left text-sm text-slate-200 hover:bg-slate-700 transition-colors",
875
+ children: "Add Resistance Line"
876
+ }
877
+ ),
878
+ /* @__PURE__ */ jsx("div", { className: "border-t border-slate-600 my-1" }),
879
+ /* @__PURE__ */ jsx(
880
+ "button",
881
+ {
882
+ onClick: handleClearAllLines,
883
+ className: "w-full px-3 py-2 text-left text-sm text-red-400 hover:bg-slate-700 transition-colors",
884
+ children: "Clear All Lines"
885
+ }
886
+ )
887
+ ]
888
+ }
889
+ )
890
+ ] });
891
+ }
892
+ function getLineStyle(style) {
893
+ switch (style) {
894
+ case "dashed":
895
+ return LineStyle.Dashed;
896
+ case "dotted":
897
+ return LineStyle.Dotted;
898
+ default:
899
+ return LineStyle.Solid;
900
+ }
901
+ }
902
+ var IndicatorCategory = /* @__PURE__ */ ((IndicatorCategory2) => {
903
+ IndicatorCategory2["TREND"] = "Trend";
904
+ IndicatorCategory2["MOMENTUM"] = "Momentum";
905
+ IndicatorCategory2["VOLATILITY"] = "Volatility";
906
+ IndicatorCategory2["VOLUME"] = "Volume";
907
+ IndicatorCategory2["OSCILLATORS"] = "Oscillators";
908
+ return IndicatorCategory2;
909
+ })(IndicatorCategory || {});
910
+ var ChartSeriesType = /* @__PURE__ */ ((ChartSeriesType2) => {
911
+ ChartSeriesType2["LINE"] = "line";
912
+ ChartSeriesType2["HISTOGRAM"] = "histogram";
913
+ ChartSeriesType2["AREA"] = "area";
914
+ return ChartSeriesType2;
915
+ })(ChartSeriesType || {});
916
+ var SettingFieldTypeSchema = z.enum([
917
+ "number",
918
+ "color",
919
+ "boolean",
920
+ "select",
921
+ "lineStyle"
922
+ ]);
923
+ var LineStyleSchema = z.enum(["solid", "dashed", "dotted"]);
924
+ var SettingFieldSchema = z.object({
925
+ type: SettingFieldTypeSchema,
926
+ label: z.string(),
927
+ defaultValue: z.union([z.string(), z.number(), z.boolean()]),
928
+ description: z.string().optional(),
929
+ min: z.number().optional(),
930
+ max: z.number().optional(),
931
+ step: z.number().optional(),
932
+ options: z.array(z.object({ label: z.string(), value: z.string() })).optional()
933
+ });
934
+ var IndicatorSettingsSchema = z.record(z.string(), SettingFieldSchema);
935
+ var IndicatorMetadataSchema = z.object({
936
+ id: z.string(),
937
+ name: z.string(),
938
+ description: z.string(),
939
+ category: z.nativeEnum(IndicatorCategory),
940
+ version: z.string().default("1.0.0")
941
+ });
942
+ var RenderConfigSchema = z.object({
943
+ seriesType: z.nativeEnum(ChartSeriesType),
944
+ outputCount: z.number().default(1),
945
+ overlay: z.boolean().default(true),
946
+ hasBandFill: z.boolean().optional(),
947
+ fillBands: z.object({
948
+ upper: z.string(),
949
+ lower: z.string()
950
+ }).optional()
951
+ });
952
+ var IndicatorInstanceSchema = z.object({
953
+ id: z.string(),
954
+ definitionId: z.string(),
955
+ name: z.string(),
956
+ settings: z.record(z.string(), z.any())
957
+ });
958
+ function IndicatorBrowser({
959
+ isOpen,
960
+ onClose,
961
+ onAddIndicator
962
+ }) {
963
+ const [searchQuery, setSearchQuery] = useState("");
964
+ const [selectedCategory, setSelectedCategory] = useState("All");
965
+ const categories = useMemo(() => {
966
+ return ["All", ...Object.values(IndicatorCategory)];
967
+ }, []);
968
+ const filteredIndicators = useMemo(() => {
969
+ let indicators = indicatorRegistry.getAll();
970
+ if (selectedCategory !== "All") {
971
+ indicators = indicators.filter(
972
+ (ind) => ind.metadata.category === selectedCategory
973
+ );
974
+ }
975
+ if (searchQuery.trim()) {
976
+ indicators = indicatorRegistry.search(searchQuery);
977
+ if (selectedCategory !== "All") {
978
+ indicators = indicators.filter(
979
+ (ind) => ind.metadata.category === selectedCategory
980
+ );
981
+ }
982
+ }
983
+ return indicators;
984
+ }, [searchQuery, selectedCategory]);
985
+ if (!isOpen) return null;
986
+ const handleAddIndicator = (definitionId) => {
987
+ onAddIndicator(definitionId);
988
+ onClose();
989
+ };
990
+ return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50", children: /* @__PURE__ */ jsxs("div", { className: "bg-slate-800 rounded-lg shadow-xl w-full max-w-3xl max-h-[80vh] flex flex-col", children: [
991
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-4 border-b border-slate-700", children: [
992
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-slate-100", children: "Add Indicator" }),
993
+ /* @__PURE__ */ jsx(
994
+ "button",
995
+ {
996
+ onClick: onClose,
997
+ className: "text-slate-400 hover:text-slate-200 transition",
998
+ children: /* @__PURE__ */ jsx(X, { size: 20 })
999
+ }
1000
+ )
1001
+ ] }),
1002
+ /* @__PURE__ */ jsxs("div", { className: "p-4 border-b border-slate-700", children: [
1003
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1004
+ /* @__PURE__ */ jsx(
1005
+ Search,
1006
+ {
1007
+ className: "absolute left-3 top-1/2 -translate-y-1/2 text-slate-400",
1008
+ size: 20
1009
+ }
1010
+ ),
1011
+ /* @__PURE__ */ jsx(
1012
+ "input",
1013
+ {
1014
+ type: "text",
1015
+ placeholder: "Search indicators...",
1016
+ value: searchQuery,
1017
+ onChange: (e) => setSearchQuery(e.target.value),
1018
+ className: "w-full pl-10 pr-4 py-2 bg-slate-700 border border-slate-600 rounded-lg text-slate-100 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
1019
+ }
1020
+ )
1021
+ ] }),
1022
+ /* @__PURE__ */ jsx("div", { className: "flex gap-2 mt-3 overflow-x-auto pb-2", children: categories.map((category) => /* @__PURE__ */ jsx(
1023
+ "button",
1024
+ {
1025
+ onClick: () => setSelectedCategory(category),
1026
+ className: `px-3 py-1.5 rounded-lg text-sm font-medium whitespace-nowrap transition ${selectedCategory === category ? "bg-blue-600 text-white" : "bg-slate-700 text-slate-300 hover:bg-slate-600"}`,
1027
+ children: category
1028
+ },
1029
+ category
1030
+ )) })
1031
+ ] }),
1032
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-4", children: filteredIndicators.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center py-12 text-slate-400", children: [
1033
+ /* @__PURE__ */ jsx("p", { children: "No indicators found" }),
1034
+ searchQuery && /* @__PURE__ */ jsx("p", { className: "text-sm mt-2", children: "Try adjusting your search or category filter" })
1035
+ ] }) : /* @__PURE__ */ jsx("div", { className: "grid gap-3", children: filteredIndicators.map((indicator) => /* @__PURE__ */ jsx(
1036
+ IndicatorCard,
1037
+ {
1038
+ indicator,
1039
+ onAdd: () => handleAddIndicator(indicator.metadata.id)
1040
+ },
1041
+ indicator.metadata.id
1042
+ )) }) })
1043
+ ] }) });
1044
+ }
1045
+ function IndicatorCard({ indicator, onAdd }) {
1046
+ const getCategoryColor = (category) => {
1047
+ switch (category) {
1048
+ case "Trend" /* TREND */:
1049
+ return "bg-blue-500/20 text-blue-400";
1050
+ case "Momentum" /* MOMENTUM */:
1051
+ return "bg-purple-500/20 text-purple-400";
1052
+ case "Volatility" /* VOLATILITY */:
1053
+ return "bg-orange-500/20 text-orange-400";
1054
+ case "Volume" /* VOLUME */:
1055
+ return "bg-green-500/20 text-green-400";
1056
+ case "Oscillators" /* OSCILLATORS */:
1057
+ return "bg-pink-500/20 text-pink-400";
1058
+ default:
1059
+ return "bg-slate-500/20 text-slate-400";
1060
+ }
1061
+ };
1062
+ return /* @__PURE__ */ jsx("div", { className: "bg-slate-700/50 rounded-lg p-4 hover:bg-slate-700 transition group", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
1063
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
1064
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
1065
+ /* @__PURE__ */ jsx("h3", { className: "text-slate-100 font-medium", children: indicator.metadata.name }),
1066
+ /* @__PURE__ */ jsx(
1067
+ "span",
1068
+ {
1069
+ className: `px-2 py-0.5 rounded text-xs font-medium ${getCategoryColor(
1070
+ indicator.metadata.category
1071
+ )}`,
1072
+ children: indicator.metadata.category
1073
+ }
1074
+ )
1075
+ ] }),
1076
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-400", children: indicator.metadata.description })
1077
+ ] }),
1078
+ /* @__PURE__ */ jsxs(
1079
+ "button",
1080
+ {
1081
+ onClick: onAdd,
1082
+ className: "flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition text-sm font-medium opacity-0 group-hover:opacity-100",
1083
+ children: [
1084
+ /* @__PURE__ */ jsx(Plus, { size: 16 }),
1085
+ "Add"
1086
+ ]
1087
+ }
1088
+ )
1089
+ ] }) });
1090
+ }
1091
+ function IndicatorSettingsForm({
1092
+ settings,
1093
+ currentValues,
1094
+ onChange
1095
+ }) {
1096
+ return /* @__PURE__ */ jsx("div", { className: "space-y-4", children: Object.entries(settings).map(([key, field]) => /* @__PURE__ */ jsxs("div", { children: [
1097
+ /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-slate-300 mb-1", children: field.label }),
1098
+ field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-400 mb-2", children: field.description }),
1099
+ renderField(key, field, currentValues[key], onChange)
1100
+ ] }, key)) });
1101
+ }
1102
+ function parseColor(colorValue) {
1103
+ const rgbaMatch = colorValue.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)$/);
1104
+ if (rgbaMatch) {
1105
+ const r = parseInt(rgbaMatch[1]);
1106
+ const g = parseInt(rgbaMatch[2]);
1107
+ const b = parseInt(rgbaMatch[3]);
1108
+ const a = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
1109
+ const hex = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
1110
+ return { hex, opacity: Math.round(a * 100) };
1111
+ }
1112
+ return { hex: colorValue, opacity: 100 };
1113
+ }
1114
+ function hexToRgba(hex, opacity) {
1115
+ const r = parseInt(hex.slice(1, 3), 16);
1116
+ const g = parseInt(hex.slice(3, 5), 16);
1117
+ const b = parseInt(hex.slice(5, 7), 16);
1118
+ const a = opacity / 100;
1119
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
1120
+ }
1121
+ function ColorPicker({ value, defaultValue, onChange }) {
1122
+ const colorValue = value ?? defaultValue;
1123
+ const { hex, opacity } = parseColor(colorValue);
1124
+ const [localHex, setLocalHex] = useState(hex);
1125
+ const [localOpacity, setLocalOpacity] = useState(opacity);
1126
+ const handleColorChange = (newHex) => {
1127
+ setLocalHex(newHex);
1128
+ onChange(hexToRgba(newHex, localOpacity));
1129
+ };
1130
+ const handleOpacityChange = (newOpacity) => {
1131
+ setLocalOpacity(newOpacity);
1132
+ onChange(hexToRgba(localHex, newOpacity));
1133
+ };
1134
+ const handleTextChange = (newValue) => {
1135
+ const parsed = parseColor(newValue);
1136
+ setLocalHex(parsed.hex);
1137
+ setLocalOpacity(parsed.opacity);
1138
+ onChange(newValue);
1139
+ };
1140
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1141
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1142
+ /* @__PURE__ */ jsx(
1143
+ "input",
1144
+ {
1145
+ type: "color",
1146
+ value: localHex,
1147
+ onChange: (e) => handleColorChange(e.target.value),
1148
+ className: "w-12 h-10 rounded cursor-pointer"
1149
+ }
1150
+ ),
1151
+ /* @__PURE__ */ jsx(
1152
+ "input",
1153
+ {
1154
+ type: "text",
1155
+ value: colorValue,
1156
+ onChange: (e) => handleTextChange(e.target.value),
1157
+ className: "flex-1 px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
1158
+ }
1159
+ )
1160
+ ] }),
1161
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1162
+ /* @__PURE__ */ jsx(
1163
+ "input",
1164
+ {
1165
+ type: "range",
1166
+ min: "0",
1167
+ max: "100",
1168
+ value: localOpacity,
1169
+ onChange: (e) => handleOpacityChange(parseInt(e.target.value)),
1170
+ className: "flex-1 h-2 bg-slate-600 rounded-lg appearance-none cursor-pointer"
1171
+ }
1172
+ ),
1173
+ /* @__PURE__ */ jsxs("span", { className: "text-sm text-slate-400 w-12 text-right", children: [
1174
+ localOpacity,
1175
+ "%"
1176
+ ] })
1177
+ ] })
1178
+ ] });
1179
+ }
1180
+ function renderField(key, field, value, onChange) {
1181
+ switch (field.type) {
1182
+ case "number":
1183
+ return /* @__PURE__ */ jsx(
1184
+ "input",
1185
+ {
1186
+ type: "number",
1187
+ value: value ?? field.defaultValue,
1188
+ onChange: (e) => onChange(key, parseFloat(e.target.value)),
1189
+ min: field.min,
1190
+ max: field.max,
1191
+ step: field.step,
1192
+ className: "w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
1193
+ }
1194
+ );
1195
+ case "color":
1196
+ return /* @__PURE__ */ jsx(
1197
+ ColorPicker,
1198
+ {
1199
+ value,
1200
+ defaultValue: field.defaultValue,
1201
+ onChange: (newValue) => onChange(key, newValue)
1202
+ }
1203
+ );
1204
+ case "boolean":
1205
+ return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
1206
+ /* @__PURE__ */ jsx(
1207
+ "input",
1208
+ {
1209
+ type: "checkbox",
1210
+ checked: value ?? field.defaultValue,
1211
+ onChange: (e) => onChange(key, e.target.checked),
1212
+ className: "w-4 h-4 text-blue-600 bg-slate-700 border-slate-600 rounded focus:ring-blue-500"
1213
+ }
1214
+ ),
1215
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-300", children: value ?? field.defaultValue ? "Enabled" : "Disabled" })
1216
+ ] });
1217
+ case "select":
1218
+ return /* @__PURE__ */ jsx(
1219
+ "select",
1220
+ {
1221
+ value: value ?? field.defaultValue,
1222
+ onChange: (e) => onChange(key, e.target.value),
1223
+ className: "w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500",
1224
+ children: field.options?.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
1225
+ }
1226
+ );
1227
+ case "lineStyle":
1228
+ return /* @__PURE__ */ jsxs(
1229
+ "select",
1230
+ {
1231
+ value: value ?? field.defaultValue,
1232
+ onChange: (e) => onChange(key, e.target.value),
1233
+ className: "w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500",
1234
+ children: [
1235
+ /* @__PURE__ */ jsx("option", { value: "solid", children: "Solid" }),
1236
+ /* @__PURE__ */ jsx("option", { value: "dashed", children: "Dashed" }),
1237
+ /* @__PURE__ */ jsx("option", { value: "dotted", children: "Dotted" })
1238
+ ]
1239
+ }
1240
+ );
1241
+ default:
1242
+ return /* @__PURE__ */ jsx(
1243
+ "input",
1244
+ {
1245
+ type: "text",
1246
+ value: value ?? field.defaultValue,
1247
+ onChange: (e) => onChange(key, e.target.value),
1248
+ className: "w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
1249
+ }
1250
+ );
1251
+ }
1252
+ }
1253
+ function SettingsDialog({
1254
+ isOpen,
1255
+ onClose,
1256
+ indicator,
1257
+ onSave
1258
+ }) {
1259
+ const [settings, setSettings] = useState({});
1260
+ useEffect(() => {
1261
+ if (indicator) {
1262
+ setSettings({ ...indicator.settings });
1263
+ }
1264
+ }, [indicator]);
1265
+ if (!isOpen || !indicator) return null;
1266
+ const definition = indicatorRegistry.get(indicator.definitionId);
1267
+ if (!definition) return null;
1268
+ const handleSave = () => {
1269
+ onSave(indicator.id, settings);
1270
+ onClose();
1271
+ };
1272
+ const handleChange = (key, value) => {
1273
+ setSettings((prev) => ({
1274
+ ...prev,
1275
+ [key]: value
1276
+ }));
1277
+ };
1278
+ return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50", children: /* @__PURE__ */ jsxs("div", { className: "bg-slate-800 rounded-lg shadow-xl w-full max-w-md", children: [
1279
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-4 border-b border-slate-700", children: [
1280
+ /* @__PURE__ */ jsxs("h2", { className: "text-lg font-semibold text-slate-100", children: [
1281
+ indicator.name,
1282
+ " Settings"
1283
+ ] }),
1284
+ /* @__PURE__ */ jsx(
1285
+ "button",
1286
+ {
1287
+ onClick: onClose,
1288
+ className: "text-slate-400 hover:text-slate-200 transition",
1289
+ children: /* @__PURE__ */ jsx(X, { size: 20 })
1290
+ }
1291
+ )
1292
+ ] }),
1293
+ /* @__PURE__ */ jsx("div", { className: "p-4", children: /* @__PURE__ */ jsx(
1294
+ IndicatorSettingsForm,
1295
+ {
1296
+ settings: definition.settings,
1297
+ currentValues: settings,
1298
+ onChange: handleChange
1299
+ }
1300
+ ) }),
1301
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 p-4 border-t border-slate-700", children: [
1302
+ /* @__PURE__ */ jsx(
1303
+ "button",
1304
+ {
1305
+ onClick: onClose,
1306
+ className: "px-4 py-2 text-slate-300 hover:text-slate-100 transition",
1307
+ children: "Cancel"
1308
+ }
1309
+ ),
1310
+ /* @__PURE__ */ jsx(
1311
+ "button",
1312
+ {
1313
+ onClick: handleSave,
1314
+ className: "px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition",
1315
+ children: "Save Changes"
1316
+ }
1317
+ )
1318
+ ] })
1319
+ ] }) });
1320
+ }
1321
+ function useChartAPI(options) {
1322
+ const [lines, setLines] = useState([]);
1323
+ const [indicators, setIndicators] = useState([]);
1324
+ const [isLoading, setIsLoading] = useState(true);
1325
+ useEffect(() => {
1326
+ if (options?.persistenceAdapter) {
1327
+ options.persistenceAdapter.loadIndicators().then((loaded) => {
1328
+ setIndicators(loaded);
1329
+ setIsLoading(false);
1330
+ });
1331
+ } else {
1332
+ setIsLoading(false);
1333
+ }
1334
+ }, [options?.persistenceAdapter]);
1335
+ useEffect(() => {
1336
+ if (!isLoading && options?.persistenceAdapter) {
1337
+ options.persistenceAdapter.saveIndicators(indicators);
1338
+ }
1339
+ }, [indicators, isLoading, options?.persistenceAdapter]);
1340
+ const addLine = useCallback(
1341
+ (id, price, options2) => {
1342
+ const newLine = {
1343
+ id,
1344
+ price,
1345
+ color: options2?.color || "#3b82f6",
1346
+ lineWidth: options2?.lineWidth || 2,
1347
+ lineStyle: options2?.lineStyle || "solid",
1348
+ title: options2?.title
1349
+ };
1350
+ setLines((prev) => [...prev, newLine]);
1351
+ },
1352
+ []
1353
+ );
1354
+ const removeLine = useCallback((id) => {
1355
+ setLines((prev) => prev.filter((line) => line.id !== id));
1356
+ }, []);
1357
+ const updateLine = useCallback((id, price) => {
1358
+ setLines(
1359
+ (prev) => prev.map((line) => line.id === id ? { ...line, price } : line)
1360
+ );
1361
+ }, []);
1362
+ const addEntryLine = useCallback(
1363
+ (price) => {
1364
+ const id = `entry-${Date.now()}`;
1365
+ addLine(id, price, {
1366
+ color: "#3b82f6",
1367
+ title: "Entry",
1368
+ lineStyle: "solid"
1369
+ });
1370
+ },
1371
+ [addLine]
1372
+ );
1373
+ const addStopLoss = useCallback(
1374
+ (price) => {
1375
+ const id = `stop-loss-${Date.now()}`;
1376
+ addLine(id, price, {
1377
+ color: "#ef4444",
1378
+ title: "Stop Loss",
1379
+ lineStyle: "dashed"
1380
+ });
1381
+ },
1382
+ [addLine]
1383
+ );
1384
+ const addTakeProfit = useCallback(
1385
+ (price) => {
1386
+ const id = `take-profit-${Date.now()}`;
1387
+ addLine(id, price, {
1388
+ color: "#10b981",
1389
+ title: "Take Profit",
1390
+ lineStyle: "dashed"
1391
+ });
1392
+ },
1393
+ [addLine]
1394
+ );
1395
+ const addSupportLine = useCallback(
1396
+ (price) => {
1397
+ const id = `support-${Date.now()}`;
1398
+ addLine(id, price, {
1399
+ color: "#10b981",
1400
+ title: "Support",
1401
+ lineStyle: "dotted"
1402
+ });
1403
+ },
1404
+ [addLine]
1405
+ );
1406
+ const addResistanceLine = useCallback(
1407
+ (price) => {
1408
+ const id = `resistance-${Date.now()}`;
1409
+ addLine(id, price, {
1410
+ color: "#f97316",
1411
+ title: "Resistance",
1412
+ lineStyle: "dotted"
1413
+ });
1414
+ },
1415
+ [addLine]
1416
+ );
1417
+ const clearAllLines = useCallback(() => {
1418
+ setLines([]);
1419
+ }, []);
1420
+ const addLineByType = useCallback(
1421
+ (type, price) => {
1422
+ if (type === "entry") {
1423
+ addEntryLine(price);
1424
+ } else if (type === "stopLoss") {
1425
+ addStopLoss(price);
1426
+ } else if (type === "takeProfit") {
1427
+ addTakeProfit(price);
1428
+ } else if (type === "support") {
1429
+ addSupportLine(price);
1430
+ } else if (type === "resistance") {
1431
+ addResistanceLine(price);
1432
+ }
1433
+ },
1434
+ [addEntryLine, addStopLoss, addTakeProfit, addSupportLine, addResistanceLine]
1435
+ );
1436
+ const addIndicator = useCallback((definitionId, customSettings) => {
1437
+ const instance = indicatorRegistry.createInstance(definitionId, customSettings);
1438
+ setIndicators((prev) => [...prev, instance]);
1439
+ return instance;
1440
+ }, []);
1441
+ const removeIndicator = useCallback((id) => {
1442
+ setIndicators((prev) => prev.filter((indicator) => indicator.id !== id));
1443
+ indicatorCalculator.invalidateCache(id);
1444
+ }, []);
1445
+ const updateIndicatorSettings = useCallback(
1446
+ (id, settings) => {
1447
+ setIndicators(
1448
+ (prev) => prev.map(
1449
+ (indicator) => indicator.id === id ? { ...indicator, settings } : indicator
1450
+ )
1451
+ );
1452
+ indicatorCalculator.invalidateCache(id);
1453
+ },
1454
+ []
1455
+ );
1456
+ return {
1457
+ lines,
1458
+ indicators,
1459
+ isLoadingIndicators: isLoading,
1460
+ addLine,
1461
+ removeLine,
1462
+ updateLine,
1463
+ addEntryLine,
1464
+ addStopLoss,
1465
+ addTakeProfit,
1466
+ addSupportLine,
1467
+ addResistanceLine,
1468
+ clearAllLines,
1469
+ addLineByType,
1470
+ addIndicator,
1471
+ removeIndicator,
1472
+ updateIndicatorSettings
1473
+ };
1474
+ }
1475
+
1476
+ // src/utils/barUtils.ts
1477
+ function validateBar(bar) {
1478
+ if (!bar || typeof bar !== "object") return false;
1479
+ const required = ["timestamp", "open", "high", "low", "close", "volume"];
1480
+ for (const field of required) {
1481
+ if (!(field in bar)) return false;
1482
+ if (typeof bar[field] !== "number") return false;
1483
+ }
1484
+ if (bar.timestamp <= 0) return false;
1485
+ if (bar.open <= 0 || bar.high <= 0 || bar.low <= 0 || bar.close <= 0) return false;
1486
+ if (bar.volume < 0) return false;
1487
+ if (bar.high < bar.low) return false;
1488
+ if (bar.high < bar.open || bar.high < bar.close) return false;
1489
+ if (bar.low > bar.open || bar.low > bar.close) return false;
1490
+ return true;
1491
+ }
1492
+ function isValidBar(bar) {
1493
+ return validateBar(bar);
1494
+ }
1495
+ function normalizeTimestamp(timestamp) {
1496
+ if (timestamp < 1e10) {
1497
+ return timestamp * 1e3;
1498
+ }
1499
+ return timestamp;
1500
+ }
1501
+ function sortBars(bars) {
1502
+ return [...bars].sort((a, b) => a.timestamp - b.timestamp);
1503
+ }
1504
+ function deduplicateBars(bars) {
1505
+ const seen = /* @__PURE__ */ new Map();
1506
+ for (const bar of bars) {
1507
+ const existing = seen.get(bar.timestamp);
1508
+ if (!existing) {
1509
+ seen.set(bar.timestamp, bar);
1510
+ }
1511
+ }
1512
+ return Array.from(seen.values()).sort((a, b) => a.timestamp - b.timestamp);
1513
+ }
1514
+ function mergeBars(existingBars, newBars) {
1515
+ const allBars = [...existingBars, ...newBars];
1516
+ return deduplicateBars(allBars);
1517
+ }
1518
+ function validateAndNormalizeBars(bars) {
1519
+ console.log(`validateAndNormalizeBars: received ${bars.length} bars`);
1520
+ const validated = bars.filter((bar) => {
1521
+ if (!validateBar(bar)) {
1522
+ console.warn("Invalid bar data:", bar);
1523
+ return false;
1524
+ }
1525
+ return true;
1526
+ }).map((bar) => ({
1527
+ ...bar,
1528
+ timestamp: normalizeTimestamp(bar.timestamp)
1529
+ }));
1530
+ console.log(`validateAndNormalizeBars: returning ${validated.length} valid bars`);
1531
+ return validated;
1532
+ }
1533
+ function updateBarInArray(bars, updatedBar) {
1534
+ const index = bars.findIndex((b) => b.timestamp === updatedBar.timestamp);
1535
+ if (index === -1) {
1536
+ return [...bars, updatedBar].sort((a, b) => a.timestamp - b.timestamp);
1537
+ }
1538
+ const updated = [...bars];
1539
+ updated[index] = updatedBar;
1540
+ return updated;
1541
+ }
1542
+ function appendBar(bars, newBar) {
1543
+ const exists = bars.find((b) => b.timestamp === newBar.timestamp);
1544
+ if (exists) {
1545
+ return bars;
1546
+ }
1547
+ return [...bars, newBar];
1548
+ }
1549
+ function prependBars(bars, newBars) {
1550
+ const existingTimestamps = new Set(bars.map((b) => b.timestamp));
1551
+ const uniqueNewBars = newBars.filter((b) => !existingTimestamps.has(b.timestamp));
1552
+ return [...uniqueNewBars, ...bars];
1553
+ }
1554
+ function getOldestBar(bars) {
1555
+ if (bars.length === 0) return null;
1556
+ return bars[0];
1557
+ }
1558
+ function getNewestBar(bars) {
1559
+ if (bars.length === 0) return null;
1560
+ return bars[bars.length - 1];
1561
+ }
1562
+ function updateCurrentBar(bars, tradePrice, tradeVolume) {
1563
+ if (bars.length === 0) return bars;
1564
+ const updated = [...bars];
1565
+ const lastBar = { ...updated[updated.length - 1] };
1566
+ lastBar.close = tradePrice;
1567
+ lastBar.high = Math.max(lastBar.high, tradePrice);
1568
+ lastBar.low = Math.min(lastBar.low, tradePrice);
1569
+ lastBar.volume += tradeVolume;
1570
+ updated[updated.length - 1] = lastBar;
1571
+ return updated;
1572
+ }
1573
+
1574
+ // src/hooks/useBarsData.ts
1575
+ function useBarsData(options = {}) {
1576
+ const {
1577
+ adapter,
1578
+ symbol = "AAPL",
1579
+ timeframe = "5Min",
1580
+ autoFetch = false,
1581
+ autoSubscribe = false,
1582
+ limit = 500
1583
+ } = options;
1584
+ const [bars, setBarsState] = useState([]);
1585
+ const [loading, setLoading] = useState(false);
1586
+ const [error, setError] = useState(null);
1587
+ const [connected, setConnected] = useState(false);
1588
+ const subscriptionRef = useRef(null);
1589
+ const isMountedRef = useRef(true);
1590
+ const hasInitialFetchRef = useRef(false);
1591
+ const isFetchingRef = useRef(false);
1592
+ const setBars = useCallback((newBars) => {
1593
+ const validated = validateAndNormalizeBars(newBars);
1594
+ const sorted = sortBars(validated);
1595
+ setBarsState(sorted);
1596
+ }, []);
1597
+ const appendBar2 = useCallback((bar) => {
1598
+ setBarsState((prev) => appendBar(prev, bar));
1599
+ }, []);
1600
+ const updateLastBar = useCallback((bar) => {
1601
+ setBarsState((prev) => {
1602
+ if (prev.length === 0) return [bar];
1603
+ const updated = [...prev];
1604
+ updated[updated.length - 1] = bar;
1605
+ return updated;
1606
+ });
1607
+ }, []);
1608
+ const updateCurrentBar2 = useCallback((tradePrice, tradeVolume) => {
1609
+ setBarsState((prev) => updateCurrentBar(prev, tradePrice, tradeVolume));
1610
+ }, []);
1611
+ const prependBars2 = useCallback((newBars) => {
1612
+ const validated = validateAndNormalizeBars(newBars);
1613
+ setBarsState((prev) => prependBars(prev, validated));
1614
+ }, []);
1615
+ const clearBars = useCallback(() => {
1616
+ setBarsState([]);
1617
+ }, []);
1618
+ const fetchHistorical = useCallback(
1619
+ async (params = {}) => {
1620
+ if (!adapter) {
1621
+ setError(new Error("No adapter provided"));
1622
+ return;
1623
+ }
1624
+ if (isFetchingRef.current) {
1625
+ console.log("fetchHistorical: already fetching, skipping");
1626
+ return;
1627
+ }
1628
+ console.log("fetchHistorical called, params:", params);
1629
+ console.log("isMountedRef.current:", isMountedRef.current);
1630
+ isFetchingRef.current = true;
1631
+ setLoading(true);
1632
+ setError(null);
1633
+ try {
1634
+ const fetchParams = {
1635
+ symbol: params.symbol || symbol,
1636
+ timeframe: params.timeframe || timeframe,
1637
+ limit: params.limit || limit,
1638
+ before: params.before,
1639
+ after: params.after
1640
+ };
1641
+ console.log("Calling adapter.fetchHistoricalBars with:", fetchParams);
1642
+ const data = await adapter.fetchHistoricalBars(fetchParams);
1643
+ console.log("adapter.fetchHistoricalBars returned, data.length:", data.length);
1644
+ if (isMountedRef.current) {
1645
+ console.log("Component still mounted, processing data");
1646
+ const validated = validateAndNormalizeBars(data);
1647
+ if (params.before) {
1648
+ console.log("Prepending bars (before param present)");
1649
+ setBarsState((prev) => prependBars(prev, validated));
1650
+ } else {
1651
+ console.log("Setting initial bars (no before param)");
1652
+ const sorted = sortBars(validated);
1653
+ setBarsState(sorted);
1654
+ }
1655
+ } else {
1656
+ console.log("Component unmounted, skipping data update");
1657
+ }
1658
+ } catch (err) {
1659
+ console.log("Error caught:", err);
1660
+ if (isMountedRef.current) {
1661
+ const error2 = err instanceof Error ? err : new Error("Failed to fetch historical data");
1662
+ setError(error2);
1663
+ console.error("Failed to fetch historical data:", error2);
1664
+ }
1665
+ } finally {
1666
+ console.log("Finally block executing, isMountedRef.current:", isMountedRef.current);
1667
+ isFetchingRef.current = false;
1668
+ if (isMountedRef.current) {
1669
+ console.log("Setting loading to false");
1670
+ setLoading(false);
1671
+ } else {
1672
+ console.log("Skipping setLoading(false) - component unmounted");
1673
+ }
1674
+ }
1675
+ },
1676
+ [adapter, symbol, timeframe, limit]
1677
+ );
1678
+ const subscribe = useCallback(() => {
1679
+ if (!adapter?.subscribeRealtime) {
1680
+ console.warn("Adapter does not support real-time subscriptions");
1681
+ return;
1682
+ }
1683
+ if (subscriptionRef.current) {
1684
+ console.warn("Already subscribed");
1685
+ return;
1686
+ }
1687
+ subscriptionRef.current = adapter.subscribeRealtime(symbol, {
1688
+ onBar: (bar) => {
1689
+ if (isMountedRef.current) {
1690
+ appendBar2(bar);
1691
+ }
1692
+ },
1693
+ onTrade: (price, volume) => {
1694
+ if (isMountedRef.current) {
1695
+ updateCurrentBar2(price, volume);
1696
+ }
1697
+ },
1698
+ onError: (err) => {
1699
+ if (isMountedRef.current) {
1700
+ setError(err);
1701
+ setConnected(false);
1702
+ }
1703
+ },
1704
+ onConnect: () => {
1705
+ if (isMountedRef.current) {
1706
+ setConnected(true);
1707
+ }
1708
+ },
1709
+ onDisconnect: () => {
1710
+ if (isMountedRef.current) {
1711
+ setConnected(false);
1712
+ }
1713
+ }
1714
+ });
1715
+ }, [adapter, symbol, appendBar2, updateCurrentBar2]);
1716
+ const unsubscribe = useCallback(() => {
1717
+ if (subscriptionRef.current) {
1718
+ subscriptionRef.current.unsubscribe();
1719
+ subscriptionRef.current = null;
1720
+ setConnected(false);
1721
+ }
1722
+ }, []);
1723
+ const refetch = useCallback(async () => {
1724
+ await fetchHistorical();
1725
+ }, [fetchHistorical]);
1726
+ useEffect(() => {
1727
+ console.log("useEffect[autoFetch] running, autoFetch:", autoFetch, "adapter:", !!adapter, "hasInitialFetch:", hasInitialFetchRef.current);
1728
+ if (autoFetch && adapter && !hasInitialFetchRef.current) {
1729
+ console.log("Calling fetchHistorical from useEffect (initial fetch)");
1730
+ hasInitialFetchRef.current = true;
1731
+ fetchHistorical();
1732
+ }
1733
+ }, [autoFetch, adapter, fetchHistorical]);
1734
+ useEffect(() => {
1735
+ if (autoSubscribe && adapter?.subscribeRealtime) {
1736
+ subscribe();
1737
+ }
1738
+ return () => {
1739
+ unsubscribe();
1740
+ };
1741
+ }, [autoSubscribe, adapter, subscribe, unsubscribe]);
1742
+ useEffect(() => {
1743
+ isMountedRef.current = true;
1744
+ return () => {
1745
+ isMountedRef.current = false;
1746
+ if (subscriptionRef.current) {
1747
+ subscriptionRef.current.unsubscribe();
1748
+ subscriptionRef.current = null;
1749
+ }
1750
+ };
1751
+ }, []);
1752
+ return {
1753
+ bars,
1754
+ loading,
1755
+ error,
1756
+ connected,
1757
+ setBars,
1758
+ appendBar: appendBar2,
1759
+ updateLastBar,
1760
+ updateCurrentBar: updateCurrentBar2,
1761
+ prependBars: prependBars2,
1762
+ clearBars,
1763
+ fetchHistorical,
1764
+ subscribe,
1765
+ unsubscribe,
1766
+ refetch
1767
+ };
1768
+ }
1769
+ function useRealtimeUpdates(initialBars, config) {
1770
+ const [bars, setBars] = useState([]);
1771
+ const updateIntervalRef = useRef();
1772
+ const newBarIntervalRef = useRef();
1773
+ const lastBarTimeRef = useRef(0);
1774
+ const initializedRef = useRef(false);
1775
+ const updateCurrentBar2 = useCallback((tradePrice, tradeVolume) => {
1776
+ setBars((prev) => {
1777
+ if (prev.length === 0) return prev;
1778
+ const updated = [...prev];
1779
+ const lastBar = { ...updated[updated.length - 1] };
1780
+ lastBar.close = tradePrice;
1781
+ lastBar.high = Math.max(lastBar.high, tradePrice);
1782
+ lastBar.low = Math.min(lastBar.low, tradePrice);
1783
+ lastBar.volume += tradeVolume;
1784
+ updated[updated.length - 1] = lastBar;
1785
+ return updated;
1786
+ });
1787
+ }, []);
1788
+ const addNewBar = useCallback((newBar) => {
1789
+ setBars((prev) => {
1790
+ const exists = prev.find((b) => b.timestamp === newBar.timestamp);
1791
+ if (exists) return prev;
1792
+ return [...prev, newBar];
1793
+ });
1794
+ }, []);
1795
+ const prependBars2 = useCallback((newBars) => {
1796
+ setBars((prev) => {
1797
+ const existingTimestamps = new Set(prev.map((b) => b.timestamp));
1798
+ const uniqueNewBars = newBars.filter((b) => !existingTimestamps.has(b.timestamp));
1799
+ return [...uniqueNewBars, ...prev];
1800
+ });
1801
+ }, []);
1802
+ useCallback((currentPrice) => {
1803
+ const volatility = 5e-4;
1804
+ const drift = 0;
1805
+ const change = currentPrice * (drift + volatility * (Math.random() * 2 - 1));
1806
+ return currentPrice + change;
1807
+ }, []);
1808
+ useEffect(() => {
1809
+ if (!initializedRef.current && initialBars.length > 0) {
1810
+ setBars(initialBars);
1811
+ initializedRef.current = true;
1812
+ }
1813
+ }, [initialBars]);
1814
+ useEffect(() => {
1815
+ if (!config.enabled || bars.length === 0) return;
1816
+ const lastBar = bars[bars.length - 1];
1817
+ lastBarTimeRef.current = lastBar.timestamp;
1818
+ updateIntervalRef.current = window.setInterval(() => {
1819
+ setBars((prev) => {
1820
+ if (prev.length === 0) return prev;
1821
+ const updated = [...prev];
1822
+ const currentBar = { ...updated[updated.length - 1] };
1823
+ const newPrice = currentBar.close * (1 + (Math.random() - 0.5) * 1e-3);
1824
+ const newVolume = Math.floor(Math.random() * 100) + 10;
1825
+ currentBar.close = newPrice;
1826
+ currentBar.high = Math.max(currentBar.high, newPrice);
1827
+ currentBar.low = Math.min(currentBar.low, newPrice);
1828
+ currentBar.volume += newVolume;
1829
+ updated[updated.length - 1] = currentBar;
1830
+ return updated;
1831
+ });
1832
+ }, config.updateIntervalMs);
1833
+ newBarIntervalRef.current = window.setInterval(() => {
1834
+ setBars((prev) => {
1835
+ if (prev.length === 0) return prev;
1836
+ const lastBar2 = prev[prev.length - 1];
1837
+ const newTimestamp = lastBar2.timestamp + config.newBarIntervalMs;
1838
+ const newPrice = lastBar2.close * (1 + (Math.random() - 0.5) * 2e-3);
1839
+ const newBar = {
1840
+ timestamp: newTimestamp,
1841
+ open: lastBar2.close,
1842
+ high: newPrice,
1843
+ low: Math.min(lastBar2.close, newPrice),
1844
+ close: newPrice,
1845
+ volume: Math.floor(Math.random() * 1e3) + 100
1846
+ };
1847
+ lastBarTimeRef.current = newTimestamp;
1848
+ return [...prev, newBar];
1849
+ });
1850
+ }, config.newBarIntervalMs);
1851
+ return () => {
1852
+ if (updateIntervalRef.current) {
1853
+ clearInterval(updateIntervalRef.current);
1854
+ }
1855
+ if (newBarIntervalRef.current) {
1856
+ clearInterval(newBarIntervalRef.current);
1857
+ }
1858
+ };
1859
+ }, [config.enabled, config.updateIntervalMs, config.newBarIntervalMs, bars.length]);
1860
+ return {
1861
+ bars,
1862
+ updateCurrentBar: updateCurrentBar2,
1863
+ addNewBar,
1864
+ prependBars: prependBars2
1865
+ };
1866
+ }
1867
+
1868
+ // src/indicators/core/persistence.ts
1869
+ var LocalStoragePersistenceAdapter = class {
1870
+ constructor() {
1871
+ __publicField(this, "storageKey", "chart_indicators");
1872
+ }
1873
+ async saveIndicators(indicators) {
1874
+ try {
1875
+ const serialized = JSON.stringify(indicators);
1876
+ localStorage.setItem(this.storageKey, serialized);
1877
+ } catch (error) {
1878
+ console.error("Error saving indicators to localStorage:", error);
1879
+ }
1880
+ }
1881
+ async loadIndicators() {
1882
+ try {
1883
+ const serialized = localStorage.getItem(this.storageKey);
1884
+ if (!serialized) {
1885
+ return [];
1886
+ }
1887
+ const parsed = JSON.parse(serialized);
1888
+ return Array.isArray(parsed) ? parsed : [];
1889
+ } catch (error) {
1890
+ console.error("Error loading indicators from localStorage:", error);
1891
+ return [];
1892
+ }
1893
+ }
1894
+ async deleteIndicator(id) {
1895
+ try {
1896
+ const indicators = await this.loadIndicators();
1897
+ const filtered = indicators.filter((ind) => ind.id !== id);
1898
+ await this.saveIndicators(filtered);
1899
+ } catch (error) {
1900
+ console.error("Error deleting indicator from localStorage:", error);
1901
+ }
1902
+ }
1903
+ };
1904
+ var NoOpPersistenceAdapter = class {
1905
+ async saveIndicators(indicators) {
1906
+ }
1907
+ async loadIndicators() {
1908
+ return [];
1909
+ }
1910
+ async deleteIndicator(id) {
1911
+ }
1912
+ };
1913
+ function createPersistenceAdapter(kind) {
1914
+ switch (kind) {
1915
+ case "localStorage":
1916
+ return new LocalStoragePersistenceAdapter();
1917
+ case "noop":
1918
+ return new NoOpPersistenceAdapter();
1919
+ default:
1920
+ throw new Error("Unknown persistence kind: " + kind);
1921
+ }
1922
+ }
1923
+
1924
+ // src/indicators/utils/calculations.ts
1925
+ function padIndicatorArray(result, barsLength) {
1926
+ const missing = barsLength - result.length;
1927
+ if (missing <= 0) return result;
1928
+ return [...Array(missing).fill(NaN), ...result];
1929
+ }
1930
+ function displaceArray(arr, offset) {
1931
+ if (offset <= 0) return arr;
1932
+ return [...arr.slice(offset), ...Array(offset).fill(NaN)];
1933
+ }
1934
+ function calculateSMA(bars, period) {
1935
+ const result = [];
1936
+ for (let i = 0; i < bars.length; i++) {
1937
+ if (i < period - 1) {
1938
+ result.push(NaN);
1939
+ } else {
1940
+ let sum = 0;
1941
+ for (let j = 0; j < period; j++) {
1942
+ sum += bars[i - j].close;
1943
+ }
1944
+ result.push(sum / period);
1945
+ }
1946
+ }
1947
+ return result;
1948
+ }
1949
+ function calculateEMA(bars, period) {
1950
+ const result = [];
1951
+ const multiplier = 2 / (period + 1);
1952
+ let ema = 0;
1953
+ for (let i = 0; i < bars.length; i++) {
1954
+ if (i < period - 1) {
1955
+ result.push(NaN);
1956
+ } else if (i === period - 1) {
1957
+ let sum = 0;
1958
+ for (let j = 0; j < period; j++) {
1959
+ sum += bars[i - j].close;
1960
+ }
1961
+ ema = sum / period;
1962
+ result.push(ema);
1963
+ } else {
1964
+ ema = (bars[i].close - ema) * multiplier + ema;
1965
+ result.push(ema);
1966
+ }
1967
+ }
1968
+ return result;
1969
+ }
1970
+ function calculateRSI(bars, period) {
1971
+ const result = [];
1972
+ const changes = [];
1973
+ for (let i = 1; i < bars.length; i++) {
1974
+ changes.push(bars[i].close - bars[i - 1].close);
1975
+ }
1976
+ for (let i = 0; i < bars.length; i++) {
1977
+ if (i < period) {
1978
+ result.push(NaN);
1979
+ } else {
1980
+ let gains = 0;
1981
+ let losses = 0;
1982
+ for (let j = i - period; j < i; j++) {
1983
+ const change = changes[j - 1];
1984
+ if (change > 0) {
1985
+ gains += change;
1986
+ } else {
1987
+ losses += Math.abs(change);
1988
+ }
1989
+ }
1990
+ const avgGain = gains / period;
1991
+ const avgLoss = losses / period;
1992
+ if (avgLoss === 0) {
1993
+ result.push(100);
1994
+ } else {
1995
+ const rs = avgGain / avgLoss;
1996
+ const rsi = 100 - 100 / (1 + rs);
1997
+ result.push(rsi);
1998
+ }
1999
+ }
2000
+ }
2001
+ return result;
2002
+ }
2003
+ function calculateStandardDeviation(values, period) {
2004
+ const result = [];
2005
+ for (let i = 0; i < values.length; i++) {
2006
+ if (i < period - 1 || isNaN(values[i])) {
2007
+ result.push(NaN);
2008
+ } else {
2009
+ let sum = 0;
2010
+ let count = 0;
2011
+ for (let j = 0; j < period; j++) {
2012
+ const val = values[i - j];
2013
+ if (!isNaN(val)) {
2014
+ sum += val;
2015
+ count++;
2016
+ }
2017
+ }
2018
+ const mean = sum / count;
2019
+ let squaredDiffSum = 0;
2020
+ for (let j = 0; j < period; j++) {
2021
+ const val = values[i - j];
2022
+ if (!isNaN(val)) {
2023
+ squaredDiffSum += Math.pow(val - mean, 2);
2024
+ }
2025
+ }
2026
+ const variance = squaredDiffSum / count;
2027
+ result.push(Math.sqrt(variance));
2028
+ }
2029
+ }
2030
+ return result;
2031
+ }
2032
+ function calculateBollingerBands(bars, period, stdDev) {
2033
+ const middle = calculateSMA(bars, period);
2034
+ const closes = bars.map((b) => b.close);
2035
+ const std = calculateStandardDeviation(closes, period);
2036
+ const upper = [];
2037
+ const lower = [];
2038
+ for (let i = 0; i < middle.length; i++) {
2039
+ if (isNaN(middle[i]) || isNaN(std[i])) {
2040
+ upper.push(NaN);
2041
+ lower.push(NaN);
2042
+ } else {
2043
+ upper.push(middle[i] + std[i] * stdDev);
2044
+ lower.push(middle[i] - std[i] * stdDev);
2045
+ }
2046
+ }
2047
+ return { upper, middle, lower };
2048
+ }
2049
+
2050
+ // src/indicators/registry/sma.ts
2051
+ var SMAIndicator = {
2052
+ metadata: {
2053
+ id: "sma",
2054
+ name: "SMA",
2055
+ description: "Simple Moving Average - smooths price data by averaging over a period",
2056
+ category: "Trend" /* TREND */,
2057
+ version: "1.0.0"
2058
+ },
2059
+ settings: {
2060
+ period: {
2061
+ type: "number",
2062
+ label: "Period",
2063
+ defaultValue: 20,
2064
+ description: "Number of bars to average",
2065
+ min: 1,
2066
+ max: 500,
2067
+ step: 1
2068
+ },
2069
+ color: {
2070
+ type: "color",
2071
+ label: "Line Color",
2072
+ defaultValue: "#3b82f6",
2073
+ description: "Color of the SMA line"
2074
+ },
2075
+ lineWidth: {
2076
+ type: "number",
2077
+ label: "Line Width",
2078
+ defaultValue: 2,
2079
+ description: "Width of the SMA line",
2080
+ min: 1,
2081
+ max: 5,
2082
+ step: 1
2083
+ }
2084
+ },
2085
+ renderConfig: {
2086
+ seriesType: "line" /* LINE */,
2087
+ outputCount: 1,
2088
+ overlay: true
2089
+ },
2090
+ calculate: (bars, settings) => {
2091
+ const smaValues = calculateSMA(bars, settings.period);
2092
+ return bars.map((bar, i) => ({
2093
+ time: bar.timestamp / 1e3,
2094
+ value: smaValues[i]
2095
+ }));
2096
+ }
2097
+ };
2098
+
2099
+ // src/indicators/registry/ema.ts
2100
+ var EMAIndicator = {
2101
+ metadata: {
2102
+ id: "ema",
2103
+ name: "EMA",
2104
+ description: "Exponential Moving Average - gives more weight to recent prices",
2105
+ category: "Trend" /* TREND */,
2106
+ version: "1.0.0"
2107
+ },
2108
+ settings: {
2109
+ period: {
2110
+ type: "number",
2111
+ label: "Period",
2112
+ defaultValue: 12,
2113
+ description: "Number of bars for EMA calculation",
2114
+ min: 1,
2115
+ max: 500,
2116
+ step: 1
2117
+ },
2118
+ color: {
2119
+ type: "color",
2120
+ label: "Line Color",
2121
+ defaultValue: "#10b981",
2122
+ description: "Color of the EMA line"
2123
+ },
2124
+ lineWidth: {
2125
+ type: "number",
2126
+ label: "Line Width",
2127
+ defaultValue: 2,
2128
+ description: "Width of the EMA line",
2129
+ min: 1,
2130
+ max: 5,
2131
+ step: 1
2132
+ }
2133
+ },
2134
+ renderConfig: {
2135
+ seriesType: "line" /* LINE */,
2136
+ outputCount: 1,
2137
+ overlay: true
2138
+ },
2139
+ calculate: (bars, settings) => {
2140
+ const emaValues = calculateEMA(bars, settings.period);
2141
+ return bars.map((bar, i) => ({
2142
+ time: bar.timestamp / 1e3,
2143
+ value: emaValues[i]
2144
+ }));
2145
+ }
2146
+ };
2147
+
2148
+ // src/indicators/registry/rsi.ts
2149
+ var RSIIndicator = {
2150
+ metadata: {
2151
+ id: "rsi",
2152
+ name: "RSI",
2153
+ description: "Relative Strength Index - momentum oscillator measuring speed and magnitude of price changes",
2154
+ category: "Momentum" /* MOMENTUM */,
2155
+ version: "1.0.0"
2156
+ },
2157
+ settings: {
2158
+ period: {
2159
+ type: "number",
2160
+ label: "Period",
2161
+ defaultValue: 14,
2162
+ description: "Number of bars for RSI calculation",
2163
+ min: 2,
2164
+ max: 100,
2165
+ step: 1
2166
+ },
2167
+ color: {
2168
+ type: "color",
2169
+ label: "Line Color",
2170
+ defaultValue: "#8b5cf6",
2171
+ description: "Color of the RSI line"
2172
+ },
2173
+ lineWidth: {
2174
+ type: "number",
2175
+ label: "Line Width",
2176
+ defaultValue: 2,
2177
+ description: "Width of the RSI line",
2178
+ min: 1,
2179
+ max: 5,
2180
+ step: 1
2181
+ }
2182
+ },
2183
+ renderConfig: {
2184
+ seriesType: "line" /* LINE */,
2185
+ outputCount: 1,
2186
+ overlay: false
2187
+ },
2188
+ calculate: (bars, settings) => {
2189
+ const rsiValues = calculateRSI(bars, settings.period);
2190
+ return bars.map((bar, i) => ({
2191
+ time: bar.timestamp / 1e3,
2192
+ value: rsiValues[i]
2193
+ }));
2194
+ }
2195
+ };
2196
+
2197
+ // src/indicators/registry/bollinger.ts
2198
+ var BollingerBandsIndicator = {
2199
+ metadata: {
2200
+ id: "bollinger",
2201
+ name: "Bollinger Bands",
2202
+ description: "Bollinger Bands - volatility bands placed above and below a moving average",
2203
+ category: "Volatility" /* VOLATILITY */,
2204
+ version: "1.0.0"
2205
+ },
2206
+ settings: {
2207
+ period: {
2208
+ type: "number",
2209
+ label: "Period",
2210
+ defaultValue: 20,
2211
+ description: "Number of bars for moving average",
2212
+ min: 2,
2213
+ max: 200,
2214
+ step: 1
2215
+ },
2216
+ stdDev: {
2217
+ type: "number",
2218
+ label: "Standard Deviation",
2219
+ defaultValue: 2,
2220
+ description: "Number of standard deviations for bands",
2221
+ min: 0.5,
2222
+ max: 5,
2223
+ step: 0.1
2224
+ },
2225
+ upperColor: {
2226
+ type: "color",
2227
+ label: "Upper Band Color",
2228
+ defaultValue: "#ef4444",
2229
+ description: "Color of the upper band"
2230
+ },
2231
+ middleColor: {
2232
+ type: "color",
2233
+ label: "Middle Band Color",
2234
+ defaultValue: "#3b82f6",
2235
+ description: "Color of the middle band"
2236
+ },
2237
+ lowerColor: {
2238
+ type: "color",
2239
+ label: "Lower Band Color",
2240
+ defaultValue: "#10b981",
2241
+ description: "Color of the lower band"
2242
+ },
2243
+ lineWidth: {
2244
+ type: "number",
2245
+ label: "Line Width",
2246
+ defaultValue: 2,
2247
+ description: "Width of the band lines",
2248
+ min: 1,
2249
+ max: 5,
2250
+ step: 1
2251
+ },
2252
+ showFill: {
2253
+ type: "boolean",
2254
+ label: "Show Fill",
2255
+ defaultValue: true,
2256
+ description: "Show shaded area between bands"
2257
+ },
2258
+ fillColor: {
2259
+ type: "color",
2260
+ label: "Fill Color",
2261
+ defaultValue: "rgba(59, 130, 246, 0.1)",
2262
+ description: "Color of the shaded area between bands"
2263
+ }
2264
+ },
2265
+ renderConfig: {
2266
+ seriesType: "line" /* LINE */,
2267
+ outputCount: 3,
2268
+ overlay: true,
2269
+ hasBandFill: true,
2270
+ fillBands: {
2271
+ upper: "upper",
2272
+ lower: "lower"
2273
+ }
2274
+ },
2275
+ calculate: (bars, settings) => {
2276
+ const { upper, middle, lower } = calculateBollingerBands(
2277
+ bars,
2278
+ settings.period,
2279
+ settings.stdDev
2280
+ );
2281
+ return bars.map((bar, i) => ({
2282
+ time: bar.timestamp / 1e3,
2283
+ value: middle[i],
2284
+ upper: upper[i],
2285
+ lower: lower[i]
2286
+ }));
2287
+ }
2288
+ };
2289
+
2290
+ // src/indicators/registry/vwap.ts
2291
+ function calculateVWAPWithBands(bars, {
2292
+ reset = "none",
2293
+ bandMultiplier = 1,
2294
+ includeBands = true
2295
+ }) {
2296
+ if (!bars.length) return { vwap: [], upper: [], lower: [] };
2297
+ let cumulativeVolume = 0;
2298
+ let cumulativeTPV = 0;
2299
+ let sessionPrices = [];
2300
+ const vwap = [];
2301
+ const upper = [];
2302
+ const lower = [];
2303
+ const getWeek = (d) => {
2304
+ const day = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
2305
+ const dayNum = day.getUTCDay() || 7;
2306
+ day.setUTCDate(day.getUTCDate() + 4 - dayNum);
2307
+ const yearStart = new Date(Date.UTC(day.getUTCFullYear(), 0, 1));
2308
+ return Math.ceil(
2309
+ ((day.getTime() - yearStart.getTime()) / 864e5 + 1) / 7
2310
+ );
2311
+ };
2312
+ const std = (arr) => {
2313
+ const mean = arr.reduce((a, b) => a + b, 0) / arr.length;
2314
+ return Math.sqrt(arr.reduce((a, b) => a + (b - mean) ** 2, 0) / arr.length);
2315
+ };
2316
+ for (let i = 0; i < bars.length; i++) {
2317
+ const bar = bars[i];
2318
+ const typical = (bar.high + bar.low + bar.close) / 3;
2319
+ if (i > 0 && reset !== "none") {
2320
+ const prev = new Date(bars[i - 1].timestamp);
2321
+ const curr = new Date(bar.timestamp);
2322
+ let resetFlag = false;
2323
+ if (reset === "daily") {
2324
+ resetFlag = curr.getDate() !== prev.getDate() || curr.getMonth() !== prev.getMonth() || curr.getFullYear() !== prev.getFullYear();
2325
+ } else if (reset === "weekly") {
2326
+ resetFlag = getWeek(curr) !== getWeek(prev) || curr.getFullYear() !== prev.getFullYear();
2327
+ } else if (reset === "monthly") {
2328
+ resetFlag = curr.getMonth() !== prev.getMonth() || curr.getFullYear() !== prev.getFullYear();
2329
+ }
2330
+ if (resetFlag) {
2331
+ cumulativeVolume = 0;
2332
+ cumulativeTPV = 0;
2333
+ sessionPrices = [];
2334
+ }
2335
+ }
2336
+ cumulativeTPV += typical * bar.volume;
2337
+ cumulativeVolume += bar.volume;
2338
+ sessionPrices.push(typical);
2339
+ const vwapValue = cumulativeTPV / cumulativeVolume;
2340
+ vwap.push(vwapValue);
2341
+ if (includeBands && sessionPrices.length > 1) {
2342
+ const sigma = std(sessionPrices);
2343
+ upper.push(vwapValue + sigma * bandMultiplier);
2344
+ lower.push(vwapValue - sigma * bandMultiplier);
2345
+ } else {
2346
+ upper.push(NaN);
2347
+ lower.push(NaN);
2348
+ }
2349
+ }
2350
+ return { vwap, upper, lower };
2351
+ }
2352
+ var VWAPIndicator = {
2353
+ metadata: {
2354
+ id: "vwap",
2355
+ name: "VWAP",
2356
+ description: "Volume Weighted Average Price with standard deviation bands",
2357
+ category: "Volume" /* VOLUME */,
2358
+ version: "1.0.0"
2359
+ },
2360
+ settings: {
2361
+ reset: {
2362
+ type: "select",
2363
+ label: "Reset Period",
2364
+ defaultValue: "daily",
2365
+ description: "When to reset VWAP calculation",
2366
+ options: [
2367
+ { label: "None", value: "none" },
2368
+ { label: "Daily", value: "daily" },
2369
+ { label: "Weekly", value: "weekly" },
2370
+ { label: "Monthly", value: "monthly" }
2371
+ ]
2372
+ },
2373
+ bandMultiplier: {
2374
+ type: "number",
2375
+ label: "Band Multiplier",
2376
+ defaultValue: 1,
2377
+ description: "Standard deviation multiplier for bands",
2378
+ min: 0.5,
2379
+ max: 5,
2380
+ step: 0.1
2381
+ },
2382
+ showBands: {
2383
+ type: "boolean",
2384
+ label: "Show Bands",
2385
+ defaultValue: true,
2386
+ description: "Show standard deviation bands"
2387
+ },
2388
+ vwapColor: {
2389
+ type: "color",
2390
+ label: "VWAP Color",
2391
+ defaultValue: "#f59e0b",
2392
+ description: "Color of the VWAP line"
2393
+ },
2394
+ upperColor: {
2395
+ type: "color",
2396
+ label: "Upper Band Color",
2397
+ defaultValue: "#ef4444",
2398
+ description: "Color of the upper band"
2399
+ },
2400
+ lowerColor: {
2401
+ type: "color",
2402
+ label: "Lower Band Color",
2403
+ defaultValue: "#10b981",
2404
+ description: "Color of the lower band"
2405
+ },
2406
+ lineWidth: {
2407
+ type: "number",
2408
+ label: "Line Width",
2409
+ defaultValue: 2,
2410
+ description: "Width of the lines",
2411
+ min: 1,
2412
+ max: 5,
2413
+ step: 1
2414
+ },
2415
+ showFill: {
2416
+ type: "boolean",
2417
+ label: "Show Fill",
2418
+ defaultValue: true,
2419
+ description: "Show shaded area between bands"
2420
+ },
2421
+ fillColor: {
2422
+ type: "color",
2423
+ label: "Fill Color",
2424
+ defaultValue: "rgba(245, 158, 11, 0.1)",
2425
+ description: "Color of the shaded area between bands"
2426
+ }
2427
+ },
2428
+ renderConfig: {
2429
+ seriesType: "line" /* LINE */,
2430
+ outputCount: 3,
2431
+ overlay: true,
2432
+ hasBandFill: true,
2433
+ fillBands: {
2434
+ upper: "upper",
2435
+ lower: "lower"
2436
+ }
2437
+ },
2438
+ calculate: (bars, settings) => {
2439
+ const { vwap, upper, lower } = calculateVWAPWithBands(bars, {
2440
+ reset: settings.reset,
2441
+ bandMultiplier: settings.bandMultiplier,
2442
+ includeBands: settings.showBands
2443
+ });
2444
+ return bars.map((bar, i) => ({
2445
+ time: bar.timestamp / 1e3,
2446
+ value: vwap[i],
2447
+ upper: upper[i],
2448
+ lower: lower[i]
2449
+ }));
2450
+ }
2451
+ };
2452
+ var StochasticIndicator = {
2453
+ metadata: {
2454
+ id: "stochastic",
2455
+ name: "Stochastic Oscillator",
2456
+ description: "Compares closing price to price range over time - momentum indicator",
2457
+ category: "Momentum" /* MOMENTUM */,
2458
+ version: "1.0.0"
2459
+ },
2460
+ settings: {
2461
+ period: {
2462
+ type: "number",
2463
+ label: "Period (K)",
2464
+ defaultValue: 14,
2465
+ description: "Number of bars for %K calculation",
2466
+ min: 2,
2467
+ max: 100,
2468
+ step: 1
2469
+ },
2470
+ signalPeriod: {
2471
+ type: "number",
2472
+ label: "Signal Period (D)",
2473
+ defaultValue: 3,
2474
+ description: "Number of bars for %D signal line",
2475
+ min: 1,
2476
+ max: 50,
2477
+ step: 1
2478
+ },
2479
+ kColor: {
2480
+ type: "color",
2481
+ label: "%K Line Color",
2482
+ defaultValue: "#3b82f6",
2483
+ description: "Color of the %K line"
2484
+ },
2485
+ dColor: {
2486
+ type: "color",
2487
+ label: "%D Line Color",
2488
+ defaultValue: "#ef4444",
2489
+ description: "Color of the %D signal line"
2490
+ },
2491
+ lineWidth: {
2492
+ type: "number",
2493
+ label: "Line Width",
2494
+ defaultValue: 2,
2495
+ description: "Width of the indicator lines",
2496
+ min: 1,
2497
+ max: 5,
2498
+ step: 1
2499
+ }
2500
+ },
2501
+ renderConfig: {
2502
+ seriesType: "line" /* LINE */,
2503
+ outputCount: 2,
2504
+ overlay: false
2505
+ },
2506
+ calculate: (bars, settings) => {
2507
+ const high = bars.map((bar) => bar.high);
2508
+ const low = bars.map((bar) => bar.low);
2509
+ const close = bars.map((bar) => bar.close);
2510
+ const stochValues = stochastic({
2511
+ high,
2512
+ low,
2513
+ close,
2514
+ period: settings.period,
2515
+ signalPeriod: settings.signalPeriod
2516
+ });
2517
+ return bars.map((bar, i) => ({
2518
+ time: bar.timestamp / 1e3,
2519
+ value: stochValues[i]?.k ?? NaN,
2520
+ signal: stochValues[i]?.d ?? NaN
2521
+ }));
2522
+ }
2523
+ };
2524
+ var OBVIndicator = {
2525
+ metadata: {
2526
+ id: "obv",
2527
+ name: "OBV",
2528
+ description: "On-Balance Volume - cumulative volume indicator showing buying/selling pressure",
2529
+ category: "Volume" /* VOLUME */,
2530
+ version: "1.0.0"
2531
+ },
2532
+ settings: {
2533
+ color: {
2534
+ type: "color",
2535
+ label: "Line Color",
2536
+ defaultValue: "#10b981",
2537
+ description: "Color of the OBV line"
2538
+ },
2539
+ lineWidth: {
2540
+ type: "number",
2541
+ label: "Line Width",
2542
+ defaultValue: 2,
2543
+ description: "Width of the OBV line",
2544
+ min: 1,
2545
+ max: 5,
2546
+ step: 1
2547
+ }
2548
+ },
2549
+ renderConfig: {
2550
+ seriesType: "line" /* LINE */,
2551
+ outputCount: 1,
2552
+ overlay: false
2553
+ },
2554
+ calculate: (bars) => {
2555
+ const close = bars.map((bar) => bar.close);
2556
+ const volume = bars.map((bar) => bar.volume || 0);
2557
+ const obvValues = obv({ close, volume });
2558
+ return bars.map((bar, i) => ({
2559
+ time: bar.timestamp / 1e3,
2560
+ value: obvValues[i] ?? NaN
2561
+ }));
2562
+ }
2563
+ };
2564
+ var MFIIndicator = {
2565
+ metadata: {
2566
+ id: "mfi",
2567
+ name: "MFI",
2568
+ description: "Money Flow Index - volume-weighted momentum indicator (RSI with volume)",
2569
+ category: "Volume" /* VOLUME */,
2570
+ version: "1.0.0"
2571
+ },
2572
+ settings: {
2573
+ period: {
2574
+ type: "number",
2575
+ label: "Period",
2576
+ defaultValue: 14,
2577
+ description: "Number of bars for MFI calculation",
2578
+ min: 2,
2579
+ max: 100,
2580
+ step: 1
2581
+ },
2582
+ color: {
2583
+ type: "color",
2584
+ label: "Line Color",
2585
+ defaultValue: "#f59e0b",
2586
+ description: "Color of the MFI line"
2587
+ },
2588
+ lineWidth: {
2589
+ type: "number",
2590
+ label: "Line Width",
2591
+ defaultValue: 2,
2592
+ description: "Width of the MFI line",
2593
+ min: 1,
2594
+ max: 5,
2595
+ step: 1
2596
+ }
2597
+ },
2598
+ renderConfig: {
2599
+ seriesType: "line" /* LINE */,
2600
+ outputCount: 1,
2601
+ overlay: false
2602
+ },
2603
+ calculate: (bars, settings) => {
2604
+ const high = bars.map((bar) => bar.high);
2605
+ const low = bars.map((bar) => bar.low);
2606
+ const close = bars.map((bar) => bar.close);
2607
+ const volume = bars.map((bar) => bar.volume || 0);
2608
+ const mfiValues = mfi({
2609
+ high,
2610
+ low,
2611
+ close,
2612
+ volume,
2613
+ period: settings.period
2614
+ });
2615
+ return bars.map((bar, i) => ({
2616
+ time: bar.timestamp / 1e3,
2617
+ value: mfiValues[i] ?? NaN
2618
+ }));
2619
+ }
2620
+ };
2621
+ var ForceIndexIndicator = {
2622
+ metadata: {
2623
+ id: "forceindex",
2624
+ name: "Force Index",
2625
+ description: "Measures buying/selling pressure using price and volume",
2626
+ category: "Volume" /* VOLUME */,
2627
+ version: "1.0.0"
2628
+ },
2629
+ settings: {
2630
+ period: {
2631
+ type: "number",
2632
+ label: "Period",
2633
+ defaultValue: 13,
2634
+ description: "Smoothing period for force index",
2635
+ min: 1,
2636
+ max: 100,
2637
+ step: 1
2638
+ },
2639
+ color: {
2640
+ type: "color",
2641
+ label: "Line Color",
2642
+ defaultValue: "#8b5cf6",
2643
+ description: "Color of the Force Index line"
2644
+ },
2645
+ lineWidth: {
2646
+ type: "number",
2647
+ label: "Line Width",
2648
+ defaultValue: 2,
2649
+ description: "Width of the Force Index line",
2650
+ min: 1,
2651
+ max: 5,
2652
+ step: 1
2653
+ }
2654
+ },
2655
+ renderConfig: {
2656
+ seriesType: "line" /* LINE */,
2657
+ outputCount: 1,
2658
+ overlay: false
2659
+ },
2660
+ calculate: (bars, settings) => {
2661
+ const close = bars.map((bar) => bar.close);
2662
+ const volume = bars.map((bar) => bar.volume || 0);
2663
+ const forceValues = forceindex({
2664
+ close,
2665
+ volume,
2666
+ period: settings.period
2667
+ });
2668
+ return bars.map((bar, i) => ({
2669
+ time: bar.timestamp / 1e3,
2670
+ value: forceValues[i] ?? NaN
2671
+ }));
2672
+ }
2673
+ };
2674
+ var ATRIndicator = {
2675
+ metadata: {
2676
+ id: "atr",
2677
+ name: "ATR",
2678
+ description: "Average True Range - measures market volatility",
2679
+ category: "Volatility" /* VOLATILITY */,
2680
+ version: "1.0.0"
2681
+ },
2682
+ settings: {
2683
+ period: {
2684
+ type: "number",
2685
+ label: "Period",
2686
+ defaultValue: 14,
2687
+ description: "Number of bars for ATR calculation",
2688
+ min: 1,
2689
+ max: 100,
2690
+ step: 1
2691
+ },
2692
+ color: {
2693
+ type: "color",
2694
+ label: "Line Color",
2695
+ defaultValue: "#14b8a6",
2696
+ description: "Color of the ATR line"
2697
+ },
2698
+ lineWidth: {
2699
+ type: "number",
2700
+ label: "Line Width",
2701
+ defaultValue: 2,
2702
+ description: "Width of the ATR line",
2703
+ min: 1,
2704
+ max: 5,
2705
+ step: 1
2706
+ }
2707
+ },
2708
+ renderConfig: {
2709
+ seriesType: "line" /* LINE */,
2710
+ outputCount: 1,
2711
+ overlay: false
2712
+ },
2713
+ calculate: (bars, settings) => {
2714
+ const high = bars.map((bar) => bar.high);
2715
+ const low = bars.map((bar) => bar.low);
2716
+ const close = bars.map((bar) => bar.close);
2717
+ const atrValues = atr({
2718
+ high,
2719
+ low,
2720
+ close,
2721
+ period: settings.period
2722
+ });
2723
+ return bars.map((bar, i) => ({
2724
+ time: bar.timestamp / 1e3,
2725
+ value: atrValues[i] ?? NaN
2726
+ }));
2727
+ }
2728
+ };
2729
+ var ADXIndicator = {
2730
+ metadata: {
2731
+ id: "adx",
2732
+ name: "ADX",
2733
+ description: "Average Directional Index - measures trend strength (not direction)",
2734
+ category: "Trend" /* TREND */,
2735
+ version: "1.0.0"
2736
+ },
2737
+ settings: {
2738
+ period: {
2739
+ type: "number",
2740
+ label: "Period",
2741
+ defaultValue: 14,
2742
+ description: "Number of bars for ADX calculation",
2743
+ min: 2,
2744
+ max: 100,
2745
+ step: 1
2746
+ },
2747
+ color: {
2748
+ type: "color",
2749
+ label: "Line Color",
2750
+ defaultValue: "#06b6d4",
2751
+ description: "Color of the ADX line"
2752
+ },
2753
+ lineWidth: {
2754
+ type: "number",
2755
+ label: "Line Width",
2756
+ defaultValue: 2,
2757
+ description: "Width of the ADX line",
2758
+ min: 1,
2759
+ max: 5,
2760
+ step: 1
2761
+ }
2762
+ },
2763
+ renderConfig: {
2764
+ seriesType: "line" /* LINE */,
2765
+ outputCount: 1,
2766
+ overlay: false
2767
+ },
2768
+ calculate: (bars, settings) => {
2769
+ const high = bars.map((bar) => bar.high);
2770
+ const low = bars.map((bar) => bar.low);
2771
+ const close = bars.map((bar) => bar.close);
2772
+ const adxValues = adx({
2773
+ high,
2774
+ low,
2775
+ close,
2776
+ period: settings.period
2777
+ });
2778
+ return bars.map((bar, i) => ({
2779
+ time: bar.timestamp / 1e3,
2780
+ value: adxValues[i]?.adx ?? NaN
2781
+ }));
2782
+ }
2783
+ };
2784
+ var MACDIndicator = {
2785
+ metadata: {
2786
+ id: "macd",
2787
+ name: "MACD",
2788
+ description: "Moving Average Convergence Divergence - trend-following momentum indicator",
2789
+ category: "Momentum" /* MOMENTUM */,
2790
+ version: "1.0.0"
2791
+ },
2792
+ settings: {
2793
+ fastPeriod: {
2794
+ type: "number",
2795
+ label: "Fast Period",
2796
+ defaultValue: 12,
2797
+ description: "Period for fast EMA",
2798
+ min: 2,
2799
+ max: 100,
2800
+ step: 1
2801
+ },
2802
+ slowPeriod: {
2803
+ type: "number",
2804
+ label: "Slow Period",
2805
+ defaultValue: 26,
2806
+ description: "Period for slow EMA",
2807
+ min: 2,
2808
+ max: 100,
2809
+ step: 1
2810
+ },
2811
+ signalPeriod: {
2812
+ type: "number",
2813
+ label: "Signal Period",
2814
+ defaultValue: 9,
2815
+ description: "Period for signal line",
2816
+ min: 1,
2817
+ max: 50,
2818
+ step: 1
2819
+ },
2820
+ macdColor: {
2821
+ type: "color",
2822
+ label: "MACD Line Color",
2823
+ defaultValue: "#3b82f6",
2824
+ description: "Color of the MACD line"
2825
+ },
2826
+ signalColor: {
2827
+ type: "color",
2828
+ label: "Signal Line Color",
2829
+ defaultValue: "#ef4444",
2830
+ description: "Color of the signal line"
2831
+ },
2832
+ histogramColor: {
2833
+ type: "color",
2834
+ label: "Histogram Color",
2835
+ defaultValue: "#10b981",
2836
+ description: "Color of the histogram"
2837
+ },
2838
+ lineWidth: {
2839
+ type: "number",
2840
+ label: "Line Width",
2841
+ defaultValue: 2,
2842
+ description: "Width of the MACD and signal lines",
2843
+ min: 1,
2844
+ max: 5,
2845
+ step: 1
2846
+ }
2847
+ },
2848
+ renderConfig: {
2849
+ seriesType: "line" /* LINE */,
2850
+ outputCount: 3,
2851
+ overlay: false
2852
+ },
2853
+ calculate: (bars, settings) => {
2854
+ const values = bars.map((bar) => bar.close);
2855
+ const macdValues = macd({
2856
+ values,
2857
+ fastPeriod: settings.fastPeriod,
2858
+ slowPeriod: settings.slowPeriod,
2859
+ signalPeriod: settings.signalPeriod,
2860
+ SimpleMAOscillator: false,
2861
+ SimpleMASignal: false
2862
+ });
2863
+ return bars.map((bar, i) => ({
2864
+ time: bar.timestamp / 1e3,
2865
+ value: macdValues[i]?.MACD ?? NaN,
2866
+ signal: macdValues[i]?.signal ?? NaN,
2867
+ histogram: macdValues[i]?.histogram ?? NaN
2868
+ }));
2869
+ }
2870
+ };
2871
+ var PSARIndicator = {
2872
+ metadata: {
2873
+ id: "psar",
2874
+ name: "Parabolic SAR",
2875
+ description: "Parabolic Stop and Reverse - shows potential reversal points as dots",
2876
+ category: "Trend" /* TREND */,
2877
+ version: "1.0.0"
2878
+ },
2879
+ settings: {
2880
+ step: {
2881
+ type: "number",
2882
+ label: "Step (AF)",
2883
+ defaultValue: 0.02,
2884
+ description: "Acceleration Factor step",
2885
+ min: 1e-3,
2886
+ max: 1,
2887
+ step: 1e-3
2888
+ },
2889
+ max: {
2890
+ type: "number",
2891
+ label: "Max AF",
2892
+ defaultValue: 0.2,
2893
+ description: "Maximum Acceleration Factor",
2894
+ min: 0.01,
2895
+ max: 1,
2896
+ step: 0.01
2897
+ },
2898
+ color: {
2899
+ type: "color",
2900
+ label: "Color",
2901
+ defaultValue: "#f59e0b",
2902
+ description: "Color of the SAR dots"
2903
+ }
2904
+ },
2905
+ renderConfig: {
2906
+ seriesType: "line" /* LINE */,
2907
+ outputCount: 1,
2908
+ overlay: true
2909
+ },
2910
+ calculate: (bars, settings) => {
2911
+ const high = bars.map((bar) => bar.high);
2912
+ const low = bars.map((bar) => bar.low);
2913
+ const psarValues = psar({
2914
+ high,
2915
+ low,
2916
+ step: settings.step,
2917
+ max: settings.max
2918
+ });
2919
+ return bars.map((bar, i) => ({
2920
+ time: bar.timestamp / 1e3,
2921
+ value: psarValues[i] ?? NaN
2922
+ }));
2923
+ }
2924
+ };
2925
+ var CCIIndicator = {
2926
+ metadata: {
2927
+ id: "cci",
2928
+ name: "CCI",
2929
+ description: "Commodity Channel Index - identifies cyclical trends and overbought/oversold levels",
2930
+ category: "Oscillators" /* OSCILLATORS */,
2931
+ version: "1.0.0"
2932
+ },
2933
+ settings: {
2934
+ period: {
2935
+ type: "number",
2936
+ label: "Period",
2937
+ defaultValue: 20,
2938
+ description: "Number of bars for CCI calculation",
2939
+ min: 2,
2940
+ max: 100,
2941
+ step: 1
2942
+ },
2943
+ color: {
2944
+ type: "color",
2945
+ label: "Line Color",
2946
+ defaultValue: "#ec4899",
2947
+ description: "Color of the CCI line"
2948
+ },
2949
+ lineWidth: {
2950
+ type: "number",
2951
+ label: "Line Width",
2952
+ defaultValue: 2,
2953
+ description: "Width of the CCI line",
2954
+ min: 1,
2955
+ max: 5,
2956
+ step: 1
2957
+ }
2958
+ },
2959
+ renderConfig: {
2960
+ seriesType: "line" /* LINE */,
2961
+ outputCount: 1,
2962
+ overlay: false
2963
+ },
2964
+ calculate: (bars, settings) => {
2965
+ const high = bars.map((bar) => bar.high);
2966
+ const low = bars.map((bar) => bar.low);
2967
+ const close = bars.map((bar) => bar.close);
2968
+ const cciValues = cci({
2969
+ high,
2970
+ low,
2971
+ close,
2972
+ period: settings.period
2973
+ });
2974
+ return bars.map((bar, i) => ({
2975
+ time: bar.timestamp / 1e3,
2976
+ value: cciValues[i] ?? NaN
2977
+ }));
2978
+ }
2979
+ };
2980
+ var WilliamsRIndicator = {
2981
+ metadata: {
2982
+ id: "williamsr",
2983
+ name: "Williams %R",
2984
+ description: "Williams Percent Range - momentum indicator showing overbought/oversold levels",
2985
+ category: "Momentum" /* MOMENTUM */,
2986
+ version: "1.0.0"
2987
+ },
2988
+ settings: {
2989
+ period: {
2990
+ type: "number",
2991
+ label: "Period",
2992
+ defaultValue: 14,
2993
+ description: "Number of bars for Williams %R calculation",
2994
+ min: 2,
2995
+ max: 100,
2996
+ step: 1
2997
+ },
2998
+ color: {
2999
+ type: "color",
3000
+ label: "Line Color",
3001
+ defaultValue: "#a855f7",
3002
+ description: "Color of the Williams %R line"
3003
+ },
3004
+ lineWidth: {
3005
+ type: "number",
3006
+ label: "Line Width",
3007
+ defaultValue: 2,
3008
+ description: "Width of the Williams %R line",
3009
+ min: 1,
3010
+ max: 5,
3011
+ step: 1
3012
+ }
3013
+ },
3014
+ renderConfig: {
3015
+ seriesType: "line" /* LINE */,
3016
+ outputCount: 1,
3017
+ overlay: false
3018
+ },
3019
+ calculate: (bars, settings) => {
3020
+ const high = bars.map((bar) => bar.high);
3021
+ const low = bars.map((bar) => bar.low);
3022
+ const close = bars.map((bar) => bar.close);
3023
+ const williamsValues = williamsr({
3024
+ high,
3025
+ low,
3026
+ close,
3027
+ period: settings.period
3028
+ });
3029
+ return bars.map((bar, i) => ({
3030
+ time: bar.timestamp / 1e3,
3031
+ value: williamsValues[i] ?? NaN
3032
+ }));
3033
+ }
3034
+ };
3035
+ var KeltnerChannelsIndicator = {
3036
+ metadata: {
3037
+ id: "keltner",
3038
+ name: "Keltner Channels",
3039
+ description: "Volatility-based channels using ATR - similar to Bollinger Bands",
3040
+ category: "Volatility" /* VOLATILITY */,
3041
+ version: "1.0.0"
3042
+ },
3043
+ settings: {
3044
+ period: {
3045
+ type: "number",
3046
+ label: "Period",
3047
+ defaultValue: 20,
3048
+ description: "Period for middle line (EMA)",
3049
+ min: 2,
3050
+ max: 200,
3051
+ step: 1
3052
+ },
3053
+ atrPeriod: {
3054
+ type: "number",
3055
+ label: "ATR Period",
3056
+ defaultValue: 10,
3057
+ description: "Period for ATR calculation",
3058
+ min: 1,
3059
+ max: 100,
3060
+ step: 1
3061
+ },
3062
+ atrMultiplier: {
3063
+ type: "number",
3064
+ label: "ATR Multiplier",
3065
+ defaultValue: 2,
3066
+ description: "Multiplier for channel width",
3067
+ min: 0.1,
3068
+ max: 10,
3069
+ step: 0.1
3070
+ },
3071
+ upperColor: {
3072
+ type: "color",
3073
+ label: "Upper Band Color",
3074
+ defaultValue: "#ef4444",
3075
+ description: "Color of the upper band"
3076
+ },
3077
+ middleColor: {
3078
+ type: "color",
3079
+ label: "Middle Line Color",
3080
+ defaultValue: "#3b82f6",
3081
+ description: "Color of the middle line"
3082
+ },
3083
+ lowerColor: {
3084
+ type: "color",
3085
+ label: "Lower Band Color",
3086
+ defaultValue: "#10b981",
3087
+ description: "Color of the lower band"
3088
+ },
3089
+ lineWidth: {
3090
+ type: "number",
3091
+ label: "Line Width",
3092
+ defaultValue: 2,
3093
+ description: "Width of the channel lines",
3094
+ min: 1,
3095
+ max: 5,
3096
+ step: 1
3097
+ },
3098
+ showFill: {
3099
+ type: "boolean",
3100
+ label: "Show Fill",
3101
+ defaultValue: true,
3102
+ description: "Fill area between bands"
3103
+ },
3104
+ fillColor: {
3105
+ type: "color",
3106
+ label: "Fill Color",
3107
+ defaultValue: "rgba(59, 130, 246, 0.1)",
3108
+ description: "Color of the channel fill"
3109
+ }
3110
+ },
3111
+ renderConfig: {
3112
+ seriesType: "line" /* LINE */,
3113
+ outputCount: 3,
3114
+ overlay: true,
3115
+ hasBandFill: true
3116
+ },
3117
+ calculate: (bars, settings) => {
3118
+ const high = bars.map((bar) => bar.high);
3119
+ const low = bars.map((bar) => bar.low);
3120
+ const close = bars.map((bar) => bar.close);
3121
+ const keltnerValues = keltnerchannel({
3122
+ high,
3123
+ low,
3124
+ close,
3125
+ period: settings.period,
3126
+ atrPeriod: settings.atrPeriod,
3127
+ multiplier: settings.atrMultiplier
3128
+ });
3129
+ return bars.map((bar, i) => ({
3130
+ time: bar.timestamp / 1e3,
3131
+ value: keltnerValues[i]?.middle ?? NaN,
3132
+ upper: keltnerValues[i]?.upper ?? NaN,
3133
+ lower: keltnerValues[i]?.lower ?? NaN
3134
+ }));
3135
+ }
3136
+ };
3137
+ var SuperTrendIndicator = {
3138
+ metadata: {
3139
+ id: "supertrend",
3140
+ name: "SuperTrend",
3141
+ description: "Modern trend-following indicator with clear buy/sell signals",
3142
+ category: "Trend" /* TREND */,
3143
+ version: "1.0.0"
3144
+ },
3145
+ settings: {
3146
+ period: {
3147
+ type: "number",
3148
+ label: "Period",
3149
+ defaultValue: 10,
3150
+ description: "Period for ATR calculation",
3151
+ min: 1,
3152
+ max: 100,
3153
+ step: 1
3154
+ },
3155
+ multiplier: {
3156
+ type: "number",
3157
+ label: "Multiplier",
3158
+ defaultValue: 3,
3159
+ description: "ATR multiplier for bands",
3160
+ min: 0.1,
3161
+ max: 10,
3162
+ step: 0.1
3163
+ },
3164
+ color: {
3165
+ type: "color",
3166
+ label: "Line Color",
3167
+ defaultValue: "#06b6d4",
3168
+ description: "Color of the SuperTrend line"
3169
+ },
3170
+ lineWidth: {
3171
+ type: "number",
3172
+ label: "Line Width",
3173
+ defaultValue: 2,
3174
+ description: "Width of the SuperTrend line",
3175
+ min: 1,
3176
+ max: 5,
3177
+ step: 1
3178
+ }
3179
+ },
3180
+ renderConfig: {
3181
+ seriesType: "line" /* LINE */,
3182
+ outputCount: 1,
3183
+ overlay: true
3184
+ },
3185
+ calculate: (bars, settings) => {
3186
+ const high = bars.map((bar) => bar.high);
3187
+ const low = bars.map((bar) => bar.low);
3188
+ const close = bars.map((bar) => bar.close);
3189
+ const supertrendValues = supertrend({
3190
+ high,
3191
+ low,
3192
+ close,
3193
+ period: settings.period,
3194
+ multiplier: settings.multiplier
3195
+ });
3196
+ return bars.map((bar, i) => ({
3197
+ time: bar.timestamp / 1e3,
3198
+ value: supertrendValues[i]?.supertrend ?? NaN
3199
+ }));
3200
+ }
3201
+ };
3202
+ var IchimokuIndicator = {
3203
+ metadata: {
3204
+ id: "ichimoku",
3205
+ name: "Ichimoku Cloud",
3206
+ description: "Comprehensive trend system with conversion, base, and span lines forming a cloud",
3207
+ category: "Trend" /* TREND */,
3208
+ version: "1.0.0"
3209
+ },
3210
+ settings: {
3211
+ conversionPeriod: {
3212
+ type: "number",
3213
+ label: "Conversion Period (Tenkan)",
3214
+ defaultValue: 9,
3215
+ description: "Period for conversion line",
3216
+ min: 1,
3217
+ max: 100,
3218
+ step: 1
3219
+ },
3220
+ basePeriod: {
3221
+ type: "number",
3222
+ label: "Base Period (Kijun)",
3223
+ defaultValue: 26,
3224
+ description: "Period for base line",
3225
+ min: 1,
3226
+ max: 100,
3227
+ step: 1
3228
+ },
3229
+ spanPeriod: {
3230
+ type: "number",
3231
+ label: "Span Period (Senkou)",
3232
+ defaultValue: 52,
3233
+ description: "Period for span B",
3234
+ min: 1,
3235
+ max: 200,
3236
+ step: 1
3237
+ },
3238
+ displacement: {
3239
+ type: "number",
3240
+ label: "Displacement",
3241
+ defaultValue: 26,
3242
+ description: "Forward displacement for span lines",
3243
+ min: 0,
3244
+ max: 100,
3245
+ step: 1
3246
+ },
3247
+ conversionColor: {
3248
+ type: "color",
3249
+ label: "Conversion Line Color",
3250
+ defaultValue: "#3b82f6",
3251
+ description: "Color of the conversion line"
3252
+ },
3253
+ baseColor: {
3254
+ type: "color",
3255
+ label: "Base Line Color",
3256
+ defaultValue: "#ef4444",
3257
+ description: "Color of the base line"
3258
+ },
3259
+ spanAColor: {
3260
+ type: "color",
3261
+ label: "Span A Color",
3262
+ defaultValue: "#10b981",
3263
+ description: "Color of span A (cloud edge)"
3264
+ },
3265
+ spanBColor: {
3266
+ type: "color",
3267
+ label: "Span B Color",
3268
+ defaultValue: "#f59e0b",
3269
+ description: "Color of span B (cloud edge)"
3270
+ },
3271
+ lineWidth: {
3272
+ type: "number",
3273
+ label: "Line Width",
3274
+ defaultValue: 2,
3275
+ description: "Width of the indicator lines",
3276
+ min: 1,
3277
+ max: 5,
3278
+ step: 1
3279
+ },
3280
+ showCloud: {
3281
+ type: "boolean",
3282
+ label: "Show Cloud",
3283
+ defaultValue: true,
3284
+ description: "Fill area between span A and span B"
3285
+ },
3286
+ cloudColor: {
3287
+ type: "color",
3288
+ label: "Cloud Color",
3289
+ defaultValue: "rgba(59, 130, 246, 0.1)",
3290
+ description: "Color of the cloud fill"
3291
+ }
3292
+ },
3293
+ renderConfig: {
3294
+ seriesType: "line" /* LINE */,
3295
+ outputCount: 5,
3296
+ overlay: true,
3297
+ hasBandFill: true,
3298
+ fillBands: {
3299
+ upper: "spanA",
3300
+ lower: "spanB"
3301
+ }
3302
+ },
3303
+ calculate: (bars, settings) => {
3304
+ const high = bars.map((bar) => bar.high);
3305
+ const low = bars.map((bar) => bar.low);
3306
+ const ichimokuValues = ichimokucloud({
3307
+ high,
3308
+ low,
3309
+ conversionPeriod: settings.conversionPeriod,
3310
+ basePeriod: settings.basePeriod,
3311
+ spanPeriod: settings.spanPeriod,
3312
+ displacement: 0
3313
+ });
3314
+ const conversion = bars.map((_, i) => ichimokuValues[i]?.conversion ?? NaN);
3315
+ const base = bars.map((_, i) => ichimokuValues[i]?.base ?? NaN);
3316
+ const spanA = bars.map((_, i) => ichimokuValues[i]?.spanA ?? NaN);
3317
+ const spanB = bars.map((_, i) => ichimokuValues[i]?.spanB ?? NaN);
3318
+ const displacedSpanA = displaceArray(spanA, settings.displacement);
3319
+ const displacedSpanB = displaceArray(spanB, settings.displacement);
3320
+ return bars.map((bar, i) => ({
3321
+ time: bar.timestamp / 1e3,
3322
+ value: conversion[i],
3323
+ base: base[i],
3324
+ spanA: displacedSpanA[i],
3325
+ spanB: displacedSpanB[i]
3326
+ }));
3327
+ }
3328
+ };
3329
+ var DonchianChannelsIndicator = {
3330
+ metadata: {
3331
+ id: "donchian",
3332
+ name: "Donchian Channels",
3333
+ description: "Highest high and lowest low over a period - classic breakout indicator",
3334
+ category: "Volatility" /* VOLATILITY */,
3335
+ version: "1.0.0"
3336
+ },
3337
+ settings: {
3338
+ period: {
3339
+ type: "number",
3340
+ label: "Period",
3341
+ defaultValue: 20,
3342
+ description: "Number of bars to look back",
3343
+ min: 2,
3344
+ max: 200,
3345
+ step: 1
3346
+ },
3347
+ upperColor: {
3348
+ type: "color",
3349
+ label: "Upper Band Color",
3350
+ defaultValue: "#ef4444",
3351
+ description: "Color of the upper band"
3352
+ },
3353
+ middleColor: {
3354
+ type: "color",
3355
+ label: "Middle Line Color",
3356
+ defaultValue: "#3b82f6",
3357
+ description: "Color of the middle line"
3358
+ },
3359
+ lowerColor: {
3360
+ type: "color",
3361
+ label: "Lower Band Color",
3362
+ defaultValue: "#10b981",
3363
+ description: "Color of the lower band"
3364
+ },
3365
+ lineWidth: {
3366
+ type: "number",
3367
+ label: "Line Width",
3368
+ defaultValue: 2,
3369
+ description: "Width of the channel lines",
3370
+ min: 1,
3371
+ max: 5,
3372
+ step: 1
3373
+ },
3374
+ showFill: {
3375
+ type: "boolean",
3376
+ label: "Show Fill",
3377
+ defaultValue: true,
3378
+ description: "Fill area between bands"
3379
+ },
3380
+ fillColor: {
3381
+ type: "color",
3382
+ label: "Fill Color",
3383
+ defaultValue: "rgba(59, 130, 246, 0.1)",
3384
+ description: "Color of the channel fill"
3385
+ }
3386
+ },
3387
+ renderConfig: {
3388
+ seriesType: "line" /* LINE */,
3389
+ outputCount: 3,
3390
+ overlay: true,
3391
+ hasBandFill: true,
3392
+ fillBands: {
3393
+ upper: "upper",
3394
+ lower: "lower"
3395
+ }
3396
+ },
3397
+ calculate: (bars, settings) => {
3398
+ const high = bars.map((bar) => bar.high);
3399
+ const low = bars.map((bar) => bar.low);
3400
+ const donchianValues = donchianchannels({
3401
+ high,
3402
+ low,
3403
+ period: settings.period
3404
+ });
3405
+ return bars.map((bar, i) => ({
3406
+ time: bar.timestamp / 1e3,
3407
+ value: donchianValues[i]?.middle ?? NaN,
3408
+ upper: donchianValues[i]?.upper ?? NaN,
3409
+ lower: donchianValues[i]?.lower ?? NaN
3410
+ }));
3411
+ }
3412
+ };
3413
+ var ROCIndicator = {
3414
+ metadata: {
3415
+ id: "roc",
3416
+ name: "ROC",
3417
+ description: "Rate of Change - measures percentage change in price over a period",
3418
+ category: "Momentum" /* MOMENTUM */,
3419
+ version: "1.0.0"
3420
+ },
3421
+ settings: {
3422
+ period: {
3423
+ type: "number",
3424
+ label: "Period",
3425
+ defaultValue: 12,
3426
+ description: "Number of bars for ROC calculation",
3427
+ min: 1,
3428
+ max: 100,
3429
+ step: 1
3430
+ },
3431
+ color: {
3432
+ type: "color",
3433
+ label: "Line Color",
3434
+ defaultValue: "#06b6d4",
3435
+ description: "Color of the ROC line"
3436
+ },
3437
+ lineWidth: {
3438
+ type: "number",
3439
+ label: "Line Width",
3440
+ defaultValue: 2,
3441
+ description: "Width of the ROC line",
3442
+ min: 1,
3443
+ max: 5,
3444
+ step: 1
3445
+ }
3446
+ },
3447
+ renderConfig: {
3448
+ seriesType: "line" /* LINE */,
3449
+ outputCount: 1,
3450
+ overlay: false
3451
+ },
3452
+ calculate: (bars, settings) => {
3453
+ const values = bars.map((bar) => bar.close);
3454
+ const rocValues = roc({
3455
+ values,
3456
+ period: settings.period
3457
+ });
3458
+ return bars.map((bar, i) => ({
3459
+ time: bar.timestamp / 1e3,
3460
+ value: rocValues[i] ?? NaN
3461
+ }));
3462
+ }
3463
+ };
3464
+ var StochRSIIndicator = {
3465
+ metadata: {
3466
+ id: "stochrsi",
3467
+ name: "Stochastic RSI",
3468
+ description: "Stochastic RSI - applies Stochastic oscillator to RSI values",
3469
+ category: "Momentum" /* MOMENTUM */,
3470
+ version: "1.0.0"
3471
+ },
3472
+ settings: {
3473
+ rsiPeriod: {
3474
+ type: "number",
3475
+ label: "RSI Period",
3476
+ defaultValue: 14,
3477
+ description: "Period for RSI calculation",
3478
+ min: 2,
3479
+ max: 100,
3480
+ step: 1
3481
+ },
3482
+ stochPeriod: {
3483
+ type: "number",
3484
+ label: "Stochastic Period",
3485
+ defaultValue: 14,
3486
+ description: "Period for Stochastic calculation",
3487
+ min: 2,
3488
+ max: 100,
3489
+ step: 1
3490
+ },
3491
+ kPeriod: {
3492
+ type: "number",
3493
+ label: "%K Period",
3494
+ defaultValue: 3,
3495
+ description: "Smoothing period for %K",
3496
+ min: 1,
3497
+ max: 50,
3498
+ step: 1
3499
+ },
3500
+ dPeriod: {
3501
+ type: "number",
3502
+ label: "%D Period",
3503
+ defaultValue: 3,
3504
+ description: "Smoothing period for %D signal line",
3505
+ min: 1,
3506
+ max: 50,
3507
+ step: 1
3508
+ },
3509
+ kColor: {
3510
+ type: "color",
3511
+ label: "%K Line Color",
3512
+ defaultValue: "#3b82f6",
3513
+ description: "Color of the %K line"
3514
+ },
3515
+ dColor: {
3516
+ type: "color",
3517
+ label: "%D Line Color",
3518
+ defaultValue: "#ef4444",
3519
+ description: "Color of the %D signal line"
3520
+ },
3521
+ lineWidth: {
3522
+ type: "number",
3523
+ label: "Line Width",
3524
+ defaultValue: 2,
3525
+ description: "Width of the indicator lines",
3526
+ min: 1,
3527
+ max: 5,
3528
+ step: 1
3529
+ }
3530
+ },
3531
+ renderConfig: {
3532
+ seriesType: "line" /* LINE */,
3533
+ outputCount: 2,
3534
+ overlay: false
3535
+ },
3536
+ calculate: (bars, settings) => {
3537
+ const values = bars.map((bar) => bar.close);
3538
+ const stochRSIValues = stochasticrsi({
3539
+ values,
3540
+ rsiPeriod: settings.rsiPeriod,
3541
+ stochasticPeriod: settings.stochPeriod,
3542
+ kPeriod: settings.kPeriod,
3543
+ dPeriod: settings.dPeriod
3544
+ });
3545
+ return bars.map((bar, i) => ({
3546
+ time: bar.timestamp / 1e3,
3547
+ value: stochRSIValues[i]?.k ?? NaN,
3548
+ signal: stochRSIValues[i]?.d ?? NaN
3549
+ }));
3550
+ }
3551
+ };
3552
+ var WMAIndicator = {
3553
+ metadata: {
3554
+ id: "wma",
3555
+ name: "WMA",
3556
+ description: "Weighted Moving Average - gives more weight to recent prices",
3557
+ category: "Trend" /* TREND */,
3558
+ version: "1.0.0"
3559
+ },
3560
+ settings: {
3561
+ period: {
3562
+ type: "number",
3563
+ label: "Period",
3564
+ defaultValue: 20,
3565
+ description: "Number of bars to average",
3566
+ min: 1,
3567
+ max: 500,
3568
+ step: 1
3569
+ },
3570
+ color: {
3571
+ type: "color",
3572
+ label: "Line Color",
3573
+ defaultValue: "#f59e0b",
3574
+ description: "Color of the WMA line"
3575
+ },
3576
+ lineWidth: {
3577
+ type: "number",
3578
+ label: "Line Width",
3579
+ defaultValue: 2,
3580
+ description: "Width of the WMA line",
3581
+ min: 1,
3582
+ max: 5,
3583
+ step: 1
3584
+ }
3585
+ },
3586
+ renderConfig: {
3587
+ seriesType: "line" /* LINE */,
3588
+ outputCount: 1,
3589
+ overlay: true
3590
+ },
3591
+ calculate: (bars, settings) => {
3592
+ const values = bars.map((bar) => bar.close);
3593
+ const wmaValues = wma({
3594
+ values,
3595
+ period: settings.period
3596
+ });
3597
+ return bars.map((bar, i) => ({
3598
+ time: bar.timestamp / 1e3,
3599
+ value: wmaValues[i] ?? NaN
3600
+ }));
3601
+ }
3602
+ };
3603
+
3604
+ // src/indicators/registry/index.ts
3605
+ function registerBuiltInIndicators() {
3606
+ indicatorRegistry.register(SMAIndicator);
3607
+ indicatorRegistry.register(EMAIndicator);
3608
+ indicatorRegistry.register(RSIIndicator);
3609
+ indicatorRegistry.register(BollingerBandsIndicator);
3610
+ indicatorRegistry.register(VWAPIndicator);
3611
+ indicatorRegistry.register(StochasticIndicator);
3612
+ indicatorRegistry.register(OBVIndicator);
3613
+ indicatorRegistry.register(MFIIndicator);
3614
+ indicatorRegistry.register(ForceIndexIndicator);
3615
+ indicatorRegistry.register(ATRIndicator);
3616
+ indicatorRegistry.register(ADXIndicator);
3617
+ indicatorRegistry.register(MACDIndicator);
3618
+ indicatorRegistry.register(PSARIndicator);
3619
+ indicatorRegistry.register(CCIIndicator);
3620
+ indicatorRegistry.register(WilliamsRIndicator);
3621
+ indicatorRegistry.register(KeltnerChannelsIndicator);
3622
+ indicatorRegistry.register(SuperTrendIndicator);
3623
+ indicatorRegistry.register(IchimokuIndicator);
3624
+ indicatorRegistry.register(DonchianChannelsIndicator);
3625
+ indicatorRegistry.register(ROCIndicator);
3626
+ indicatorRegistry.register(StochRSIIndicator);
3627
+ indicatorRegistry.register(WMAIndicator);
3628
+ }
3629
+
3630
+ // src/adapters/alpaca.ts
3631
+ var AlpacaBarAdapter = class {
3632
+ constructor(options) {
3633
+ __publicField(this, "apiKey");
3634
+ __publicField(this, "secretKey");
3635
+ __publicField(this, "baseUrl");
3636
+ __publicField(this, "wsUrl");
3637
+ __publicField(this, "ws", null);
3638
+ __publicField(this, "reconnectAttempts", 0);
3639
+ __publicField(this, "maxReconnectAttempts", 5);
3640
+ __publicField(this, "reconnectDelay", 1e3);
3641
+ __publicField(this, "currentHandlers", null);
3642
+ __publicField(this, "currentSymbol", null);
3643
+ __publicField(this, "authenticated", false);
3644
+ if (!options.apiKey || !options.secretKey) {
3645
+ throw new Error("Alpaca API key and secret key are required");
3646
+ }
3647
+ this.apiKey = options.apiKey;
3648
+ this.secretKey = options.secretKey;
3649
+ this.baseUrl = options.baseUrl || "https://data.alpaca.markets";
3650
+ this.wsUrl = options.wsUrl || "wss://stream.data.alpaca.markets/v2/iex";
3651
+ }
3652
+ async fetchHistoricalBars(params) {
3653
+ const { symbol, timeframe, before, after, limit = 500 } = params;
3654
+ const url = new URL(`${this.baseUrl}/v2/stocks/${symbol}/bars`);
3655
+ url.searchParams.set("timeframe", timeframe);
3656
+ url.searchParams.set("limit", limit.toString());
3657
+ if (before) {
3658
+ const beforeDate = new Date(before).toISOString();
3659
+ url.searchParams.set("end", beforeDate);
3660
+ }
3661
+ if (after) {
3662
+ const afterDate = new Date(after).toISOString();
3663
+ url.searchParams.set("start", afterDate);
3664
+ }
3665
+ url.searchParams.set("feed", "iex");
3666
+ const headers = {
3667
+ "APCA-API-KEY-ID": this.apiKey,
3668
+ "APCA-API-SECRET-KEY": this.secretKey
3669
+ };
3670
+ try {
3671
+ const response = await fetch(url.toString(), { headers });
3672
+ if (!response.ok) {
3673
+ const errorText = await response.text();
3674
+ throw new Error(`Alpaca API error: ${response.status} ${errorText}`);
3675
+ }
3676
+ const data = await response.json();
3677
+ const bars = data.bars[symbol] || [];
3678
+ return this.convertAlpacaBars(bars);
3679
+ } catch (error) {
3680
+ console.error("Failed to fetch Alpaca bars:", error);
3681
+ throw error;
3682
+ }
3683
+ }
3684
+ subscribeRealtime(symbol, handlers) {
3685
+ this.currentSymbol = symbol;
3686
+ this.currentHandlers = handlers;
3687
+ this.connectWebSocket();
3688
+ return {
3689
+ unsubscribe: () => this.disconnect()
3690
+ };
3691
+ }
3692
+ unsubscribeAll() {
3693
+ this.disconnect();
3694
+ }
3695
+ connectWebSocket() {
3696
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
3697
+ console.log("WebSocket already connected");
3698
+ return;
3699
+ }
3700
+ try {
3701
+ this.ws = new WebSocket(this.wsUrl);
3702
+ this.ws.onopen = () => {
3703
+ console.log("Alpaca WebSocket connected");
3704
+ this.authenticated = false;
3705
+ this.authenticate();
3706
+ };
3707
+ this.ws.onmessage = (event) => {
3708
+ this.handleMessage(event.data);
3709
+ };
3710
+ this.ws.onerror = (event) => {
3711
+ console.error("Alpaca WebSocket error:", event);
3712
+ this.currentHandlers?.onError?.(new Error("WebSocket error"));
3713
+ };
3714
+ this.ws.onclose = () => {
3715
+ console.log("Alpaca WebSocket closed");
3716
+ this.authenticated = false;
3717
+ this.currentHandlers?.onDisconnect?.();
3718
+ this.attemptReconnect();
3719
+ };
3720
+ } catch (error) {
3721
+ console.error("Failed to create WebSocket:", error);
3722
+ this.currentHandlers?.onError?.(
3723
+ error instanceof Error ? error : new Error("Failed to create WebSocket")
3724
+ );
3725
+ }
3726
+ }
3727
+ authenticate() {
3728
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
3729
+ const authMessage = {
3730
+ action: "auth",
3731
+ key: this.apiKey,
3732
+ secret: this.secretKey
3733
+ };
3734
+ this.ws.send(JSON.stringify(authMessage));
3735
+ }
3736
+ subscribe() {
3737
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.currentSymbol) return;
3738
+ const subscribeMessage = {
3739
+ action: "subscribe",
3740
+ trades: [this.currentSymbol],
3741
+ bars: [this.currentSymbol]
3742
+ };
3743
+ this.ws.send(JSON.stringify(subscribeMessage));
3744
+ console.log(`Subscribed to ${this.currentSymbol}`);
3745
+ }
3746
+ handleMessage(data) {
3747
+ try {
3748
+ const messages = JSON.parse(data);
3749
+ if (!Array.isArray(messages)) {
3750
+ console.warn("Unexpected message format:", messages);
3751
+ return;
3752
+ }
3753
+ for (const message of messages) {
3754
+ if (message.T === "success" && message.msg === "authenticated") {
3755
+ console.log("Alpaca WebSocket authenticated");
3756
+ this.authenticated = true;
3757
+ this.reconnectAttempts = 0;
3758
+ this.currentHandlers?.onConnect?.();
3759
+ this.subscribe();
3760
+ } else if (message.T === "error") {
3761
+ console.error("Alpaca error:", message.msg);
3762
+ this.currentHandlers?.onError?.(new Error(message.msg));
3763
+ } else if (message.T === "t") {
3764
+ this.handleTrade(message);
3765
+ } else if (message.T === "b") {
3766
+ this.handleBar(message);
3767
+ } else if (message.T === "subscription") {
3768
+ console.log("Subscription confirmed:", message);
3769
+ }
3770
+ }
3771
+ } catch (error) {
3772
+ console.error("Failed to parse WebSocket message:", error);
3773
+ }
3774
+ }
3775
+ handleTrade(trade) {
3776
+ if (trade.S !== this.currentSymbol) return;
3777
+ this.currentHandlers?.onTrade?.(trade.p, trade.s);
3778
+ }
3779
+ handleBar(bar) {
3780
+ if (bar.S !== this.currentSymbol) return;
3781
+ const ohlcvBar = {
3782
+ timestamp: new Date(bar.t).getTime(),
3783
+ open: bar.o,
3784
+ high: bar.h,
3785
+ low: bar.l,
3786
+ close: bar.c,
3787
+ volume: bar.v,
3788
+ trade_count: bar.n,
3789
+ vwap: bar.vw
3790
+ };
3791
+ this.currentHandlers?.onBar?.(ohlcvBar);
3792
+ }
3793
+ attemptReconnect() {
3794
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
3795
+ console.error("Max reconnection attempts reached");
3796
+ this.currentHandlers?.onError?.(new Error("Max reconnection attempts reached"));
3797
+ return;
3798
+ }
3799
+ this.reconnectAttempts++;
3800
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
3801
+ console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
3802
+ setTimeout(() => {
3803
+ if (this.currentSymbol && this.currentHandlers) {
3804
+ this.connectWebSocket();
3805
+ }
3806
+ }, delay);
3807
+ }
3808
+ disconnect() {
3809
+ if (this.ws) {
3810
+ this.ws.close();
3811
+ this.ws = null;
3812
+ }
3813
+ this.authenticated = false;
3814
+ this.currentSymbol = null;
3815
+ this.currentHandlers = null;
3816
+ this.reconnectAttempts = 0;
3817
+ }
3818
+ convertAlpacaBars(bars) {
3819
+ return bars.map((bar) => ({
3820
+ timestamp: new Date(bar.t).getTime(),
3821
+ open: bar.o,
3822
+ high: bar.h,
3823
+ low: bar.l,
3824
+ close: bar.c,
3825
+ volume: bar.v,
3826
+ trade_count: bar.n,
3827
+ vwap: bar.vw
3828
+ }));
3829
+ }
3830
+ };
3831
+
3832
+ // src/adapters/mock.ts
3833
+ var MockAdapter = class {
3834
+ constructor() {
3835
+ __publicField(this, "intervalId", null);
3836
+ }
3837
+ async fetchHistoricalBars(params) {
3838
+ const { limit = 100, before } = params;
3839
+ await new Promise((resolve) => setTimeout(resolve, 800));
3840
+ const bars = [];
3841
+ const now = before || Date.now();
3842
+ let price = 100;
3843
+ for (let i = limit - 1; i >= 0; i--) {
3844
+ const timestamp = now - i * 3e5;
3845
+ const change = (Math.random() - 0.5) * 2;
3846
+ price = Math.max(price + change, 1);
3847
+ const open = price;
3848
+ const close = price + (Math.random() - 0.5) * 1.5;
3849
+ const high = Math.max(open, close) + Math.random() * 0.5;
3850
+ const low = Math.min(open, close) - Math.random() * 0.5;
3851
+ bars.push({
3852
+ timestamp,
3853
+ open: Math.max(open, 0.01),
3854
+ high: Math.max(high, 0.01),
3855
+ low: Math.max(low, 0.01),
3856
+ close: Math.max(close, 0.01),
3857
+ volume: Math.floor(Math.random() * 1e4) + 1e3
3858
+ });
3859
+ price = Math.max(close, 1);
3860
+ }
3861
+ console.log(`MockAdapter: Generated ${bars.length} bars (before: ${before})`);
3862
+ console.log("First bar:", bars[0]);
3863
+ console.log("Last bar:", bars[bars.length - 1]);
3864
+ return bars;
3865
+ }
3866
+ subscribeRealtime(symbol, handlers) {
3867
+ handlers.onConnect?.();
3868
+ let lastPrice = 100;
3869
+ this.intervalId = window.setInterval(() => {
3870
+ const now = Date.now();
3871
+ const timestamp = Math.floor(now / 3e5) * 3e5;
3872
+ const change = (Math.random() - 0.5) * 2;
3873
+ lastPrice = Math.max(lastPrice + change, 1);
3874
+ const open = lastPrice;
3875
+ const close = lastPrice + (Math.random() - 0.5) * 1.5;
3876
+ const high = Math.max(open, close) + Math.random() * 0.5;
3877
+ const low = Math.min(open, close) - Math.random() * 0.5;
3878
+ const bar = {
3879
+ timestamp,
3880
+ open,
3881
+ high,
3882
+ low,
3883
+ close,
3884
+ volume: Math.floor(Math.random() * 1e4) + 1e3
3885
+ };
3886
+ handlers.onBar?.(bar);
3887
+ lastPrice = close;
3888
+ }, 5e3);
3889
+ return {
3890
+ unsubscribe: () => this.unsubscribeAll()
3891
+ };
3892
+ }
3893
+ unsubscribeAll() {
3894
+ if (this.intervalId !== null) {
3895
+ clearInterval(this.intervalId);
3896
+ this.intervalId = null;
3897
+ }
3898
+ }
3899
+ };
3900
+
3901
+ export { ADXIndicator, ATRIndicator, AlpacaBarAdapter, BollingerBandsIndicator, CCIIndicator, ChartComponent, ChartSeriesType, DonchianChannelsIndicator, EMAIndicator, ForceIndexIndicator, IchimokuIndicator, IndicatorBrowser, IndicatorCategory, IndicatorInstanceSchema, IndicatorMetadataSchema, IndicatorSettingsForm, IndicatorSettingsSchema, KeltnerChannelsIndicator, LineStyleSchema, LocalStoragePersistenceAdapter, MACDIndicator, MFIIndicator, MockAdapter, NoOpPersistenceAdapter, OBVIndicator, PSARIndicator, ROCIndicator, RSIIndicator, RenderConfigSchema, SMAIndicator, SettingFieldSchema, SettingFieldTypeSchema, SettingsDialog, StochRSIIndicator, StochasticIndicator, SuperTrendIndicator, VWAPIndicator, WMAIndicator, WilliamsRIndicator, appendBar, calculateBollingerBands, calculateEMA, calculateRSI, calculateSMA, calculateStandardDeviation, createPersistenceAdapter, deduplicateBars, displaceArray, getNewestBar, getOldestBar, indicatorCalculator, indicatorRegistry, isValidBar, mergeBars, normalizeTimestamp, padIndicatorArray, prependBars, registerBuiltInIndicators, sortBars, updateBarInArray, updateCurrentBar, useBarsData, useChartAPI, useRealtimeUpdates, validateAndNormalizeBars, validateBar };
3902
+ //# sourceMappingURL=index.js.map
3903
+ //# sourceMappingURL=index.js.map