react-klinecharts-ui 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
- import { registerExtensions } from './chunk-PUULIBFN.js';
2
- export { abcd_default as abcd, anyWaves_default as anyWaves, arrow_default as arrow, circle_default as circle, eightWaves_default as eightWaves, fibonacciCircle_default as fibonacciCircle, fibonacciExtension_default as fibonacciExtension, fibonacciSegment_default as fibonacciSegment, fibonacciSpeedResistanceFan_default as fibonacciSpeedResistanceFan, fibonacciSpiral_default as fibonacciSpiral, fiveWaves_default as fiveWaves, gannBox_default as gannBox, orderLine_default as orderLine, overlays, parallelogram_default as parallelogram, rect_default as rect, registerExtensions, threeWaves_default as threeWaves, triangle_default as triangle, xabcd_default as xabcd } from './chunk-PUULIBFN.js';
1
+ import { registerExtensions, TA_default } from './chunk-U325AHHX.js';
2
+ export { TA_default as TA, abcd_default as abcd, anyWaves_default as anyWaves, arrow_default as arrow, bollTv_default as bollTv, brush_default as brush, cci_default as cci, circle_default as circle, depthOverlay_default as depthOverlay, eightWaves_default as eightWaves, elliottWave_default as elliottWave, fibRetracement_default as fibRetracement, fibonacciCircle_default as fibonacciCircle, fibonacciExtension_default as fibonacciExtension, fibonacciSegment_default as fibonacciSegment, fibonacciSpeedResistanceFan_default as fibonacciSpeedResistanceFan, fibonacciSpiral_default as fibonacciSpiral, fiveWaves_default as fiveWaves, gannBox_default as gannBox, gannFan_default as gannFan, hma_default as hma, ichimoku_default as ichimoku, indicators, longPosition_default as longPosition, maRibbon_default as maRibbon, macdTv_default as macdTv, measure_default as measure, orderLine_default as orderLine, overlays, parallelChannel_default as parallelChannel, parallelogram_default as parallelogram, pivotPoints_default as pivotPoints, ray_default as ray, rect_default as rect, registerExtensions, rsiTv_default as rsiTv, shortPosition_default as shortPosition, stochastic_default as stochastic, superTrend_default as superTrend, threeWaves_default as threeWaves, triangle_default as triangle, vwap_default as vwap, xabcd_default as xabcd } from './chunk-U325AHHX.js';
3
3
  import { createContext, useContext, useReducer, useRef, useCallback, useEffect, useMemo, useState } from 'react';
4
- import { registerOverlay } from 'react-klinecharts';
4
+ import { registerOverlay, registerIndicator } from 'react-klinecharts';
5
5
  import { jsx } from 'react/jsx-runtime';
6
6
 
7
7
  var KlinechartsUIStateContext = createContext(null);
@@ -124,6 +124,7 @@ function KlinechartsUIProvider({
124
124
  })
125
125
  );
126
126
  const fullscreenContainerRef = useRef(null);
127
+ const undoRedoListenerRef = useRef(null);
127
128
  const stateRef = useRef(state);
128
129
  stateRef.current = state;
129
130
  const callbacksRef = useRef({
@@ -189,7 +190,8 @@ function KlinechartsUIProvider({
189
190
  dispatch: enhancedDispatch,
190
191
  datafeed,
191
192
  onSettingsChange,
192
- fullscreenContainerRef
193
+ fullscreenContainerRef,
194
+ undoRedoListenerRef
193
195
  }),
194
196
  [enhancedDispatch, datafeed, onSettingsChange]
195
197
  );
@@ -357,7 +359,14 @@ var MAIN_INDICATORS = [
357
359
  "SMA",
358
360
  "BOLL",
359
361
  "SAR",
360
- "BBI"
362
+ "BBI",
363
+ "BOLL_TV",
364
+ "Ichimoku",
365
+ "SuperTrend",
366
+ "HMA",
367
+ "VWAP",
368
+ "PivotPoints",
369
+ "MA_Ribbon"
361
370
  ];
362
371
  var SUB_INDICATORS = [
363
372
  "MA",
@@ -385,7 +394,11 @@ var SUB_INDICATORS = [
385
394
  "ROC",
386
395
  "PVT",
387
396
  "BBI",
388
- "AO"
397
+ "AO",
398
+ "MACD_TV",
399
+ "RSI_TV",
400
+ "CCI_TV",
401
+ "Stochastic"
389
402
  ];
390
403
  var INDICATOR_PARAMS = {
391
404
  SMA: {
@@ -544,12 +557,98 @@ var INDICATOR_PARAMS = {
544
557
  { label: "params_1", defaultValue: 5 },
545
558
  { label: "params_2", defaultValue: 34 }
546
559
  ]
560
+ },
561
+ BOLL_TV: {
562
+ name: "BOLL_TV",
563
+ localeKey: "boll_tv",
564
+ params: [
565
+ { label: "period", defaultValue: 20 },
566
+ { label: "multiplier", defaultValue: 2 }
567
+ ]
568
+ },
569
+ Ichimoku: {
570
+ name: "Ichimoku",
571
+ localeKey: "ichimoku",
572
+ params: [
573
+ { label: "tenkan", defaultValue: 9 },
574
+ { label: "kijun", defaultValue: 26 },
575
+ { label: "senkou_b", defaultValue: 52 },
576
+ { label: "offset", defaultValue: 26 }
577
+ ]
578
+ },
579
+ SuperTrend: {
580
+ name: "SuperTrend",
581
+ localeKey: "super_trend",
582
+ params: [
583
+ { label: "period", defaultValue: 10 },
584
+ { label: "factor", defaultValue: 3 }
585
+ ]
586
+ },
587
+ HMA: {
588
+ name: "HMA",
589
+ localeKey: "hma",
590
+ params: [{ label: "period", defaultValue: 9 }]
591
+ },
592
+ VWAP: {
593
+ name: "VWAP",
594
+ localeKey: "vwap",
595
+ params: []
596
+ },
597
+ PivotPoints: {
598
+ name: "PivotPoints",
599
+ localeKey: "pivot_points",
600
+ params: []
601
+ },
602
+ MA_Ribbon: {
603
+ name: "MA_Ribbon",
604
+ localeKey: "ma_ribbon",
605
+ params: [
606
+ { label: "period_1", defaultValue: 10 },
607
+ { label: "period_2", defaultValue: 20 },
608
+ { label: "period_3", defaultValue: 30 },
609
+ { label: "period_4", defaultValue: 40 }
610
+ ]
611
+ },
612
+ MACD_TV: {
613
+ name: "MACD_TV",
614
+ localeKey: "macd_tv",
615
+ params: [
616
+ { label: "fast", defaultValue: 12 },
617
+ { label: "slow", defaultValue: 26 },
618
+ { label: "signal", defaultValue: 9 }
619
+ ]
620
+ },
621
+ RSI_TV: {
622
+ name: "RSI_TV",
623
+ localeKey: "rsi_tv",
624
+ params: [
625
+ { label: "rsi_period", defaultValue: 14 },
626
+ { label: "ma_period", defaultValue: 14 }
627
+ ]
628
+ },
629
+ CCI_TV: {
630
+ name: "CCI_TV",
631
+ localeKey: "cci_tv",
632
+ params: [{ label: "period", defaultValue: 20 }]
633
+ },
634
+ Stochastic: {
635
+ name: "Stochastic",
636
+ localeKey: "stochastic",
637
+ params: [
638
+ { label: "period", defaultValue: 14 },
639
+ { label: "smooth_k", defaultValue: 3 },
640
+ { label: "smooth_d", defaultValue: 3 }
641
+ ]
547
642
  }
548
643
  };
549
644
 
550
645
  // src/hooks/useIndicators.ts
646
+ var COLLAPSED_HEIGHT = 30;
551
647
  function useIndicators() {
552
648
  const { state, dispatch } = useKlinechartsUI();
649
+ const { undoRedoListenerRef } = useKlinechartsUIDispatch();
650
+ const paneHeightsRef = useRef({});
651
+ const collapsedPanesRef = useRef(/* @__PURE__ */ new Set());
553
652
  const mainIndicators = useMemo(() => {
554
653
  const activeNames = state.mainIndicators;
555
654
  const inactive = MAIN_INDICATORS.filter((n) => !activeNames.includes(n));
@@ -576,16 +675,24 @@ function useIndicators() {
576
675
  );
577
676
  const newIndicators = [...state.mainIndicators, name];
578
677
  dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
678
+ undoRedoListenerRef.current?.({
679
+ type: "indicator_toggled",
680
+ data: { name, wasActive: false, isMain: true, paneId: "candle_pane" }
681
+ });
579
682
  },
580
- [state.chart, state.mainIndicators, dispatch]
683
+ [state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
581
684
  );
582
685
  const removeMainIndicator = useCallback(
583
686
  (name) => {
584
687
  state.chart?.removeIndicator({ id: `main_${name}` });
585
688
  const newIndicators = state.mainIndicators.filter((n) => n !== name);
586
689
  dispatch({ type: "SET_MAIN_INDICATORS", indicators: newIndicators });
690
+ undoRedoListenerRef.current?.({
691
+ type: "indicator_toggled",
692
+ data: { name, wasActive: true, isMain: true, paneId: "candle_pane" }
693
+ });
587
694
  },
588
- [state.chart, state.mainIndicators, dispatch]
695
+ [state.chart, state.mainIndicators, dispatch, undoRedoListenerRef]
589
696
  );
590
697
  const addSubIndicator = useCallback(
591
698
  (name) => {
@@ -595,17 +702,26 @@ function useIndicators() {
595
702
  const paneId = state.chart?.getIndicators({ id })?.[0]?.paneId ?? "";
596
703
  const newIndicators = { ...state.subIndicators, [name]: paneId };
597
704
  dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
705
+ undoRedoListenerRef.current?.({
706
+ type: "indicator_toggled",
707
+ data: { name, wasActive: false, isMain: false, paneId }
708
+ });
598
709
  },
599
- [state.chart, state.subIndicators, dispatch]
710
+ [state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
600
711
  );
601
712
  const removeSubIndicator = useCallback(
602
713
  (name) => {
714
+ const paneId = state.subIndicators[name] ?? "";
603
715
  state.chart?.removeIndicator({ id: `sub_${name}` });
604
716
  const newIndicators = { ...state.subIndicators };
605
717
  delete newIndicators[name];
606
718
  dispatch({ type: "SET_SUB_INDICATORS", indicators: newIndicators });
719
+ undoRedoListenerRef.current?.({
720
+ type: "indicator_toggled",
721
+ data: { name, wasActive: true, isMain: false, paneId }
722
+ });
607
723
  },
608
- [state.chart, state.subIndicators, dispatch]
724
+ [state.chart, state.subIndicators, dispatch, undoRedoListenerRef]
609
725
  );
610
726
  const toggleMainIndicator = useCallback(
611
727
  (name) => {
@@ -651,9 +767,15 @@ function useIndicators() {
651
767
  const moveToMain = useCallback(
652
768
  (name) => {
653
769
  if (!state.chart) return;
770
+ const prevCalcParams = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.calcParams;
654
771
  state.chart.removeIndicator({ id: `sub_${name}` });
655
772
  state.chart.createIndicator(
656
- { name, id: `main_${name}`, paneId: "candle_pane" },
773
+ {
774
+ name,
775
+ id: `main_${name}`,
776
+ paneId: "candle_pane",
777
+ ...prevCalcParams ? { calcParams: prevCalcParams } : {}
778
+ },
657
779
  true,
658
780
  { id: "candle_pane" }
659
781
  );
@@ -670,9 +792,14 @@ function useIndicators() {
670
792
  const moveToSub = useCallback(
671
793
  (name) => {
672
794
  if (!state.chart) return;
795
+ const prevCalcParams = state.chart.getIndicators({ id: `main_${name}` })?.[0]?.calcParams;
673
796
  state.chart.removeIndicator({ id: `main_${name}` });
674
797
  const subId = `sub_${name}`;
675
- state.chart.createIndicator({ name, id: subId });
798
+ state.chart.createIndicator({
799
+ name,
800
+ id: subId,
801
+ ...prevCalcParams ? { calcParams: prevCalcParams } : {}
802
+ });
676
803
  const paneId = state.chart.getIndicators({ id: subId })?.[0]?.paneId ?? "";
677
804
  const newMain = state.mainIndicators.filter((n) => n !== name);
678
805
  dispatch({ type: "SET_MAIN_INDICATORS", indicators: newMain });
@@ -691,6 +818,90 @@ function useIndicators() {
691
818
  (name) => name in state.subIndicators,
692
819
  [state.subIndicators]
693
820
  );
821
+ const collapseSubIndicator = useCallback(
822
+ (name) => {
823
+ const paneId = state.subIndicators[name];
824
+ if (!state.chart || !paneId) return;
825
+ const currentSize = state.chart.getSize?.(paneId);
826
+ if (currentSize?.height && currentSize.height > COLLAPSED_HEIGHT) {
827
+ paneHeightsRef.current[paneId] = currentSize.height;
828
+ }
829
+ collapsedPanesRef.current.add(paneId);
830
+ state.chart.setPaneOptions?.({
831
+ id: paneId,
832
+ height: COLLAPSED_HEIGHT
833
+ });
834
+ state.chart.overrideIndicator({ name, id: `sub_${name}`, visible: false });
835
+ },
836
+ [state.chart, state.subIndicators]
837
+ );
838
+ const expandSubIndicator = useCallback(
839
+ (name) => {
840
+ const paneId = state.subIndicators[name];
841
+ if (!state.chart || !paneId) return;
842
+ const savedHeight = paneHeightsRef.current[paneId] ?? 100;
843
+ collapsedPanesRef.current.delete(paneId);
844
+ state.chart.setPaneOptions?.({
845
+ id: paneId,
846
+ height: savedHeight
847
+ });
848
+ state.chart.overrideIndicator({ name, id: `sub_${name}`, visible: true });
849
+ },
850
+ [state.chart, state.subIndicators]
851
+ );
852
+ const isSubIndicatorCollapsed = useCallback(
853
+ (name) => {
854
+ const paneId = state.subIndicators[name];
855
+ return paneId ? collapsedPanesRef.current.has(paneId) : false;
856
+ },
857
+ [state.subIndicators]
858
+ );
859
+ const reorderSubIndicator = useCallback(
860
+ (name, direction) => {
861
+ if (!state.chart) return;
862
+ const subNames = Object.keys(state.subIndicators);
863
+ const idx = subNames.indexOf(name);
864
+ if (idx === -1) return;
865
+ const swapIdx = direction === "up" ? idx - 1 : idx + 1;
866
+ if (swapIdx < 0 || swapIdx >= subNames.length) return;
867
+ const subStates = subNames.map((n) => {
868
+ const id = `sub_${n}`;
869
+ const indicator = state.chart.getIndicators({ id })?.[0];
870
+ return {
871
+ name: n,
872
+ calcParams: indicator?.calcParams,
873
+ visible: indicator?.visible ?? true,
874
+ styles: indicator?.styles,
875
+ paneId: state.subIndicators[n]
876
+ };
877
+ });
878
+ [subStates[idx], subStates[swapIdx]] = [subStates[swapIdx], subStates[idx]];
879
+ for (const n of subNames) {
880
+ state.chart.removeIndicator({ id: `sub_${n}` });
881
+ }
882
+ const newSubIndicators = {};
883
+ for (const sub of subStates) {
884
+ const id = `sub_${sub.name}`;
885
+ state.chart.createIndicator({
886
+ name: sub.name,
887
+ id,
888
+ ...sub.calcParams ? { calcParams: sub.calcParams } : {},
889
+ visible: sub.visible
890
+ });
891
+ const paneId = state.chart.getIndicators({ id })?.[0]?.paneId ?? "";
892
+ newSubIndicators[sub.name] = paneId;
893
+ if (sub.styles) {
894
+ state.chart.overrideIndicator({
895
+ name: sub.name,
896
+ paneId,
897
+ styles: sub.styles
898
+ });
899
+ }
900
+ }
901
+ dispatch({ type: "SET_SUB_INDICATORS", indicators: newSubIndicators });
902
+ },
903
+ [state.chart, state.subIndicators, dispatch]
904
+ );
694
905
  return {
695
906
  mainIndicators,
696
907
  subIndicators,
@@ -710,7 +921,11 @@ function useIndicators() {
710
921
  updateIndicatorParams,
711
922
  getIndicatorParams,
712
923
  isMainIndicatorActive,
713
- isSubIndicatorActive
924
+ isSubIndicatorActive,
925
+ collapseSubIndicator,
926
+ expandSubIndicator,
927
+ isSubIndicatorCollapsed,
928
+ reorderSubIndicator
714
929
  };
715
930
  }
716
931
 
@@ -736,7 +951,9 @@ var DRAWING_CATEGORIES = [
736
951
  key: "moreLine",
737
952
  tools: [
738
953
  { name: "priceChannelLine", localeKey: "price_channel_line" },
739
- { name: "parallelStraightLine", localeKey: "parallel_straight_line" }
954
+ { name: "parallelStraightLine", localeKey: "parallel_straight_line" },
955
+ { name: "ray", localeKey: "ray" },
956
+ { name: "parallelChannel", localeKey: "parallel_channel" }
740
957
  ]
741
958
  },
742
959
  {
@@ -760,7 +977,9 @@ var DRAWING_CATEGORIES = [
760
977
  localeKey: "fibonacci_speed_resistance_fan"
761
978
  },
762
979
  { name: "fibonacciExtension", localeKey: "fibonacci_extension" },
763
- { name: "gannBox", localeKey: "gann_box" }
980
+ { name: "fibRetracement", localeKey: "fib_retracement" },
981
+ { name: "gannBox", localeKey: "gann_box" },
982
+ { name: "gannFan", localeKey: "gann_fan" }
764
983
  ]
765
984
  },
766
985
  {
@@ -771,7 +990,27 @@ var DRAWING_CATEGORIES = [
771
990
  { name: "threeWaves", localeKey: "three_waves" },
772
991
  { name: "fiveWaves", localeKey: "five_waves" },
773
992
  { name: "eightWaves", localeKey: "eight_waves" },
774
- { name: "anyWaves", localeKey: "any_waves" }
993
+ { name: "anyWaves", localeKey: "any_waves" },
994
+ { name: "elliottWave", localeKey: "elliott_wave" }
995
+ ]
996
+ },
997
+ {
998
+ key: "measure",
999
+ tools: [
1000
+ { name: "measure", localeKey: "measure" }
1001
+ ]
1002
+ },
1003
+ {
1004
+ key: "position",
1005
+ tools: [
1006
+ { name: "longPosition", localeKey: "long_position" },
1007
+ { name: "shortPosition", localeKey: "short_position" }
1008
+ ]
1009
+ },
1010
+ {
1011
+ key: "annotation",
1012
+ tools: [
1013
+ { name: "brush", localeKey: "brush" }
775
1014
  ]
776
1015
  }
777
1016
  ];
@@ -780,10 +1019,22 @@ var DRAWING_CATEGORIES = [
780
1019
  var DRAWING_GROUP_ID = "drawing_tools";
781
1020
  function useDrawingTools() {
782
1021
  const { state } = useKlinechartsUI();
1022
+ const { undoRedoListenerRef } = useKlinechartsUIDispatch();
783
1023
  const [activeTool, setActiveTool] = useState(null);
784
1024
  const [magnetMode, setMagnetModeState] = useState("normal");
785
1025
  const [isLocked, setIsLocked] = useState(false);
786
1026
  const [isVisible, setIsVisible] = useState(true);
1027
+ const [autoRetrigger, setAutoRetrigger] = useState(true);
1028
+ const activeToolRef = useRef(activeTool);
1029
+ activeToolRef.current = activeTool;
1030
+ const autoRetriggerRef = useRef(autoRetrigger);
1031
+ autoRetriggerRef.current = autoRetrigger;
1032
+ const isLockedRef = useRef(isLocked);
1033
+ isLockedRef.current = isLocked;
1034
+ const isVisibleRef = useRef(isVisible);
1035
+ isVisibleRef.current = isVisible;
1036
+ const magnetModeRef = useRef(magnetMode);
1037
+ magnetModeRef.current = magnetMode;
787
1038
  const categories = useMemo(
788
1039
  () => DRAWING_CATEGORIES.map((cat) => ({
789
1040
  key: cat.key,
@@ -794,18 +1045,45 @@ function useDrawingTools() {
794
1045
  })),
795
1046
  []
796
1047
  );
797
- const selectTool = useCallback(
1048
+ const createOverlayForTool = useCallback(
798
1049
  (name) => {
799
- setActiveTool(name);
1050
+ const mode = magnetModeRef.current === "strong" ? "strong_magnet" : magnetModeRef.current === "weak" ? "weak_magnet" : "normal";
800
1051
  state.chart?.createOverlay({
801
1052
  name,
802
1053
  groupId: DRAWING_GROUP_ID,
803
- lock: isLocked,
804
- visible: isVisible,
805
- mode: magnetMode === "strong" ? "strong_magnet" : magnetMode === "weak" ? "weak_magnet" : "normal"
1054
+ lock: isLockedRef.current,
1055
+ visible: isVisibleRef.current,
1056
+ mode,
1057
+ onDrawEnd: (event) => {
1058
+ const o = event.overlay;
1059
+ undoRedoListenerRef.current?.({
1060
+ type: "overlay_added",
1061
+ data: {
1062
+ id: o.id,
1063
+ overlayData: {
1064
+ name: o.name,
1065
+ points: o.points,
1066
+ styles: o.styles,
1067
+ extendData: o.extendData
1068
+ }
1069
+ }
1070
+ });
1071
+ if (autoRetriggerRef.current && activeToolRef.current === name) {
1072
+ requestAnimationFrame(() => {
1073
+ createOverlayForTool(name);
1074
+ });
1075
+ }
1076
+ }
806
1077
  });
807
1078
  },
808
- [state.chart, isLocked, isVisible, magnetMode]
1079
+ [state.chart, undoRedoListenerRef]
1080
+ );
1081
+ const selectTool = useCallback(
1082
+ (name) => {
1083
+ setActiveTool(name);
1084
+ createOverlayForTool(name);
1085
+ },
1086
+ [createOverlayForTool]
809
1087
  );
810
1088
  const clearActiveTool = useCallback(() => {
811
1089
  setActiveTool(null);
@@ -853,21 +1131,37 @@ function useDrawingTools() {
853
1131
  }
854
1132
  }, [state.chart, isVisible]);
855
1133
  const removeAllDrawings = useCallback(() => {
1134
+ const overlays2 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
1135
+ if (overlays2 && overlays2.length > 0) {
1136
+ const snapshot = overlays2.map((o) => ({
1137
+ name: o.name,
1138
+ id: o.id,
1139
+ points: o.points,
1140
+ styles: o.styles,
1141
+ extendData: o.extendData
1142
+ }));
1143
+ undoRedoListenerRef.current?.({
1144
+ type: "overlays_removed",
1145
+ data: { overlays: snapshot }
1146
+ });
1147
+ }
856
1148
  state.chart?.removeOverlay({ groupId: DRAWING_GROUP_ID });
857
1149
  setActiveTool(null);
858
- }, [state.chart]);
1150
+ }, [state.chart, undoRedoListenerRef]);
859
1151
  return {
860
1152
  categories,
861
1153
  activeTool,
862
1154
  magnetMode,
863
1155
  isLocked,
864
1156
  isVisible,
1157
+ autoRetrigger,
865
1158
  selectTool,
866
1159
  clearActiveTool,
867
1160
  setMagnetMode,
868
1161
  toggleLock,
869
1162
  toggleVisibility,
870
- removeAllDrawings
1163
+ removeAllDrawings,
1164
+ setAutoRetrigger
871
1165
  };
872
1166
  }
873
1167
 
@@ -1259,12 +1553,12 @@ function useOrderLines() {
1259
1553
  if (onPriceChange) {
1260
1554
  callbacksRef.current.set(id, onPriceChange);
1261
1555
  }
1262
- const dataCount = state.chart.getDataList().length;
1263
- const anchorIndex = dataCount > 0 ? dataCount - 1 : 0;
1556
+ const dataList = state.chart.getDataList();
1557
+ const anchorTimestamp = dataList.length > 0 ? dataList[dataList.length - 1].timestamp : Date.now();
1264
1558
  state.chart.createOverlay({
1265
1559
  name: "orderLine",
1266
1560
  id,
1267
- points: [{ dataIndex: anchorIndex, value: price }],
1561
+ points: [{ timestamp: anchorTimestamp, value: price }],
1268
1562
  extendData,
1269
1563
  lock: !draggable,
1270
1564
  mode: "normal",
@@ -1288,11 +1582,11 @@ function useOrderLines() {
1288
1582
  if (onPriceChange !== void 0) {
1289
1583
  callbacksRef.current.set(id, onPriceChange);
1290
1584
  }
1291
- const dataCount = state.chart.getDataList().length;
1292
- const anchorIndex = dataCount > 0 ? dataCount - 1 : 0;
1585
+ const dataList = state.chart.getDataList();
1586
+ const anchorTimestamp = dataList.length > 0 ? dataList[dataList.length - 1].timestamp : Date.now();
1293
1587
  state.chart.overrideOverlay({
1294
1588
  id,
1295
- ...price != null ? { points: [{ dataIndex: anchorIndex, value: price }] } : {},
1589
+ ...price != null ? { points: [{ timestamp: anchorTimestamp, value: price }] } : {},
1296
1590
  ...draggable != null ? { lock: !draggable } : {},
1297
1591
  extendData: {
1298
1592
  ...existing.extendData,
@@ -1320,6 +1614,1496 @@ function useOrderLines() {
1320
1614
  removeAllOrderLines
1321
1615
  };
1322
1616
  }
1617
+ var DRAWING_GROUP_ID2 = "drawing_tools";
1618
+ function useUndoRedo() {
1619
+ const { state, dispatch } = useKlinechartsUI();
1620
+ const { undoRedoListenerRef } = useKlinechartsUIDispatch();
1621
+ const [undoStack, setUndoStack] = useState([]);
1622
+ const [redoStack, setRedoStack] = useState([]);
1623
+ const isProcessingRef = useRef(false);
1624
+ const canUndo = undoStack.length > 0;
1625
+ const canRedo = redoStack.length > 0;
1626
+ const pushAction = useCallback((action) => {
1627
+ if (isProcessingRef.current) return;
1628
+ setUndoStack((prev) => [...prev, action]);
1629
+ setRedoStack([]);
1630
+ }, []);
1631
+ useEffect(() => {
1632
+ undoRedoListenerRef.current = pushAction;
1633
+ return () => {
1634
+ undoRedoListenerRef.current = null;
1635
+ };
1636
+ }, [pushAction, undoRedoListenerRef]);
1637
+ const clear = useCallback(() => {
1638
+ setUndoStack([]);
1639
+ setRedoStack([]);
1640
+ }, []);
1641
+ const undo = useCallback(() => {
1642
+ if (undoStack.length === 0 || !state.chart || isProcessingRef.current)
1643
+ return;
1644
+ isProcessingRef.current = true;
1645
+ const action = undoStack[undoStack.length - 1];
1646
+ setUndoStack((prev) => prev.slice(0, -1));
1647
+ try {
1648
+ switch (action.type) {
1649
+ case "overlay_added": {
1650
+ const { id, overlayData } = action.data;
1651
+ state.chart.removeOverlay({ id });
1652
+ setRedoStack((prev) => [
1653
+ ...prev,
1654
+ {
1655
+ type: "overlay_added",
1656
+ data: { id, overlayData }
1657
+ }
1658
+ ]);
1659
+ break;
1660
+ }
1661
+ case "overlays_removed": {
1662
+ const { overlays: overlays2 } = action.data;
1663
+ const restoredIds = [];
1664
+ for (const overlay of overlays2) {
1665
+ const newId = state.chart.createOverlay({
1666
+ ...overlay,
1667
+ groupId: DRAWING_GROUP_ID2
1668
+ });
1669
+ if (typeof newId === "string") {
1670
+ restoredIds.push(newId);
1671
+ }
1672
+ }
1673
+ setRedoStack((prev) => [
1674
+ ...prev,
1675
+ {
1676
+ type: "overlays_removed",
1677
+ data: {
1678
+ overlays: overlays2.map((o, i) => ({
1679
+ ...o,
1680
+ id: restoredIds[i]
1681
+ }))
1682
+ }
1683
+ }
1684
+ ]);
1685
+ break;
1686
+ }
1687
+ case "indicator_toggled": {
1688
+ const { name, wasActive, isMain, paneId } = action.data;
1689
+ if (wasActive) {
1690
+ if (isMain) {
1691
+ state.chart.createIndicator(
1692
+ { name, id: `main_${name}`, paneId: "candle_pane" },
1693
+ true,
1694
+ { id: "candle_pane" }
1695
+ );
1696
+ dispatch({
1697
+ type: "SET_MAIN_INDICATORS",
1698
+ indicators: [...state.mainIndicators, name]
1699
+ });
1700
+ } else {
1701
+ state.chart.createIndicator({ name, id: `sub_${name}` });
1702
+ const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
1703
+ dispatch({
1704
+ type: "SET_SUB_INDICATORS",
1705
+ indicators: {
1706
+ ...state.subIndicators,
1707
+ [name]: newPaneId
1708
+ }
1709
+ });
1710
+ }
1711
+ } else {
1712
+ if (isMain) {
1713
+ state.chart.removeIndicator({ id: `main_${name}` });
1714
+ dispatch({
1715
+ type: "SET_MAIN_INDICATORS",
1716
+ indicators: state.mainIndicators.filter(
1717
+ (n) => n !== name
1718
+ )
1719
+ });
1720
+ } else {
1721
+ state.chart.removeIndicator({ id: `sub_${name}` });
1722
+ const newSub = { ...state.subIndicators };
1723
+ delete newSub[name];
1724
+ dispatch({
1725
+ type: "SET_SUB_INDICATORS",
1726
+ indicators: newSub
1727
+ });
1728
+ }
1729
+ }
1730
+ setRedoStack((prev) => [
1731
+ ...prev,
1732
+ {
1733
+ type: "indicator_toggled",
1734
+ data: {
1735
+ name,
1736
+ wasActive: !wasActive,
1737
+ isMain,
1738
+ paneId
1739
+ }
1740
+ }
1741
+ ]);
1742
+ break;
1743
+ }
1744
+ }
1745
+ } finally {
1746
+ isProcessingRef.current = false;
1747
+ }
1748
+ }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
1749
+ const redo = useCallback(() => {
1750
+ if (redoStack.length === 0 || !state.chart || isProcessingRef.current)
1751
+ return;
1752
+ isProcessingRef.current = true;
1753
+ const action = redoStack[redoStack.length - 1];
1754
+ setRedoStack((prev) => prev.slice(0, -1));
1755
+ try {
1756
+ switch (action.type) {
1757
+ case "overlay_added": {
1758
+ const { overlayData } = action.data;
1759
+ const newId = state.chart.createOverlay({
1760
+ ...overlayData,
1761
+ groupId: DRAWING_GROUP_ID2
1762
+ });
1763
+ setUndoStack((prev) => [
1764
+ ...prev,
1765
+ {
1766
+ type: "overlay_added",
1767
+ data: {
1768
+ id: typeof newId === "string" ? newId : action.data.id,
1769
+ overlayData
1770
+ }
1771
+ }
1772
+ ]);
1773
+ break;
1774
+ }
1775
+ case "overlays_removed": {
1776
+ const { overlays: overlays2 } = action.data;
1777
+ const snapshotOverlays = overlays2.map((o) => {
1778
+ const allOverlays = state.chart?.getOverlays({ id: o.id });
1779
+ const actual = allOverlays?.[0];
1780
+ return actual ? {
1781
+ name: actual.name,
1782
+ points: actual.points,
1783
+ styles: actual.styles,
1784
+ extendData: actual.extendData
1785
+ } : o;
1786
+ });
1787
+ for (const overlay of overlays2) {
1788
+ state.chart.removeOverlay({ id: overlay.id });
1789
+ }
1790
+ setUndoStack((prev) => [
1791
+ ...prev,
1792
+ {
1793
+ type: "overlays_removed",
1794
+ data: { overlays: snapshotOverlays }
1795
+ }
1796
+ ]);
1797
+ break;
1798
+ }
1799
+ case "indicator_toggled": {
1800
+ const { name, wasActive, isMain } = action.data;
1801
+ if (wasActive) {
1802
+ if (isMain) {
1803
+ state.chart.createIndicator(
1804
+ { name, id: `main_${name}`, paneId: "candle_pane" },
1805
+ true,
1806
+ { id: "candle_pane" }
1807
+ );
1808
+ dispatch({
1809
+ type: "SET_MAIN_INDICATORS",
1810
+ indicators: [...state.mainIndicators, name]
1811
+ });
1812
+ } else {
1813
+ state.chart.createIndicator({ name, id: `sub_${name}` });
1814
+ const newPaneId = state.chart.getIndicators({ id: `sub_${name}` })?.[0]?.paneId ?? "";
1815
+ dispatch({
1816
+ type: "SET_SUB_INDICATORS",
1817
+ indicators: {
1818
+ ...state.subIndicators,
1819
+ [name]: newPaneId
1820
+ }
1821
+ });
1822
+ }
1823
+ } else {
1824
+ if (isMain) {
1825
+ state.chart.removeIndicator({ id: `main_${name}` });
1826
+ dispatch({
1827
+ type: "SET_MAIN_INDICATORS",
1828
+ indicators: state.mainIndicators.filter(
1829
+ (n) => n !== name
1830
+ )
1831
+ });
1832
+ } else {
1833
+ state.chart.removeIndicator({ id: `sub_${name}` });
1834
+ const newSub = { ...state.subIndicators };
1835
+ delete newSub[name];
1836
+ dispatch({
1837
+ type: "SET_SUB_INDICATORS",
1838
+ indicators: newSub
1839
+ });
1840
+ }
1841
+ }
1842
+ setUndoStack((prev) => [
1843
+ ...prev,
1844
+ {
1845
+ type: "indicator_toggled",
1846
+ data: {
1847
+ name,
1848
+ wasActive: !wasActive,
1849
+ isMain
1850
+ }
1851
+ }
1852
+ ]);
1853
+ break;
1854
+ }
1855
+ }
1856
+ } finally {
1857
+ isProcessingRef.current = false;
1858
+ }
1859
+ }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, dispatch]);
1860
+ useEffect(() => {
1861
+ const handleKeyDown = (e) => {
1862
+ const isCtrlOrMeta = e.ctrlKey || e.metaKey;
1863
+ if (!isCtrlOrMeta) return;
1864
+ if (e.key === "z" && !e.shiftKey) {
1865
+ e.preventDefault();
1866
+ undo();
1867
+ } else if (e.key === "y" || e.key === "z" && e.shiftKey) {
1868
+ e.preventDefault();
1869
+ redo();
1870
+ }
1871
+ };
1872
+ window.addEventListener("keydown", handleKeyDown);
1873
+ return () => window.removeEventListener("keydown", handleKeyDown);
1874
+ }, [undo, redo]);
1875
+ return {
1876
+ canUndo,
1877
+ canRedo,
1878
+ undo,
1879
+ redo,
1880
+ pushAction,
1881
+ clear
1882
+ };
1883
+ }
1884
+ var STORAGE_KEY_PREFIX = "klinecharts_layout:";
1885
+ var INDEX_KEY = "klinecharts_layout_index";
1886
+ var STATE_VERSION = "1.0";
1887
+ function generateId() {
1888
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
1889
+ /[xy]/g,
1890
+ (c) => {
1891
+ const r = Math.random() * 16 | 0;
1892
+ return (c === "x" ? r : r & 3 | 8).toString(16);
1893
+ }
1894
+ );
1895
+ }
1896
+ function getLayoutIds() {
1897
+ try {
1898
+ const raw = localStorage.getItem(INDEX_KEY);
1899
+ return raw ? JSON.parse(raw) : [];
1900
+ } catch {
1901
+ return [];
1902
+ }
1903
+ }
1904
+ function getEntry(id) {
1905
+ try {
1906
+ const raw = localStorage.getItem(STORAGE_KEY_PREFIX + id);
1907
+ return raw ? JSON.parse(raw) : null;
1908
+ } catch {
1909
+ return null;
1910
+ }
1911
+ }
1912
+ function useLayoutManager() {
1913
+ const { state, dispatch } = useKlinechartsUI();
1914
+ const [layouts, setLayouts] = useState([]);
1915
+ const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
1916
+ const autoSaveTimerRef = useRef(null);
1917
+ const autoSaveIdRef = useRef(null);
1918
+ const refreshLayouts = useCallback(() => {
1919
+ const entries = getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null);
1920
+ setLayouts(entries);
1921
+ }, []);
1922
+ useEffect(() => {
1923
+ refreshLayouts();
1924
+ }, [refreshLayouts]);
1925
+ const serializeState = useCallback(() => {
1926
+ const chart = state.chart;
1927
+ if (!chart) return null;
1928
+ const indicators2 = [];
1929
+ const indicatorMap = chart.getIndicatorByPaneId?.();
1930
+ if (indicatorMap) {
1931
+ indicatorMap.forEach((paneIndicators, paneId) => {
1932
+ paneIndicators.forEach((indicator) => {
1933
+ indicators2.push({
1934
+ paneId,
1935
+ name: indicator.name,
1936
+ calcParams: indicator.calcParams,
1937
+ visible: indicator.visible,
1938
+ ...indicator.styles ? { styles: indicator.styles } : {}
1939
+ });
1940
+ });
1941
+ });
1942
+ }
1943
+ const drawings = [];
1944
+ const allOverlays = chart.getOverlays({});
1945
+ if (allOverlays) {
1946
+ for (const overlay of allOverlays) {
1947
+ drawings.push({
1948
+ name: overlay.name,
1949
+ points: overlay.points,
1950
+ styles: overlay.styles,
1951
+ extendData: overlay.extendData
1952
+ });
1953
+ }
1954
+ }
1955
+ return {
1956
+ version: STATE_VERSION,
1957
+ meta: {
1958
+ symbol: state.symbol?.ticker ?? "",
1959
+ period: state.period?.label ?? "",
1960
+ timestamp: Date.now(),
1961
+ lastModified: Date.now()
1962
+ },
1963
+ indicators: indicators2,
1964
+ drawings
1965
+ };
1966
+ }, [state.chart, state.symbol, state.period]);
1967
+ const saveLayout = useCallback(
1968
+ (name) => {
1969
+ const chartState = serializeState();
1970
+ if (!chartState) return null;
1971
+ const id = generateId();
1972
+ const now = Date.now();
1973
+ const entry = {
1974
+ id,
1975
+ name: name.trim() || `Layout ${new Date(now).toLocaleString()}`,
1976
+ symbol: chartState.meta.symbol,
1977
+ period: chartState.meta.period,
1978
+ timestamp: now,
1979
+ lastModified: now,
1980
+ state: chartState
1981
+ };
1982
+ localStorage.setItem(
1983
+ STORAGE_KEY_PREFIX + id,
1984
+ JSON.stringify(entry)
1985
+ );
1986
+ const ids = getLayoutIds();
1987
+ if (!ids.includes(id)) {
1988
+ ids.push(id);
1989
+ localStorage.setItem(INDEX_KEY, JSON.stringify(ids));
1990
+ }
1991
+ refreshLayouts();
1992
+ return id;
1993
+ },
1994
+ [serializeState, refreshLayouts]
1995
+ );
1996
+ const loadLayout = useCallback(
1997
+ (id) => {
1998
+ const entry = getEntry(id);
1999
+ if (!entry || !state.chart) return false;
2000
+ const chartState = entry.state;
2001
+ if (chartState.version !== STATE_VERSION) return false;
2002
+ const chart = state.chart;
2003
+ chart.removeOverlay();
2004
+ const indicatorMap = chart.getIndicatorByPaneId?.();
2005
+ if (indicatorMap) {
2006
+ indicatorMap.forEach((_paneIndicators, paneId) => {
2007
+ _paneIndicators.forEach((_, indicatorName) => {
2008
+ chart.removeIndicator({ paneId, name: indicatorName });
2009
+ });
2010
+ });
2011
+ }
2012
+ const newMainIndicators = [];
2013
+ const newSubIndicators = {};
2014
+ if (chartState.indicators) {
2015
+ for (const ind of chartState.indicators) {
2016
+ chart.createIndicator(
2017
+ {
2018
+ name: ind.name,
2019
+ calcParams: ind.calcParams,
2020
+ visible: ind.visible
2021
+ },
2022
+ ind.paneId !== "candle_pane",
2023
+ { id: ind.paneId }
2024
+ );
2025
+ if (ind.styles) {
2026
+ chart.overrideIndicator({
2027
+ name: ind.name,
2028
+ paneId: ind.paneId,
2029
+ styles: ind.styles
2030
+ });
2031
+ }
2032
+ if (ind.paneId === "candle_pane") {
2033
+ newMainIndicators.push(ind.name);
2034
+ } else {
2035
+ newSubIndicators[ind.name] = ind.paneId;
2036
+ }
2037
+ }
2038
+ }
2039
+ dispatch({
2040
+ type: "SET_MAIN_INDICATORS",
2041
+ indicators: newMainIndicators
2042
+ });
2043
+ dispatch({
2044
+ type: "SET_SUB_INDICATORS",
2045
+ indicators: newSubIndicators
2046
+ });
2047
+ if (chartState.drawings) {
2048
+ for (const drawing of chartState.drawings) {
2049
+ chart.createOverlay({
2050
+ name: drawing.name,
2051
+ points: drawing.points,
2052
+ styles: drawing.styles,
2053
+ extendData: drawing.extendData
2054
+ });
2055
+ }
2056
+ }
2057
+ return true;
2058
+ },
2059
+ [state.chart, dispatch]
2060
+ );
2061
+ const deleteLayout = useCallback(
2062
+ (id) => {
2063
+ localStorage.removeItem(STORAGE_KEY_PREFIX + id);
2064
+ const ids = getLayoutIds().filter((i) => i !== id);
2065
+ localStorage.setItem(INDEX_KEY, JSON.stringify(ids));
2066
+ refreshLayouts();
2067
+ },
2068
+ [refreshLayouts]
2069
+ );
2070
+ const renameLayout = useCallback(
2071
+ (id, name) => {
2072
+ const entry = getEntry(id);
2073
+ if (!entry) return false;
2074
+ const updated = {
2075
+ ...entry,
2076
+ name: name.trim(),
2077
+ lastModified: Date.now()
2078
+ };
2079
+ localStorage.setItem(
2080
+ STORAGE_KEY_PREFIX + id,
2081
+ JSON.stringify(updated)
2082
+ );
2083
+ refreshLayouts();
2084
+ return true;
2085
+ },
2086
+ [refreshLayouts]
2087
+ );
2088
+ useEffect(() => {
2089
+ if (!autoSaveEnabled || !state.chart) return;
2090
+ if (autoSaveTimerRef.current) {
2091
+ clearTimeout(autoSaveTimerRef.current);
2092
+ }
2093
+ autoSaveTimerRef.current = setTimeout(() => {
2094
+ const chartState = serializeState();
2095
+ if (!chartState) return;
2096
+ if (autoSaveIdRef.current) {
2097
+ const entry = getEntry(autoSaveIdRef.current);
2098
+ if (entry) {
2099
+ const updated = {
2100
+ ...entry,
2101
+ lastModified: Date.now(),
2102
+ state: chartState
2103
+ };
2104
+ localStorage.setItem(
2105
+ STORAGE_KEY_PREFIX + autoSaveIdRef.current,
2106
+ JSON.stringify(updated)
2107
+ );
2108
+ refreshLayouts();
2109
+ }
2110
+ } else {
2111
+ const id = generateId();
2112
+ const now = Date.now();
2113
+ const entry = {
2114
+ id,
2115
+ name: "Auto-save",
2116
+ symbol: chartState.meta.symbol,
2117
+ period: chartState.meta.period,
2118
+ timestamp: now,
2119
+ lastModified: now,
2120
+ state: chartState
2121
+ };
2122
+ localStorage.setItem(
2123
+ STORAGE_KEY_PREFIX + id,
2124
+ JSON.stringify(entry)
2125
+ );
2126
+ const ids = getLayoutIds();
2127
+ ids.push(id);
2128
+ localStorage.setItem(INDEX_KEY, JSON.stringify(ids));
2129
+ autoSaveIdRef.current = id;
2130
+ refreshLayouts();
2131
+ }
2132
+ }, 5e3);
2133
+ return () => {
2134
+ if (autoSaveTimerRef.current) {
2135
+ clearTimeout(autoSaveTimerRef.current);
2136
+ }
2137
+ };
2138
+ }, [
2139
+ autoSaveEnabled,
2140
+ state.chart,
2141
+ state.mainIndicators,
2142
+ state.subIndicators,
2143
+ serializeState,
2144
+ refreshLayouts
2145
+ ]);
2146
+ return {
2147
+ layouts,
2148
+ saveLayout,
2149
+ loadLayout,
2150
+ deleteLayout,
2151
+ renameLayout,
2152
+ refreshLayouts,
2153
+ autoSaveEnabled,
2154
+ setAutoSaveEnabled
2155
+ };
2156
+ }
2157
+ var DEFAULT_SCRIPT = `// Custom Indicator \u2014 Pine Script Style
2158
+ // Available: TA, dataList, params
2159
+
2160
+ const period = params[0] ?? 14
2161
+ const closes = dataList.map(d => d.close)
2162
+ const highs = dataList.map(d => d.high)
2163
+ const lows = dataList.map(d => d.low)
2164
+
2165
+ // Example: RSI + Bollinger Bands
2166
+ const rsi = TA.rsi(closes, period)
2167
+ const boll = TA.bollinger(closes, period, 2)
2168
+
2169
+ // Return one object per candle \u2014 each key = one line on chart
2170
+ return rsi.map((v, i) => ({
2171
+ rsi: v,
2172
+ upper: boll.upper[i],
2173
+ mid: boll.mid[i],
2174
+ lower: boll.lower[i],
2175
+ }))`;
2176
+ var SERIES_COLORS = [
2177
+ "#2962FF",
2178
+ "#FF6D00",
2179
+ "#00BCD4",
2180
+ "#E91E63",
2181
+ "#76FF03",
2182
+ "#FFD600"
2183
+ ];
2184
+ var SHADOW_KEYS = [
2185
+ "fetch",
2186
+ "XMLHttpRequest",
2187
+ "WebSocket",
2188
+ "Worker",
2189
+ "SharedWorker",
2190
+ "importScripts",
2191
+ "self",
2192
+ "caches",
2193
+ "indexedDB"
2194
+ ];
2195
+ var scriptCounter = 0;
2196
+ function useScriptEditor() {
2197
+ const { state } = useKlinechartsUI();
2198
+ const [code, setCode] = useState(DEFAULT_SCRIPT);
2199
+ const [scriptName, setScriptName] = useState("");
2200
+ const [params, setParams] = useState("14");
2201
+ const [placement, setPlacement] = useState("sub");
2202
+ const [error, setError] = useState("");
2203
+ const [status, setStatus] = useState("");
2204
+ const [isRunning, setIsRunning] = useState(false);
2205
+ const activeIdRef = useRef(null);
2206
+ const activeNameRef = useRef(null);
2207
+ const hasActiveScript = activeNameRef.current !== null;
2208
+ const runScript = useCallback(() => {
2209
+ const chart = state.chart;
2210
+ if (!chart) {
2211
+ setError("Chart not initialized");
2212
+ return;
2213
+ }
2214
+ setError("");
2215
+ setStatus("Running...");
2216
+ setIsRunning(true);
2217
+ try {
2218
+ const parsedParams = params.split(",").map((s) => parseFloat(s.trim())).filter((n) => !isNaN(n));
2219
+ const dataList = chart.getDataList?.() ?? [];
2220
+ const allArgs = [
2221
+ ...SHADOW_KEYS,
2222
+ "TA",
2223
+ "dataList",
2224
+ "params",
2225
+ `"use strict";
2226
+ ${code}`
2227
+ ];
2228
+ const fn = new Function(...allArgs);
2229
+ const shadowValues = SHADOW_KEYS.map(() => void 0);
2230
+ const result = fn(...shadowValues, TA_default, dataList, parsedParams);
2231
+ if (!Array.isArray(result)) {
2232
+ throw new Error("Script must return an Array.");
2233
+ }
2234
+ const sample = result.find((v) => v !== null && v !== void 0);
2235
+ const seriesKeys = sample ? Object.keys(sample) : ["value"];
2236
+ if (seriesKeys.length === 0) {
2237
+ throw new Error("Returned objects have no keys.");
2238
+ }
2239
+ scriptCounter++;
2240
+ const indicatorName = `_custom_script_${scriptCounter}`;
2241
+ const figures = seriesKeys.map((key, i) => ({
2242
+ key,
2243
+ title: `${key}: `,
2244
+ type: "line",
2245
+ styles: () => ({ color: SERIES_COLORS[i % SERIES_COLORS.length] })
2246
+ }));
2247
+ registerIndicator({
2248
+ name: indicatorName,
2249
+ shortName: (scriptName.trim() || `Script #${scriptCounter}`) + (placement === "main" ? " (Main)" : " (Sub)"),
2250
+ calcParams: parsedParams,
2251
+ figures,
2252
+ calc: () => result,
2253
+ extendData: {
2254
+ isCustomScript: true,
2255
+ code,
2256
+ placement
2257
+ }
2258
+ });
2259
+ if (activeNameRef.current) {
2260
+ try {
2261
+ chart.removeIndicator({
2262
+ id: activeIdRef.current === "candle_pane" ? "candle_pane" : activeIdRef.current,
2263
+ name: activeNameRef.current
2264
+ });
2265
+ } catch {
2266
+ }
2267
+ }
2268
+ let paneId = null;
2269
+ if (placement === "main") {
2270
+ paneId = chart.createIndicator(
2271
+ { name: indicatorName },
2272
+ true,
2273
+ { id: "candle_pane" }
2274
+ );
2275
+ } else {
2276
+ paneId = chart.createIndicator({ name: indicatorName });
2277
+ }
2278
+ activeIdRef.current = typeof paneId === "string" ? paneId : null;
2279
+ activeNameRef.current = indicatorName;
2280
+ const title = scriptName.trim() || `Script #${scriptCounter}`;
2281
+ setStatus(
2282
+ `${title} applied to ${placement === "main" ? "Main Chart" : "Sub Pane"} \u2014 ${seriesKeys.length} series: ${seriesKeys.join(", ")}`
2283
+ );
2284
+ } catch (e) {
2285
+ setError(e?.message ?? String(e));
2286
+ } finally {
2287
+ setIsRunning(false);
2288
+ }
2289
+ }, [state.chart, code, scriptName, params, placement]);
2290
+ const removeScript = useCallback(() => {
2291
+ const chart = state.chart;
2292
+ if (chart && activeNameRef.current && activeIdRef.current) {
2293
+ try {
2294
+ chart.removeIndicator({
2295
+ id: activeIdRef.current,
2296
+ name: activeNameRef.current
2297
+ });
2298
+ } catch {
2299
+ }
2300
+ activeIdRef.current = null;
2301
+ activeNameRef.current = null;
2302
+ setStatus("Indicator removed.");
2303
+ }
2304
+ }, [state.chart]);
2305
+ const resetCode = useCallback(() => {
2306
+ setCode(DEFAULT_SCRIPT);
2307
+ setError("");
2308
+ setStatus("");
2309
+ }, []);
2310
+ const exportScript = useCallback(() => {
2311
+ const blob = new Blob([code], {
2312
+ type: "text/javascript;charset=utf-8"
2313
+ });
2314
+ const url = URL.createObjectURL(blob);
2315
+ const a = document.createElement("a");
2316
+ a.href = url;
2317
+ a.download = `${scriptName.trim() || "custom_indicator"}.js`;
2318
+ document.body.appendChild(a);
2319
+ a.click();
2320
+ document.body.removeChild(a);
2321
+ URL.revokeObjectURL(url);
2322
+ setStatus("Script exported.");
2323
+ }, [code, scriptName]);
2324
+ const importScript = useCallback(
2325
+ (file) => {
2326
+ const reader = new FileReader();
2327
+ reader.onload = (e) => {
2328
+ const content = e.target?.result;
2329
+ if (content) {
2330
+ setCode(content);
2331
+ if (!scriptName) {
2332
+ setScriptName(file.name.replace(/\.(js|ts|txt)$/i, ""));
2333
+ }
2334
+ setError("");
2335
+ setStatus(`Loaded ${file.name}`);
2336
+ }
2337
+ };
2338
+ reader.readAsText(file);
2339
+ },
2340
+ [scriptName]
2341
+ );
2342
+ return {
2343
+ code,
2344
+ setCode,
2345
+ scriptName,
2346
+ setScriptName,
2347
+ params,
2348
+ setParams,
2349
+ placement,
2350
+ setPlacement,
2351
+ error,
2352
+ status,
2353
+ isRunning,
2354
+ hasActiveScript,
2355
+ runScript,
2356
+ removeScript,
2357
+ resetCode,
2358
+ exportScript,
2359
+ importScript,
2360
+ defaultScript: DEFAULT_SCRIPT
2361
+ };
2362
+ }
2363
+ function useCrosshair() {
2364
+ const { state } = useKlinechartsUI();
2365
+ const [barData, setBarData] = useState(null);
2366
+ const rafRef = useRef(0);
2367
+ const handler = useCallback(
2368
+ (event) => {
2369
+ if (rafRef.current) {
2370
+ cancelAnimationFrame(rafRef.current);
2371
+ }
2372
+ rafRef.current = requestAnimationFrame(() => {
2373
+ rafRef.current = 0;
2374
+ const klineData = event?.kLineData;
2375
+ if (!klineData) {
2376
+ setBarData(null);
2377
+ return;
2378
+ }
2379
+ const pricePrecision = state.symbol?.pricePrecision ?? 2;
2380
+ const open = klineData.open ?? 0;
2381
+ const close = klineData.close ?? 0;
2382
+ const change = parseFloat((close - open).toFixed(pricePrecision));
2383
+ const changePercent = open !== 0 ? parseFloat((change / open * 100).toFixed(2)) : 0;
2384
+ setBarData({
2385
+ open,
2386
+ high: klineData.high ?? 0,
2387
+ low: klineData.low ?? 0,
2388
+ close,
2389
+ volume: klineData.volume ?? 0,
2390
+ timestamp: klineData.timestamp ?? 0,
2391
+ change,
2392
+ changePercent
2393
+ });
2394
+ });
2395
+ },
2396
+ [state.symbol?.pricePrecision]
2397
+ );
2398
+ useEffect(() => {
2399
+ const chart = state.chart;
2400
+ if (!chart) return;
2401
+ chart.subscribeAction?.("onCrosshairChange", handler);
2402
+ return () => {
2403
+ if (rafRef.current) {
2404
+ cancelAnimationFrame(rafRef.current);
2405
+ rafRef.current = 0;
2406
+ }
2407
+ chart.unsubscribeAction?.("onCrosshairChange", handler);
2408
+ };
2409
+ }, [state.chart, handler]);
2410
+ return { barData };
2411
+ }
2412
+ var alertCounter = 0;
2413
+ function useAlerts() {
2414
+ const { state } = useKlinechartsUI();
2415
+ const [alerts, setAlerts] = useState([]);
2416
+ const callbackRef = useRef(null);
2417
+ const prevCloseRef = useRef(null);
2418
+ const addAlert = useCallback(
2419
+ (price, condition, message) => {
2420
+ const id = `alert_${++alertCounter}`;
2421
+ const alert = {
2422
+ id,
2423
+ price,
2424
+ condition,
2425
+ message,
2426
+ triggered: false
2427
+ };
2428
+ setAlerts((prev) => [...prev, alert]);
2429
+ if (state.chart) {
2430
+ state.chart.createOverlay({
2431
+ name: "horizontalStraightLine",
2432
+ id,
2433
+ groupId: "price_alerts",
2434
+ points: [{ value: price }],
2435
+ styles: {
2436
+ line: {
2437
+ style: "dashed",
2438
+ color: "#ff9800",
2439
+ size: 1
2440
+ }
2441
+ },
2442
+ lock: true
2443
+ });
2444
+ }
2445
+ return id;
2446
+ },
2447
+ [state.chart]
2448
+ );
2449
+ const removeAlert = useCallback(
2450
+ (id) => {
2451
+ setAlerts((prev) => prev.filter((a) => a.id !== id));
2452
+ state.chart?.removeOverlay({ id });
2453
+ },
2454
+ [state.chart]
2455
+ );
2456
+ const clearAlerts = useCallback(() => {
2457
+ setAlerts((prev) => {
2458
+ for (const alert of prev) {
2459
+ state.chart?.removeOverlay({ id: alert.id });
2460
+ }
2461
+ return [];
2462
+ });
2463
+ }, [state.chart]);
2464
+ const onAlertTriggered = useCallback(
2465
+ (callback) => {
2466
+ callbackRef.current = callback;
2467
+ },
2468
+ []
2469
+ );
2470
+ useEffect(() => {
2471
+ if (!state.chart) return;
2472
+ const interval = setInterval(() => {
2473
+ const dataList = state.chart?.getDataList();
2474
+ if (!dataList || dataList.length === 0) return;
2475
+ const lastBar = dataList[dataList.length - 1];
2476
+ const currentClose = lastBar.close;
2477
+ const prevClose = prevCloseRef.current;
2478
+ if (prevClose === null) {
2479
+ prevCloseRef.current = currentClose;
2480
+ return;
2481
+ }
2482
+ prevCloseRef.current = currentClose;
2483
+ setAlerts((prev) => {
2484
+ let changed = false;
2485
+ const next = prev.map((alert) => {
2486
+ if (alert.triggered) return alert;
2487
+ let shouldTrigger = false;
2488
+ if (alert.condition === "crossing_up") {
2489
+ shouldTrigger = prevClose < alert.price && currentClose >= alert.price;
2490
+ } else if (alert.condition === "crossing_down") {
2491
+ shouldTrigger = prevClose > alert.price && currentClose <= alert.price;
2492
+ } else if (alert.condition === "crossing") {
2493
+ shouldTrigger = prevClose < alert.price && currentClose >= alert.price || prevClose > alert.price && currentClose <= alert.price;
2494
+ }
2495
+ if (shouldTrigger) {
2496
+ changed = true;
2497
+ const triggered = { ...alert, triggered: true };
2498
+ callbackRef.current?.(triggered);
2499
+ return triggered;
2500
+ }
2501
+ return alert;
2502
+ });
2503
+ return changed ? next : prev;
2504
+ });
2505
+ }, 1e3);
2506
+ return () => clearInterval(interval);
2507
+ }, [state.chart]);
2508
+ return {
2509
+ alerts,
2510
+ addAlert,
2511
+ removeAlert,
2512
+ clearAlerts,
2513
+ onAlertTriggered
2514
+ };
2515
+ }
2516
+ function useDataExport() {
2517
+ const { state } = useKlinechartsUI();
2518
+ const exportAll = useCallback(
2519
+ (format) => {
2520
+ if (!state.chart) return;
2521
+ const dataList = state.chart.getDataList();
2522
+ if (!dataList || dataList.length === 0) return;
2523
+ downloadData(dataList, format, state.symbol?.ticker);
2524
+ },
2525
+ [state.chart, state.symbol?.ticker]
2526
+ );
2527
+ const exportVisible = useCallback(
2528
+ (format) => {
2529
+ if (!state.chart) return;
2530
+ const dataList = state.chart.getDataList();
2531
+ if (!dataList || dataList.length === 0) return;
2532
+ const visibleRange = state.chart.getVisibleRange?.();
2533
+ if (!visibleRange) {
2534
+ downloadData(dataList, format, state.symbol?.ticker);
2535
+ return;
2536
+ }
2537
+ const { from, to } = visibleRange;
2538
+ const visibleData = dataList.slice(from, to);
2539
+ downloadData(visibleData, format, state.symbol?.ticker);
2540
+ },
2541
+ [state.chart, state.symbol?.ticker]
2542
+ );
2543
+ return { exportAll, exportVisible };
2544
+ }
2545
+ function formatRow(bar) {
2546
+ return {
2547
+ date: new Date(bar.timestamp).toISOString(),
2548
+ open: bar.open,
2549
+ high: bar.high,
2550
+ low: bar.low,
2551
+ close: bar.close,
2552
+ volume: bar.volume ?? 0
2553
+ };
2554
+ }
2555
+ function buildCsv(dataList) {
2556
+ const header = "Date,Open,High,Low,Close,Volume\n";
2557
+ const rows = dataList.map((bar) => {
2558
+ const r = formatRow(bar);
2559
+ return `${r.date},${r.open},${r.high},${r.low},${r.close},${r.volume}`;
2560
+ });
2561
+ return header + rows.join("\n");
2562
+ }
2563
+ function buildJson(dataList) {
2564
+ return JSON.stringify(dataList.map(formatRow), null, 2);
2565
+ }
2566
+ function downloadData(dataList, format, ticker) {
2567
+ const content = format === "csv" ? buildCsv(dataList) : buildJson(dataList);
2568
+ const mimeType = format === "csv" ? "text/csv" : "application/json";
2569
+ const blob = new Blob([content], { type: mimeType });
2570
+ const url = URL.createObjectURL(blob);
2571
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2572
+ const prefix = ticker ?? "chart";
2573
+ const filename = `${prefix}_${date}.${format}`;
2574
+ const link = document.createElement("a");
2575
+ link.href = url;
2576
+ link.download = filename;
2577
+ document.body.appendChild(link);
2578
+ link.click();
2579
+ document.body.removeChild(link);
2580
+ URL.revokeObjectURL(url);
2581
+ }
2582
+ function useWatchlist() {
2583
+ const { state } = useKlinechartsUI();
2584
+ const { dispatch, datafeed } = useKlinechartsUIDispatch();
2585
+ const [items, setItems] = useState([]);
2586
+ const subscriptionsRef = useRef(/* @__PURE__ */ new Map());
2587
+ const activeSymbol = state.symbol?.ticker ?? null;
2588
+ const addSymbol = useCallback(
2589
+ (ticker) => {
2590
+ setItems((prev) => {
2591
+ if (prev.some((item) => item.ticker === ticker)) return prev;
2592
+ return [
2593
+ ...prev,
2594
+ { ticker, lastPrice: null, change: null, changePercent: null }
2595
+ ];
2596
+ });
2597
+ if (subscriptionsRef.current.has(ticker)) return;
2598
+ const symbolInfo = { ticker };
2599
+ const period = state.period;
2600
+ subscriptionsRef.current.set(ticker, { symbolInfo, period });
2601
+ datafeed.subscribe(symbolInfo, period, (bar) => {
2602
+ setItems(
2603
+ (prev) => prev.map((item) => {
2604
+ if (item.ticker !== ticker) return item;
2605
+ const change = bar.close - bar.open;
2606
+ const changePercent = bar.open !== 0 ? change / bar.open * 100 : null;
2607
+ return {
2608
+ ...item,
2609
+ lastPrice: bar.close,
2610
+ change,
2611
+ changePercent
2612
+ };
2613
+ })
2614
+ );
2615
+ });
2616
+ },
2617
+ [datafeed, state.period]
2618
+ );
2619
+ const removeSymbol = useCallback(
2620
+ (ticker) => {
2621
+ const sub = subscriptionsRef.current.get(ticker);
2622
+ if (sub) {
2623
+ datafeed.unsubscribe(sub.symbolInfo, sub.period);
2624
+ subscriptionsRef.current.delete(ticker);
2625
+ }
2626
+ setItems((prev) => prev.filter((item) => item.ticker !== ticker));
2627
+ },
2628
+ [datafeed]
2629
+ );
2630
+ const switchSymbol = useCallback(
2631
+ (ticker) => {
2632
+ dispatch({ type: "SET_SYMBOL", symbol: { ticker } });
2633
+ },
2634
+ [dispatch]
2635
+ );
2636
+ useEffect(() => {
2637
+ return () => {
2638
+ subscriptionsRef.current.forEach((sub) => {
2639
+ datafeed.unsubscribe(sub.symbolInfo, sub.period);
2640
+ });
2641
+ subscriptionsRef.current.clear();
2642
+ };
2643
+ }, [datafeed]);
2644
+ return {
2645
+ items,
2646
+ addSymbol,
2647
+ removeSymbol,
2648
+ switchSymbol,
2649
+ activeSymbol
2650
+ };
2651
+ }
2652
+ function useReplay() {
2653
+ const { state } = useKlinechartsUI();
2654
+ const [isReplaying, setIsReplaying] = useState(false);
2655
+ const [isPaused, setIsPaused] = useState(false);
2656
+ const [speed, setSpeedState] = useState(1);
2657
+ const [barIndex, setBarIndex] = useState(0);
2658
+ const [totalBars, setTotalBars] = useState(0);
2659
+ const intervalRef = useRef(null);
2660
+ const savedDataRef = useRef([]);
2661
+ const currentIndexRef = useRef(0);
2662
+ const clearInterval_ = useCallback(() => {
2663
+ if (intervalRef.current !== null) {
2664
+ clearInterval(intervalRef.current);
2665
+ intervalRef.current = null;
2666
+ }
2667
+ }, []);
2668
+ const addNextBar = useCallback(() => {
2669
+ if (!state.chart) return;
2670
+ const data = savedDataRef.current;
2671
+ const idx = currentIndexRef.current;
2672
+ if (idx >= data.length) {
2673
+ clearInterval_();
2674
+ setIsPaused(true);
2675
+ return;
2676
+ }
2677
+ const bar = data[idx];
2678
+ state.chart.updateData?.(bar);
2679
+ currentIndexRef.current = idx + 1;
2680
+ setBarIndex(idx + 1);
2681
+ }, [state.chart, clearInterval_]);
2682
+ const startInterval = useCallback(
2683
+ (currentSpeed) => {
2684
+ clearInterval_();
2685
+ intervalRef.current = setInterval(addNextBar, 1e3 / currentSpeed);
2686
+ },
2687
+ [addNextBar, clearInterval_]
2688
+ );
2689
+ const startReplay = useCallback(() => {
2690
+ if (!state.chart) return;
2691
+ const dataList = state.chart.getDataList();
2692
+ if (!dataList || dataList.length === 0) return;
2693
+ savedDataRef.current = [...dataList];
2694
+ currentIndexRef.current = 0;
2695
+ setTotalBars(dataList.length);
2696
+ setBarIndex(0);
2697
+ setIsReplaying(true);
2698
+ setIsPaused(false);
2699
+ state.chart.clearData?.();
2700
+ startInterval(speed);
2701
+ }, [state.chart, speed, startInterval]);
2702
+ const stopReplay = useCallback(() => {
2703
+ if (!state.chart) return;
2704
+ clearInterval_();
2705
+ const data = savedDataRef.current;
2706
+ if (data.length > 0) {
2707
+ state.chart.clearData?.();
2708
+ for (const bar of data) {
2709
+ state.chart.updateData?.(bar);
2710
+ }
2711
+ }
2712
+ savedDataRef.current = [];
2713
+ currentIndexRef.current = 0;
2714
+ setIsReplaying(false);
2715
+ setIsPaused(false);
2716
+ setBarIndex(0);
2717
+ setTotalBars(0);
2718
+ }, [state.chart, clearInterval_]);
2719
+ const togglePause = useCallback(() => {
2720
+ if (!isReplaying) return;
2721
+ if (isPaused) {
2722
+ startInterval(speed);
2723
+ setIsPaused(false);
2724
+ } else {
2725
+ clearInterval_();
2726
+ setIsPaused(true);
2727
+ }
2728
+ }, [isReplaying, isPaused, speed, startInterval, clearInterval_]);
2729
+ const stepForward = useCallback(() => {
2730
+ if (!isReplaying || !isPaused) return;
2731
+ addNextBar();
2732
+ }, [isReplaying, isPaused, addNextBar]);
2733
+ const stepBackward = useCallback(() => {
2734
+ if (!isReplaying || !isPaused || !state.chart) return;
2735
+ const idx = currentIndexRef.current;
2736
+ if (idx <= 1) return;
2737
+ const data = savedDataRef.current;
2738
+ state.chart.clearData?.();
2739
+ for (let i = 0; i < idx - 1; i++) {
2740
+ state.chart.updateData?.(data[i]);
2741
+ }
2742
+ currentIndexRef.current = idx - 1;
2743
+ setBarIndex(idx - 1);
2744
+ }, [isReplaying, isPaused, state.chart]);
2745
+ const seekTo = useCallback(
2746
+ (targetIndex) => {
2747
+ if (!isReplaying || !state.chart) return;
2748
+ const data = savedDataRef.current;
2749
+ const clamped = Math.max(0, Math.min(targetIndex, data.length));
2750
+ clearInterval_();
2751
+ setIsPaused(true);
2752
+ state.chart.clearData?.();
2753
+ for (let i = 0; i < clamped; i++) {
2754
+ state.chart.updateData?.(data[i]);
2755
+ }
2756
+ currentIndexRef.current = clamped;
2757
+ setBarIndex(clamped);
2758
+ },
2759
+ [isReplaying, state.chart, clearInterval_]
2760
+ );
2761
+ const setSpeed = useCallback(
2762
+ (newSpeed) => {
2763
+ setSpeedState(newSpeed);
2764
+ if (isReplaying && !isPaused) {
2765
+ startInterval(newSpeed);
2766
+ }
2767
+ },
2768
+ [isReplaying, isPaused, startInterval]
2769
+ );
2770
+ useEffect(() => {
2771
+ return () => {
2772
+ clearInterval_();
2773
+ };
2774
+ }, [clearInterval_]);
2775
+ return {
2776
+ isReplaying,
2777
+ isPaused,
2778
+ speed,
2779
+ barIndex,
2780
+ totalBars,
2781
+ startReplay,
2782
+ stopReplay,
2783
+ togglePause,
2784
+ stepForward,
2785
+ stepBackward,
2786
+ seekTo,
2787
+ setSpeed
2788
+ };
2789
+ }
2790
+ var DEFAULT_COLORS = [
2791
+ "#2196f3",
2792
+ "#ff9800",
2793
+ "#4caf50",
2794
+ "#e91e63",
2795
+ "#9c27b0",
2796
+ "#00bcd4",
2797
+ "#ff5722",
2798
+ "#8bc34a"
2799
+ ];
2800
+ var colorIndex = 0;
2801
+ var compareCounter = 0;
2802
+ function useCompare() {
2803
+ const { state, datafeed } = useKlinechartsUI();
2804
+ const [symbols, setSymbols] = useState([]);
2805
+ const indicatorsRef = useRef(/* @__PURE__ */ new Map());
2806
+ const addSymbol = useCallback(
2807
+ async (ticker, color) => {
2808
+ if (!state.chart || !datafeed) return;
2809
+ if (indicatorsRef.current.has(ticker)) return;
2810
+ const assignedColor = color ?? DEFAULT_COLORS[colorIndex++ % DEFAULT_COLORS.length];
2811
+ const mainDataList = state.chart.getDataList();
2812
+ if (!mainDataList || mainDataList.length === 0) return;
2813
+ const from = mainDataList[0].timestamp;
2814
+ const to = mainDataList[mainDataList.length - 1].timestamp;
2815
+ const compareData = await datafeed.getHistoryKLineData(
2816
+ { ticker },
2817
+ { ...state.period, label: "" },
2818
+ from,
2819
+ to
2820
+ );
2821
+ if (!compareData || compareData.length === 0) return;
2822
+ const compareMap = /* @__PURE__ */ new Map();
2823
+ for (const bar of compareData) {
2824
+ compareMap.set(bar.timestamp, bar.close);
2825
+ }
2826
+ let basePrice = null;
2827
+ for (const bar of mainDataList) {
2828
+ const close = compareMap.get(bar.timestamp);
2829
+ if (close != null) {
2830
+ basePrice = close;
2831
+ break;
2832
+ }
2833
+ }
2834
+ if (basePrice === null || basePrice === 0) return;
2835
+ const bp = basePrice;
2836
+ const normalizedData = mainDataList.map((mainBar) => {
2837
+ const close = compareMap.get(mainBar.timestamp);
2838
+ return {
2839
+ pct: close != null ? (close - bp) / bp * 100 : NaN
2840
+ };
2841
+ });
2842
+ const indicatorName = `__cmp_${++compareCounter}_${ticker}`;
2843
+ registerIndicator({
2844
+ name: indicatorName,
2845
+ shortName: ticker,
2846
+ figures: [
2847
+ {
2848
+ key: "pct",
2849
+ title: `${ticker} %: `,
2850
+ type: "line",
2851
+ styles: () => ({ color: assignedColor })
2852
+ }
2853
+ ],
2854
+ calc: () => normalizedData
2855
+ });
2856
+ const paneId = state.chart.createIndicator(
2857
+ { name: indicatorName },
2858
+ true,
2859
+ { id: "candle_pane" }
2860
+ );
2861
+ indicatorsRef.current.set(ticker, {
2862
+ name: indicatorName,
2863
+ paneId: typeof paneId === "string" ? paneId : "candle_pane"
2864
+ });
2865
+ setSymbols((prev) => {
2866
+ if (prev.some((s) => s.ticker === ticker)) return prev;
2867
+ return [
2868
+ ...prev,
2869
+ { ticker, basePrice: bp, color: assignedColor, visible: true }
2870
+ ];
2871
+ });
2872
+ },
2873
+ [state.chart, state.period, datafeed]
2874
+ );
2875
+ const removeSymbol = useCallback(
2876
+ (ticker) => {
2877
+ const info = indicatorsRef.current.get(ticker);
2878
+ if (info && state.chart) {
2879
+ try {
2880
+ state.chart.removeIndicator({ name: info.name });
2881
+ } catch {
2882
+ }
2883
+ indicatorsRef.current.delete(ticker);
2884
+ }
2885
+ setSymbols((prev) => prev.filter((s) => s.ticker !== ticker));
2886
+ },
2887
+ [state.chart]
2888
+ );
2889
+ const toggleSymbol = useCallback(
2890
+ (ticker) => {
2891
+ const info = indicatorsRef.current.get(ticker);
2892
+ if (!info || !state.chart) return;
2893
+ setSymbols((prev) => {
2894
+ const sym = prev.find((s) => s.ticker === ticker);
2895
+ if (!sym) return prev;
2896
+ const newVisible = !sym.visible;
2897
+ state.chart?.overrideIndicator({
2898
+ name: info.name,
2899
+ visible: newVisible
2900
+ });
2901
+ return prev.map(
2902
+ (s) => s.ticker === ticker ? { ...s, visible: newVisible } : s
2903
+ );
2904
+ });
2905
+ },
2906
+ [state.chart]
2907
+ );
2908
+ const clearAll = useCallback(() => {
2909
+ indicatorsRef.current.forEach((info) => {
2910
+ try {
2911
+ state.chart?.removeIndicator({ name: info.name });
2912
+ } catch {
2913
+ }
2914
+ });
2915
+ indicatorsRef.current.clear();
2916
+ setSymbols([]);
2917
+ }, [state.chart]);
2918
+ useEffect(() => {
2919
+ return () => {
2920
+ indicatorsRef.current.forEach((info) => {
2921
+ try {
2922
+ state.chart?.removeIndicator({ name: info.name });
2923
+ } catch {
2924
+ }
2925
+ });
2926
+ indicatorsRef.current.clear();
2927
+ };
2928
+ }, [state.chart]);
2929
+ return { symbols, addSymbol, removeSymbol, toggleSymbol, clearAll };
2930
+ }
2931
+ var MEASURE_OVERLAY_ID = "__measure_line__";
2932
+ function useMeasure() {
2933
+ const { state } = useKlinechartsUI();
2934
+ const [isActive, setIsActive] = useState(false);
2935
+ const [fromPoint, setFromPoint] = useState(null);
2936
+ const [result, setResult] = useState(null);
2937
+ const clickCountRef = useRef(0);
2938
+ const computeResult = useCallback(
2939
+ (from, to) => {
2940
+ const priceDiff = to.price - from.price;
2941
+ const pricePercent = from.price !== 0 ? priceDiff / from.price * 100 : 0;
2942
+ const bars = Math.abs(to.barIndex - from.barIndex);
2943
+ const timeDiff = Math.abs(to.timestamp - from.timestamp);
2944
+ return { from, to, priceDiff, pricePercent, bars, timeDiff };
2945
+ },
2946
+ []
2947
+ );
2948
+ const cleanup = useCallback(() => {
2949
+ state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });
2950
+ }, [state.chart]);
2951
+ const startMeasure = useCallback(() => {
2952
+ cleanup();
2953
+ setIsActive(true);
2954
+ setFromPoint(null);
2955
+ setResult(null);
2956
+ clickCountRef.current = 0;
2957
+ if (!state.chart) return;
2958
+ state.chart.createOverlay({
2959
+ name: "segment",
2960
+ id: MEASURE_OVERLAY_ID,
2961
+ groupId: "measure",
2962
+ styles: {
2963
+ line: {
2964
+ style: "dashed",
2965
+ color: "#FFD600",
2966
+ size: 1
2967
+ }
2968
+ },
2969
+ lock: true,
2970
+ onDrawEnd: (event) => {
2971
+ const overlay = event?.overlay;
2972
+ const points = overlay?.points;
2973
+ if (!points || points.length < 2) return;
2974
+ const chart = state.chart;
2975
+ if (!chart) return;
2976
+ const dataList = chart.getDataList();
2977
+ if (!dataList || dataList.length === 0) return;
2978
+ const makePoint = (p) => {
2979
+ const idx = Math.max(
2980
+ 0,
2981
+ Math.min(p.dataIndex ?? 0, dataList.length - 1)
2982
+ );
2983
+ const bar = dataList[idx];
2984
+ return {
2985
+ price: p.value ?? bar?.close ?? 0,
2986
+ timestamp: bar?.timestamp ?? 0,
2987
+ barIndex: idx
2988
+ };
2989
+ };
2990
+ const from = makePoint(points[0]);
2991
+ const to = makePoint(points[1]);
2992
+ setFromPoint(from);
2993
+ setResult(computeResult(from, to));
2994
+ setIsActive(false);
2995
+ }
2996
+ });
2997
+ }, [state.chart, cleanup, computeResult]);
2998
+ const cancelMeasure = useCallback(() => {
2999
+ cleanup();
3000
+ setIsActive(false);
3001
+ setFromPoint(null);
3002
+ clickCountRef.current = 0;
3003
+ }, [cleanup]);
3004
+ const clearResult = useCallback(() => {
3005
+ cleanup();
3006
+ setResult(null);
3007
+ setFromPoint(null);
3008
+ }, [cleanup]);
3009
+ useEffect(() => {
3010
+ return () => {
3011
+ state.chart?.removeOverlay({ id: MEASURE_OVERLAY_ID });
3012
+ };
3013
+ }, [state.chart]);
3014
+ return {
3015
+ isActive,
3016
+ startMeasure,
3017
+ cancelMeasure,
3018
+ result,
3019
+ clearResult,
3020
+ fromPoint
3021
+ };
3022
+ }
3023
+ var annotationCounter = 0;
3024
+ function useAnnotations() {
3025
+ const { state } = useKlinechartsUI();
3026
+ const [annotations, setAnnotations] = useState([]);
3027
+ const addAnnotation = useCallback(
3028
+ (text, price, timestamp, color) => {
3029
+ const id = `annotation_${++annotationCounter}`;
3030
+ const annotation = {
3031
+ id,
3032
+ text,
3033
+ price,
3034
+ timestamp,
3035
+ color
3036
+ };
3037
+ setAnnotations((prev) => [...prev, annotation]);
3038
+ if (state.chart) {
3039
+ state.chart.createOverlay({
3040
+ name: "simpleAnnotation",
3041
+ id,
3042
+ groupId: "annotations",
3043
+ points: [{ timestamp, value: price }],
3044
+ extendData: text,
3045
+ styles: color ? {
3046
+ text: {
3047
+ color
3048
+ }
3049
+ } : void 0,
3050
+ lock: true
3051
+ });
3052
+ }
3053
+ return id;
3054
+ },
3055
+ [state.chart]
3056
+ );
3057
+ const removeAnnotation = useCallback(
3058
+ (id) => {
3059
+ setAnnotations((prev) => prev.filter((a) => a.id !== id));
3060
+ state.chart?.removeOverlay({ id });
3061
+ },
3062
+ [state.chart]
3063
+ );
3064
+ const updateAnnotation = useCallback(
3065
+ (id, updates) => {
3066
+ setAnnotations(
3067
+ (prev) => prev.map((a) => a.id === id ? { ...a, ...updates } : a)
3068
+ );
3069
+ if (state.chart) {
3070
+ const overrideData = { id };
3071
+ if (updates.text !== void 0) {
3072
+ overrideData.extendData = updates.text;
3073
+ }
3074
+ if (updates.color !== void 0) {
3075
+ overrideData.styles = {
3076
+ text: {
3077
+ color: updates.color
3078
+ }
3079
+ };
3080
+ }
3081
+ state.chart.overrideOverlay(overrideData);
3082
+ }
3083
+ },
3084
+ [state.chart]
3085
+ );
3086
+ const clearAnnotations = useCallback(() => {
3087
+ setAnnotations((prev) => {
3088
+ for (const annotation of prev) {
3089
+ state.chart?.removeOverlay({ id: annotation.id });
3090
+ }
3091
+ return [];
3092
+ });
3093
+ }, [state.chart]);
3094
+ useEffect(() => {
3095
+ return () => {
3096
+ state.chart?.removeOverlay({ groupId: "annotations" });
3097
+ };
3098
+ }, [state.chart]);
3099
+ return {
3100
+ annotations,
3101
+ addAnnotation,
3102
+ removeAnnotation,
3103
+ updateAnnotation,
3104
+ clearAnnotations
3105
+ };
3106
+ }
1323
3107
 
1324
3108
  // src/utils/createDataLoader.ts
1325
3109
  function createDataLoader(datafeed, dispatch) {
@@ -1385,6 +3169,6 @@ function createDataLoader(datafeed, dispatch) {
1385
3169
  };
1386
3170
  }
1387
3171
 
1388
- export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIDispatchContext, KlinechartsUIProvider, KlinechartsUIStateContext, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, YAXIS_POSITIONS, createDataLoader, useDrawingTools, useFullscreen, useIndicators, useKlinechartsUI, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useOrderLines, usePeriods, useScreenshot, useSymbolSearch, useTimezone };
3172
+ export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIDispatchContext, KlinechartsUIProvider, KlinechartsUIStateContext, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, YAXIS_POSITIONS, createDataLoader, useAlerts, useAnnotations, useCompare, useCrosshair, useDataExport, useDrawingTools, useFullscreen, useIndicators, useKlinechartsUI, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useLayoutManager, useMeasure, useOrderLines, usePeriods, useReplay, useScreenshot, useScriptEditor, useSymbolSearch, useTimezone, useUndoRedo, useWatchlist };
1389
3173
  //# sourceMappingURL=index.js.map
1390
3174
  //# sourceMappingURL=index.js.map