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