@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
@@ -5,27 +5,40 @@ import {
5
5
  parseStrategyOrderLinkKey,
6
6
  } from '@tradejs/core/trade';
7
7
  import type {
8
- ClosedPnlRecord,
9
8
  ExchangeEntryRecord,
10
9
  PositionPnlSnapshot,
11
10
  RuntimeTradeRecord,
12
- RuntimeLineage,
13
11
  SimpleOrderLogData,
14
12
  StrategyConfig,
13
+ StrategyEvidenceTimeline,
15
14
  TestStat,
16
15
  MarketUniverse,
17
16
  Interval,
18
17
  } from '@tradejs/types';
18
+ import {
19
+ takeExactClosedPnlMatch,
20
+ type ClosedPnlRecordWithOrderLinkId,
21
+ } from './runtimeTradeReconciliation';
22
+ export {
23
+ assignLegacyRuntimeTradeAccountScopes,
24
+ buildRuntimeStrategyAiGateChanges,
25
+ buildRuntimeStrategyIdentityKey,
26
+ buildRuntimeStrategyMaxLossValueTimeline,
27
+ getRuntimeStrategyAiGateObservedFrom,
28
+ isRuntimeStrategyLineageScope,
29
+ } from './runtimeStrategyLineage';
30
+ export type {
31
+ RuntimeStrategyAccountScope,
32
+ RuntimeStrategyAiGateChange,
33
+ RuntimeStrategyLineageScope,
34
+ RuntimeStrategyMaxLossValueChange,
35
+ RuntimeStrategyMaxLossValueTimeline,
36
+ } from './runtimeStrategyLineage';
37
+ export { takeClosedPnlMatch } from './runtimeTradeReconciliation';
19
38
 
20
39
  const MS_IN_DAY = 24 * 60 * 60 * 1000;
21
40
  const AVG_DAYS_IN_MONTH = 30.4375;
22
41
 
23
- type ClosedPnlRecordWithOrderLinkId = ClosedPnlRecord & {
24
- direction?: RuntimeTradeRecord['direction'];
25
- entryTimestamp?: number;
26
- orderLinkId?: string;
27
- };
28
-
29
42
  type RuntimeTradeWithResolvedPnl = RuntimeTradeRecord & {
30
43
  resolvedPnl: number;
31
44
  resolvedTimestamp: number;
@@ -73,35 +86,6 @@ export interface RuntimeStrategyTradeView {
73
86
  lastSyncedAt: number | null;
74
87
  }
75
88
 
76
- export interface RuntimeStrategyLineageScope {
77
- strategy: string;
78
- symbol: string;
79
- runtimeConfigId?: string;
80
- lineage: RuntimeLineage & {
81
- maxLossValue?: number | null;
82
- };
83
- firstTimestamp: number;
84
- lastTimestamp: number;
85
- }
86
-
87
- export interface RuntimeStrategyAiGateChange {
88
- timestamp: number;
89
- previousFingerprint: string;
90
- fingerprint: string;
91
- }
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
-
105
89
  export interface RuntimeStrategyView {
106
90
  runtimeKey: string;
107
91
  strategyName: string;
@@ -119,72 +103,11 @@ export interface RuntimeStrategyView {
119
103
  stat: TestStat;
120
104
  summary: RuntimeStrategyTradeSummary;
121
105
  orderLog: SimpleOrderLogData;
122
- aiGateObservedFrom: number | null;
123
- aiGateChanges: RuntimeStrategyAiGateChange[];
124
- maxLossValueTimeline: RuntimeStrategyMaxLossValueTimeline;
106
+ evidenceTimeline: StrategyEvidenceTimeline;
125
107
  recentTrades: RuntimeStrategyTradeView[];
126
108
  orders: RuntimeStrategyTradeView[];
127
109
  }
128
110
 
129
- export const buildRuntimeStrategyIdentityKey = ({
130
- strategyName,
131
- configId,
132
- universe,
133
- accountId,
134
- deploymentId,
135
- policyProfileId,
136
- }: {
137
- strategyName: string;
138
- configId?: string;
139
- universe?: MarketUniverse;
140
- accountId?: string;
141
- deploymentId?: string;
142
- policyProfileId?: string;
143
- }) =>
144
- [
145
- strategyName,
146
- configId ?? 'config',
147
- universe ?? 'crypto',
148
- accountId ?? 'default',
149
- deploymentId ?? 'default',
150
- policyProfileId ?? 'default',
151
- ].join(':');
152
-
153
- export interface RuntimeStrategyAccountScope {
154
- strategyName: string;
155
- configId: string;
156
- universe: MarketUniverse;
157
- accountId?: string;
158
- }
159
-
160
- export const assignLegacyRuntimeTradeAccountScopes = (
161
- trades: RuntimeTradeRecord[],
162
- scopes: RuntimeStrategyAccountScope[],
163
- ): RuntimeTradeRecord[] =>
164
- trades.map((trade) => {
165
- if (trade.accountId || trade.deploymentId) {
166
- return trade;
167
- }
168
-
169
- const matchingAccountIds = new Set(
170
- scopes
171
- .filter(
172
- (scope) =>
173
- scope.strategyName === trade.strategy &&
174
- scope.configId === (trade.runtimeConfigId ?? 'config') &&
175
- scope.universe === (trade.universe ?? 'crypto'),
176
- )
177
- .map((scope) => scope.accountId)
178
- .filter((accountId): accountId is string => Boolean(accountId)),
179
- );
180
-
181
- if (matchingAccountIds.size !== 1) {
182
- return trade;
183
- }
184
-
185
- return { ...trade, accountId: [...matchingAccountIds][0] };
186
- });
187
-
188
111
  export interface RuntimeStrategiesResponse {
189
112
  provider: string;
190
113
  hours: number;
@@ -197,212 +120,6 @@ export interface RuntimeStrategiesResponse {
197
120
  strategies: RuntimeStrategyView[];
198
121
  }
199
122
 
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
-
308
- export const isRuntimeStrategyLineageScope = (
309
- value: unknown,
310
- ): value is RuntimeStrategyLineageScope => {
311
- if (!value || typeof value !== 'object') {
312
- return false;
313
- }
314
-
315
- const record = value as Record<string, unknown>;
316
- const lineage = record.lineage as Record<string, unknown> | undefined;
317
-
318
- return (
319
- typeof record.strategy === 'string' &&
320
- typeof record.symbol === 'string' &&
321
- typeof record.firstTimestamp === 'number' &&
322
- Number.isFinite(record.firstTimestamp) &&
323
- typeof record.lastTimestamp === 'number' &&
324
- Number.isFinite(record.lastTimestamp) &&
325
- lineage != null &&
326
- typeof lineage.gateFingerprint === 'string' &&
327
- lineage.gateFingerprint.trim().length > 0
328
- );
329
- };
330
-
331
- export const buildRuntimeStrategyAiGateChanges = ({
332
- scopes,
333
- strategyName,
334
- configId,
335
- startTime,
336
- endTime,
337
- }: {
338
- scopes: RuntimeStrategyLineageScope[];
339
- strategyName: string;
340
- configId?: string;
341
- startTime: number;
342
- endTime: number;
343
- }): RuntimeStrategyAiGateChange[] => {
344
- const normalizedConfigId = configId ?? 'config';
345
- const observationsByTimestamp = new Map<
346
- number,
347
- { fingerprint: string; lastTimestamp: number }
348
- >();
349
-
350
- for (const scope of scopes) {
351
- if (
352
- scope.strategy !== strategyName ||
353
- (scope.runtimeConfigId ?? 'config') !== normalizedConfigId ||
354
- scope.firstTimestamp > endTime
355
- ) {
356
- continue;
357
- }
358
-
359
- const fingerprint = scope.lineage.gateFingerprint.trim();
360
- const existing = observationsByTimestamp.get(scope.firstTimestamp);
361
-
362
- // Multiple symbols are evaluated for the same strategy timestamp. If a
363
- // deploy happens during that cycle, prefer the lineage that kept running
364
- // afterwards instead of making the marker order depend on the symbol name.
365
- if (
366
- !existing ||
367
- scope.lastTimestamp > existing.lastTimestamp ||
368
- (scope.lastTimestamp === existing.lastTimestamp &&
369
- fingerprint > existing.fingerprint)
370
- ) {
371
- observationsByTimestamp.set(scope.firstTimestamp, {
372
- fingerprint,
373
- lastTimestamp: scope.lastTimestamp,
374
- });
375
- }
376
- }
377
-
378
- const observations = [...observationsByTimestamp.entries()].sort(
379
- ([leftTimestamp], [rightTimestamp]) => leftTimestamp - rightTimestamp,
380
- );
381
- const changes: RuntimeStrategyAiGateChange[] = [];
382
- let currentFingerprint: string | null = null;
383
-
384
- for (const [timestamp, observation] of observations) {
385
- if (currentFingerprint == null) {
386
- currentFingerprint = observation.fingerprint;
387
- continue;
388
- }
389
- if (observation.fingerprint === currentFingerprint) {
390
- continue;
391
- }
392
-
393
- if (timestamp >= startTime) {
394
- changes.push({
395
- timestamp,
396
- previousFingerprint: currentFingerprint,
397
- fingerprint: observation.fingerprint,
398
- });
399
- }
400
- currentFingerprint = observation.fingerprint;
401
- }
402
-
403
- return changes;
404
- };
405
-
406
123
  const roundValue = (value: number, digits = 2) => {
407
124
  if (!Number.isFinite(value)) {
408
125
  return 0;
@@ -804,37 +521,6 @@ const createEmptyRuntimeStat = (
804
521
  };
805
522
  };
806
523
 
807
- export const resolveStrategyNameByConfigKey = (
808
- userName: string,
809
- key: string,
810
- ): string | null => {
811
- return (
812
- resolveStrategyConfigIdentityByKey(userName, key)?.strategyName ?? null
813
- );
814
- };
815
-
816
- export const resolveStrategyConfigIdentityByKey = (
817
- userName: string,
818
- key: string,
819
- ): { strategyName: string; configId: string } | null => {
820
- const parts = key.split(':');
821
- if (parts.length !== 5) return null;
822
- const [users, keyUserName, strategies, strategyName, configId] = parts;
823
- if (
824
- users !== 'users' ||
825
- keyUserName !== userName ||
826
- strategies !== 'strategies' ||
827
- !strategyName ||
828
- strategyName === 'charts' ||
829
- !configId ||
830
- configId === 'results' ||
831
- !/^[a-zA-Z0-9_-]+$/.test(configId)
832
- ) {
833
- return null;
834
- }
835
- return { strategyName, configId };
836
- };
837
-
838
524
  const buildStrategyNameKeyMap = (strategyNames: string[]) => {
839
525
  const map = new Map<string, string>();
840
526
 
@@ -1134,127 +820,6 @@ const removeClosedPnlFromExactMaps = ({
1134
820
  }
1135
821
  };
1136
822
 
1137
- const removeClosedPnlFromSymbolBuckets = (
1138
- buckets: Map<string, ClosedPnlRecordWithOrderLinkId[]>,
1139
- row: ClosedPnlRecordWithOrderLinkId,
1140
- ) => {
1141
- const rows = buckets.get(row.symbol);
1142
- if (!rows?.length) {
1143
- return;
1144
- }
1145
-
1146
- const index = rows.findIndex((candidate) => candidate === row);
1147
- if (index >= 0) {
1148
- rows.splice(index, 1);
1149
- }
1150
- };
1151
-
1152
- const takeExactClosedPnlMatch = ({
1153
- exactByOrderLinkId,
1154
- exactByOrderId,
1155
- symbolBuckets,
1156
- orderLinkId,
1157
- orderId,
1158
- }: {
1159
- exactByOrderLinkId: Map<string, ClosedPnlRecordWithOrderLinkId>;
1160
- exactByOrderId: Map<string, ClosedPnlRecordWithOrderLinkId>;
1161
- symbolBuckets: Map<string, ClosedPnlRecordWithOrderLinkId[]>;
1162
- orderLinkId?: string | null;
1163
- orderId?: string | null;
1164
- }) => {
1165
- const exactKeys: Array<
1166
- [Map<string, ClosedPnlRecordWithOrderLinkId>, string | null | undefined]
1167
- > = [
1168
- [exactByOrderLinkId, orderLinkId],
1169
- [exactByOrderId, orderId],
1170
- ];
1171
-
1172
- for (const [bucket, key] of exactKeys) {
1173
- const normalizedKey = toNonEmptyString(key);
1174
-
1175
- if (!normalizedKey) {
1176
- continue;
1177
- }
1178
-
1179
- const exactMatch = bucket.get(normalizedKey);
1180
- if (!exactMatch) {
1181
- continue;
1182
- }
1183
-
1184
- removeClosedPnlFromExactMaps({
1185
- exactByOrderLinkId,
1186
- exactByOrderId,
1187
- row: exactMatch,
1188
- });
1189
- removeClosedPnlFromSymbolBuckets(symbolBuckets, exactMatch);
1190
- return exactMatch;
1191
- }
1192
-
1193
- return null;
1194
- };
1195
-
1196
- export const takeClosedPnlMatch = ({
1197
- exactByOrderLinkId,
1198
- exactByOrderId = new Map<string, ClosedPnlRecordWithOrderLinkId>(),
1199
- symbolBuckets,
1200
- trade,
1201
- }: {
1202
- exactByOrderLinkId: Map<string, ClosedPnlRecordWithOrderLinkId>;
1203
- exactByOrderId?: Map<string, ClosedPnlRecordWithOrderLinkId>;
1204
- symbolBuckets: Map<string, ClosedPnlRecordWithOrderLinkId[]>;
1205
- trade: RuntimeTradeRecord;
1206
- }) => {
1207
- const exactMatch = takeExactClosedPnlMatch({
1208
- exactByOrderLinkId,
1209
- exactByOrderId,
1210
- symbolBuckets,
1211
- orderLinkId: trade.orderId,
1212
- orderId: trade.orderId,
1213
- });
1214
-
1215
- if (exactMatch) {
1216
- return exactMatch;
1217
- }
1218
-
1219
- const rows = symbolBuckets.get(trade.symbol);
1220
- if (!rows?.length) {
1221
- return null;
1222
- }
1223
-
1224
- const minimumClosedAt = trade.entryTimestamp - 5 * 60_000;
1225
- const matchIndex = rows.reduce((bestIndex, row, index) => {
1226
- if (
1227
- !Number.isFinite(row.closedAt) ||
1228
- row.closedAt < minimumClosedAt ||
1229
- (row.direction && row.direction !== trade.direction)
1230
- ) {
1231
- return bestIndex;
1232
- }
1233
-
1234
- if (bestIndex < 0) {
1235
- return index;
1236
- }
1237
-
1238
- const best = rows[bestIndex];
1239
- return row.closedAt < best.closedAt ? index : bestIndex;
1240
- }, -1);
1241
-
1242
- if (matchIndex < 0) {
1243
- return null;
1244
- }
1245
-
1246
- const [row] = rows.splice(matchIndex, 1);
1247
- if (row) {
1248
- removeClosedPnlFromExactMaps({
1249
- exactByOrderLinkId,
1250
- exactByOrderId,
1251
- row,
1252
- });
1253
- }
1254
-
1255
- return row ?? null;
1256
- };
1257
-
1258
823
  const takeClosedPnlMatchForExchangeEntry = ({
1259
824
  exactByOrderLinkId,
1260
825
  exactByOrderId,