@tradejs/app 2.0.9 → 2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/app",
3
- "version": "2.0.9",
3
+ "version": "2.0.11",
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.9",
55
- "@tradejs/core": "^2.0.9",
56
- "@tradejs/indicators": "^2.0.9",
57
- "@tradejs/infra": "^2.0.9",
58
- "@tradejs/node": "^2.0.9",
59
- "@tradejs/types": "^2.0.9",
54
+ "@tradejs/connectors": "^2.0.11",
55
+ "@tradejs/core": "^2.0.11",
56
+ "@tradejs/indicators": "^2.0.11",
57
+ "@tradejs/infra": "^2.0.11",
58
+ "@tradejs/node": "^2.0.11",
59
+ "@tradejs/types": "^2.0.11",
60
60
  "@types/bcryptjs": "2.4.6",
61
61
  "@types/lodash": "4.17.24",
62
62
  "@types/node": "24.13.3",
@@ -29,10 +29,13 @@ import { getCurrentUserName } from '#app/lib/currentUser';
29
29
  import {
30
30
  assignLegacyRuntimeTradeAccountScopes,
31
31
  buildRuntimeStrategyAnalytics,
32
+ buildRuntimeStrategyAiGateChanges,
32
33
  buildRuntimeStrategyIdentityKey,
33
34
  buildExchangeFallbackRuntimeTrades,
34
35
  isRuntimeTradeRecord,
36
+ isRuntimeStrategyLineageScope,
35
37
  resolveStrategyConfigIdentityByKey,
38
+ RuntimeStrategyLineageScope,
36
39
  RuntimeStrategiesResponse,
37
40
  selectTradesForWindow,
38
41
  toRuntimeTradeView,
@@ -52,6 +55,7 @@ const MIN_HOURS = 6;
52
55
  const MAX_HOURS = 24 * 90;
53
56
  const BYBIT_MAX_TIME_RANGE_MS = 7 * 24 * 60 * 60 * 1000 - 1_000;
54
57
  const EXCHANGE_REQUEST_TIMEOUT_MS = 15_000;
58
+ const RUNTIME_LINEAGE_BASELINE_MS = 7 * 24 * 60 * 60 * 1000;
55
59
 
56
60
  const coerceHours = (value: string | null) => {
57
61
  const parsed = Number(value ?? Number.NaN);
@@ -162,6 +166,26 @@ const loadRuntimeTrades = async (
162
166
  .sort((left, right) => left.entryTimestamp - right.entryTimestamp);
163
167
  };
164
168
 
169
+ const loadRuntimeLineageScopes = async (
170
+ userName: string,
171
+ startTime: number,
172
+ endTime: number,
173
+ ): Promise<RuntimeStrategyLineageScope[]> =>
174
+ (
175
+ await Promise.all(
176
+ getRuntimeStorageDayKeys(
177
+ Math.max(0, startTime - RUNTIME_LINEAGE_BASELINE_MS),
178
+ endTime,
179
+ ).map((dayKey) =>
180
+ getHashJsonValues<RuntimeStrategyLineageScope>(
181
+ redisKeys.runtimeLineageScopeBucket(userName, dayKey),
182
+ ),
183
+ ),
184
+ )
185
+ )
186
+ .flat()
187
+ .filter(isRuntimeStrategyLineageScope);
188
+
165
189
  const buildExchangeTimeRanges = (startTime: number, endTime: number) => {
166
190
  const ranges: Array<{ startTime: number; endTime: number }> = [];
167
191
  let cursor = startTime;
@@ -380,6 +404,7 @@ export const GET = async (request: NextRequest) => {
380
404
  openPositionsSnapshot,
381
405
  runtimeDeployments,
382
406
  tradingAccounts,
407
+ runtimeLineageScopes,
383
408
  ] = await Promise.all([
384
409
  loadRuntimeStrategyConfigs(userName),
385
410
  loadConfiguredStrategyNames(),
@@ -400,6 +425,7 @@ export const GET = async (request: NextRequest) => {
400
425
  loadOpenPositions(connector, exchangeErrors),
401
426
  listRuntimeDeployments(userName),
402
427
  listTradingAccounts(userName),
428
+ loadRuntimeLineageScopes(userName, startTime, endTime),
403
429
  ]);
404
430
  const relevantTrades = selectTradesForWindow(
405
431
  runtimeTrades,
@@ -607,6 +633,13 @@ export const GET = async (request: NextRequest) => {
607
633
  stat: analytics.stat,
608
634
  summary: analytics.summary,
609
635
  orderLog: analytics.orderLog,
636
+ aiGateChanges: buildRuntimeStrategyAiGateChanges({
637
+ scopes: runtimeLineageScopes,
638
+ strategyName,
639
+ configId: identity.configId,
640
+ startTime,
641
+ endTime,
642
+ }),
610
643
  recentTrades: strategyTrades
611
644
  .slice(0, 8)
612
645
  .map((trade) => toRuntimeTradeView(trade, endTime)),
@@ -2175,7 +2175,11 @@ export const RuntimeStrategyCard = ({
2175
2175
  </Portal>
2176
2176
  </Drawer.Root>
2177
2177
 
2178
- <RuntimeStrategyChart orderLog={strategy.orderLog} stat={strategy.stat} />
2178
+ <RuntimeStrategyChart
2179
+ orderLog={strategy.orderLog}
2180
+ stat={strategy.stat}
2181
+ aiGateChanges={strategy.aiGateChanges}
2182
+ />
2179
2183
 
2180
2184
  <SimpleGrid columns={{ base: 4, md: 8 }} p={4}>
2181
2185
  <StatItem stat={strategy.stat} id="netProfit" title="P&L" />
@@ -16,22 +16,25 @@ import {
16
16
  import { format } from 'date-fns';
17
17
  import { getFormatted } from '@tradejs/core/backtest';
18
18
  import type { SimpleOrderLogData, TestStat } from '@tradejs/types';
19
+ import type { RuntimeStrategyAiGateChange } from '#app/lib/runtimeStrategies';
19
20
 
20
21
  interface RuntimeStrategyChartProps {
21
22
  orderLog: SimpleOrderLogData;
22
23
  stat: TestStat;
24
+ aiGateChanges: RuntimeStrategyAiGateChange[];
23
25
  height?: string | number;
24
26
  }
25
27
 
26
28
  export const RuntimeStrategyChart = ({
27
29
  orderLog,
28
30
  stat,
31
+ aiGateChanges,
29
32
  height = '350px',
30
33
  }: RuntimeStrategyChartProps) => {
31
34
  const chartData = useMemo(
32
35
  () => ({
33
36
  data: orderLog.map(([timestamp, amount]) => ({
34
- timestamp: format(timestamp, 'dd.MM'),
37
+ timestamp,
35
38
  equity: amount,
36
39
  })),
37
40
  series: [
@@ -47,6 +50,7 @@ export const RuntimeStrategyChart = ({
47
50
  const chart = useChart(chartData as any);
48
51
  const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
49
52
  const { formatted: minAmount } = getFormatted(stat, 'minAmount');
53
+ const gateChangeColor = chart.color('purple.400');
50
54
 
51
55
  if (!orderLog.length) {
52
56
  return (
@@ -96,12 +100,40 @@ export const RuntimeStrategyChart = ({
96
100
  position: 'bottom',
97
101
  }}
98
102
  />
99
- <XAxis dataKey="timestamp" />
103
+ {aiGateChanges.map((change) => (
104
+ <ReferenceLine
105
+ key={`${change.timestamp}:${change.fingerprint}`}
106
+ x={change.timestamp}
107
+ stroke={gateChangeColor}
108
+ strokeDasharray="3 5"
109
+ strokeWidth={1.5}
110
+ label={{
111
+ value: `AI-gate ${change.fingerprint.slice(0, 7)}`,
112
+ fill: gateChangeColor,
113
+ fontSize: 10,
114
+ offset: 6,
115
+ position: 'insideTopRight',
116
+ }}
117
+ />
118
+ ))}
119
+ <XAxis
120
+ dataKey="timestamp"
121
+ type="number"
122
+ scale="time"
123
+ domain={['dataMin', 'dataMax']}
124
+ tickFormatter={(timestamp) => format(timestamp, 'dd.MM')}
125
+ />
100
126
  <YAxis tickCount={10} domain={[stat.minAmount - 10, 'auto']} />
101
127
  <Tooltip
102
128
  animationDuration={100}
103
129
  cursor={false}
104
- content={<Chart.Tooltip />}
130
+ content={
131
+ <Chart.Tooltip
132
+ labelFormatter={(timestamp) =>
133
+ format(Number(timestamp), 'dd.MM.yyyy HH:mm')
134
+ }
135
+ />
136
+ }
105
137
  />
106
138
  {chart.series.map((item) => (
107
139
  <Line
@@ -111,6 +143,7 @@ export const RuntimeStrategyChart = ({
111
143
  stroke={chart.color(item.color)}
112
144
  strokeWidth={2}
113
145
  dot={false}
146
+ activeDot={{ r: 5, strokeWidth: 2 }}
114
147
  />
115
148
  ))}
116
149
  </LineChart>
@@ -9,6 +9,7 @@ import type {
9
9
  ExchangeEntryRecord,
10
10
  PositionPnlSnapshot,
11
11
  RuntimeTradeRecord,
12
+ RuntimeLineage,
12
13
  SimpleOrderLogData,
13
14
  StrategyConfig,
14
15
  TestStat,
@@ -72,6 +73,21 @@ export interface RuntimeStrategyTradeView {
72
73
  lastSyncedAt: number | null;
73
74
  }
74
75
 
76
+ export interface RuntimeStrategyLineageScope {
77
+ strategy: string;
78
+ symbol: string;
79
+ runtimeConfigId?: string;
80
+ lineage: RuntimeLineage;
81
+ firstTimestamp: number;
82
+ lastTimestamp: number;
83
+ }
84
+
85
+ export interface RuntimeStrategyAiGateChange {
86
+ timestamp: number;
87
+ previousFingerprint: string;
88
+ fingerprint: string;
89
+ }
90
+
75
91
  export interface RuntimeStrategyView {
76
92
  runtimeKey: string;
77
93
  strategyName: string;
@@ -89,6 +105,7 @@ export interface RuntimeStrategyView {
89
105
  stat: TestStat;
90
106
  summary: RuntimeStrategyTradeSummary;
91
107
  orderLog: SimpleOrderLogData;
108
+ aiGateChanges: RuntimeStrategyAiGateChange[];
92
109
  recentTrades: RuntimeStrategyTradeView[];
93
110
  orders: RuntimeStrategyTradeView[];
94
111
  }
@@ -164,6 +181,104 @@ export interface RuntimeStrategiesResponse {
164
181
  strategies: RuntimeStrategyView[];
165
182
  }
166
183
 
184
+ export const isRuntimeStrategyLineageScope = (
185
+ value: unknown,
186
+ ): value is RuntimeStrategyLineageScope => {
187
+ if (!value || typeof value !== 'object') {
188
+ return false;
189
+ }
190
+
191
+ const record = value as Record<string, unknown>;
192
+ const lineage = record.lineage as Record<string, unknown> | undefined;
193
+
194
+ return (
195
+ typeof record.strategy === 'string' &&
196
+ typeof record.symbol === 'string' &&
197
+ typeof record.firstTimestamp === 'number' &&
198
+ Number.isFinite(record.firstTimestamp) &&
199
+ typeof record.lastTimestamp === 'number' &&
200
+ Number.isFinite(record.lastTimestamp) &&
201
+ lineage != null &&
202
+ typeof lineage.gateFingerprint === 'string' &&
203
+ lineage.gateFingerprint.trim().length > 0
204
+ );
205
+ };
206
+
207
+ export const buildRuntimeStrategyAiGateChanges = ({
208
+ scopes,
209
+ strategyName,
210
+ configId,
211
+ startTime,
212
+ endTime,
213
+ }: {
214
+ scopes: RuntimeStrategyLineageScope[];
215
+ strategyName: string;
216
+ configId?: string;
217
+ startTime: number;
218
+ endTime: number;
219
+ }): RuntimeStrategyAiGateChange[] => {
220
+ const normalizedConfigId = configId ?? 'config';
221
+ const observationsByTimestamp = new Map<
222
+ number,
223
+ { fingerprint: string; lastTimestamp: number }
224
+ >();
225
+
226
+ for (const scope of scopes) {
227
+ if (
228
+ scope.strategy !== strategyName ||
229
+ (scope.runtimeConfigId ?? 'config') !== normalizedConfigId ||
230
+ scope.firstTimestamp > endTime
231
+ ) {
232
+ continue;
233
+ }
234
+
235
+ const fingerprint = scope.lineage.gateFingerprint.trim();
236
+ const existing = observationsByTimestamp.get(scope.firstTimestamp);
237
+
238
+ // Multiple symbols are evaluated for the same strategy timestamp. If a
239
+ // deploy happens during that cycle, prefer the lineage that kept running
240
+ // afterwards instead of making the marker order depend on the symbol name.
241
+ if (
242
+ !existing ||
243
+ scope.lastTimestamp > existing.lastTimestamp ||
244
+ (scope.lastTimestamp === existing.lastTimestamp &&
245
+ fingerprint > existing.fingerprint)
246
+ ) {
247
+ observationsByTimestamp.set(scope.firstTimestamp, {
248
+ fingerprint,
249
+ lastTimestamp: scope.lastTimestamp,
250
+ });
251
+ }
252
+ }
253
+
254
+ const observations = [...observationsByTimestamp.entries()].sort(
255
+ ([leftTimestamp], [rightTimestamp]) => leftTimestamp - rightTimestamp,
256
+ );
257
+ const changes: RuntimeStrategyAiGateChange[] = [];
258
+ let currentFingerprint: string | null = null;
259
+
260
+ for (const [timestamp, observation] of observations) {
261
+ if (currentFingerprint == null) {
262
+ currentFingerprint = observation.fingerprint;
263
+ continue;
264
+ }
265
+ if (observation.fingerprint === currentFingerprint) {
266
+ continue;
267
+ }
268
+
269
+ if (timestamp >= startTime) {
270
+ changes.push({
271
+ timestamp,
272
+ previousFingerprint: currentFingerprint,
273
+ fingerprint: observation.fingerprint,
274
+ });
275
+ }
276
+ currentFingerprint = observation.fingerprint;
277
+ }
278
+
279
+ return changes;
280
+ };
281
+
167
282
  const roundValue = (value: number, digits = 2) => {
168
283
  if (!Number.isFinite(value)) {
169
284
  return 0;