@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
@@ -0,0 +1,900 @@
1
+ 'use client';
2
+
3
+ import { type ReactNode, useMemo } from 'react';
4
+ import {
5
+ Badge,
6
+ Box,
7
+ Card,
8
+ Heading,
9
+ SimpleGrid,
10
+ Skeleton,
11
+ SkeletonText,
12
+ Stat,
13
+ Table,
14
+ Text,
15
+ } from '@chakra-ui/react';
16
+ import { Chart, useChart } from '@chakra-ui/charts';
17
+ import {
18
+ Area,
19
+ AreaChart,
20
+ Bar,
21
+ BarChart,
22
+ CartesianGrid,
23
+ Cell,
24
+ Legend,
25
+ ReferenceLine,
26
+ ResponsiveContainer,
27
+ Tooltip,
28
+ YAxis,
29
+ } from 'recharts';
30
+ import { format } from 'date-fns';
31
+ import { FiBarChart2 } from 'react-icons/fi';
32
+ import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
33
+ import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
34
+ import { EmptyState } from '#ui';
35
+ import { FIXED_SYMBOLS, type ChartWindow } from './derivativesDashboardConfig';
36
+ import {
37
+ type DerivativesChartRow,
38
+ type PriceChartRow,
39
+ type SymbolMetrics,
40
+ buildDerivativesDashboardViewModel,
41
+ toFiniteNumber,
42
+ } from './derivativesViewModel';
43
+
44
+ type SymbolChartTheme = {
45
+ primary: string;
46
+ primaryNegative: string;
47
+ secondary: string;
48
+ };
49
+
50
+ const SYMBOL_THEMES: Record<string, SymbolChartTheme> = {
51
+ BTCUSDT: {
52
+ primary: 'orange.solid',
53
+ primaryNegative: 'red.solid',
54
+ secondary: 'orange.solid',
55
+ },
56
+ ETHUSDT: {
57
+ primary: 'teal.solid',
58
+ primaryNegative: 'pink.solid',
59
+ secondary: 'teal.solid',
60
+ },
61
+ };
62
+
63
+ const compactFormatter = new Intl.NumberFormat('en-US', {
64
+ notation: 'compact',
65
+ maximumFractionDigits: 2,
66
+ });
67
+
68
+ const compactSignedFormatter = new Intl.NumberFormat('en-US', {
69
+ notation: 'compact',
70
+ maximumFractionDigits: 2,
71
+ signDisplay: 'always',
72
+ });
73
+
74
+ const formatCompact = (value: number | null | undefined) => {
75
+ const parsed = toFiniteNumber(value);
76
+ if (parsed == null) return 'n/a';
77
+ return compactFormatter.format(parsed);
78
+ };
79
+
80
+ const formatSignedCompact = (value: number | null | undefined) => {
81
+ const parsed = toFiniteNumber(value);
82
+ if (parsed == null) return 'n/a';
83
+ return compactSignedFormatter.format(parsed);
84
+ };
85
+
86
+ const formatPercent = (value: number | null | undefined) => {
87
+ const parsed = toFiniteNumber(value);
88
+ if (parsed == null) return 'n/a';
89
+ return `${parsed >= 0 ? '+' : ''}${parsed.toFixed(2)}%`;
90
+ };
91
+
92
+ const formatFunding = (value: number | null | undefined) => {
93
+ const parsed = toFiniteNumber(value);
94
+ if (parsed == null) return 'n/a';
95
+ const basisPoints = parsed * 10_000;
96
+ return `${basisPoints >= 0 ? '+' : ''}${basisPoints.toFixed(2)} bps`;
97
+ };
98
+
99
+ const formatAxisCompact = (value: number) =>
100
+ compactFormatter.format(Math.abs(value));
101
+
102
+ const formatPrice = (value: number | null | undefined) => {
103
+ const parsed = toFiniteNumber(value);
104
+ if (parsed == null) return 'n/a';
105
+
106
+ const digits = parsed >= 1000 ? 2 : parsed >= 1 ? 3 : 6;
107
+ return parsed.toLocaleString('en-US', {
108
+ maximumFractionDigits: digits,
109
+ minimumFractionDigits: 0,
110
+ });
111
+ };
112
+
113
+ const getChartDomain = (
114
+ values: Array<number | null | undefined>,
115
+ options?: { includeZero?: boolean; minPaddingPct?: number },
116
+ ): [number, number] | undefined => {
117
+ const finite = values.filter(
118
+ (value): value is number =>
119
+ typeof value === 'number' && Number.isFinite(value),
120
+ );
121
+
122
+ if (!finite.length) return undefined;
123
+
124
+ let min = Math.min(...finite);
125
+ let max = Math.max(...finite);
126
+
127
+ if (options?.includeZero) {
128
+ min = Math.min(min, 0);
129
+ max = Math.max(max, 0);
130
+ }
131
+
132
+ const span = max - min;
133
+ const paddingPct = options?.minPaddingPct ?? 0.06;
134
+ const basePadding =
135
+ span > 0
136
+ ? span * paddingPct
137
+ : Math.max(Math.abs(max || min || 1) * paddingPct, 1e-6);
138
+
139
+ return [min - basePadding, max + basePadding];
140
+ };
141
+
142
+ const formatFullTime = (value: string | null | undefined) => {
143
+ if (!value) return 'n/a';
144
+ const parsed = new Date(value);
145
+ if (Number.isNaN(parsed.getTime())) return value;
146
+ return format(parsed, 'dd.MM.yyyy HH:mm');
147
+ };
148
+
149
+ const getValueColor = (value: number | null | undefined) => {
150
+ const parsed = toFiniteNumber(value);
151
+ if (parsed == null || parsed === 0) return 'gray.200';
152
+ return parsed > 0 ? 'teal.300' : 'red.300';
153
+ };
154
+
155
+ const getFundingColor = (value: number | null | undefined) => {
156
+ const parsed = toFiniteNumber(value);
157
+ if (parsed == null || parsed === 0) return 'gray.200';
158
+ return parsed > 0 ? 'orange.300' : 'teal.300';
159
+ };
160
+
161
+ const getSymbolLabel = (symbol: string) =>
162
+ symbol === 'BTCUSDT' ? 'BTC' : symbol === 'ETHUSDT' ? 'ETH' : symbol;
163
+
164
+ const DashboardSkeleton = () => (
165
+ <>
166
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
167
+ {FIXED_SYMBOLS.map((symbol) => (
168
+ <Card.Root
169
+ key={symbol}
170
+ bg="gray.900"
171
+ borderColor="gray.800"
172
+ borderWidth="1px"
173
+ size="sm"
174
+ >
175
+ <Card.Header>
176
+ <Skeleton height="24px" width="120px" />
177
+ </Card.Header>
178
+ <Card.Body>
179
+ <SimpleGrid columns={{ base: 2, xl: 4 }} gap={4}>
180
+ {Array.from({ length: 4 }).map((_, index) => (
181
+ <Box key={`${symbol}:${index}`}>
182
+ <SkeletonText noOfLines={2} gap="3" mb={2} />
183
+ <Skeleton height="18px" width="70%" />
184
+ </Box>
185
+ ))}
186
+ </SimpleGrid>
187
+ </Card.Body>
188
+ </Card.Root>
189
+ ))}
190
+ </SimpleGrid>
191
+
192
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
193
+ <Card.Root
194
+ bg="gray.900"
195
+ borderColor="gray.800"
196
+ borderWidth="1px"
197
+ size="sm"
198
+ >
199
+ <Card.Header>
200
+ <Skeleton height="24px" width="220px" />
201
+ </Card.Header>
202
+ <Card.Body>
203
+ <Skeleton height="280px" />
204
+ </Card.Body>
205
+ </Card.Root>
206
+ <Card.Root
207
+ bg="gray.900"
208
+ borderColor="gray.800"
209
+ borderWidth="1px"
210
+ size="sm"
211
+ >
212
+ <Card.Header>
213
+ <Skeleton height="24px" width="220px" />
214
+ </Card.Header>
215
+ <Card.Body>
216
+ <Skeleton height="280px" />
217
+ </Card.Body>
218
+ </Card.Root>
219
+ </SimpleGrid>
220
+
221
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
222
+ <Card.Root
223
+ bg="gray.900"
224
+ borderColor="gray.800"
225
+ borderWidth="1px"
226
+ size="sm"
227
+ >
228
+ <Card.Header>
229
+ <Skeleton height="24px" width="220px" />
230
+ </Card.Header>
231
+ <Card.Body>
232
+ <Skeleton height="280px" />
233
+ </Card.Body>
234
+ </Card.Root>
235
+ <Card.Root
236
+ bg="gray.900"
237
+ borderColor="gray.800"
238
+ borderWidth="1px"
239
+ size="sm"
240
+ >
241
+ <Card.Header>
242
+ <Skeleton height="24px" width="220px" />
243
+ </Card.Header>
244
+ <Card.Body>
245
+ <Skeleton height="280px" />
246
+ </Card.Body>
247
+ </Card.Root>
248
+ </SimpleGrid>
249
+
250
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
251
+ <Card.Root
252
+ bg="gray.900"
253
+ borderColor="gray.800"
254
+ borderWidth="1px"
255
+ size="sm"
256
+ >
257
+ <Card.Header>
258
+ <Skeleton height="24px" width="220px" />
259
+ </Card.Header>
260
+ <Card.Body>
261
+ <Skeleton height="280px" />
262
+ </Card.Body>
263
+ </Card.Root>
264
+ <Card.Root
265
+ bg="gray.900"
266
+ borderColor="gray.800"
267
+ borderWidth="1px"
268
+ size="sm"
269
+ >
270
+ <Card.Header>
271
+ <Skeleton height="24px" width="220px" />
272
+ </Card.Header>
273
+ <Card.Body>
274
+ <Skeleton height="280px" />
275
+ </Card.Body>
276
+ </Card.Root>
277
+ </SimpleGrid>
278
+
279
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
280
+ <Card.Root
281
+ bg="gray.900"
282
+ borderColor="gray.800"
283
+ borderWidth="1px"
284
+ size="sm"
285
+ >
286
+ <Card.Header>
287
+ <Skeleton height="24px" width="220px" />
288
+ </Card.Header>
289
+ <Card.Body>
290
+ <Skeleton height="320px" />
291
+ </Card.Body>
292
+ </Card.Root>
293
+ <Card.Root
294
+ bg="gray.900"
295
+ borderColor="gray.800"
296
+ borderWidth="1px"
297
+ size="sm"
298
+ >
299
+ <Card.Header>
300
+ <Skeleton height="24px" width="260px" />
301
+ </Card.Header>
302
+ <Card.Body>
303
+ <Skeleton height="320px" />
304
+ </Card.Body>
305
+ </Card.Root>
306
+ </SimpleGrid>
307
+
308
+ <Card.Root bg="gray.900" borderColor="gray.800" borderWidth="1px" size="sm">
309
+ <Card.Header>
310
+ <Skeleton height="24px" width="180px" />
311
+ </Card.Header>
312
+ <Card.Body>
313
+ <SkeletonText noOfLines={8} gap="5" />
314
+ </Card.Body>
315
+ </Card.Root>
316
+ </>
317
+ );
318
+
319
+ const SymbolMetricsCard = ({
320
+ title,
321
+ metrics,
322
+ }: {
323
+ title: string;
324
+ metrics: SymbolMetrics;
325
+ }) => (
326
+ <Card.Root bg="gray.900" borderColor="gray.800" borderWidth="1px" size="sm">
327
+ <Card.Header pb={0}>
328
+ <Heading size="md">{title}</Heading>
329
+ <Text mt={1} color="gray.500" fontSize="sm">
330
+ Updated {formatFullTime(metrics.lastTs)}
331
+ </Text>
332
+ </Card.Header>
333
+ <Card.Body>
334
+ <SimpleGrid columns={{ base: 2, xl: 4 }} gap={4}>
335
+ <Stat.Root>
336
+ <Stat.Label color="gray.400">Open Interest</Stat.Label>
337
+ <Stat.ValueText color={getValueColor(metrics.oiChange)}>
338
+ {formatCompact(metrics.currentOpenInterest)}
339
+ </Stat.ValueText>
340
+ </Stat.Root>
341
+ <Stat.Root>
342
+ <Stat.Label color="gray.400">OI Change</Stat.Label>
343
+ <Stat.ValueText color={getValueColor(metrics.oiChangePct)}>
344
+ {formatPercent(metrics.oiChangePct)}
345
+ </Stat.ValueText>
346
+ <Stat.HelpText color="gray.500">
347
+ {formatSignedCompact(metrics.oiChange)}
348
+ </Stat.HelpText>
349
+ </Stat.Root>
350
+ <Stat.Root>
351
+ <Stat.Label color="gray.400">Funding</Stat.Label>
352
+ <Stat.ValueText color={getFundingColor(metrics.currentFundingRate)}>
353
+ {formatFunding(metrics.currentFundingRate)}
354
+ </Stat.ValueText>
355
+ <Stat.HelpText color="gray.500">
356
+ Delta {formatFunding(metrics.fundingChange)}
357
+ </Stat.HelpText>
358
+ </Stat.Root>
359
+ <Stat.Root>
360
+ <Stat.Label color="gray.400">Liquidations</Stat.Label>
361
+ <Stat.ValueText color="gray.200">
362
+ {formatCompact(metrics.sumLiqTotal)}
363
+ </Stat.ValueText>
364
+ <Stat.HelpText color="gray.500">
365
+ Long {formatCompact(metrics.sumLiqLong)} / Short{' '}
366
+ {formatCompact(metrics.sumLiqShort)}
367
+ </Stat.HelpText>
368
+ </Stat.Root>
369
+ </SimpleGrid>
370
+ </Card.Body>
371
+ </Card.Root>
372
+ );
373
+
374
+ const ChartCard = ({
375
+ title,
376
+ description,
377
+ children,
378
+ }: {
379
+ title: string;
380
+ description: string;
381
+ children: ReactNode;
382
+ }) => (
383
+ <Card.Root bg="gray.900" borderColor="gray.800" borderWidth="1px" size="sm">
384
+ <Card.Header pb={0}>
385
+ <Heading size="md">{title}</Heading>
386
+ <Text mt={1} color="gray.500" fontSize="sm">
387
+ {description}
388
+ </Text>
389
+ </Card.Header>
390
+ <Card.Body>{children}</Card.Body>
391
+ </Card.Root>
392
+ );
393
+
394
+ const SymbolPriceCard = ({
395
+ symbol,
396
+ chartRows,
397
+ window,
398
+ }: {
399
+ symbol: string;
400
+ chartRows: PriceChartRow[];
401
+ window: ChartWindow;
402
+ }) => {
403
+ const theme = SYMBOL_THEMES[symbol];
404
+ const symbolLabel = getSymbolLabel(symbol);
405
+
406
+ const priceChartConfig = useMemo(
407
+ () => ({
408
+ data: chartRows,
409
+ series: [{ name: 'price', color: theme.primary }],
410
+ }),
411
+ [chartRows, theme.primary],
412
+ );
413
+
414
+ const priceChart = useChart(priceChartConfig as never);
415
+ const latestPrice = chartRows[chartRows.length - 1]?.price ?? null;
416
+ const priceDomain = useMemo(
417
+ () => getChartDomain(chartRows.map((row) => row.price)),
418
+ [chartRows],
419
+ );
420
+
421
+ return (
422
+ <ChartCard
423
+ title={`${symbolLabel} Price`}
424
+ description={`Last close ${formatPrice(latestPrice)}`}
425
+ >
426
+ <Box h="280px">
427
+ <ResponsiveContainer width="100%" height="100%">
428
+ <Chart.Root chart={priceChart}>
429
+ <AreaChart data={priceChart.data}>
430
+ <defs>
431
+ <linearGradient
432
+ id={`${symbol}-price-fill`}
433
+ x1="0"
434
+ y1="0"
435
+ x2="0"
436
+ y2="1"
437
+ >
438
+ <stop
439
+ offset="5%"
440
+ stopColor={priceChart.color(theme.primary)}
441
+ stopOpacity={0.28}
442
+ />
443
+ <stop
444
+ offset="95%"
445
+ stopColor={priceChart.color(theme.primary)}
446
+ stopOpacity={0}
447
+ />
448
+ </linearGradient>
449
+ </defs>
450
+ <CartesianGrid
451
+ stroke={priceChart.color('border')}
452
+ vertical={false}
453
+ />
454
+ <TimeSeriesXAxis
455
+ startTimestamp={window.startTimestamp}
456
+ endTimestamp={window.endTimestamp}
457
+ tickCount={5}
458
+ minTickGap={36}
459
+ />
460
+ <YAxis domain={priceDomain} tickFormatter={formatPrice} />
461
+ <Tooltip
462
+ cursor={false}
463
+ content={
464
+ <Chart.Tooltip
465
+ labelFormatter={formatTimeSeriesTooltipTimestamp}
466
+ />
467
+ }
468
+ />
469
+ <Area
470
+ type="monotone"
471
+ dataKey={priceChart.key('price') as string}
472
+ stroke={priceChart.color(theme.primary)}
473
+ fill={`url(#${symbol}-price-fill)`}
474
+ strokeWidth={2}
475
+ dot={false}
476
+ isAnimationActive={false}
477
+ />
478
+ </AreaChart>
479
+ </Chart.Root>
480
+ </ResponsiveContainer>
481
+ </Box>
482
+ </ChartCard>
483
+ );
484
+ };
485
+
486
+ const SymbolOpenInterestCard = ({
487
+ symbol,
488
+ chartRows,
489
+ window,
490
+ }: {
491
+ symbol: string;
492
+ chartRows: DerivativesChartRow[];
493
+ window: ChartWindow;
494
+ }) => {
495
+ const theme = SYMBOL_THEMES[symbol];
496
+ const symbolLabel = getSymbolLabel(symbol);
497
+
498
+ const oiChartConfig = useMemo(
499
+ () => ({
500
+ data: chartRows,
501
+ series: [{ name: 'openInterest', color: theme.primary }],
502
+ }),
503
+ [chartRows, theme.primary],
504
+ );
505
+
506
+ const oiChart = useChart(oiChartConfig as never);
507
+ const oiDomain = useMemo(
508
+ () => getChartDomain(chartRows.map((row) => row.openInterest)),
509
+ [chartRows],
510
+ );
511
+
512
+ return (
513
+ <ChartCard
514
+ title={`${symbolLabel} Open Interest`}
515
+ description="Position size through the selected window."
516
+ >
517
+ <Box h="280px">
518
+ <ResponsiveContainer width="100%" height="100%">
519
+ <Chart.Root chart={oiChart}>
520
+ <AreaChart data={oiChart.data}>
521
+ <defs>
522
+ <linearGradient
523
+ id={`${symbol}-oi-fill`}
524
+ x1="0"
525
+ y1="0"
526
+ x2="0"
527
+ y2="1"
528
+ >
529
+ <stop
530
+ offset="5%"
531
+ stopColor={oiChart.color(theme.primary)}
532
+ stopOpacity={0.3}
533
+ />
534
+ <stop
535
+ offset="95%"
536
+ stopColor={oiChart.color(theme.primary)}
537
+ stopOpacity={0}
538
+ />
539
+ </linearGradient>
540
+ </defs>
541
+ <CartesianGrid
542
+ stroke={oiChart.color('border')}
543
+ vertical={false}
544
+ />
545
+ <TimeSeriesXAxis
546
+ startTimestamp={window.startTimestamp}
547
+ endTimestamp={window.endTimestamp}
548
+ tickCount={5}
549
+ minTickGap={36}
550
+ />
551
+ <YAxis domain={oiDomain} tickFormatter={formatAxisCompact} />
552
+ <Tooltip
553
+ cursor={false}
554
+ content={
555
+ <Chart.Tooltip
556
+ labelFormatter={formatTimeSeriesTooltipTimestamp}
557
+ />
558
+ }
559
+ />
560
+ <Area
561
+ type="monotone"
562
+ dataKey={oiChart.key('openInterest') as string}
563
+ stroke={oiChart.color(theme.primary)}
564
+ fill={`url(#${symbol}-oi-fill)`}
565
+ strokeWidth={2}
566
+ dot={false}
567
+ isAnimationActive={false}
568
+ />
569
+ </AreaChart>
570
+ </Chart.Root>
571
+ </ResponsiveContainer>
572
+ </Box>
573
+ </ChartCard>
574
+ );
575
+ };
576
+
577
+ const SymbolFundingCard = ({
578
+ symbol,
579
+ chartRows,
580
+ window,
581
+ }: {
582
+ symbol: string;
583
+ chartRows: DerivativesChartRow[];
584
+ window: ChartWindow;
585
+ }) => {
586
+ const theme = SYMBOL_THEMES[symbol];
587
+ const symbolLabel = getSymbolLabel(symbol);
588
+
589
+ const fundingChartConfig = useMemo(
590
+ () => ({
591
+ data: chartRows,
592
+ series: [{ name: 'funding', color: theme.primary }],
593
+ }),
594
+ [chartRows, theme.primary],
595
+ );
596
+
597
+ const fundingChart = useChart(fundingChartConfig as never);
598
+
599
+ return (
600
+ <ChartCard
601
+ title={`${symbolLabel} Funding`}
602
+ description="Positive values mean longs pay shorts."
603
+ >
604
+ <Box h="280px">
605
+ <ResponsiveContainer width="100%" height="100%">
606
+ <Chart.Root chart={fundingChart}>
607
+ <BarChart data={fundingChart.data}>
608
+ <CartesianGrid
609
+ stroke={fundingChart.color('border')}
610
+ vertical={false}
611
+ />
612
+ <ReferenceLine
613
+ y={0}
614
+ stroke={fundingChart.color('gray.600')}
615
+ strokeDasharray="4 4"
616
+ />
617
+ <TimeSeriesXAxis
618
+ startTimestamp={window.startTimestamp}
619
+ endTimestamp={window.endTimestamp}
620
+ tickCount={5}
621
+ minTickGap={36}
622
+ />
623
+ <YAxis tickFormatter={(value) => `${value} bps`} />
624
+ <Tooltip
625
+ cursor={false}
626
+ content={
627
+ <Chart.Tooltip
628
+ labelFormatter={formatTimeSeriesTooltipTimestamp}
629
+ />
630
+ }
631
+ />
632
+ <Bar
633
+ dataKey={fundingChart.key('funding') as string}
634
+ isAnimationActive={false}
635
+ >
636
+ {fundingChart.data.map((entry, idx) => (
637
+ <Cell
638
+ key={`${symbol}:${entry.timestamp}:${idx}`}
639
+ fill={
640
+ entry.funding >= 0
641
+ ? fundingChart.color(theme.primary)
642
+ : fundingChart.color(theme.primaryNegative)
643
+ }
644
+ />
645
+ ))}
646
+ </Bar>
647
+ </BarChart>
648
+ </Chart.Root>
649
+ </ResponsiveContainer>
650
+ </Box>
651
+ </ChartCard>
652
+ );
653
+ };
654
+
655
+ const SymbolLiquidationCard = ({
656
+ symbol,
657
+ chartRows,
658
+ window,
659
+ }: {
660
+ symbol: string;
661
+ chartRows: DerivativesChartRow[];
662
+ window: ChartWindow;
663
+ }) => {
664
+ const theme = SYMBOL_THEMES[symbol];
665
+ const symbolLabel = getSymbolLabel(symbol);
666
+
667
+ const liquidationChartConfig = useMemo(
668
+ () => ({
669
+ data: chartRows,
670
+ series: [
671
+ { name: 'longLiquidations', color: theme.primaryNegative },
672
+ { name: 'shortLiquidations', color: theme.secondary },
673
+ ],
674
+ }),
675
+ [chartRows, theme.primaryNegative, theme.secondary],
676
+ );
677
+
678
+ const liquidationChart = useChart(liquidationChartConfig as never);
679
+
680
+ return (
681
+ <ChartCard
682
+ title={`${symbolLabel} Liquidation Pressure`}
683
+ description="Long liquidations are below zero, short liquidations are above zero."
684
+ >
685
+ <Box h="320px">
686
+ <ResponsiveContainer width="100%" height="100%">
687
+ <Chart.Root chart={liquidationChart}>
688
+ <BarChart data={liquidationChart.data}>
689
+ <CartesianGrid
690
+ stroke={liquidationChart.color('border')}
691
+ vertical={false}
692
+ />
693
+ <ReferenceLine
694
+ y={0}
695
+ stroke={liquidationChart.color('gray.600')}
696
+ strokeDasharray="4 4"
697
+ />
698
+ <TimeSeriesXAxis
699
+ startTimestamp={window.startTimestamp}
700
+ endTimestamp={window.endTimestamp}
701
+ tickCount={5}
702
+ minTickGap={28}
703
+ />
704
+ <YAxis tickFormatter={formatAxisCompact} />
705
+ <Tooltip
706
+ cursor={false}
707
+ content={
708
+ <Chart.Tooltip
709
+ labelFormatter={formatTimeSeriesTooltipTimestamp}
710
+ />
711
+ }
712
+ />
713
+ <Legend />
714
+ <Bar
715
+ dataKey={liquidationChart.key('longLiquidations') as string}
716
+ name={`${symbolLabel} long liq`}
717
+ fill={liquidationChart.color(theme.primaryNegative)}
718
+ isAnimationActive={false}
719
+ />
720
+ <Bar
721
+ dataKey={liquidationChart.key('shortLiquidations') as string}
722
+ name={`${symbolLabel} short liq`}
723
+ fill={liquidationChart.color(theme.secondary)}
724
+ isAnimationActive={false}
725
+ />
726
+ </BarChart>
727
+ </Chart.Root>
728
+ </ResponsiveContainer>
729
+ </Box>
730
+ </ChartCard>
731
+ );
732
+ };
733
+
734
+ type DashboardViewModel = ReturnType<typeof buildDerivativesDashboardViewModel>;
735
+
736
+ export const DerivativesDashboardView = ({
737
+ dashboard,
738
+ chartWindow,
739
+ }: {
740
+ dashboard: DashboardViewModel;
741
+ chartWindow: ChartWindow;
742
+ }) => {
743
+ const {
744
+ chartDataBySymbol,
745
+ metricsBySymbol,
746
+ noDetailData,
747
+ noSummaryData,
748
+ overviewRows,
749
+ showSkeleton,
750
+ } = dashboard;
751
+
752
+ if (showSkeleton) return <DashboardSkeleton />;
753
+ if (noSummaryData) {
754
+ return (
755
+ <EmptyState
756
+ icon={FiBarChart2}
757
+ title="No derivatives data found"
758
+ description="There are no BTC or ETH derivatives rows for the selected time window and interval."
759
+ />
760
+ );
761
+ }
762
+
763
+ return (
764
+ <>
765
+ {noDetailData ? (
766
+ <EmptyState
767
+ icon={FiBarChart2}
768
+ title="No chart data for BTC and ETH"
769
+ description="Try another interval or a wider time window."
770
+ />
771
+ ) : (
772
+ <>
773
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
774
+ {FIXED_SYMBOLS.map((symbol) => (
775
+ <SymbolMetricsCard
776
+ key={symbol}
777
+ title={`${getSymbolLabel(symbol)} Snapshot`}
778
+ metrics={metricsBySymbol[symbol]}
779
+ />
780
+ ))}
781
+ </SimpleGrid>
782
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
783
+ {FIXED_SYMBOLS.map((symbol) => (
784
+ <SymbolPriceCard
785
+ key={`${symbol}:price`}
786
+ symbol={symbol}
787
+ chartRows={chartDataBySymbol[symbol].prices}
788
+ window={chartWindow}
789
+ />
790
+ ))}
791
+ </SimpleGrid>
792
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
793
+ {FIXED_SYMBOLS.map((symbol) => (
794
+ <SymbolOpenInterestCard
795
+ key={`${symbol}:oi`}
796
+ symbol={symbol}
797
+ chartRows={chartDataBySymbol[symbol].derivatives}
798
+ window={chartWindow}
799
+ />
800
+ ))}
801
+ </SimpleGrid>
802
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
803
+ {FIXED_SYMBOLS.map((symbol) => (
804
+ <SymbolFundingCard
805
+ key={`${symbol}:funding`}
806
+ symbol={symbol}
807
+ chartRows={chartDataBySymbol[symbol].derivatives}
808
+ window={chartWindow}
809
+ />
810
+ ))}
811
+ </SimpleGrid>
812
+ <SimpleGrid columns={{ base: 1, lg: 2 }} gap={4} mb={6}>
813
+ {FIXED_SYMBOLS.map((symbol) => (
814
+ <SymbolLiquidationCard
815
+ key={`${symbol}:liq`}
816
+ symbol={symbol}
817
+ chartRows={chartDataBySymbol[symbol].derivatives}
818
+ window={chartWindow}
819
+ />
820
+ ))}
821
+ </SimpleGrid>
822
+ </>
823
+ )}
824
+
825
+ <Card.Root
826
+ bg="gray.900"
827
+ borderColor="gray.800"
828
+ borderWidth="1px"
829
+ size="sm"
830
+ >
831
+ <Card.Header>
832
+ <Heading size="md">BTC / ETH Overview</Heading>
833
+ <Text mt={1} color="gray.500" fontSize="sm">
834
+ One row per symbol for the selected interval and window.
835
+ </Text>
836
+ </Card.Header>
837
+ <Card.Body>
838
+ <Box overflowX="auto">
839
+ <Table.Root size="sm">
840
+ <Table.Header>
841
+ <Table.Row>
842
+ <Table.ColumnHeader>Symbol</Table.ColumnHeader>
843
+ <Table.ColumnHeader textAlign="right">OI</Table.ColumnHeader>
844
+ <Table.ColumnHeader textAlign="right">
845
+ OI Δ
846
+ </Table.ColumnHeader>
847
+ <Table.ColumnHeader textAlign="right">
848
+ Funding
849
+ </Table.ColumnHeader>
850
+ <Table.ColumnHeader textAlign="right">
851
+ Long Liq
852
+ </Table.ColumnHeader>
853
+ <Table.ColumnHeader textAlign="right">
854
+ Short Liq
855
+ </Table.ColumnHeader>
856
+ <Table.ColumnHeader>Pressure</Table.ColumnHeader>
857
+ <Table.ColumnHeader>Updated</Table.ColumnHeader>
858
+ </Table.Row>
859
+ </Table.Header>
860
+ <Table.Body>
861
+ {overviewRows.map(({ symbol, metrics, bias }) => (
862
+ <Table.Row key={symbol}>
863
+ <Table.Cell>
864
+ <Text fontWeight="semibold">
865
+ {getSymbolLabel(symbol)}
866
+ </Text>
867
+ </Table.Cell>
868
+ <Table.Cell textAlign="right">
869
+ {formatCompact(metrics.currentOpenInterest)}
870
+ </Table.Cell>
871
+ <Table.Cell textAlign="right">
872
+ <Text color={getValueColor(metrics.oiChangePct)}>
873
+ {formatPercent(metrics.oiChangePct)}
874
+ </Text>
875
+ </Table.Cell>
876
+ <Table.Cell textAlign="right">
877
+ <Text color={getFundingColor(metrics.currentFundingRate)}>
878
+ {formatFunding(metrics.currentFundingRate)}
879
+ </Text>
880
+ </Table.Cell>
881
+ <Table.Cell textAlign="right">
882
+ {formatCompact(metrics.sumLiqLong)}
883
+ </Table.Cell>
884
+ <Table.Cell textAlign="right">
885
+ {formatCompact(metrics.sumLiqShort)}
886
+ </Table.Cell>
887
+ <Table.Cell>
888
+ <Badge colorPalette={bias.tone}>{bias.label}</Badge>
889
+ </Table.Cell>
890
+ <Table.Cell>{formatFullTime(metrics.lastTs)}</Table.Cell>
891
+ </Table.Row>
892
+ ))}
893
+ </Table.Body>
894
+ </Table.Root>
895
+ </Box>
896
+ </Card.Body>
897
+ </Card.Root>
898
+ </>
899
+ );
900
+ };