@adrienhobbs/candlekit 0.1.0 → 0.2.1
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.d.ts +26 -3
- package/dist/index.js +170 -86
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
|
|
4
5
|
interface OHLCVBar {
|
|
@@ -18,7 +19,15 @@ interface ChartLine {
|
|
|
18
19
|
lineWidth?: number;
|
|
19
20
|
lineStyle?: 'solid' | 'dashed' | 'dotted';
|
|
20
21
|
title?: string;
|
|
21
|
-
type?: 'entry' | 'stopLoss' | 'takeProfit';
|
|
22
|
+
type?: 'entry' | 'stopLoss' | 'takeProfit' | 'mfe' | 'mae';
|
|
23
|
+
}
|
|
24
|
+
interface ChartTrade {
|
|
25
|
+
id: string;
|
|
26
|
+
entryTime: number;
|
|
27
|
+
exitTime: number;
|
|
28
|
+
entryPrice: number;
|
|
29
|
+
exitPrice: number;
|
|
30
|
+
outcome: 'win' | 'loss';
|
|
22
31
|
}
|
|
23
32
|
interface IndicatorSettings$1 {
|
|
24
33
|
[key: string]: any;
|
|
@@ -154,8 +163,22 @@ interface ChartComponentProps {
|
|
|
154
163
|
onClearAllLines?: () => void;
|
|
155
164
|
enableBarSelection?: boolean;
|
|
156
165
|
onBarClick?: (bar: OHLCVBar | null) => void;
|
|
166
|
+
trades?: ChartTrade[];
|
|
167
|
+
selectedTradeId?: string | null;
|
|
168
|
+
renderTradePopup?: (trade: ChartTrade) => ReactNode;
|
|
169
|
+
/**
|
|
170
|
+
* Fixed chart height in px. When omitted, the chart fills its container's
|
|
171
|
+
* height (observed via ResizeObserver) instead of a fixed default — so a
|
|
172
|
+
* flex/percent-sized parent gets a full-height chart.
|
|
173
|
+
*/
|
|
174
|
+
height?: number;
|
|
175
|
+
/**
|
|
176
|
+
* When set, recenter the time scale on this trade (by id) so it scrolls into
|
|
177
|
+
* view. Pairs with `selectedTradeId` to drive both selection and focus.
|
|
178
|
+
*/
|
|
179
|
+
focusTradeId?: string | null;
|
|
157
180
|
}
|
|
158
|
-
declare function ChartComponent({ bars, onLoadMoreData, indicators, lines, onBarUpdate, onNewBar, onDeleteLine, onAddLine, onClearAllLines, enableBarSelection, onBarClick, }: ChartComponentProps): react_jsx_runtime.JSX.Element;
|
|
181
|
+
declare function ChartComponent({ bars, onLoadMoreData, indicators, lines, onBarUpdate, onNewBar, onDeleteLine, onAddLine, onClearAllLines, enableBarSelection, onBarClick, trades, selectedTradeId, renderTradePopup, height, focusTradeId, }: ChartComponentProps): react_jsx_runtime.JSX.Element;
|
|
159
182
|
|
|
160
183
|
interface IndicatorBrowserProps {
|
|
161
184
|
isOpen: boolean;
|
|
@@ -422,4 +445,4 @@ declare function getOldestBar(bars: OHLCVBar[]): OHLCVBar | null;
|
|
|
422
445
|
declare function getNewestBar(bars: OHLCVBar[]): OHLCVBar | null;
|
|
423
446
|
declare function updateCurrentBar(bars: OHLCVBar[], tradePrice: number, tradeVolume: number): OHLCVBar[];
|
|
424
447
|
|
|
425
|
-
export { ADXIndicator, ATRIndicator, AlpacaBarAdapter, type BarDataAdapter, type BarDataAdapterOptions, BollingerBandsIndicator, CCIIndicator, ChartComponent, type ChartLine, ChartSeriesType, DonchianChannelsIndicator, EMAIndicator, ForceIndexIndicator, type HistoricalDataParams, IchimokuIndicator, IndicatorBrowser, type IndicatorCalculation, IndicatorCategory, type IndicatorDefinition, type IndicatorInstance, IndicatorInstanceSchema, type IndicatorMetadata, IndicatorMetadataSchema, type IndicatorOutput, type IndicatorPanel, type IndicatorSettings$1 as IndicatorSettings, IndicatorSettingsForm, IndicatorSettingsSchema, KeltnerChannelsIndicator, type LineStyle, LineStyleSchema, LocalStoragePersistenceAdapter, MACDIndicator, MFIIndicator, MockAdapter, NoOpPersistenceAdapter, OBVIndicator, type OHLCVBar, PSARIndicator, type PersistenceAdapter, ROCIndicator, RSIIndicator, type RealtimeHandlers, type RealtimeSubscription, type RenderConfig, RenderConfigSchema, SMAIndicator, type SettingField, SettingFieldSchema, type SettingFieldType, 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 };
|
|
448
|
+
export { ADXIndicator, ATRIndicator, AlpacaBarAdapter, type BarDataAdapter, type BarDataAdapterOptions, BollingerBandsIndicator, CCIIndicator, ChartComponent, type ChartLine, ChartSeriesType, type ChartTrade, DonchianChannelsIndicator, EMAIndicator, ForceIndexIndicator, type HistoricalDataParams, IchimokuIndicator, IndicatorBrowser, type IndicatorCalculation, IndicatorCategory, type IndicatorDefinition, type IndicatorInstance, IndicatorInstanceSchema, type IndicatorMetadata, IndicatorMetadataSchema, type IndicatorOutput, type IndicatorPanel, type IndicatorSettings$1 as IndicatorSettings, IndicatorSettingsForm, IndicatorSettingsSchema, KeltnerChannelsIndicator, type LineStyle, LineStyleSchema, LocalStoragePersistenceAdapter, MACDIndicator, MFIIndicator, MockAdapter, NoOpPersistenceAdapter, OBVIndicator, type OHLCVBar, PSARIndicator, type PersistenceAdapter, ROCIndicator, RSIIndicator, type RealtimeHandlers, type RealtimeSubscription, type RenderConfig, RenderConfigSchema, SMAIndicator, type SettingField, SettingFieldSchema, type SettingFieldType, 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 };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useRef, useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import { createChart, ColorType, CandlestickSeries, HistogramSeries, createSeriesMarkers, LineSeries, AreaSeries, LineStyle } from 'lightweight-charts';
|
|
3
4
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
5
|
import { X, Search, Plus } from 'lucide-react';
|
|
@@ -255,6 +256,35 @@ var BandsPrimitive = class {
|
|
|
255
256
|
this._paneViews.forEach((pw) => pw.update());
|
|
256
257
|
}
|
|
257
258
|
};
|
|
259
|
+
|
|
260
|
+
// src/components/trade-markers.ts
|
|
261
|
+
var WIN = "#10b981";
|
|
262
|
+
var LOSS = "#ef4444";
|
|
263
|
+
var SEL = "#3b82f6";
|
|
264
|
+
function buildTradeMarkers(trades, selectedTradeId) {
|
|
265
|
+
const markers = [];
|
|
266
|
+
for (const t of trades) {
|
|
267
|
+
const selected = t.id === selectedTradeId;
|
|
268
|
+
const base = t.outcome === "win" ? WIN : LOSS;
|
|
269
|
+
const color = selected ? SEL : base;
|
|
270
|
+
markers.push({
|
|
271
|
+
time: t.entryTime / 1e3,
|
|
272
|
+
position: "belowBar",
|
|
273
|
+
shape: "arrowUp",
|
|
274
|
+
color,
|
|
275
|
+
text: selected ? `${t.outcome === "win" ? "+" : ""}entry` : ""
|
|
276
|
+
});
|
|
277
|
+
markers.push({
|
|
278
|
+
time: t.exitTime / 1e3,
|
|
279
|
+
position: "aboveBar",
|
|
280
|
+
shape: "arrowDown",
|
|
281
|
+
color,
|
|
282
|
+
text: selected ? "exit" : ""
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
markers.sort((a, b) => a.time - b.time);
|
|
286
|
+
return markers;
|
|
287
|
+
}
|
|
258
288
|
function ChartComponent({
|
|
259
289
|
bars,
|
|
260
290
|
onLoadMoreData,
|
|
@@ -266,7 +296,12 @@ function ChartComponent({
|
|
|
266
296
|
onAddLine,
|
|
267
297
|
onClearAllLines,
|
|
268
298
|
enableBarSelection = true,
|
|
269
|
-
onBarClick
|
|
299
|
+
onBarClick,
|
|
300
|
+
trades = [],
|
|
301
|
+
selectedTradeId = null,
|
|
302
|
+
renderTradePopup,
|
|
303
|
+
height,
|
|
304
|
+
focusTradeId = null
|
|
270
305
|
}) {
|
|
271
306
|
const chartContainerRef = useRef(null);
|
|
272
307
|
const chartRef = useRef(null);
|
|
@@ -283,14 +318,18 @@ function ChartComponent({
|
|
|
283
318
|
const [selectedBar, setSelectedBar] = useState(null);
|
|
284
319
|
const selectedBarRef = useRef(null);
|
|
285
320
|
const [spotlightPosition, setSpotlightPosition] = useState(null);
|
|
321
|
+
const [tradePopupPos, setTradePopupPos] = useState(null);
|
|
286
322
|
const previousBarsRef = useRef([]);
|
|
287
323
|
const previousBarsLengthRef = useRef(0);
|
|
288
324
|
const isLoadingRef = useRef(false);
|
|
289
325
|
const barsRef = useRef(bars);
|
|
290
326
|
const isDraggingRef = useRef(false);
|
|
291
327
|
const mouseDownPosRef = useRef(null);
|
|
328
|
+
const heightRef = useRef(height);
|
|
329
|
+
heightRef.current = height;
|
|
292
330
|
useEffect(() => {
|
|
293
331
|
if (!chartContainerRef.current) return;
|
|
332
|
+
chartContainerRef.current.style.position = "relative";
|
|
294
333
|
const chart = createChart(chartContainerRef.current, {
|
|
295
334
|
layout: {
|
|
296
335
|
background: { type: ColorType.Solid, color: "#0f172a" },
|
|
@@ -306,7 +345,10 @@ function ChartComponent({
|
|
|
306
345
|
horzLines: { color: "#1e293b" }
|
|
307
346
|
},
|
|
308
347
|
width: chartContainerRef.current.clientWidth,
|
|
309
|
-
height
|
|
348
|
+
// Auto-fill the container's height unless an explicit `height` is given.
|
|
349
|
+
// Fall back to 600 only when the container hasn't been laid out yet (a
|
|
350
|
+
// ResizeObserver below corrects it on first measure).
|
|
351
|
+
height: height ?? (chartContainerRef.current.clientHeight || 600),
|
|
310
352
|
timeScale: {
|
|
311
353
|
timeVisible: true,
|
|
312
354
|
secondsVisible: true,
|
|
@@ -349,11 +391,9 @@ function ChartComponent({
|
|
|
349
391
|
seriesMarkersRef.current = createSeriesMarkers(candlestickSeries, []);
|
|
350
392
|
chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
|
|
351
393
|
if (logicalRange && logicalRange.from < 5 && !isLoadingRef.current && onLoadMoreData) {
|
|
352
|
-
console.log("Triggering load more, logicalRange.from:", logicalRange.from);
|
|
353
394
|
isLoadingRef.current = true;
|
|
354
395
|
setIsLoadingMore(true);
|
|
355
396
|
previousBarsLengthRef.current = barsRef.current.length;
|
|
356
|
-
console.log("Set previousBarsLengthRef to:", previousBarsLengthRef.current);
|
|
357
397
|
const sortedBars = [...barsRef.current].sort((a, b) => a.timestamp - b.timestamp);
|
|
358
398
|
const oldestTimestamp = sortedBars[0]?.timestamp;
|
|
359
399
|
if (oldestTimestamp) {
|
|
@@ -363,29 +403,33 @@ function ChartComponent({
|
|
|
363
403
|
});
|
|
364
404
|
const handleResize = () => {
|
|
365
405
|
if (chartContainerRef.current && chart) {
|
|
366
|
-
|
|
406
|
+
const nextHeight = heightRef.current ?? chartContainerRef.current.clientHeight;
|
|
407
|
+
chart.applyOptions({
|
|
408
|
+
width: chartContainerRef.current.clientWidth,
|
|
409
|
+
// Only drive height when auto-filling and the container has a real
|
|
410
|
+
// measured height; otherwise leave the current height untouched.
|
|
411
|
+
...heightRef.current === void 0 && nextHeight > 0 ? { height: nextHeight } : {},
|
|
412
|
+
...heightRef.current !== void 0 ? { height: heightRef.current } : {}
|
|
413
|
+
});
|
|
367
414
|
}
|
|
368
415
|
};
|
|
416
|
+
const resizeObserver = new ResizeObserver(handleResize);
|
|
417
|
+
resizeObserver.observe(chartContainerRef.current);
|
|
369
418
|
const handleContextMenu = (e) => {
|
|
370
|
-
console.log("Context menu event triggered!", e);
|
|
371
419
|
e.preventDefault();
|
|
372
420
|
e.stopPropagation();
|
|
373
421
|
if (!candlestickSeriesRef.current) {
|
|
374
|
-
console.log("No candlestick series yet");
|
|
375
422
|
return;
|
|
376
423
|
}
|
|
377
424
|
const rect = chartContainerRef.current.getBoundingClientRect();
|
|
378
425
|
const y = e.clientY - rect.top;
|
|
379
|
-
console.log("Y position:", y, "Rect:", rect);
|
|
380
426
|
const price = candlestickSeriesRef.current.coordinateToPrice(y);
|
|
381
|
-
console.log("Calculated price:", price);
|
|
382
427
|
if (price !== null) {
|
|
383
428
|
setContextMenu({
|
|
384
429
|
x: e.clientX - rect.left,
|
|
385
430
|
y,
|
|
386
431
|
price
|
|
387
432
|
});
|
|
388
|
-
console.log("Context menu set:", { x: e.clientX - rect.left, y, price });
|
|
389
433
|
}
|
|
390
434
|
};
|
|
391
435
|
const handleMouseDown = (e) => {
|
|
@@ -405,54 +449,34 @@ function ChartComponent({
|
|
|
405
449
|
mouseDownPosRef.current = null;
|
|
406
450
|
};
|
|
407
451
|
const handleClick = (e) => {
|
|
408
|
-
console.log("=== CLICK DEBUG START ===");
|
|
409
|
-
console.log("Click event - closing context menu");
|
|
410
452
|
setContextMenu(null);
|
|
411
453
|
if (isDraggingRef.current) {
|
|
412
|
-
console.log("Ignoring click - user was dragging");
|
|
413
454
|
isDraggingRef.current = false;
|
|
414
455
|
return;
|
|
415
456
|
}
|
|
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
457
|
if (!enableBarSelection) {
|
|
421
|
-
console.log("Bar selection is DISABLED - exiting");
|
|
422
458
|
return;
|
|
423
459
|
}
|
|
424
460
|
if (!chartRef.current) {
|
|
425
|
-
console.log("Chart ref is NULL - exiting");
|
|
426
461
|
return;
|
|
427
462
|
}
|
|
428
463
|
if (!candlestickSeriesRef.current) {
|
|
429
|
-
console.log("Candlestick series ref is NULL - exiting");
|
|
430
464
|
return;
|
|
431
465
|
}
|
|
432
|
-
console.log("All checks passed - proceeding with bar selection");
|
|
433
466
|
const rect = chartContainerRef.current.getBoundingClientRect();
|
|
434
467
|
const x = e.clientX - rect.left;
|
|
435
|
-
console.log("Click X coordinate:", x);
|
|
436
468
|
const timeScale = chartRef.current.timeScale();
|
|
437
469
|
const coordinate = timeScale.coordinateToTime(x);
|
|
438
|
-
console.log("Coordinate from timeScale:", coordinate);
|
|
439
470
|
if (!coordinate) {
|
|
440
|
-
console.log("No coordinate from timeScale - exiting");
|
|
441
471
|
return;
|
|
442
472
|
}
|
|
443
473
|
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
474
|
const clickedBar = barsRef.current.find((bar) => {
|
|
447
475
|
const diff = Math.abs(bar.timestamp / 1e3 - clickedTime);
|
|
448
476
|
return diff < 300;
|
|
449
477
|
});
|
|
450
|
-
console.log("Clicked bar found:", !!clickedBar);
|
|
451
478
|
if (clickedBar) {
|
|
452
|
-
console.log("Clicked bar details:", clickedBar);
|
|
453
|
-
console.log("Current selectedBarRef.current:", selectedBarRef.current);
|
|
454
479
|
if (selectedBarRef.current && selectedBarRef.current.timestamp === clickedBar.timestamp) {
|
|
455
|
-
console.log("Deselecting bar (same bar clicked)");
|
|
456
480
|
selectedBarRef.current = null;
|
|
457
481
|
setSelectedBar(null);
|
|
458
482
|
setSpotlightPosition(null);
|
|
@@ -460,24 +484,18 @@ function ChartComponent({
|
|
|
460
484
|
onBarClick(null);
|
|
461
485
|
}
|
|
462
486
|
} else {
|
|
463
|
-
console.log("Selecting new bar");
|
|
464
487
|
selectedBarRef.current = clickedBar;
|
|
465
488
|
setSelectedBar(clickedBar);
|
|
466
489
|
const barX = timeScale.timeToCoordinate(clickedBar.timestamp / 1e3);
|
|
467
|
-
console.log("Bar X position:", barX);
|
|
468
490
|
if (barX !== null) {
|
|
469
491
|
const barWidth = Math.max(8, rect.width / barsRef.current.length);
|
|
470
|
-
console.log("Bar width:", barWidth);
|
|
471
492
|
setSpotlightPosition({ x: barX - barWidth / 2, width: barWidth });
|
|
472
493
|
}
|
|
473
494
|
if (onBarClick) {
|
|
474
495
|
onBarClick(clickedBar);
|
|
475
496
|
}
|
|
476
497
|
}
|
|
477
|
-
} else {
|
|
478
|
-
console.log("No bar found at clicked position");
|
|
479
498
|
}
|
|
480
|
-
console.log("=== CLICK DEBUG END ===");
|
|
481
499
|
};
|
|
482
500
|
window.addEventListener("resize", handleResize);
|
|
483
501
|
chartContainerRef.current.addEventListener("contextmenu", handleContextMenu, true);
|
|
@@ -488,6 +506,7 @@ function ChartComponent({
|
|
|
488
506
|
const container = chartContainerRef.current;
|
|
489
507
|
return () => {
|
|
490
508
|
window.removeEventListener("resize", handleResize);
|
|
509
|
+
resizeObserver.disconnect();
|
|
491
510
|
container.removeEventListener("contextmenu", handleContextMenu, true);
|
|
492
511
|
container.removeEventListener("mousedown", handleMouseDown, true);
|
|
493
512
|
container.removeEventListener("mousemove", handleMouseMove, true);
|
|
@@ -499,6 +518,31 @@ function ChartComponent({
|
|
|
499
518
|
useEffect(() => {
|
|
500
519
|
barsRef.current = bars;
|
|
501
520
|
}, [bars]);
|
|
521
|
+
useEffect(() => {
|
|
522
|
+
if (chartRef.current && height !== void 0) {
|
|
523
|
+
chartRef.current.applyOptions({ height });
|
|
524
|
+
}
|
|
525
|
+
}, [height]);
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
if (!chartRef.current || !focusTradeId) return;
|
|
528
|
+
const trade = trades.find((t) => t.id === focusTradeId);
|
|
529
|
+
if (!trade) return;
|
|
530
|
+
const entrySec = trade.entryTime / 1e3;
|
|
531
|
+
const exitSec = trade.exitTime / 1e3;
|
|
532
|
+
const sorted = [...bars].sort((a, b) => a.timestamp - b.timestamp);
|
|
533
|
+
const stepSec = sorted.length > 1 ? (sorted[sorted.length - 1].timestamp - sorted[0].timestamp) / 1e3 / (sorted.length - 1) : 300;
|
|
534
|
+
const pad = Math.max(exitSec - entrySec, stepSec * 12);
|
|
535
|
+
const raf = requestAnimationFrame(() => {
|
|
536
|
+
try {
|
|
537
|
+
chartRef.current?.timeScale().setVisibleRange({
|
|
538
|
+
from: entrySec - pad,
|
|
539
|
+
to: exitSec + pad
|
|
540
|
+
});
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
return () => cancelAnimationFrame(raf);
|
|
545
|
+
}, [focusTradeId, trades, bars]);
|
|
502
546
|
useEffect(() => {
|
|
503
547
|
if (!candlestickSeriesRef.current || !volumeSeriesRef.current) return;
|
|
504
548
|
const sortedBars = [...bars].sort((a, b) => a.timestamp - b.timestamp);
|
|
@@ -509,21 +553,15 @@ function ChartComponent({
|
|
|
509
553
|
}
|
|
510
554
|
return acc;
|
|
511
555
|
}, []);
|
|
512
|
-
console.log("Debug - bars.length:", bars.length, "uniqueBars.length:", uniqueBars.length, "isLoadingRef:", isLoadingRef.current, "previousBarsLength:", previousBarsLengthRef.current);
|
|
513
556
|
const previousBars = previousBarsRef.current;
|
|
514
557
|
const isInitialLoad = previousBars.length === 0;
|
|
515
558
|
const hasNewBars = uniqueBars.length > previousBars.length;
|
|
516
559
|
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
560
|
if (isLoadingRef.current && uniqueBars.length > previousBarsLengthRef.current) {
|
|
520
|
-
console.log("Hiding loading indicator");
|
|
521
561
|
isLoadingRef.current = false;
|
|
522
562
|
setIsLoadingMore(false);
|
|
523
563
|
}
|
|
524
|
-
const firstBarChanged = uniqueBars.length > 0 && previousBars.length > 0 && uniqueBars[0].timestamp !== previousBars[0].timestamp;
|
|
525
564
|
if (isInitialLoad || hasNewBars) {
|
|
526
|
-
console.log("Setting data with", uniqueBars.length, "bars", "firstBarChanged:", firstBarChanged);
|
|
527
565
|
const candleData = uniqueBars.map((bar) => ({
|
|
528
566
|
time: bar.timestamp / 1e3,
|
|
529
567
|
open: bar.open,
|
|
@@ -536,8 +574,6 @@ function ChartComponent({
|
|
|
536
574
|
value: bar.volume,
|
|
537
575
|
color: bar.close >= bar.open ? "#10b98180" : "#ef444480"
|
|
538
576
|
}));
|
|
539
|
-
console.log("candleData sample (first 3):", candleData.slice(0, 3));
|
|
540
|
-
console.log("candleData sample (last 3):", candleData.slice(-3));
|
|
541
577
|
candlestickSeriesRef.current.setData(candleData);
|
|
542
578
|
volumeSeriesRef.current.setData(volumeData);
|
|
543
579
|
} else if (lastBarChanged && uniqueBars.length > 0 && !hasNewBars) {
|
|
@@ -565,7 +601,6 @@ function ChartComponent({
|
|
|
565
601
|
candlestickSeriesRef.current?.removePriceLine(priceLine);
|
|
566
602
|
});
|
|
567
603
|
priceLineRefs.current.clear();
|
|
568
|
-
const newPositions = /* @__PURE__ */ new Map();
|
|
569
604
|
lines.forEach((line) => {
|
|
570
605
|
const priceLine = candlestickSeriesRef.current?.createPriceLine({
|
|
571
606
|
price: line.price,
|
|
@@ -578,28 +613,47 @@ function ChartComponent({
|
|
|
578
613
|
if (priceLine) {
|
|
579
614
|
priceLineRefs.current.set(line.id, priceLine);
|
|
580
615
|
}
|
|
581
|
-
const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
|
|
582
|
-
if (y !== null && y !== void 0) {
|
|
583
|
-
newPositions.set(line.id, y);
|
|
584
|
-
}
|
|
585
616
|
});
|
|
586
|
-
|
|
617
|
+
requestAnimationFrame(() => {
|
|
618
|
+
if (!candlestickSeriesRef.current || !chartContainerRef.current) return;
|
|
619
|
+
const rect = chartContainerRef.current.getBoundingClientRect();
|
|
620
|
+
const newPositions = /* @__PURE__ */ new Map();
|
|
621
|
+
lines.forEach((line) => {
|
|
622
|
+
const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
|
|
623
|
+
if (y !== null && y !== void 0) {
|
|
624
|
+
newPositions.set(line.id, {
|
|
625
|
+
top: rect.top + y,
|
|
626
|
+
right: window.innerWidth - rect.right + 68 + estimateLabelTitleWidth(line.title)
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
setLinePositions(newPositions);
|
|
631
|
+
});
|
|
587
632
|
}, [lines]);
|
|
588
633
|
useEffect(() => {
|
|
589
634
|
if (!chartRef.current || !candlestickSeriesRef.current) return;
|
|
590
635
|
const updatePositions = () => {
|
|
636
|
+
if (!candlestickSeriesRef.current || !chartContainerRef.current) return;
|
|
637
|
+
const rect = chartContainerRef.current.getBoundingClientRect();
|
|
591
638
|
const newPositions = /* @__PURE__ */ new Map();
|
|
592
639
|
lines.forEach((line) => {
|
|
593
640
|
const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
|
|
594
641
|
if (y !== null && y !== void 0) {
|
|
595
|
-
newPositions.set(line.id,
|
|
642
|
+
newPositions.set(line.id, {
|
|
643
|
+
top: rect.top + y,
|
|
644
|
+
right: window.innerWidth - rect.right + 68 + estimateLabelTitleWidth(line.title)
|
|
645
|
+
});
|
|
596
646
|
}
|
|
597
647
|
});
|
|
598
648
|
setLinePositions(newPositions);
|
|
599
649
|
};
|
|
600
650
|
chartRef.current.timeScale().subscribeVisibleLogicalRangeChange(updatePositions);
|
|
651
|
+
window.addEventListener("scroll", updatePositions, { passive: true });
|
|
652
|
+
window.addEventListener("resize", updatePositions);
|
|
601
653
|
return () => {
|
|
602
654
|
chartRef.current?.timeScale().unsubscribeVisibleLogicalRangeChange(updatePositions);
|
|
655
|
+
window.removeEventListener("scroll", updatePositions);
|
|
656
|
+
window.removeEventListener("resize", updatePositions);
|
|
603
657
|
};
|
|
604
658
|
}, [lines]);
|
|
605
659
|
useEffect(() => {
|
|
@@ -719,31 +773,22 @@ function ChartComponent({
|
|
|
719
773
|
});
|
|
720
774
|
}, [bars, indicators]);
|
|
721
775
|
useEffect(() => {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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]);
|
|
776
|
+
if (!seriesMarkersRef.current) return;
|
|
777
|
+
const tradeMarkers = buildTradeMarkers(trades, selectedTradeId);
|
|
778
|
+
const selectionMarker = enableBarSelection && selectedBar ? [
|
|
779
|
+
{
|
|
780
|
+
time: selectedBar.timestamp / 1e3,
|
|
781
|
+
position: "aboveBar",
|
|
782
|
+
color: "#3b82f6",
|
|
783
|
+
shape: "circle",
|
|
784
|
+
text: ""
|
|
785
|
+
}
|
|
786
|
+
] : [];
|
|
787
|
+
const all = [...tradeMarkers, ...selectionMarker].sort(
|
|
788
|
+
(a, b) => a.time - b.time
|
|
789
|
+
);
|
|
790
|
+
seriesMarkersRef.current.setMarkers(all);
|
|
791
|
+
}, [trades, selectedTradeId, selectedBar, enableBarSelection]);
|
|
747
792
|
useEffect(() => {
|
|
748
793
|
if (!chartRef.current || !selectedBar || !enableBarSelection) {
|
|
749
794
|
setSpotlightPosition(null);
|
|
@@ -768,6 +813,28 @@ function ChartComponent({
|
|
|
768
813
|
}
|
|
769
814
|
};
|
|
770
815
|
}, [selectedBar, enableBarSelection, bars.length]);
|
|
816
|
+
const selectedTrade = trades.find((t) => t.id === selectedTradeId) ?? null;
|
|
817
|
+
useEffect(() => {
|
|
818
|
+
if (!chartRef.current || !selectedTrade) {
|
|
819
|
+
setTradePopupPos(null);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const updateTradePopupPos = () => {
|
|
823
|
+
if (!chartRef.current || !selectedTrade || !chartContainerRef.current) return;
|
|
824
|
+
const timeScale2 = chartRef.current.timeScale();
|
|
825
|
+
const x = timeScale2.timeToCoordinate(selectedTrade.entryTime / 1e3);
|
|
826
|
+
const width = chartContainerRef.current.getBoundingClientRect().width;
|
|
827
|
+
setTradePopupPos(x !== null && x >= 0 && x <= width ? { x } : null);
|
|
828
|
+
};
|
|
829
|
+
updateTradePopupPos();
|
|
830
|
+
const timeScale = chartRef.current.timeScale();
|
|
831
|
+
timeScale.subscribeVisibleLogicalRangeChange(updateTradePopupPos);
|
|
832
|
+
return () => {
|
|
833
|
+
if (chartRef.current) {
|
|
834
|
+
chartRef.current.timeScale().unsubscribeVisibleLogicalRangeChange(updateTradePopupPos);
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
}, [selectedTrade, selectedTrade?.entryTime]);
|
|
771
838
|
const handleDeleteLine = (lineId) => {
|
|
772
839
|
if (onDeleteLine) {
|
|
773
840
|
onDeleteLine(lineId);
|
|
@@ -802,25 +869,38 @@ function ChartComponent({
|
|
|
802
869
|
}
|
|
803
870
|
}
|
|
804
871
|
),
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
872
|
+
selectedTrade && tradePopupPos && renderTradePopup && /* @__PURE__ */ jsx(
|
|
873
|
+
"div",
|
|
874
|
+
{
|
|
875
|
+
className: "absolute z-20 pointer-events-auto",
|
|
876
|
+
style: { left: `${tradePopupPos.x}px`, top: 8, transform: "translateX(-50%)" },
|
|
877
|
+
children: renderTradePopup(selectedTrade)
|
|
878
|
+
}
|
|
879
|
+
)
|
|
880
|
+
] }),
|
|
881
|
+
chartContainerRef.current && lines.map((line) => {
|
|
882
|
+
const pos = linePositions.get(line.id);
|
|
883
|
+
if (!pos) return null;
|
|
884
|
+
return createPortal(
|
|
885
|
+
/* @__PURE__ */ jsx(
|
|
809
886
|
"button",
|
|
810
887
|
{
|
|
811
888
|
onClick: () => handleDeleteLine(line.id),
|
|
812
|
-
className: "
|
|
889
|
+
className: "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 shadow-md",
|
|
813
890
|
style: {
|
|
814
|
-
|
|
815
|
-
top: `${
|
|
891
|
+
position: "fixed",
|
|
892
|
+
top: `${pos.top - 8}px`,
|
|
893
|
+
right: `${pos.right}px`,
|
|
894
|
+
zIndex: 9999
|
|
816
895
|
},
|
|
817
896
|
title: "Delete line",
|
|
818
897
|
children: "\xD7"
|
|
819
898
|
},
|
|
820
899
|
line.id
|
|
821
|
-
)
|
|
822
|
-
|
|
823
|
-
|
|
900
|
+
),
|
|
901
|
+
document.body
|
|
902
|
+
);
|
|
903
|
+
}),
|
|
824
904
|
contextMenu && /* @__PURE__ */ jsxs(
|
|
825
905
|
"div",
|
|
826
906
|
{
|
|
@@ -889,6 +969,10 @@ function ChartComponent({
|
|
|
889
969
|
)
|
|
890
970
|
] });
|
|
891
971
|
}
|
|
972
|
+
function estimateLabelTitleWidth(title) {
|
|
973
|
+
if (!title) return 0;
|
|
974
|
+
return title.length * 7 + 18;
|
|
975
|
+
}
|
|
892
976
|
function getLineStyle(style) {
|
|
893
977
|
switch (style) {
|
|
894
978
|
case "dashed":
|