@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.
- package/package.json +14 -8
- package/src/app/actions/backtest.ts +2 -1
- package/src/app/actions/scanner.ts +2 -1
- package/src/app/api/backtest/files/route.ts +2 -1
- package/src/app/api/backtest/test/[strategy]/[name]/route.ts +1 -1
- package/src/app/api/derivatives/[symbol]/[interval]/route.ts +1 -1
- package/src/app/api/derivatives/summary/route.ts +1 -1
- package/src/app/api/spread/[symbol]/[interval]/route.ts +1 -1
- package/src/app/api/spread/summary/route.ts +1 -1
- package/src/app/api/strategies/runtime/route.ts +4 -674
- package/src/app/api/user/runtime-deployments/[deploymentId]/route.ts +1 -1
- package/src/app/api/user/runtime-deployments/route.ts +2 -2
- package/src/app/api/user/runtime-strategy-configs/route.ts +22 -212
- package/src/app/api/user/trading-accounts/[accountId]/route.ts +1 -1
- package/src/app/components/Backtest/TestList/index.tsx +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/circle.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/label.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/figures/star.ts +1 -1
- package/src/app/components/Dashboard/KlineChart/index.tsx +2 -1
- package/src/app/components/Shared/Filters/Root/index.tsx +1 -1
- package/src/app/components/Shared/Filters/context.ts +1 -1
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +81 -858
- package/src/app/components/Strategies/StrategyPerformanceCharts.tsx +419 -0
- package/src/app/components/Strategies/StrategySnapshotCard.tsx +122 -937
- package/src/app/components/UI/Segment/index.tsx +1 -1
- package/src/app/components/UI/Select/index.tsx +1 -1
- package/src/app/components/UI/SelectWithSearch/index.tsx +1 -1
- package/src/app/lib/backtestJobContracts.ts +67 -0
- package/src/app/lib/backtestJobProgress.ts +28 -0
- package/src/app/lib/backtestJobRequest.ts +107 -0
- package/src/app/lib/backtestJobs.ts +25 -257
- package/src/app/lib/runtimeDashboard.ts +684 -0
- package/src/app/lib/runtimeStrategies.ts +24 -454
- package/src/app/lib/runtimeStrategyConfigService.ts +279 -0
- package/src/app/lib/runtimeStrategyLineage.ts +264 -0
- package/src/app/lib/runtimeTradeReconciliation.ts +113 -0
- package/src/app/lib/runtimeTradeSync.ts +1 -1
- package/src/app/lib/strategyPerformance.ts +387 -0
- package/src/app/routes/dashboard/Dashboard.tsx +2 -6
- package/src/app/routes/derivatives/derivativesViewModel.ts +253 -0
- package/src/app/routes/derivatives/page.tsx +70 -262
- package/src/app/store/filters.ts +2 -1
- package/src/app/store/indicators.ts +2 -1
- package/src/app/store/tests.ts +1 -1
- package/src/app/store/tickers.ts +2 -1
- package/src/app/types/ui.ts +20 -0
|
@@ -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
|
|