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/README.md +528 -29
- package/dist/chunk-PIFLJKYF.cjs +2512 -0
- package/dist/chunk-PIFLJKYF.cjs.map +1 -0
- package/dist/chunk-U325AHHX.js +2468 -0
- package/dist/chunk-U325AHHX.js.map +1 -0
- package/dist/extensions.cjs +12 -4
- package/dist/extensions.d.cts +22 -1
- package/dist/extensions.d.ts +22 -1
- package/dist/extensions.js +1 -1
- package/dist/index.cjs +1936 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +441 -3
- package/dist/index.d.ts +441 -3
- package/dist/index.js +1815 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-PUULIBFN.js +0 -911
- package/dist/chunk-PUULIBFN.js.map +0 -1
- package/dist/chunk-SLTXNLKN.cjs +0 -932
- package/dist/chunk-SLTXNLKN.cjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { registerExtensions } from './chunk-
|
|
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-
|
|
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
|
-
{
|
|
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({
|
|
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: "
|
|
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
|
|
1048
|
+
const createOverlayForTool = useCallback(
|
|
798
1049
|
(name) => {
|
|
799
|
-
|
|
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:
|
|
804
|
-
visible:
|
|
805
|
-
mode
|
|
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,
|
|
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
|
|
1263
|
-
const
|
|
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: [{
|
|
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
|
|
1292
|
-
const
|
|
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: [{
|
|
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
|