@tradejs/app 1.0.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.
Files changed (126) hide show
  1. package/README.md +12 -0
  2. package/bin/tradejs-app.mjs +54 -0
  3. package/next-env.d.ts +6 -0
  4. package/next.config.mjs +31 -0
  5. package/package.json +60 -0
  6. package/src/app/actions/ai.ts +33 -0
  7. package/src/app/actions/backtest.ts +55 -0
  8. package/src/app/actions/kline.ts +18 -0
  9. package/src/app/actions/scanner.ts +10 -0
  10. package/src/app/actions/signal.ts +19 -0
  11. package/src/app/api/ai/route.ts +151 -0
  12. package/src/app/api/auth/[...nextauth]/route.ts +5 -0
  13. package/src/app/api/backtest/files/route.ts +60 -0
  14. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +47 -0
  15. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +63 -0
  16. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +57 -0
  17. package/src/app/api/cron/route.ts +4 -0
  18. package/src/app/api/derivatives/[symbol]/[interval]/route.ts +57 -0
  19. package/src/app/api/derivatives/summary/route.ts +20 -0
  20. package/src/app/api/files/screenshot/[name]/route.ts +42 -0
  21. package/src/app/api/indicators/route.ts +24 -0
  22. package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +123 -0
  23. package/src/app/api/scanner/[provider]/route.ts +41 -0
  24. package/src/app/api/scanner/route.ts +31 -0
  25. package/src/app/api/signal/[symbol]/[signalId]/route.ts +42 -0
  26. package/src/app/api/spread/[symbol]/[interval]/route.ts +57 -0
  27. package/src/app/api/spread/summary/route.ts +20 -0
  28. package/src/app/auth.ts +76 -0
  29. package/src/app/components/Backtest/CompareList/index.tsx +34 -0
  30. package/src/app/components/Backtest/TestCard/Chart/index.tsx +118 -0
  31. package/src/app/components/Backtest/TestCard/Chart/utils/index.ts +81 -0
  32. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +21 -0
  33. package/src/app/components/Backtest/TestCard/ConfigDrawer/JsonCodeBlock.tsx +46 -0
  34. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +94 -0
  35. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +128 -0
  36. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +18 -0
  37. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +40 -0
  38. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +24 -0
  39. package/src/app/components/Backtest/TestCard/Root/index.tsx +55 -0
  40. package/src/app/components/Backtest/TestCard/Skeleton/index.tsx +21 -0
  41. package/src/app/components/Backtest/TestCard/Stat/index.tsx +119 -0
  42. package/src/app/components/Backtest/TestCard/Title/index.tsx +84 -0
  43. package/src/app/components/Backtest/TestCard/context.ts +14 -0
  44. package/src/app/components/Backtest/TestCard/index.ts +28 -0
  45. package/src/app/components/Backtest/TestList/index.tsx +124 -0
  46. package/src/app/components/Dashboard/AiDrawer/Message.tsx +34 -0
  47. package/src/app/components/Dashboard/AiDrawer/index.tsx +163 -0
  48. package/src/app/components/Dashboard/KlineChart/figures/backtestFigureTypes.ts +7 -0
  49. package/src/app/components/Dashboard/KlineChart/figures/backtestMarkersPointFigure.ts +76 -0
  50. package/src/app/components/Dashboard/KlineChart/figures/circle.ts +15 -0
  51. package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +25 -0
  52. package/src/app/components/Dashboard/KlineChart/figures/entryLinePointFigure.ts +1 -0
  53. package/src/app/components/Dashboard/KlineChart/figures/entryPointsPointFigure.ts +1 -0
  54. package/src/app/components/Dashboard/KlineChart/figures/entryZonePointFigure.ts +1 -0
  55. package/src/app/components/Dashboard/KlineChart/figures/index.ts +213 -0
  56. package/src/app/components/Dashboard/KlineChart/figures/label.ts +14 -0
  57. package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +20 -0
  58. package/src/app/components/Dashboard/KlineChart/figures/square.ts +21 -0
  59. package/src/app/components/Dashboard/KlineChart/figures/star.ts +39 -0
  60. package/src/app/components/Dashboard/KlineChart/figures/tradeZonePointFigure.ts +44 -0
  61. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointFigure.ts +37 -0
  62. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointsPointFigure.ts +26 -0
  63. package/src/app/components/Dashboard/KlineChart/figures/triangle.ts +23 -0
  64. package/src/app/components/Dashboard/KlineChart/hooks/index.ts +14 -0
  65. package/src/app/components/Dashboard/KlineChart/hooks/indicatorShared.ts +30 -0
  66. package/src/app/components/Dashboard/KlineChart/hooks/useAtrIndicator.ts +75 -0
  67. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +533 -0
  68. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +74 -0
  69. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +155 -0
  70. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +185 -0
  71. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +62 -0
  72. package/src/app/components/Dashboard/KlineChart/hooks/useMaIndicator.ts +62 -0
  73. package/src/app/components/Dashboard/KlineChart/hooks/useManagedIndicator.ts +140 -0
  74. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +212 -0
  75. package/src/app/components/Dashboard/KlineChart/hooks/useResize.ts +29 -0
  76. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +122 -0
  77. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +85 -0
  78. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +243 -0
  79. package/src/app/components/Dashboard/KlineChart/hooks/useSupportResistanceLines.ts +125 -0
  80. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +139 -0
  81. package/src/app/components/Dashboard/KlineChart/hooks/useVolIndicator.ts +18 -0
  82. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +62 -0
  83. package/src/app/components/Dashboard/KlineChart/index.tsx +169 -0
  84. package/src/app/components/Dashboard/KlineChart/styles.ts +70 -0
  85. package/src/app/components/Dashboard/MainChart/index.tsx +35 -0
  86. package/src/app/components/Shared/AppShell.tsx +28 -0
  87. package/src/app/components/Shared/FavoriteButton/index.tsx +23 -0
  88. package/src/app/components/Shared/Filters/Backtest/index.tsx +164 -0
  89. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +18 -0
  90. package/src/app/components/Shared/Filters/Indicators/index.tsx +21 -0
  91. package/src/app/components/Shared/Filters/Interval/index.tsx +31 -0
  92. package/src/app/components/Shared/Filters/Interval/intervals.ts +6 -0
  93. package/src/app/components/Shared/Filters/Provider/index.tsx +32 -0
  94. package/src/app/components/Shared/Filters/Root/index.tsx +28 -0
  95. package/src/app/components/Shared/Filters/Symbol/index.tsx +49 -0
  96. package/src/app/components/Shared/Filters/context.ts +17 -0
  97. package/src/app/components/Shared/Filters/index.ts +17 -0
  98. package/src/app/components/Shared/Sidebar/index.tsx +72 -0
  99. package/src/app/components/UI/ColorMode/index.tsx +112 -0
  100. package/src/app/components/UI/EmptyState/index.tsx +28 -0
  101. package/src/app/components/UI/OverlaySpinner/index.tsx +23 -0
  102. package/src/app/components/UI/Segment/index.tsx +23 -0
  103. package/src/app/components/UI/Select/index.tsx +81 -0
  104. package/src/app/components/UI/SelectWithSearch/index.tsx +104 -0
  105. package/src/app/components/UI/Switcher/index.tsx +24 -0
  106. package/src/app/components/UI/Toaster/index.tsx +45 -0
  107. package/src/app/components/UI/index.ts +8 -0
  108. package/src/app/favicon.ico +0 -0
  109. package/src/app/globals.css +5 -0
  110. package/src/app/layout.tsx +31 -0
  111. package/src/app/page.tsx +14 -0
  112. package/src/app/provider.tsx +39 -0
  113. package/src/app/routes/backtest/[test]/page.tsx +33 -0
  114. package/src/app/routes/backtest/page.tsx +374 -0
  115. package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +124 -0
  116. package/src/app/routes/dashboard/page.tsx +20 -0
  117. package/src/app/routes/derivatives/page.tsx +202 -0
  118. package/src/app/routes/signin/page.tsx +155 -0
  119. package/src/app/store/data.ts +144 -0
  120. package/src/app/store/filters.ts +29 -0
  121. package/src/app/store/index.ts +13 -0
  122. package/src/app/store/indicators.ts +229 -0
  123. package/src/app/store/tests.ts +464 -0
  124. package/src/app/store/tickers.ts +89 -0
  125. package/src/proxy.ts +142 -0
  126. package/tsconfig.json +40 -0
@@ -0,0 +1,155 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import _ from 'lodash';
3
+ import { Chart } from 'klinecharts';
4
+ import { Filters, Provider } from '@tradejs/types';
5
+ import { useData } from '@store';
6
+ import { CORRELATION_WINDOW } from '@tradejs/core/constants';
7
+ import { getCloseAtOrBefore, grayDashedLineStyle } from './indicatorShared';
8
+ import { useManagedIndicator } from './useManagedIndicator';
9
+
10
+ const WINDOW = CORRELATION_WINDOW;
11
+
12
+ const pearson = (x: number[], y: number[]) => {
13
+ const len = x.length;
14
+ if (len < 2) return undefined;
15
+
16
+ const sumX = x.reduce((acc, value) => acc + value, 0);
17
+ const sumY = y.reduce((acc, value) => acc + value, 0);
18
+ const meanX = sumX / len;
19
+ const meanY = sumY / len;
20
+
21
+ let numerator = 0;
22
+ let denomX = 0;
23
+ let denomY = 0;
24
+
25
+ for (let i = 0; i < len; i++) {
26
+ const dx = x[i] - meanX;
27
+ const dy = y[i] - meanY;
28
+
29
+ numerator += dx * dy;
30
+ denomX += dx * dx;
31
+ denomY += dy * dy;
32
+ }
33
+
34
+ if (denomX <= 0 || denomY <= 0) return undefined;
35
+
36
+ const value = numerator / Math.sqrt(denomX * denomY);
37
+ if (!Number.isFinite(value)) return undefined;
38
+
39
+ return Math.max(-1, Math.min(1, value));
40
+ };
41
+
42
+ const buildCorrelationValues = (
43
+ kLineDataList: Array<{ timestamp: number; close: number }>,
44
+ btcByTs: Record<number, { close: number }>,
45
+ btcCandles: Array<{ timestamp: number; close: number }>,
46
+ ) => {
47
+ const valuesByTs: Record<number, Record<string, number | undefined>> = {};
48
+ const symbolWindow: number[] = [];
49
+ const btcWindow: number[] = [];
50
+
51
+ for (let i = 0; i < kLineDataList.length; i++) {
52
+ const candle = kLineDataList[i];
53
+ const btcClose =
54
+ btcByTs[candle.timestamp]?.close ??
55
+ getCloseAtOrBefore(btcCandles, candle.timestamp);
56
+
57
+ if (Number.isFinite(candle.close) && Number.isFinite(btcClose)) {
58
+ symbolWindow.push(candle.close);
59
+ btcWindow.push(Number(btcClose));
60
+
61
+ if (symbolWindow.length > WINDOW) {
62
+ symbolWindow.shift();
63
+ btcWindow.shift();
64
+ }
65
+ }
66
+
67
+ valuesByTs[candle.timestamp] = {
68
+ BTC_CORR: pearson(symbolWindow, btcWindow),
69
+ BTC_CORR_LEVEL: 0.5,
70
+ };
71
+ }
72
+
73
+ return valuesByTs;
74
+ };
75
+
76
+ export const useBtcCorrelation = (
77
+ chart: Chart | null,
78
+ enabled: boolean,
79
+ filters: Filters,
80
+ ) => {
81
+ const indicatorId = 'btc_correlation_indicator';
82
+ const paneId = 'btc_correlation_indicator_pane';
83
+ const btcByTimestampRef = useRef<Record<number, { close: number }>>({});
84
+ const btcCandlesRef = useRef<Array<{ timestamp: number; close: number }>>([]);
85
+
86
+ const btcFilters = useMemo(
87
+ () => ({
88
+ ...filters,
89
+ provider: (filters.provider || 'bybit') as Provider,
90
+ symbol: enabled ? 'BTCUSDT' : '',
91
+ }),
92
+ [filters, enabled],
93
+ );
94
+
95
+ const { data: btcData } = useData(btcFilters);
96
+
97
+ const btcByTimestamp = useMemo(
98
+ () => _.keyBy(btcData, 'timestamp'),
99
+ [btcData],
100
+ );
101
+ const btcCandles = useMemo(
102
+ () =>
103
+ btcData
104
+ .map((candle) => ({ timestamp: candle.timestamp, close: candle.close }))
105
+ .sort((a, b) => a.timestamp - b.timestamp),
106
+ [btcData],
107
+ );
108
+
109
+ useEffect(() => {
110
+ btcByTimestampRef.current = btcByTimestamp;
111
+ btcCandlesRef.current = btcCandles;
112
+ }, [btcByTimestamp, btcCandles]);
113
+
114
+ const calc = useCallback(
115
+ (kLineDataList: Array<{ timestamp: number; close: number }>) =>
116
+ buildCorrelationValues(
117
+ kLineDataList,
118
+ btcByTimestampRef.current,
119
+ btcCandlesRef.current,
120
+ ),
121
+ [],
122
+ );
123
+
124
+ const template = useMemo(
125
+ () => ({
126
+ shortName: 'BTC Correlation',
127
+ calcParams: [],
128
+ figures: [
129
+ {
130
+ key: 'BTC_CORR',
131
+ title: `BTC Correlation(${WINDOW}): `,
132
+ type: 'line',
133
+ },
134
+ {
135
+ key: 'BTC_CORR_LEVEL',
136
+ title: 'Correlation Level(0.5): ',
137
+ type: 'line',
138
+ styles: grayDashedLineStyle,
139
+ },
140
+ ],
141
+ }),
142
+ [],
143
+ );
144
+
145
+ useManagedIndicator({
146
+ chart,
147
+ enabled,
148
+ indicatorName: 'BTC_CORRELATION',
149
+ indicatorId,
150
+ paneId,
151
+ template,
152
+ calc,
153
+ updateDeps: [btcData],
154
+ });
155
+ };
@@ -0,0 +1,185 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import _ from 'lodash';
3
+ import { Chart } from 'klinecharts';
4
+ import { Filters, Provider } from '@tradejs/types';
5
+ import { useData } from '@store';
6
+ import { getCloseAtOrBefore } from './indicatorShared';
7
+ import { useManagedIndicator } from './useManagedIndicator';
8
+
9
+ const buildBtcValues = (
10
+ kLineDataList: Array<{ timestamp: number }>,
11
+ bybitByTs: Record<number, { close: number }>,
12
+ binanceByTs: Record<number, { close: number }>,
13
+ coinbaseByTs: Record<number, { close: number }>,
14
+ bybitCandles: Array<{ timestamp: number; close: number }>,
15
+ binanceCandles: Array<{ timestamp: number; close: number }>,
16
+ coinbaseCandles: Array<{ timestamp: number; close: number }>,
17
+ ) => {
18
+ return kLineDataList.reduce<
19
+ Record<number, Record<string, number | undefined>>
20
+ >((acc, { timestamp }) => {
21
+ const bybitValue =
22
+ bybitByTs[timestamp]?.close ??
23
+ getCloseAtOrBefore(bybitCandles, timestamp);
24
+ const binanceValue =
25
+ binanceByTs[timestamp]?.close ??
26
+ getCloseAtOrBefore(binanceCandles, timestamp);
27
+ const coinbaseValue =
28
+ coinbaseByTs[timestamp]?.close ??
29
+ getCloseAtOrBefore(coinbaseCandles, timestamp);
30
+
31
+ acc[timestamp] = {
32
+ BTC_BYBIT: bybitValue,
33
+ BTC_BINANCE: binanceValue,
34
+ BTC_COINBASE: coinbaseValue,
35
+ };
36
+
37
+ return acc;
38
+ }, {});
39
+ };
40
+
41
+ export const useBtcIndicator = (
42
+ chart: Chart | null,
43
+ enabled: boolean,
44
+ filters: Filters,
45
+ ) => {
46
+ const indicatorId = 'btc_indicator';
47
+ const paneId = 'btc_indicator_pane';
48
+ const bybitByTimestampRef = useRef<Record<number, { close: number }>>({});
49
+ const binanceByTimestampRef = useRef<Record<number, { close: number }>>({});
50
+ const coinbaseByTimestampRef = useRef<Record<number, { close: number }>>({});
51
+ const bybitCandlesRef = useRef<Array<{ timestamp: number; close: number }>>(
52
+ [],
53
+ );
54
+ const binanceCandlesRef = useRef<Array<{ timestamp: number; close: number }>>(
55
+ [],
56
+ );
57
+ const coinbaseCandlesRef = useRef<
58
+ Array<{ timestamp: number; close: number }>
59
+ >([]);
60
+
61
+ const btcFilters = useMemo(
62
+ () => ({
63
+ bybit: {
64
+ ...filters,
65
+ provider: 'bybit' as Provider,
66
+ symbol: enabled ? 'BTCUSDT' : '',
67
+ },
68
+ binance: {
69
+ ...filters,
70
+ provider: 'binance' as Provider,
71
+ symbol: enabled ? 'BTCUSDT' : '',
72
+ },
73
+ coinbase: {
74
+ ...filters,
75
+ provider: 'coinbase' as Provider,
76
+ symbol: enabled ? 'BTCUSDT' : '',
77
+ },
78
+ }),
79
+ [filters, enabled],
80
+ );
81
+
82
+ const { data: bybitData } = useData(btcFilters.bybit);
83
+ const { data: binanceData } = useData(btcFilters.binance);
84
+ const { data: coinbaseData } = useData(btcFilters.coinbase);
85
+
86
+ const bybitByTimestamp = useMemo(
87
+ () => _.keyBy(bybitData, 'timestamp'),
88
+ [bybitData],
89
+ );
90
+ const bybitCandles = useMemo(
91
+ () =>
92
+ bybitData
93
+ .map((candle) => ({ timestamp: candle.timestamp, close: candle.close }))
94
+ .sort((a, b) => a.timestamp - b.timestamp),
95
+ [bybitData],
96
+ );
97
+ const binanceByTimestamp = useMemo(
98
+ () => _.keyBy(binanceData, 'timestamp'),
99
+ [binanceData],
100
+ );
101
+ const binanceCandles = useMemo(
102
+ () =>
103
+ binanceData
104
+ .map((candle) => ({ timestamp: candle.timestamp, close: candle.close }))
105
+ .sort((a, b) => a.timestamp - b.timestamp),
106
+ [binanceData],
107
+ );
108
+ const coinbaseByTimestamp = useMemo(
109
+ () => _.keyBy(coinbaseData, 'timestamp'),
110
+ [coinbaseData],
111
+ );
112
+ const coinbaseCandles = useMemo(
113
+ () =>
114
+ coinbaseData
115
+ .map((candle) => ({ timestamp: candle.timestamp, close: candle.close }))
116
+ .sort((a, b) => a.timestamp - b.timestamp),
117
+ [coinbaseData],
118
+ );
119
+
120
+ useEffect(() => {
121
+ bybitByTimestampRef.current = bybitByTimestamp;
122
+ binanceByTimestampRef.current = binanceByTimestamp;
123
+ coinbaseByTimestampRef.current = coinbaseByTimestamp;
124
+ bybitCandlesRef.current = bybitCandles;
125
+ binanceCandlesRef.current = binanceCandles;
126
+ coinbaseCandlesRef.current = coinbaseCandles;
127
+ }, [
128
+ bybitByTimestamp,
129
+ binanceByTimestamp,
130
+ coinbaseByTimestamp,
131
+ bybitCandles,
132
+ binanceCandles,
133
+ coinbaseCandles,
134
+ ]);
135
+
136
+ const calc = useCallback(
137
+ (kLineDataList: Array<{ timestamp: number }>) =>
138
+ buildBtcValues(
139
+ kLineDataList,
140
+ bybitByTimestampRef.current,
141
+ binanceByTimestampRef.current,
142
+ coinbaseByTimestampRef.current,
143
+ bybitCandlesRef.current,
144
+ binanceCandlesRef.current,
145
+ coinbaseCandlesRef.current,
146
+ ),
147
+ [],
148
+ );
149
+
150
+ const template = useMemo(
151
+ () => ({
152
+ shortName: 'BTC',
153
+ calcParams: [],
154
+ figures: [
155
+ {
156
+ key: 'BTC_BYBIT',
157
+ title: 'BTC ByBit: ',
158
+ type: 'line',
159
+ },
160
+ {
161
+ key: 'BTC_BINANCE',
162
+ title: 'BTC Binance: ',
163
+ type: 'line',
164
+ },
165
+ {
166
+ key: 'BTC_COINBASE',
167
+ title: 'BTC Coinbase: ',
168
+ type: 'line',
169
+ },
170
+ ],
171
+ }),
172
+ [],
173
+ );
174
+
175
+ useManagedIndicator({
176
+ chart,
177
+ enabled,
178
+ indicatorName: 'BTC',
179
+ indicatorId,
180
+ paneId,
181
+ template,
182
+ calc,
183
+ updateDeps: [bybitData, binanceData, coinbaseData],
184
+ });
185
+ };
@@ -0,0 +1,62 @@
1
+ import { useEffect } from 'react';
2
+ import { EMA } from 'technicalindicators';
3
+ import { registerIndicator, Chart } from 'klinecharts';
4
+
5
+ export const useEmaIndicator = (
6
+ chart: Chart | null,
7
+ enabled: boolean,
8
+ periods: number[],
9
+ ) => {
10
+ useEffect(() => {
11
+ registerIndicator({
12
+ name: 'EMA',
13
+ shortName: 'EMA',
14
+ calcParams: periods,
15
+ figures: periods.map((period) => ({
16
+ key: `EMA${period}`,
17
+ title: `EMA${period}: `,
18
+ type: 'line',
19
+ })),
20
+
21
+ // Calculation results
22
+ calc: (kLineDataList) => {
23
+ const closesPrices = kLineDataList.map((item) => item.close);
24
+
25
+ const values = periods.map((period) =>
26
+ EMA.calculate({
27
+ period,
28
+ values: closesPrices,
29
+ }),
30
+ );
31
+
32
+ return kLineDataList.reduce<Record<number, Record<string, number>>>(
33
+ (acc, { timestamp }, candleIndex) => {
34
+ const ema: Record<string, number> = {};
35
+ periods.forEach((period, j) => {
36
+ if (candleIndex >= period - 1) {
37
+ ema[`EMA${period}`] = values[j][candleIndex - (period - 1)];
38
+ }
39
+ });
40
+
41
+ acc[timestamp] = ema;
42
+
43
+ return acc;
44
+ },
45
+ {},
46
+ );
47
+ },
48
+ });
49
+ }, [periods]);
50
+
51
+ useEffect(() => {
52
+ if (!chart || !enabled) {
53
+ return () => null;
54
+ }
55
+
56
+ chart.createIndicator('EMA', true, { id: 'candle_pane' });
57
+
58
+ return () => {
59
+ chart.removeIndicator({ name: 'EMA' });
60
+ };
61
+ }, [chart, enabled]);
62
+ };
@@ -0,0 +1,62 @@
1
+ import { useEffect } from 'react';
2
+ import { SMA } from 'technicalindicators';
3
+ import { registerIndicator, Chart } from 'klinecharts';
4
+
5
+ export const useMaIndicator = (
6
+ chart: Chart | null,
7
+ enabled: boolean,
8
+ periods: number[],
9
+ ) => {
10
+ useEffect(() => {
11
+ registerIndicator({
12
+ name: 'MA',
13
+ shortName: 'MA',
14
+ calcParams: periods,
15
+ figures: periods.map((period) => ({
16
+ key: `MA${period}`,
17
+ title: `MA${period}: `,
18
+ type: 'line',
19
+ })),
20
+
21
+ // Calculation results
22
+ calc: (kLineDataList) => {
23
+ const closesPrices = kLineDataList.map((item) => item.close);
24
+
25
+ const values = periods.map((period) =>
26
+ SMA.calculate({
27
+ period,
28
+ values: closesPrices,
29
+ }),
30
+ );
31
+
32
+ return kLineDataList.reduce<Record<number, Record<string, number>>>(
33
+ (acc, { timestamp }, candleIndex) => {
34
+ const ma: Record<string, number> = {};
35
+ periods.forEach((period, j) => {
36
+ if (candleIndex >= period - 1) {
37
+ ma[`MA${period}`] = values[j][candleIndex - (period - 1)];
38
+ }
39
+ }, {});
40
+
41
+ acc[timestamp] = ma;
42
+
43
+ return acc;
44
+ },
45
+ {},
46
+ );
47
+ },
48
+ });
49
+ }, [periods]);
50
+
51
+ useEffect(() => {
52
+ if (!chart || !enabled) {
53
+ return () => null;
54
+ }
55
+
56
+ chart.createIndicator('MA', true, { id: 'candle_pane' });
57
+
58
+ return () => {
59
+ chart.removeIndicator({ name: 'MA' });
60
+ };
61
+ }, [chart, enabled]);
62
+ };
@@ -0,0 +1,140 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Chart, registerIndicator } from 'klinecharts';
3
+
4
+ type CalcFn = (
5
+ kLineDataList: any[],
6
+ ) => Record<number, Record<string, number | undefined>>;
7
+
8
+ type IndicatorTemplate = {
9
+ shortName: string;
10
+ calcParams: unknown[];
11
+ figures: Array<Record<string, unknown>>;
12
+ };
13
+
14
+ interface UseManagedIndicatorParams {
15
+ chart: Chart | null;
16
+ enabled: boolean;
17
+ indicatorName: string;
18
+ indicatorId: string;
19
+ paneId: string;
20
+ minHeight?: number;
21
+ template: IndicatorTemplate;
22
+ calc: CalcFn;
23
+ updateDeps: unknown[];
24
+ }
25
+
26
+ const removeIndicator = (
27
+ chart: Chart,
28
+ params: Pick<UseManagedIndicatorParams, 'indicatorId' | 'indicatorName'>,
29
+ ) => {
30
+ chart.removeIndicator({ id: params.indicatorId });
31
+ chart.removeIndicator({ name: params.indicatorName });
32
+ };
33
+
34
+ const hasIndicator = (
35
+ chart: Chart,
36
+ params: Pick<UseManagedIndicatorParams, 'indicatorId'>,
37
+ ) => chart.getIndicators({ id: params.indicatorId }).length > 0;
38
+
39
+ const createIndicator = (
40
+ chart: Chart,
41
+ params: Pick<
42
+ UseManagedIndicatorParams,
43
+ 'indicatorName' | 'indicatorId' | 'paneId' | 'minHeight'
44
+ >,
45
+ ) => {
46
+ chart.createIndicator(
47
+ { name: params.indicatorName, id: params.indicatorId },
48
+ true,
49
+ {
50
+ id: params.paneId,
51
+ minHeight: params.minHeight ?? 100,
52
+ },
53
+ );
54
+ };
55
+
56
+ const ensureIndicator = (
57
+ chart: Chart,
58
+ params: Pick<
59
+ UseManagedIndicatorParams,
60
+ 'indicatorName' | 'indicatorId' | 'paneId' | 'minHeight'
61
+ >,
62
+ ) => {
63
+ if (hasIndicator(chart, { indicatorId: params.indicatorId })) return;
64
+ createIndicator(chart, params);
65
+ };
66
+
67
+ export const useManagedIndicator = ({
68
+ chart,
69
+ enabled,
70
+ indicatorName,
71
+ indicatorId,
72
+ paneId,
73
+ minHeight = 100,
74
+ template,
75
+ calc,
76
+ updateDeps,
77
+ }: UseManagedIndicatorParams) => {
78
+ const [registered, setRegistered] = useState(false);
79
+ const updateDepsKey = JSON.stringify(updateDeps);
80
+
81
+ useEffect(() => {
82
+ if (registered) return;
83
+
84
+ registerIndicator({
85
+ name: indicatorName,
86
+ shortName: template.shortName,
87
+ calcParams: template.calcParams,
88
+ figures: template.figures as any,
89
+ calc,
90
+ });
91
+
92
+ setRegistered(true);
93
+ }, [registered, indicatorName, template, calc]);
94
+
95
+ useEffect(() => {
96
+ if (!registered || !chart) return;
97
+
98
+ if (!enabled) {
99
+ removeIndicator(chart, { indicatorId, indicatorName });
100
+ return;
101
+ }
102
+
103
+ ensureIndicator(chart, { indicatorName, indicatorId, paneId, minHeight });
104
+
105
+ return () => {
106
+ removeIndicator(chart, { indicatorId, indicatorName });
107
+ };
108
+ }, [
109
+ chart,
110
+ enabled,
111
+ registered,
112
+ indicatorName,
113
+ indicatorId,
114
+ paneId,
115
+ minHeight,
116
+ ]);
117
+
118
+ useEffect(() => {
119
+ if (!registered || !chart || !enabled) return;
120
+
121
+ const updated = chart.overrideIndicator({
122
+ name: indicatorName,
123
+ calc,
124
+ });
125
+
126
+ if (!updated) {
127
+ ensureIndicator(chart, { indicatorName, indicatorId, paneId, minHeight });
128
+ }
129
+ }, [
130
+ chart,
131
+ enabled,
132
+ registered,
133
+ indicatorName,
134
+ indicatorId,
135
+ paneId,
136
+ minHeight,
137
+ calc,
138
+ updateDepsKey,
139
+ ]);
140
+ };