@tradejs/app 3.0.1 → 3.1.1

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 (28) hide show
  1. package/README.md +2 -0
  2. package/package.json +10 -11
  3. package/src/app/actions/strategies.ts +4 -2
  4. package/src/app/api/ai/route.ts +21 -50
  5. package/src/app/api/strategies/runtime/route.ts +1 -1
  6. package/src/app/api/user/runtime-deployments/[deploymentId]/strategies/[strategyName]/control/route.ts +73 -0
  7. package/src/app/api/user/runtime-deployments/route.ts +55 -8
  8. package/src/app/api/user/runtime-strategy-releases/route.ts +60 -0
  9. package/src/app/components/Strategies/RuntimeStrategyCard.presenter.ts +5 -2
  10. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +65 -7
  11. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +46 -3
  12. package/src/app/components/Strategies/RuntimeStrategyStatsDrawer.tsx +1 -2
  13. package/src/app/components/Strategies/StrategyEvidencePopover.tsx +3 -1
  14. package/src/app/lib/connectorCreator.ts +1 -10
  15. package/src/app/lib/runtimeStrategyReleaseService.ts +123 -0
  16. package/src/app/routes/derivatives/DerivativesDashboardView.tsx +900 -0
  17. package/src/app/routes/derivatives/derivativesDashboardConfig.ts +53 -0
  18. package/src/app/routes/derivatives/derivativesDashboardLoader.ts +74 -0
  19. package/src/app/routes/derivatives/page.tsx +18 -1087
  20. package/src/app/routes/derivatives/useDerivativesDashboard.ts +105 -0
  21. package/src/app/routes/strategies/StrategiesPageClient.tsx +5 -20
  22. package/src/app/lib/runtimeDashboard.ts +0 -700
  23. package/src/app/lib/runtimeStrategies.ts +0 -1107
  24. package/src/app/lib/runtimeStrategyContracts.ts +0 -85
  25. package/src/app/lib/runtimeStrategyLineage.ts +0 -264
  26. package/src/app/lib/runtimeTradeReconciliation.ts +0 -113
  27. package/src/app/lib/runtimeTradeSync.ts +0 -280
  28. package/src/app/lib/strategyEvidenceTimeline.ts +0 -298
@@ -1,922 +1,21 @@
1
1
  'use client';
2
2
 
3
- import {
4
- type ReactNode,
5
- useCallback,
6
- useEffect,
7
- useMemo,
8
- useState,
9
- } from 'react';
10
- import {
11
- Badge,
12
- Box,
13
- Card,
14
- ClientOnly,
15
- Flex,
16
- Heading,
17
- SimpleGrid,
18
- Skeleton,
19
- SkeletonText,
20
- Stat,
21
- Table,
22
- Text,
23
- } from '@chakra-ui/react';
24
- import { Chart, useChart } from '@chakra-ui/charts';
25
- import {
26
- Area,
27
- AreaChart,
28
- Bar,
29
- BarChart,
30
- CartesianGrid,
31
- Cell,
32
- Legend,
33
- ReferenceLine,
34
- ResponsiveContainer,
35
- Tooltip,
36
- YAxis,
37
- } from 'recharts';
38
- import { format } from 'date-fns';
39
- import { FiBarChart2 } from 'react-icons/fi';
40
- import { API } from '@tradejs/core/api';
41
- import { buildKlinePath } from '#app/lib/marketRoutes';
42
- import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
43
- import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
44
- import { EmptyState, Segment, Select, toaster } from '#ui';
45
- import {
46
- buildDerivativesDashboardViewModel,
47
- toFiniteNumber,
48
- type DerivativesChartRow,
49
- type DerivativesInterval,
50
- type DetailResponse,
51
- type DetailRow,
52
- type PriceChartRow,
53
- type PriceResponse,
54
- type PriceRow,
55
- type SummaryResponse,
56
- type SymbolMetrics,
57
- } from './derivativesViewModel';
58
-
59
- type ChartWindow = {
60
- startTimestamp: number;
61
- endTimestamp: number;
62
- };
63
-
64
- type SymbolChartTheme = {
65
- primary: string;
66
- primaryNegative: string;
67
- secondary: string;
68
- };
69
-
70
- const HOURS_OPTIONS = [
71
- { label: 'Last 24h', value: '24' },
72
- { label: 'Last 7d', value: '168' },
73
- { label: 'Last 30d', value: '720' },
74
- { label: 'Last 60d', value: '1440' },
75
- { label: 'Last 90d', value: '2160' },
76
- ];
77
-
78
- const INTERVAL_OPTIONS = ['15m', '1h'] as const;
79
-
80
- const FIXED_SYMBOLS = ['BTCUSDT', 'ETHUSDT'] as const;
81
-
82
- const DERIVATIVES_TO_KLINE_INTERVAL = {
83
- '15m': '15',
84
- '1h': '60',
85
- } as const;
86
-
87
- const SYMBOL_THEMES: Record<string, SymbolChartTheme> = {
88
- BTCUSDT: {
89
- primary: 'orange.solid',
90
- primaryNegative: 'red.solid',
91
- secondary: 'orange.solid',
92
- },
93
- ETHUSDT: {
94
- primary: 'teal.solid',
95
- primaryNegative: 'pink.solid',
96
- secondary: 'teal.solid',
97
- },
98
- };
99
-
100
- const compactFormatter = new Intl.NumberFormat('en-US', {
101
- notation: 'compact',
102
- maximumFractionDigits: 2,
103
- });
104
-
105
- const compactSignedFormatter = new Intl.NumberFormat('en-US', {
106
- notation: 'compact',
107
- maximumFractionDigits: 2,
108
- signDisplay: 'always',
109
- });
110
-
111
- const formatCompact = (value: number | null | undefined) => {
112
- const parsed = toFiniteNumber(value);
113
- if (parsed == null) return 'n/a';
114
- return compactFormatter.format(parsed);
115
- };
116
-
117
- const formatSignedCompact = (value: number | null | undefined) => {
118
- const parsed = toFiniteNumber(value);
119
- if (parsed == null) return 'n/a';
120
- return compactSignedFormatter.format(parsed);
121
- };
122
-
123
- const formatPercent = (value: number | null | undefined) => {
124
- const parsed = toFiniteNumber(value);
125
- if (parsed == null) return 'n/a';
126
- return `${parsed >= 0 ? '+' : ''}${parsed.toFixed(2)}%`;
127
- };
128
-
129
- const formatFunding = (value: number | null | undefined) => {
130
- const parsed = toFiniteNumber(value);
131
- if (parsed == null) return 'n/a';
132
- const basisPoints = parsed * 10_000;
133
- return `${basisPoints >= 0 ? '+' : ''}${basisPoints.toFixed(2)} bps`;
134
- };
135
-
136
- const formatAxisCompact = (value: number) =>
137
- compactFormatter.format(Math.abs(value));
138
-
139
- const formatPrice = (value: number | null | undefined) => {
140
- const parsed = toFiniteNumber(value);
141
- if (parsed == null) return 'n/a';
142
-
143
- const digits = parsed >= 1000 ? 2 : parsed >= 1 ? 3 : 6;
144
- return parsed.toLocaleString('en-US', {
145
- maximumFractionDigits: digits,
146
- minimumFractionDigits: 0,
147
- });
148
- };
149
-
150
- const getChartDomain = (
151
- values: Array<number | null | undefined>,
152
- options?: { includeZero?: boolean; minPaddingPct?: number },
153
- ): [number, number] | undefined => {
154
- const finite = values.filter(
155
- (value): value is number =>
156
- typeof value === 'number' && Number.isFinite(value),
157
- );
158
-
159
- if (!finite.length) return undefined;
160
-
161
- let min = Math.min(...finite);
162
- let max = Math.max(...finite);
163
-
164
- if (options?.includeZero) {
165
- min = Math.min(min, 0);
166
- max = Math.max(max, 0);
167
- }
168
-
169
- const span = max - min;
170
- const paddingPct = options?.minPaddingPct ?? 0.06;
171
- const basePadding =
172
- span > 0
173
- ? span * paddingPct
174
- : Math.max(Math.abs(max || min || 1) * paddingPct, 1e-6);
175
-
176
- return [min - basePadding, max + basePadding];
177
- };
178
-
179
- const formatFullTime = (value: string | null | undefined) => {
180
- if (!value) return 'n/a';
181
- const parsed = new Date(value);
182
- if (Number.isNaN(parsed.getTime())) return value;
183
- return format(parsed, 'dd.MM.yyyy HH:mm');
184
- };
185
-
186
- const getValueColor = (value: number | null | undefined) => {
187
- const parsed = toFiniteNumber(value);
188
- if (parsed == null || parsed === 0) return 'gray.200';
189
- return parsed > 0 ? 'teal.300' : 'red.300';
190
- };
191
-
192
- const getFundingColor = (value: number | null | undefined) => {
193
- const parsed = toFiniteNumber(value);
194
- if (parsed == null || parsed === 0) return 'gray.200';
195
- return parsed > 0 ? 'orange.300' : 'teal.300';
196
- };
197
-
198
- const getSymbolLabel = (symbol: string) =>
199
- symbol === 'BTCUSDT' ? 'BTC' : symbol === 'ETHUSDT' ? 'ETH' : symbol;
200
-
201
- const DashboardSkeleton = () => (
202
- <>
203
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
204
- {FIXED_SYMBOLS.map((symbol) => (
205
- <Card.Root
206
- key={symbol}
207
- bg="gray.900"
208
- borderColor="gray.800"
209
- borderWidth="1px"
210
- size="sm"
211
- >
212
- <Card.Header>
213
- <Skeleton height="24px" width="120px" />
214
- </Card.Header>
215
- <Card.Body>
216
- <SimpleGrid columns={{ base: 2, xl: 4 }} gap={4}>
217
- {Array.from({ length: 4 }).map((_, index) => (
218
- <Box key={`${symbol}:${index}`}>
219
- <SkeletonText noOfLines={2} gap="3" mb={2} />
220
- <Skeleton height="18px" width="70%" />
221
- </Box>
222
- ))}
223
- </SimpleGrid>
224
- </Card.Body>
225
- </Card.Root>
226
- ))}
227
- </SimpleGrid>
228
-
229
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
230
- <Card.Root
231
- bg="gray.900"
232
- borderColor="gray.800"
233
- borderWidth="1px"
234
- size="sm"
235
- >
236
- <Card.Header>
237
- <Skeleton height="24px" width="220px" />
238
- </Card.Header>
239
- <Card.Body>
240
- <Skeleton height="280px" />
241
- </Card.Body>
242
- </Card.Root>
243
- <Card.Root
244
- bg="gray.900"
245
- borderColor="gray.800"
246
- borderWidth="1px"
247
- size="sm"
248
- >
249
- <Card.Header>
250
- <Skeleton height="24px" width="220px" />
251
- </Card.Header>
252
- <Card.Body>
253
- <Skeleton height="280px" />
254
- </Card.Body>
255
- </Card.Root>
256
- </SimpleGrid>
257
-
258
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
259
- <Card.Root
260
- bg="gray.900"
261
- borderColor="gray.800"
262
- borderWidth="1px"
263
- size="sm"
264
- >
265
- <Card.Header>
266
- <Skeleton height="24px" width="220px" />
267
- </Card.Header>
268
- <Card.Body>
269
- <Skeleton height="280px" />
270
- </Card.Body>
271
- </Card.Root>
272
- <Card.Root
273
- bg="gray.900"
274
- borderColor="gray.800"
275
- borderWidth="1px"
276
- size="sm"
277
- >
278
- <Card.Header>
279
- <Skeleton height="24px" width="220px" />
280
- </Card.Header>
281
- <Card.Body>
282
- <Skeleton height="280px" />
283
- </Card.Body>
284
- </Card.Root>
285
- </SimpleGrid>
286
-
287
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
288
- <Card.Root
289
- bg="gray.900"
290
- borderColor="gray.800"
291
- borderWidth="1px"
292
- size="sm"
293
- >
294
- <Card.Header>
295
- <Skeleton height="24px" width="220px" />
296
- </Card.Header>
297
- <Card.Body>
298
- <Skeleton height="280px" />
299
- </Card.Body>
300
- </Card.Root>
301
- <Card.Root
302
- bg="gray.900"
303
- borderColor="gray.800"
304
- borderWidth="1px"
305
- size="sm"
306
- >
307
- <Card.Header>
308
- <Skeleton height="24px" width="220px" />
309
- </Card.Header>
310
- <Card.Body>
311
- <Skeleton height="280px" />
312
- </Card.Body>
313
- </Card.Root>
314
- </SimpleGrid>
315
-
316
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
317
- <Card.Root
318
- bg="gray.900"
319
- borderColor="gray.800"
320
- borderWidth="1px"
321
- size="sm"
322
- >
323
- <Card.Header>
324
- <Skeleton height="24px" width="220px" />
325
- </Card.Header>
326
- <Card.Body>
327
- <Skeleton height="320px" />
328
- </Card.Body>
329
- </Card.Root>
330
- <Card.Root
331
- bg="gray.900"
332
- borderColor="gray.800"
333
- borderWidth="1px"
334
- size="sm"
335
- >
336
- <Card.Header>
337
- <Skeleton height="24px" width="260px" />
338
- </Card.Header>
339
- <Card.Body>
340
- <Skeleton height="320px" />
341
- </Card.Body>
342
- </Card.Root>
343
- </SimpleGrid>
344
-
345
- <Card.Root bg="gray.900" borderColor="gray.800" borderWidth="1px" size="sm">
346
- <Card.Header>
347
- <Skeleton height="24px" width="180px" />
348
- </Card.Header>
349
- <Card.Body>
350
- <SkeletonText noOfLines={8} gap="5" />
351
- </Card.Body>
352
- </Card.Root>
353
- </>
354
- );
355
-
356
- const SymbolMetricsCard = ({
357
- title,
358
- metrics,
359
- }: {
360
- title: string;
361
- metrics: SymbolMetrics;
362
- }) => (
363
- <Card.Root bg="gray.900" borderColor="gray.800" borderWidth="1px" size="sm">
364
- <Card.Header pb={0}>
365
- <Heading size="md">{title}</Heading>
366
- <Text mt={1} color="gray.500" fontSize="sm">
367
- Updated {formatFullTime(metrics.lastTs)}
368
- </Text>
369
- </Card.Header>
370
- <Card.Body>
371
- <SimpleGrid columns={{ base: 2, xl: 4 }} gap={4}>
372
- <Stat.Root>
373
- <Stat.Label color="gray.400">Open Interest</Stat.Label>
374
- <Stat.ValueText color={getValueColor(metrics.oiChange)}>
375
- {formatCompact(metrics.currentOpenInterest)}
376
- </Stat.ValueText>
377
- </Stat.Root>
378
- <Stat.Root>
379
- <Stat.Label color="gray.400">OI Change</Stat.Label>
380
- <Stat.ValueText color={getValueColor(metrics.oiChangePct)}>
381
- {formatPercent(metrics.oiChangePct)}
382
- </Stat.ValueText>
383
- <Stat.HelpText color="gray.500">
384
- {formatSignedCompact(metrics.oiChange)}
385
- </Stat.HelpText>
386
- </Stat.Root>
387
- <Stat.Root>
388
- <Stat.Label color="gray.400">Funding</Stat.Label>
389
- <Stat.ValueText color={getFundingColor(metrics.currentFundingRate)}>
390
- {formatFunding(metrics.currentFundingRate)}
391
- </Stat.ValueText>
392
- <Stat.HelpText color="gray.500">
393
- Delta {formatFunding(metrics.fundingChange)}
394
- </Stat.HelpText>
395
- </Stat.Root>
396
- <Stat.Root>
397
- <Stat.Label color="gray.400">Liquidations</Stat.Label>
398
- <Stat.ValueText color="gray.200">
399
- {formatCompact(metrics.sumLiqTotal)}
400
- </Stat.ValueText>
401
- <Stat.HelpText color="gray.500">
402
- Long {formatCompact(metrics.sumLiqLong)} / Short{' '}
403
- {formatCompact(metrics.sumLiqShort)}
404
- </Stat.HelpText>
405
- </Stat.Root>
406
- </SimpleGrid>
407
- </Card.Body>
408
- </Card.Root>
409
- );
410
-
411
- const ChartCard = ({
412
- title,
413
- description,
414
- children,
415
- }: {
416
- title: string;
417
- description: string;
418
- children: ReactNode;
419
- }) => (
420
- <Card.Root bg="gray.900" borderColor="gray.800" borderWidth="1px" size="sm">
421
- <Card.Header pb={0}>
422
- <Heading size="md">{title}</Heading>
423
- <Text mt={1} color="gray.500" fontSize="sm">
424
- {description}
425
- </Text>
426
- </Card.Header>
427
- <Card.Body>{children}</Card.Body>
428
- </Card.Root>
429
- );
430
-
431
- const SymbolPriceCard = ({
432
- symbol,
433
- chartRows,
434
- window,
435
- }: {
436
- symbol: string;
437
- chartRows: PriceChartRow[];
438
- window: ChartWindow;
439
- }) => {
440
- const theme = SYMBOL_THEMES[symbol];
441
- const symbolLabel = getSymbolLabel(symbol);
442
-
443
- const priceChartConfig = useMemo(
444
- () => ({
445
- data: chartRows,
446
- series: [{ name: 'price', color: theme.primary }],
447
- }),
448
- [chartRows, theme.primary],
449
- );
450
-
451
- const priceChart = useChart(priceChartConfig as never);
452
- const latestPrice = chartRows[chartRows.length - 1]?.price ?? null;
453
- const priceDomain = useMemo(
454
- () => getChartDomain(chartRows.map((row) => row.price)),
455
- [chartRows],
456
- );
457
-
458
- return (
459
- <ChartCard
460
- title={`${symbolLabel} Price`}
461
- description={`Last close ${formatPrice(latestPrice)}`}
462
- >
463
- <Box h="280px">
464
- <ResponsiveContainer width="100%" height="100%">
465
- <Chart.Root chart={priceChart}>
466
- <AreaChart data={priceChart.data}>
467
- <defs>
468
- <linearGradient
469
- id={`${symbol}-price-fill`}
470
- x1="0"
471
- y1="0"
472
- x2="0"
473
- y2="1"
474
- >
475
- <stop
476
- offset="5%"
477
- stopColor={priceChart.color(theme.primary)}
478
- stopOpacity={0.28}
479
- />
480
- <stop
481
- offset="95%"
482
- stopColor={priceChart.color(theme.primary)}
483
- stopOpacity={0}
484
- />
485
- </linearGradient>
486
- </defs>
487
- <CartesianGrid
488
- stroke={priceChart.color('border')}
489
- vertical={false}
490
- />
491
- <TimeSeriesXAxis
492
- startTimestamp={window.startTimestamp}
493
- endTimestamp={window.endTimestamp}
494
- tickCount={5}
495
- minTickGap={36}
496
- />
497
- <YAxis domain={priceDomain} tickFormatter={formatPrice} />
498
- <Tooltip
499
- cursor={false}
500
- content={
501
- <Chart.Tooltip
502
- labelFormatter={formatTimeSeriesTooltipTimestamp}
503
- />
504
- }
505
- />
506
- <Area
507
- type="monotone"
508
- dataKey={priceChart.key('price') as string}
509
- stroke={priceChart.color(theme.primary)}
510
- fill={`url(#${symbol}-price-fill)`}
511
- strokeWidth={2}
512
- dot={false}
513
- isAnimationActive={false}
514
- />
515
- </AreaChart>
516
- </Chart.Root>
517
- </ResponsiveContainer>
518
- </Box>
519
- </ChartCard>
520
- );
521
- };
522
-
523
- const SymbolOpenInterestCard = ({
524
- symbol,
525
- chartRows,
526
- window,
527
- }: {
528
- symbol: string;
529
- chartRows: DerivativesChartRow[];
530
- window: ChartWindow;
531
- }) => {
532
- const theme = SYMBOL_THEMES[symbol];
533
- const symbolLabel = getSymbolLabel(symbol);
534
-
535
- const oiChartConfig = useMemo(
536
- () => ({
537
- data: chartRows,
538
- series: [{ name: 'openInterest', color: theme.primary }],
539
- }),
540
- [chartRows, theme.primary],
541
- );
542
-
543
- const oiChart = useChart(oiChartConfig as never);
544
- const oiDomain = useMemo(
545
- () => getChartDomain(chartRows.map((row) => row.openInterest)),
546
- [chartRows],
547
- );
548
-
549
- return (
550
- <ChartCard
551
- title={`${symbolLabel} Open Interest`}
552
- description="Position size through the selected window."
553
- >
554
- <Box h="280px">
555
- <ResponsiveContainer width="100%" height="100%">
556
- <Chart.Root chart={oiChart}>
557
- <AreaChart data={oiChart.data}>
558
- <defs>
559
- <linearGradient
560
- id={`${symbol}-oi-fill`}
561
- x1="0"
562
- y1="0"
563
- x2="0"
564
- y2="1"
565
- >
566
- <stop
567
- offset="5%"
568
- stopColor={oiChart.color(theme.primary)}
569
- stopOpacity={0.3}
570
- />
571
- <stop
572
- offset="95%"
573
- stopColor={oiChart.color(theme.primary)}
574
- stopOpacity={0}
575
- />
576
- </linearGradient>
577
- </defs>
578
- <CartesianGrid
579
- stroke={oiChart.color('border')}
580
- vertical={false}
581
- />
582
- <TimeSeriesXAxis
583
- startTimestamp={window.startTimestamp}
584
- endTimestamp={window.endTimestamp}
585
- tickCount={5}
586
- minTickGap={36}
587
- />
588
- <YAxis domain={oiDomain} tickFormatter={formatAxisCompact} />
589
- <Tooltip
590
- cursor={false}
591
- content={
592
- <Chart.Tooltip
593
- labelFormatter={formatTimeSeriesTooltipTimestamp}
594
- />
595
- }
596
- />
597
- <Area
598
- type="monotone"
599
- dataKey={oiChart.key('openInterest') as string}
600
- stroke={oiChart.color(theme.primary)}
601
- fill={`url(#${symbol}-oi-fill)`}
602
- strokeWidth={2}
603
- dot={false}
604
- isAnimationActive={false}
605
- />
606
- </AreaChart>
607
- </Chart.Root>
608
- </ResponsiveContainer>
609
- </Box>
610
- </ChartCard>
611
- );
612
- };
613
-
614
- const SymbolFundingCard = ({
615
- symbol,
616
- chartRows,
617
- window,
618
- }: {
619
- symbol: string;
620
- chartRows: DerivativesChartRow[];
621
- window: ChartWindow;
622
- }) => {
623
- const theme = SYMBOL_THEMES[symbol];
624
- const symbolLabel = getSymbolLabel(symbol);
625
-
626
- const fundingChartConfig = useMemo(
627
- () => ({
628
- data: chartRows,
629
- series: [{ name: 'funding', color: theme.primary }],
630
- }),
631
- [chartRows, theme.primary],
632
- );
633
-
634
- const fundingChart = useChart(fundingChartConfig as never);
635
-
636
- return (
637
- <ChartCard
638
- title={`${symbolLabel} Funding`}
639
- description="Positive values mean longs pay shorts."
640
- >
641
- <Box h="280px">
642
- <ResponsiveContainer width="100%" height="100%">
643
- <Chart.Root chart={fundingChart}>
644
- <BarChart data={fundingChart.data}>
645
- <CartesianGrid
646
- stroke={fundingChart.color('border')}
647
- vertical={false}
648
- />
649
- <ReferenceLine
650
- y={0}
651
- stroke={fundingChart.color('gray.600')}
652
- strokeDasharray="4 4"
653
- />
654
- <TimeSeriesXAxis
655
- startTimestamp={window.startTimestamp}
656
- endTimestamp={window.endTimestamp}
657
- tickCount={5}
658
- minTickGap={36}
659
- />
660
- <YAxis tickFormatter={(value) => `${value} bps`} />
661
- <Tooltip
662
- cursor={false}
663
- content={
664
- <Chart.Tooltip
665
- labelFormatter={formatTimeSeriesTooltipTimestamp}
666
- />
667
- }
668
- />
669
- <Bar
670
- dataKey={fundingChart.key('funding') as string}
671
- isAnimationActive={false}
672
- >
673
- {fundingChart.data.map((entry, idx) => (
674
- <Cell
675
- key={`${symbol}:${entry.timestamp}:${idx}`}
676
- fill={
677
- entry.funding >= 0
678
- ? fundingChart.color(theme.primary)
679
- : fundingChart.color(theme.primaryNegative)
680
- }
681
- />
682
- ))}
683
- </Bar>
684
- </BarChart>
685
- </Chart.Root>
686
- </ResponsiveContainer>
687
- </Box>
688
- </ChartCard>
689
- );
690
- };
691
-
692
- const SymbolLiquidationCard = ({
693
- symbol,
694
- chartRows,
695
- window,
696
- }: {
697
- symbol: string;
698
- chartRows: DerivativesChartRow[];
699
- window: ChartWindow;
700
- }) => {
701
- const theme = SYMBOL_THEMES[symbol];
702
- const symbolLabel = getSymbolLabel(symbol);
703
-
704
- const liquidationChartConfig = useMemo(
705
- () => ({
706
- data: chartRows,
707
- series: [
708
- { name: 'longLiquidations', color: theme.primaryNegative },
709
- { name: 'shortLiquidations', color: theme.secondary },
710
- ],
711
- }),
712
- [chartRows, theme.primaryNegative, theme.secondary],
713
- );
714
-
715
- const liquidationChart = useChart(liquidationChartConfig as never);
716
-
717
- return (
718
- <ChartCard
719
- title={`${symbolLabel} Liquidation Pressure`}
720
- description="Long liquidations are below zero, short liquidations are above zero."
721
- >
722
- <Box h="320px">
723
- <ResponsiveContainer width="100%" height="100%">
724
- <Chart.Root chart={liquidationChart}>
725
- <BarChart data={liquidationChart.data}>
726
- <CartesianGrid
727
- stroke={liquidationChart.color('border')}
728
- vertical={false}
729
- />
730
- <ReferenceLine
731
- y={0}
732
- stroke={liquidationChart.color('gray.600')}
733
- strokeDasharray="4 4"
734
- />
735
- <TimeSeriesXAxis
736
- startTimestamp={window.startTimestamp}
737
- endTimestamp={window.endTimestamp}
738
- tickCount={5}
739
- minTickGap={28}
740
- />
741
- <YAxis tickFormatter={formatAxisCompact} />
742
- <Tooltip
743
- cursor={false}
744
- content={
745
- <Chart.Tooltip
746
- labelFormatter={formatTimeSeriesTooltipTimestamp}
747
- />
748
- }
749
- />
750
- <Legend />
751
- <Bar
752
- dataKey={liquidationChart.key('longLiquidations') as string}
753
- name={`${symbolLabel} long liq`}
754
- fill={liquidationChart.color(theme.primaryNegative)}
755
- isAnimationActive={false}
756
- />
757
- <Bar
758
- dataKey={liquidationChart.key('shortLiquidations') as string}
759
- name={`${symbolLabel} short liq`}
760
- fill={liquidationChart.color(theme.secondary)}
761
- isAnimationActive={false}
762
- />
763
- </BarChart>
764
- </Chart.Root>
765
- </ResponsiveContainer>
766
- </Box>
767
- </ChartCard>
768
- );
769
- };
3
+ import { Box, ClientOnly, Flex, Heading } from '@chakra-ui/react';
4
+ import { Segment, Select } from '#ui';
5
+ import { HOURS_OPTIONS, INTERVAL_OPTIONS } from './derivativesDashboardConfig';
6
+ import { DerivativesDashboardView } from './DerivativesDashboardView';
7
+ import type { DerivativesInterval } from './derivativesViewModel';
8
+ import { useDerivativesDashboard } from './useDerivativesDashboard';
770
9
 
771
10
  const DerivativesPage = () => {
772
- const [hours, setHours] = useState('24');
773
- const [selectedInterval, setSelectedInterval] =
774
- useState<DerivativesInterval>('1h');
775
- const [summary, setSummary] = useState<SummaryResponse | null>(null);
776
- const [detailsBySymbol, setDetailsBySymbol] = useState<
777
- Record<string, DetailRow[]>
778
- >({});
779
- const [pricesBySymbol, setPricesBySymbol] = useState<
780
- Record<string, PriceRow[]>
781
- >({});
782
- const [chartWindow, setChartWindow] = useState<ChartWindow>(() => {
783
- const endTimestamp = Date.now();
784
- return {
785
- startTimestamp: endTimestamp - 24 * 60 * 60 * 1000,
786
- endTimestamp,
787
- };
788
- });
789
- const [summaryLoading, setSummaryLoading] = useState(false);
790
- const [detailLoading, setDetailLoading] = useState(false);
791
- const [summaryError, setSummaryError] = useState('');
792
- const [detailError, setDetailError] = useState('');
793
-
794
- const loadSummary = useCallback(async () => {
795
- setSummaryLoading(true);
796
- setSummaryError('');
797
-
798
- try {
799
- const symbolsParam = FIXED_SYMBOLS.join(',');
800
- const response = await API.get<SummaryResponse>(
801
- `/api/derivatives/summary?hours=${hours}&limit=200&symbols=${symbolsParam}`,
802
- );
803
- setSummary(response);
804
- } catch (err) {
805
- const message = (err as Error)?.message || 'Failed to load derivatives';
806
- setSummaryError(message);
807
- toaster.error({
808
- title: 'Failed to load derivatives',
809
- description: message,
810
- });
811
- } finally {
812
- setSummaryLoading(false);
813
- }
814
- }, [hours]);
815
-
816
- const loadDetails = useCallback(async () => {
817
- setDetailLoading(true);
818
- setDetailError('');
819
-
820
- const now = Date.now();
821
- const from = now - Number(hours) * 60 * 60 * 1000;
822
- const klineInterval = DERIVATIVES_TO_KLINE_INTERVAL[selectedInterval];
823
-
824
- try {
825
- const responses = await Promise.all(
826
- FIXED_SYMBOLS.map(async (symbol) => {
827
- const [derivativesResponse, priceResponse] = await Promise.all([
828
- API.get<DetailResponse>(
829
- `/api/derivatives/${symbol}/${selectedInterval}?from=${from}&to=${now}`,
830
- ),
831
- API.post<PriceResponse>(
832
- buildKlinePath({
833
- provider: 'bybit',
834
- universe: 'crypto',
835
- symbol,
836
- interval: klineInterval,
837
- }),
838
- {
839
- start: from,
840
- end: now,
841
- },
842
- ),
843
- ]);
844
-
845
- return [
846
- symbol,
847
- {
848
- detailRows: derivativesResponse.rows,
849
- priceRows: priceResponse.data ?? [],
850
- },
851
- ] as const;
852
- }),
853
- );
854
-
855
- setDetailsBySymbol(
856
- Object.fromEntries(
857
- responses.map(([symbol, payload]) => [symbol, payload.detailRows]),
858
- ),
859
- );
860
- setPricesBySymbol(
861
- Object.fromEntries(
862
- responses.map(([symbol, payload]) => [symbol, payload.priceRows]),
863
- ),
864
- );
865
- setChartWindow({ startTimestamp: from, endTimestamp: now });
866
- } catch (err) {
867
- const message =
868
- (err as Error)?.message ||
869
- 'Failed to load symbol derivatives and price data';
870
- setDetailError(message);
871
- toaster.error({
872
- title: 'Failed to load derivative details',
873
- description: message,
874
- });
875
- } finally {
876
- setDetailLoading(false);
877
- }
878
- }, [hours, selectedInterval]);
879
-
880
- useEffect(() => {
881
- void loadSummary();
882
- }, [loadSummary]);
883
-
884
- useEffect(() => {
885
- void loadDetails();
886
- }, [loadDetails]);
887
-
888
- const dashboard = useMemo(
889
- () =>
890
- buildDerivativesDashboardViewModel({
891
- symbols: FIXED_SYMBOLS,
892
- selectedInterval,
893
- summary,
894
- detailsBySymbol,
895
- pricesBySymbol,
896
- summaryLoading,
897
- detailLoading,
898
- summaryError,
899
- detailError,
900
- }),
901
- [
902
- detailError,
903
- detailLoading,
904
- detailsBySymbol,
905
- pricesBySymbol,
906
- selectedInterval,
907
- summary,
908
- summaryError,
909
- summaryLoading,
910
- ],
911
- );
912
11
  const {
913
- chartDataBySymbol,
914
- metricsBySymbol,
915
- noDetailData,
916
- noSummaryData,
917
- overviewRows,
918
- showSkeleton,
919
- } = dashboard;
12
+ hours,
13
+ setHours,
14
+ selectedInterval,
15
+ setSelectedInterval,
16
+ chartWindow,
17
+ dashboard,
18
+ } = useDerivativesDashboard();
920
19
 
921
20
  return (
922
21
  <ClientOnly>
@@ -929,10 +28,7 @@ const DerivativesPage = () => {
929
28
  justifyContent="space-between"
930
29
  wrap="wrap"
931
30
  >
932
- <Box>
933
- <Heading size="lg">Derivatives Dashboard</Heading>
934
- </Box>
935
-
31
+ <Heading size="lg">Derivatives Dashboard</Heading>
936
32
  <Flex gap={3} wrap="wrap" alignItems="center">
937
33
  <Select
938
34
  placeholder="Window"
@@ -942,7 +38,6 @@ const DerivativesPage = () => {
942
38
  items={HOURS_OPTIONS}
943
39
  width="180px"
944
40
  />
945
-
946
41
  <Segment
947
42
  defaultValue={selectedInterval}
948
43
  value={selectedInterval}
@@ -958,174 +53,10 @@ const DerivativesPage = () => {
958
53
  />
959
54
  </Flex>
960
55
  </Flex>
961
-
962
- {showSkeleton ? <DashboardSkeleton /> : null}
963
-
964
- {noSummaryData ? (
965
- <EmptyState
966
- icon={FiBarChart2}
967
- title="No derivatives data found"
968
- description="There are no BTC or ETH derivatives rows for the selected time window and interval."
969
- />
970
- ) : null}
971
-
972
- {!showSkeleton && !noSummaryData && (
973
- <>
974
- {noDetailData ? (
975
- <EmptyState
976
- icon={FiBarChart2}
977
- title="No chart data for BTC and ETH"
978
- description="Try another interval or a wider time window."
979
- />
980
- ) : null}
981
-
982
- {!noDetailData && (
983
- <>
984
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
985
- {FIXED_SYMBOLS.map((symbol) => (
986
- <SymbolMetricsCard
987
- key={symbol}
988
- title={`${getSymbolLabel(symbol)} Snapshot`}
989
- metrics={metricsBySymbol[symbol]}
990
- />
991
- ))}
992
- </SimpleGrid>
993
-
994
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
995
- {FIXED_SYMBOLS.map((symbol) => (
996
- <SymbolPriceCard
997
- key={`${symbol}:price`}
998
- symbol={symbol}
999
- chartRows={chartDataBySymbol[symbol].prices}
1000
- window={chartWindow}
1001
- />
1002
- ))}
1003
- </SimpleGrid>
1004
-
1005
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
1006
- {FIXED_SYMBOLS.map((symbol) => (
1007
- <SymbolOpenInterestCard
1008
- key={`${symbol}:oi`}
1009
- symbol={symbol}
1010
- chartRows={chartDataBySymbol[symbol].derivatives}
1011
- window={chartWindow}
1012
- />
1013
- ))}
1014
- </SimpleGrid>
1015
-
1016
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
1017
- {FIXED_SYMBOLS.map((symbol) => (
1018
- <SymbolFundingCard
1019
- key={`${symbol}:funding`}
1020
- symbol={symbol}
1021
- chartRows={chartDataBySymbol[symbol].derivatives}
1022
- window={chartWindow}
1023
- />
1024
- ))}
1025
- </SimpleGrid>
1026
-
1027
- <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
1028
- {FIXED_SYMBOLS.map((symbol) => (
1029
- <SymbolLiquidationCard
1030
- key={`${symbol}:liq`}
1031
- symbol={symbol}
1032
- chartRows={chartDataBySymbol[symbol].derivatives}
1033
- window={chartWindow}
1034
- />
1035
- ))}
1036
- </SimpleGrid>
1037
- </>
1038
- )}
1039
-
1040
- <Card.Root
1041
- bg="gray.900"
1042
- borderColor="gray.800"
1043
- borderWidth="1px"
1044
- size="sm"
1045
- >
1046
- <Card.Header>
1047
- <Heading size="md">BTC / ETH Overview</Heading>
1048
- <Text mt={1} color="gray.500" fontSize="sm">
1049
- One row per symbol for the selected interval and window.
1050
- </Text>
1051
- </Card.Header>
1052
- <Card.Body>
1053
- <Box overflowX="auto">
1054
- <Table.Root size="sm">
1055
- <Table.Header>
1056
- <Table.Row>
1057
- <Table.ColumnHeader>Symbol</Table.ColumnHeader>
1058
- <Table.ColumnHeader textAlign="right">
1059
- OI
1060
- </Table.ColumnHeader>
1061
- <Table.ColumnHeader textAlign="right">
1062
- OI Δ
1063
- </Table.ColumnHeader>
1064
- <Table.ColumnHeader textAlign="right">
1065
- Funding
1066
- </Table.ColumnHeader>
1067
- <Table.ColumnHeader textAlign="right">
1068
- Long Liq
1069
- </Table.ColumnHeader>
1070
- <Table.ColumnHeader textAlign="right">
1071
- Short Liq
1072
- </Table.ColumnHeader>
1073
- <Table.ColumnHeader>Pressure</Table.ColumnHeader>
1074
- <Table.ColumnHeader>Updated</Table.ColumnHeader>
1075
- </Table.Row>
1076
- </Table.Header>
1077
- <Table.Body>
1078
- {overviewRows.map(({ symbol, metrics, bias }) => {
1079
- return (
1080
- <Table.Row key={symbol}>
1081
- <Table.Cell>
1082
- <Text fontWeight="semibold">
1083
- {getSymbolLabel(symbol)}
1084
- </Text>
1085
- </Table.Cell>
1086
- <Table.Cell textAlign="right">
1087
- {formatCompact(metrics.currentOpenInterest)}
1088
- </Table.Cell>
1089
- <Table.Cell textAlign="right">
1090
- <Text
1091
- color={getValueColor(metrics.oiChangePct)}
1092
- >
1093
- {formatPercent(metrics.oiChangePct)}
1094
- </Text>
1095
- </Table.Cell>
1096
- <Table.Cell textAlign="right">
1097
- <Text
1098
- color={getFundingColor(
1099
- metrics.currentFundingRate,
1100
- )}
1101
- >
1102
- {formatFunding(metrics.currentFundingRate)}
1103
- </Text>
1104
- </Table.Cell>
1105
- <Table.Cell textAlign="right">
1106
- {formatCompact(metrics.sumLiqLong)}
1107
- </Table.Cell>
1108
- <Table.Cell textAlign="right">
1109
- {formatCompact(metrics.sumLiqShort)}
1110
- </Table.Cell>
1111
- <Table.Cell>
1112
- <Badge colorPalette={bias.tone}>
1113
- {bias.label}
1114
- </Badge>
1115
- </Table.Cell>
1116
- <Table.Cell>
1117
- {formatFullTime(metrics.lastTs)}
1118
- </Table.Cell>
1119
- </Table.Row>
1120
- );
1121
- })}
1122
- </Table.Body>
1123
- </Table.Root>
1124
- </Box>
1125
- </Card.Body>
1126
- </Card.Root>
1127
- </>
1128
- )}
56
+ <DerivativesDashboardView
57
+ dashboard={dashboard}
58
+ chartWindow={chartWindow}
59
+ />
1129
60
  </Box>
1130
61
  </Box>
1131
62
  </ClientOnly>