@tradejs/app 3.0.0 → 3.1.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.
- package/package.json +6 -10
- package/src/app/actions/strategies.ts +4 -2
- package/src/app/api/ai/route.ts +21 -50
- package/src/app/api/strategies/runtime/route.ts +1 -1
- package/src/app/components/Strategies/RuntimeStrategyCard.presenter.ts +5 -2
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +1 -2
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +1 -2
- package/src/app/components/Strategies/RuntimeStrategyStatsDrawer.tsx +1 -2
- package/src/app/lib/connectorCreator.ts +1 -10
- package/src/app/routes/derivatives/DerivativesDashboardView.tsx +900 -0
- package/src/app/routes/derivatives/derivativesDashboardConfig.ts +53 -0
- package/src/app/routes/derivatives/derivativesDashboardLoader.ts +74 -0
- package/src/app/routes/derivatives/page.tsx +18 -1087
- package/src/app/routes/derivatives/useDerivativesDashboard.ts +105 -0
- package/src/app/routes/strategies/StrategiesPageClient.tsx +4 -2
- package/src/app/lib/runtimeDashboard.ts +0 -700
- package/src/app/lib/runtimeStrategies.ts +0 -1107
- package/src/app/lib/runtimeStrategyContracts.ts +0 -85
- package/src/app/lib/runtimeStrategyLineage.ts +0 -264
- package/src/app/lib/runtimeTradeReconciliation.ts +0 -113
- package/src/app/lib/runtimeTradeSync.ts +0 -280
- package/src/app/lib/strategyEvidenceTimeline.ts +0 -298
|
@@ -1,922 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
} =
|
|
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
|
-
<
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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>
|