@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,533 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import _ from 'lodash';
5
+ import { registerOverlay, registerIndicator, Chart } from 'klinecharts';
6
+ import { KlineChartItem, OrderLogData } from '@tradejs/types';
7
+ import { useBacktest as useBacktestStore } from '@store';
8
+ import {
9
+ TradeZoneMode,
10
+ createTradeZonePointFigure,
11
+ } from '../figures/tradeZonePointFigure';
12
+ import { createBacktestMarkersPointFigure } from '../figures/backtestMarkersPointFigure';
13
+ import { MarkerMeta, MarkerShape } from '../figures/backtestFigureTypes';
14
+ import {
15
+ collectSignalFiguresFromOrderLog,
16
+ drawSignalFigures,
17
+ ensureBaseFigureOverlaysRegistered,
18
+ removeSignalFigures,
19
+ } from '@tradejs/core/figures';
20
+ import '../figures';
21
+
22
+ const green = '#84cc16';
23
+ const red = '#dc2626';
24
+ const darkRed = '#7f1d1d';
25
+ const darkGreen = '#365314';
26
+ const orange = '#fb923c';
27
+ const grayTransparent = 'rgba(156,163,175,0.45)';
28
+ const greenTransparent = 'rgba(132,204,22,0.45)';
29
+ const redTransparent = 'rgba(220,38,38,0.45)';
30
+
31
+ type ChartPoint = { timestamp: number; value: number };
32
+
33
+ interface AlignedOrderEvent {
34
+ event: OrderLogData[number];
35
+ alignedTimestamp: number;
36
+ }
37
+
38
+ interface TradeZone {
39
+ id: string;
40
+ start: ChartPoint;
41
+ tpEnd: ChartPoint;
42
+ slEnd: ChartPoint;
43
+ }
44
+
45
+ const resolveShapeAndColor = (
46
+ eventType: string,
47
+ ): {
48
+ shape: MarkerShape;
49
+ color: string;
50
+ } => {
51
+ switch (eventType) {
52
+ case 'OPEN_LONG':
53
+ return { shape: 'RECT', color: green };
54
+ case 'TAKE_PROFIT_LONG':
55
+ return { shape: 'STAR', color: red };
56
+ case 'CLOSE_LONG':
57
+ return { shape: 'DIAMOND', color: darkRed };
58
+ case 'STOP_LOSS_LONG':
59
+ return { shape: 'CIRCLE', color: darkRed };
60
+
61
+ case 'OPEN_SHORT':
62
+ return { shape: 'RECT', color: red };
63
+ case 'TAKE_PROFIT_SHORT':
64
+ return { shape: 'STAR', color: green };
65
+ case 'CLOSE_SHORT':
66
+ return { shape: 'DIAMOND', color: darkRed };
67
+ case 'STOP_LOSS_SHORT':
68
+ return { shape: 'CIRCLE', color: darkGreen };
69
+
70
+ default:
71
+ return { shape: 'CIRCLE', color: '#ffffff' };
72
+ }
73
+ };
74
+
75
+ const walkCandlesAndEvents = (
76
+ candles: KlineChartItem[],
77
+ rawEvents: OrderLogData,
78
+ ): {
79
+ markersFlat: MarkerMeta[];
80
+ markersByTs: Record<number, MarkerMeta[]>;
81
+ profitByIndex: Array<number | undefined>;
82
+ alignedEvents: AlignedOrderEvent[];
83
+ } => {
84
+ const events = [...rawEvents].sort((a, b) => a.timestamp - b.timestamp);
85
+
86
+ const markersFlat: MarkerMeta[] = [];
87
+ const markersByTs: Record<number, MarkerMeta[]> = {};
88
+ const profitByIndex: Array<number | undefined> = new Array(
89
+ candles.length,
90
+ ).fill(undefined);
91
+ const alignedEvents: AlignedOrderEvent[] = [];
92
+
93
+ let eventCursor = 0;
94
+ let currentAmount: number | undefined = undefined;
95
+
96
+ for (let candleIndex = 0; candleIndex < candles.length; candleIndex++) {
97
+ const candle = candles[candleIndex];
98
+ const currTs = candle.timestamp;
99
+ const prevTs =
100
+ candleIndex > 0 ? candles[candleIndex - 1].timestamp : -Infinity;
101
+
102
+ for (; eventCursor < events.length; eventCursor++) {
103
+ const evt = events[eventCursor];
104
+
105
+ if (evt.timestamp > currTs) {
106
+ break;
107
+ }
108
+
109
+ if (evt.timestamp > prevTs && evt.timestamp <= currTs) {
110
+ const { shape, color } = resolveShapeAndColor(evt.type);
111
+
112
+ const marker: MarkerMeta = {
113
+ shape,
114
+ color,
115
+ timestamp: currTs,
116
+ value: evt.price,
117
+ type: evt.type,
118
+ profit: evt.profit,
119
+ amount: evt.amount,
120
+ tradeIndex: evt.index,
121
+ };
122
+
123
+ markersFlat.push(marker);
124
+
125
+ if (!markersByTs[currTs]) {
126
+ markersByTs[currTs] = [];
127
+ }
128
+ markersByTs[currTs].push(marker);
129
+ alignedEvents.push({ event: evt, alignedTimestamp: currTs });
130
+
131
+ currentAmount = evt.amount;
132
+ continue;
133
+ }
134
+
135
+ if (evt.timestamp <= prevTs) {
136
+ currentAmount = evt.amount;
137
+ continue;
138
+ }
139
+ }
140
+
141
+ profitByIndex[candleIndex] = currentAmount;
142
+ }
143
+
144
+ return { markersFlat, markersByTs, profitByIndex, alignedEvents };
145
+ };
146
+
147
+ const groupMarkersForOverlay = (
148
+ markers: MarkerMeta[],
149
+ ): {
150
+ points: Array<{ timestamp: number; value: number }>;
151
+ groupedExtendData: MarkerMeta[][];
152
+ } => {
153
+ const byKey: Record<
154
+ string,
155
+ { timestamp: number; value: number; items: MarkerMeta[] }
156
+ > = {};
157
+
158
+ for (const marker of markers) {
159
+ const key = `${marker.timestamp}__${marker.value}`;
160
+ if (!byKey[key]) {
161
+ byKey[key] = {
162
+ timestamp: marker.timestamp,
163
+ value: marker.value,
164
+ items: [],
165
+ };
166
+ }
167
+ byKey[key].items.push(marker);
168
+ }
169
+
170
+ const points: Array<{ timestamp: number; value: number }> = [];
171
+ const groupedExtendData: MarkerMeta[][] = [];
172
+
173
+ for (const { timestamp, value, items } of Object.values(byKey)) {
174
+ points.push({ timestamp, value });
175
+ groupedExtendData.push(items);
176
+ }
177
+
178
+ return { points, groupedExtendData };
179
+ };
180
+
181
+ const buildIndicatorData = (
182
+ candles: KlineChartItem[],
183
+ markersByTs: Record<number, MarkerMeta[]>,
184
+ profitByIndex: Array<number | undefined>,
185
+ ): Record<
186
+ number,
187
+ {
188
+ profit?: number;
189
+ startAmount?: number;
190
+ endAmount?: number;
191
+ maxAmount?: number;
192
+ minAmount?: number;
193
+ markers: MarkerMeta[];
194
+ }
195
+ > => {
196
+ const result: Record<
197
+ number,
198
+ {
199
+ profit?: number;
200
+ startAmount?: number;
201
+ endAmount?: number;
202
+ maxAmount?: number;
203
+ minAmount?: number;
204
+ markers: MarkerMeta[];
205
+ }
206
+ > = {};
207
+ const amounts = profitByIndex.filter((value): value is number =>
208
+ Number.isFinite(value),
209
+ );
210
+ const startAmount = amounts[0];
211
+ const endAmount =
212
+ amounts.length > 0 ? amounts[amounts.length - 1] : undefined;
213
+ const maxAmount = amounts.length > 0 ? Math.max(...amounts) : undefined;
214
+ const minAmount = amounts.length > 0 ? Math.min(...amounts) : undefined;
215
+
216
+ for (let i = 0; i < candles.length; i++) {
217
+ const ts = candles[i].timestamp;
218
+ result[ts] = {
219
+ profit: profitByIndex[i],
220
+ startAmount,
221
+ endAmount,
222
+ maxAmount,
223
+ minAmount,
224
+ markers: markersByTs[ts] ?? [],
225
+ };
226
+ }
227
+
228
+ return result;
229
+ };
230
+
231
+ let backtestMarkersRegistered = false;
232
+ let backtestTradeZonesRegistered = false;
233
+
234
+ const ensureBacktestMarkersRegistered = () => {
235
+ if (backtestMarkersRegistered) return;
236
+
237
+ registerOverlay({
238
+ name: 'backtestMarkers',
239
+ totalStep: 1,
240
+ createPointFigures: createBacktestMarkersPointFigure,
241
+ });
242
+
243
+ backtestMarkersRegistered = true;
244
+ };
245
+
246
+ const ensureBacktestTradeZonesRegistered = () => {
247
+ if (backtestTradeZonesRegistered) return;
248
+
249
+ registerOverlay({
250
+ name: 'BacktestTradeZone',
251
+ totalStep: 2,
252
+ needDefaultPointFigure: false,
253
+ needDefaultXAxisFigure: false,
254
+ needDefaultYAxisFigure: false,
255
+ createPointFigures: createTradeZonePointFigure,
256
+ });
257
+
258
+ backtestTradeZonesRegistered = true;
259
+ };
260
+
261
+ const buildBacktestTradeZones = (
262
+ alignedEvents: AlignedOrderEvent[],
263
+ ): TradeZone[] => {
264
+ const trades = new Map<
265
+ string,
266
+ { open?: AlignedOrderEvent; lastClose?: AlignedOrderEvent }
267
+ >();
268
+
269
+ for (const aligned of alignedEvents) {
270
+ const signalId = aligned.event.signal?.signalId;
271
+ if (!signalId) continue;
272
+
273
+ const current = trades.get(signalId) ?? {};
274
+ const isOpen = aligned.event.type.startsWith('OPEN_');
275
+
276
+ if (isOpen) {
277
+ current.open = aligned;
278
+ } else {
279
+ current.lastClose = aligned;
280
+ }
281
+
282
+ trades.set(signalId, current);
283
+ }
284
+
285
+ const zones: TradeZone[] = [];
286
+
287
+ for (const [signalId, trade] of trades.entries()) {
288
+ const open = trade.open;
289
+ const close = trade.lastClose;
290
+ if (!open || !close) continue;
291
+
292
+ const prices = open.event.signal?.prices;
293
+ if (!prices) continue;
294
+
295
+ zones.push({
296
+ id: signalId,
297
+ start: {
298
+ timestamp: open.alignedTimestamp,
299
+ value: open.event.price,
300
+ },
301
+ tpEnd: {
302
+ timestamp: close.alignedTimestamp,
303
+ value: prices.takeProfitPrice,
304
+ },
305
+ slEnd: {
306
+ timestamp: close.alignedTimestamp,
307
+ value: prices.stopLossPrice,
308
+ },
309
+ });
310
+ }
311
+
312
+ return zones;
313
+ };
314
+
315
+ const createBacktestProfit = (
316
+ chart: Chart,
317
+ latestByTs: Record<
318
+ number,
319
+ {
320
+ profit?: number;
321
+ startAmount?: number;
322
+ endAmount?: number;
323
+ maxAmount?: number;
324
+ minAmount?: number;
325
+ markers: MarkerMeta[];
326
+ }
327
+ > = {},
328
+ ) => {
329
+ registerIndicator({
330
+ name: 'BacktestProfit',
331
+ shortName: 'Backtest',
332
+ series: 'price',
333
+ figures: [
334
+ {
335
+ key: 'profit',
336
+ title: 'Profit',
337
+ type: 'line',
338
+ },
339
+ {
340
+ key: 'startAmount',
341
+ title: 'Start: ',
342
+ type: 'line',
343
+ styles: () =>
344
+ ({
345
+ color: grayTransparent,
346
+ size: 1,
347
+ style: 'dashed',
348
+ dashedValue: [4, 4],
349
+ }) as any,
350
+ },
351
+ {
352
+ key: 'endAmount',
353
+ title: 'End: ',
354
+ type: 'line',
355
+ styles: () =>
356
+ ({
357
+ color: grayTransparent,
358
+ size: 1,
359
+ style: 'dashed',
360
+ dashedValue: [4, 4],
361
+ }) as any,
362
+ },
363
+ {
364
+ key: 'maxAmount',
365
+ title: 'Max: ',
366
+ type: 'line',
367
+ styles: () =>
368
+ ({
369
+ color: greenTransparent,
370
+ size: 1,
371
+ style: 'dashed',
372
+ dashedValue: [4, 4],
373
+ }) as any,
374
+ },
375
+ {
376
+ key: 'minAmount',
377
+ title: 'Min: ',
378
+ type: 'line',
379
+ styles: () =>
380
+ ({
381
+ color: redTransparent,
382
+ size: 1,
383
+ style: 'dashed',
384
+ dashedValue: [4, 4],
385
+ }) as any,
386
+ },
387
+ ],
388
+
389
+ calc: () => latestByTs,
390
+
391
+ createTooltipDataSource: ({ indicator, crosshair }) => {
392
+ const result = indicator.result as typeof latestByTs;
393
+ const ts = crosshair.kLineData?.timestamp;
394
+ const bucket = ts ? result[ts] : undefined;
395
+
396
+ const legends: Array<{
397
+ title: string;
398
+ value: { text: string; color: string };
399
+ }> = [];
400
+
401
+ if (bucket && bucket.profit !== undefined) {
402
+ legends.push({
403
+ title: 'amount: ',
404
+ value: {
405
+ text: bucket.profit.toFixed(2),
406
+ color: orange,
407
+ },
408
+ });
409
+ }
410
+
411
+ if (bucket && bucket.markers.length > 0) {
412
+ for (const meta of bucket.markers) {
413
+ legends.push({
414
+ title: `${meta.tradeIndex}:type: `,
415
+ value: { text: meta.type, color: 'white' },
416
+ });
417
+
418
+ legends.push({
419
+ title: `${meta.tradeIndex}:profit: `,
420
+ value: {
421
+ text: meta.profit.toFixed(2),
422
+ color: meta.profit >= 0 ? green : red,
423
+ },
424
+ });
425
+ }
426
+ }
427
+
428
+ return {
429
+ name: 'Backtest',
430
+ calcParamsText: '',
431
+ features: [],
432
+ legends,
433
+ };
434
+ },
435
+ });
436
+
437
+ chart.createIndicator('BacktestProfit', false);
438
+ };
439
+
440
+ export const useBacktest = (chart: Chart | null, id: string | undefined) => {
441
+ const { backtest } = useBacktestStore(id);
442
+ const enabled = Boolean(id);
443
+ const candlesLength = chart?.getDataList()?.length || 0;
444
+
445
+ useEffect(() => {
446
+ if (!chart || !enabled || _.isEmpty(backtest)) {
447
+ return;
448
+ }
449
+
450
+ const candles = chart.getDataList() as KlineChartItem[];
451
+ if (!candles || candles.length === 0) {
452
+ return;
453
+ }
454
+
455
+ const { markersFlat, markersByTs, profitByIndex, alignedEvents } =
456
+ walkCandlesAndEvents(candles, backtest);
457
+
458
+ const { points, groupedExtendData } = groupMarkersForOverlay(markersFlat);
459
+ const tradeZones = buildBacktestTradeZones(alignedEvents);
460
+ const signalFigures = collectSignalFiguresFromOrderLog(backtest);
461
+ const signalFigureOverlays: ReturnType<typeof drawSignalFigures> = [];
462
+ const tradeZoneOverlayIds: string[] = [];
463
+
464
+ ensureBacktestMarkersRegistered();
465
+ if (points.length > 0) {
466
+ chart.createOverlay({
467
+ name: 'backtestMarkers',
468
+ points,
469
+ extendData: groupedExtendData,
470
+ });
471
+ }
472
+
473
+ if (tradeZones.length > 0) {
474
+ ensureBacktestTradeZonesRegistered();
475
+
476
+ for (const zone of tradeZones) {
477
+ const tpId = `backtest-trade-zone-${zone.id}-tp`;
478
+ const slId = `backtest-trade-zone-${zone.id}-sl`;
479
+ tradeZoneOverlayIds.push(tpId, slId);
480
+
481
+ chart.createOverlay({
482
+ name: 'BacktestTradeZone',
483
+ id: tpId,
484
+ points: [zone.start, zone.tpEnd],
485
+ zLevel: 2,
486
+ extendData: { mode: 'TP' satisfies TradeZoneMode },
487
+ });
488
+
489
+ chart.createOverlay({
490
+ name: 'BacktestTradeZone',
491
+ id: slId,
492
+ points: [zone.start, zone.slEnd],
493
+ zLevel: 2,
494
+ extendData: { mode: 'SL' satisfies TradeZoneMode },
495
+ });
496
+ }
497
+ }
498
+
499
+ if (signalFigures.length > 0) {
500
+ ensureBaseFigureOverlaysRegistered();
501
+
502
+ for (const item of signalFigures) {
503
+ const idPrefix = `backtest-entry-model-${item.signalId ?? `idx-${item.index}`}`;
504
+ signalFigureOverlays.push(
505
+ ...drawSignalFigures({
506
+ chart,
507
+ idPrefix,
508
+ figures: item.figures,
509
+ }),
510
+ );
511
+ }
512
+ }
513
+
514
+ const latestByTs = buildIndicatorData(candles, markersByTs, profitByIndex);
515
+
516
+ createBacktestProfit(chart, latestByTs);
517
+
518
+ return () => {
519
+ chart.removeOverlay({ name: 'backtestMarkers' });
520
+ chart.removeIndicator({ name: 'BacktestProfit' });
521
+ if (signalFigureOverlays.length > 0) {
522
+ removeSignalFigures(chart, signalFigureOverlays);
523
+ }
524
+ if (tradeZoneOverlayIds.length > 0) {
525
+ for (const overlayId of tradeZoneOverlayIds) {
526
+ chart.removeOverlay({ name: 'BacktestTradeZone', id: overlayId });
527
+ }
528
+ }
529
+ };
530
+ }, [chart, enabled, backtest, id, candlesLength]);
531
+
532
+ return null;
533
+ };
@@ -0,0 +1,74 @@
1
+ import _ from 'lodash';
2
+ import { useEffect } from 'react';
3
+ import { BollingerBands } from 'technicalindicators';
4
+ import { registerIndicator, Chart } from 'klinecharts';
5
+
6
+ export const useBbIndicator = (
7
+ chart: Chart | null,
8
+ enabled: boolean,
9
+ periods: number[],
10
+ ) => {
11
+ useEffect(() => {
12
+ registerIndicator({
13
+ name: 'BB',
14
+ shortName: 'BB',
15
+ calcParams: periods,
16
+ figures: [
17
+ ...periods.map((period) => ({
18
+ key: `BBLower${period}`,
19
+ title: `BollingerBands Lower ${period}: `,
20
+ type: 'line',
21
+ })),
22
+ ...periods.map((period) => ({
23
+ key: `BBUpper${period}`,
24
+ title: `BollingerBands Upper ${period}: `,
25
+ type: 'line',
26
+ })),
27
+ ],
28
+
29
+ // Calculation results
30
+ calc: (kLineDataList) => {
31
+ const closes = kLineDataList.map((item) => item.close);
32
+
33
+ const values = periods.map((period) =>
34
+ BollingerBands.calculate({
35
+ period,
36
+ stdDev: 3,
37
+ values: closes,
38
+ }),
39
+ );
40
+
41
+ return kLineDataList.reduce<Record<number, Record<string, number>>>(
42
+ (acc, { timestamp }, candleIndex) => {
43
+ const bb: Record<string, number> = {};
44
+ periods.forEach((period, periodIndex) => {
45
+ if (candleIndex >= period - 1) {
46
+ bb[`BBUpper${period}`] =
47
+ values[periodIndex][candleIndex - (period - 1)].upper;
48
+ bb[`BBLower${period}`] =
49
+ values[periodIndex][candleIndex - (period - 1)].lower;
50
+ }
51
+ });
52
+
53
+ acc[timestamp] = bb;
54
+
55
+ return acc;
56
+ },
57
+ {},
58
+ );
59
+ },
60
+ });
61
+ }, [periods]);
62
+
63
+ useEffect(() => {
64
+ if (!chart || !enabled) {
65
+ return () => null;
66
+ }
67
+
68
+ chart.createIndicator('BB', false, { id: 'candle_pane' });
69
+
70
+ return () => {
71
+ chart.removeIndicator({ name: 'BB' });
72
+ };
73
+ }, [chart, enabled]);
74
+ };