@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
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { SegmentGroup } from '@chakra-ui/react';
4
- import { Items } from '@tradejs/types';
4
+ import type { Items } from '#app/types/ui';
5
5
 
6
6
  interface SegmentProps {
7
7
  defaultValue: string;
@@ -8,7 +8,7 @@ import {
8
8
  Select as UISelect,
9
9
  createListCollection,
10
10
  } from '@chakra-ui/react';
11
- import { Items } from '@tradejs/types';
11
+ import type { Items } from '#app/types/ui';
12
12
 
13
13
  interface SelectProps {
14
14
  defaultValue: string[];
@@ -11,7 +11,7 @@ import {
11
11
  useFilter,
12
12
  useListCollection,
13
13
  } from '@chakra-ui/react';
14
- import { Items } from '@tradejs/types';
14
+ import type { Items } from '#app/types/ui';
15
15
 
16
16
  interface SelectWithSearchProps {
17
17
  defaultValue: string[];
@@ -0,0 +1,67 @@
1
+ export type BacktestJobStatus =
2
+ | 'running'
3
+ | 'pausing'
4
+ | 'paused'
5
+ | 'completed'
6
+ | 'failed'
7
+ | 'cancelled';
8
+
9
+ export type BacktestPeriodMode = 'days' | 'range';
10
+
11
+ export interface BacktestJobRequest {
12
+ strategyName: string;
13
+ configId: string;
14
+ periodMode: BacktestPeriodMode;
15
+ days?: number;
16
+ startTime?: number;
17
+ endTime?: number;
18
+ ai: boolean;
19
+ fast: boolean;
20
+ interval: string;
21
+ connector: string;
22
+ tickers?: string;
23
+ tickersLimit?: number;
24
+ testsLimit?: number;
25
+ parallel?: number;
26
+ }
27
+
28
+ export interface BacktestJobProgress {
29
+ completed: number;
30
+ total: number | null;
31
+ percent: number;
32
+ averageProfit: number | null;
33
+ winRate: number | null;
34
+ successTests: number | null;
35
+ errorTests: number | null;
36
+ }
37
+
38
+ export interface BacktestJobRecord {
39
+ id: string;
40
+ userName: string;
41
+ status: BacktestJobStatus;
42
+ request: BacktestJobRequest;
43
+ command: string;
44
+ args: string[];
45
+ createdAt: string;
46
+ updatedAt: string;
47
+ startedAt?: string;
48
+ finishedAt?: string;
49
+ pausedAt?: string;
50
+ cancelledAt?: string;
51
+ lastHeartbeatAt?: string;
52
+ pid?: number;
53
+ exitCode?: number | null;
54
+ signal?: NodeJS.Signals | null;
55
+ runCount: number;
56
+ progress: BacktestJobProgress;
57
+ logs: string[];
58
+ error?: string;
59
+ pauseReason?: string;
60
+ }
61
+
62
+ export interface BacktestConfigSummary {
63
+ id: string;
64
+ strategyName: string;
65
+ paramCount: number;
66
+ combinationCount: number;
67
+ }
@@ -0,0 +1,28 @@
1
+ const stripAnsi = (value: string) =>
2
+ value.replace(
3
+ /[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g,
4
+ '',
5
+ );
6
+
7
+ export const parseBacktestProgressLine = (line: string, offset = 0) => {
8
+ const text = stripAnsi(line);
9
+ const progressMatch = text.match(
10
+ /(\d+)\/(\d+).*?\bavg\s+(-?\d+(?:\.\d+)?)\$\s+win\s+(-?\d+(?:\.\d+)?)%/i,
11
+ );
12
+ if (progressMatch) {
13
+ return {
14
+ completed: offset + Number(progressMatch[1]),
15
+ total: offset + Number(progressMatch[2]),
16
+ averageProfit: Number(progressMatch[3]),
17
+ winRate: Number(progressMatch[4]),
18
+ };
19
+ }
20
+ const testsMatch = text.match(/\btests:\s*(\d+)\b/i);
21
+ if (testsMatch) return { total: offset + Number(testsMatch[1]) };
22
+ const successMatch = text.match(/\bSUCCESS TESTS:\s*(\d+)\b/i);
23
+ if (successMatch) {
24
+ return { successTests: offset + Number(successMatch[1]) };
25
+ }
26
+ const errorMatch = text.match(/\bERRORS:\s*(\d+)\b/i);
27
+ return errorMatch ? { errorTests: Number(errorMatch[1]) } : null;
28
+ };
@@ -0,0 +1,107 @@
1
+ import type {
2
+ BacktestJobRequest,
3
+ BacktestPeriodMode,
4
+ } from './backtestJobContracts';
5
+
6
+ const DEFAULT_INTERVAL = '15';
7
+ const DEFAULT_CONNECTOR = 'binance';
8
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
9
+ Boolean(value) && typeof value === 'object' && !Array.isArray(value);
10
+ const normalizeText = (value: unknown) =>
11
+ typeof value === 'string' ? value.trim() : '';
12
+ const toPositiveNumber = (value: unknown) => {
13
+ const parsed = Number(value);
14
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
15
+ };
16
+ const toPositiveInteger = (value: unknown) => {
17
+ const parsed = toPositiveNumber(value);
18
+ return parsed == null ? undefined : Math.trunc(parsed);
19
+ };
20
+
21
+ export const normalizeBacktestJobRequest = (
22
+ payload: unknown,
23
+ ): BacktestJobRequest => {
24
+ if (!isPlainObject(payload)) throw new Error('Invalid backtest request');
25
+ const configId = normalizeText(payload.configId);
26
+ if (!configId) throw new Error('Backtest config is required');
27
+ const strategyName =
28
+ normalizeText(payload.strategyName) || configId.split(':')[0] || configId;
29
+ const periodMode: BacktestPeriodMode =
30
+ payload.periodMode === 'range' ? 'range' : 'days';
31
+ const request: BacktestJobRequest = {
32
+ strategyName,
33
+ configId,
34
+ periodMode,
35
+ ai: payload.ai === true,
36
+ fast: payload.fast === true,
37
+ interval: normalizeText(payload.interval) || DEFAULT_INTERVAL,
38
+ connector: normalizeText(payload.connector) || DEFAULT_CONNECTOR,
39
+ };
40
+ if (periodMode === 'range') {
41
+ const startTime = toPositiveInteger(payload.startTime);
42
+ const endTime = toPositiveInteger(payload.endTime);
43
+ if (!startTime || !endTime || startTime >= endTime) {
44
+ throw new Error('Valid start and end timestamps are required');
45
+ }
46
+ request.startTime = startTime;
47
+ request.endTime = endTime;
48
+ } else {
49
+ request.days = toPositiveNumber(payload.days) ?? 30;
50
+ }
51
+
52
+ const tickers = normalizeText(payload.tickers);
53
+ const tickersLimit = toPositiveInteger(payload.tickersLimit);
54
+ const testsLimit = toPositiveInteger(payload.testsLimit);
55
+ const parallel = toPositiveInteger(payload.parallel);
56
+ if (tickers) request.tickers = tickers;
57
+ if (tickersLimit) request.tickersLimit = tickersLimit;
58
+ if (testsLimit) request.testsLimit = testsLimit;
59
+ if (parallel) request.parallel = parallel;
60
+ return request;
61
+ };
62
+
63
+ export const buildBacktestCommandArgs = ({
64
+ request,
65
+ userName,
66
+ skip = 0,
67
+ }: {
68
+ request: BacktestJobRequest;
69
+ userName: string;
70
+ skip?: number;
71
+ }) => {
72
+ const args = [
73
+ 'backtest',
74
+ '--config',
75
+ request.configId,
76
+ '--user',
77
+ userName,
78
+ '--timeframe',
79
+ request.interval,
80
+ '--connector',
81
+ request.connector,
82
+ '--progressStep',
83
+ '1',
84
+ ];
85
+ if (request.periodMode === 'range') {
86
+ args.push(
87
+ '--startTime',
88
+ String(request.startTime),
89
+ '--endTime',
90
+ String(request.endTime),
91
+ );
92
+ } else if (request.days) {
93
+ args.push('--days', String(request.days));
94
+ }
95
+ if (request.ai) args.push('--ai');
96
+ if (request.fast) args.push('--fast');
97
+ if (request.tickers) args.push('--tickers', request.tickers);
98
+ if (request.tickersLimit) {
99
+ args.push('--tickersLimit', String(request.tickersLimit));
100
+ }
101
+ if (request.parallel) args.push('--parallel', String(request.parallel));
102
+ if (skip > 0) args.push('--skip', String(skip));
103
+ if (request.testsLimit) {
104
+ args.push('--tests', String(Math.max(0, request.testsLimit - skip)));
105
+ }
106
+ return args;
107
+ };
@@ -12,81 +12,35 @@ import {
12
12
  } from '@tradejs/infra/redis';
13
13
  import { logger } from '@tradejs/infra/logger';
14
14
  import type { StrategyConfigGrid } from '@tradejs/types';
15
+ import type {
16
+ BacktestConfigSummary,
17
+ BacktestJobProgress,
18
+ BacktestJobRecord,
19
+ BacktestJobRequest,
20
+ BacktestJobStatus,
21
+ } from './backtestJobContracts';
22
+ import {
23
+ buildBacktestCommandArgs,
24
+ normalizeBacktestJobRequest,
25
+ } from './backtestJobRequest';
26
+ import { parseBacktestProgressLine } from './backtestJobProgress';
27
+ export type {
28
+ BacktestConfigSummary,
29
+ BacktestJobProgress,
30
+ BacktestJobRecord,
31
+ BacktestJobRequest,
32
+ BacktestJobStatus,
33
+ BacktestPeriodMode,
34
+ } from './backtestJobContracts';
35
+ export {
36
+ buildBacktestCommandArgs,
37
+ normalizeBacktestJobRequest,
38
+ } from './backtestJobRequest';
39
+ export { parseBacktestProgressLine } from './backtestJobProgress';
15
40
 
16
41
  const HEARTBEAT_TIMEOUT_MS = 20_000;
17
42
  const SWEEP_INTERVAL_MS = 5_000;
18
43
  const MAX_LOG_LINES = 220;
19
- const DEFAULT_INTERVAL = '15';
20
- const DEFAULT_CONNECTOR = 'binance';
21
-
22
- export type BacktestJobStatus =
23
- | 'running'
24
- | 'pausing'
25
- | 'paused'
26
- | 'completed'
27
- | 'failed'
28
- | 'cancelled';
29
-
30
- export type BacktestPeriodMode = 'days' | 'range';
31
-
32
- export interface BacktestJobRequest {
33
- strategyName: string;
34
- configId: string;
35
- periodMode: BacktestPeriodMode;
36
- days?: number;
37
- startTime?: number;
38
- endTime?: number;
39
- ai: boolean;
40
- fast: boolean;
41
- interval: string;
42
- connector: string;
43
- tickers?: string;
44
- tickersLimit?: number;
45
- testsLimit?: number;
46
- parallel?: number;
47
- }
48
-
49
- export interface BacktestJobProgress {
50
- completed: number;
51
- total: number | null;
52
- percent: number;
53
- averageProfit: number | null;
54
- winRate: number | null;
55
- successTests: number | null;
56
- errorTests: number | null;
57
- }
58
-
59
- export interface BacktestJobRecord {
60
- id: string;
61
- userName: string;
62
- status: BacktestJobStatus;
63
- request: BacktestJobRequest;
64
- command: string;
65
- args: string[];
66
- createdAt: string;
67
- updatedAt: string;
68
- startedAt?: string;
69
- finishedAt?: string;
70
- pausedAt?: string;
71
- cancelledAt?: string;
72
- lastHeartbeatAt?: string;
73
- pid?: number;
74
- exitCode?: number | null;
75
- signal?: NodeJS.Signals | null;
76
- runCount: number;
77
- progress: BacktestJobProgress;
78
- logs: string[];
79
- error?: string;
80
- pauseReason?: string;
81
- }
82
-
83
- export interface BacktestConfigSummary {
84
- id: string;
85
- strategyName: string;
86
- paramCount: number;
87
- combinationCount: number;
88
- }
89
-
90
44
  type BacktestProcessHandle = {
91
45
  child: ChildProcess;
92
46
  record: BacktestJobRecord;
@@ -136,21 +90,6 @@ const emptyProgress = (): BacktestJobProgress => ({
136
90
  errorTests: null,
137
91
  });
138
92
 
139
- const toFiniteNumber = (value: unknown): number | null => {
140
- const parsed = Number(value);
141
- return Number.isFinite(parsed) ? parsed : null;
142
- };
143
-
144
- const toPositiveNumber = (value: unknown): number | undefined => {
145
- const parsed = toFiniteNumber(value);
146
- return parsed != null && parsed > 0 ? parsed : undefined;
147
- };
148
-
149
- const toPositiveInteger = (value: unknown): number | undefined => {
150
- const parsed = toPositiveNumber(value);
151
- return parsed == null ? undefined : Math.trunc(parsed);
152
- };
153
-
154
93
  const normalizeText = (value: unknown) =>
155
94
  typeof value === 'string' ? value.trim() : '';
156
95
 
@@ -281,47 +220,6 @@ const recalculatePercent = (progress: BacktestJobProgress) => {
281
220
  );
282
221
  };
283
222
 
284
- export const parseBacktestProgressLine = (line: string, offset = 0) => {
285
- const text = stripAnsi(line);
286
- const progressMatch = text.match(
287
- /(\d+)\/(\d+).*?\bavg\s+(-?\d+(?:\.\d+)?)\$\s+win\s+(-?\d+(?:\.\d+)?)%/i,
288
- );
289
-
290
- if (progressMatch) {
291
- const completed = Number(progressMatch[1]);
292
- const total = Number(progressMatch[2]);
293
- return {
294
- completed: offset + completed,
295
- total: offset + total,
296
- averageProfit: Number(progressMatch[3]),
297
- winRate: Number(progressMatch[4]),
298
- };
299
- }
300
-
301
- const testsMatch = text.match(/\btests:\s*(\d+)\b/i);
302
- if (testsMatch) {
303
- return {
304
- total: offset + Number(testsMatch[1]),
305
- };
306
- }
307
-
308
- const successMatch = text.match(/\bSUCCESS TESTS:\s*(\d+)\b/i);
309
- if (successMatch) {
310
- return {
311
- successTests: offset + Number(successMatch[1]),
312
- };
313
- }
314
-
315
- const errorMatch = text.match(/\bERRORS:\s*(\d+)\b/i);
316
- if (errorMatch) {
317
- return {
318
- errorTests: Number(errorMatch[1]),
319
- };
320
- }
321
-
322
- return null;
323
- };
324
-
325
223
  const applyProgressLine = (
326
224
  record: BacktestJobRecord,
327
225
  line: string,
@@ -386,136 +284,6 @@ const getLiveRecord = async (userName: string, jobId: string) => {
386
284
  return loadJob(userName, jobId);
387
285
  };
388
286
 
389
- export const normalizeBacktestJobRequest = (
390
- payload: unknown,
391
- ): BacktestJobRequest => {
392
- if (!isPlainObject(payload)) {
393
- throw new Error('Invalid backtest request');
394
- }
395
-
396
- const configId = normalizeText(payload.configId);
397
- if (!configId) {
398
- throw new Error('Backtest config is required');
399
- }
400
-
401
- const derivedStrategyName = configId.split(':')[0] || configId;
402
- const strategyName =
403
- normalizeText(payload.strategyName) || derivedStrategyName;
404
- const periodMode: BacktestPeriodMode =
405
- payload.periodMode === 'range' ? 'range' : 'days';
406
- const interval = normalizeText(payload.interval) || DEFAULT_INTERVAL;
407
- const connector = normalizeText(payload.connector) || DEFAULT_CONNECTOR;
408
-
409
- const request: BacktestJobRequest = {
410
- strategyName,
411
- configId,
412
- periodMode,
413
- ai: payload.ai === true,
414
- fast: payload.fast === true,
415
- interval,
416
- connector,
417
- };
418
-
419
- if (periodMode === 'range') {
420
- const startTime = toPositiveInteger(payload.startTime);
421
- const endTime = toPositiveInteger(payload.endTime);
422
- if (!startTime || !endTime || startTime >= endTime) {
423
- throw new Error('Valid start and end timestamps are required');
424
- }
425
- request.startTime = startTime;
426
- request.endTime = endTime;
427
- } else {
428
- request.days = toPositiveNumber(payload.days) ?? 30;
429
- }
430
-
431
- const tickers = normalizeText(payload.tickers);
432
- if (tickers) {
433
- request.tickers = tickers;
434
- }
435
-
436
- const tickersLimit = toPositiveInteger(payload.tickersLimit);
437
- if (tickersLimit) {
438
- request.tickersLimit = tickersLimit;
439
- }
440
-
441
- const testsLimit = toPositiveInteger(payload.testsLimit);
442
- if (testsLimit) {
443
- request.testsLimit = testsLimit;
444
- }
445
-
446
- const parallel = toPositiveInteger(payload.parallel);
447
- if (parallel) {
448
- request.parallel = parallel;
449
- }
450
-
451
- return request;
452
- };
453
-
454
- export const buildBacktestCommandArgs = ({
455
- request,
456
- userName,
457
- skip = 0,
458
- }: {
459
- request: BacktestJobRequest;
460
- userName: string;
461
- skip?: number;
462
- }) => {
463
- const args = [
464
- 'backtest',
465
- '--config',
466
- request.configId,
467
- '--user',
468
- userName,
469
- '--timeframe',
470
- request.interval,
471
- '--connector',
472
- request.connector,
473
- '--progressStep',
474
- '1',
475
- ];
476
-
477
- if (request.periodMode === 'range') {
478
- args.push(
479
- '--startTime',
480
- String(request.startTime),
481
- '--endTime',
482
- String(request.endTime),
483
- );
484
- } else if (request.days) {
485
- args.push('--days', String(request.days));
486
- }
487
-
488
- if (request.ai) {
489
- args.push('--ai');
490
- }
491
-
492
- if (request.fast) {
493
- args.push('--fast');
494
- }
495
-
496
- if (request.tickers) {
497
- args.push('--tickers', request.tickers);
498
- }
499
-
500
- if (request.tickersLimit) {
501
- args.push('--tickersLimit', String(request.tickersLimit));
502
- }
503
-
504
- if (request.parallel) {
505
- args.push('--parallel', String(request.parallel));
506
- }
507
-
508
- if (skip > 0) {
509
- args.push('--skip', String(skip));
510
- }
511
-
512
- if (request.testsLimit) {
513
- args.push('--tests', String(Math.max(0, request.testsLimit - skip)));
514
- }
515
-
516
- return args;
517
- };
518
-
519
287
  const shouldSkipLaunch = (record: BacktestJobRecord, skip: number) =>
520
288
  Boolean(record.request.testsLimit && skip >= record.request.testsLimit);
521
289