@tradejs/app 2.0.18 → 2.0.20

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 (52) hide show
  1. package/package.json +14 -8
  2. package/src/app/actions/backtest.ts +2 -1
  3. package/src/app/actions/scanner.ts +2 -1
  4. package/src/app/api/backtest/files/route.ts +2 -1
  5. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +1 -1
  6. package/src/app/api/derivatives/[symbol]/[interval]/route.ts +1 -1
  7. package/src/app/api/derivatives/summary/route.ts +1 -1
  8. package/src/app/api/signal/[symbol]/[signalId]/route.ts +2 -1
  9. package/src/app/api/spread/[symbol]/[interval]/route.ts +1 -1
  10. package/src/app/api/spread/summary/route.ts +1 -1
  11. package/src/app/api/strategies/runtime/route.ts +4 -674
  12. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +1 -1
  13. package/src/app/api/user/runtime-deployments/route.ts +2 -2
  14. package/src/app/api/user/runtime-strategy-configs/route.ts +22 -212
  15. package/src/app/api/user/trading-accounts/[accountId]/route.ts +1 -1
  16. package/src/app/components/Backtest/TestList/index.tsx +1 -1
  17. package/src/app/components/Dashboard/KlineChart/figures/circle.ts +1 -1
  18. package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +1 -1
  19. package/src/app/components/Dashboard/KlineChart/figures/label.ts +1 -1
  20. package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +1 -1
  21. package/src/app/components/Dashboard/KlineChart/figures/star.ts +1 -1
  22. package/src/app/components/Dashboard/KlineChart/index.tsx +2 -1
  23. package/src/app/components/Shared/Filters/Root/index.tsx +1 -1
  24. package/src/app/components/Shared/Filters/context.ts +1 -1
  25. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +82 -861
  26. package/src/app/components/Strategies/RuntimeStrategyChart.tsx +115 -184
  27. package/src/app/components/Strategies/StrategyEvidencePopover.tsx +259 -0
  28. package/src/app/components/Strategies/StrategyPerformanceCharts.tsx +419 -0
  29. package/src/app/components/Strategies/StrategySnapshotCard.tsx +122 -937
  30. package/src/app/components/UI/Segment/index.tsx +1 -1
  31. package/src/app/components/UI/Select/index.tsx +1 -1
  32. package/src/app/components/UI/SelectWithSearch/index.tsx +1 -1
  33. package/src/app/lib/backtestJobContracts.ts +67 -0
  34. package/src/app/lib/backtestJobProgress.ts +28 -0
  35. package/src/app/lib/backtestJobRequest.ts +107 -0
  36. package/src/app/lib/backtestJobs.ts +25 -257
  37. package/src/app/lib/runtimeDashboard.ts +700 -0
  38. package/src/app/lib/runtimeStrategies.ts +22 -457
  39. package/src/app/lib/runtimeStrategyConfigService.ts +279 -0
  40. package/src/app/lib/runtimeStrategyLineage.ts +264 -0
  41. package/src/app/lib/runtimeTradeReconciliation.ts +113 -0
  42. package/src/app/lib/runtimeTradeSync.ts +1 -1
  43. package/src/app/lib/strategyEvidenceTimeline.ts +298 -0
  44. package/src/app/lib/strategyPerformance.ts +387 -0
  45. package/src/app/routes/dashboard/Dashboard.tsx +2 -6
  46. package/src/app/routes/derivatives/derivativesViewModel.ts +253 -0
  47. package/src/app/routes/derivatives/page.tsx +70 -262
  48. package/src/app/store/filters.ts +2 -1
  49. package/src/app/store/indicators.ts +2 -1
  50. package/src/app/store/tests.ts +1 -1
  51. package/src/app/store/tickers.ts +2 -1
  52. package/src/app/types/ui.ts +20 -0
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useMemo } from 'react';
3
+ import { useMemo, useState } from 'react';
4
4
  import { Box, Flex, Text } from '@chakra-ui/react';
5
5
  import { Chart, useChart } from '@chakra-ui/charts';
6
6
  import {
@@ -13,20 +13,24 @@ import {
13
13
  YAxis,
14
14
  } from 'recharts';
15
15
  import { getFormatted } from '@tradejs/core/backtest';
16
- import type { SimpleOrderLogData, TestStat } from '@tradejs/types';
17
16
  import type {
18
- RuntimeStrategyAiGateChange,
19
- RuntimeStrategyMaxLossValueTimeline,
20
- } from '#app/lib/runtimeStrategies';
17
+ SimpleOrderLogData,
18
+ StrategyEvidenceMarkerType,
19
+ StrategyEvidenceTimeline,
20
+ TestStat,
21
+ } from '@tradejs/types';
21
22
  import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
22
23
  import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
24
+ import {
25
+ filterStrategyEvidenceMarkers,
26
+ STRATEGY_EVIDENCE_MARKER_PRESENTATION,
27
+ StrategyEvidencePopover,
28
+ } from './StrategyEvidencePopover';
23
29
 
24
30
  interface RuntimeStrategyChartProps {
25
31
  orderLog: SimpleOrderLogData;
26
32
  stat: TestStat;
27
- aiGateObservedFrom: number | null;
28
- aiGateChanges: RuntimeStrategyAiGateChange[];
29
- maxLossValueTimeline: RuntimeStrategyMaxLossValueTimeline;
33
+ evidenceTimeline: StrategyEvidenceTimeline;
30
34
  startTimestamp: number;
31
35
  endTimestamp: number;
32
36
  height?: string | number;
@@ -35,13 +39,13 @@ interface RuntimeStrategyChartProps {
35
39
  export const RuntimeStrategyChart = ({
36
40
  orderLog,
37
41
  stat,
38
- aiGateObservedFrom,
39
- aiGateChanges,
40
- maxLossValueTimeline,
42
+ evidenceTimeline,
41
43
  startTimestamp,
42
44
  endTimestamp,
43
45
  height = '350px',
44
46
  }: RuntimeStrategyChartProps) => {
47
+ const [showParity, setShowParity] = useState(true);
48
+ const [showRecommendations, setShowRecommendations] = useState(true);
45
49
  const chartData = useMemo(
46
50
  () => ({
47
51
  data: orderLog.map(([timestamp, amount]) => ({
@@ -61,193 +65,120 @@ export const RuntimeStrategyChart = ({
61
65
  const chart = useChart(chartData as any);
62
66
  const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
63
67
  const { formatted: minAmount } = getFormatted(stat, 'minAmount');
64
- const gateChangeColor = chart.color('purple.400');
65
- const maxLossValueChangeColor = chart.color('orange.400');
66
-
67
- if (!orderLog.length) {
68
- return (
69
- <Box
70
- w="100%"
71
- minW="600px"
72
- h={height}
73
- display="flex"
74
- alignItems="center"
75
- justifyContent="center"
76
- >
77
- <Text color="gray.500">No runtime trades for the selected window.</Text>
78
- </Box>
79
- );
80
- }
68
+ const visibleEvidenceMarkers = filterStrategyEvidenceMarkers({
69
+ markers:
70
+ evidenceTimeline.status === 'verified' ? evidenceTimeline.markers : [],
71
+ showParity,
72
+ showRecommendations,
73
+ });
74
+ const markerColor = (type: StrategyEvidenceMarkerType) =>
75
+ chart.color(STRATEGY_EVIDENCE_MARKER_PRESENTATION[type].color);
76
+ const markerDash = (type: StrategyEvidenceMarkerType) => {
77
+ if (type === 'L') return '7 4';
78
+ if (type === 'P' || type === 'R') return '2 5';
79
+ return '3 5';
80
+ };
81
81
 
82
82
  return (
83
83
  <Box w="100%" minW="600px" h={height} display="flex" flexDirection="column">
84
- {aiGateObservedFrom != null ? (
85
- <Flex
86
- minH="24px"
87
- px={2}
88
- gap={4}
89
- alignItems="center"
90
- overflowX="auto"
91
- flexShrink={0}
92
- whiteSpace="nowrap"
93
- >
94
- <Text
95
- flexShrink={0}
96
- fontSize="xs"
97
- color="gray.500"
98
- title="AI-gate changes before this first runtime observation are unavailable"
99
- >
100
- AI-gate history from{' '}
101
- {formatTimeSeriesTooltipTimestamp(aiGateObservedFrom)}
102
- </Text>
103
- {aiGateChanges.map((change, index) => (
104
- <Text
105
- key={`${change.timestamp}:${change.fingerprint}:legend`}
106
- flexShrink={0}
107
- fontSize="xs"
108
- color="gray.400"
109
- title={`${change.previousFingerprint} → ${change.fingerprint}`}
110
- >
111
- <Text as="span" color={gateChangeColor} fontWeight="semibold">
112
- G{index + 1}
113
- </Text>{' '}
114
- {formatTimeSeriesTooltipTimestamp(change.timestamp)} ·{' '}
115
- {change.fingerprint.slice(0, 7)}
116
- </Text>
117
- ))}
118
- {maxLossValueTimeline.observedFrom != null &&
119
- maxLossValueTimeline.initialValue != null ? (
120
- <Text
121
- flexShrink={0}
122
- fontSize="xs"
123
- color="gray.500"
124
- title="MAX_LOSS_VALUE changes before this first runtime observation are unavailable"
125
- >
126
- MAX_LOSS_VALUE history from{' '}
127
- {formatTimeSeriesTooltipTimestamp(
128
- maxLossValueTimeline.observedFrom,
129
- )}{' '}
130
- · initial {maxLossValueTimeline.initialValue}$
131
- </Text>
132
- ) : null}
133
- {maxLossValueTimeline.changes.map((change, index) => (
134
- <Text
135
- key={`${change.timestamp}:${change.value}:max-loss-legend`}
136
- flexShrink={0}
137
- fontSize="xs"
138
- color="gray.400"
139
- >
140
- <Text
141
- as="span"
142
- color={maxLossValueChangeColor}
143
- fontWeight="semibold"
144
- >
145
- L{index + 1}
146
- </Text>{' '}
147
- {formatTimeSeriesTooltipTimestamp(change.timestamp)} ·{' '}
148
- {change.previousValue}$ → {change.value}$
149
- </Text>
150
- ))}
151
- </Flex>
152
- ) : null}
84
+ <Flex minH="32px" px={2} justify="flex-end" align="center" flexShrink={0}>
85
+ <StrategyEvidencePopover
86
+ timeline={evidenceTimeline}
87
+ showParity={showParity}
88
+ showRecommendations={showRecommendations}
89
+ onShowParityChange={setShowParity}
90
+ onShowRecommendationsChange={setShowRecommendations}
91
+ />
92
+ </Flex>
153
93
  <Box flex="1" minH={0} pr={2}>
154
- <ResponsiveContainer width="100%" height="100%">
155
- <Chart.Root maxH="md" chart={chart}>
156
- <LineChart data={chart.data}>
157
- <CartesianGrid stroke={chart.color('border')} vertical={false} />
158
- <ReferenceLine
159
- stroke={chart.color('gray.600')}
160
- strokeDasharray="5 5"
161
- y={stat.maxAmount}
162
- label={{
163
- value: `Max: ${maxAmount}`,
164
- offset: 10,
165
- fill: chart.color('gray.600'),
166
- position: 'top',
167
- }}
168
- />
169
- <ReferenceLine
170
- stroke={chart.color('gray.600')}
171
- strokeDasharray="8 8"
172
- y={100}
173
- />
174
- <ReferenceLine
175
- stroke={chart.color('gray.600')}
176
- strokeDasharray="5 5"
177
- y={stat.minAmount}
178
- label={{
179
- value: `Min: ${minAmount}`,
180
- offset: 10,
181
- fill: chart.color('gray.600'),
182
- position: 'bottom',
183
- }}
184
- />
185
- {aiGateChanges.map((change, index) => (
94
+ {orderLog.length ? (
95
+ <ResponsiveContainer width="100%" height="100%">
96
+ <Chart.Root maxH="md" chart={chart}>
97
+ <LineChart data={chart.data}>
98
+ <CartesianGrid
99
+ stroke={chart.color('border')}
100
+ vertical={false}
101
+ />
186
102
  <ReferenceLine
187
- key={`${change.timestamp}:${change.fingerprint}`}
188
- x={change.timestamp}
189
- stroke={gateChangeColor}
190
- strokeDasharray="3 5"
191
- strokeWidth={1.5}
103
+ stroke={chart.color('gray.600')}
104
+ strokeDasharray="5 5"
105
+ y={stat.maxAmount}
192
106
  label={{
193
- value: `G${index + 1}`,
194
- fill: gateChangeColor,
195
- fontSize: 10,
196
- fontWeight: 600,
197
- offset: 6,
198
- position:
199
- index % 2 === 0 ? 'insideTopRight' : 'insideTopLeft',
107
+ value: `Max: ${maxAmount}`,
108
+ offset: 10,
109
+ fill: chart.color('gray.600'),
110
+ position: 'top',
200
111
  }}
201
112
  />
202
- ))}
203
- {maxLossValueTimeline.changes.map((change, index) => (
204
113
  <ReferenceLine
205
- key={`${change.timestamp}:${change.value}:max-loss`}
206
- x={change.timestamp}
207
- stroke={maxLossValueChangeColor}
208
- strokeDasharray="7 4"
209
- strokeWidth={1.5}
114
+ stroke={chart.color('gray.600')}
115
+ strokeDasharray="8 8"
116
+ y={100}
117
+ />
118
+ <ReferenceLine
119
+ stroke={chart.color('gray.600')}
120
+ strokeDasharray="5 5"
121
+ y={stat.minAmount}
210
122
  label={{
211
- value: `L${index + 1}`,
212
- fill: maxLossValueChangeColor,
213
- fontSize: 10,
214
- fontWeight: 600,
215
- offset: 6,
216
- position:
217
- index % 2 === 0
218
- ? 'insideBottomRight'
219
- : 'insideBottomLeft',
123
+ value: `Min: ${minAmount}`,
124
+ offset: 10,
125
+ fill: chart.color('gray.600'),
126
+ position: 'bottom',
220
127
  }}
221
128
  />
222
- ))}
223
- <TimeSeriesXAxis
224
- startTimestamp={startTimestamp}
225
- endTimestamp={endTimestamp}
226
- />
227
- <YAxis tickCount={10} domain={[stat.minAmount - 10, 'auto']} />
228
- <Tooltip
229
- animationDuration={100}
230
- cursor={false}
231
- content={
232
- <Chart.Tooltip
233
- labelFormatter={formatTimeSeriesTooltipTimestamp}
129
+ {visibleEvidenceMarkers.map((marker, index) => (
130
+ <ReferenceLine
131
+ key={marker.id}
132
+ x={marker.timestamp}
133
+ stroke={markerColor(marker.type)}
134
+ strokeDasharray={markerDash(marker.type)}
135
+ strokeWidth={1.5}
136
+ label={{
137
+ value: marker.type,
138
+ fill: markerColor(marker.type),
139
+ fontSize: 10,
140
+ fontWeight: 600,
141
+ offset: 6,
142
+ position:
143
+ index % 2 === 0 ? 'insideTopRight' : 'insideTopLeft',
144
+ }}
234
145
  />
235
- }
236
- />
237
- {chart.series.map((item) => (
238
- <Line
239
- key={item.name as string}
240
- isAnimationActive={false}
241
- dataKey={chart.key(item.name) as string}
242
- stroke={chart.color(item.color)}
243
- strokeWidth={2}
244
- dot={false}
245
- activeDot={{ r: 5, strokeWidth: 2 }}
146
+ ))}
147
+ <TimeSeriesXAxis
148
+ startTimestamp={startTimestamp}
149
+ endTimestamp={endTimestamp}
246
150
  />
247
- ))}
248
- </LineChart>
249
- </Chart.Root>
250
- </ResponsiveContainer>
151
+ <YAxis tickCount={10} domain={[stat.minAmount - 10, 'auto']} />
152
+ <Tooltip
153
+ animationDuration={100}
154
+ cursor={false}
155
+ content={
156
+ <Chart.Tooltip
157
+ labelFormatter={formatTimeSeriesTooltipTimestamp}
158
+ />
159
+ }
160
+ />
161
+ {chart.series.map((item) => (
162
+ <Line
163
+ key={item.name as string}
164
+ isAnimationActive={false}
165
+ dataKey={chart.key(item.name) as string}
166
+ stroke={chart.color(item.color)}
167
+ strokeWidth={2}
168
+ dot={false}
169
+ activeDot={{ r: 5, strokeWidth: 2 }}
170
+ />
171
+ ))}
172
+ </LineChart>
173
+ </Chart.Root>
174
+ </ResponsiveContainer>
175
+ ) : (
176
+ <Flex h="100%" align="center" justify="center">
177
+ <Text color="gray.500">
178
+ No runtime trades for the selected window.
179
+ </Text>
180
+ </Flex>
181
+ )}
251
182
  </Box>
252
183
  </Box>
253
184
  );
@@ -0,0 +1,259 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Badge,
5
+ Box,
6
+ Button,
7
+ Checkbox,
8
+ Flex,
9
+ Popover,
10
+ Portal,
11
+ Text,
12
+ } from '@chakra-ui/react';
13
+ import type {
14
+ StrategyEvidenceMarker,
15
+ StrategyEvidenceMarkerType,
16
+ StrategyEvidenceTimeline,
17
+ } from '@tradejs/types';
18
+ import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
19
+
20
+ export const STRATEGY_EVIDENCE_MARKER_PRESENTATION: Record<
21
+ StrategyEvidenceMarkerType,
22
+ { name: string; color: string; optional: boolean }
23
+ > = {
24
+ G: { name: 'Composition / gate', color: 'purple.400', optional: false },
25
+ L: { name: 'MAX_LOSS_VALUE', color: 'orange.400', optional: false },
26
+ E: { name: 'Evidence boundary', color: 'teal.400', optional: false },
27
+ D: { name: 'Deployment', color: 'blue.400', optional: false },
28
+ P: { name: 'Runtime parity', color: 'cyan.400', optional: true },
29
+ R: { name: 'Recommendation', color: 'pink.400', optional: true },
30
+ };
31
+
32
+ const MARKER_TYPES = Object.keys(
33
+ STRATEGY_EVIDENCE_MARKER_PRESENTATION,
34
+ ) as StrategyEvidenceMarkerType[];
35
+
36
+ export const filterStrategyEvidenceMarkers = ({
37
+ markers,
38
+ showParity,
39
+ showRecommendations,
40
+ }: {
41
+ markers: StrategyEvidenceMarker[];
42
+ showParity: boolean;
43
+ showRecommendations: boolean;
44
+ }) =>
45
+ markers.filter(
46
+ (marker) =>
47
+ (marker.type !== 'P' || showParity) &&
48
+ (marker.type !== 'R' || showRecommendations),
49
+ );
50
+
51
+ const statusColor = (status: StrategyEvidenceTimeline['status']) => {
52
+ if (status === 'verified') return 'teal';
53
+ if (status === 'invalid') return 'red';
54
+ return 'orange';
55
+ };
56
+
57
+ const uniqueProvenance = (markers: StrategyEvidenceMarker[]) => {
58
+ const seen = new Set<string>();
59
+ return markers.filter((marker) => {
60
+ const key = `${marker.artifactId}:${marker.artifactSha256}`;
61
+ if (seen.has(key)) return false;
62
+ seen.add(key);
63
+ return true;
64
+ });
65
+ };
66
+
67
+ const FilterCheckbox = ({
68
+ checked,
69
+ label,
70
+ onChange,
71
+ }: {
72
+ checked: boolean;
73
+ label: string;
74
+ onChange: (checked: boolean) => void;
75
+ }) => (
76
+ <Checkbox.Root
77
+ size="sm"
78
+ checked={checked}
79
+ onCheckedChange={({ checked: nextChecked }) =>
80
+ onChange(nextChecked === true)
81
+ }
82
+ >
83
+ <Checkbox.HiddenInput />
84
+ <Checkbox.Control>
85
+ <Checkbox.Indicator />
86
+ </Checkbox.Control>
87
+ <Checkbox.Label>{label}</Checkbox.Label>
88
+ </Checkbox.Root>
89
+ );
90
+
91
+ export const StrategyEvidencePopover = ({
92
+ timeline,
93
+ showParity,
94
+ showRecommendations,
95
+ onShowParityChange,
96
+ onShowRecommendationsChange,
97
+ }: {
98
+ timeline: StrategyEvidenceTimeline;
99
+ showParity: boolean;
100
+ showRecommendations: boolean;
101
+ onShowParityChange: (checked: boolean) => void;
102
+ onShowRecommendationsChange: (checked: boolean) => void;
103
+ }) => {
104
+ const provenance =
105
+ timeline.status === 'verified' ? uniqueProvenance(timeline.markers) : [];
106
+
107
+ return (
108
+ <Popover.Root positioning={{ placement: 'bottom-end' }} lazyMount>
109
+ <Popover.Trigger asChild>
110
+ <Button
111
+ size="xs"
112
+ variant="outline"
113
+ aria-label={`Evidence: ${timeline.status}`}
114
+ >
115
+ Evidence
116
+ <Badge size="xs" colorPalette={statusColor(timeline.status)}>
117
+ {timeline.status === 'verified'
118
+ ? `${timeline.markers.length}`
119
+ : timeline.status}
120
+ </Badge>
121
+ </Button>
122
+ </Popover.Trigger>
123
+ <Portal>
124
+ <Popover.Positioner>
125
+ <Popover.Content width="min(430px, calc(100vw - 24px))">
126
+ <Popover.Arrow />
127
+ <Popover.Body>
128
+ <Flex direction="column" gap={3}>
129
+ <Box>
130
+ <Text fontWeight="semibold">Immutable evidence</Text>
131
+ <Text fontSize="xs" color="gray.500">
132
+ {timeline.status === 'verified'
133
+ ? timeline.observedFrom == null
134
+ ? 'Verified artifact; no observation boundary supplied.'
135
+ : `Verified from ${formatTimeSeriesTooltipTimestamp(
136
+ timeline.observedFrom,
137
+ )}.`
138
+ : timeline.status === 'invalid'
139
+ ? 'Evidence failed checksum, identity, or structure verification. No markers are rendered.'
140
+ : 'No checksum-verified evidence artifact is available. No mutable fallback is used.'}
141
+ </Text>
142
+ </Box>
143
+
144
+ <Box>
145
+ <Text fontSize="xs" fontWeight="semibold" mb={1}>
146
+ Legend
147
+ </Text>
148
+ <Flex wrap="wrap" gapX={3} gapY={1}>
149
+ {MARKER_TYPES.map((type) => (
150
+ <Flex key={type} align="center" gap={1}>
151
+ <Text
152
+ color={
153
+ STRATEGY_EVIDENCE_MARKER_PRESENTATION[type].color
154
+ }
155
+ fontWeight="bold"
156
+ fontSize="xs"
157
+ >
158
+ {type}
159
+ </Text>
160
+ <Text fontSize="xs" color="gray.500">
161
+ {STRATEGY_EVIDENCE_MARKER_PRESENTATION[type].name}
162
+ </Text>
163
+ </Flex>
164
+ ))}
165
+ </Flex>
166
+ </Box>
167
+
168
+ <Box>
169
+ <Text fontSize="xs" fontWeight="semibold" mb={1}>
170
+ Optional markers
171
+ </Text>
172
+ <Flex gap={4}>
173
+ <FilterCheckbox
174
+ checked={showParity}
175
+ label="Parity (P)"
176
+ onChange={onShowParityChange}
177
+ />
178
+ <FilterCheckbox
179
+ checked={showRecommendations}
180
+ label="Recommendations (R)"
181
+ onChange={onShowRecommendationsChange}
182
+ />
183
+ </Flex>
184
+ </Box>
185
+
186
+ <Box>
187
+ <Text fontSize="xs" fontWeight="semibold" mb={1}>
188
+ Events and provenance
189
+ </Text>
190
+ {timeline.status === 'verified' && timeline.markers.length ? (
191
+ <Flex
192
+ direction="column"
193
+ gap={2}
194
+ maxH="220px"
195
+ overflowY="auto"
196
+ >
197
+ {timeline.markers.map((marker) => (
198
+ <Box key={marker.id}>
199
+ <Flex align="baseline" gap={2}>
200
+ <Text
201
+ color={
202
+ STRATEGY_EVIDENCE_MARKER_PRESENTATION[
203
+ marker.type
204
+ ].color
205
+ }
206
+ fontWeight="bold"
207
+ fontSize="xs"
208
+ >
209
+ {marker.type}
210
+ </Text>
211
+ <Text fontSize="xs" fontWeight="semibold">
212
+ {marker.label}
213
+ </Text>
214
+ <Text fontSize="xs" color="gray.500">
215
+ {formatTimeSeriesTooltipTimestamp(
216
+ marker.timestamp,
217
+ )}
218
+ </Text>
219
+ </Flex>
220
+ <Text fontSize="xs" color="gray.500">
221
+ {marker.summary}
222
+ </Text>
223
+ </Box>
224
+ ))}
225
+ </Flex>
226
+ ) : (
227
+ <Text fontSize="xs" color="gray.500">
228
+ No verified events in the selected window.
229
+ </Text>
230
+ )}
231
+ </Box>
232
+
233
+ {provenance.length ? (
234
+ <Box>
235
+ <Text fontSize="xs" fontWeight="semibold" mb={1}>
236
+ Artifact provenance
237
+ </Text>
238
+ {provenance.map((marker) => (
239
+ <Text
240
+ key={`${marker.artifactId}:${marker.artifactSha256}`}
241
+ fontSize="xs"
242
+ color="gray.500"
243
+ fontFamily="mono"
244
+ title={marker.artifactSha256}
245
+ >
246
+ {marker.artifactId} ·{' '}
247
+ {marker.artifactSha256.slice(0, 12)}
248
+ </Text>
249
+ ))}
250
+ </Box>
251
+ ) : null}
252
+ </Flex>
253
+ </Popover.Body>
254
+ </Popover.Content>
255
+ </Popover.Positioner>
256
+ </Portal>
257
+ </Popover.Root>
258
+ );
259
+ };