@tradejs/node 1.0.4 → 1.0.5

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
@@ -3,7 +3,7 @@ import {
3
3
  KLINE_CONCURRENCY_LIMIT,
4
4
  SCREENSHOT_CONCURRENCY_LIMIT,
5
5
  TG_CONCURRENCY_LIMIT
6
- } from "./chunk-4ZPGZWO7.mjs";
6
+ } from "./chunk-SCMBUEGK.mjs";
7
7
  import {
8
8
  require_lodash
9
9
  } from "./chunk-GKDBAF3A.mjs";
@@ -36,72 +36,332 @@ import {
36
36
  getData as getData2,
37
37
  redisKeys as redisKeys2
38
38
  } from "@tradejs/infra/redis";
39
- import { logger as logger2 } from "@tradejs/infra/logger";
39
+ import { logger as logger3 } from "@tradejs/infra/logger";
40
40
 
41
41
  // src/screenshot.ts
42
42
  import fs from "fs/promises";
43
43
  import path from "path";
44
44
  import puppeteer from "puppeteer";
45
45
  import { delay } from "@tradejs/core/async";
46
+ import { logger } from "@tradejs/infra/logger";
46
47
  import { getData, redisKeys } from "@tradejs/infra/redis";
47
48
  var { APP_URL } = process.env;
49
+ var SCREENSHOT_NAVIGATION_ATTEMPTS = 3;
50
+ var SCREENSHOT_NAVIGATION_RETRY_DELAY_MS = 2e3;
51
+ var SCREENSHOT_CAPTURE_ATTEMPTS = 2;
52
+ var SCREENSHOT_CAPTURE_RETRY_DELAY_MS = 2e3;
53
+ var SCREENSHOT_NAVIGATION_TIMEOUT_MS = 3e4;
54
+ var SCREENSHOT_RENDER_DELAY_MS = 1e4;
55
+ var SCREENSHOT_CONSOLE_LOG_LIMIT = 20;
56
+ var SCREENSHOT_CONSOLE_TEXT_LIMIT = 500;
57
+ var SCREENSHOT_BROWSER_STDERR = process.env.SCREENSHOT_BROWSER_STDERR === "1";
58
+ var PUPPETEER_DUMPIO = process.env.PUPPETEER_DUMPIO === "1";
59
+ var SCREENSHOT_VIEWPORT = {
60
+ width: 1280,
61
+ height: 800,
62
+ deviceScaleFactor: process.env.NODE_ENV === "production" ? 1 : 2
63
+ };
48
64
  var getProjectRoot = (projectRoot) => path.resolve(getTradejsProjectCwd(projectRoot));
49
65
  var getScreenshotsDir = (projectRoot) => path.join(getProjectRoot(projectRoot), "data", "screenshots");
50
- var getImageUrl = ({ symbol, signalId, interval }) => `${APP_URL}/api/files/screenshot/${symbol}_${signalId}_${interval}`;
66
+ var maskTokenInUrl = (url) => url.replace(/([?&]token=)[^&]+/i, "$1<hidden>");
67
+ var getErrorMessage = (error) => {
68
+ const maybeError = error;
69
+ const message = maybeError?.message || String(error);
70
+ if (maybeError?.cause == null) {
71
+ return message;
72
+ }
73
+ const cause = maybeError.cause instanceof Error ? maybeError.cause.message : String(maybeError.cause);
74
+ return `${message}; cause: ${cause}`;
75
+ };
76
+ var truncateText = (value, maxLength = SCREENSHOT_CONSOLE_TEXT_LIMIT) => value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
77
+ var shouldLogConsoleMessage = (type) => ["error", "warning", "assert"].includes(type);
78
+ var getScreenshotBuffer = async (signal, projectRoot) => {
79
+ const screenshotPath = getScreenshotPath(signal, projectRoot);
80
+ return fs.readFile(screenshotPath);
81
+ };
82
+ var getScreenshotFilename = ({
83
+ symbol,
84
+ signalId,
85
+ interval
86
+ }) => `${symbol}_${signalId}_${interval}.png`;
87
+ var getScreenshotRenderBaseUrl = () => {
88
+ const fallback = String(APP_URL || "").trim();
89
+ if (fallback) {
90
+ return fallback;
91
+ }
92
+ throw new Error("APP_URL is required for screenshots");
93
+ };
51
94
  var getScreenshotPath = ({ symbol, signalId, interval }, projectRoot) => {
52
95
  return path.join(
53
96
  getScreenshotsDir(projectRoot),
54
- `${symbol}_${signalId}_${interval}.png`
97
+ getScreenshotFilename({ symbol, signalId, interval })
55
98
  );
56
99
  };
57
100
  var screenDashboard = async (signal, projectRoot) => {
58
101
  const { symbol, signalId, interval } = signal;
102
+ const screenshotBaseUrl = getScreenshotRenderBaseUrl();
103
+ const screenshotPath = getScreenshotPath(signal, projectRoot);
59
104
  const rootUser = await getData(redisKeys.user("root"), null);
60
105
  const token2 = rootUser && typeof rootUser === "object" ? rootUser.token : null;
61
106
  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();
107
+ const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1${tokenParam}`;
108
+ const maskedDashboardUrl = maskTokenInUrl(dashboardUrl);
109
+ logger.info(
110
+ "screenshot start: %s %sm url=%s path=%s",
111
+ symbol,
112
+ interval,
113
+ maskedDashboardUrl,
114
+ screenshotPath
115
+ );
116
+ let captureError = null;
117
+ for (let captureAttempt = 1; captureAttempt <= SCREENSHOT_CAPTURE_ATTEMPTS; captureAttempt += 1) {
118
+ const browser = await puppeteer.launch({
119
+ headless: true,
120
+ dumpio: PUPPETEER_DUMPIO,
121
+ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
122
+ args: [
123
+ "--no-sandbox",
124
+ "--disable-setuid-sandbox",
125
+ "--disable-dev-shm-usage",
126
+ "--disable-gpu",
127
+ "--font-render-hinting=medium"
128
+ ]
129
+ });
130
+ const browserProcess = browser.process();
75
131
  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}`
132
+ if (browserProcess) {
133
+ logger.info(
134
+ "screenshot browser: %s %sm captureAttempt=%d pid=%s exec=%s",
135
+ symbol,
136
+ interval,
137
+ captureAttempt,
138
+ String(browserProcess.pid ?? "unknown"),
139
+ browserProcess.spawnfile || "unknown"
140
+ );
141
+ }
142
+ logger.info(
143
+ "screenshot browser version: %s %sm captureAttempt=%d version=%s",
144
+ symbol,
145
+ interval,
146
+ captureAttempt,
147
+ await browser.version()
83
148
  );
84
- await delay(1e4);
85
- await fs.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
86
- await page.screenshot({
87
- path: getScreenshotPath(signal, projectRoot)
149
+ browser.on("disconnected", () => {
150
+ logger.error(
151
+ "screenshot browser disconnected: %s %sm captureAttempt=%d",
152
+ symbol,
153
+ interval,
154
+ captureAttempt
155
+ );
88
156
  });
157
+ if (SCREENSHOT_BROWSER_STDERR && browserProcess?.stderr) {
158
+ browserProcess.stderr.on("data", (chunk) => {
159
+ const text = String(chunk).trim();
160
+ if (!text) {
161
+ return;
162
+ }
163
+ logger.error(
164
+ "screenshot browser stderr: %s %sm captureAttempt=%d (%s)",
165
+ symbol,
166
+ interval,
167
+ captureAttempt,
168
+ truncateText(text)
169
+ );
170
+ });
171
+ }
172
+ const page = await browser.newPage();
173
+ try {
174
+ let consoleMessagesLogged = 0;
175
+ page.on("requestfailed", (request) => {
176
+ if (!request.isNavigationRequest() && request.resourceType() !== "document") {
177
+ return;
178
+ }
179
+ logger.error(
180
+ "screenshot request failed: %s %sm %s (%s)",
181
+ symbol,
182
+ interval,
183
+ maskTokenInUrl(request.url()),
184
+ request.failure()?.errorText || "unknown"
185
+ );
186
+ });
187
+ page.on("pageerror", (error) => {
188
+ logger.error(
189
+ "screenshot page error: %s %sm (%s)",
190
+ symbol,
191
+ interval,
192
+ error.message || String(error)
193
+ );
194
+ });
195
+ page.on("error", (error) => {
196
+ logger.error(
197
+ "screenshot target error: %s %sm (%s)",
198
+ symbol,
199
+ interval,
200
+ error.message || String(error)
201
+ );
202
+ });
203
+ page.on("close", () => {
204
+ logger.info(
205
+ "screenshot page closed: %s %sm captureAttempt=%d",
206
+ symbol,
207
+ interval,
208
+ captureAttempt
209
+ );
210
+ });
211
+ page.on("console", (message) => {
212
+ if (!shouldLogConsoleMessage(message.type())) {
213
+ return;
214
+ }
215
+ if (consoleMessagesLogged >= SCREENSHOT_CONSOLE_LOG_LIMIT) {
216
+ return;
217
+ }
218
+ consoleMessagesLogged += 1;
219
+ const location = message.location();
220
+ const suffix = location.url ? ` ${maskTokenInUrl(location.url)}:${location.lineNumber ?? 0}:${location.columnNumber ?? 0}` : "";
221
+ logger.error(
222
+ "screenshot console %s: %s %sm captureAttempt=%d (%s%s)",
223
+ message.type(),
224
+ symbol,
225
+ interval,
226
+ captureAttempt,
227
+ truncateText(message.text()),
228
+ suffix
229
+ );
230
+ });
231
+ page.on("response", (response) => {
232
+ const status = response.status();
233
+ if (status < 400) {
234
+ return;
235
+ }
236
+ const request = response.request();
237
+ const resourceType = request.resourceType();
238
+ if (!["document", "script", "stylesheet", "fetch", "xhr"].includes(
239
+ resourceType
240
+ )) {
241
+ return;
242
+ }
243
+ logger.error(
244
+ "screenshot response failed: %s %sm captureAttempt=%d status=%d type=%s url=%s",
245
+ symbol,
246
+ interval,
247
+ captureAttempt,
248
+ status,
249
+ resourceType,
250
+ maskTokenInUrl(response.url())
251
+ );
252
+ });
253
+ await page.setViewport(SCREENSHOT_VIEWPORT);
254
+ let gotoError = null;
255
+ logger.info(
256
+ "screenshot render: %s %sm captureAttempt=%d viewport=%dx%d@%d",
257
+ symbol,
258
+ interval,
259
+ captureAttempt,
260
+ SCREENSHOT_VIEWPORT.width,
261
+ SCREENSHOT_VIEWPORT.height,
262
+ SCREENSHOT_VIEWPORT.deviceScaleFactor
263
+ );
264
+ for (let attempt = 1; attempt <= SCREENSHOT_NAVIGATION_ATTEMPTS; attempt += 1) {
265
+ try {
266
+ logger.info(
267
+ "screenshot goto: %s %sm captureAttempt=%d attempt=%d url=%s",
268
+ symbol,
269
+ interval,
270
+ captureAttempt,
271
+ attempt,
272
+ maskedDashboardUrl
273
+ );
274
+ const response = await page.goto(dashboardUrl, {
275
+ waitUntil: "domcontentloaded",
276
+ timeout: SCREENSHOT_NAVIGATION_TIMEOUT_MS
277
+ });
278
+ logger.info(
279
+ "screenshot goto ok: %s %sm captureAttempt=%d attempt=%d status=%s finalUrl=%s",
280
+ symbol,
281
+ interval,
282
+ captureAttempt,
283
+ attempt,
284
+ response ? String(response.status()) : "null",
285
+ maskTokenInUrl(page.url())
286
+ );
287
+ logger.info(
288
+ "screenshot page info: %s %sm captureAttempt=%d title=%s ua=%s",
289
+ symbol,
290
+ interval,
291
+ captureAttempt,
292
+ truncateText(await page.title(), 120),
293
+ truncateText(await page.evaluate(() => navigator.userAgent), 240)
294
+ );
295
+ gotoError = null;
296
+ break;
297
+ } catch (error) {
298
+ gotoError = error;
299
+ logger.error(
300
+ "screenshot goto failed: %s %sm captureAttempt=%d attempt=%d url=%s (%s)",
301
+ symbol,
302
+ interval,
303
+ captureAttempt,
304
+ attempt,
305
+ maskedDashboardUrl,
306
+ gotoError.message || String(gotoError)
307
+ );
308
+ if (attempt >= SCREENSHOT_NAVIGATION_ATTEMPTS) {
309
+ break;
310
+ }
311
+ await delay(SCREENSHOT_NAVIGATION_RETRY_DELAY_MS);
312
+ }
313
+ }
314
+ if (gotoError) {
315
+ throw new Error(
316
+ `Failed to open dashboard ${dashboardUrl}: ${gotoError.message || String(gotoError)}`
317
+ );
318
+ }
319
+ await delay(SCREENSHOT_RENDER_DELAY_MS);
320
+ await fs.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
321
+ await page.screenshot({
322
+ path: screenshotPath,
323
+ type: "png"
324
+ });
325
+ logger.info(
326
+ "screenshot saved: %s %sm path=%s",
327
+ symbol,
328
+ interval,
329
+ screenshotPath
330
+ );
331
+ return;
332
+ } finally {
333
+ await page.close().catch(() => void 0);
334
+ }
335
+ } catch (error) {
336
+ captureError = error;
337
+ logger.error(
338
+ "screenshot capture failed: %s %sm captureAttempt=%d (%s)",
339
+ symbol,
340
+ interval,
341
+ captureAttempt,
342
+ getErrorMessage(captureError)
343
+ );
344
+ if (captureAttempt < SCREENSHOT_CAPTURE_ATTEMPTS) {
345
+ await delay(SCREENSHOT_CAPTURE_RETRY_DELAY_MS);
346
+ }
89
347
  } finally {
90
- await page.close();
348
+ await browser.close().catch(() => void 0);
91
349
  }
92
- } finally {
93
- await browser.close();
94
350
  }
351
+ throw captureError || new Error(`Failed screenshot: ${symbol} ${interval}`);
95
352
  };
96
353
 
97
354
  // src/signals.ts
355
+ import { delay as delay2 } from "@tradejs/core/async";
98
356
  import { formatNumber } from "@tradejs/core/math";
99
- import { logger } from "@tradejs/infra/logger";
357
+ import { logger as logger2 } from "@tradejs/infra/logger";
100
358
  var escapeHtml = (s) => {
101
359
  if (!s) return "";
102
360
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
103
361
  };
104
362
  var { APP_URL: APP_URL2, TG_BOT_TOKEN: token, TG_CHAT_ID: chatId } = process.env;
363
+ var TG_REQUEST_ATTEMPTS = 3;
364
+ var TG_REQUEST_RETRY_DELAY_MS = 2e3;
105
365
  var normalizeQuality = (value) => typeof value === "number" ? Math.max(1, Math.min(5, Math.round(value))) : null;
106
366
  var getLastNumber = (value) => {
107
367
  if (Array.isArray(value)) {
@@ -122,6 +382,99 @@ var getAiQualityLine = (analysis) => {
122
382
  }
123
383
  return `\u{1F534} AI Quality: ${quality}/5`;
124
384
  };
385
+ var getTelegramErrorReason = (data) => {
386
+ if (data && typeof data === "object") {
387
+ const record = data;
388
+ const description = typeof record.description === "string" ? record.description : "";
389
+ const errorCode = typeof record.error_code === "number" ? record.error_code : void 0;
390
+ if (description && errorCode != null) {
391
+ return `${errorCode}: ${description}`;
392
+ }
393
+ if (description) {
394
+ return description;
395
+ }
396
+ }
397
+ try {
398
+ return JSON.stringify(data);
399
+ } catch {
400
+ return String(data);
401
+ }
402
+ };
403
+ var getErrorMessage2 = (error) => {
404
+ const maybeError = error;
405
+ const message = maybeError?.message || String(error);
406
+ if (maybeError?.cause == null) {
407
+ return message;
408
+ }
409
+ const cause = maybeError.cause instanceof Error ? maybeError.cause.message : String(maybeError.cause);
410
+ return `${message}; cause: ${cause}`;
411
+ };
412
+ var parseTelegramResponse = async (response) => {
413
+ const fallback = {
414
+ ok: response.ok,
415
+ error_code: response.status,
416
+ description: response.statusText
417
+ };
418
+ try {
419
+ return await response.json();
420
+ } catch {
421
+ try {
422
+ const text = await response.text();
423
+ return text ? {
424
+ ...fallback,
425
+ description: text
426
+ } : fallback;
427
+ } catch {
428
+ return fallback;
429
+ }
430
+ }
431
+ };
432
+ var requestTelegram = async (method, init) => {
433
+ let lastError = null;
434
+ for (let attempt = 1; attempt <= TG_REQUEST_ATTEMPTS; attempt += 1) {
435
+ try {
436
+ const res = await fetch(
437
+ `https://api.telegram.org/bot${token}/${method}`,
438
+ {
439
+ ...init
440
+ }
441
+ );
442
+ return await parseTelegramResponse(res);
443
+ } catch (error) {
444
+ lastError = error;
445
+ logger2.error(
446
+ "tg %s network failed: attempt=%d (%s)",
447
+ method,
448
+ attempt,
449
+ getErrorMessage2(error)
450
+ );
451
+ if (attempt < TG_REQUEST_ATTEMPTS) {
452
+ await delay2(TG_REQUEST_RETRY_DELAY_MS);
453
+ }
454
+ }
455
+ }
456
+ throw lastError || new Error(`Telegram ${method} request failed`);
457
+ };
458
+ var sendTelegramMessage = async ({
459
+ message,
460
+ markup
461
+ }) => {
462
+ const data = await requestTelegram("sendMessage", {
463
+ method: "POST",
464
+ headers: { "Content-Type": "application/json" },
465
+ body: JSON.stringify({
466
+ chat_id: chatId,
467
+ text: message,
468
+ reply_markup: markup,
469
+ parse_mode: "HTML"
470
+ })
471
+ });
472
+ logger2.info(
473
+ "tg sendMessage: %s",
474
+ data?.ok ? "sent" : getTelegramErrorReason(data)
475
+ );
476
+ return data;
477
+ };
125
478
  var formatMessage = (signal, analysis) => {
126
479
  const {
127
480
  symbol,
@@ -222,55 +575,76 @@ var formatMessage = (signal, analysis) => {
222
575
  var sendSignal = async (signal, imgInterval, analysis) => {
223
576
  const { symbol, signalId, interval } = signal;
224
577
  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`, {
578
+ const publicAppUrl = APP_URL2?.startsWith("https") ? APP_URL2 : null;
579
+ const dashboardUrl = publicAppUrl ? `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}` : null;
580
+ const actionButtons = [
581
+ dashboardUrl ? { text: "Dashboard", url: dashboardUrl } : null
582
+ ].filter(Boolean);
583
+ const markup = actionButtons.length ? {
584
+ inline_keyboard: [actionButtons]
585
+ } : void 0;
586
+ let screenshotBytes = null;
587
+ try {
588
+ const screenshot = await getScreenshotBuffer({
589
+ ...signal,
590
+ interval: imgInterval
591
+ });
592
+ screenshotBytes = screenshot.buffer.slice(
593
+ screenshot.byteOffset,
594
+ screenshot.byteOffset + screenshot.byteLength
595
+ );
596
+ } catch (error) {
597
+ logger2.error(
598
+ "tg screenshot unavailable: %s (%s)",
599
+ symbol,
600
+ error?.message || String(error)
601
+ );
602
+ await sendTelegramMessage({ message, markup });
603
+ return;
604
+ }
605
+ const photoBody = new FormData();
606
+ photoBody.set("chat_id", String(chatId || ""));
607
+ photoBody.set(
608
+ "photo",
609
+ new Blob([screenshotBytes], { type: "image/png" }),
610
+ getScreenshotFilename({ ...signal, interval: imgInterval })
611
+ );
612
+ photoBody.set("caption", message);
613
+ photoBody.set("parse_mode", "HTML");
614
+ if (markup) {
615
+ photoBody.set("reply_markup", JSON.stringify(markup));
616
+ }
617
+ let data;
618
+ try {
619
+ data = await requestTelegram("sendPhoto", {
237
620
  method: "POST",
238
- headers: { "Content-Type": "application/json" },
239
- body: JSON.stringify({
240
- chat_id: chatId,
241
- text: message,
242
- parse_mode: "HTML"
243
- })
621
+ body: photoBody
622
+ });
623
+ } catch (error) {
624
+ const reason = getErrorMessage2(error);
625
+ logger2.error("tg sendPhoto request failed: %s", reason);
626
+ await sendTelegramMessage({
627
+ message: `${message}
628
+
629
+ \u26A0\uFE0F <b>Photo delivery failed</b>
630
+ Reason: <code>${escapeHtml(reason)}</code>`,
631
+ markup
244
632
  });
245
633
  return;
246
634
  }
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
635
  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
- })
636
+ const reason = getTelegramErrorReason(data);
637
+ logger2.error("tg sendPhoto failed: %s", JSON.stringify(data));
638
+ await sendTelegramMessage({
639
+ message: `${message}
640
+
641
+ \u26A0\uFE0F <b>Photo delivery failed</b>
642
+ Reason: <code>${escapeHtml(reason)}</code>`,
643
+ markup
271
644
  });
645
+ return;
272
646
  }
273
- logger.info("tg sendPhoto: %s", data?.ok ? "sent" : JSON.stringify(data));
647
+ logger2.info("tg sendPhoto: sent");
274
648
  };
275
649
  var formatAnalysisMessage = (signal, analysis) => {
276
650
  const lines = [];
@@ -316,7 +690,7 @@ ${escapeHtml(clean)}`);
316
690
  };
317
691
  var sendSignalAnalysis = async (signal, analysis) => {
318
692
  const message = formatAnalysisMessage(signal, analysis);
319
- const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
693
+ const data = await requestTelegram("sendMessage", {
320
694
  method: "POST",
321
695
  headers: { "Content-Type": "application/json" },
322
696
  body: JSON.stringify({
@@ -325,10 +699,9 @@ var sendSignalAnalysis = async (signal, analysis) => {
325
699
  parse_mode: "HTML"
326
700
  })
327
701
  });
328
- const data = await res.json();
329
- logger.info(
702
+ logger2.info(
330
703
  "tg sendMessage (analysis): %s",
331
- data?.ok ? "sent" : JSON.stringify(data)
704
+ data?.ok ? "sent" : getTelegramErrorReason(data)
332
705
  );
333
706
  };
334
707
 
@@ -342,7 +715,7 @@ var cleanFiles = async (dir) => {
342
715
  total: files.length,
343
716
  width: 30
344
717
  });
345
- logger2.info(chalk.yellow("clean:", dir));
718
+ logger3.info(chalk.yellow("clean:", dir));
346
719
  for await (const file of files) {
347
720
  completed++;
348
721
  const fullPath = `${dir}/${file}`;
@@ -356,7 +729,7 @@ var cleanFiles = async (dir) => {
356
729
  bar.tick(completed === files.length ? completed % 100 : 100);
357
730
  }
358
731
  }
359
- logger2.info("");
732
+ logger3.info("");
360
733
  };
361
734
  var cleanRedis = async (area) => {
362
735
  let completed = 0;
@@ -365,7 +738,7 @@ var cleanRedis = async (area) => {
365
738
  total: keys.length,
366
739
  width: 30
367
740
  });
368
- logger2.info(chalk.yellow("clean:", area));
741
+ logger3.info(chalk.yellow("clean:", area));
369
742
  try {
370
743
  for await (const key of keys) {
371
744
  completed++;
@@ -376,18 +749,18 @@ var cleanRedis = async (area) => {
376
749
  }
377
750
  } catch (e) {
378
751
  if (e instanceof RedisWriteBlockedError || String(e).includes("MISCONF")) {
379
- logger2.error(
752
+ logger3.error(
380
753
  "Redis write is blocked by MISCONF (RDB save failure). Cleanup stopped early."
381
754
  );
382
- logger2.error(
755
+ logger3.error(
383
756
  "Check Redis logs and fix persistence/memory pressure before retry."
384
757
  );
385
- logger2.info("");
758
+ logger3.info("");
386
759
  return;
387
760
  }
388
761
  throw e;
389
762
  }
390
- logger2.info("");
763
+ logger3.info("");
391
764
  };
392
765
  var update = async (connector, interval, tickers) => {
393
766
  const PRELOAD_START = getTimestamp(PRELOAD_DAYS);
@@ -399,7 +772,11 @@ var update = async (connector, interval, tickers) => {
399
772
  width: 30
400
773
  }
401
774
  );
402
- logger2.info(chalk.yellow("update:", tickers.length));
775
+ logger3.info(
776
+ chalk.yellow(
777
+ `update: ${tickers.length} (klineConcurrency=${KLINE_CONCURRENCY_LIMIT})`
778
+ )
779
+ );
403
780
  const queue = tickers.slice();
404
781
  if (!queue.includes("BTCUSDT")) {
405
782
  queue.unshift("BTCUSDT");
@@ -414,12 +791,12 @@ var update = async (connector, interval, tickers) => {
414
791
  silent: true
415
792
  });
416
793
  } catch {
417
- logger2.error("Failed loading: %s", symbol);
794
+ logger3.error("Failed loading: %s", symbol);
418
795
  } finally {
419
796
  bar.tick(1, { symbol: chalk.gray(symbol) });
420
797
  }
421
798
  });
422
- logger2.info("");
799
+ logger3.info("");
423
800
  };
424
801
  var parseSymbolsFromCLI = (symbol = "") => symbol.split(",").map((s) => {
425
802
  const ticker = s.toUpperCase();
@@ -457,7 +834,7 @@ var getTickers = async (connector, include = "", exclude = "", limit, chunk) =>
457
834
  }
458
835
  if (chunk) {
459
836
  const [currentChunk, chunksCount] = chunk.split("/").map((c) => parseInt(c));
460
- logger2.info("chunk: %d / %d", currentChunk, chunksCount);
837
+ logger3.info("chunk: %d / %d", currentChunk, chunksCount);
461
838
  const chunkSize = Math.ceil(tickers.length / chunksCount);
462
839
  const chunks = import_lodash.default.chunk(tickers, chunkSize);
463
840
  tickers = chunks[currentChunk - 1];
@@ -473,7 +850,7 @@ var makeScreenshots = async (signals, interval) => {
473
850
  width: 30
474
851
  }
475
852
  );
476
- logger2.info(chalk.yellow("screenshots:", `${interval}m`, signals.length));
853
+ logger3.info(chalk.yellow("screenshots:", `${interval}m`, signals.length));
477
854
  await runWithConcurrency(
478
855
  signals,
479
856
  SCREENSHOT_CONCURRENCY_LIMIT,
@@ -481,7 +858,7 @@ var makeScreenshots = async (signals, interval) => {
481
858
  try {
482
859
  await screenDashboard({ ...signal, interval }, projectRoot);
483
860
  } catch (error) {
484
- logger2.error(
861
+ logger3.error(
485
862
  "Failed screenshot: %s (%s)",
486
863
  signal.symbol,
487
864
  error?.message || String(error)
@@ -491,7 +868,7 @@ var makeScreenshots = async (signals, interval) => {
491
868
  }
492
869
  }
493
870
  );
494
- logger2.info("");
871
+ logger3.info("");
495
872
  };
496
873
  var sendToAI = async (signals) => {
497
874
  const bar = new ProgressBar(
@@ -501,17 +878,17 @@ var sendToAI = async (signals) => {
501
878
  width: 30
502
879
  }
503
880
  );
504
- logger2.info(chalk.yellow("AI:", signals.length));
881
+ logger3.info(chalk.yellow("AI:", signals.length));
505
882
  await runWithConcurrency(signals, AI_CONCURRENCY_LIMIT, async (signal) => {
506
883
  try {
507
884
  await askAI(signal);
508
885
  } catch {
509
- logger2.error("Failed ask: %s", signal.symbol);
886
+ logger3.error("Failed ask: %s", signal.symbol);
510
887
  } finally {
511
888
  bar.tick(1, { symbol: chalk.gray(signal.symbol) });
512
889
  }
513
890
  });
514
- logger2.info("");
891
+ logger3.info("");
515
892
  };
516
893
  var sendToTG = async (signals, imgInterval) => {
517
894
  const bar = new ProgressBar(
@@ -521,7 +898,7 @@ var sendToTG = async (signals, imgInterval) => {
521
898
  width: 30
522
899
  }
523
900
  );
524
- logger2.info(chalk.yellow("messages:", signals.length));
901
+ logger3.info(chalk.yellow("messages:", signals.length));
525
902
  await runWithConcurrency(signals, TG_CONCURRENCY_LIMIT, async (signal) => {
526
903
  try {
527
904
  const analysis = await getData2(
@@ -533,7 +910,7 @@ var sendToTG = async (signals, imgInterval) => {
533
910
  await sendSignalAnalysis(signal, analysis);
534
911
  }
535
912
  } catch (err) {
536
- logger2.error(
913
+ logger3.error(
537
914
  "Failed sent: %s %s",
538
915
  signal.symbol,
539
916
  err?.message || String(err)
@@ -542,7 +919,7 @@ var sendToTG = async (signals, imgInterval) => {
542
919
  bar.tick(1, { symbol: chalk.gray(signal.symbol) });
543
920
  }
544
921
  });
545
- logger2.info("");
922
+ logger3.info("");
546
923
  };
547
924
  export {
548
925
  cleanFiles,