@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.13",
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.13",
55
- "@tradejs/core": "^2.0.13",
56
- "@tradejs/indicators": "^2.0.13",
57
- "@tradejs/infra": "^2.0.13",
58
- "@tradejs/node": "^2.0.13",
59
- "@tradejs/types": "^2.0.13",
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 RUNTIME_LINEAGE_BASELINE_MS = 7 * 24 * 60 * 60 * 1000;
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, startTime - RUNTIME_LINEAGE_BASELINE_MS),
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, startTime, endTime),
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 { RuntimeStrategyAiGateChange } from '#app/lib/runtimeStrategies';
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} pr={2}>
76
- <ResponsiveContainer width="100%" height="100%">
77
- <Chart.Root maxH="md" chart={chart}>
78
- <LineChart data={chart.data}>
79
- <CartesianGrid stroke={chart.color('border')} vertical={false} />
80
- <ReferenceLine
81
- stroke={chart.color('gray.600')}
82
- strokeDasharray="5 5"
83
- y={stat.maxAmount}
84
- label={{
85
- value: `Max: ${maxAmount}`,
86
- offset: 10,
87
- fill: chart.color('gray.600'),
88
- position: 'top',
89
- }}
90
- />
91
- <ReferenceLine
92
- stroke={chart.color('gray.600')}
93
- strokeDasharray="8 8"
94
- y={100}
95
- />
96
- <ReferenceLine
97
- stroke={chart.color('gray.600')}
98
- strokeDasharray="5 5"
99
- y={stat.minAmount}
100
- label={{
101
- value: `Min: ${minAmount}`,
102
- offset: 10,
103
- fill: chart.color('gray.600'),
104
- position: 'bottom',
105
- }}
106
- />
107
- {aiGateChanges.map((change) => (
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
- key={`${change.timestamp}:${change.fingerprint}`}
110
- x={change.timestamp}
111
- stroke={gateChangeColor}
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: `AI-gate ${change.fingerprint.slice(0, 7)}`,
116
- fill: gateChangeColor,
117
- fontSize: 10,
118
- offset: 6,
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
- <TimeSeriesXAxis
124
- startTimestamp={startTimestamp}
125
- endTimestamp={endTimestamp}
126
- />
127
- <YAxis tickCount={10} domain={[stat.minAmount - 10, 'auto']} />
128
- <Tooltip
129
- animationDuration={100}
130
- cursor={false}
131
- content={
132
- <Chart.Tooltip
133
- labelFormatter={formatTimeSeriesTooltipTimestamp}
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
- {chart.series.map((item) => (
138
- <Line
139
- key={item.name as string}
140
- isAnimationActive={false}
141
- dataKey={chart.key(item.name) as string}
142
- stroke={chart.color(item.color)}
143
- strokeWidth={2}
144
- dot={false}
145
- activeDot={{ r: 5, strokeWidth: 2 }}
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
- </LineChart>
149
- </Chart.Root>
150
- </ResponsiveContainer>
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 => {