@tradejs/app 1.0.9 → 1.0.11

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 (125) hide show
  1. package/README.md +16 -3
  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/install/route.ts +51 -0
  20. package/src/app/api/kline/[provider]/[universe]/[symbol]/[interval]/route.ts +1 -0
  21. package/src/app/api/kline/{[provider]/[symbol]/[interval]/route.ts → handler.ts} +61 -39
  22. package/src/app/api/scanner/[provider]/[universe]/route.ts +1 -0
  23. package/src/app/api/scanner/[provider]/route.ts +30 -12
  24. package/src/app/api/scanner/route.ts +3 -3
  25. package/src/app/api/signal/[symbol]/[signalId]/route.ts +1 -1
  26. package/src/app/api/strategies/ai/[cardId]/route.ts +22 -0
  27. package/src/app/api/strategies/ai/route.ts +139 -0
  28. package/src/app/api/strategies/replay/[cardId]/route.ts +18 -0
  29. package/src/app/api/strategies/replay/route.ts +44 -0
  30. package/src/app/api/strategies/runtime/route.ts +831 -0
  31. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +25 -0
  32. package/src/app/api/user/runtime-deployments/route.ts +94 -0
  33. package/src/app/api/user/runtime-strategy-configs/route.ts +241 -0
  34. package/src/app/api/user/settings/route.ts +18 -1
  35. package/src/app/api/user/trading-accounts/[accountId]/route.ts +35 -0
  36. package/src/app/api/user/trading-accounts/route.ts +134 -0
  37. package/src/app/auth.ts +1 -19
  38. package/src/app/components/Backtest/CompareList/index.tsx +1 -1
  39. package/src/app/components/Backtest/ResultsPageClient.tsx +376 -0
  40. package/src/app/components/Backtest/TestCard/ActionsMenu/index.tsx +123 -0
  41. package/src/app/components/Backtest/TestCard/Chart/index.tsx +1 -1
  42. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +1 -1
  43. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +28 -12
  44. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +33 -17
  45. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +2 -2
  46. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +10 -1
  47. package/src/app/components/Backtest/TestCard/OrdersDrawer/index.tsx +128 -0
  48. package/src/app/components/Backtest/TestCard/Root/index.tsx +18 -4
  49. package/src/app/components/Backtest/TestCard/Stat/index.tsx +2 -0
  50. package/src/app/components/Backtest/TestCard/StatDrawer/index.tsx +66 -0
  51. package/src/app/components/Backtest/TestCard/index.ts +6 -2
  52. package/src/app/components/Backtest/TestList/index.tsx +4 -9
  53. package/src/app/components/Dashboard/AiDrawer/index.tsx +1 -1
  54. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +1 -1
  55. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +10 -5
  56. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +1 -1
  57. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +4 -1
  58. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +3 -1
  59. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +1 -1
  60. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +1 -1
  61. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +1 -1
  62. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +1 -1
  63. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +1 -1
  64. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +3 -1
  65. package/src/app/components/Dashboard/KlineChart/index.tsx +21 -7
  66. package/src/app/components/Dashboard/MainChart/index.tsx +3 -22
  67. package/src/app/components/Shared/AppShell.tsx +2 -2
  68. package/src/app/components/Shared/BulkSelection/index.tsx +140 -0
  69. package/src/app/components/Shared/BulkSelection/useBulkSelection.ts +97 -0
  70. package/src/app/components/Shared/BulkSelection/utils.ts +126 -0
  71. package/src/app/components/Shared/Filters/Backtest/index.tsx +51 -51
  72. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +5 -3
  73. package/src/app/components/Shared/Filters/Indicators/index.tsx +2 -2
  74. package/src/app/components/Shared/Filters/Interval/index.tsx +1 -1
  75. package/src/app/components/Shared/Filters/Provider/index.tsx +8 -2
  76. package/src/app/components/Shared/Filters/Symbol/index.tsx +4 -3
  77. package/src/app/components/Shared/Filters/Universe/index.tsx +36 -0
  78. package/src/app/components/Shared/Filters/index.ts +2 -0
  79. package/src/app/components/Shared/OrdersDrawer.tsx +629 -0
  80. package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +55 -28
  81. package/src/app/components/Shared/Sidebar/TradingAccountsPanel.tsx +445 -0
  82. package/src/app/components/Shared/Sidebar/index.tsx +17 -3
  83. package/src/app/components/Strategies/AdvancedMetricsPanel.tsx +727 -0
  84. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +2204 -0
  85. package/src/app/components/Strategies/RuntimeStrategyCardSkeleton.tsx +40 -0
  86. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +121 -0
  87. package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +458 -0
  88. package/src/app/components/Strategies/StrategySnapshotCard.tsx +2890 -0
  89. package/src/app/components/Strategies/StrategySnapshotChart.tsx +94 -0
  90. package/src/app/components/Strategies/StrategySnapshotList.tsx +82 -0
  91. package/src/app/components/UI/Segment/index.tsx +8 -1
  92. package/src/app/components/UI/Select/index.tsx +24 -13
  93. package/src/app/components/UI/SelectWithSearch/index.tsx +46 -5
  94. package/src/app/layout.tsx +1 -1
  95. package/src/app/lib/backtestJobs.ts +925 -0
  96. package/src/app/lib/cardPageLayout.ts +1 -0
  97. package/src/app/lib/connectorCreator.ts +28 -0
  98. package/src/app/lib/currentUser.ts +1 -1
  99. package/src/app/lib/errorMessage.ts +34 -0
  100. package/src/app/lib/installation.ts +83 -0
  101. package/src/app/lib/marketDefaults.ts +12 -0
  102. package/src/app/lib/marketRoutes.ts +58 -0
  103. package/src/app/lib/runtimeStrategies.ts +1376 -0
  104. package/src/app/lib/runtimeStrategyConfigForm.ts +86 -0
  105. package/src/app/provider.tsx +1 -1
  106. package/src/app/routes/backtest/page.tsx +1100 -302
  107. package/src/app/routes/dashboard/{[provider]/[symbol]/[interval]/page.tsx → Dashboard.tsx} +76 -35
  108. package/src/app/routes/dashboard/[provider]/[universe]/[symbol]/[interval]/page.tsx +1 -0
  109. package/src/app/routes/dashboard/page.tsx +15 -3
  110. package/src/app/routes/derivatives/page.tsx +1222 -165
  111. package/src/app/routes/install/page.tsx +175 -0
  112. package/src/app/routes/signin/page.tsx +7 -0
  113. package/src/app/routes/strategies/StrategiesPageClient.tsx +593 -0
  114. package/src/app/routes/strategies/[mode]/page.tsx +20 -0
  115. package/src/app/routes/strategies/page.tsx +7 -0
  116. package/src/app/store/ai.ts +1 -1
  117. package/src/app/store/data.ts +73 -17
  118. package/src/app/store/filters.ts +1 -0
  119. package/src/app/store/marketKlineStream.ts +87 -0
  120. package/src/app/store/tests.ts +83 -45
  121. package/src/app/store/tickers.ts +59 -22
  122. package/src/proxy.ts +4 -0
  123. package/tsconfig.json +17 -12
  124. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +0 -24
  125. 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
+ );