@tradejs/cli 1.0.9 → 1.0.11

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 (77) hide show
  1. package/README.md +13 -4
  2. package/dist/cli.js +29531 -13520
  3. package/dist/lib/aiPocketSearch.js +1316 -0
  4. package/dist/lib/aiTrainCharts.js +711 -0
  5. package/dist/lib/aiTrainDataset.js +40 -0
  6. package/dist/lib/aiTrainEvaluationDump.js +76 -0
  7. package/dist/lib/aiTrainMetrics.js +148 -8
  8. package/dist/lib/aiTrainOptions.js +130 -0
  9. package/dist/lib/aiTrainQuarantine.js +199 -0
  10. package/dist/lib/aiTrainResearch.js +467 -0
  11. package/dist/lib/binanceMarketContextBackfill.js +841 -0
  12. package/dist/lib/binanceMarketData.js +392 -0
  13. package/dist/lib/coinMarketCapContextBackfill.js +1126 -0
  14. package/dist/lib/continuity.js +60 -0
  15. package/dist/lib/derivativesContextBackfill.js +490 -77
  16. package/dist/lib/executionCalibration.js +705 -0
  17. package/dist/lib/marketContextPrepare.js +4441 -0
  18. package/dist/lib/paritySummary.js +101 -0
  19. package/dist/lib/runBot.js +1 -0
  20. package/dist/lib/runEnvironment.js +2259 -0
  21. package/dist/lib/runFormatting.js +1657 -0
  22. package/dist/lib/runtimeDebugEvidence.js +526 -0
  23. package/dist/lib/runtimeModeConfig.js +53 -0
  24. package/dist/lib/runtimeParity.js +88 -11
  25. package/dist/lib/runtimeParityDetails.js +751 -0
  26. package/dist/lib/runtimeRedis.js +157 -0
  27. package/dist/lib/runtimeSignalsLoader.js +40 -6
  28. package/dist/lib/runtimeSignalsStorage.js +45 -22
  29. package/dist/lib/runtimeStrategyBacktest.js +134 -0
  30. package/dist/lib/runtimeTradeSync.js +263 -0
  31. package/dist/lib/telegramReports.js +9 -0
  32. package/dist/lib/tickerUniverseCache.js +116 -0
  33. package/dist/lib/timeWindow.js +3 -3
  34. package/dist/scripts/agentRun.js +14 -8
  35. package/dist/scripts/aiExport.js +1721 -7
  36. package/dist/scripts/aiExportSelect.js +16 -4
  37. package/dist/scripts/aiPocketSearch.js +4190 -0
  38. package/dist/scripts/aiTrain.js +1866 -87
  39. package/dist/scripts/backtest.js +4534 -803
  40. package/dist/scripts/binanceMarketIngest.js +565 -0
  41. package/dist/scripts/bot.js +16 -2
  42. package/dist/scripts/candlesMigrateProvider.js +32 -22
  43. package/dist/scripts/cleanDir.js +15 -2
  44. package/dist/scripts/cleanRedis.js +15 -2
  45. package/dist/scripts/cleanTests.js +38 -2
  46. package/dist/scripts/cleanupMarketContext.js +69 -0
  47. package/dist/scripts/continuity.js +148 -67
  48. package/dist/scripts/derivativesIngest.js +14 -4
  49. package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
  50. package/dist/scripts/doctor.js +14 -4
  51. package/dist/scripts/executionCalibration.js +792 -0
  52. package/dist/scripts/infraCommon.js +18 -5
  53. package/dist/scripts/infraDown.js +14 -4
  54. package/dist/scripts/infraInit.js +32 -9
  55. package/dist/scripts/infraUp.js +24 -6
  56. package/dist/scripts/marketWs.js +222 -0
  57. package/dist/scripts/migration.js +15 -2
  58. package/dist/scripts/mlExport.js +1698 -10
  59. package/dist/scripts/mlExportSelect.js +16 -4
  60. package/dist/scripts/mlInspect.js +13 -3
  61. package/dist/scripts/mlTrainLatestSelect.js +14 -4
  62. package/dist/scripts/replay.js +8867 -0
  63. package/dist/scripts/replayRunner.js +8870 -0
  64. package/dist/scripts/replayRuntimeEvidence.js +868 -0
  65. package/dist/scripts/researchAuto.js +286 -59
  66. package/dist/scripts/results.js +3 -3
  67. package/dist/scripts/runtimeEvidence.js +605 -0
  68. package/dist/scripts/runtimeParity.js +6400 -1043
  69. package/dist/scripts/serverHealth.js +588 -0
  70. package/dist/scripts/signals.js +5628 -511
  71. package/dist/scripts/signalsDaemon.js +6205 -0
  72. package/dist/scripts/signalsSummary.js +833 -248
  73. package/dist/scripts/test-ml.js +25 -3
  74. package/dist/scripts/test.js +26 -1
  75. package/dist/scripts/user-add.js +15 -2
  76. package/dist/workers/testerWorker.js +130 -21
  77. package/package.json +18 -12
@@ -30,15 +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,
40
44
  main: () => main,
41
45
  parseJsonOutput: () => parseJsonOutput,
46
+ releaseResearchAutoLock: () => releaseResearchAutoLock,
47
+ resolveHeapMbFromEnv: () => resolveHeapMbFromEnv,
42
48
  resolveStrategyNameByConfigKey: () => resolveStrategyNameByConfigKey,
43
49
  resolveTarget: () => resolveTarget,
44
50
  runCliCommand: () => runCliCommand,
@@ -47,15 +53,83 @@ __export(researchAuto_exports, {
47
53
  });
48
54
  module.exports = __toCommonJS(researchAuto_exports);
49
55
  var import_args = __toESM(require("args"));
50
- var import_chalk = __toESM(require("chalk"));
56
+ var import_chalk2 = __toESM(require("chalk"));
51
57
  var import_node_child_process = require("child_process");
52
58
  var import_promises = __toESM(require("fs/promises"));
59
+ var import_node_os = __toESM(require("os"));
53
60
  var import_node_path = __toESM(require("path"));
61
+ var import_constants = require("@tradejs/core/constants");
54
62
  var import_ai = require("@tradejs/infra/ai");
55
63
  var import_logger = require("@tradejs/infra/logger");
56
- var import_redis = require("@tradejs/infra/redis");
64
+ var import_redis2 = require("@tradejs/infra/redis");
57
65
  var import_strategies = require("@tradejs/strategies");
58
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
+
59
133
  // src/lib/telegramReports.ts
60
134
  var import_cli = require("@tradejs/node/cli");
61
135
  var TELEGRAM_REPORT_CHUNK_LIMIT = 3900;
@@ -100,8 +174,16 @@ ${line}` : line;
100
174
  };
101
175
  var sendTelegramReport = async (message, options = {}) => {
102
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
+ };
103
184
  if (chunks.length === 1) {
104
185
  await (0, import_cli.sendTextToTG)(chunks[0], options);
186
+ await sendAttachments();
105
187
  return chunks.length;
106
188
  }
107
189
  for (const [index, chunk] of chunks.entries()) {
@@ -113,6 +195,7 @@ ${chunk}`, {
113
195
  markup: index === chunks.length - 1 ? options.markup : void 0
114
196
  });
115
197
  }
198
+ await sendAttachments();
116
199
  return chunks.length;
117
200
  };
118
201
 
@@ -128,16 +211,150 @@ import_args.default.option(["o", "connector"], "Backtest connector", "bybit");
128
211
  import_args.default.option(["f", "timeframe"], "Backtest timeframe", "15");
129
212
  import_args.default.option(["d", "days"], "Backtest days window", 45);
130
213
  import_args.default.option(["n", "recent"], "AI train recent rows window", 1e3);
131
- import_args.default.option("skip", "AI train skip rows from end", 0);
132
- import_args.default.option("minQuality", "Minimum AI quality required to approve entry", 4);
133
- import_args.default.option("outDir", "AI export output directory", "data/ai/export");
134
- import_args.default.option("skipAgent", "Skip direct research agent invocation", false);
135
- 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);
136
223
  var flags = import_args.default.parse(process.argv);
137
224
  var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
138
225
  var dotenvPath = String(
139
226
  process.env.DOTENV_CONFIG_PATH || import_node_path.default.join(projectRoot, ".env")
140
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
+ );
141
358
  var trimTextTail = (value, limit = 4e3) => value.length <= limit ? value : value.slice(-limit);
142
359
  var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
143
360
  var createEmptySteps = () => ({
@@ -178,11 +395,11 @@ var createEmptySteps = () => ({
178
395
  }
179
396
  });
180
397
  var saveRun = async (run) => {
181
- await (0, import_redis.setData)(import_redis.redisKeys.researchRun(run.userName, run.runId), run, {
182
- expire: 0
398
+ await (0, import_redis2.setData)(import_redis2.redisKeys.researchRun(run.userName, run.runId), run, {
399
+ expire: import_constants.TTL_1M
183
400
  });
184
- await (0, import_redis.setData)(import_redis.redisKeys.researchLatestRun(run.userName, run.strategy), run, {
185
- expire: 0
401
+ await (0, import_redis2.setData)(import_redis2.redisKeys.researchLatestRun(run.userName, run.strategy), run, {
402
+ expire: import_constants.TTL_1M
186
403
  });
187
404
  };
188
405
  var listMergedFiles = async (outDir, strategyName) => {
@@ -195,24 +412,13 @@ var listMergedFiles = async (outDir, strategyName) => {
195
412
  }
196
413
  };
197
414
  var getBacktestResultsPrefix = (userName, config) => `users:${userName}:backtests:results:${config}:`;
198
- var resolveStrategyNameByConfigKey = (userName, key) => {
199
- const parts = key.split(":");
200
- if (parts.length !== 5) {
201
- return null;
202
- }
203
- const [users, keyUserName, strategiesKey, strategyName, configKey] = parts;
204
- if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || configKey !== "config" || !strategyName) {
205
- return null;
206
- }
207
- return strategyName;
208
- };
209
415
  var listRuntimeStrategyNames = async (userName) => {
210
- const keys = await (0, import_redis.getKeys)(`${import_redis.redisKeys.strategies(userName)}:`);
211
- 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));
212
418
  };
213
419
  var toStrategyConfigGrid = (strategyConfig) => {
214
420
  return Object.fromEntries(
215
- 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]])
216
422
  );
217
423
  };
218
424
  var formatMskDateTime = (value) => {
@@ -321,11 +527,10 @@ var runCliCommand = async (params) => {
321
527
  ...process.env,
322
528
  PROJECT_CWD: projectRoot,
323
529
  DOTENV_CONFIG_PATH: dotenvPath,
324
- ...params.node8g ? {
325
- NODE_OPTIONS: [
326
- "--max-old-space-size=8192",
327
- String(process.env.NODE_OPTIONS || "").trim()
328
- ].filter(Boolean).join(" ")
530
+ ...params.nodeHeapMb ? {
531
+ NODE_OPTIONS: buildNodeOptions({
532
+ heapMb: params.nodeHeapMb
533
+ })
329
534
  } : {}
330
535
  };
331
536
  return new Promise((resolve, reject) => {
@@ -351,14 +556,14 @@ var runCliCommand = async (params) => {
351
556
  const emitLines = (source, text) => {
352
557
  const prefix = params.liveLogPrefix ? `[research:auto:${params.liveLogPrefix}:${source}] ` : `[research:auto:${source}] `;
353
558
  for (const line of text.split(/\r?\n/).map((value) => value.trimEnd()).filter(Boolean)) {
354
- import_logger.logger.info(import_chalk.default.gray(`${prefix}${line}`));
559
+ import_logger.logger.info(import_chalk2.default.gray(`${prefix}${line}`));
355
560
  }
356
561
  };
357
562
  if (params.heartbeatMs && params.heartbeatMs > 0) {
358
563
  heartbeatTimer = setInterval(() => {
359
564
  const elapsedMs = Date.now() - startedAt;
360
565
  import_logger.logger.info(
361
- import_chalk.default.gray(
566
+ import_chalk2.default.gray(
362
567
  `[research:auto:${params.liveLogPrefix || params.command}] still running after ${Math.round(elapsedMs / 1e3)}s`
363
568
  )
364
569
  );
@@ -419,8 +624,8 @@ var resolveTarget = async () => {
419
624
  const candidates = [];
420
625
  for (const strategy of availableStrategies) {
421
626
  const config = `${strategy}:research`;
422
- const latestRun = await (0, import_redis.getData)(
423
- import_redis.redisKeys.researchLatestRun(userName, strategy),
627
+ const latestRun = await (0, import_redis2.getData)(
628
+ import_redis2.redisKeys.researchLatestRun(userName, strategy),
424
629
  null
425
630
  );
426
631
  const lastFinishedAt = latestRun?.finishedAt ? Date.parse(latestRun.finishedAt) : 0;
@@ -457,7 +662,7 @@ var parseJsonOutput = (stdout, label) => {
457
662
  return JSON.parse(trimmed);
458
663
  };
459
664
  var logResearch = (message) => {
460
- import_logger.logger.info(import_chalk.default.cyan(`[research:auto] ${message}`));
665
+ import_logger.logger.info(import_chalk2.default.cyan(`[research:auto] ${message}`));
461
666
  };
462
667
  var executeNonBlockingStep = async (run, stepName, command, commandArgs, options = {}) => {
463
668
  logResearch(
@@ -474,7 +679,7 @@ var executeNonBlockingStep = async (run, stepName, command, commandArgs, options
474
679
  const result = await runCliCommand({
475
680
  command,
476
681
  args: commandArgs,
477
- node8g: options.node8g,
682
+ nodeHeapMb: options.nodeHeapMb,
478
683
  liveLogPrefix: options.liveLog ? stepName : void 0,
479
684
  heartbeatMs: options.liveLog ? options.heartbeatMs || 6e4 : void 0,
480
685
  captureMode: options.captureMode
@@ -534,6 +739,7 @@ var main = async () => {
534
739
  steps: createEmptySteps(),
535
740
  artifacts: {}
536
741
  };
742
+ let lockPath = null;
537
743
  const executeStep = async (stepName, command, commandArgs, options = {}) => {
538
744
  logResearch(
539
745
  `starting step ${stepName}: ${command} ${commandArgs.join(" ")}`.trim()
@@ -549,7 +755,7 @@ var main = async () => {
549
755
  const result = await runCliCommand({
550
756
  command,
551
757
  args: commandArgs,
552
- node8g: options.node8g,
758
+ nodeHeapMb: options.nodeHeapMb,
553
759
  liveLogPrefix: options.liveLog ? stepName : void 0,
554
760
  heartbeatMs: options.liveLog ? options.heartbeatMs || 6e4 : void 0,
555
761
  captureMode: options.captureMode
@@ -597,13 +803,13 @@ var main = async () => {
597
803
  startedAt
598
804
  };
599
805
  await saveRun(run);
600
- const strategyConfigKey = import_redis.redisKeys.strategyConfig(
806
+ const strategyConfigKey = import_redis2.redisKeys.strategyConfig(
601
807
  userName,
602
808
  target.strategy
603
809
  );
604
- const backtestConfigKey = import_redis.redisKeys.backtestConfig(userName, target.config);
810
+ const backtestConfigKey = import_redis2.redisKeys.backtestConfig(userName, target.config);
605
811
  const defaults = (0, import_strategies.getBuiltInStrategyDefaultConfig)(target.strategy) || {};
606
- const userStrategyConfig = await (0, import_redis.getData)(
812
+ const userStrategyConfig = await (0, import_redis2.getData)(
607
813
  strategyConfigKey,
608
814
  {}
609
815
  );
@@ -617,7 +823,7 @@ var main = async () => {
617
823
  );
618
824
  }
619
825
  const backtestConfig = toStrategyConfigGrid(strategyConfig);
620
- await (0, import_redis.setData)(backtestConfigKey, backtestConfig, { expire: 0 });
826
+ await (0, import_redis2.setData)(backtestConfigKey, backtestConfig, { expire: 0 });
621
827
  logResearch(
622
828
  `stored backtest config at ${backtestConfigKey} with ${Object.keys(strategyConfig).length} top-level fields`
623
829
  );
@@ -639,13 +845,25 @@ var main = async () => {
639
845
  const message = buildTelegramReport(run);
640
846
  await sendTelegramReport(message, { userName });
641
847
  };
642
- await saveRun(run);
643
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);
644
859
  logResearch(
645
860
  `run ${runId} selected target strategy=${target.strategy}, config=${target.config}, selectedBy=${target.selectedBy}`
646
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
+ );
647
865
  const backtestKeysBefore = new Set(
648
- await (0, import_redis.getKeys)(getBacktestResultsPrefix(userName, target.config))
866
+ await (0, import_redis2.getKeys)(getBacktestResultsPrefix(userName, target.config))
649
867
  );
650
868
  const aiFilesBefore = new Set(
651
869
  await listMergedFiles(outDir, target.strategy)
@@ -675,7 +893,11 @@ var main = async () => {
675
893
  String(days),
676
894
  "--ai"
677
895
  ],
678
- { node8g: true, liveLog: true, heartbeatMs: 3e4 }
896
+ {
897
+ nodeHeapMb: RESEARCH_AUTO_BACKTEST_PARENT_HEAP_MB,
898
+ liveLog: true,
899
+ heartbeatMs: 3e4
900
+ }
679
901
  );
680
902
  await executeStep("aiExport", "ai-export", [
681
903
  "--strategy",
@@ -698,16 +920,19 @@ var main = async () => {
698
920
  "--localOnly",
699
921
  "--json"
700
922
  ],
701
- { node8g: true, captureMode: "full" }
923
+ {
924
+ nodeHeapMb: RESEARCH_AUTO_AI_TRAIN_PARENT_HEAP_MB,
925
+ captureMode: "full"
926
+ }
702
927
  );
703
- const backtestKeysAfter = await (0, import_redis.getKeys)(
928
+ const backtestKeysAfter = await (0, import_redis2.getKeys)(
704
929
  getBacktestResultsPrefix(userName, target.config)
705
930
  );
706
931
  const newBacktestKeys = backtestKeysAfter.filter((key) => !backtestKeysBefore.has(key)).sort();
707
932
  const backtestResultKey = newBacktestKeys[newBacktestKeys.length - 1] || backtestKeysAfter.sort().at(-1);
708
933
  if (backtestResultKey) {
709
934
  logResearch(`detected backtest result key ${backtestResultKey}`);
710
- const backtestResult = await (0, import_redis.getData)(backtestResultKey, null);
935
+ const backtestResult = await (0, import_redis2.getData)(backtestResultKey, null);
711
936
  run.artifacts.backtestResultKey = backtestResultKey;
712
937
  run.artifacts.backtestResult = backtestResult ? {
713
938
  finishedAt: backtestResult.finishedAt,
@@ -755,7 +980,7 @@ var main = async () => {
755
980
  "--json"
756
981
  ],
757
982
  {
758
- node8g: true,
983
+ nodeHeapMb: RESEARCH_AUTO_AGENT_PARENT_HEAP_MB,
759
984
  liveLog: true,
760
985
  heartbeatMs: 3e4,
761
986
  captureMode: "full"
@@ -783,19 +1008,19 @@ var main = async () => {
783
1008
  console.log(JSON.stringify(run));
784
1009
  process.exit(0);
785
1010
  }
786
- console.log(import_chalk.default.green("Research auto finished"));
1011
+ console.log(import_chalk2.default.green("Research auto finished"));
787
1012
  console.log(
788
- import_chalk.default.gray(
1013
+ import_chalk2.default.gray(
789
1014
  `strategy=${run.strategy}, config=${run.config}, runId=${run.runId}`
790
1015
  )
791
1016
  );
792
1017
  if (run.artifacts.backtestResultKey) {
793
1018
  console.log(
794
- import_chalk.default.gray(`backtestResultKey=${run.artifacts.backtestResultKey}`)
1019
+ import_chalk2.default.gray(`backtestResultKey=${run.artifacts.backtestResultKey}`)
795
1020
  );
796
1021
  }
797
1022
  if (run.artifacts.aiExportFile) {
798
- console.log(import_chalk.default.gray(`aiExportFile=${run.artifacts.aiExportFile}`));
1023
+ console.log(import_chalk2.default.gray(`aiExportFile=${run.artifacts.aiExportFile}`));
799
1024
  }
800
1025
  process.exit(0);
801
1026
  } catch (error) {
@@ -817,28 +1042,30 @@ Telegram report failed: ${suffix}`;
817
1042
  if (jsonOutput) {
818
1043
  console.log(JSON.stringify(run));
819
1044
  } else {
820
- console.error(import_chalk.default.red(run.error));
1045
+ console.error(import_chalk2.default.red(run.error));
821
1046
  }
822
1047
  process.exit(1);
1048
+ } finally {
1049
+ await releaseResearchAutoLock(lockPath);
823
1050
  }
824
1051
  };
825
- if (require.main === module) {
826
- main().catch((error) => {
827
- console.error(import_chalk.default.red(error?.message || String(error)));
828
- process.exit(1);
829
- });
830
- }
831
1052
  // Annotate the CommonJS export names for ESM import in node:
832
1053
  0 && (module.exports = {
1054
+ acquireResearchAutoLock,
1055
+ buildNodeOptions,
833
1056
  buildTelegramReport,
834
1057
  createEmptySteps,
835
1058
  escapeHtml,
836
1059
  formatMskDateTime,
837
1060
  getBacktestResultsPrefix,
1061
+ getResearchAutoLockPath,
1062
+ isProcessAlive,
838
1063
  listMergedFiles,
839
1064
  listRuntimeStrategyNames,
840
1065
  main,
841
1066
  parseJsonOutput,
1067
+ releaseResearchAutoLock,
1068
+ resolveHeapMbFromEnv,
842
1069
  resolveStrategyNameByConfigKey,
843
1070
  resolveTarget,
844
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
  });