@tradejs/cli 1.0.5 → 1.0.8

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 (34) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +13791 -7652
  3. package/dist/lib/aiTrainMetrics.js +173 -0
  4. package/dist/lib/candlesProviderMigration.js +266 -0
  5. package/dist/lib/cliArgs.js +40 -0
  6. package/dist/lib/derivativesContextBackfill.js +487 -0
  7. package/dist/lib/researchAgent.js +195 -0
  8. package/dist/lib/runtimeParity.js +234 -0
  9. package/dist/lib/telegramReports.js +91 -0
  10. package/dist/lib/timeWindow.js +101 -0
  11. package/dist/scripts/agentRun.js +895 -0
  12. package/dist/scripts/aiExport.js +192 -0
  13. package/dist/scripts/aiExportSelect.js +135 -0
  14. package/dist/scripts/aiTrain.js +2321 -0
  15. package/dist/scripts/backtest.js +998 -111
  16. package/dist/scripts/candlesMigrateProvider.js +5302 -0
  17. package/dist/scripts/continuity.js +1 -1
  18. package/dist/scripts/derivativesIngest.js +5 -0
  19. package/dist/scripts/derivativesIngestCoinalyzeAll.js +21 -12
  20. package/dist/scripts/migration.js +1 -1
  21. package/dist/scripts/mlExport.js +112 -13
  22. package/dist/scripts/mlExportSelect.js +43 -9
  23. package/dist/scripts/mlTrainLatestSelect.js +4 -3
  24. package/dist/scripts/researchAuto.js +845 -0
  25. package/dist/scripts/resolveExportStrategy.js +133 -0
  26. package/dist/scripts/results.js +46 -22
  27. package/dist/scripts/runtimeParity.js +1651 -0
  28. package/dist/scripts/selectStrategy.js +4 -3
  29. package/dist/scripts/signals.js +799 -62
  30. package/dist/scripts/signalsSummary.js +655 -0
  31. package/dist/scripts/spawnSiblingScript.js +71 -0
  32. package/dist/scripts/user-add.js +0 -9
  33. package/dist/workers/testerWorker.js +13 -2
  34. package/package.json +18 -12
@@ -0,0 +1,845 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/scripts/researchAuto.ts
31
+ var researchAuto_exports = {};
32
+ __export(researchAuto_exports, {
33
+ buildTelegramReport: () => buildTelegramReport,
34
+ createEmptySteps: () => createEmptySteps,
35
+ escapeHtml: () => escapeHtml,
36
+ formatMskDateTime: () => formatMskDateTime,
37
+ getBacktestResultsPrefix: () => getBacktestResultsPrefix,
38
+ listMergedFiles: () => listMergedFiles,
39
+ listRuntimeStrategyNames: () => listRuntimeStrategyNames,
40
+ parseJsonOutput: () => parseJsonOutput,
41
+ resolveStrategyNameByConfigKey: () => resolveStrategyNameByConfigKey,
42
+ resolveTarget: () => resolveTarget,
43
+ runCliCommand: () => runCliCommand,
44
+ toStrategyConfigGrid: () => toStrategyConfigGrid,
45
+ trimTextTail: () => trimTextTail
46
+ });
47
+ module.exports = __toCommonJS(researchAuto_exports);
48
+ var import_args = __toESM(require("args"));
49
+ var import_chalk = __toESM(require("chalk"));
50
+ var import_node_child_process = require("child_process");
51
+ var import_promises = __toESM(require("fs/promises"));
52
+ var import_node_path = __toESM(require("path"));
53
+ var import_ai = require("@tradejs/infra/ai");
54
+ var import_logger = require("@tradejs/infra/logger");
55
+ var import_redis = require("@tradejs/infra/redis");
56
+ var import_strategies = require("@tradejs/strategies");
57
+
58
+ // src/lib/telegramReports.ts
59
+ var import_cli = require("@tradejs/node/cli");
60
+ var TELEGRAM_REPORT_CHUNK_LIMIT = 3900;
61
+ var buildPartHeader = (index, total) => `\u{1F4E9} <b>Part ${index}/${total}</b>`;
62
+ var splitLongLine = (line, limit) => {
63
+ const chunks = [];
64
+ for (let index = 0; index < line.length; index += limit) {
65
+ chunks.push(line.slice(index, index + limit));
66
+ }
67
+ return chunks;
68
+ };
69
+ var splitTelegramReport = (message, limit = TELEGRAM_REPORT_CHUNK_LIMIT) => {
70
+ const normalizedLimit = Math.max(100, Math.floor(limit));
71
+ if (message.length <= normalizedLimit) {
72
+ return [message];
73
+ }
74
+ const chunks = [];
75
+ let current = "";
76
+ const flush = () => {
77
+ if (!current) {
78
+ return;
79
+ }
80
+ chunks.push(current);
81
+ current = "";
82
+ };
83
+ for (const line of message.split("\n")) {
84
+ const next = current ? `${current}
85
+ ${line}` : line;
86
+ if (next.length <= normalizedLimit) {
87
+ current = next;
88
+ continue;
89
+ }
90
+ flush();
91
+ if (line.length <= normalizedLimit) {
92
+ current = line;
93
+ continue;
94
+ }
95
+ chunks.push(...splitLongLine(line, normalizedLimit));
96
+ }
97
+ flush();
98
+ return chunks;
99
+ };
100
+ var sendTelegramReport = async (message, options = {}) => {
101
+ const chunks = splitTelegramReport(message);
102
+ if (chunks.length === 1) {
103
+ await (0, import_cli.sendTextToTG)(chunks[0], options);
104
+ return chunks.length;
105
+ }
106
+ for (const [index, chunk] of chunks.entries()) {
107
+ const header = buildPartHeader(index + 1, chunks.length);
108
+ await (0, import_cli.sendTextToTG)(`${header}
109
+
110
+ ${chunk}`, {
111
+ userName: options.userName,
112
+ markup: index === chunks.length - 1 ? options.markup : void 0
113
+ });
114
+ }
115
+ return chunks.length;
116
+ };
117
+
118
+ // src/scripts/researchAuto.ts
119
+ import_args.default.example(
120
+ "yarn cli:node8g research:auto --strategy TrendLine --days 45",
121
+ "Run the nightly backtest -> ai-export -> ai-train local research pipeline and save structured results to Redis"
122
+ );
123
+ import_args.default.option(["s", "strategy"], "Strategy name");
124
+ import_args.default.option(["c", "config"], "Backtest config name");
125
+ import_args.default.option(["U", "user"], "Use user config", "root");
126
+ import_args.default.option(["o", "connector"], "Backtest connector", "bybit");
127
+ import_args.default.option(["f", "timeframe"], "Backtest timeframe", "15");
128
+ import_args.default.option(["d", "days"], "Backtest days window", 45);
129
+ import_args.default.option(["n", "recent"], "AI train recent rows window", 1e3);
130
+ import_args.default.option("skip", "AI train skip rows from end", 0);
131
+ import_args.default.option("minQuality", "Minimum AI quality required to approve entry", 4);
132
+ import_args.default.option("outDir", "AI export output directory", "data/ai/export");
133
+ import_args.default.option("skipAgent", "Skip direct research agent invocation", false);
134
+ import_args.default.option("json", "Print structured JSON summary", false);
135
+ var flags = import_args.default.parse(process.argv);
136
+ var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
137
+ var dotenvPath = String(
138
+ process.env.DOTENV_CONFIG_PATH || import_node_path.default.join(projectRoot, ".env")
139
+ ).trim() || import_node_path.default.join(projectRoot, ".env");
140
+ var trimTextTail = (value, limit = 4e3) => value.length <= limit ? value : value.slice(-limit);
141
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
142
+ var createEmptySteps = () => ({
143
+ prepareBacktestConfig: {
144
+ status: "pending",
145
+ command: "prepare-backtest-config",
146
+ args: []
147
+ },
148
+ cleanTests: {
149
+ status: "pending",
150
+ command: "clean-tests",
151
+ args: []
152
+ },
153
+ cleanAiExport: {
154
+ status: "pending",
155
+ command: "clean-dir",
156
+ args: ["--dir", "ai/export"]
157
+ },
158
+ backtest: {
159
+ status: "pending",
160
+ command: "backtest",
161
+ args: []
162
+ },
163
+ aiExport: {
164
+ status: "pending",
165
+ command: "ai-export",
166
+ args: []
167
+ },
168
+ aiTrainLocal: {
169
+ status: "pending",
170
+ command: "ai-train",
171
+ args: []
172
+ },
173
+ agentRun: {
174
+ status: "pending",
175
+ command: "agent-run",
176
+ args: []
177
+ }
178
+ });
179
+ var saveRun = async (run) => {
180
+ await (0, import_redis.setData)(import_redis.redisKeys.researchRun(run.userName, run.runId), run, {
181
+ expire: 0
182
+ });
183
+ await (0, import_redis.setData)(import_redis.redisKeys.researchLatestRun(run.userName, run.strategy), run, {
184
+ expire: 0
185
+ });
186
+ };
187
+ var listMergedFiles = async (outDir, strategyName) => {
188
+ try {
189
+ const entries = await import_promises.default.readdir(outDir);
190
+ const prefix = `ai-dataset-${(0, import_ai.toFileToken)(strategyName)}-merged-`;
191
+ return entries.filter((name) => name.startsWith(prefix) && name.endsWith(".jsonl")).sort().map((name) => import_node_path.default.join(outDir, name));
192
+ } catch {
193
+ return [];
194
+ }
195
+ };
196
+ var getBacktestResultsPrefix = (userName, config) => `users:${userName}:backtests:results:${config}:`;
197
+ var resolveStrategyNameByConfigKey = (userName, key) => {
198
+ const parts = key.split(":");
199
+ if (parts.length !== 5) {
200
+ return null;
201
+ }
202
+ const [users, keyUserName, strategiesKey, strategyName, configKey] = parts;
203
+ if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || configKey !== "config" || !strategyName) {
204
+ return null;
205
+ }
206
+ return strategyName;
207
+ };
208
+ var listRuntimeStrategyNames = async (userName) => {
209
+ const keys = await (0, import_redis.getKeys)(`${import_redis.redisKeys.strategies(userName)}:`);
210
+ return keys.filter((key) => key.endsWith(":config")).map((key) => resolveStrategyNameByConfigKey(userName, key)).filter((value) => Boolean(value)).sort((left, right) => left.localeCompare(right));
211
+ };
212
+ var toStrategyConfigGrid = (strategyConfig) => {
213
+ return Object.fromEntries(
214
+ Object.entries(strategyConfig).map(([key, value]) => [key, [value]])
215
+ );
216
+ };
217
+ var formatMskDateTime = (value) => {
218
+ if (!value) {
219
+ return "n/a";
220
+ }
221
+ const timestamp = Date.parse(value);
222
+ if (!Number.isFinite(timestamp)) {
223
+ return value;
224
+ }
225
+ return new Intl.DateTimeFormat("ru-RU", {
226
+ timeZone: "Europe/Moscow",
227
+ year: "numeric",
228
+ month: "2-digit",
229
+ day: "2-digit",
230
+ hour: "2-digit",
231
+ minute: "2-digit",
232
+ hour12: false
233
+ }).format(new Date(timestamp));
234
+ };
235
+ var buildTelegramReport = (run) => {
236
+ const aiTrainLocal = run.artifacts.aiTrainLocal;
237
+ const agentRun = run.artifacts.agentRun;
238
+ const lines = [
239
+ `<b>Research auto: ${escapeHtml(run.strategy)}</b>`,
240
+ `Status: <b>${run.status}</b>`,
241
+ `Run: <code>${escapeHtml(run.runId)}</code>`,
242
+ `Selected by: <code>${escapeHtml(run.selectedBy)}</code>`,
243
+ `Backtest config: <code>${escapeHtml(run.config)}</code>`,
244
+ `Window: <code>${run.days}d</code>, timeframe <code>${escapeHtml(run.timeframe)}</code>, connector <code>${escapeHtml(run.connector)}</code>`,
245
+ `Started: <code>${escapeHtml(formatMskDateTime(run.startedAt))} MSK</code>`,
246
+ `Finished: <code>${escapeHtml(formatMskDateTime(run.finishedAt))} MSK</code>`
247
+ ];
248
+ if (run.artifacts.backtestResultKey) {
249
+ lines.push(
250
+ `Backtest result key: <code>${escapeHtml(
251
+ run.artifacts.backtestResultKey
252
+ )}</code>`
253
+ );
254
+ }
255
+ if (run.artifacts.aiExportFile) {
256
+ lines.push(
257
+ `AI export: <code>${escapeHtml(run.artifacts.aiExportFile)}</code>`
258
+ );
259
+ }
260
+ if (aiTrainLocal?.run || aiTrainLocal?.outcome) {
261
+ lines.push("");
262
+ lines.push("<b>AI Train Local</b>");
263
+ if (typeof aiTrainLocal.run?.totalRows === "number") {
264
+ lines.push(
265
+ `Rows: <code>${aiTrainLocal.run.totalRows}</code>, approved: <code>${aiTrainLocal.run.approvedRows ?? 0}</code>, minQuality: <code>${aiTrainLocal.run.minQuality ?? run.minQuality}</code>`
266
+ );
267
+ }
268
+ if (typeof aiTrainLocal.outcome?.approvalRate === "number") {
269
+ lines.push(
270
+ `Approval rate: <code>${aiTrainLocal.outcome.approvalRate.toFixed(4)}</code>, precision: <code>${(aiTrainLocal.outcome.precisionApproved ?? 0).toFixed(4)}</code>, recall winners: <code>${(aiTrainLocal.outcome.recallWinners ?? 0).toFixed(4)}</code>`
271
+ );
272
+ lines.push(
273
+ `Avg approved PnL: <code>${(aiTrainLocal.outcome.avgProfitApproved ?? 0).toFixed(4)}</code>, monthly: <code>${(aiTrainLocal.outcome.avgProfitApprovedPerMonth ?? 0).toFixed(4)}</code>, expectancy delta: <code>${(aiTrainLocal.outcome.expectancyDelta ?? 0).toFixed(4)}</code>`
274
+ );
275
+ }
276
+ }
277
+ if (run.error) {
278
+ lines.push("");
279
+ lines.push("<b>Error</b>");
280
+ lines.push(`<code>${escapeHtml(run.error)}</code>`);
281
+ }
282
+ lines.push("");
283
+ if (agentRun) {
284
+ lines.push(`Agent layer: <code>${escapeHtml(agentRun.status)}</code>`);
285
+ if (agentRun.branchName) {
286
+ lines.push(
287
+ `Agent branch: <code>${escapeHtml(agentRun.branchName)}</code>`
288
+ );
289
+ }
290
+ if (agentRun.commitHash) {
291
+ lines.push(
292
+ `Agent commit: <code>${escapeHtml(agentRun.commitHash)}</code>`
293
+ );
294
+ }
295
+ if (typeof agentRun.pullRequestNumber === "number") {
296
+ lines.push(`Agent PR: <code>#${agentRun.pullRequestNumber}</code>`);
297
+ }
298
+ if (agentRun.pullRequestUrl) {
299
+ lines.push(
300
+ `Agent PR URL: <code>${escapeHtml(agentRun.pullRequestUrl)}</code>`
301
+ );
302
+ }
303
+ if (agentRun.summary) {
304
+ lines.push(`Agent summary: <code>${escapeHtml(agentRun.summary)}</code>`);
305
+ }
306
+ } else if (run.steps.agentRun?.status === "pending") {
307
+ lines.push("Agent layer: <code>pending</code>");
308
+ } else {
309
+ lines.push(
310
+ `Agent layer: <code>${escapeHtml(run.steps.agentRun?.status || "unknown")}</code>`
311
+ );
312
+ }
313
+ return lines.join("\n");
314
+ };
315
+ var runCliCommand = async (params) => {
316
+ const startedAt = Date.now();
317
+ const captureMode = params.captureMode || "tail";
318
+ const tailLimit = Math.max(512, params.tailLimit || 16384);
319
+ const env = {
320
+ ...process.env,
321
+ PROJECT_CWD: projectRoot,
322
+ DOTENV_CONFIG_PATH: dotenvPath,
323
+ ...params.node8g ? {
324
+ NODE_OPTIONS: [
325
+ "--max-old-space-size=8192",
326
+ String(process.env.NODE_OPTIONS || "").trim()
327
+ ].filter(Boolean).join(" ")
328
+ } : {}
329
+ };
330
+ return new Promise((resolve, reject) => {
331
+ const child = (0, import_node_child_process.spawn)(
332
+ "bash",
333
+ ["./bin/run-cli-dist.sh", params.command, ...params.args],
334
+ {
335
+ cwd: projectRoot,
336
+ env,
337
+ stdio: ["ignore", "pipe", "pipe"]
338
+ }
339
+ );
340
+ let stdout = "";
341
+ let stderr = "";
342
+ let heartbeatTimer = null;
343
+ const appendChunk = (current, incoming) => {
344
+ if (captureMode === "full") {
345
+ return current + incoming;
346
+ }
347
+ const next = current + incoming;
348
+ return next.length <= tailLimit ? next : next.slice(-tailLimit);
349
+ };
350
+ const emitLines = (source, text) => {
351
+ const prefix = params.liveLogPrefix ? `[research:auto:${params.liveLogPrefix}:${source}] ` : `[research:auto:${source}] `;
352
+ for (const line of text.split(/\r?\n/).map((value) => value.trimEnd()).filter(Boolean)) {
353
+ import_logger.logger.info(import_chalk.default.gray(`${prefix}${line}`));
354
+ }
355
+ };
356
+ if (params.heartbeatMs && params.heartbeatMs > 0) {
357
+ heartbeatTimer = setInterval(() => {
358
+ const elapsedMs = Date.now() - startedAt;
359
+ import_logger.logger.info(
360
+ import_chalk.default.gray(
361
+ `[research:auto:${params.liveLogPrefix || params.command}] still running after ${Math.round(elapsedMs / 1e3)}s`
362
+ )
363
+ );
364
+ }, params.heartbeatMs);
365
+ }
366
+ child.stdout.on("data", (chunk) => {
367
+ const text = chunk.toString();
368
+ stdout = appendChunk(stdout, text);
369
+ if (params.liveLogPrefix) {
370
+ emitLines("stdout", text);
371
+ }
372
+ });
373
+ child.stderr.on("data", (chunk) => {
374
+ const text = chunk.toString();
375
+ stderr = appendChunk(stderr, text);
376
+ if (params.liveLogPrefix) {
377
+ emitLines("stderr", text);
378
+ }
379
+ });
380
+ child.on("error", (error) => {
381
+ if (heartbeatTimer) {
382
+ clearInterval(heartbeatTimer);
383
+ }
384
+ reject(error);
385
+ });
386
+ child.on("close", (code) => {
387
+ if (heartbeatTimer) {
388
+ clearInterval(heartbeatTimer);
389
+ }
390
+ resolve({
391
+ exitCode: code ?? 1,
392
+ stdout,
393
+ stderr,
394
+ durationMs: Date.now() - startedAt
395
+ });
396
+ });
397
+ });
398
+ };
399
+ var resolveTarget = async () => {
400
+ const userName = String(flags.user || "root").trim() || "root";
401
+ const explicitConfig = String(flags.config || "").trim();
402
+ if (explicitConfig) {
403
+ return {
404
+ strategy: explicitConfig.split(":")[0] || explicitConfig,
405
+ config: explicitConfig,
406
+ selectedBy: "config"
407
+ };
408
+ }
409
+ const explicitStrategy = String(flags.strategy || "").trim();
410
+ if (explicitStrategy) {
411
+ return {
412
+ strategy: explicitStrategy,
413
+ config: `${explicitStrategy}:research`,
414
+ selectedBy: "strategy"
415
+ };
416
+ }
417
+ const availableStrategies = await listRuntimeStrategyNames(userName);
418
+ const candidates = [];
419
+ for (const strategy of availableStrategies) {
420
+ const config = `${strategy}:research`;
421
+ const latestRun = await (0, import_redis.getData)(
422
+ import_redis.redisKeys.researchLatestRun(userName, strategy),
423
+ null
424
+ );
425
+ const lastFinishedAt = latestRun?.finishedAt ? Date.parse(latestRun.finishedAt) : 0;
426
+ candidates.push({
427
+ strategy,
428
+ config,
429
+ lastFinishedAt: Number.isFinite(lastFinishedAt) ? lastFinishedAt : 0
430
+ });
431
+ }
432
+ if (!candidates.length) {
433
+ throw new Error(
434
+ `No research candidates found from runtime strategy configs for user ${userName}.`
435
+ );
436
+ }
437
+ candidates.sort(
438
+ (left, right) => left.lastFinishedAt - right.lastFinishedAt || left.strategy.localeCompare(right.strategy)
439
+ );
440
+ logResearch(
441
+ `auto candidates from runtime configs: ${candidates.map(
442
+ ({ strategy, lastFinishedAt }) => `${strategy}=${lastFinishedAt ? new Date(lastFinishedAt).toISOString() : "never"}`
443
+ ).join(", ")}`
444
+ );
445
+ return {
446
+ strategy: candidates[0].strategy,
447
+ config: candidates[0].config,
448
+ selectedBy: "auto"
449
+ };
450
+ };
451
+ var parseJsonOutput = (stdout, label) => {
452
+ const trimmed = stdout.trim();
453
+ if (!trimmed) {
454
+ throw new Error(`${label} returned empty stdout`);
455
+ }
456
+ return JSON.parse(trimmed);
457
+ };
458
+ var logResearch = (message) => {
459
+ import_logger.logger.info(import_chalk.default.cyan(`[research:auto] ${message}`));
460
+ };
461
+ var executeNonBlockingStep = async (run, stepName, command, commandArgs, options = {}) => {
462
+ logResearch(
463
+ `starting optional step ${stepName}: ${command} ${commandArgs.join(" ")}`.trim()
464
+ );
465
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
466
+ run.steps[stepName] = {
467
+ status: "running",
468
+ command,
469
+ args: commandArgs,
470
+ startedAt
471
+ };
472
+ await saveRun(run);
473
+ const result = await runCliCommand({
474
+ command,
475
+ args: commandArgs,
476
+ node8g: options.node8g,
477
+ liveLogPrefix: options.liveLog ? stepName : void 0,
478
+ heartbeatMs: options.liveLog ? options.heartbeatMs || 6e4 : void 0,
479
+ captureMode: options.captureMode
480
+ });
481
+ run.steps[stepName] = {
482
+ status: result.exitCode === 0 ? "completed" : "failed",
483
+ command,
484
+ args: commandArgs,
485
+ startedAt,
486
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
487
+ durationMs: result.durationMs,
488
+ exitCode: result.exitCode,
489
+ stdoutTail: trimTextTail(result.stdout),
490
+ stderrTail: trimTextTail(result.stderr)
491
+ };
492
+ await saveRun(run);
493
+ logResearch(
494
+ `finished optional step ${stepName}: exit=${result.exitCode}, duration=${result.durationMs}ms`
495
+ );
496
+ return result;
497
+ };
498
+ var main = async () => {
499
+ const userName = String(flags.user || "root").trim() || "root";
500
+ const connector = String(flags.connector || "bybit").trim() || "bybit";
501
+ const timeframe = String(flags.timeframe || "15").trim() || "15";
502
+ const days = Math.max(1, Number.parseInt(String(flags.days ?? 45), 10) || 45);
503
+ const recent = Math.max(
504
+ 0,
505
+ Number.parseInt(String(flags.recent ?? 1e3), 10) || 1e3
506
+ );
507
+ const skip = Math.max(0, Number.parseInt(String(flags.skip ?? 0), 10) || 0);
508
+ const minQuality = Math.max(
509
+ 0,
510
+ Number.parseInt(String(flags.minQuality ?? 4), 10) || 4
511
+ );
512
+ const jsonOutput = Boolean(flags.json);
513
+ const skipAgent = Boolean(flags.skipAgent);
514
+ const outDirArg = String(flags.outDir || "data/ai/export").trim() || "data/ai/export";
515
+ const outDir = import_node_path.default.resolve(projectRoot, outDirArg);
516
+ const cleanDirArg = import_node_path.default.relative(import_node_path.default.join(projectRoot, "data"), outDir).replace(/\\/g, "/");
517
+ const target = await resolveTarget();
518
+ const runId = `${Date.now()}-${(0, import_ai.toFileToken)(target.strategy)}`;
519
+ const run = {
520
+ runId,
521
+ userName,
522
+ strategy: target.strategy,
523
+ config: target.config,
524
+ connector,
525
+ timeframe,
526
+ days,
527
+ recent,
528
+ skip,
529
+ minQuality,
530
+ selectedBy: target.selectedBy,
531
+ status: "running",
532
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
533
+ steps: createEmptySteps(),
534
+ artifacts: {}
535
+ };
536
+ const executeStep = async (stepName, command, commandArgs, options = {}) => {
537
+ logResearch(
538
+ `starting step ${stepName}: ${command} ${commandArgs.join(" ")}`.trim()
539
+ );
540
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
541
+ run.steps[stepName] = {
542
+ status: "running",
543
+ command,
544
+ args: commandArgs,
545
+ startedAt
546
+ };
547
+ await saveRun(run);
548
+ const result = await runCliCommand({
549
+ command,
550
+ args: commandArgs,
551
+ node8g: options.node8g,
552
+ liveLogPrefix: options.liveLog ? stepName : void 0,
553
+ heartbeatMs: options.liveLog ? options.heartbeatMs || 6e4 : void 0,
554
+ captureMode: options.captureMode
555
+ });
556
+ run.steps[stepName] = {
557
+ status: result.exitCode === 0 ? "completed" : "failed",
558
+ command,
559
+ args: commandArgs,
560
+ startedAt,
561
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
562
+ durationMs: result.durationMs,
563
+ exitCode: result.exitCode,
564
+ stdoutTail: trimTextTail(result.stdout),
565
+ stderrTail: trimTextTail(result.stderr)
566
+ };
567
+ await saveRun(run);
568
+ if (result.exitCode !== 0) {
569
+ logResearch(
570
+ `step ${stepName} failed: exit=${result.exitCode}, output=${trimTextTail(
571
+ result.stderr || result.stdout,
572
+ 400
573
+ )}`
574
+ );
575
+ throw new Error(
576
+ `${stepName} failed (exitCode=${result.exitCode}): ${trimTextTail(
577
+ result.stderr || result.stdout,
578
+ 1200
579
+ )}`
580
+ );
581
+ }
582
+ logResearch(
583
+ `finished step ${stepName}: exit=${result.exitCode}, duration=${result.durationMs}ms`
584
+ );
585
+ return result;
586
+ };
587
+ const prepareBacktestConfig = async () => {
588
+ logResearch(
589
+ `preparing research backtest config ${target.config} from runtime strategy ${target.strategy}`
590
+ );
591
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
592
+ run.steps.prepareBacktestConfig = {
593
+ status: "running",
594
+ command: "prepare-backtest-config",
595
+ args: ["--strategy", target.strategy, "--config", target.config],
596
+ startedAt
597
+ };
598
+ await saveRun(run);
599
+ const strategyConfigKey = import_redis.redisKeys.strategyConfig(
600
+ userName,
601
+ target.strategy
602
+ );
603
+ const backtestConfigKey = import_redis.redisKeys.backtestConfig(userName, target.config);
604
+ const defaults = (0, import_strategies.getBuiltInStrategyDefaultConfig)(target.strategy) || {};
605
+ const userStrategyConfig = await (0, import_redis.getData)(
606
+ strategyConfigKey,
607
+ {}
608
+ );
609
+ const strategyConfig = {
610
+ ...defaults,
611
+ ...userStrategyConfig
612
+ };
613
+ if (!Object.keys(strategyConfig).length) {
614
+ throw new Error(
615
+ `Strategy config "${target.strategy}" not found in Redis and no built-in defaults available`
616
+ );
617
+ }
618
+ const backtestConfig = toStrategyConfigGrid(strategyConfig);
619
+ await (0, import_redis.setData)(backtestConfigKey, backtestConfig, { expire: 0 });
620
+ logResearch(
621
+ `stored backtest config at ${backtestConfigKey} with ${Object.keys(strategyConfig).length} top-level fields`
622
+ );
623
+ run.artifacts.strategyConfigKey = strategyConfigKey;
624
+ run.artifacts.backtestConfigKey = backtestConfigKey;
625
+ run.artifacts.strategyConfig = strategyConfig;
626
+ run.artifacts.backtestConfig = backtestConfig;
627
+ run.steps.prepareBacktestConfig = {
628
+ status: "completed",
629
+ command: "prepare-backtest-config",
630
+ args: ["--strategy", target.strategy, "--config", target.config],
631
+ startedAt,
632
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
633
+ durationMs: Date.now() - Date.parse(startedAt)
634
+ };
635
+ await saveRun(run);
636
+ };
637
+ const sendRunReport = async () => {
638
+ const message = buildTelegramReport(run);
639
+ await sendTelegramReport(message, { userName });
640
+ };
641
+ await saveRun(run);
642
+ try {
643
+ logResearch(
644
+ `run ${runId} selected target strategy=${target.strategy}, config=${target.config}, selectedBy=${target.selectedBy}`
645
+ );
646
+ const backtestKeysBefore = new Set(
647
+ await (0, import_redis.getKeys)(getBacktestResultsPrefix(userName, target.config))
648
+ );
649
+ const aiFilesBefore = new Set(
650
+ await listMergedFiles(outDir, target.strategy)
651
+ );
652
+ logResearch(
653
+ `baseline artifacts before run: backtests=${backtestKeysBefore.size}, aiMergedFiles=${aiFilesBefore.size}`
654
+ );
655
+ await prepareBacktestConfig();
656
+ await executeStep("cleanTests", "clean-tests", ["--user", userName]);
657
+ await executeStep("cleanAiExport", "clean-dir", [
658
+ "--dir",
659
+ cleanDirArg.startsWith("..") ? "ai/export" : cleanDirArg
660
+ ]);
661
+ await executeStep(
662
+ "backtest",
663
+ "backtest",
664
+ [
665
+ "--user",
666
+ userName,
667
+ "--config",
668
+ target.config,
669
+ "--connector",
670
+ connector,
671
+ "--timeframe",
672
+ timeframe,
673
+ "--days",
674
+ String(days),
675
+ "--ai"
676
+ ],
677
+ { node8g: true, liveLog: true, heartbeatMs: 3e4 }
678
+ );
679
+ await executeStep("aiExport", "ai-export", [
680
+ "--strategy",
681
+ target.strategy,
682
+ "--outDir",
683
+ outDirArg
684
+ ]);
685
+ const aiTrainResult = await executeStep(
686
+ "aiTrainLocal",
687
+ "ai-train",
688
+ [
689
+ "--strategy",
690
+ target.strategy,
691
+ "--recent",
692
+ String(recent),
693
+ "--skip",
694
+ String(skip),
695
+ "--minQuality",
696
+ String(minQuality),
697
+ "--localOnly",
698
+ "--json"
699
+ ],
700
+ { node8g: true, captureMode: "full" }
701
+ );
702
+ const backtestKeysAfter = await (0, import_redis.getKeys)(
703
+ getBacktestResultsPrefix(userName, target.config)
704
+ );
705
+ const newBacktestKeys = backtestKeysAfter.filter((key) => !backtestKeysBefore.has(key)).sort();
706
+ const backtestResultKey = newBacktestKeys[newBacktestKeys.length - 1] || backtestKeysAfter.sort().at(-1);
707
+ if (backtestResultKey) {
708
+ logResearch(`detected backtest result key ${backtestResultKey}`);
709
+ const backtestResult = await (0, import_redis.getData)(backtestResultKey, null);
710
+ run.artifacts.backtestResultKey = backtestResultKey;
711
+ run.artifacts.backtestResult = backtestResult ? {
712
+ finishedAt: backtestResult.finishedAt,
713
+ durationSeconds: backtestResult.durationSeconds,
714
+ successTests: backtestResult.successTests,
715
+ errorTests: backtestResult.errorTests,
716
+ bestConfig: backtestResult.bestConfig,
717
+ mergedConfig: backtestResult.mergedConfig
718
+ } : void 0;
719
+ } else {
720
+ logResearch("no backtest result key detected after backtest step");
721
+ }
722
+ const aiFilesAfter = await listMergedFiles(outDir, target.strategy);
723
+ const newAiFiles = aiFilesAfter.filter((filePath) => !aiFilesBefore.has(filePath)).sort();
724
+ run.artifacts.aiExportFile = newAiFiles[newAiFiles.length - 1] || aiFilesAfter.at(-1);
725
+ if (run.artifacts.aiExportFile) {
726
+ logResearch(`detected AI export file ${run.artifacts.aiExportFile}`);
727
+ } else {
728
+ logResearch("no AI export file detected after ai-export step");
729
+ }
730
+ run.artifacts.aiTrainLocal = parseJsonOutput(
731
+ aiTrainResult.stdout,
732
+ "ai-train --json"
733
+ );
734
+ logResearch("parsed ai-train --json output successfully");
735
+ run.status = "completed";
736
+ run.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
737
+ await saveRun(run);
738
+ logResearch(`run ${runId} completed successfully`);
739
+ await sendRunReport();
740
+ logResearch(`TG report sent for run ${runId}`);
741
+ if (!skipAgent) {
742
+ logResearch(`starting direct agent invocation for run ${runId}`);
743
+ const agentResult = await executeNonBlockingStep(
744
+ run,
745
+ "agentRun",
746
+ "agent-run",
747
+ [
748
+ "--user",
749
+ userName,
750
+ "--runId",
751
+ run.runId,
752
+ "--strategy",
753
+ run.strategy,
754
+ "--json"
755
+ ],
756
+ {
757
+ node8g: true,
758
+ liveLog: true,
759
+ heartbeatMs: 3e4,
760
+ captureMode: "full"
761
+ }
762
+ );
763
+ try {
764
+ run.artifacts.agentRun = parseJsonOutput(
765
+ agentResult.stdout,
766
+ "agent-run --json"
767
+ );
768
+ } catch (error) {
769
+ run.artifacts.agentRun = {
770
+ status: "failed",
771
+ error: error?.message || String(error),
772
+ stdout: trimTextTail(agentResult.stdout),
773
+ stderr: trimTextTail(agentResult.stderr)
774
+ };
775
+ }
776
+ await saveRun(run);
777
+ logResearch(`agent invocation finished for run ${runId}`);
778
+ await sendRunReport();
779
+ logResearch(`updated TG report sent after agent for run ${runId}`);
780
+ }
781
+ if (jsonOutput) {
782
+ console.log(JSON.stringify(run));
783
+ process.exit(0);
784
+ }
785
+ console.log(import_chalk.default.green("Research auto finished"));
786
+ console.log(
787
+ import_chalk.default.gray(
788
+ `strategy=${run.strategy}, config=${run.config}, runId=${run.runId}`
789
+ )
790
+ );
791
+ if (run.artifacts.backtestResultKey) {
792
+ console.log(
793
+ import_chalk.default.gray(`backtestResultKey=${run.artifacts.backtestResultKey}`)
794
+ );
795
+ }
796
+ if (run.artifacts.aiExportFile) {
797
+ console.log(import_chalk.default.gray(`aiExportFile=${run.artifacts.aiExportFile}`));
798
+ }
799
+ process.exit(0);
800
+ } catch (error) {
801
+ run.status = "failed";
802
+ run.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
803
+ run.error = error?.message || String(error);
804
+ await saveRun(run);
805
+ logResearch(`run ${runId} failed: ${run.error}`);
806
+ try {
807
+ await sendRunReport();
808
+ logResearch(`TG report sent for failed run ${runId}`);
809
+ } catch (reportError) {
810
+ const suffix = reportError?.message || String(reportError);
811
+ run.error = `${run.error}
812
+ Telegram report failed: ${suffix}`;
813
+ await saveRun(run);
814
+ logResearch(`TG report failed for run ${runId}: ${suffix}`);
815
+ }
816
+ if (jsonOutput) {
817
+ console.log(JSON.stringify(run));
818
+ } else {
819
+ console.error(import_chalk.default.red(run.error));
820
+ }
821
+ process.exit(1);
822
+ }
823
+ };
824
+ if (require.main === module) {
825
+ main().catch((error) => {
826
+ console.error(import_chalk.default.red(error?.message || String(error)));
827
+ process.exit(1);
828
+ });
829
+ }
830
+ // Annotate the CommonJS export names for ESM import in node:
831
+ 0 && (module.exports = {
832
+ buildTelegramReport,
833
+ createEmptySteps,
834
+ escapeHtml,
835
+ formatMskDateTime,
836
+ getBacktestResultsPrefix,
837
+ listMergedFiles,
838
+ listRuntimeStrategyNames,
839
+ parseJsonOutput,
840
+ resolveStrategyNameByConfigKey,
841
+ resolveTarget,
842
+ runCliCommand,
843
+ toStrategyConfigGrid,
844
+ trimTextTail
845
+ });