@tradejs/node 1.0.4 → 1.0.6
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/README.md +1 -1
- package/dist/ai.d.mts +31 -0
- package/dist/ai.d.ts +31 -0
- package/dist/ai.js +926 -0
- package/dist/ai.mjs +35 -0
- package/dist/backtest.js +408 -5687
- package/dist/backtest.mjs +331 -175
- package/dist/chunk-72FKXJ2I.mjs +473 -0
- package/dist/{chunk-P2ZUWONT.mjs → chunk-CGJ2UU6H.mjs} +1 -1
- package/dist/{chunk-ZY6ULOWK.mjs → chunk-EOZSJKUM.mjs} +1 -1
- package/dist/{chunk-7ICOZAKA.mjs → chunk-H6LIYHU4.mjs} +29 -11
- package/dist/{chunk-RBE4PZER.mjs → chunk-PQH5PAFC.mjs} +1 -1
- package/dist/chunk-SCMBUEGK.mjs +22 -0
- package/dist/{chunk-CIY64D57.mjs → chunk-XW5L327F.mjs} +1 -1
- package/dist/cli.d.mts +4 -4
- package/dist/cli.d.ts +4 -4
- package/dist/cli.js +1179 -207
- package/dist/cli.mjs +652 -147
- package/dist/connectors.js +1 -1
- package/dist/connectors.mjs +2 -2
- package/dist/constants.js +10 -1
- package/dist/constants.mjs +1 -1
- package/dist/pine.d.mts +10 -6
- package/dist/pine.d.ts +10 -6
- package/dist/pine.js +40 -18
- package/dist/pine.mjs +21 -13
- package/dist/registry.js +1 -1
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +6 -12
- package/dist/strategies.d.ts +6 -12
- package/dist/strategies.js +426 -83
- package/dist/strategies.mjs +200 -34
- package/package.json +18 -5
- package/dist/ai-G7ATN4YL.mjs +0 -19
- package/dist/chunk-4ZPGZWO7.mjs +0 -13
- package/dist/chunk-LMAKIC3C.mjs +0 -294
package/dist/cli.mjs
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AI_CONCURRENCY_LIMIT,
|
|
3
3
|
KLINE_CONCURRENCY_LIMIT,
|
|
4
|
-
SCREENSHOT_CONCURRENCY_LIMIT
|
|
5
|
-
|
|
6
|
-
} from "./chunk-4ZPGZWO7.mjs";
|
|
4
|
+
SCREENSHOT_CONCURRENCY_LIMIT
|
|
5
|
+
} from "./chunk-SCMBUEGK.mjs";
|
|
7
6
|
import {
|
|
8
7
|
require_lodash
|
|
9
8
|
} from "./chunk-GKDBAF3A.mjs";
|
|
10
9
|
import {
|
|
11
10
|
askAI
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-72FKXJ2I.mjs";
|
|
12
|
+
import "./chunk-EOZSJKUM.mjs";
|
|
14
13
|
import {
|
|
15
14
|
getTradejsProjectCwd
|
|
16
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-CGJ2UU6H.mjs";
|
|
17
16
|
import {
|
|
18
17
|
__toESM
|
|
19
18
|
} from "./chunk-6DZX6EAA.mjs";
|
|
@@ -33,76 +32,438 @@ import {
|
|
|
33
32
|
RedisWriteBlockedError,
|
|
34
33
|
delKeyWithOptions,
|
|
35
34
|
getKeys,
|
|
36
|
-
getData
|
|
37
|
-
redisKeys
|
|
35
|
+
getData,
|
|
36
|
+
redisKeys
|
|
38
37
|
} from "@tradejs/infra/redis";
|
|
39
|
-
import { logger as
|
|
38
|
+
import { logger as logger3 } from "@tradejs/infra/logger";
|
|
40
39
|
|
|
41
40
|
// src/screenshot.ts
|
|
42
41
|
import fs from "fs/promises";
|
|
43
42
|
import path from "path";
|
|
44
43
|
import puppeteer from "puppeteer";
|
|
45
44
|
import { delay } from "@tradejs/core/async";
|
|
46
|
-
import {
|
|
45
|
+
import { logger } from "@tradejs/infra/logger";
|
|
46
|
+
import { getUserSettings } from "@tradejs/infra/userSettings";
|
|
47
47
|
var { APP_URL } = process.env;
|
|
48
|
+
var SCREENSHOT_NAVIGATION_ATTEMPTS = 3;
|
|
49
|
+
var SCREENSHOT_NAVIGATION_RETRY_DELAY_MS = 2e3;
|
|
50
|
+
var SCREENSHOT_CAPTURE_ATTEMPTS = 2;
|
|
51
|
+
var SCREENSHOT_CAPTURE_RETRY_DELAY_MS = 2e3;
|
|
52
|
+
var SCREENSHOT_NAVIGATION_TIMEOUT_MS = 3e4;
|
|
53
|
+
var SCREENSHOT_RENDER_DELAY_MS = 1e4;
|
|
54
|
+
var SCREENSHOT_CONSOLE_LOG_LIMIT = 20;
|
|
55
|
+
var SCREENSHOT_CONSOLE_TEXT_LIMIT = 500;
|
|
56
|
+
var SCREENSHOT_BROWSER_STDERR = process.env.SCREENSHOT_BROWSER_STDERR === "1";
|
|
57
|
+
var PUPPETEER_DUMPIO = process.env.PUPPETEER_DUMPIO === "1";
|
|
58
|
+
var SCREENSHOT_VIEWPORT = {
|
|
59
|
+
width: 1280,
|
|
60
|
+
height: 800,
|
|
61
|
+
deviceScaleFactor: process.env.NODE_ENV === "production" ? 1 : 2
|
|
62
|
+
};
|
|
48
63
|
var getProjectRoot = (projectRoot) => path.resolve(getTradejsProjectCwd(projectRoot));
|
|
49
64
|
var getScreenshotsDir = (projectRoot) => path.join(getProjectRoot(projectRoot), "data", "screenshots");
|
|
50
|
-
var
|
|
65
|
+
var maskTokenInUrl = (url) => url.replace(/([?&]token=)[^&]+/i, "$1<hidden>");
|
|
66
|
+
var getErrorFields = (value) => {
|
|
67
|
+
if (!value || typeof value !== "object") {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const record = value;
|
|
71
|
+
const fields = [
|
|
72
|
+
["name", record.name],
|
|
73
|
+
["code", record.code],
|
|
74
|
+
["errno", record.errno],
|
|
75
|
+
["type", record.type],
|
|
76
|
+
["syscall", record.syscall],
|
|
77
|
+
["hostname", record.hostname],
|
|
78
|
+
["host", record.host],
|
|
79
|
+
["address", record.address],
|
|
80
|
+
["port", record.port]
|
|
81
|
+
];
|
|
82
|
+
return fields.filter(([, fieldValue]) => fieldValue != null && String(fieldValue).trim()).map(([key, fieldValue]) => `${key}=${String(fieldValue)}`);
|
|
83
|
+
};
|
|
84
|
+
var describeErrorValue = (value) => {
|
|
85
|
+
if (value == null) {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === "string") {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
if (value instanceof Error) {
|
|
92
|
+
const message = value.message?.trim() || "";
|
|
93
|
+
const fields = getErrorFields(value);
|
|
94
|
+
return [message, ...fields].filter(Boolean).join(" ");
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === "object") {
|
|
97
|
+
const fields = getErrorFields(value);
|
|
98
|
+
if (fields.length) {
|
|
99
|
+
return fields.join(" ");
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return JSON.stringify(value);
|
|
103
|
+
} catch {
|
|
104
|
+
return String(value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return String(value);
|
|
108
|
+
};
|
|
109
|
+
var getErrorMessage = (error) => {
|
|
110
|
+
const maybeError = error;
|
|
111
|
+
const message = describeErrorValue(error) || String(error);
|
|
112
|
+
if (maybeError?.cause == null) {
|
|
113
|
+
return message;
|
|
114
|
+
}
|
|
115
|
+
const cause = describeErrorValue(maybeError.cause);
|
|
116
|
+
if (!cause) {
|
|
117
|
+
return message;
|
|
118
|
+
}
|
|
119
|
+
return `${message}; cause: ${cause}`;
|
|
120
|
+
};
|
|
121
|
+
var truncateText = (value, maxLength = SCREENSHOT_CONSOLE_TEXT_LIMIT) => value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
122
|
+
var shouldLogConsoleMessage = (type) => ["error", "warning", "assert"].includes(type);
|
|
123
|
+
var getScreenshotBuffer = async (signal, projectRoot) => {
|
|
124
|
+
const screenshotPath = getScreenshotPath(signal, projectRoot);
|
|
125
|
+
return fs.readFile(screenshotPath);
|
|
126
|
+
};
|
|
127
|
+
var getScreenshotFilename = ({
|
|
128
|
+
symbol,
|
|
129
|
+
signalId,
|
|
130
|
+
interval
|
|
131
|
+
}) => `${symbol}_${signalId}_${interval}.png`;
|
|
132
|
+
var getScreenshotRenderBaseUrl = () => {
|
|
133
|
+
const fallback = String(APP_URL || "").trim();
|
|
134
|
+
if (fallback) {
|
|
135
|
+
return fallback;
|
|
136
|
+
}
|
|
137
|
+
throw new Error("APP_URL is required for screenshots");
|
|
138
|
+
};
|
|
51
139
|
var getScreenshotPath = ({ symbol, signalId, interval }, projectRoot) => {
|
|
52
140
|
return path.join(
|
|
53
141
|
getScreenshotsDir(projectRoot),
|
|
54
|
-
|
|
142
|
+
getScreenshotFilename({ symbol, signalId, interval })
|
|
55
143
|
);
|
|
56
144
|
};
|
|
57
|
-
var screenDashboard = async (signal, projectRoot) => {
|
|
145
|
+
var screenDashboard = async (signal, projectRoot, userName = "root") => {
|
|
58
146
|
const { symbol, signalId, interval } = signal;
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
147
|
+
const screenshotBaseUrl = getScreenshotRenderBaseUrl();
|
|
148
|
+
const screenshotPath = getScreenshotPath(signal, projectRoot);
|
|
149
|
+
const settings = await getUserSettings(userName);
|
|
150
|
+
const token = settings.token;
|
|
151
|
+
const tokenParam = typeof token === "string" && token.length > 0 ? `&token=${encodeURIComponent(token)}` : "";
|
|
152
|
+
const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1${tokenParam}`;
|
|
153
|
+
const maskedDashboardUrl = maskTokenInUrl(dashboardUrl);
|
|
154
|
+
logger.info(
|
|
155
|
+
"screenshot start: %s %sm url=%s path=%s",
|
|
156
|
+
symbol,
|
|
157
|
+
interval,
|
|
158
|
+
maskedDashboardUrl,
|
|
159
|
+
screenshotPath
|
|
160
|
+
);
|
|
161
|
+
let captureError = null;
|
|
162
|
+
for (let captureAttempt = 1; captureAttempt <= SCREENSHOT_CAPTURE_ATTEMPTS; captureAttempt += 1) {
|
|
163
|
+
const browser = await puppeteer.launch({
|
|
164
|
+
headless: true,
|
|
165
|
+
dumpio: PUPPETEER_DUMPIO,
|
|
166
|
+
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
|
|
167
|
+
args: [
|
|
168
|
+
"--no-sandbox",
|
|
169
|
+
"--disable-setuid-sandbox",
|
|
170
|
+
"--disable-dev-shm-usage",
|
|
171
|
+
"--disable-gpu",
|
|
172
|
+
"--font-render-hinting=medium"
|
|
173
|
+
]
|
|
174
|
+
});
|
|
175
|
+
const browserProcess = browser.process();
|
|
176
|
+
let browserCloseRequested = false;
|
|
75
177
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
178
|
+
if (browserProcess) {
|
|
179
|
+
logger.info(
|
|
180
|
+
"screenshot browser: %s %sm captureAttempt=%d pid=%s exec=%s",
|
|
181
|
+
symbol,
|
|
182
|
+
interval,
|
|
183
|
+
captureAttempt,
|
|
184
|
+
String(browserProcess.pid ?? "unknown"),
|
|
185
|
+
browserProcess.spawnfile || "unknown"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
logger.info(
|
|
189
|
+
"screenshot browser version: %s %sm captureAttempt=%d version=%s",
|
|
190
|
+
symbol,
|
|
191
|
+
interval,
|
|
192
|
+
captureAttempt,
|
|
193
|
+
await browser.version()
|
|
83
194
|
);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
195
|
+
browser.on("disconnected", () => {
|
|
196
|
+
if (browserCloseRequested) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
logger.error(
|
|
200
|
+
"screenshot browser disconnected: %s %sm captureAttempt=%d",
|
|
201
|
+
symbol,
|
|
202
|
+
interval,
|
|
203
|
+
captureAttempt
|
|
204
|
+
);
|
|
88
205
|
});
|
|
206
|
+
if (SCREENSHOT_BROWSER_STDERR && browserProcess?.stderr) {
|
|
207
|
+
browserProcess.stderr.on("data", (chunk) => {
|
|
208
|
+
const text = String(chunk).trim();
|
|
209
|
+
if (!text) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
logger.error(
|
|
213
|
+
"screenshot browser stderr: %s %sm captureAttempt=%d (%s)",
|
|
214
|
+
symbol,
|
|
215
|
+
interval,
|
|
216
|
+
captureAttempt,
|
|
217
|
+
truncateText(text)
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const page = await browser.newPage();
|
|
222
|
+
try {
|
|
223
|
+
let consoleMessagesLogged = 0;
|
|
224
|
+
page.on("requestfailed", (request) => {
|
|
225
|
+
if (!request.isNavigationRequest() && request.resourceType() !== "document") {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
logger.error(
|
|
229
|
+
"screenshot request failed: %s %sm %s (%s)",
|
|
230
|
+
symbol,
|
|
231
|
+
interval,
|
|
232
|
+
maskTokenInUrl(request.url()),
|
|
233
|
+
request.failure()?.errorText || "unknown"
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
page.on("pageerror", (error) => {
|
|
237
|
+
logger.error(
|
|
238
|
+
"screenshot page error: %s %sm (%s)",
|
|
239
|
+
symbol,
|
|
240
|
+
interval,
|
|
241
|
+
error.message || String(error)
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
page.on("error", (error) => {
|
|
245
|
+
logger.error(
|
|
246
|
+
"screenshot target error: %s %sm (%s)",
|
|
247
|
+
symbol,
|
|
248
|
+
interval,
|
|
249
|
+
error.message || String(error)
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
page.on("close", () => {
|
|
253
|
+
logger.info(
|
|
254
|
+
"screenshot page closed: %s %sm captureAttempt=%d",
|
|
255
|
+
symbol,
|
|
256
|
+
interval,
|
|
257
|
+
captureAttempt
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
page.on("console", (message) => {
|
|
261
|
+
if (!shouldLogConsoleMessage(message.type())) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (consoleMessagesLogged >= SCREENSHOT_CONSOLE_LOG_LIMIT) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
consoleMessagesLogged += 1;
|
|
268
|
+
const location = message.location();
|
|
269
|
+
const suffix = location.url ? ` ${maskTokenInUrl(location.url)}:${location.lineNumber ?? 0}:${location.columnNumber ?? 0}` : "";
|
|
270
|
+
logger.error(
|
|
271
|
+
"screenshot console %s: %s %sm captureAttempt=%d (%s%s)",
|
|
272
|
+
message.type(),
|
|
273
|
+
symbol,
|
|
274
|
+
interval,
|
|
275
|
+
captureAttempt,
|
|
276
|
+
truncateText(message.text()),
|
|
277
|
+
suffix
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
page.on("response", (response) => {
|
|
281
|
+
const status = response.status();
|
|
282
|
+
if (status < 400) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const request = response.request();
|
|
286
|
+
const resourceType = request.resourceType();
|
|
287
|
+
if (!["document", "script", "stylesheet", "fetch", "xhr"].includes(
|
|
288
|
+
resourceType
|
|
289
|
+
)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
logger.error(
|
|
293
|
+
"screenshot response failed: %s %sm captureAttempt=%d status=%d type=%s url=%s",
|
|
294
|
+
symbol,
|
|
295
|
+
interval,
|
|
296
|
+
captureAttempt,
|
|
297
|
+
status,
|
|
298
|
+
resourceType,
|
|
299
|
+
maskTokenInUrl(response.url())
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
await page.setViewport(SCREENSHOT_VIEWPORT);
|
|
303
|
+
let gotoError = null;
|
|
304
|
+
logger.info(
|
|
305
|
+
"screenshot render: %s %sm captureAttempt=%d viewport=%dx%d@%d",
|
|
306
|
+
symbol,
|
|
307
|
+
interval,
|
|
308
|
+
captureAttempt,
|
|
309
|
+
SCREENSHOT_VIEWPORT.width,
|
|
310
|
+
SCREENSHOT_VIEWPORT.height,
|
|
311
|
+
SCREENSHOT_VIEWPORT.deviceScaleFactor
|
|
312
|
+
);
|
|
313
|
+
for (let attempt = 1; attempt <= SCREENSHOT_NAVIGATION_ATTEMPTS; attempt += 1) {
|
|
314
|
+
try {
|
|
315
|
+
logger.info(
|
|
316
|
+
"screenshot goto: %s %sm captureAttempt=%d attempt=%d url=%s",
|
|
317
|
+
symbol,
|
|
318
|
+
interval,
|
|
319
|
+
captureAttempt,
|
|
320
|
+
attempt,
|
|
321
|
+
maskedDashboardUrl
|
|
322
|
+
);
|
|
323
|
+
const response = await page.goto(dashboardUrl, {
|
|
324
|
+
waitUntil: "domcontentloaded",
|
|
325
|
+
timeout: SCREENSHOT_NAVIGATION_TIMEOUT_MS
|
|
326
|
+
});
|
|
327
|
+
logger.info(
|
|
328
|
+
"screenshot goto ok: %s %sm captureAttempt=%d attempt=%d status=%s finalUrl=%s",
|
|
329
|
+
symbol,
|
|
330
|
+
interval,
|
|
331
|
+
captureAttempt,
|
|
332
|
+
attempt,
|
|
333
|
+
response ? String(response.status()) : "null",
|
|
334
|
+
maskTokenInUrl(page.url())
|
|
335
|
+
);
|
|
336
|
+
logger.info(
|
|
337
|
+
"screenshot page info: %s %sm captureAttempt=%d title=%s ua=%s",
|
|
338
|
+
symbol,
|
|
339
|
+
interval,
|
|
340
|
+
captureAttempt,
|
|
341
|
+
truncateText(await page.title(), 120),
|
|
342
|
+
truncateText(await page.evaluate(() => navigator.userAgent), 240)
|
|
343
|
+
);
|
|
344
|
+
gotoError = null;
|
|
345
|
+
break;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
gotoError = error;
|
|
348
|
+
logger.error(
|
|
349
|
+
"screenshot goto failed: %s %sm captureAttempt=%d attempt=%d url=%s (%s)",
|
|
350
|
+
symbol,
|
|
351
|
+
interval,
|
|
352
|
+
captureAttempt,
|
|
353
|
+
attempt,
|
|
354
|
+
maskedDashboardUrl,
|
|
355
|
+
gotoError.message || String(gotoError)
|
|
356
|
+
);
|
|
357
|
+
if (attempt >= SCREENSHOT_NAVIGATION_ATTEMPTS) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
await delay(SCREENSHOT_NAVIGATION_RETRY_DELAY_MS);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (gotoError) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Failed to open dashboard ${dashboardUrl}: ${gotoError.message || String(gotoError)}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
await delay(SCREENSHOT_RENDER_DELAY_MS);
|
|
369
|
+
await fs.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
|
|
370
|
+
await page.screenshot({
|
|
371
|
+
path: screenshotPath,
|
|
372
|
+
type: "png"
|
|
373
|
+
});
|
|
374
|
+
logger.info(
|
|
375
|
+
"screenshot saved: %s %sm path=%s",
|
|
376
|
+
symbol,
|
|
377
|
+
interval,
|
|
378
|
+
screenshotPath
|
|
379
|
+
);
|
|
380
|
+
return;
|
|
381
|
+
} finally {
|
|
382
|
+
await page.close().catch(() => void 0);
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
captureError = error;
|
|
386
|
+
logger.error(
|
|
387
|
+
"screenshot capture failed: %s %sm captureAttempt=%d (%s)",
|
|
388
|
+
symbol,
|
|
389
|
+
interval,
|
|
390
|
+
captureAttempt,
|
|
391
|
+
getErrorMessage(captureError)
|
|
392
|
+
);
|
|
393
|
+
if (captureAttempt < SCREENSHOT_CAPTURE_ATTEMPTS) {
|
|
394
|
+
await delay(SCREENSHOT_CAPTURE_RETRY_DELAY_MS);
|
|
395
|
+
}
|
|
89
396
|
} finally {
|
|
90
|
-
|
|
397
|
+
browserCloseRequested = true;
|
|
398
|
+
await browser.close().catch(() => void 0);
|
|
91
399
|
}
|
|
92
|
-
} finally {
|
|
93
|
-
await browser.close();
|
|
94
400
|
}
|
|
401
|
+
throw captureError || new Error(`Failed screenshot: ${symbol} ${interval}`);
|
|
95
402
|
};
|
|
96
403
|
|
|
97
404
|
// src/signals.ts
|
|
405
|
+
import { delay as delay2 } from "@tradejs/core/async";
|
|
98
406
|
import { formatNumber } from "@tradejs/core/math";
|
|
99
|
-
import { logger } from "@tradejs/infra/logger";
|
|
407
|
+
import { logger as logger2 } from "@tradejs/infra/logger";
|
|
408
|
+
import { getUserSettings as getUserSettings2 } from "@tradejs/infra/userSettings";
|
|
100
409
|
var escapeHtml = (s) => {
|
|
101
410
|
if (!s) return "";
|
|
102
411
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
103
412
|
};
|
|
104
|
-
var { APP_URL: APP_URL2
|
|
413
|
+
var { APP_URL: APP_URL2 } = process.env;
|
|
414
|
+
var TG_REQUEST_ATTEMPTS = 3;
|
|
415
|
+
var TG_REQUEST_RETRY_DELAY_MS = 2e3;
|
|
416
|
+
var getErrorFields2 = (value) => {
|
|
417
|
+
if (!value || typeof value !== "object") {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
const record = value;
|
|
421
|
+
const fields = [
|
|
422
|
+
["name", record.name],
|
|
423
|
+
["code", record.code],
|
|
424
|
+
["errno", record.errno],
|
|
425
|
+
["type", record.type],
|
|
426
|
+
["syscall", record.syscall],
|
|
427
|
+
["hostname", record.hostname],
|
|
428
|
+
["host", record.host],
|
|
429
|
+
["address", record.address],
|
|
430
|
+
["port", record.port]
|
|
431
|
+
];
|
|
432
|
+
return fields.filter(([, fieldValue]) => fieldValue != null && String(fieldValue).trim()).map(([key, fieldValue]) => `${key}=${String(fieldValue)}`);
|
|
433
|
+
};
|
|
434
|
+
var describeErrorValue2 = (value) => {
|
|
435
|
+
if (value == null) {
|
|
436
|
+
return "";
|
|
437
|
+
}
|
|
438
|
+
if (typeof value === "string") {
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
if (value instanceof Error) {
|
|
442
|
+
const message = value.message?.trim() || "";
|
|
443
|
+
const fields = getErrorFields2(value);
|
|
444
|
+
return [message, ...fields].filter(Boolean).join(" ");
|
|
445
|
+
}
|
|
446
|
+
if (typeof value === "object") {
|
|
447
|
+
const fields = getErrorFields2(value);
|
|
448
|
+
if (fields.length) {
|
|
449
|
+
return fields.join(" ");
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
return JSON.stringify(value);
|
|
453
|
+
} catch {
|
|
454
|
+
return String(value);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return String(value);
|
|
458
|
+
};
|
|
105
459
|
var normalizeQuality = (value) => typeof value === "number" ? Math.max(1, Math.min(5, Math.round(value))) : null;
|
|
460
|
+
var formatOrderSkipReason = (reason) => {
|
|
461
|
+
if (!reason) return "";
|
|
462
|
+
if (reason.startsWith("AI_QUALITY_BELOW_MIN")) {
|
|
463
|
+
return "AI_QUALITY_BELOW_MIN";
|
|
464
|
+
}
|
|
465
|
+
return reason;
|
|
466
|
+
};
|
|
106
467
|
var getLastNumber = (value) => {
|
|
107
468
|
if (Array.isArray(value)) {
|
|
108
469
|
const last = value[value.length - 1];
|
|
@@ -122,6 +483,121 @@ var getAiQualityLine = (analysis) => {
|
|
|
122
483
|
}
|
|
123
484
|
return `\u{1F534} AI Quality: ${quality}/5`;
|
|
124
485
|
};
|
|
486
|
+
var getTelegramErrorReason = (data) => {
|
|
487
|
+
if (data && typeof data === "object") {
|
|
488
|
+
const record = data;
|
|
489
|
+
const description = typeof record.description === "string" ? record.description : "";
|
|
490
|
+
const errorCode = typeof record.error_code === "number" ? record.error_code : void 0;
|
|
491
|
+
if (description && errorCode != null) {
|
|
492
|
+
return `${errorCode}: ${description}`;
|
|
493
|
+
}
|
|
494
|
+
if (description) {
|
|
495
|
+
return description;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
return JSON.stringify(data);
|
|
500
|
+
} catch {
|
|
501
|
+
return String(data);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
var getErrorMessage2 = (error) => {
|
|
505
|
+
const maybeError = error;
|
|
506
|
+
const message = describeErrorValue2(error) || String(error);
|
|
507
|
+
if (maybeError?.cause == null) {
|
|
508
|
+
return message;
|
|
509
|
+
}
|
|
510
|
+
const cause = describeErrorValue2(maybeError.cause);
|
|
511
|
+
if (!cause) {
|
|
512
|
+
return message;
|
|
513
|
+
}
|
|
514
|
+
return `${message}; cause: ${cause}`;
|
|
515
|
+
};
|
|
516
|
+
var parseTelegramResponse = async (response) => {
|
|
517
|
+
const fallback = {
|
|
518
|
+
ok: response.ok,
|
|
519
|
+
error_code: response.status,
|
|
520
|
+
description: response.statusText
|
|
521
|
+
};
|
|
522
|
+
try {
|
|
523
|
+
return await response.json();
|
|
524
|
+
} catch {
|
|
525
|
+
try {
|
|
526
|
+
const text = await response.text();
|
|
527
|
+
return text ? {
|
|
528
|
+
...fallback,
|
|
529
|
+
description: text
|
|
530
|
+
} : fallback;
|
|
531
|
+
} catch {
|
|
532
|
+
return fallback;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
var getTelegramSettings = async (userName = "root") => {
|
|
537
|
+
const settings = await getUserSettings2(userName);
|
|
538
|
+
const token = settings.TG_BOT_TOKEN;
|
|
539
|
+
const chatId = settings.TG_CHAT_ID;
|
|
540
|
+
if (!token || !chatId) {
|
|
541
|
+
throw new Error(`Telegram settings are incomplete for user ${userName}`);
|
|
542
|
+
}
|
|
543
|
+
return { token, chatId };
|
|
544
|
+
};
|
|
545
|
+
var requestTelegram = async ({
|
|
546
|
+
method,
|
|
547
|
+
token,
|
|
548
|
+
init
|
|
549
|
+
}) => {
|
|
550
|
+
let lastError = null;
|
|
551
|
+
for (let attempt = 1; attempt <= TG_REQUEST_ATTEMPTS; attempt += 1) {
|
|
552
|
+
try {
|
|
553
|
+
const res = await fetch(
|
|
554
|
+
`https://api.telegram.org/bot${token}/${method}`,
|
|
555
|
+
{
|
|
556
|
+
...init
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
return await parseTelegramResponse(res);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
lastError = error;
|
|
562
|
+
logger2.error(
|
|
563
|
+
"tg %s network failed: attempt=%d (%s)",
|
|
564
|
+
method,
|
|
565
|
+
attempt,
|
|
566
|
+
getErrorMessage2(error)
|
|
567
|
+
);
|
|
568
|
+
if (attempt < TG_REQUEST_ATTEMPTS) {
|
|
569
|
+
await delay2(TG_REQUEST_RETRY_DELAY_MS);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
throw lastError || new Error(`Telegram ${method} request failed`);
|
|
574
|
+
};
|
|
575
|
+
var sendTelegramMessage = async ({
|
|
576
|
+
message,
|
|
577
|
+
markup,
|
|
578
|
+
token,
|
|
579
|
+
chatId
|
|
580
|
+
}) => {
|
|
581
|
+
const data = await requestTelegram({
|
|
582
|
+
method: "sendMessage",
|
|
583
|
+
token,
|
|
584
|
+
init: {
|
|
585
|
+
method: "POST",
|
|
586
|
+
headers: { "Content-Type": "application/json" },
|
|
587
|
+
body: JSON.stringify({
|
|
588
|
+
chat_id: chatId,
|
|
589
|
+
text: message,
|
|
590
|
+
reply_markup: markup,
|
|
591
|
+
parse_mode: "HTML"
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
logger2.info(
|
|
596
|
+
"tg sendMessage: %s",
|
|
597
|
+
data?.ok ? "sent" : getTelegramErrorReason(data)
|
|
598
|
+
);
|
|
599
|
+
return data;
|
|
600
|
+
};
|
|
125
601
|
var formatMessage = (signal, analysis) => {
|
|
126
602
|
const {
|
|
127
603
|
symbol,
|
|
@@ -173,7 +649,9 @@ var formatMessage = (signal, analysis) => {
|
|
|
173
649
|
}
|
|
174
650
|
lines.push(orderStatusText);
|
|
175
651
|
if ((orderStatus === "skipped" || orderStatus === "canceled") && orderSkipReason) {
|
|
176
|
-
lines.push(
|
|
652
|
+
lines.push(
|
|
653
|
+
`Skip reason: <b>${escapeHtml(formatOrderSkipReason(orderSkipReason))}</b>`
|
|
654
|
+
);
|
|
177
655
|
}
|
|
178
656
|
}
|
|
179
657
|
if (isConfigFromBacktest) {
|
|
@@ -191,21 +669,9 @@ var formatMessage = (signal, analysis) => {
|
|
|
191
669
|
lines.push(aiQualityLine);
|
|
192
670
|
}
|
|
193
671
|
lines.push("");
|
|
194
|
-
if (touches) {
|
|
195
|
-
lines.push(`Points: ${touches}`);
|
|
196
|
-
}
|
|
197
|
-
if (atrPct != null && Number.isFinite(atrPct)) {
|
|
198
|
-
lines.push(`ATR: ${atrPct.toFixed(2)}`);
|
|
199
|
-
}
|
|
200
|
-
if (distance) {
|
|
201
|
-
lines.push(`Distance: ${distance}`);
|
|
202
|
-
}
|
|
203
672
|
if (correlation) {
|
|
204
673
|
lines.push(`BTC correlation: ${correlation}`);
|
|
205
674
|
}
|
|
206
|
-
if (spread != null && Number.isFinite(spread)) {
|
|
207
|
-
lines.push(`BTC spread (CB-BN)/BN: ${spread.toFixed(6)}`);
|
|
208
|
-
}
|
|
209
675
|
const prices = formatPrices();
|
|
210
676
|
if (prices) {
|
|
211
677
|
lines.push("");
|
|
@@ -219,58 +685,88 @@ var formatMessage = (signal, analysis) => {
|
|
|
219
685
|
\u0414\u0435\u0442\u0430\u043B\u0438: ${err.message || String(err)}`;
|
|
220
686
|
}
|
|
221
687
|
};
|
|
222
|
-
var sendSignal = async (signal, imgInterval, analysis) => {
|
|
688
|
+
var sendSignal = async (signal, imgInterval, analysis, options = {}) => {
|
|
223
689
|
const { symbol, signalId, interval } = signal;
|
|
690
|
+
const { token, chatId } = await getTelegramSettings(options.userName);
|
|
224
691
|
const message = formatMessage(signal, analysis);
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
692
|
+
const publicAppUrl = APP_URL2?.startsWith("https") ? APP_URL2 : null;
|
|
693
|
+
const dashboardUrl = publicAppUrl ? `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}` : null;
|
|
694
|
+
const actionButtons = [
|
|
695
|
+
dashboardUrl ? { text: "Dashboard", url: dashboardUrl } : null
|
|
696
|
+
].filter(Boolean);
|
|
697
|
+
const markup = actionButtons.length ? {
|
|
698
|
+
inline_keyboard: [actionButtons]
|
|
699
|
+
} : void 0;
|
|
700
|
+
let screenshotBytes = null;
|
|
701
|
+
try {
|
|
702
|
+
const screenshot = await getScreenshotBuffer({
|
|
703
|
+
...signal,
|
|
704
|
+
interval: imgInterval
|
|
705
|
+
});
|
|
706
|
+
screenshotBytes = screenshot.buffer.slice(
|
|
707
|
+
screenshot.byteOffset,
|
|
708
|
+
screenshot.byteOffset + screenshot.byteLength
|
|
709
|
+
);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
logger2.error(
|
|
712
|
+
"tg screenshot unavailable: %s (%s)",
|
|
713
|
+
symbol,
|
|
714
|
+
error?.message || String(error)
|
|
715
|
+
);
|
|
716
|
+
await sendTelegramMessage({ message, markup, token, chatId });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const photoBody = new FormData();
|
|
720
|
+
photoBody.set("chat_id", String(chatId || ""));
|
|
721
|
+
photoBody.set(
|
|
722
|
+
"photo",
|
|
723
|
+
new Blob([screenshotBytes], { type: "image/png" }),
|
|
724
|
+
getScreenshotFilename({ ...signal, interval: imgInterval })
|
|
725
|
+
);
|
|
726
|
+
photoBody.set("caption", message);
|
|
727
|
+
photoBody.set("parse_mode", "HTML");
|
|
728
|
+
if (markup) {
|
|
729
|
+
photoBody.set("reply_markup", JSON.stringify(markup));
|
|
730
|
+
}
|
|
731
|
+
let data;
|
|
732
|
+
try {
|
|
733
|
+
data = await requestTelegram({
|
|
734
|
+
method: "sendPhoto",
|
|
735
|
+
token,
|
|
736
|
+
init: {
|
|
737
|
+
method: "POST",
|
|
738
|
+
body: photoBody
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
} catch (error) {
|
|
742
|
+
const reason = getErrorMessage2(error);
|
|
743
|
+
logger2.error("tg sendPhoto request failed: %s", reason);
|
|
744
|
+
await sendTelegramMessage({
|
|
745
|
+
message: `${message}
|
|
746
|
+
|
|
747
|
+
\u26A0\uFE0F <b>Photo delivery failed</b>
|
|
748
|
+
Reason: <code>${escapeHtml(reason)}</code>`,
|
|
749
|
+
markup,
|
|
750
|
+
token,
|
|
751
|
+
chatId
|
|
244
752
|
});
|
|
245
753
|
return;
|
|
246
754
|
}
|
|
247
|
-
const imageUrl = getImageUrl({ ...signal, interval: imgInterval });
|
|
248
|
-
const res = await fetch(`https://api.telegram.org/bot${token}/sendPhoto`, {
|
|
249
|
-
method: "POST",
|
|
250
|
-
headers: { "Content-Type": "application/json" },
|
|
251
|
-
body: JSON.stringify({
|
|
252
|
-
chat_id: chatId,
|
|
253
|
-
photo: imageUrl,
|
|
254
|
-
caption: message,
|
|
255
|
-
reply_markup: markup,
|
|
256
|
-
parse_mode: "HTML"
|
|
257
|
-
})
|
|
258
|
-
});
|
|
259
|
-
const data = await res.json();
|
|
260
755
|
if (!data?.ok) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
756
|
+
const reason = getTelegramErrorReason(data);
|
|
757
|
+
logger2.error("tg sendPhoto failed: %s", JSON.stringify(data));
|
|
758
|
+
await sendTelegramMessage({
|
|
759
|
+
message: `${message}
|
|
760
|
+
|
|
761
|
+
\u26A0\uFE0F <b>Photo delivery failed</b>
|
|
762
|
+
Reason: <code>${escapeHtml(reason)}</code>`,
|
|
763
|
+
markup,
|
|
764
|
+
token,
|
|
765
|
+
chatId
|
|
271
766
|
});
|
|
767
|
+
return;
|
|
272
768
|
}
|
|
273
|
-
|
|
769
|
+
logger2.info("tg sendPhoto: sent");
|
|
274
770
|
};
|
|
275
771
|
var formatAnalysisMessage = (signal, analysis) => {
|
|
276
772
|
const lines = [];
|
|
@@ -314,21 +810,25 @@ ${escapeHtml(clean)}`);
|
|
|
314
810
|
}
|
|
315
811
|
return lines.join("\n");
|
|
316
812
|
};
|
|
317
|
-
var sendSignalAnalysis = async (signal, analysis) => {
|
|
813
|
+
var sendSignalAnalysis = async (signal, analysis, options = {}) => {
|
|
814
|
+
const { token, chatId } = await getTelegramSettings(options.userName);
|
|
318
815
|
const message = formatAnalysisMessage(signal, analysis);
|
|
319
|
-
const
|
|
320
|
-
method: "
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
816
|
+
const data = await requestTelegram({
|
|
817
|
+
method: "sendMessage",
|
|
818
|
+
token,
|
|
819
|
+
init: {
|
|
820
|
+
method: "POST",
|
|
821
|
+
headers: { "Content-Type": "application/json" },
|
|
822
|
+
body: JSON.stringify({
|
|
823
|
+
chat_id: chatId,
|
|
824
|
+
text: message,
|
|
825
|
+
parse_mode: "HTML"
|
|
826
|
+
})
|
|
827
|
+
}
|
|
327
828
|
});
|
|
328
|
-
|
|
329
|
-
logger.info(
|
|
829
|
+
logger2.info(
|
|
330
830
|
"tg sendMessage (analysis): %s",
|
|
331
|
-
data?.ok ? "sent" :
|
|
831
|
+
data?.ok ? "sent" : getTelegramErrorReason(data)
|
|
332
832
|
);
|
|
333
833
|
};
|
|
334
834
|
|
|
@@ -342,7 +842,7 @@ var cleanFiles = async (dir) => {
|
|
|
342
842
|
total: files.length,
|
|
343
843
|
width: 30
|
|
344
844
|
});
|
|
345
|
-
|
|
845
|
+
logger3.info(chalk.yellow("clean:", dir));
|
|
346
846
|
for await (const file of files) {
|
|
347
847
|
completed++;
|
|
348
848
|
const fullPath = `${dir}/${file}`;
|
|
@@ -356,7 +856,7 @@ var cleanFiles = async (dir) => {
|
|
|
356
856
|
bar.tick(completed === files.length ? completed % 100 : 100);
|
|
357
857
|
}
|
|
358
858
|
}
|
|
359
|
-
|
|
859
|
+
logger3.info("");
|
|
360
860
|
};
|
|
361
861
|
var cleanRedis = async (area) => {
|
|
362
862
|
let completed = 0;
|
|
@@ -365,7 +865,7 @@ var cleanRedis = async (area) => {
|
|
|
365
865
|
total: keys.length,
|
|
366
866
|
width: 30
|
|
367
867
|
});
|
|
368
|
-
|
|
868
|
+
logger3.info(chalk.yellow("clean:", area));
|
|
369
869
|
try {
|
|
370
870
|
for await (const key of keys) {
|
|
371
871
|
completed++;
|
|
@@ -376,21 +876,21 @@ var cleanRedis = async (area) => {
|
|
|
376
876
|
}
|
|
377
877
|
} catch (e) {
|
|
378
878
|
if (e instanceof RedisWriteBlockedError || String(e).includes("MISCONF")) {
|
|
379
|
-
|
|
879
|
+
logger3.error(
|
|
380
880
|
"Redis write is blocked by MISCONF (RDB save failure). Cleanup stopped early."
|
|
381
881
|
);
|
|
382
|
-
|
|
882
|
+
logger3.error(
|
|
383
883
|
"Check Redis logs and fix persistence/memory pressure before retry."
|
|
384
884
|
);
|
|
385
|
-
|
|
885
|
+
logger3.info("");
|
|
386
886
|
return;
|
|
387
887
|
}
|
|
388
888
|
throw e;
|
|
389
889
|
}
|
|
390
|
-
|
|
890
|
+
logger3.info("");
|
|
391
891
|
};
|
|
392
|
-
var update = async (connector, interval, tickers) => {
|
|
393
|
-
const PRELOAD_START = getTimestamp(
|
|
892
|
+
var update = async (connector, interval, tickers, preloadDays = PRELOAD_DAYS) => {
|
|
893
|
+
const PRELOAD_START = getTimestamp(preloadDays);
|
|
394
894
|
const PRELOAD_END = getTimestamp();
|
|
395
895
|
const bar = new ProgressBar(
|
|
396
896
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
@@ -399,7 +899,11 @@ var update = async (connector, interval, tickers) => {
|
|
|
399
899
|
width: 30
|
|
400
900
|
}
|
|
401
901
|
);
|
|
402
|
-
|
|
902
|
+
logger3.info(
|
|
903
|
+
chalk.yellow(
|
|
904
|
+
`update: ${tickers.length} (klineConcurrency=${KLINE_CONCURRENCY_LIMIT}, preloadDays=${preloadDays})`
|
|
905
|
+
)
|
|
906
|
+
);
|
|
403
907
|
const queue = tickers.slice();
|
|
404
908
|
if (!queue.includes("BTCUSDT")) {
|
|
405
909
|
queue.unshift("BTCUSDT");
|
|
@@ -411,15 +915,16 @@ var update = async (connector, interval, tickers) => {
|
|
|
411
915
|
start: PRELOAD_START,
|
|
412
916
|
end: PRELOAD_END,
|
|
413
917
|
interval,
|
|
414
|
-
silent: true
|
|
918
|
+
silent: true,
|
|
919
|
+
warmOnly: true
|
|
415
920
|
});
|
|
416
921
|
} catch {
|
|
417
|
-
|
|
922
|
+
logger3.error("Failed loading: %s", symbol);
|
|
418
923
|
} finally {
|
|
419
924
|
bar.tick(1, { symbol: chalk.gray(symbol) });
|
|
420
925
|
}
|
|
421
926
|
});
|
|
422
|
-
|
|
927
|
+
logger3.info("");
|
|
423
928
|
};
|
|
424
929
|
var parseSymbolsFromCLI = (symbol = "") => symbol.split(",").map((s) => {
|
|
425
930
|
const ticker = s.toUpperCase();
|
|
@@ -457,14 +962,14 @@ var getTickers = async (connector, include = "", exclude = "", limit, chunk) =>
|
|
|
457
962
|
}
|
|
458
963
|
if (chunk) {
|
|
459
964
|
const [currentChunk, chunksCount] = chunk.split("/").map((c) => parseInt(c));
|
|
460
|
-
|
|
965
|
+
logger3.info("chunk: %d / %d", currentChunk, chunksCount);
|
|
461
966
|
const chunkSize = Math.ceil(tickers.length / chunksCount);
|
|
462
967
|
const chunks = import_lodash.default.chunk(tickers, chunkSize);
|
|
463
968
|
tickers = chunks[currentChunk - 1];
|
|
464
969
|
}
|
|
465
970
|
return tickers.filter((t) => !excludeTickers.includes(t));
|
|
466
971
|
};
|
|
467
|
-
var makeScreenshots = async (signals, interval) => {
|
|
972
|
+
var makeScreenshots = async (signals, interval, userName = "root") => {
|
|
468
973
|
const projectRoot = getProjectRoot2();
|
|
469
974
|
const bar = new ProgressBar(
|
|
470
975
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
@@ -473,15 +978,15 @@ var makeScreenshots = async (signals, interval) => {
|
|
|
473
978
|
width: 30
|
|
474
979
|
}
|
|
475
980
|
);
|
|
476
|
-
|
|
981
|
+
logger3.info(chalk.yellow("screenshots:", `${interval}m`, signals.length));
|
|
477
982
|
await runWithConcurrency(
|
|
478
983
|
signals,
|
|
479
984
|
SCREENSHOT_CONCURRENCY_LIMIT,
|
|
480
985
|
async (signal) => {
|
|
481
986
|
try {
|
|
482
|
-
await screenDashboard({ ...signal, interval }, projectRoot);
|
|
987
|
+
await screenDashboard({ ...signal, interval }, projectRoot, userName);
|
|
483
988
|
} catch (error) {
|
|
484
|
-
|
|
989
|
+
logger3.error(
|
|
485
990
|
"Failed screenshot: %s (%s)",
|
|
486
991
|
signal.symbol,
|
|
487
992
|
error?.message || String(error)
|
|
@@ -491,9 +996,9 @@ var makeScreenshots = async (signals, interval) => {
|
|
|
491
996
|
}
|
|
492
997
|
}
|
|
493
998
|
);
|
|
494
|
-
|
|
999
|
+
logger3.info("");
|
|
495
1000
|
};
|
|
496
|
-
var sendToAI = async (signals) => {
|
|
1001
|
+
var sendToAI = async (signals, userName = "root") => {
|
|
497
1002
|
const bar = new ProgressBar(
|
|
498
1003
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
499
1004
|
{
|
|
@@ -501,19 +1006,19 @@ var sendToAI = async (signals) => {
|
|
|
501
1006
|
width: 30
|
|
502
1007
|
}
|
|
503
1008
|
);
|
|
504
|
-
|
|
1009
|
+
logger3.info(chalk.yellow("AI:", signals.length));
|
|
505
1010
|
await runWithConcurrency(signals, AI_CONCURRENCY_LIMIT, async (signal) => {
|
|
506
1011
|
try {
|
|
507
|
-
await askAI(signal);
|
|
1012
|
+
await askAI(signal, { userName });
|
|
508
1013
|
} catch {
|
|
509
|
-
|
|
1014
|
+
logger3.error("Failed ask: %s", signal.symbol);
|
|
510
1015
|
} finally {
|
|
511
1016
|
bar.tick(1, { symbol: chalk.gray(signal.symbol) });
|
|
512
1017
|
}
|
|
513
1018
|
});
|
|
514
|
-
|
|
1019
|
+
logger3.info("");
|
|
515
1020
|
};
|
|
516
|
-
var sendToTG = async (signals, imgInterval) => {
|
|
1021
|
+
var sendToTG = async (signals, imgInterval, userName = "root") => {
|
|
517
1022
|
const bar = new ProgressBar(
|
|
518
1023
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
519
1024
|
{
|
|
@@ -521,19 +1026,19 @@ var sendToTG = async (signals, imgInterval) => {
|
|
|
521
1026
|
width: 30
|
|
522
1027
|
}
|
|
523
1028
|
);
|
|
524
|
-
|
|
525
|
-
await runWithConcurrency(signals,
|
|
1029
|
+
logger3.info(chalk.yellow("messages:", signals.length));
|
|
1030
|
+
await runWithConcurrency(signals, 1, async (signal) => {
|
|
526
1031
|
try {
|
|
527
|
-
const analysis = await
|
|
528
|
-
|
|
1032
|
+
const analysis = await getData(
|
|
1033
|
+
redisKeys.analysis(signal.symbol, signal.signalId),
|
|
529
1034
|
null
|
|
530
1035
|
);
|
|
531
|
-
await sendSignal(signal, imgInterval, analysis);
|
|
1036
|
+
await sendSignal(signal, imgInterval, analysis, { userName });
|
|
532
1037
|
if (analysis && typeof analysis === "object" && Object.keys(analysis).length > 0) {
|
|
533
|
-
await sendSignalAnalysis(signal, analysis);
|
|
1038
|
+
await sendSignalAnalysis(signal, analysis, { userName });
|
|
534
1039
|
}
|
|
535
1040
|
} catch (err) {
|
|
536
|
-
|
|
1041
|
+
logger3.error(
|
|
537
1042
|
"Failed sent: %s %s",
|
|
538
1043
|
signal.symbol,
|
|
539
1044
|
err?.message || String(err)
|
|
@@ -542,7 +1047,7 @@ var sendToTG = async (signals, imgInterval) => {
|
|
|
542
1047
|
bar.tick(1, { symbol: chalk.gray(signal.symbol) });
|
|
543
1048
|
}
|
|
544
1049
|
});
|
|
545
|
-
|
|
1050
|
+
logger3.info("");
|
|
546
1051
|
};
|
|
547
1052
|
export {
|
|
548
1053
|
cleanFiles,
|