@adrienhobbs/candlekit 0.2.3 → 0.2.5

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 CHANGED
@@ -21,6 +21,16 @@ interface ChartLine {
21
21
  title?: string;
22
22
  type?: 'entry' | 'stopLoss' | 'takeProfit' | 'mfe' | 'mae';
23
23
  }
24
+ /** A shaded horizontal price band (e.g. an MFE↔MAE excursion zone). */
25
+ interface PriceBand {
26
+ id: string;
27
+ /** Upper price bound of the band. */
28
+ top: number;
29
+ /** Lower price bound of the band. */
30
+ bottom: number;
31
+ /** CSS color (use an rgba/low-opacity fill so candles show through). */
32
+ color: string;
33
+ }
24
34
  interface ChartTrade {
25
35
  id: string;
26
36
  entryTime: number;
@@ -177,8 +187,15 @@ interface ChartComponentProps {
177
187
  * view. Pairs with `selectedTradeId` to drive both selection and focus.
178
188
  */
179
189
  focusTradeId?: string | null;
190
+ /**
191
+ * IANA timezone (e.g. "America/New_York") for the axis ticks + crosshair
192
+ * labels. Omit to use the viewer's local timezone.
193
+ */
194
+ timeZone?: string;
195
+ /** Shaded horizontal price bands (e.g. an MFE↔MAE excursion zone). */
196
+ priceBands?: PriceBand[];
180
197
  }
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;
198
+ declare function ChartComponent({ bars, onLoadMoreData, indicators, lines, onBarUpdate, onNewBar, onDeleteLine, onAddLine, onClearAllLines, enableBarSelection, onBarClick, trades, selectedTradeId, renderTradePopup, height, focusTradeId, timeZone, priceBands, }: ChartComponentProps): react_jsx_runtime.JSX.Element;
182
199
 
183
200
  interface IndicatorBrowserProps {
184
201
  isOpen: boolean;
@@ -445,4 +462,4 @@ declare function getOldestBar(bars: OHLCVBar[]): OHLCVBar | null;
445
462
  declare function getNewestBar(bars: OHLCVBar[]): OHLCVBar | null;
446
463
  declare function updateCurrentBar(bars: OHLCVBar[], tradePrice: number, tradeVolume: number): OHLCVBar[];
447
464
 
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 };
465
+ 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, type PriceBand, 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
@@ -260,13 +260,11 @@ var BandsPrimitive = class {
260
260
  // src/components/trade-markers.ts
261
261
  var WIN = "#10b981";
262
262
  var LOSS = "#ef4444";
263
- var SEL = "#3b82f6";
264
263
  function buildTradeMarkers(trades, selectedTradeId) {
265
264
  const markers = [];
266
265
  for (const t of trades) {
267
266
  const selected = t.id === selectedTradeId;
268
- const base = t.outcome === "win" ? WIN : LOSS;
269
- const color = selected ? SEL : base;
267
+ const color = t.outcome === "win" ? WIN : LOSS;
270
268
  markers.push({
271
269
  time: t.entryTime / 1e3,
272
270
  position: "belowBar",
@@ -301,7 +299,9 @@ function ChartComponent({
301
299
  selectedTradeId = null,
302
300
  renderTradePopup,
303
301
  height,
304
- focusTradeId = null
302
+ focusTradeId = null,
303
+ timeZone,
304
+ priceBands = []
305
305
  }) {
306
306
  const chartContainerRef = useRef(null);
307
307
  const chartRef = useRef(null);
@@ -315,6 +315,7 @@ function ChartComponent({
315
315
  const [isLoadingMore, setIsLoadingMore] = useState(false);
316
316
  const [contextMenu, setContextMenu] = useState(null);
317
317
  const [linePositions, setLinePositions] = useState(/* @__PURE__ */ new Map());
318
+ const [bandRects, setBandRects] = useState([]);
318
319
  const [selectedBar, setSelectedBar] = useState(null);
319
320
  const selectedBarRef = useRef(null);
320
321
  const [spotlightPosition, setSpotlightPosition] = useState(null);
@@ -346,11 +347,11 @@ function ChartComponent({
346
347
  vertLines: { color: "#1e293b" },
347
348
  horzLines: { color: "#1e293b" }
348
349
  },
349
- // Render axis ticks + crosshair in the viewer's LOCAL timezone (lightweight-
350
- // charts otherwise renders numeric times as UTC, which mismatches any
351
- // local-time table/labels alongside the chart).
350
+ // Render axis ticks + crosshair in `timeZone` (default: the viewer's local
351
+ // timezone). lightweight-charts otherwise renders numeric times as UTC,
352
+ // which mismatches local/exchange-time labels alongside the chart.
352
353
  localization: {
353
- timeFormatter: localCrosshairTimeFormatter
354
+ timeFormatter: makeCrosshairTimeFormatter(timeZone)
354
355
  },
355
356
  width: chartContainerRef.current.clientWidth,
356
357
  // Auto-fill the container's height unless an explicit `height` is given.
@@ -361,7 +362,7 @@ function ChartComponent({
361
362
  timeVisible: true,
362
363
  secondsVisible: true,
363
364
  borderColor: "#334155",
364
- tickMarkFormatter: localTickMarkFormatter
365
+ tickMarkFormatter: makeTickMarkFormatter(timeZone)
365
366
  },
366
367
  rightPriceScale: {
367
368
  borderColor: "#334155"
@@ -523,6 +524,9 @@ function ChartComponent({
523
524
  container.removeEventListener("mouseup", handleMouseUp, true);
524
525
  container.removeEventListener("click", handleClick, true);
525
526
  chart.remove();
527
+ indicatorSeriesRef.current.clear();
528
+ indicatorPaneIndexRef.current.clear();
529
+ nextPaneIndexRef.current = 2;
526
530
  };
527
531
  }, []);
528
532
  useEffect(() => {
@@ -687,6 +691,36 @@ function ChartComponent({
687
691
  window.removeEventListener("resize", updatePositions);
688
692
  };
689
693
  }, [lines]);
694
+ useEffect(() => {
695
+ const series = candlestickSeriesRef.current;
696
+ if (!chartRef.current || !series) return;
697
+ if (priceBands.length === 0) {
698
+ setBandRects([]);
699
+ return;
700
+ }
701
+ const recompute = () => {
702
+ if (!candlestickSeriesRef.current) return;
703
+ const rects = [];
704
+ for (const band of priceBands) {
705
+ const yTop = candlestickSeriesRef.current.priceToCoordinate(band.top);
706
+ const yBottom = candlestickSeriesRef.current.priceToCoordinate(band.bottom);
707
+ if (yTop == null || yBottom == null) continue;
708
+ const top = Math.min(yTop, yBottom);
709
+ const height2 = Math.abs(yBottom - yTop);
710
+ rects.push({ id: band.id, top, height: height2, color: band.color });
711
+ }
712
+ setBandRects(rects);
713
+ };
714
+ const raf = requestAnimationFrame(recompute);
715
+ const timeScale = chartRef.current.timeScale();
716
+ timeScale.subscribeVisibleLogicalRangeChange(recompute);
717
+ window.addEventListener("resize", recompute);
718
+ return () => {
719
+ cancelAnimationFrame(raf);
720
+ chartRef.current?.timeScale().unsubscribeVisibleLogicalRangeChange(recompute);
721
+ window.removeEventListener("resize", recompute);
722
+ };
723
+ }, [priceBands]);
690
724
  useEffect(() => {
691
725
  if (!chartRef.current) return;
692
726
  indicatorSeriesRef.current.forEach((series) => {
@@ -714,7 +748,7 @@ function ChartComponent({
714
748
  lineWidth: indicator.settings.lineWidth || 2,
715
749
  title: indicator.name
716
750
  }, paneIndex);
717
- const lineData = data.map((d) => ({ time: d.time, value: d.value }));
751
+ const lineData = data.map((d) => ({ time: d.time, value: d.value })).filter((d) => Number.isFinite(d.value));
718
752
  series.setData(lineData);
719
753
  indicatorSeriesRef.current.set(indicator.id, series);
720
754
  } else if (definition.renderConfig.hasBandFill && definition.renderConfig.fillBands) {
@@ -757,7 +791,7 @@ function ChartComponent({
757
791
  color: indicator.settings.color || "#8b5cf6",
758
792
  title: indicator.name
759
793
  }, paneIndex);
760
- const histData = data.map((d) => ({ time: d.time, value: d.value }));
794
+ const histData = data.map((d) => ({ time: d.time, value: d.value })).filter((d) => Number.isFinite(d.value));
761
795
  series.setData(histData);
762
796
  indicatorSeriesRef.current.set(indicator.id, series);
763
797
  } else if (seriesType === "area") {
@@ -768,7 +802,7 @@ function ChartComponent({
768
802
  lineWidth: indicator.settings.lineWidth || 2,
769
803
  title: indicator.name
770
804
  }, paneIndex);
771
- const areaData = data.map((d) => ({ time: d.time, value: d.value }));
805
+ const areaData = data.map((d) => ({ time: d.time, value: d.value })).filter((d) => Number.isFinite(d.value));
772
806
  series.setData(areaData);
773
807
  indicatorSeriesRef.current.set(indicator.id, series);
774
808
  }
@@ -784,7 +818,7 @@ function ChartComponent({
784
818
  if (definition.renderConfig.outputCount === 1) {
785
819
  const series = indicatorSeriesRef.current.get(indicator.id);
786
820
  if (series) {
787
- const lineData = data.map((d) => ({ time: d.time, value: d.value }));
821
+ const lineData = data.map((d) => ({ time: d.time, value: d.value })).filter((d) => Number.isFinite(d.value));
788
822
  series.setData(lineData);
789
823
  }
790
824
  } else if (definition.renderConfig.hasBandFill && definition.renderConfig.fillBands) {
@@ -887,6 +921,14 @@ function ChartComponent({
887
921
  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..." }),
888
922
  /* @__PURE__ */ jsxs("div", { className: "relative", children: [
889
923
  /* @__PURE__ */ jsx("div", { ref: chartContainerRef, className: "w-full" }),
924
+ bandRects.map((b) => /* @__PURE__ */ jsx(
925
+ "div",
926
+ {
927
+ className: "absolute left-0 right-0 pointer-events-none z-4",
928
+ style: { top: `${b.top}px`, height: `${b.height}px`, background: b.color }
929
+ },
930
+ b.id
931
+ )),
890
932
  enableBarSelection && spotlightPosition && /* @__PURE__ */ jsx(
891
933
  "div",
892
934
  {
@@ -1014,30 +1056,38 @@ function getLineStyle(style) {
1014
1056
  return LineStyle.Solid;
1015
1057
  }
1016
1058
  }
1017
- var pad2 = (n) => String(n).padStart(2, "0");
1018
- function localTickMarkFormatter(time, tickMarkType) {
1019
- const d = new Date(time * 1e3);
1020
- switch (tickMarkType) {
1021
- case 0:
1022
- return String(d.getFullYear());
1023
- case 1:
1024
- return d.toLocaleString(void 0, { month: "short" });
1025
- case 2:
1026
- return d.toLocaleString(void 0, { month: "short", day: "numeric" });
1027
- case 4:
1028
- return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
1029
- default:
1030
- return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
1031
- }
1059
+ function makeTickMarkFormatter(timeZone) {
1060
+ const time = new Intl.DateTimeFormat("en-US", { timeZone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" });
1061
+ const timeSec = new Intl.DateTimeFormat("en-US", { timeZone, hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" });
1062
+ const month = new Intl.DateTimeFormat("en-US", { timeZone, month: "short" });
1063
+ const day = new Intl.DateTimeFormat("en-US", { timeZone, month: "short", day: "numeric" });
1064
+ const year = new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric" });
1065
+ return (t, tickMarkType) => {
1066
+ const d = new Date(t * 1e3);
1067
+ switch (tickMarkType) {
1068
+ case 0:
1069
+ return year.format(d);
1070
+ case 1:
1071
+ return month.format(d);
1072
+ case 2:
1073
+ return day.format(d);
1074
+ case 4:
1075
+ return timeSec.format(d);
1076
+ default:
1077
+ return time.format(d);
1078
+ }
1079
+ };
1032
1080
  }
1033
- function localCrosshairTimeFormatter(time) {
1034
- return new Date(time * 1e3).toLocaleString(void 0, {
1081
+ function makeCrosshairTimeFormatter(timeZone) {
1082
+ const fmt = new Intl.DateTimeFormat("en-US", {
1083
+ timeZone,
1035
1084
  month: "short",
1036
1085
  day: "2-digit",
1037
1086
  hour: "2-digit",
1038
1087
  minute: "2-digit",
1039
- hour12: false
1088
+ hourCycle: "h23"
1040
1089
  });
1090
+ return (t) => fmt.format(new Date(t * 1e3));
1041
1091
  }
1042
1092
  var IndicatorCategory = /* @__PURE__ */ ((IndicatorCategory2) => {
1043
1093
  IndicatorCategory2["TREND"] = "Trend";