@tradejs/app 2.0.13 → 2.0.15
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"@emotion/react": "^11.14.0",
|
|
52
52
|
"@langchain/core": "^1.2.3",
|
|
53
53
|
"@langchain/openai": "^1.5.5",
|
|
54
|
-
"@tradejs/connectors": "^2.0.
|
|
55
|
-
"@tradejs/core": "^2.0.
|
|
56
|
-
"@tradejs/indicators": "^2.0.
|
|
57
|
-
"@tradejs/infra": "^2.0.
|
|
58
|
-
"@tradejs/node": "^2.0.
|
|
59
|
-
"@tradejs/types": "^2.0.
|
|
54
|
+
"@tradejs/connectors": "^2.0.15",
|
|
55
|
+
"@tradejs/core": "^2.0.15",
|
|
56
|
+
"@tradejs/indicators": "^2.0.15",
|
|
57
|
+
"@tradejs/infra": "^2.0.15",
|
|
58
|
+
"@tradejs/node": "^2.0.15",
|
|
59
|
+
"@tradejs/types": "^2.0.15",
|
|
60
60
|
"@types/bcryptjs": "2.4.6",
|
|
61
61
|
"@types/lodash": "4.17.24",
|
|
62
62
|
"@types/node": "24.13.3",
|
|
@@ -30,8 +30,10 @@ import {
|
|
|
30
30
|
assignLegacyRuntimeTradeAccountScopes,
|
|
31
31
|
buildRuntimeStrategyAnalytics,
|
|
32
32
|
buildRuntimeStrategyAiGateChanges,
|
|
33
|
+
buildRuntimeStrategyMaxLossValueTimeline,
|
|
33
34
|
buildRuntimeStrategyIdentityKey,
|
|
34
35
|
buildExchangeFallbackRuntimeTrades,
|
|
36
|
+
getRuntimeStrategyAiGateObservedFrom,
|
|
35
37
|
isRuntimeTradeRecord,
|
|
36
38
|
isRuntimeStrategyLineageScope,
|
|
37
39
|
resolveStrategyConfigIdentityByKey,
|
|
@@ -55,7 +57,7 @@ const MIN_HOURS = 6;
|
|
|
55
57
|
const MAX_HOURS = 24 * 90;
|
|
56
58
|
const BYBIT_MAX_TIME_RANGE_MS = 7 * 24 * 60 * 60 * 1000 - 1_000;
|
|
57
59
|
const EXCHANGE_REQUEST_TIMEOUT_MS = 15_000;
|
|
58
|
-
const
|
|
60
|
+
const RUNTIME_LINEAGE_HISTORY_MS = 30 * 24 * 60 * 60 * 1000;
|
|
59
61
|
|
|
60
62
|
const coerceHours = (value: string | null) => {
|
|
61
63
|
const parsed = Number(value ?? Number.NaN);
|
|
@@ -168,13 +170,12 @@ const loadRuntimeTrades = async (
|
|
|
168
170
|
|
|
169
171
|
const loadRuntimeLineageScopes = async (
|
|
170
172
|
userName: string,
|
|
171
|
-
startTime: number,
|
|
172
173
|
endTime: number,
|
|
173
174
|
): Promise<RuntimeStrategyLineageScope[]> =>
|
|
174
175
|
(
|
|
175
176
|
await Promise.all(
|
|
176
177
|
getRuntimeStorageDayKeys(
|
|
177
|
-
Math.max(0,
|
|
178
|
+
Math.max(0, endTime - RUNTIME_LINEAGE_HISTORY_MS),
|
|
178
179
|
endTime,
|
|
179
180
|
).map((dayKey) =>
|
|
180
181
|
getHashJsonValues<RuntimeStrategyLineageScope>(
|
|
@@ -425,7 +426,7 @@ export const GET = async (request: NextRequest) => {
|
|
|
425
426
|
loadOpenPositions(connector, exchangeErrors),
|
|
426
427
|
listRuntimeDeployments(userName),
|
|
427
428
|
listTradingAccounts(userName),
|
|
428
|
-
loadRuntimeLineageScopes(userName,
|
|
429
|
+
loadRuntimeLineageScopes(userName, endTime),
|
|
429
430
|
]);
|
|
430
431
|
const relevantTrades = selectTradesForWindow(
|
|
431
432
|
runtimeTrades,
|
|
@@ -610,6 +611,13 @@ export const GET = async (request: NextRequest) => {
|
|
|
610
611
|
startTime,
|
|
611
612
|
endTime,
|
|
612
613
|
});
|
|
614
|
+
const maxLossValueTimeline = buildRuntimeStrategyMaxLossValueTimeline({
|
|
615
|
+
scopes: runtimeLineageScopes,
|
|
616
|
+
strategyName,
|
|
617
|
+
configId: identity.configId,
|
|
618
|
+
startTime,
|
|
619
|
+
endTime,
|
|
620
|
+
});
|
|
613
621
|
const effectiveStrategyConfig = identity.config ?? null;
|
|
614
622
|
|
|
615
623
|
return {
|
|
@@ -633,6 +641,12 @@ export const GET = async (request: NextRequest) => {
|
|
|
633
641
|
stat: analytics.stat,
|
|
634
642
|
summary: analytics.summary,
|
|
635
643
|
orderLog: analytics.orderLog,
|
|
644
|
+
aiGateObservedFrom: getRuntimeStrategyAiGateObservedFrom({
|
|
645
|
+
scopes: runtimeLineageScopes,
|
|
646
|
+
strategyName,
|
|
647
|
+
configId: identity.configId,
|
|
648
|
+
endTime,
|
|
649
|
+
}),
|
|
636
650
|
aiGateChanges: buildRuntimeStrategyAiGateChanges({
|
|
637
651
|
scopes: runtimeLineageScopes,
|
|
638
652
|
strategyName,
|
|
@@ -640,6 +654,7 @@ export const GET = async (request: NextRequest) => {
|
|
|
640
654
|
startTime,
|
|
641
655
|
endTime,
|
|
642
656
|
}),
|
|
657
|
+
maxLossValueTimeline,
|
|
643
658
|
recentTrades: strategyTrades
|
|
644
659
|
.slice(0, 8)
|
|
645
660
|
.map((trade) => toRuntimeTradeView(trade, endTime)),
|
|
@@ -2182,7 +2182,9 @@ export const RuntimeStrategyCard = ({
|
|
|
2182
2182
|
<RuntimeStrategyChart
|
|
2183
2183
|
orderLog={strategy.orderLog}
|
|
2184
2184
|
stat={strategy.stat}
|
|
2185
|
+
aiGateObservedFrom={strategy.aiGateObservedFrom}
|
|
2185
2186
|
aiGateChanges={strategy.aiGateChanges}
|
|
2187
|
+
maxLossValueTimeline={strategy.maxLossValueTimeline}
|
|
2186
2188
|
startTimestamp={startTimestamp}
|
|
2187
2189
|
endTimestamp={endTimestamp}
|
|
2188
2190
|
/>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
import { Box, Text } from '@chakra-ui/react';
|
|
4
|
+
import { Box, Flex, Text } from '@chakra-ui/react';
|
|
5
5
|
import { Chart, useChart } from '@chakra-ui/charts';
|
|
6
6
|
import {
|
|
7
7
|
CartesianGrid,
|
|
@@ -14,14 +14,19 @@ import {
|
|
|
14
14
|
} from 'recharts';
|
|
15
15
|
import { getFormatted } from '@tradejs/core/backtest';
|
|
16
16
|
import type { SimpleOrderLogData, TestStat } from '@tradejs/types';
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
RuntimeStrategyAiGateChange,
|
|
19
|
+
RuntimeStrategyMaxLossValueTimeline,
|
|
20
|
+
} from '#app/lib/runtimeStrategies';
|
|
18
21
|
import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
|
|
19
22
|
import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
|
|
20
23
|
|
|
21
24
|
interface RuntimeStrategyChartProps {
|
|
22
25
|
orderLog: SimpleOrderLogData;
|
|
23
26
|
stat: TestStat;
|
|
27
|
+
aiGateObservedFrom: number | null;
|
|
24
28
|
aiGateChanges: RuntimeStrategyAiGateChange[];
|
|
29
|
+
maxLossValueTimeline: RuntimeStrategyMaxLossValueTimeline;
|
|
25
30
|
startTimestamp: number;
|
|
26
31
|
endTimestamp: number;
|
|
27
32
|
height?: string | number;
|
|
@@ -30,7 +35,9 @@ interface RuntimeStrategyChartProps {
|
|
|
30
35
|
export const RuntimeStrategyChart = ({
|
|
31
36
|
orderLog,
|
|
32
37
|
stat,
|
|
38
|
+
aiGateObservedFrom,
|
|
33
39
|
aiGateChanges,
|
|
40
|
+
maxLossValueTimeline,
|
|
34
41
|
startTimestamp,
|
|
35
42
|
endTimestamp,
|
|
36
43
|
height = '350px',
|
|
@@ -55,6 +62,7 @@ export const RuntimeStrategyChart = ({
|
|
|
55
62
|
const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
|
|
56
63
|
const { formatted: minAmount } = getFormatted(stat, 'minAmount');
|
|
57
64
|
const gateChangeColor = chart.color('purple.400');
|
|
65
|
+
const maxLossValueChangeColor = chart.color('orange.400');
|
|
58
66
|
|
|
59
67
|
if (!orderLog.length) {
|
|
60
68
|
return (
|
|
@@ -72,82 +80,175 @@ export const RuntimeStrategyChart = ({
|
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
return (
|
|
75
|
-
<Box w="100%" minW="600px" h={height}
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
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}
|
|
153
|
+
<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} />
|
|
108
158
|
<ReferenceLine
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
strokeDasharray="3 5"
|
|
113
|
-
strokeWidth={1.5}
|
|
159
|
+
stroke={chart.color('gray.600')}
|
|
160
|
+
strokeDasharray="5 5"
|
|
161
|
+
y={stat.maxAmount}
|
|
114
162
|
label={{
|
|
115
|
-
value: `
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
position: 'insideTopRight',
|
|
163
|
+
value: `Max: ${maxAmount}`,
|
|
164
|
+
offset: 10,
|
|
165
|
+
fill: chart.color('gray.600'),
|
|
166
|
+
position: 'top',
|
|
120
167
|
}}
|
|
121
168
|
/>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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) => (
|
|
186
|
+
<ReferenceLine
|
|
187
|
+
key={`${change.timestamp}:${change.fingerprint}`}
|
|
188
|
+
x={change.timestamp}
|
|
189
|
+
stroke={gateChangeColor}
|
|
190
|
+
strokeDasharray="3 5"
|
|
191
|
+
strokeWidth={1.5}
|
|
192
|
+
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',
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
))}
|
|
203
|
+
{maxLossValueTimeline.changes.map((change, index) => (
|
|
204
|
+
<ReferenceLine
|
|
205
|
+
key={`${change.timestamp}:${change.value}:max-loss`}
|
|
206
|
+
x={change.timestamp}
|
|
207
|
+
stroke={maxLossValueChangeColor}
|
|
208
|
+
strokeDasharray="7 4"
|
|
209
|
+
strokeWidth={1.5}
|
|
210
|
+
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',
|
|
220
|
+
}}
|
|
134
221
|
/>
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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}
|
|
234
|
+
/>
|
|
235
|
+
}
|
|
146
236
|
/>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 }}
|
|
246
|
+
/>
|
|
247
|
+
))}
|
|
248
|
+
</LineChart>
|
|
249
|
+
</Chart.Root>
|
|
250
|
+
</ResponsiveContainer>
|
|
251
|
+
</Box>
|
|
151
252
|
</Box>
|
|
152
253
|
);
|
|
153
254
|
};
|
|
@@ -77,7 +77,9 @@ export interface RuntimeStrategyLineageScope {
|
|
|
77
77
|
strategy: string;
|
|
78
78
|
symbol: string;
|
|
79
79
|
runtimeConfigId?: string;
|
|
80
|
-
lineage: RuntimeLineage
|
|
80
|
+
lineage: RuntimeLineage & {
|
|
81
|
+
maxLossValue?: number | null;
|
|
82
|
+
};
|
|
81
83
|
firstTimestamp: number;
|
|
82
84
|
lastTimestamp: number;
|
|
83
85
|
}
|
|
@@ -88,6 +90,18 @@ export interface RuntimeStrategyAiGateChange {
|
|
|
88
90
|
fingerprint: string;
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
export interface RuntimeStrategyMaxLossValueChange {
|
|
94
|
+
timestamp: number;
|
|
95
|
+
previousValue: number;
|
|
96
|
+
value: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface RuntimeStrategyMaxLossValueTimeline {
|
|
100
|
+
observedFrom: number | null;
|
|
101
|
+
initialValue: number | null;
|
|
102
|
+
changes: RuntimeStrategyMaxLossValueChange[];
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
export interface RuntimeStrategyView {
|
|
92
106
|
runtimeKey: string;
|
|
93
107
|
strategyName: string;
|
|
@@ -105,7 +119,9 @@ export interface RuntimeStrategyView {
|
|
|
105
119
|
stat: TestStat;
|
|
106
120
|
summary: RuntimeStrategyTradeSummary;
|
|
107
121
|
orderLog: SimpleOrderLogData;
|
|
122
|
+
aiGateObservedFrom: number | null;
|
|
108
123
|
aiGateChanges: RuntimeStrategyAiGateChange[];
|
|
124
|
+
maxLossValueTimeline: RuntimeStrategyMaxLossValueTimeline;
|
|
109
125
|
recentTrades: RuntimeStrategyTradeView[];
|
|
110
126
|
orders: RuntimeStrategyTradeView[];
|
|
111
127
|
}
|
|
@@ -181,6 +197,114 @@ export interface RuntimeStrategiesResponse {
|
|
|
181
197
|
strategies: RuntimeStrategyView[];
|
|
182
198
|
}
|
|
183
199
|
|
|
200
|
+
export const getRuntimeStrategyAiGateObservedFrom = ({
|
|
201
|
+
scopes,
|
|
202
|
+
strategyName,
|
|
203
|
+
configId,
|
|
204
|
+
endTime,
|
|
205
|
+
}: {
|
|
206
|
+
scopes: RuntimeStrategyLineageScope[];
|
|
207
|
+
strategyName: string;
|
|
208
|
+
configId?: string;
|
|
209
|
+
endTime: number;
|
|
210
|
+
}) => {
|
|
211
|
+
const normalizedConfigId = configId ?? 'config';
|
|
212
|
+
let observedFrom: number | null = null;
|
|
213
|
+
|
|
214
|
+
for (const scope of scopes) {
|
|
215
|
+
if (
|
|
216
|
+
scope.strategy !== strategyName ||
|
|
217
|
+
(scope.runtimeConfigId ?? 'config') !== normalizedConfigId ||
|
|
218
|
+
scope.firstTimestamp > endTime
|
|
219
|
+
) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
observedFrom =
|
|
224
|
+
observedFrom == null
|
|
225
|
+
? scope.firstTimestamp
|
|
226
|
+
: Math.min(observedFrom, scope.firstTimestamp);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return observedFrom;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const buildRuntimeStrategyMaxLossValueTimeline = ({
|
|
233
|
+
scopes,
|
|
234
|
+
strategyName,
|
|
235
|
+
configId,
|
|
236
|
+
startTime,
|
|
237
|
+
endTime,
|
|
238
|
+
}: {
|
|
239
|
+
scopes: RuntimeStrategyLineageScope[];
|
|
240
|
+
strategyName: string;
|
|
241
|
+
configId?: string;
|
|
242
|
+
startTime: number;
|
|
243
|
+
endTime: number;
|
|
244
|
+
}): RuntimeStrategyMaxLossValueTimeline => {
|
|
245
|
+
const normalizedConfigId = configId ?? 'config';
|
|
246
|
+
const observationsByTimestamp = new Map<
|
|
247
|
+
number,
|
|
248
|
+
{ value: number; lastTimestamp: number }
|
|
249
|
+
>();
|
|
250
|
+
|
|
251
|
+
for (const scope of scopes) {
|
|
252
|
+
const value = scope.lineage.maxLossValue;
|
|
253
|
+
if (
|
|
254
|
+
scope.strategy !== strategyName ||
|
|
255
|
+
(scope.runtimeConfigId ?? 'config') !== normalizedConfigId ||
|
|
256
|
+
scope.firstTimestamp > endTime ||
|
|
257
|
+
typeof value !== 'number' ||
|
|
258
|
+
!Number.isFinite(value)
|
|
259
|
+
) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const existing = observationsByTimestamp.get(scope.firstTimestamp);
|
|
264
|
+
if (
|
|
265
|
+
!existing ||
|
|
266
|
+
scope.lastTimestamp > existing.lastTimestamp ||
|
|
267
|
+
(scope.lastTimestamp === existing.lastTimestamp && value > existing.value)
|
|
268
|
+
) {
|
|
269
|
+
observationsByTimestamp.set(scope.firstTimestamp, {
|
|
270
|
+
value,
|
|
271
|
+
lastTimestamp: scope.lastTimestamp,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const observations = [...observationsByTimestamp.entries()].sort(
|
|
277
|
+
([leftTimestamp], [rightTimestamp]) => leftTimestamp - rightTimestamp,
|
|
278
|
+
);
|
|
279
|
+
const changes: RuntimeStrategyMaxLossValueChange[] = [];
|
|
280
|
+
let observedFrom: number | null = null;
|
|
281
|
+
let initialValue: number | null = null;
|
|
282
|
+
let currentValue: number | null = null;
|
|
283
|
+
|
|
284
|
+
for (const [timestamp, observation] of observations) {
|
|
285
|
+
if (currentValue == null) {
|
|
286
|
+
observedFrom = timestamp;
|
|
287
|
+
initialValue = observation.value;
|
|
288
|
+
currentValue = observation.value;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (observation.value === currentValue) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (timestamp >= startTime) {
|
|
296
|
+
changes.push({
|
|
297
|
+
timestamp,
|
|
298
|
+
previousValue: currentValue,
|
|
299
|
+
value: observation.value,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
currentValue = observation.value;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { observedFrom, initialValue, changes };
|
|
306
|
+
};
|
|
307
|
+
|
|
184
308
|
export const isRuntimeStrategyLineageScope = (
|
|
185
309
|
value: unknown,
|
|
186
310
|
): value is RuntimeStrategyLineageScope => {
|