@tradejs/app 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +7 -2
  2. package/bin/tradejs-app.mjs +146 -2
  3. package/next.config.mjs +0 -14
  4. package/package.json +35 -19
  5. package/src/app/actions/backtest.ts +54 -0
  6. package/src/app/actions/kline.ts +10 -5
  7. package/src/app/actions/scanner.ts +8 -3
  8. package/src/app/actions/strategies.ts +39 -0
  9. package/src/app/api/ai/route.ts +5 -4
  10. package/src/app/api/auth/[...nextauth]/route.ts +1 -1
  11. package/src/app/api/backtest/configs/route.ts +25 -0
  12. package/src/app/api/backtest/files/route.ts +5 -2
  13. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +26 -2
  14. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +27 -2
  15. package/src/app/api/backtest/runs/[jobId]/route.ts +124 -0
  16. package/src/app/api/backtest/runs/route.ts +45 -0
  17. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +19 -2
  18. package/src/app/api/derivatives/summary/route.ts +8 -1
  19. package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
  20. package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
  21. package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
  22. package/src/app/api/scanner/[provider]/route.ts +30 -12
  23. package/src/app/api/scanner/route.ts +3 -3
  24. package/src/app/api/signal/[symbol]/[signalId]/route.ts +4 -7
  25. package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
  26. package/src/app/api/strategies/ai/route.ts +139 -0
  27. package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
  28. package/src/app/api/strategies/replay/route.ts +44 -0
  29. package/src/app/api/strategies/runtime/route.ts +831 -0
  30. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
  31. package/src/app/api/user/runtime-deployments/route.ts +94 -0
  32. package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
  33. package/src/app/api/user/settings/route.ts +18 -1
  34. package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
  35. package/src/app/api/user/trading-accounts/route.ts +134 -0
  36. package/src/app/components/Backtest/CompareList/index.tsx +1 -1
  37. package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
  38. package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
  39. package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
  40. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
  41. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
  42. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
  43. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
  44. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
  45. package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
  46. package/src/app/components/Backtest/TestCard/Root/index.tsx +1 -1
  47. package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
  48. package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
  49. package/src/app/components/Backtest/TestCard/index.ts +6 -2
  50. package/src/app/components/Backtest/TestList/index.tsx +4 -9
  51. package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
  52. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
  53. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
  54. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
  55. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
  56. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
  57. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
  58. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
  59. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
  60. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
  61. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
  62. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
  63. package/src/app/components/Dashboard/KlineChart/index.tsx +16 -6
  64. package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
  65. package/src/app/components/Shared/AppShell.tsx +1 -1
  66. package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
  67. package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
  68. package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
  69. package/src/app/components/Shared/Filters/Backtest/index.tsx +33 -48
  70. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
  71. package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
  72. package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
  73. package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
  74. package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
  75. package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
  76. package/src/app/components/Shared/Filters/index.ts +2 -0
  77. package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
  78. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
  79. package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
  80. package/src/app/components/Shared/Sidebar/index.tsx +17 -3
  81. package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
  82. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
  83. package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
  84. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
  85. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
  86. package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
  87. package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
  88. package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
  89. package/src/app/components/UI/Segment/index.tsx +8 -1
  90. package/src/app/components/UI/Select/index.tsx +24 -13
  91. package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
  92. package/src/app/layout.tsx +1 -1
  93. package/src/app/lib/backtestJobs.ts +792 -0
  94. package/src/app/lib/cardPageLayout.ts +1 -0
  95. package/src/app/lib/connectorCreator.ts +28 -0
  96. package/src/app/lib/currentUser.ts +1 -1
  97. package/src/app/lib/errorMessage.ts +34 -0
  98. package/src/app/lib/marketDefaults.ts +12 -0
  99. package/src/app/lib/marketRoutes.ts +58 -0
  100. package/src/app/lib/runtimeStrategies.ts +1376 -0
  101. package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
  102. package/src/app/provider.tsx +1 -1
  103. package/src/app/routes/backtest/page.tsx +1051 -304
  104. package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +35 -15
  105. package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
  106. package/src/app/routes/dashboard/page.tsx +15 -3
  107. package/src/app/routes/derivatives/page.tsx +1222 -165
  108. package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
  109. package/src/app/routes/strategies/[mode]/page.tsx +20 -0
  110. package/src/app/routes/strategies/page.tsx +7 -0
  111. package/src/app/store/ai.ts +1 -1
  112. package/src/app/store/data.ts +73 -17
  113. package/src/app/store/filters.ts +1 -0
  114. package/src/app/store/marketKlineStream.ts +87 -0
  115. package/src/app/store/tests.ts +1 -1
  116. package/src/app/store/tickers.ts +59 -22
  117. package/tsconfig.json +17 -12
  118. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
  119. package/src/app/routes/backtest/[test]/page.tsx +0 -33
@@ -0,0 +1,727 @@
1
+ 'use client';
2
+
3
+ import { Box, Flex, Grid, SimpleGrid, Text } from '@chakra-ui/react';
4
+ import type { AdvancedTradeMetrics } from '@tradejs/core/backtest';
5
+ import {
6
+ formatCompactNumber,
7
+ formatFee,
8
+ formatInteger,
9
+ formatPercent,
10
+ formatSignedNumber,
11
+ getPnlColor,
12
+ } from '#components/Shared/OrdersDrawer';
13
+
14
+ type MetricTone = 'amount' | 'positive' | 'negative' | 'warning' | 'neutral';
15
+ type MetricVariant = 'default' | 'primary';
16
+
17
+ interface MetricItem {
18
+ id: string;
19
+ label: string;
20
+ value: string;
21
+ detail?: string;
22
+ tone?: MetricTone;
23
+ numericValue?: number | null;
24
+ variant?: MetricVariant;
25
+ }
26
+
27
+ interface MetricGroup {
28
+ id: string;
29
+ title: string;
30
+ description?: string;
31
+ metrics: MetricItem[];
32
+ }
33
+
34
+ interface MetricSection {
35
+ id: string;
36
+ title: string;
37
+ subtitle: string;
38
+ groups: MetricGroup[];
39
+ quarterlyPnl?: AdvancedTradeMetrics['stability']['quarterlyPnl'];
40
+ }
41
+
42
+ const getAmountLabel = (value: number | null | undefined) =>
43
+ typeof value === 'number' && Number.isFinite(value)
44
+ ? `${formatSignedNumber(value)} USDT`
45
+ : 'n/a';
46
+
47
+ const getPlainAmountLabel = (value: number | null | undefined) =>
48
+ typeof value === 'number' && Number.isFinite(value)
49
+ ? `${formatCompactNumber(value, { maximumFractionDigits: 2 })} USDT`
50
+ : 'n/a';
51
+
52
+ const getRatioLabel = (value: number | null | undefined) =>
53
+ typeof value === 'number' && Number.isFinite(value)
54
+ ? `${formatCompactNumber(value, {
55
+ maximumFractionDigits: 2,
56
+ minimumFractionDigits: 2,
57
+ })}x`
58
+ : 'n/a';
59
+
60
+ const getNumberLabel = (value: number | null | undefined) =>
61
+ formatCompactNumber(value, {
62
+ maximumFractionDigits: 2,
63
+ minimumFractionDigits: 0,
64
+ });
65
+
66
+ const getMetricColor = (item: MetricItem) => {
67
+ if (item.tone === 'amount') {
68
+ return getPnlColor(item.numericValue);
69
+ }
70
+
71
+ if (item.tone === 'positive') {
72
+ return 'teal.300';
73
+ }
74
+
75
+ if (item.tone === 'negative') {
76
+ return 'red.300';
77
+ }
78
+
79
+ if (item.tone === 'warning') {
80
+ return 'orange.300';
81
+ }
82
+
83
+ return item.variant === 'primary' ? 'gray.100' : 'gray.200';
84
+ };
85
+
86
+ const amountMetric = (
87
+ id: string,
88
+ label: string,
89
+ value: number | null | undefined,
90
+ options: Partial<MetricItem> = {},
91
+ ): MetricItem => ({
92
+ id,
93
+ label,
94
+ value: getAmountLabel(value),
95
+ tone: 'amount',
96
+ numericValue: value,
97
+ ...options,
98
+ });
99
+
100
+ const plainAmountMetric = (
101
+ id: string,
102
+ label: string,
103
+ value: number | null | undefined,
104
+ options: Partial<MetricItem> = {},
105
+ ): MetricItem => ({
106
+ id,
107
+ label,
108
+ value: getPlainAmountLabel(value),
109
+ numericValue: value,
110
+ ...options,
111
+ });
112
+
113
+ const exitBreakdownMetric = (
114
+ id: string,
115
+ label: string,
116
+ bucket: AdvancedTradeMetrics['operational']['exitBreakdown']['takeProfit'],
117
+ ): MetricItem => ({
118
+ id,
119
+ label,
120
+ value: formatInteger(bucket.count),
121
+ detail: formatPercent(bucket.share),
122
+ });
123
+
124
+ const MetricCard = ({ item }: { item: MetricItem }) => {
125
+ const isPrimary = item.variant === 'primary';
126
+
127
+ return (
128
+ <Box
129
+ borderWidth="1px"
130
+ borderColor={isPrimary ? 'teal.900' : 'gray.800'}
131
+ borderRadius="md"
132
+ bg={isPrimary ? 'teal.950' : 'blackAlpha.200'}
133
+ minW="0"
134
+ px={4}
135
+ py={3}
136
+ >
137
+ <Text
138
+ color="gray.500"
139
+ fontSize="2xs"
140
+ fontWeight="bold"
141
+ textTransform="uppercase"
142
+ lineHeight="1.2"
143
+ >
144
+ {item.label}
145
+ </Text>
146
+ <Text
147
+ mt={2}
148
+ color={getMetricColor(item)}
149
+ fontSize={isPrimary ? 'xl' : 'lg'}
150
+ fontWeight="bold"
151
+ fontFamily="mono"
152
+ lineHeight="1"
153
+ whiteSpace="nowrap"
154
+ overflow="hidden"
155
+ textOverflow="ellipsis"
156
+ >
157
+ {item.value}
158
+ </Text>
159
+ {item.detail ? (
160
+ <Text
161
+ mt={2}
162
+ color="gray.500"
163
+ fontSize="xs"
164
+ lineHeight="1.25"
165
+ whiteSpace="nowrap"
166
+ overflow="hidden"
167
+ textOverflow="ellipsis"
168
+ >
169
+ {item.detail}
170
+ </Text>
171
+ ) : null}
172
+ </Box>
173
+ );
174
+ };
175
+
176
+ const MetricGroupBlock = ({ group }: { group: MetricGroup }) => (
177
+ <Box minW="0">
178
+ <Flex justify="space-between" align="baseline" gap={3} mb={3}>
179
+ <Text color="gray.300" fontSize="sm" fontWeight="semibold">
180
+ {group.title}
181
+ </Text>
182
+ {group.description ? (
183
+ <Text color="gray.600" fontSize="xs" textAlign="right">
184
+ {group.description}
185
+ </Text>
186
+ ) : null}
187
+ </Flex>
188
+ <SimpleGrid columns={{ base: 2, xl: 3 }} gap={3}>
189
+ {group.metrics.map((item) => (
190
+ <MetricCard key={item.id} item={item} />
191
+ ))}
192
+ </SimpleGrid>
193
+ </Box>
194
+ );
195
+
196
+ const QuarterlyPnlGrid = ({
197
+ quarterlyPnl,
198
+ }: {
199
+ quarterlyPnl: AdvancedTradeMetrics['stability']['quarterlyPnl'];
200
+ }) => {
201
+ if (!quarterlyPnl.length) {
202
+ return null;
203
+ }
204
+
205
+ return (
206
+ <Box mt={4}>
207
+ <Flex justify="space-between" align="baseline" gap={3} mb={3}>
208
+ <Text color="gray.300" fontSize="sm" fontWeight="semibold">
209
+ Quarterly P&L
210
+ </Text>
211
+ <Text color="gray.600" fontSize="xs" textAlign="right">
212
+ grouped by calendar quarter
213
+ </Text>
214
+ </Flex>
215
+ <Grid templateColumns={{ base: '1fr 1fr', xl: 'repeat(3, 1fr)' }} gap={2}>
216
+ {quarterlyPnl.map((quarter) => (
217
+ <Flex
218
+ key={quarter.quarter}
219
+ align="center"
220
+ justify="space-between"
221
+ gap={2}
222
+ borderWidth="1px"
223
+ borderColor="gray.800"
224
+ borderRadius="md"
225
+ bg="blackAlpha.200"
226
+ minW="0"
227
+ px={3}
228
+ py={2}
229
+ >
230
+ <Text color="gray.400" fontSize="xs" fontWeight="semibold">
231
+ {quarter.quarter}
232
+ </Text>
233
+ <Text
234
+ color={getPnlColor(quarter.pnl)}
235
+ fontSize="sm"
236
+ fontWeight="bold"
237
+ fontFamily="mono"
238
+ whiteSpace="nowrap"
239
+ >
240
+ {getAmountLabel(quarter.pnl)}
241
+ </Text>
242
+ </Flex>
243
+ ))}
244
+ </Grid>
245
+ </Box>
246
+ );
247
+ };
248
+
249
+ const MetricsSection = ({ section }: { section: MetricSection }) => (
250
+ <Box
251
+ borderWidth="1px"
252
+ borderColor="gray.800"
253
+ borderRadius="md"
254
+ bg="gray.900"
255
+ p={4}
256
+ >
257
+ <Flex justify="space-between" align="baseline" gap={4}>
258
+ <Text fontSize="md" fontWeight="semibold" color="gray.100">
259
+ {section.title}
260
+ </Text>
261
+ <Text color="gray.600" fontSize="xs" textAlign="right">
262
+ {section.subtitle}
263
+ </Text>
264
+ </Flex>
265
+ <Flex direction="column" gap={5} mt={4}>
266
+ {section.groups.map((group) => (
267
+ <MetricGroupBlock key={group.id} group={group} />
268
+ ))}
269
+ </Flex>
270
+ <QuarterlyPnlGrid quarterlyPnl={section.quarterlyPnl ?? []} />
271
+ </Box>
272
+ );
273
+
274
+ const buildMetricSections = (
275
+ metrics: AdvancedTradeMetrics,
276
+ ): MetricSection[] => [
277
+ {
278
+ id: 'overview',
279
+ title: 'Overview',
280
+ subtitle: 'result, quality and trade frequency',
281
+ groups: [
282
+ {
283
+ id: 'result',
284
+ title: 'Result',
285
+ description: 'net outcome of approved trades',
286
+ metrics: [
287
+ amountMetric('totalPnl', 'Total P&L', metrics.core.totalPnl, {
288
+ variant: 'primary',
289
+ }),
290
+ {
291
+ id: 'winrate',
292
+ label: 'Winrate',
293
+ value: formatPercent(metrics.core.winRate),
294
+ variant: 'primary',
295
+ },
296
+ amountMetric('avgTrade', 'Avg trade', metrics.core.avgTrade),
297
+ amountMetric('expectancy', 'Expectancy', metrics.core.expectancy),
298
+ ],
299
+ },
300
+ {
301
+ id: 'edge',
302
+ title: 'Edge quality',
303
+ description: 'profitability per winner vs loser',
304
+ metrics: [
305
+ {
306
+ id: 'profitFactor',
307
+ label: 'Profit factor',
308
+ value: getRatioLabel(metrics.core.profitFactor),
309
+ detail: 'gross profit / gross loss',
310
+ },
311
+ {
312
+ id: 'payoffRatio',
313
+ label: 'Payoff ratio',
314
+ value: getRatioLabel(metrics.core.payoffRatio),
315
+ detail: 'avg win / avg loss',
316
+ },
317
+ plainAmountMetric(
318
+ 'grossProfit',
319
+ 'Gross profit',
320
+ metrics.core.grossProfit,
321
+ {
322
+ tone: 'positive',
323
+ },
324
+ ),
325
+ plainAmountMetric('grossLoss', 'Gross loss', metrics.core.grossLoss, {
326
+ tone: 'negative',
327
+ }),
328
+ ],
329
+ },
330
+ {
331
+ id: 'cadence',
332
+ title: 'Cadence',
333
+ description: 'how often strategy trades',
334
+ metrics: [
335
+ {
336
+ id: 'trades',
337
+ label: 'Trades',
338
+ value: formatInteger(metrics.core.trades),
339
+ },
340
+ {
341
+ id: 'tradesPerDay',
342
+ label: 'Trades/day',
343
+ value: getNumberLabel(metrics.core.tradesPerDay),
344
+ },
345
+ {
346
+ id: 'tradesPerWeek',
347
+ label: 'Trades/week',
348
+ value: getNumberLabel(metrics.core.tradesPerWeek),
349
+ },
350
+ {
351
+ id: 'winsLosses',
352
+ label: 'Wins / Losses',
353
+ value: `${formatInteger(metrics.core.wins)} / ${formatInteger(metrics.core.losses)}`,
354
+ },
355
+ ],
356
+ },
357
+ ],
358
+ },
359
+ {
360
+ id: 'risk',
361
+ title: 'Risk',
362
+ subtitle: 'drawdown depth, recovery and worst periods',
363
+ groups: [
364
+ {
365
+ id: 'drawdown',
366
+ title: 'Drawdown',
367
+ description: 'peak-to-trough pressure',
368
+ metrics: [
369
+ plainAmountMetric('maxDrawdown', 'MaxDD', metrics.risk.maxDrawdown, {
370
+ detail: formatPercent(metrics.risk.maxDrawdownPercent),
371
+ tone: 'warning',
372
+ variant: 'primary',
373
+ }),
374
+ {
375
+ id: 'recoveryFactor',
376
+ label: 'Recovery factor',
377
+ value: getRatioLabel(metrics.risk.recoveryFactor),
378
+ detail: 'total P&L / MaxDD',
379
+ },
380
+ {
381
+ id: 'maxDrawdownToTotalProfit',
382
+ label: 'MaxDD / Total profit',
383
+ value: getRatioLabel(metrics.risk.maxDrawdownToTotalProfit),
384
+ },
385
+ {
386
+ id: 'maxDrawdownToGrossProfit',
387
+ label: 'MaxDD / Gross profit',
388
+ value: getRatioLabel(metrics.risk.maxDrawdownToGrossProfit),
389
+ },
390
+ ],
391
+ },
392
+ {
393
+ id: 'loss-pressure',
394
+ title: 'Loss pressure',
395
+ description: 'how losses cluster',
396
+ metrics: [
397
+ {
398
+ id: 'maxLossStreak',
399
+ label: 'Max loss streak',
400
+ value: formatInteger(metrics.risk.maxLossStreak),
401
+ tone: metrics.risk.maxLossStreak > 0 ? 'warning' : 'neutral',
402
+ },
403
+ {
404
+ id: 'losingMonthsCount',
405
+ label: 'Losing months',
406
+ value: formatInteger(metrics.risk.losingMonthsCount),
407
+ tone: metrics.risk.losingMonthsCount > 0 ? 'warning' : 'neutral',
408
+ },
409
+ amountMetric(
410
+ 'worstMonthPnl',
411
+ 'Worst month',
412
+ metrics.risk.worstMonthPnl,
413
+ ),
414
+ amountMetric(
415
+ 'worstRolling30dPnl',
416
+ 'Worst rolling 30d',
417
+ metrics.risk.worstRolling30dPnl,
418
+ ),
419
+ amountMetric(
420
+ 'worstRolling90dPnl',
421
+ 'Worst rolling 90d',
422
+ metrics.risk.worstRolling90dPnl,
423
+ ),
424
+ ],
425
+ },
426
+ ],
427
+ },
428
+ {
429
+ id: 'stability',
430
+ title: 'Stability',
431
+ subtitle: 'month-to-month consistency and concentration',
432
+ quarterlyPnl: metrics.stability.quarterlyPnl,
433
+ groups: [
434
+ {
435
+ id: 'calendar',
436
+ title: 'Calendar stability',
437
+ description: 'month and year behavior',
438
+ metrics: [
439
+ {
440
+ id: 'monthlyWinRate',
441
+ label: 'Monthly winrate',
442
+ value: formatPercent(metrics.stability.monthlyWinRate),
443
+ },
444
+ {
445
+ id: 'positiveMonthsPercent',
446
+ label: 'Positive months',
447
+ value: formatPercent(metrics.stability.positiveMonthsPercent),
448
+ },
449
+ amountMetric(
450
+ 'rolling365Pnl',
451
+ 'YoY rolling 365',
452
+ metrics.stability.rolling365Pnl,
453
+ ),
454
+ ],
455
+ },
456
+ {
457
+ id: 'monthly-spread',
458
+ title: 'Monthly spread',
459
+ description: 'typical month and dispersion',
460
+ metrics: [
461
+ amountMetric(
462
+ 'medianMonthlyPnl',
463
+ 'Median monthly P&L',
464
+ metrics.stability.medianMonthlyPnl,
465
+ ),
466
+ plainAmountMetric(
467
+ 'iqrMonthlyPnl',
468
+ 'IQR monthly P&L',
469
+ metrics.stability.iqrMonthlyPnl,
470
+ ),
471
+ ],
472
+ },
473
+ {
474
+ id: 'profit-concentration',
475
+ title: 'Profit concentration',
476
+ description: 'dependency on biggest winners',
477
+ metrics: [
478
+ {
479
+ id: 'top5ProfitShare',
480
+ label: 'Top 5 profit share',
481
+ value: formatPercent(metrics.stability.top5ProfitShare),
482
+ },
483
+ {
484
+ id: 'top10ProfitShare',
485
+ label: 'Top 10 profit share',
486
+ value: formatPercent(metrics.stability.top10ProfitShare),
487
+ },
488
+ ],
489
+ },
490
+ ],
491
+ },
492
+ {
493
+ id: 'distribution',
494
+ title: 'Distribution',
495
+ subtitle: 'shape of individual trade outcomes',
496
+ groups: [
497
+ {
498
+ id: 'typical-trade',
499
+ title: 'Typical trade',
500
+ description: 'central result and quartiles',
501
+ metrics: [
502
+ amountMetric(
503
+ 'medianTrade',
504
+ 'Median trade',
505
+ metrics.distribution.medianTrade,
506
+ { variant: 'primary' },
507
+ ),
508
+ amountMetric('p25Trade', 'P25 trade', metrics.distribution.p25Trade),
509
+ amountMetric('p75Trade', 'P75 trade', metrics.distribution.p75Trade),
510
+ ],
511
+ },
512
+ {
513
+ id: 'tails',
514
+ title: 'Tails',
515
+ description: 'outliers and asymmetry',
516
+ metrics: [
517
+ amountMetric('p10Trade', 'P10 trade', metrics.distribution.p10Trade),
518
+ amountMetric('p90Trade', 'P90 trade', metrics.distribution.p90Trade),
519
+ amountMetric(
520
+ 'largestWin',
521
+ 'Largest win',
522
+ metrics.distribution.largestWin,
523
+ ),
524
+ amountMetric(
525
+ 'largestLoss',
526
+ 'Largest loss',
527
+ metrics.distribution.largestLoss,
528
+ ),
529
+ {
530
+ id: 'tailRatio',
531
+ label: 'Tail ratio',
532
+ value: getRatioLabel(metrics.distribution.tailRatio),
533
+ detail: 'p95 win / abs(p5 loss)',
534
+ },
535
+ {
536
+ id: 'skewness',
537
+ label: 'Skewness',
538
+ value: getNumberLabel(metrics.distribution.skewness),
539
+ },
540
+ ],
541
+ },
542
+ ],
543
+ },
544
+ {
545
+ id: 'riskAdjusted',
546
+ title: 'Risk-adjusted',
547
+ subtitle: 'return normalized by volatility and drawdown',
548
+ groups: [
549
+ {
550
+ id: 'daily-pnl',
551
+ title: 'Daily approved P&L',
552
+ description: 'daily series, not trade-level series',
553
+ metrics: [
554
+ {
555
+ id: 'sharpeDaily',
556
+ label: 'Sharpe',
557
+ value: getNumberLabel(metrics.riskAdjusted.sharpeDaily),
558
+ },
559
+ {
560
+ id: 'sortinoDaily',
561
+ label: 'Sortino',
562
+ value: getNumberLabel(metrics.riskAdjusted.sortinoDaily),
563
+ },
564
+ ],
565
+ },
566
+ {
567
+ id: 'drawdown-adjusted',
568
+ title: 'Drawdown-adjusted',
569
+ description: 'annualized P&L / MaxDD',
570
+ metrics: [
571
+ {
572
+ id: 'calmar',
573
+ label: 'Calmar',
574
+ value: getRatioLabel(metrics.riskAdjusted.calmar),
575
+ },
576
+ {
577
+ id: 'mar',
578
+ label: 'MAR',
579
+ value: getRatioLabel(metrics.riskAdjusted.mar),
580
+ },
581
+ ],
582
+ },
583
+ ],
584
+ },
585
+ {
586
+ id: 'operational',
587
+ title: 'Operational',
588
+ subtitle: 'execution quality, approvals and exposure split',
589
+ groups: [
590
+ {
591
+ id: 'execution',
592
+ title: 'Execution',
593
+ description: 'slippage impact on net P&L',
594
+ metrics: [
595
+ {
596
+ id: 'avgSlippageCost',
597
+ label: 'Avg slippage cost',
598
+ value: formatFee(metrics.operational.avgSlippageCost),
599
+ tone: metrics.operational.avgSlippageCost ? 'warning' : 'neutral',
600
+ },
601
+ amountMetric(
602
+ 'pnlBeforeSlippage',
603
+ 'P&L before slippage',
604
+ metrics.operational.pnlBeforeSlippage,
605
+ ),
606
+ amountMetric(
607
+ 'pnlAfterSlippage',
608
+ 'P&L after slippage',
609
+ metrics.operational.pnlAfterSlippage,
610
+ ),
611
+ ],
612
+ },
613
+ {
614
+ id: 'approval',
615
+ title: 'Approval quality',
616
+ description: 'gate decisions and misses',
617
+ metrics: [
618
+ {
619
+ id: 'approvalRate',
620
+ label: 'Approval rate',
621
+ value: formatPercent(metrics.operational.approvalRate),
622
+ },
623
+ {
624
+ id: 'blockedProfitableTrades',
625
+ label: 'Blocked profitable',
626
+ value: formatInteger(metrics.operational.blockedProfitableTrades),
627
+ },
628
+ {
629
+ id: 'approvedLosingTrades',
630
+ label: 'Approved losing',
631
+ value: formatInteger(metrics.operational.approvedLosingTrades),
632
+ tone:
633
+ metrics.operational.approvedLosingTrades > 0
634
+ ? 'warning'
635
+ : 'neutral',
636
+ },
637
+ ],
638
+ },
639
+ {
640
+ id: 'close-reasons',
641
+ title: 'Close reasons',
642
+ description: 'closed trade outcomes by exit path',
643
+ metrics: [
644
+ exitBreakdownMetric(
645
+ 'takeProfitExits',
646
+ 'TP closes',
647
+ metrics.operational.exitBreakdown.takeProfit,
648
+ ),
649
+ exitBreakdownMetric(
650
+ 'stopLossExits',
651
+ 'SL closes',
652
+ metrics.operational.exitBreakdown.stopLoss,
653
+ ),
654
+ exitBreakdownMetric(
655
+ 'signalExits',
656
+ 'EXIT closes',
657
+ metrics.operational.exitBreakdown.exit,
658
+ ),
659
+ ...(metrics.operational.exitBreakdown.unknown.count > 0
660
+ ? [
661
+ exitBreakdownMetric(
662
+ 'unknownExits',
663
+ 'Unknown closes',
664
+ metrics.operational.exitBreakdown.unknown,
665
+ ),
666
+ ]
667
+ : []),
668
+ ],
669
+ },
670
+ {
671
+ id: 'concentration',
672
+ title: 'Concentration',
673
+ description: 'where absolute P&L comes from',
674
+ metrics: [
675
+ {
676
+ id: 'symbolConcentrationTop1',
677
+ label: 'Symbol top 1',
678
+ value: formatPercent(metrics.operational.symbolConcentrationTop1),
679
+ },
680
+ {
681
+ id: 'symbolConcentrationTop5',
682
+ label: 'Symbol top 5',
683
+ value: formatPercent(metrics.operational.symbolConcentrationTop5),
684
+ },
685
+ {
686
+ id: 'sessionConcentrationTop1',
687
+ label: 'Session top 1',
688
+ value: formatPercent(metrics.operational.sessionConcentrationTop1),
689
+ },
690
+ ],
691
+ },
692
+ {
693
+ id: 'direction',
694
+ title: 'Direction split',
695
+ description: 'LONG / SHORT trade count and P&L',
696
+ metrics: [
697
+ {
698
+ id: 'longTrades',
699
+ label: 'LONG trades',
700
+ value: formatInteger(metrics.operational.longTrades),
701
+ detail: getAmountLabel(metrics.operational.longPnl),
702
+ tone: 'positive',
703
+ },
704
+ {
705
+ id: 'shortTrades',
706
+ label: 'SHORT trades',
707
+ value: formatInteger(metrics.operational.shortTrades),
708
+ detail: getAmountLabel(metrics.operational.shortPnl),
709
+ tone: 'negative',
710
+ },
711
+ ],
712
+ },
713
+ ],
714
+ },
715
+ ];
716
+
717
+ export const AdvancedMetricsPanel = ({
718
+ metrics,
719
+ }: {
720
+ metrics: AdvancedTradeMetrics;
721
+ }) => (
722
+ <Flex direction="column" gap={4}>
723
+ {buildMetricSections(metrics).map((section) => (
724
+ <MetricsSection key={section.id} section={section} />
725
+ ))}
726
+ </Flex>
727
+ );