@tradejs/app 1.0.9 → 1.0.10

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 (119) hide show
  1. package/README.md +7 -2
  2. package/bin/tradejs-app.mjs +146 -2
  3. package/next.config.mjs +0 -14
  4. package/package.json +35 -19
  5. package/src/app/actions/backtest.ts +54 -0
  6. package/src/app/actions/kline.ts +10 -5
  7. package/src/app/actions/scanner.ts +8 -3
  8. package/src/app/actions/strategies.ts +39 -0
  9. package/src/app/api/ai/route.ts +5 -4
  10. package/src/app/api/auth/[...nextauth]/route.ts +1 -1
  11. package/src/app/api/backtest/configs/route.ts +25 -0
  12. package/src/app/api/backtest/files/route.ts +5 -2
  13. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +26 -2
  14. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +27 -2
  15. package/src/app/api/backtest/runs/[jobId]/route.ts +124 -0
  16. package/src/app/api/backtest/runs/route.ts +45 -0
  17. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +19 -2
  18. package/src/app/api/derivatives/summary/route.ts +8 -1
  19. package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
  20. package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
  21. package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
  22. package/src/app/api/scanner/[provider]/route.ts +30 -12
  23. package/src/app/api/scanner/route.ts +3 -3
  24. package/src/app/api/signal/[symbol]/[signalId]/route.ts +1 -1
  25. package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
  26. package/src/app/api/strategies/ai/route.ts +139 -0
  27. package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
  28. package/src/app/api/strategies/replay/route.ts +44 -0
  29. package/src/app/api/strategies/runtime/route.ts +831 -0
  30. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
  31. package/src/app/api/user/runtime-deployments/route.ts +94 -0
  32. package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
  33. package/src/app/api/user/settings/route.ts +18 -1
  34. package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
  35. package/src/app/api/user/trading-accounts/route.ts +134 -0
  36. package/src/app/components/Backtest/CompareList/index.tsx +1 -1
  37. package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
  38. package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
  39. package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
  40. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
  41. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
  42. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
  43. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
  44. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
  45. package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
  46. package/src/app/components/Backtest/TestCard/Root/index.tsx +1 -1
  47. package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
  48. package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
  49. package/src/app/components/Backtest/TestCard/index.ts +6 -2
  50. package/src/app/components/Backtest/TestList/index.tsx +4 -9
  51. package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
  52. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
  53. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
  54. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
  55. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
  56. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
  57. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
  58. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
  59. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
  60. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
  61. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
  62. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
  63. package/src/app/components/Dashboard/KlineChart/index.tsx +16 -6
  64. package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
  65. package/src/app/components/Shared/AppShell.tsx +1 -1
  66. package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
  67. package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
  68. package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
  69. package/src/app/components/Shared/Filters/Backtest/index.tsx +33 -48
  70. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
  71. package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
  72. package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
  73. package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
  74. package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
  75. package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
  76. package/src/app/components/Shared/Filters/index.ts +2 -0
  77. package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
  78. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
  79. package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
  80. package/src/app/components/Shared/Sidebar/index.tsx +17 -3
  81. package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
  82. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
  83. package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
  84. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
  85. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
  86. package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
  87. package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
  88. package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
  89. package/src/app/components/UI/Segment/index.tsx +8 -1
  90. package/src/app/components/UI/Select/index.tsx +24 -13
  91. package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
  92. package/src/app/layout.tsx +1 -1
  93. package/src/app/lib/backtestJobs.ts +792 -0
  94. package/src/app/lib/cardPageLayout.ts +1 -0
  95. package/src/app/lib/connectorCreator.ts +28 -0
  96. package/src/app/lib/currentUser.ts +1 -1
  97. package/src/app/lib/errorMessage.ts +34 -0
  98. package/src/app/lib/marketDefaults.ts +12 -0
  99. package/src/app/lib/marketRoutes.ts +58 -0
  100. package/src/app/lib/runtimeStrategies.ts +1376 -0
  101. package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
  102. package/src/app/provider.tsx +1 -1
  103. package/src/app/routes/backtest/page.tsx +1051 -304
  104. package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +35 -15
  105. package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
  106. package/src/app/routes/dashboard/page.tsx +15 -3
  107. package/src/app/routes/derivatives/page.tsx +1222 -165
  108. package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
  109. package/src/app/routes/strategies/[mode]/page.tsx +20 -0
  110. package/src/app/routes/strategies/page.tsx +7 -0
  111. package/src/app/store/ai.ts +1 -1
  112. package/src/app/store/data.ts +73 -17
  113. package/src/app/store/filters.ts +1 -0
  114. package/src/app/store/marketKlineStream.ts +87 -0
  115. package/src/app/store/tests.ts +1 -1
  116. package/src/app/store/tickers.ts +59 -22
  117. package/tsconfig.json +17 -12
  118. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
  119. package/src/app/routes/backtest/[test]/page.tsx +0 -33
@@ -0,0 +1,629 @@
1
+ 'use client';
2
+
3
+ import { type ReactNode, useCallback, useMemo, useState } from 'react';
4
+ import AutoSizer from 'react-virtualized-auto-sizer';
5
+ import { FixedSizeList, type ListChildComponentProps } from 'react-window';
6
+ import {
7
+ Badge,
8
+ Box,
9
+ Button,
10
+ CloseButton,
11
+ Drawer,
12
+ Flex,
13
+ Portal,
14
+ SimpleGrid,
15
+ Text,
16
+ } from '@chakra-ui/react';
17
+
18
+ export interface OrdersDrawerSummaryItem {
19
+ title: string;
20
+ value: ReactNode;
21
+ color?: string;
22
+ }
23
+
24
+ export interface OrdersDrawerMetric {
25
+ title: string;
26
+ value: ReactNode;
27
+ detail?: ReactNode;
28
+ color?: string;
29
+ }
30
+
31
+ export interface OrdersDrawerOrder {
32
+ id: string;
33
+ title: string;
34
+ reference?: ReactNode;
35
+ subtitle?: ReactNode;
36
+ period?: {
37
+ start: number | null | undefined;
38
+ end?: number | null;
39
+ durationHours?: number | null;
40
+ };
41
+ direction?: 'LONG' | 'SHORT' | null;
42
+ status?: 'active' | 'closed';
43
+ statusLabel?: string;
44
+ statusColor?: string;
45
+ pnl?: number | null;
46
+ accentColor?: string;
47
+ metrics: OrdersDrawerMetric[];
48
+ }
49
+
50
+ interface OrdersDrawerPanelProps {
51
+ title: string;
52
+ open: boolean;
53
+ orders: OrdersDrawerOrder[];
54
+ emptyText?: string;
55
+ rowHeight?: number;
56
+ showStatusFilter?: boolean;
57
+ statusFilterOptions?: readonly {
58
+ label: string;
59
+ value: OrderStatusFilter;
60
+ }[];
61
+ onOpenChange: (open: boolean) => void;
62
+ }
63
+
64
+ const DEFAULT_ROW_HEIGHT = 278;
65
+ const ORDER_LIST_OVERSCAN = 4;
66
+ type OrderStatusFilter = 'all' | 'closed' | 'active';
67
+ type OrderDirectionFilter = 'all' | 'LONG' | 'SHORT';
68
+
69
+ const orderStatusFilterOptions = [
70
+ { label: 'All', value: 'all' },
71
+ { label: 'Closed', value: 'closed' },
72
+ { label: 'Active', value: 'active' },
73
+ ] as const;
74
+
75
+ const orderDirectionFilterOptions = [
76
+ { label: 'All', value: 'all' },
77
+ { label: 'Long', value: 'LONG' },
78
+ { label: 'Short', value: 'SHORT' },
79
+ ] as const;
80
+
81
+ export const formatDateTime = (value: number | null | undefined) => {
82
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
83
+ return 'n/a';
84
+ }
85
+
86
+ return new Date(value).toLocaleString('ru-RU');
87
+ };
88
+
89
+ export const formatSignedNumber = (value: number | null | undefined) => {
90
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
91
+ return 'n/a';
92
+ }
93
+
94
+ return `${value > 0 ? '+' : ''}${value.toLocaleString('ru-RU', {
95
+ maximumFractionDigits: 2,
96
+ minimumFractionDigits: 2,
97
+ })}`;
98
+ };
99
+
100
+ export const formatCompactNumber = (
101
+ value: number | null | undefined,
102
+ options: Intl.NumberFormatOptions = {},
103
+ ) => {
104
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
105
+ return 'n/a';
106
+ }
107
+
108
+ return value.toLocaleString('ru-RU', {
109
+ maximumFractionDigits: 8,
110
+ ...options,
111
+ });
112
+ };
113
+
114
+ export const formatUsdt = (
115
+ value: number | null | undefined,
116
+ {
117
+ signed = false,
118
+ maximumFractionDigits = 2,
119
+ minimumFractionDigits = 2,
120
+ }: Intl.NumberFormatOptions & { signed?: boolean } = {},
121
+ ) => {
122
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
123
+ return 'n/a';
124
+ }
125
+
126
+ const prefix = signed && value > 0 ? '+' : '';
127
+ return `${prefix}${value.toLocaleString('ru-RU', {
128
+ maximumFractionDigits,
129
+ minimumFractionDigits,
130
+ })} USDT`;
131
+ };
132
+
133
+ export const formatPriceUsdt = (value: number | null | undefined) =>
134
+ formatUsdt(value, {
135
+ maximumFractionDigits: 8,
136
+ minimumFractionDigits: 0,
137
+ });
138
+
139
+ export const formatInteger = (value: number | null | undefined) => {
140
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
141
+ return 'n/a';
142
+ }
143
+
144
+ return value.toLocaleString('ru-RU', {
145
+ maximumFractionDigits: 0,
146
+ });
147
+ };
148
+
149
+ export const formatDuration = (hours: number | null | undefined) => {
150
+ if (typeof hours !== 'number' || !Number.isFinite(hours)) {
151
+ return 'n/a';
152
+ }
153
+
154
+ if (hours < 24) {
155
+ return `${formatCompactNumber(hours, {
156
+ maximumFractionDigits: 1,
157
+ minimumFractionDigits: 1,
158
+ })}h`;
159
+ }
160
+
161
+ const days = Math.floor(hours / 24);
162
+ const remainderHours = Math.round(hours % 24);
163
+ return `${days}d ${remainderHours}h`;
164
+ };
165
+
166
+ export const formatOrderPeriod = ({
167
+ start,
168
+ end,
169
+ durationHours,
170
+ }: NonNullable<OrdersDrawerOrder['period']>) => {
171
+ const startLabel = formatDateTime(start);
172
+ if (typeof end !== 'number' || !Number.isFinite(end)) {
173
+ return startLabel;
174
+ }
175
+
176
+ const durationLabel =
177
+ typeof durationHours === 'number' && Number.isFinite(durationHours)
178
+ ? ` (${formatDuration(durationHours)})`
179
+ : '';
180
+
181
+ return `${startLabel} - ${formatDateTime(end)}${durationLabel}`;
182
+ };
183
+
184
+ export const formatFee = (value: number | null | undefined) => {
185
+ return formatUsdt(value, {
186
+ signed: true,
187
+ maximumFractionDigits: 8,
188
+ minimumFractionDigits: 0,
189
+ });
190
+ };
191
+
192
+ export const formatPercent = (
193
+ value: number | null | undefined,
194
+ { signed = false }: { signed?: boolean } = {},
195
+ ) => {
196
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
197
+ return 'n/a';
198
+ }
199
+
200
+ const prefix = signed && value > 0 ? '+' : '';
201
+ return `${prefix}${value.toLocaleString('ru-RU', {
202
+ maximumFractionDigits: 2,
203
+ minimumFractionDigits: 2,
204
+ })}%`;
205
+ };
206
+
207
+ export const getPnlColor = (value: number | null | undefined) => {
208
+ if (typeof value !== 'number' || !Number.isFinite(value) || value === 0) {
209
+ return 'gray.300';
210
+ }
211
+
212
+ return value > 0 ? 'teal.300' : 'red.300';
213
+ };
214
+
215
+ const getDirectionPalette = (direction: OrdersDrawerOrder['direction']) =>
216
+ direction === 'LONG' ? 'teal' : 'red';
217
+
218
+ const filterOrders = ({
219
+ orders,
220
+ statusFilter,
221
+ directionFilter,
222
+ }: {
223
+ orders: OrdersDrawerOrder[];
224
+ statusFilter: OrderStatusFilter;
225
+ directionFilter: OrderDirectionFilter;
226
+ }) =>
227
+ orders.filter((order) => {
228
+ if (statusFilter !== 'all' && order.status !== statusFilter) {
229
+ return false;
230
+ }
231
+
232
+ if (directionFilter !== 'all' && order.direction !== directionFilter) {
233
+ return false;
234
+ }
235
+
236
+ return true;
237
+ });
238
+
239
+ interface OrdersFilterGroupProps<TValue extends string> {
240
+ label: string;
241
+ options: readonly { label: string; value: TValue }[];
242
+ value: TValue;
243
+ onChange: (value: TValue) => void;
244
+ }
245
+
246
+ const OrdersFilterGroup = <TValue extends string>({
247
+ label,
248
+ options,
249
+ value,
250
+ onChange,
251
+ }: OrdersFilterGroupProps<TValue>) => (
252
+ <Flex alignItems="center" gap={2} minW="0">
253
+ <Text
254
+ color="gray.500"
255
+ fontSize="2xs"
256
+ fontWeight="bold"
257
+ textTransform="uppercase"
258
+ whiteSpace="nowrap"
259
+ >
260
+ {label}
261
+ </Text>
262
+ <Flex
263
+ borderWidth="1px"
264
+ borderColor="gray.800"
265
+ borderRadius="md"
266
+ overflow="hidden"
267
+ flex="0 0 auto"
268
+ >
269
+ {options.map((option) => {
270
+ const selected = option.value === value;
271
+
272
+ return (
273
+ <Button
274
+ key={option.value}
275
+ size="xs"
276
+ variant={selected ? 'solid' : 'ghost'}
277
+ colorPalette={selected ? 'teal' : 'gray'}
278
+ borderRadius="0"
279
+ minW="54px"
280
+ h="8"
281
+ px={3}
282
+ fontFamily="mono"
283
+ letterSpacing="0"
284
+ onClick={() => onChange(option.value)}
285
+ >
286
+ {option.label}
287
+ </Button>
288
+ );
289
+ })}
290
+ </Flex>
291
+ </Flex>
292
+ );
293
+
294
+ const OrdersFilters = ({
295
+ statusFilter,
296
+ directionFilter,
297
+ showStatusFilter,
298
+ statusFilterOptions,
299
+ onStatusFilterChange,
300
+ onDirectionFilterChange,
301
+ }: {
302
+ statusFilter: OrderStatusFilter;
303
+ directionFilter: OrderDirectionFilter;
304
+ showStatusFilter: boolean;
305
+ statusFilterOptions: readonly {
306
+ label: string;
307
+ value: OrderStatusFilter;
308
+ }[];
309
+ onStatusFilterChange: (value: OrderStatusFilter) => void;
310
+ onDirectionFilterChange: (value: OrderDirectionFilter) => void;
311
+ }) => (
312
+ <Flex
313
+ px={1}
314
+ pb={4}
315
+ gap={3}
316
+ alignItems="center"
317
+ justifyContent="space-between"
318
+ wrap="wrap"
319
+ flex="0 0 auto"
320
+ >
321
+ {showStatusFilter ? (
322
+ <OrdersFilterGroup
323
+ label="Status"
324
+ options={statusFilterOptions}
325
+ value={statusFilter}
326
+ onChange={onStatusFilterChange}
327
+ />
328
+ ) : null}
329
+ <OrdersFilterGroup
330
+ label="Type"
331
+ options={orderDirectionFilterOptions}
332
+ value={directionFilter}
333
+ onChange={onDirectionFilterChange}
334
+ />
335
+ </Flex>
336
+ );
337
+
338
+ const OrderMetric = ({
339
+ title,
340
+ value,
341
+ detail,
342
+ color = 'gray.300',
343
+ }: OrdersDrawerMetric) => (
344
+ <Box minW="0">
345
+ <Text
346
+ fontSize="2xs"
347
+ color="gray.500"
348
+ textTransform="uppercase"
349
+ fontWeight="bold"
350
+ >
351
+ {title}
352
+ </Text>
353
+ <Text
354
+ mt={1}
355
+ color={color}
356
+ fontSize="sm"
357
+ fontWeight="semibold"
358
+ fontFamily="mono"
359
+ whiteSpace="nowrap"
360
+ overflow="hidden"
361
+ textOverflow="ellipsis"
362
+ >
363
+ {value}
364
+ </Text>
365
+ {detail ? (
366
+ <Box
367
+ mt={1}
368
+ color="gray.500"
369
+ fontSize="xs"
370
+ lineHeight="1.3"
371
+ wordBreak="break-word"
372
+ >
373
+ {detail}
374
+ </Box>
375
+ ) : null}
376
+ </Box>
377
+ );
378
+
379
+ const OrderCard = ({ order }: { order: OrdersDrawerOrder }) => {
380
+ const accentColor = order.accentColor ?? getPnlColor(order.pnl);
381
+ const subtitle = order.period
382
+ ? formatOrderPeriod(order.period)
383
+ : order.subtitle;
384
+
385
+ return (
386
+ <Box
387
+ p={0}
388
+ borderWidth="1px"
389
+ borderColor="gray.800"
390
+ borderLeftWidth="4px"
391
+ borderLeftColor={accentColor}
392
+ borderRadius="md"
393
+ bg="gray.950"
394
+ overflow="hidden"
395
+ >
396
+ <Flex
397
+ px={4}
398
+ py={3}
399
+ alignItems="center"
400
+ justifyContent="space-between"
401
+ gap={4}
402
+ borderBottomWidth="1px"
403
+ borderBottomColor="gray.800"
404
+ >
405
+ <Box minW="0" flex="1">
406
+ <Flex alignItems="center" gap={2} wrap="wrap">
407
+ <Text
408
+ fontSize="md"
409
+ fontWeight="bold"
410
+ color="gray.100"
411
+ lineHeight="1.2"
412
+ >
413
+ {order.title}
414
+ </Text>
415
+ {order.direction ? (
416
+ <Badge
417
+ colorPalette={getDirectionPalette(order.direction)}
418
+ variant="subtle"
419
+ fontFamily="mono"
420
+ letterSpacing="0"
421
+ >
422
+ {order.direction}
423
+ </Badge>
424
+ ) : null}
425
+ {order.statusLabel ? (
426
+ <Badge
427
+ colorPalette={order.statusColor ?? 'gray'}
428
+ variant="subtle"
429
+ fontFamily="mono"
430
+ letterSpacing="0"
431
+ >
432
+ {order.statusLabel}
433
+ </Badge>
434
+ ) : null}
435
+ {order.reference ? (
436
+ <Text color="gray.500" fontSize="xs" fontFamily="mono">
437
+ ref {order.reference}
438
+ </Text>
439
+ ) : null}
440
+ </Flex>
441
+ {subtitle ? (
442
+ <Box mt={1} color="gray.500" fontSize="xs" lineHeight="1.2">
443
+ {subtitle}
444
+ </Box>
445
+ ) : null}
446
+ </Box>
447
+
448
+ <Flex
449
+ justifyContent="flex-end"
450
+ alignItems="baseline"
451
+ gap={2}
452
+ flex="0 0 auto"
453
+ minW="max-content"
454
+ whiteSpace="nowrap"
455
+ >
456
+ <Text
457
+ fontSize="xs"
458
+ color="gray.500"
459
+ textTransform="uppercase"
460
+ lineHeight="1"
461
+ >
462
+ P&amp;L
463
+ </Text>
464
+ <Text
465
+ color={getPnlColor(order.pnl)}
466
+ fontWeight="bold"
467
+ fontSize="xl"
468
+ fontFamily="mono"
469
+ lineHeight="1"
470
+ whiteSpace="nowrap"
471
+ >
472
+ {formatUsdt(order.pnl, { signed: true })}
473
+ </Text>
474
+ </Flex>
475
+ </Flex>
476
+
477
+ <SimpleGrid px={4} py={4} columns={3} columnGap={6} rowGap={4}>
478
+ {order.metrics.map((metric) => (
479
+ <OrderMetric key={metric.title} {...metric} />
480
+ ))}
481
+ </SimpleGrid>
482
+ </Box>
483
+ );
484
+ };
485
+
486
+ const OrdersList = ({
487
+ orders,
488
+ rowHeight,
489
+ }: {
490
+ orders: OrdersDrawerOrder[];
491
+ rowHeight: number;
492
+ }) => {
493
+ const itemKey = useCallback(
494
+ (index: number) => orders[index]?.id ?? String(index),
495
+ [orders],
496
+ );
497
+
498
+ const Row = useCallback(
499
+ ({ index, style }: ListChildComponentProps) => {
500
+ const order = orders[index];
501
+
502
+ if (!order) {
503
+ return null;
504
+ }
505
+
506
+ return (
507
+ <Box style={style} pb={3}>
508
+ <OrderCard order={order} />
509
+ </Box>
510
+ );
511
+ },
512
+ [orders],
513
+ );
514
+
515
+ return (
516
+ <Box flex="1" h="full" minH="0" w="full">
517
+ <AutoSizer>
518
+ {({ height, width }) => (
519
+ <FixedSizeList
520
+ height={height}
521
+ width={width}
522
+ itemCount={orders.length}
523
+ itemSize={rowHeight}
524
+ overscanCount={ORDER_LIST_OVERSCAN}
525
+ itemKey={itemKey}
526
+ >
527
+ {Row}
528
+ </FixedSizeList>
529
+ )}
530
+ </AutoSizer>
531
+ </Box>
532
+ );
533
+ };
534
+
535
+ export const OrdersDrawerPanel = ({
536
+ title,
537
+ open,
538
+ orders,
539
+ emptyText = 'No orders for the selected period.',
540
+ rowHeight = DEFAULT_ROW_HEIGHT,
541
+ showStatusFilter = true,
542
+ statusFilterOptions = orderStatusFilterOptions,
543
+ onOpenChange,
544
+ }: OrdersDrawerPanelProps) => {
545
+ const [statusFilter, setStatusFilter] = useState<OrderStatusFilter>('all');
546
+ const [directionFilter, setDirectionFilter] =
547
+ useState<OrderDirectionFilter>('all');
548
+ const filteredOrders = useMemo(
549
+ () =>
550
+ filterOrders({
551
+ orders,
552
+ statusFilter: showStatusFilter ? statusFilter : 'all',
553
+ directionFilter,
554
+ }),
555
+ [directionFilter, orders, showStatusFilter, statusFilter],
556
+ );
557
+
558
+ return (
559
+ <Drawer.Root
560
+ size="xl"
561
+ open={open}
562
+ onOpenChange={(event) => onOpenChange(event.open)}
563
+ >
564
+ <Portal>
565
+ <Drawer.Backdrop />
566
+ <Drawer.Positioner>
567
+ <Drawer.Content
568
+ display="flex"
569
+ flexDirection="column"
570
+ w="50vw"
571
+ minW="640px"
572
+ maxW="50vw"
573
+ bg="gray.950"
574
+ >
575
+ <Drawer.Header>
576
+ <Drawer.Title>{title}</Drawer.Title>
577
+ <Drawer.CloseTrigger asChild>
578
+ <CloseButton size="sm" />
579
+ </Drawer.CloseTrigger>
580
+ </Drawer.Header>
581
+
582
+ <Drawer.Body
583
+ display="flex"
584
+ flexDirection="column"
585
+ flex="1"
586
+ minH="0"
587
+ overflow="hidden"
588
+ w="full"
589
+ >
590
+ <OrdersFilters
591
+ statusFilter={statusFilter}
592
+ directionFilter={directionFilter}
593
+ showStatusFilter={showStatusFilter}
594
+ statusFilterOptions={statusFilterOptions}
595
+ onStatusFilterChange={setStatusFilter}
596
+ onDirectionFilterChange={setDirectionFilter}
597
+ />
598
+ {orders.length === 0 ? (
599
+ <Box
600
+ p={4}
601
+ borderWidth="1px"
602
+ borderColor="gray.800"
603
+ borderRadius="md"
604
+ >
605
+ <Text fontSize="sm" color="gray.400">
606
+ {emptyText}
607
+ </Text>
608
+ </Box>
609
+ ) : filteredOrders.length === 0 ? (
610
+ <Box
611
+ p={4}
612
+ borderWidth="1px"
613
+ borderColor="gray.800"
614
+ borderRadius="md"
615
+ >
616
+ <Text fontSize="sm" color="gray.400">
617
+ No orders match the selected filters.
618
+ </Text>
619
+ </Box>
620
+ ) : (
621
+ <OrdersList orders={filteredOrders} rowHeight={rowHeight} />
622
+ )}
623
+ </Drawer.Body>
624
+ </Drawer.Content>
625
+ </Drawer.Positioner>
626
+ </Portal>
627
+ </Drawer.Root>
628
+ );
629
+ };