@tradejs/app 2.0.18 → 2.0.19

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 (48) 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/spread/[symbol]/[interval]/route.ts +1 -1
  9. package/src/app/api/spread/summary/route.ts +1 -1
  10. package/src/app/api/strategies/runtime/route.ts +4 -674
  11. package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +1 -1
  12. package/src/app/api/user/runtime-deployments/route.ts +2 -2
  13. package/src/app/api/user/runtime-strategy-configs/route.ts +22 -212
  14. package/src/app/api/user/trading-accounts/[accountId]/route.ts +1 -1
  15. package/src/app/components/Backtest/TestList/index.tsx +1 -1
  16. package/src/app/components/Dashboard/KlineChart/figures/circle.ts +1 -1
  17. package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +1 -1
  18. package/src/app/components/Dashboard/KlineChart/figures/label.ts +1 -1
  19. package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +1 -1
  20. package/src/app/components/Dashboard/KlineChart/figures/star.ts +1 -1
  21. package/src/app/components/Dashboard/KlineChart/index.tsx +2 -1
  22. package/src/app/components/Shared/Filters/Root/index.tsx +1 -1
  23. package/src/app/components/Shared/Filters/context.ts +1 -1
  24. package/src/app/components/Strategies/RuntimeStrategyCard.tsx +81 -858
  25. package/src/app/components/Strategies/StrategyPerformanceCharts.tsx +419 -0
  26. package/src/app/components/Strategies/StrategySnapshotCard.tsx +122 -937
  27. package/src/app/components/UI/Segment/index.tsx +1 -1
  28. package/src/app/components/UI/Select/index.tsx +1 -1
  29. package/src/app/components/UI/SelectWithSearch/index.tsx +1 -1
  30. package/src/app/lib/backtestJobContracts.ts +67 -0
  31. package/src/app/lib/backtestJobProgress.ts +28 -0
  32. package/src/app/lib/backtestJobRequest.ts +107 -0
  33. package/src/app/lib/backtestJobs.ts +25 -257
  34. package/src/app/lib/runtimeDashboard.ts +684 -0
  35. package/src/app/lib/runtimeStrategies.ts +24 -454
  36. package/src/app/lib/runtimeStrategyConfigService.ts +279 -0
  37. package/src/app/lib/runtimeStrategyLineage.ts +264 -0
  38. package/src/app/lib/runtimeTradeReconciliation.ts +113 -0
  39. package/src/app/lib/runtimeTradeSync.ts +1 -1
  40. package/src/app/lib/strategyPerformance.ts +387 -0
  41. package/src/app/routes/dashboard/Dashboard.tsx +2 -6
  42. package/src/app/routes/derivatives/derivativesViewModel.ts +253 -0
  43. package/src/app/routes/derivatives/page.tsx +70 -262
  44. package/src/app/store/filters.ts +2 -1
  45. package/src/app/store/indicators.ts +2 -1
  46. package/src/app/store/tests.ts +1 -1
  47. package/src/app/store/tickers.ts +2 -1
  48. package/src/app/types/ui.ts +20 -0
@@ -5,27 +5,43 @@ 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,
15
13
  TestStat,
16
14
  MarketUniverse,
17
15
  Interval,
18
16
  } from '@tradejs/types';
17
+ import type {
18
+ RuntimeStrategyAiGateChange,
19
+ RuntimeStrategyMaxLossValueTimeline,
20
+ } from './runtimeStrategyLineage';
21
+ import {
22
+ takeExactClosedPnlMatch,
23
+ type ClosedPnlRecordWithOrderLinkId,
24
+ } from './runtimeTradeReconciliation';
25
+ export {
26
+ assignLegacyRuntimeTradeAccountScopes,
27
+ buildRuntimeStrategyAiGateChanges,
28
+ buildRuntimeStrategyIdentityKey,
29
+ buildRuntimeStrategyMaxLossValueTimeline,
30
+ getRuntimeStrategyAiGateObservedFrom,
31
+ isRuntimeStrategyLineageScope,
32
+ } from './runtimeStrategyLineage';
33
+ export type {
34
+ RuntimeStrategyAccountScope,
35
+ RuntimeStrategyAiGateChange,
36
+ RuntimeStrategyLineageScope,
37
+ RuntimeStrategyMaxLossValueChange,
38
+ RuntimeStrategyMaxLossValueTimeline,
39
+ } from './runtimeStrategyLineage';
40
+ export { takeClosedPnlMatch } from './runtimeTradeReconciliation';
19
41
 
20
42
  const MS_IN_DAY = 24 * 60 * 60 * 1000;
21
43
  const AVG_DAYS_IN_MONTH = 30.4375;
22
44
 
23
- type ClosedPnlRecordWithOrderLinkId = ClosedPnlRecord & {
24
- direction?: RuntimeTradeRecord['direction'];
25
- entryTimestamp?: number;
26
- orderLinkId?: string;
27
- };
28
-
29
45
  type RuntimeTradeWithResolvedPnl = RuntimeTradeRecord & {
30
46
  resolvedPnl: number;
31
47
  resolvedTimestamp: number;
@@ -73,35 +89,6 @@ export interface RuntimeStrategyTradeView {
73
89
  lastSyncedAt: number | null;
74
90
  }
75
91
 
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
92
  export interface RuntimeStrategyView {
106
93
  runtimeKey: string;
107
94
  strategyName: string;
@@ -126,65 +113,6 @@ export interface RuntimeStrategyView {
126
113
  orders: RuntimeStrategyTradeView[];
127
114
  }
128
115
 
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
116
  export interface RuntimeStrategiesResponse {
189
117
  provider: string;
190
118
  hours: number;
@@ -197,212 +125,6 @@ export interface RuntimeStrategiesResponse {
197
125
  strategies: RuntimeStrategyView[];
198
126
  }
199
127
 
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
128
  const roundValue = (value: number, digits = 2) => {
407
129
  if (!Number.isFinite(value)) {
408
130
  return 0;
@@ -804,37 +526,6 @@ const createEmptyRuntimeStat = (
804
526
  };
805
527
  };
806
528
 
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
529
  const buildStrategyNameKeyMap = (strategyNames: string[]) => {
839
530
  const map = new Map<string, string>();
840
531
 
@@ -1134,127 +825,6 @@ const removeClosedPnlFromExactMaps = ({
1134
825
  }
1135
826
  };
1136
827
 
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
828
  const takeClosedPnlMatchForExchangeEntry = ({
1259
829
  exactByOrderLinkId,
1260
830
  exactByOrderId,