@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/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
- TG_CONCURRENCY_LIMIT
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-LMAKIC3C.mjs";
13
- import "./chunk-ZY6ULOWK.mjs";
11
+ } from "./chunk-72FKXJ2I.mjs";
12
+ import "./chunk-EOZSJKUM.mjs";
14
13
  import {
15
14
  getTradejsProjectCwd
16
- } from "./chunk-P2ZUWONT.mjs";
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 as getData2,
37
- redisKeys as redisKeys2
35
+ getData,
36
+ redisKeys
38
37
  } from "@tradejs/infra/redis";
39
- import { logger as logger2 } from "@tradejs/infra/logger";
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 { getData, redisKeys } from "@tradejs/infra/redis";
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 getImageUrl = ({ symbol, signalId, interval }) => `${APP_URL}/api/files/screenshot/${symbol}_${signalId}_${interval}`;
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
- `${symbol}_${signalId}_${interval}.png`
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 rootUser = await getData(redisKeys.user("root"), null);
60
- const token2 = rootUser && typeof rootUser === "object" ? rootUser.token : null;
61
- const tokenParam = typeof token2 === "string" && token2.length > 0 ? `&token=${encodeURIComponent(token2)}` : "";
62
- const browser = await puppeteer.launch({
63
- headless: true,
64
- executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
65
- args: [
66
- "--no-sandbox",
67
- "--disable-setuid-sandbox",
68
- "--disable-dev-shm-usage",
69
- "--disable-gpu",
70
- "--font-render-hinting=medium"
71
- ]
72
- });
73
- try {
74
- const page = await browser.newPage();
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
- await page.setViewport({
77
- width: 1400,
78
- height: 960,
79
- deviceScaleFactor: 2
80
- });
81
- await page.goto(
82
- `${APP_URL}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true${tokenParam}`
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
- await delay(1e4);
85
- await fs.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
86
- await page.screenshot({
87
- path: getScreenshotPath(signal, projectRoot)
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
- await page.close();
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
103
412
  };
104
- var { APP_URL: APP_URL2, TG_BOT_TOKEN: token, TG_CHAT_ID: chatId } = process.env;
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(`Skip reason: <b>${escapeHtml(orderSkipReason)}</b>`);
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 markup = {
226
- inline_keyboard: [
227
- [
228
- {
229
- text: "Dashboard",
230
- url: `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}`
231
- }
232
- ]
233
- ]
234
- };
235
- if (!APP_URL2?.startsWith("https")) {
236
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
237
- method: "POST",
238
- headers: { "Content-Type": "application/json" },
239
- body: JSON.stringify({
240
- chat_id: chatId,
241
- text: message,
242
- parse_mode: "HTML"
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
- logger.error("tg sendPhoto failed: %s", JSON.stringify(data));
262
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
263
- method: "POST",
264
- headers: { "Content-Type": "application/json" },
265
- body: JSON.stringify({
266
- chat_id: chatId,
267
- text: message,
268
- reply_markup: markup,
269
- parse_mode: "HTML"
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
- logger.info("tg sendPhoto: %s", data?.ok ? "sent" : JSON.stringify(data));
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 res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
320
- method: "POST",
321
- headers: { "Content-Type": "application/json" },
322
- body: JSON.stringify({
323
- chat_id: chatId,
324
- text: message,
325
- parse_mode: "HTML"
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
- const data = await res.json();
329
- logger.info(
829
+ logger2.info(
330
830
  "tg sendMessage (analysis): %s",
331
- data?.ok ? "sent" : JSON.stringify(data)
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
- logger2.info(chalk.yellow("clean:", dir));
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
- logger2.info("");
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
- logger2.info(chalk.yellow("clean:", area));
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
- logger2.error(
879
+ logger3.error(
380
880
  "Redis write is blocked by MISCONF (RDB save failure). Cleanup stopped early."
381
881
  );
382
- logger2.error(
882
+ logger3.error(
383
883
  "Check Redis logs and fix persistence/memory pressure before retry."
384
884
  );
385
- logger2.info("");
885
+ logger3.info("");
386
886
  return;
387
887
  }
388
888
  throw e;
389
889
  }
390
- logger2.info("");
890
+ logger3.info("");
391
891
  };
392
- var update = async (connector, interval, tickers) => {
393
- const PRELOAD_START = getTimestamp(PRELOAD_DAYS);
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
- logger2.info(chalk.yellow("update:", tickers.length));
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
- logger2.error("Failed loading: %s", symbol);
922
+ logger3.error("Failed loading: %s", symbol);
418
923
  } finally {
419
924
  bar.tick(1, { symbol: chalk.gray(symbol) });
420
925
  }
421
926
  });
422
- logger2.info("");
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
- logger2.info("chunk: %d / %d", currentChunk, chunksCount);
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
- logger2.info(chalk.yellow("screenshots:", `${interval}m`, signals.length));
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
- logger2.error(
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
- logger2.info("");
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
- logger2.info(chalk.yellow("AI:", signals.length));
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
- logger2.error("Failed ask: %s", signal.symbol);
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
- logger2.info("");
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
- logger2.info(chalk.yellow("messages:", signals.length));
525
- await runWithConcurrency(signals, TG_CONCURRENCY_LIMIT, async (signal) => {
1029
+ logger3.info(chalk.yellow("messages:", signals.length));
1030
+ await runWithConcurrency(signals, 1, async (signal) => {
526
1031
  try {
527
- const analysis = await getData2(
528
- redisKeys2.analysis(signal.symbol, signal.signalId),
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
- logger2.error(
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
- logger2.info("");
1050
+ logger3.info("");
546
1051
  };
547
1052
  export {
548
1053
  cleanFiles,