@tradejs/cli 1.0.8 → 1.0.10

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 (76) hide show
  1. package/dist/cli.js +29748 -13641
  2. package/dist/lib/aiPocketSearch.js +1316 -0
  3. package/dist/lib/aiTrainCharts.js +711 -0
  4. package/dist/lib/aiTrainDataset.js +40 -0
  5. package/dist/lib/aiTrainEvaluationDump.js +76 -0
  6. package/dist/lib/aiTrainMetrics.js +148 -8
  7. package/dist/lib/aiTrainOptions.js +130 -0
  8. package/dist/lib/aiTrainQuarantine.js +199 -0
  9. package/dist/lib/aiTrainResearch.js +467 -0
  10. package/dist/lib/binanceMarketContextBackfill.js +841 -0
  11. package/dist/lib/binanceMarketData.js +392 -0
  12. package/dist/lib/coinMarketCapContextBackfill.js +1088 -0
  13. package/dist/lib/continuity.js +60 -0
  14. package/dist/lib/derivativesContextBackfill.js +490 -77
  15. package/dist/lib/executionCalibration.js +705 -0
  16. package/dist/lib/marketContextPrepare.js +4403 -0
  17. package/dist/lib/paritySummary.js +101 -0
  18. package/dist/lib/runBot.js +1 -0
  19. package/dist/lib/runEnvironment.js +2254 -0
  20. package/dist/lib/runFormatting.js +1657 -0
  21. package/dist/lib/runtimeDebugEvidence.js +526 -0
  22. package/dist/lib/runtimeModeConfig.js +53 -0
  23. package/dist/lib/runtimeParity.js +88 -11
  24. package/dist/lib/runtimeParityDetails.js +751 -0
  25. package/dist/lib/runtimeRedis.js +157 -0
  26. package/dist/lib/runtimeSignalsLoader.js +187 -0
  27. package/dist/lib/runtimeSignalsStorage.js +185 -0
  28. package/dist/lib/runtimeStrategyBacktest.js +134 -0
  29. package/dist/lib/runtimeTradeSync.js +263 -0
  30. package/dist/lib/telegramReports.js +9 -0
  31. package/dist/lib/tickerUniverseCache.js +116 -0
  32. package/dist/lib/timeWindow.js +3 -3
  33. package/dist/scripts/agentRun.js +28 -8
  34. package/dist/scripts/aiExport.js +1721 -7
  35. package/dist/scripts/aiExportSelect.js +16 -4
  36. package/dist/scripts/aiPocketSearch.js +4190 -0
  37. package/dist/scripts/aiTrain.js +1866 -87
  38. package/dist/scripts/backtest.js +4509 -818
  39. package/dist/scripts/binanceMarketIngest.js +565 -0
  40. package/dist/scripts/bot.js +16 -2
  41. package/dist/scripts/candlesMigrateProvider.js +32 -22
  42. package/dist/scripts/cleanDir.js +15 -2
  43. package/dist/scripts/cleanRedis.js +15 -2
  44. package/dist/scripts/cleanTests.js +38 -2
  45. package/dist/scripts/cleanupMarketContext.js +69 -0
  46. package/dist/scripts/continuity.js +148 -67
  47. package/dist/scripts/derivativesIngest.js +14 -4
  48. package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
  49. package/dist/scripts/doctor.js +14 -4
  50. package/dist/scripts/executionCalibration.js +792 -0
  51. package/dist/scripts/infraCommon.js +10 -0
  52. package/dist/scripts/infraDown.js +14 -4
  53. package/dist/scripts/infraInit.js +24 -4
  54. package/dist/scripts/infraUp.js +14 -4
  55. package/dist/scripts/marketWs.js +222 -0
  56. package/dist/scripts/migration.js +15 -2
  57. package/dist/scripts/mlExport.js +1698 -10
  58. package/dist/scripts/mlExportSelect.js +16 -4
  59. package/dist/scripts/mlInspect.js +13 -3
  60. package/dist/scripts/mlTrainLatestSelect.js +14 -4
  61. package/dist/scripts/replay.js +8824 -0
  62. package/dist/scripts/replayRunner.js +8827 -0
  63. package/dist/scripts/replayRuntimeEvidence.js +868 -0
  64. package/dist/scripts/researchAuto.js +289 -60
  65. package/dist/scripts/results.js +3 -3
  66. package/dist/scripts/runtimeEvidence.js +605 -0
  67. package/dist/scripts/runtimeParity.js +6360 -1004
  68. package/dist/scripts/serverHealth.js +588 -0
  69. package/dist/scripts/signals.js +5623 -486
  70. package/dist/scripts/signalsDaemon.js +6167 -0
  71. package/dist/scripts/signalsSummary.js +954 -312
  72. package/dist/scripts/test-ml.js +25 -3
  73. package/dist/scripts/test.js +26 -1
  74. package/dist/scripts/user-add.js +15 -2
  75. package/dist/workers/testerWorker.js +130 -21
  76. package/package.json +18 -12
@@ -30,14 +30,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/scripts/researchAuto.ts
31
31
  var researchAuto_exports = {};
32
32
  __export(researchAuto_exports, {
33
+ acquireResearchAutoLock: () => acquireResearchAutoLock,
34
+ buildNodeOptions: () => buildNodeOptions,
33
35
  buildTelegramReport: () => buildTelegramReport,
34
36
  createEmptySteps: () => createEmptySteps,
35
37
  escapeHtml: () => escapeHtml,
36
38
  formatMskDateTime: () => formatMskDateTime,
37
39
  getBacktestResultsPrefix: () => getBacktestResultsPrefix,
40
+ getResearchAutoLockPath: () => getResearchAutoLockPath,
41
+ isProcessAlive: () => isProcessAlive,
38
42
  listMergedFiles: () => listMergedFiles,
39
43
  listRuntimeStrategyNames: () => listRuntimeStrategyNames,
44
+ main: () => main,
40
45
  parseJsonOutput: () => parseJsonOutput,
46
+ releaseResearchAutoLock: () => releaseResearchAutoLock,
47
+ resolveHeapMbFromEnv: () => resolveHeapMbFromEnv,
41
48
  resolveStrategyNameByConfigKey: () => resolveStrategyNameByConfigKey,
42
49
  resolveTarget: () => resolveTarget,
43
50
  runCliCommand: () => runCliCommand,
@@ -46,15 +53,83 @@ __export(researchAuto_exports, {
46
53
  });
47
54
  module.exports = __toCommonJS(researchAuto_exports);
48
55
  var import_args = __toESM(require("args"));
49
- var import_chalk = __toESM(require("chalk"));
56
+ var import_chalk2 = __toESM(require("chalk"));
50
57
  var import_node_child_process = require("child_process");
51
58
  var import_promises = __toESM(require("fs/promises"));
59
+ var import_node_os = __toESM(require("os"));
52
60
  var import_node_path = __toESM(require("path"));
61
+ var import_constants = require("@tradejs/core/constants");
53
62
  var import_ai = require("@tradejs/infra/ai");
54
63
  var import_logger = require("@tradejs/infra/logger");
55
- var import_redis = require("@tradejs/infra/redis");
64
+ var import_redis2 = require("@tradejs/infra/redis");
56
65
  var import_strategies = require("@tradejs/strategies");
57
66
 
67
+ // src/lib/runtimeRedis.ts
68
+ var import_redis = require("@tradejs/infra/redis");
69
+ var import_time = require("@tradejs/core/time");
70
+ var RESERVED_STRATEGY_NAMES = /* @__PURE__ */ new Set(["charts"]);
71
+ var RESERVED_CONFIG_IDS = /* @__PURE__ */ new Set(["results"]);
72
+ var resolveStrategyConfigIdentityByKey = (userName, key) => {
73
+ const parts = key.split(":");
74
+ if (parts.length !== 5) return null;
75
+ const [users, keyUserName, strategiesKey, strategyName, configId] = parts;
76
+ if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || !strategyName || !configId || RESERVED_STRATEGY_NAMES.has(strategyName) || RESERVED_CONFIG_IDS.has(configId) || !/^[a-zA-Z0-9_-]+$/.test(configId)) {
77
+ return null;
78
+ }
79
+ return { strategyName, configId };
80
+ };
81
+ var isRuntimeStrategyEnabled = (strategyConfig) => {
82
+ const enabled = strategyConfig.ENABLE;
83
+ return enabled !== false;
84
+ };
85
+ var resolveStrategyNameByConfigKey = (userName, key) => {
86
+ return resolveStrategyConfigIdentityByKey(userName, key)?.strategyName ?? null;
87
+ };
88
+ var getRuntimeStrategyConfigKeys = async (userName) => {
89
+ const keys = await (0, import_redis.getKeys)(`${import_redis.redisKeys.strategies(userName)}:`);
90
+ return keys.filter((key) => resolveStrategyConfigIdentityByKey(userName, key)).sort((left, right) => left.localeCompare(right));
91
+ };
92
+ var loadRuntimeStrategyConfigs = async (userName, {
93
+ onInvalidConfig
94
+ } = {}) => {
95
+ const configKeys = await getRuntimeStrategyConfigKeys(userName);
96
+ const strategyConfigs = await Promise.all(
97
+ configKeys.map(async (key) => {
98
+ const strategyName = resolveStrategyNameByConfigKey(userName, key);
99
+ const identity = resolveStrategyConfigIdentityByKey(userName, key);
100
+ if (!strategyName || !identity) {
101
+ return null;
102
+ }
103
+ const strategyConfig = await (0, import_redis.getData)(
104
+ key,
105
+ null
106
+ );
107
+ if (!strategyConfig || typeof strategyConfig !== "object" || Array.isArray(strategyConfig)) {
108
+ onInvalidConfig?.(key);
109
+ return null;
110
+ }
111
+ return {
112
+ key,
113
+ strategyName,
114
+ configId: identity.configId,
115
+ strategyConfig
116
+ };
117
+ })
118
+ );
119
+ return strategyConfigs.filter(Boolean);
120
+ };
121
+
122
+ // src/lib/runtimeStrategyBacktest.ts
123
+ var import_chalk = __toESM(require("chalk"));
124
+ var BACKTEST_CLI_RUNTIME_CONFIG_KEYS = /* @__PURE__ */ new Set([
125
+ "ENV",
126
+ "INTERVAL",
127
+ "MAKE_ORDERS",
128
+ "CLOSE_OPPOSITE_POSITIONS",
129
+ "BACKTEST_PRICE_MODE",
130
+ "BACKTEST_ENTRY_DELAY_BARS"
131
+ ]);
132
+
58
133
  // src/lib/telegramReports.ts
59
134
  var import_cli = require("@tradejs/node/cli");
60
135
  var TELEGRAM_REPORT_CHUNK_LIMIT = 3900;
@@ -99,8 +174,16 @@ ${line}` : line;
99
174
  };
100
175
  var sendTelegramReport = async (message, options = {}) => {
101
176
  const chunks = splitTelegramReport(message);
177
+ const sendAttachments = async () => {
178
+ for (const attachment of options.attachments || []) {
179
+ await (0, import_cli.sendDocumentToTG)(attachment, {
180
+ userName: options.userName
181
+ });
182
+ }
183
+ };
102
184
  if (chunks.length === 1) {
103
185
  await (0, import_cli.sendTextToTG)(chunks[0], options);
186
+ await sendAttachments();
104
187
  return chunks.length;
105
188
  }
106
189
  for (const [index, chunk] of chunks.entries()) {
@@ -112,6 +195,7 @@ ${chunk}`, {
112
195
  markup: index === chunks.length - 1 ? options.markup : void 0
113
196
  });
114
197
  }
198
+ await sendAttachments();
115
199
  return chunks.length;
116
200
  };
117
201
 
@@ -127,16 +211,150 @@ import_args.default.option(["o", "connector"], "Backtest connector", "bybit");
127
211
  import_args.default.option(["f", "timeframe"], "Backtest timeframe", "15");
128
212
  import_args.default.option(["d", "days"], "Backtest days window", 45);
129
213
  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);
214
+ import_args.default.option(["k", "skip"], "AI train skip rows from end", 0);
215
+ import_args.default.option(
216
+ ["M", "minQuality"],
217
+ "Minimum AI quality required to approve entry",
218
+ 4
219
+ );
220
+ import_args.default.option(["O", "outDir"], "AI export output directory", "data/ai/export");
221
+ import_args.default.option(["A", "skipAgent"], "Skip direct research agent invocation", false);
222
+ import_args.default.option(["j", "json"], "Print structured JSON summary", false);
135
223
  var flags = import_args.default.parse(process.argv);
136
224
  var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
137
225
  var dotenvPath = String(
138
226
  process.env.DOTENV_CONFIG_PATH || import_node_path.default.join(projectRoot, ".env")
139
227
  ).trim() || import_node_path.default.join(projectRoot, ".env");
228
+ var getResearchAutoLockPath = (userName) => import_node_path.default.join(
229
+ projectRoot,
230
+ "data",
231
+ "cache",
232
+ "research-auto",
233
+ `${(0, import_ai.toFileToken)(userName)}.lock.json`
234
+ );
235
+ var readResearchAutoLock = async (lockPath) => {
236
+ try {
237
+ const raw = await import_promises.default.readFile(lockPath, "utf8");
238
+ return JSON.parse(raw);
239
+ } catch {
240
+ return null;
241
+ }
242
+ };
243
+ var isProcessAlive = (pid) => {
244
+ if (!Number.isInteger(pid) || pid <= 0) {
245
+ return false;
246
+ }
247
+ try {
248
+ process.kill(pid, 0);
249
+ return true;
250
+ } catch (error) {
251
+ const code = error && typeof error === "object" && "code" in error ? String(error.code || "") : "";
252
+ return code === "EPERM";
253
+ }
254
+ };
255
+ var acquireResearchAutoLock = async ({
256
+ runId,
257
+ userName,
258
+ strategy,
259
+ startedAt
260
+ }) => {
261
+ const lockPath = getResearchAutoLockPath(userName);
262
+ const nextLock = {
263
+ runId,
264
+ userName,
265
+ strategy,
266
+ pid: process.pid,
267
+ ppid: process.ppid,
268
+ hostname: import_node_os.default.hostname(),
269
+ startedAt,
270
+ argv: [...process.argv]
271
+ };
272
+ await import_promises.default.mkdir(import_node_path.default.dirname(lockPath), { recursive: true });
273
+ const writeLock = async () => {
274
+ await import_promises.default.writeFile(lockPath, JSON.stringify(nextLock, null, 2), {
275
+ flag: "wx"
276
+ });
277
+ };
278
+ try {
279
+ await writeLock();
280
+ return lockPath;
281
+ } catch (error) {
282
+ const code = error && typeof error === "object" && "code" in error ? String(error.code || "") : "";
283
+ if (code !== "EEXIST") {
284
+ throw error;
285
+ }
286
+ }
287
+ const existingLock = await readResearchAutoLock(lockPath);
288
+ if (existingLock?.pid && isProcessAlive(existingLock.pid)) {
289
+ throw new Error(
290
+ `research:auto is already running for user ${userName} (runId=${existingLock.runId}, strategy=${existingLock.strategy}, pid=${existingLock.pid}, ppid=${existingLock.ppid}, startedAt=${existingLock.startedAt}, host=${existingLock.hostname})`
291
+ );
292
+ }
293
+ await import_promises.default.unlink(lockPath).catch(() => void 0);
294
+ await writeLock();
295
+ return lockPath;
296
+ };
297
+ var releaseResearchAutoLock = async (lockPath) => {
298
+ if (!lockPath) {
299
+ return;
300
+ }
301
+ const existingLock = await readResearchAutoLock(lockPath);
302
+ if (existingLock && existingLock.pid !== process.pid) {
303
+ return;
304
+ }
305
+ await import_promises.default.unlink(lockPath).catch(() => void 0);
306
+ };
307
+ var resolvePositiveInteger = (value, fallback) => {
308
+ const parsed = Number.parseInt(String(value ?? ""), 10);
309
+ if (Number.isInteger(parsed) && parsed > 0) {
310
+ return parsed;
311
+ }
312
+ return fallback;
313
+ };
314
+ var resolveHeapMbFromEnv = (envNames, fallback) => {
315
+ for (const envName of envNames) {
316
+ const resolved = resolvePositiveInteger(process.env[envName], void 0);
317
+ if (resolved != null) {
318
+ return resolved;
319
+ }
320
+ }
321
+ return fallback;
322
+ };
323
+ var buildNodeOptions = ({
324
+ heapMb,
325
+ baseNodeOptions = String(process.env.NODE_OPTIONS || "").trim()
326
+ }) => [
327
+ heapMb ? `--max-old-space-size=${Math.max(256, heapMb)}` : "",
328
+ ...baseNodeOptions.split(/\s+/).map((value) => value.trim()).filter((value) => value && !value.startsWith("--max-old-space-size="))
329
+ ].filter(Boolean).join(" ");
330
+ var RESEARCH_AUTO_PARENT_HEAP_MB = resolveHeapMbFromEnv(
331
+ ["RESEARCH_AUTO_PARENT_HEAP_MB"],
332
+ 1024
333
+ );
334
+ var RESEARCH_AUTO_BACKTEST_PARENT_HEAP_MB = resolveHeapMbFromEnv(
335
+ [
336
+ "RESEARCH_AUTO_BACKTEST_PARENT_HEAP_MB",
337
+ "BACKTEST_PARENT_HEAP_MB",
338
+ "CLI_PARENT_HEAP_MB"
339
+ ],
340
+ 1536
341
+ );
342
+ var RESEARCH_AUTO_AI_TRAIN_PARENT_HEAP_MB = resolveHeapMbFromEnv(
343
+ [
344
+ "RESEARCH_AUTO_AI_TRAIN_PARENT_HEAP_MB",
345
+ "AI_TRAIN_PARENT_HEAP_MB",
346
+ "CLI_PARENT_HEAP_MB"
347
+ ],
348
+ 2048
349
+ );
350
+ var RESEARCH_AUTO_AGENT_PARENT_HEAP_MB = resolveHeapMbFromEnv(
351
+ [
352
+ "RESEARCH_AUTO_AGENT_PARENT_HEAP_MB",
353
+ "AGENT_RUN_PARENT_HEAP_MB",
354
+ "CLI_PARENT_HEAP_MB"
355
+ ],
356
+ 2048
357
+ );
140
358
  var trimTextTail = (value, limit = 4e3) => value.length <= limit ? value : value.slice(-limit);
141
359
  var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
142
360
  var createEmptySteps = () => ({
@@ -177,11 +395,11 @@ var createEmptySteps = () => ({
177
395
  }
178
396
  });
179
397
  var saveRun = async (run) => {
180
- await (0, import_redis.setData)(import_redis.redisKeys.researchRun(run.userName, run.runId), run, {
181
- expire: 0
398
+ await (0, import_redis2.setData)(import_redis2.redisKeys.researchRun(run.userName, run.runId), run, {
399
+ expire: import_constants.TTL_1M
182
400
  });
183
- await (0, import_redis.setData)(import_redis.redisKeys.researchLatestRun(run.userName, run.strategy), run, {
184
- expire: 0
401
+ await (0, import_redis2.setData)(import_redis2.redisKeys.researchLatestRun(run.userName, run.strategy), run, {
402
+ expire: import_constants.TTL_1M
185
403
  });
186
404
  };
187
405
  var listMergedFiles = async (outDir, strategyName) => {
@@ -194,24 +412,13 @@ var listMergedFiles = async (outDir, strategyName) => {
194
412
  }
195
413
  };
196
414
  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
415
  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));
416
+ const strategyConfigs = await loadRuntimeStrategyConfigs(userName);
417
+ return strategyConfigs.filter(({ strategyConfig }) => isRuntimeStrategyEnabled(strategyConfig)).map(({ strategyName }) => strategyName).sort((left, right) => left.localeCompare(right));
211
418
  };
212
419
  var toStrategyConfigGrid = (strategyConfig) => {
213
420
  return Object.fromEntries(
214
- Object.entries(strategyConfig).map(([key, value]) => [key, [value]])
421
+ Object.entries(strategyConfig).filter(([key]) => !BACKTEST_CLI_RUNTIME_CONFIG_KEYS.has(key)).map(([key, value]) => [key, [value]])
215
422
  );
216
423
  };
217
424
  var formatMskDateTime = (value) => {
@@ -320,17 +527,16 @@ var runCliCommand = async (params) => {
320
527
  ...process.env,
321
528
  PROJECT_CWD: projectRoot,
322
529
  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(" ")
530
+ ...params.nodeHeapMb ? {
531
+ NODE_OPTIONS: buildNodeOptions({
532
+ heapMb: params.nodeHeapMb
533
+ })
328
534
  } : {}
329
535
  };
330
536
  return new Promise((resolve, reject) => {
331
537
  const child = (0, import_node_child_process.spawn)(
332
538
  "bash",
333
- ["./bin/run-cli-dist.sh", params.command, ...params.args],
539
+ ["./bin/run-cli-runtime.sh", params.command, ...params.args],
334
540
  {
335
541
  cwd: projectRoot,
336
542
  env,
@@ -350,14 +556,14 @@ var runCliCommand = async (params) => {
350
556
  const emitLines = (source, text) => {
351
557
  const prefix = params.liveLogPrefix ? `[research:auto:${params.liveLogPrefix}:${source}] ` : `[research:auto:${source}] `;
352
558
  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}`));
559
+ import_logger.logger.info(import_chalk2.default.gray(`${prefix}${line}`));
354
560
  }
355
561
  };
356
562
  if (params.heartbeatMs && params.heartbeatMs > 0) {
357
563
  heartbeatTimer = setInterval(() => {
358
564
  const elapsedMs = Date.now() - startedAt;
359
565
  import_logger.logger.info(
360
- import_chalk.default.gray(
566
+ import_chalk2.default.gray(
361
567
  `[research:auto:${params.liveLogPrefix || params.command}] still running after ${Math.round(elapsedMs / 1e3)}s`
362
568
  )
363
569
  );
@@ -418,8 +624,8 @@ var resolveTarget = async () => {
418
624
  const candidates = [];
419
625
  for (const strategy of availableStrategies) {
420
626
  const config = `${strategy}:research`;
421
- const latestRun = await (0, import_redis.getData)(
422
- import_redis.redisKeys.researchLatestRun(userName, strategy),
627
+ const latestRun = await (0, import_redis2.getData)(
628
+ import_redis2.redisKeys.researchLatestRun(userName, strategy),
423
629
  null
424
630
  );
425
631
  const lastFinishedAt = latestRun?.finishedAt ? Date.parse(latestRun.finishedAt) : 0;
@@ -456,7 +662,7 @@ var parseJsonOutput = (stdout, label) => {
456
662
  return JSON.parse(trimmed);
457
663
  };
458
664
  var logResearch = (message) => {
459
- import_logger.logger.info(import_chalk.default.cyan(`[research:auto] ${message}`));
665
+ import_logger.logger.info(import_chalk2.default.cyan(`[research:auto] ${message}`));
460
666
  };
461
667
  var executeNonBlockingStep = async (run, stepName, command, commandArgs, options = {}) => {
462
668
  logResearch(
@@ -473,7 +679,7 @@ var executeNonBlockingStep = async (run, stepName, command, commandArgs, options
473
679
  const result = await runCliCommand({
474
680
  command,
475
681
  args: commandArgs,
476
- node8g: options.node8g,
682
+ nodeHeapMb: options.nodeHeapMb,
477
683
  liveLogPrefix: options.liveLog ? stepName : void 0,
478
684
  heartbeatMs: options.liveLog ? options.heartbeatMs || 6e4 : void 0,
479
685
  captureMode: options.captureMode
@@ -533,6 +739,7 @@ var main = async () => {
533
739
  steps: createEmptySteps(),
534
740
  artifacts: {}
535
741
  };
742
+ let lockPath = null;
536
743
  const executeStep = async (stepName, command, commandArgs, options = {}) => {
537
744
  logResearch(
538
745
  `starting step ${stepName}: ${command} ${commandArgs.join(" ")}`.trim()
@@ -548,7 +755,7 @@ var main = async () => {
548
755
  const result = await runCliCommand({
549
756
  command,
550
757
  args: commandArgs,
551
- node8g: options.node8g,
758
+ nodeHeapMb: options.nodeHeapMb,
552
759
  liveLogPrefix: options.liveLog ? stepName : void 0,
553
760
  heartbeatMs: options.liveLog ? options.heartbeatMs || 6e4 : void 0,
554
761
  captureMode: options.captureMode
@@ -596,13 +803,13 @@ var main = async () => {
596
803
  startedAt
597
804
  };
598
805
  await saveRun(run);
599
- const strategyConfigKey = import_redis.redisKeys.strategyConfig(
806
+ const strategyConfigKey = import_redis2.redisKeys.strategyConfig(
600
807
  userName,
601
808
  target.strategy
602
809
  );
603
- const backtestConfigKey = import_redis.redisKeys.backtestConfig(userName, target.config);
810
+ const backtestConfigKey = import_redis2.redisKeys.backtestConfig(userName, target.config);
604
811
  const defaults = (0, import_strategies.getBuiltInStrategyDefaultConfig)(target.strategy) || {};
605
- const userStrategyConfig = await (0, import_redis.getData)(
812
+ const userStrategyConfig = await (0, import_redis2.getData)(
606
813
  strategyConfigKey,
607
814
  {}
608
815
  );
@@ -616,7 +823,7 @@ var main = async () => {
616
823
  );
617
824
  }
618
825
  const backtestConfig = toStrategyConfigGrid(strategyConfig);
619
- await (0, import_redis.setData)(backtestConfigKey, backtestConfig, { expire: 0 });
826
+ await (0, import_redis2.setData)(backtestConfigKey, backtestConfig, { expire: 0 });
620
827
  logResearch(
621
828
  `stored backtest config at ${backtestConfigKey} with ${Object.keys(strategyConfig).length} top-level fields`
622
829
  );
@@ -638,13 +845,25 @@ var main = async () => {
638
845
  const message = buildTelegramReport(run);
639
846
  await sendTelegramReport(message, { userName });
640
847
  };
641
- await saveRun(run);
642
848
  try {
849
+ lockPath = await acquireResearchAutoLock({
850
+ runId,
851
+ userName,
852
+ strategy: target.strategy,
853
+ startedAt: run.startedAt
854
+ });
855
+ logResearch(
856
+ `run ${runId} acquired lock ${lockPath} (pid=${process.pid}, ppid=${process.ppid}, host=${import_node_os.default.hostname()})`
857
+ );
858
+ await saveRun(run);
643
859
  logResearch(
644
860
  `run ${runId} selected target strategy=${target.strategy}, config=${target.config}, selectedBy=${target.selectedBy}`
645
861
  );
862
+ logResearch(
863
+ `resource knobs: researchAutoHeapMb=${RESEARCH_AUTO_PARENT_HEAP_MB}, backtestParentHeapMb=${RESEARCH_AUTO_BACKTEST_PARENT_HEAP_MB}, aiTrainParentHeapMb=${RESEARCH_AUTO_AI_TRAIN_PARENT_HEAP_MB}, agentParentHeapMb=${RESEARCH_AUTO_AGENT_PARENT_HEAP_MB}, backtestWorkerHeapMb=${process.env.BACKTEST_WORKER_HEAP_MB || "auto"}, backtestMaxParallel=${process.env.BACKTEST_MAX_PARALLEL || "auto"}, klineConcurrency=${process.env.KLINE_CONCURRENCY_LIMIT || "default"}`
864
+ );
646
865
  const backtestKeysBefore = new Set(
647
- await (0, import_redis.getKeys)(getBacktestResultsPrefix(userName, target.config))
866
+ await (0, import_redis2.getKeys)(getBacktestResultsPrefix(userName, target.config))
648
867
  );
649
868
  const aiFilesBefore = new Set(
650
869
  await listMergedFiles(outDir, target.strategy)
@@ -674,7 +893,11 @@ var main = async () => {
674
893
  String(days),
675
894
  "--ai"
676
895
  ],
677
- { node8g: true, liveLog: true, heartbeatMs: 3e4 }
896
+ {
897
+ nodeHeapMb: RESEARCH_AUTO_BACKTEST_PARENT_HEAP_MB,
898
+ liveLog: true,
899
+ heartbeatMs: 3e4
900
+ }
678
901
  );
679
902
  await executeStep("aiExport", "ai-export", [
680
903
  "--strategy",
@@ -697,16 +920,19 @@ var main = async () => {
697
920
  "--localOnly",
698
921
  "--json"
699
922
  ],
700
- { node8g: true, captureMode: "full" }
923
+ {
924
+ nodeHeapMb: RESEARCH_AUTO_AI_TRAIN_PARENT_HEAP_MB,
925
+ captureMode: "full"
926
+ }
701
927
  );
702
- const backtestKeysAfter = await (0, import_redis.getKeys)(
928
+ const backtestKeysAfter = await (0, import_redis2.getKeys)(
703
929
  getBacktestResultsPrefix(userName, target.config)
704
930
  );
705
931
  const newBacktestKeys = backtestKeysAfter.filter((key) => !backtestKeysBefore.has(key)).sort();
706
932
  const backtestResultKey = newBacktestKeys[newBacktestKeys.length - 1] || backtestKeysAfter.sort().at(-1);
707
933
  if (backtestResultKey) {
708
934
  logResearch(`detected backtest result key ${backtestResultKey}`);
709
- const backtestResult = await (0, import_redis.getData)(backtestResultKey, null);
935
+ const backtestResult = await (0, import_redis2.getData)(backtestResultKey, null);
710
936
  run.artifacts.backtestResultKey = backtestResultKey;
711
937
  run.artifacts.backtestResult = backtestResult ? {
712
938
  finishedAt: backtestResult.finishedAt,
@@ -754,7 +980,7 @@ var main = async () => {
754
980
  "--json"
755
981
  ],
756
982
  {
757
- node8g: true,
983
+ nodeHeapMb: RESEARCH_AUTO_AGENT_PARENT_HEAP_MB,
758
984
  liveLog: true,
759
985
  heartbeatMs: 3e4,
760
986
  captureMode: "full"
@@ -782,19 +1008,19 @@ var main = async () => {
782
1008
  console.log(JSON.stringify(run));
783
1009
  process.exit(0);
784
1010
  }
785
- console.log(import_chalk.default.green("Research auto finished"));
1011
+ console.log(import_chalk2.default.green("Research auto finished"));
786
1012
  console.log(
787
- import_chalk.default.gray(
1013
+ import_chalk2.default.gray(
788
1014
  `strategy=${run.strategy}, config=${run.config}, runId=${run.runId}`
789
1015
  )
790
1016
  );
791
1017
  if (run.artifacts.backtestResultKey) {
792
1018
  console.log(
793
- import_chalk.default.gray(`backtestResultKey=${run.artifacts.backtestResultKey}`)
1019
+ import_chalk2.default.gray(`backtestResultKey=${run.artifacts.backtestResultKey}`)
794
1020
  );
795
1021
  }
796
1022
  if (run.artifacts.aiExportFile) {
797
- console.log(import_chalk.default.gray(`aiExportFile=${run.artifacts.aiExportFile}`));
1023
+ console.log(import_chalk2.default.gray(`aiExportFile=${run.artifacts.aiExportFile}`));
798
1024
  }
799
1025
  process.exit(0);
800
1026
  } catch (error) {
@@ -816,27 +1042,30 @@ Telegram report failed: ${suffix}`;
816
1042
  if (jsonOutput) {
817
1043
  console.log(JSON.stringify(run));
818
1044
  } else {
819
- console.error(import_chalk.default.red(run.error));
1045
+ console.error(import_chalk2.default.red(run.error));
820
1046
  }
821
1047
  process.exit(1);
1048
+ } finally {
1049
+ await releaseResearchAutoLock(lockPath);
822
1050
  }
823
1051
  };
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
1052
  // Annotate the CommonJS export names for ESM import in node:
831
1053
  0 && (module.exports = {
1054
+ acquireResearchAutoLock,
1055
+ buildNodeOptions,
832
1056
  buildTelegramReport,
833
1057
  createEmptySteps,
834
1058
  escapeHtml,
835
1059
  formatMskDateTime,
836
1060
  getBacktestResultsPrefix,
1061
+ getResearchAutoLockPath,
1062
+ isProcessAlive,
837
1063
  listMergedFiles,
838
1064
  listRuntimeStrategyNames,
1065
+ main,
839
1066
  parseJsonOutput,
1067
+ releaseResearchAutoLock,
1068
+ resolveHeapMbFromEnv,
840
1069
  resolveStrategyNameByConfigKey,
841
1070
  resolveTarget,
842
1071
  runCliCommand,
@@ -1629,6 +1629,7 @@ var require_list_it = __commonJS({
1629
1629
  // src/scripts/results.ts
1630
1630
  var results_exports = {};
1631
1631
  __export(results_exports, {
1632
+ main: () => main,
1632
1633
  results: () => results
1633
1634
  });
1634
1635
  module.exports = __toCommonJS(results_exports);
@@ -1924,10 +1925,9 @@ var results = async () => {
1924
1925
  }
1925
1926
  process.exit();
1926
1927
  };
1927
- if (process.env.NODE_ENV !== "test") {
1928
- void results();
1929
- }
1928
+ var main = results;
1930
1929
  // Annotate the CommonJS export names for ESM import in node:
1931
1930
  0 && (module.exports = {
1931
+ main,
1932
1932
  results
1933
1933
  });