@tradejs/node 1.0.3 → 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,17 +3,17 @@ 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";
10
10
  import {
11
11
  askAI
12
- } from "./chunk-QDYCJ2OK.mjs";
13
- import "./chunk-CK2PW4L5.mjs";
12
+ } from "./chunk-LMAKIC3C.mjs";
13
+ import "./chunk-ZY6ULOWK.mjs";
14
14
  import {
15
15
  getTradejsProjectCwd
16
- } from "./chunk-3C76HVLA.mjs";
16
+ } from "./chunk-P2ZUWONT.mjs";
17
17
  import {
18
18
  __toESM
19
19
  } from "./chunk-6DZX6EAA.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,53 +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
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
262
- method: "POST",
263
- headers: { "Content-Type": "application/json" },
264
- body: JSON.stringify({
265
- chat_id: chatId,
266
- text: JSON.stringify(data),
267
- parse_mode: "HTML"
268
- })
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
269
644
  });
645
+ return;
270
646
  }
271
- logger.info("tg sendPhoto: %s", data?.ok ? "sent" : JSON.stringify(data));
647
+ logger2.info("tg sendPhoto: sent");
272
648
  };
273
649
  var formatAnalysisMessage = (signal, analysis) => {
274
650
  const lines = [];
@@ -314,7 +690,7 @@ ${escapeHtml(clean)}`);
314
690
  };
315
691
  var sendSignalAnalysis = async (signal, analysis) => {
316
692
  const message = formatAnalysisMessage(signal, analysis);
317
- const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
693
+ const data = await requestTelegram("sendMessage", {
318
694
  method: "POST",
319
695
  headers: { "Content-Type": "application/json" },
320
696
  body: JSON.stringify({
@@ -323,10 +699,9 @@ var sendSignalAnalysis = async (signal, analysis) => {
323
699
  parse_mode: "HTML"
324
700
  })
325
701
  });
326
- const data = await res.json();
327
- logger.info(
702
+ logger2.info(
328
703
  "tg sendMessage (analysis): %s",
329
- data?.ok ? "sent" : JSON.stringify(data)
704
+ data?.ok ? "sent" : getTelegramErrorReason(data)
330
705
  );
331
706
  };
332
707
 
@@ -340,7 +715,7 @@ var cleanFiles = async (dir) => {
340
715
  total: files.length,
341
716
  width: 30
342
717
  });
343
- logger2.info(chalk.yellow("clean:", dir));
718
+ logger3.info(chalk.yellow("clean:", dir));
344
719
  for await (const file of files) {
345
720
  completed++;
346
721
  const fullPath = `${dir}/${file}`;
@@ -354,7 +729,7 @@ var cleanFiles = async (dir) => {
354
729
  bar.tick(completed === files.length ? completed % 100 : 100);
355
730
  }
356
731
  }
357
- logger2.info("");
732
+ logger3.info("");
358
733
  };
359
734
  var cleanRedis = async (area) => {
360
735
  let completed = 0;
@@ -363,7 +738,7 @@ var cleanRedis = async (area) => {
363
738
  total: keys.length,
364
739
  width: 30
365
740
  });
366
- logger2.info(chalk.yellow("clean:", area));
741
+ logger3.info(chalk.yellow("clean:", area));
367
742
  try {
368
743
  for await (const key of keys) {
369
744
  completed++;
@@ -374,18 +749,18 @@ var cleanRedis = async (area) => {
374
749
  }
375
750
  } catch (e) {
376
751
  if (e instanceof RedisWriteBlockedError || String(e).includes("MISCONF")) {
377
- logger2.error(
752
+ logger3.error(
378
753
  "Redis write is blocked by MISCONF (RDB save failure). Cleanup stopped early."
379
754
  );
380
- logger2.error(
755
+ logger3.error(
381
756
  "Check Redis logs and fix persistence/memory pressure before retry."
382
757
  );
383
- logger2.info("");
758
+ logger3.info("");
384
759
  return;
385
760
  }
386
761
  throw e;
387
762
  }
388
- logger2.info("");
763
+ logger3.info("");
389
764
  };
390
765
  var update = async (connector, interval, tickers) => {
391
766
  const PRELOAD_START = getTimestamp(PRELOAD_DAYS);
@@ -397,7 +772,11 @@ var update = async (connector, interval, tickers) => {
397
772
  width: 30
398
773
  }
399
774
  );
400
- logger2.info(chalk.yellow("update:", tickers.length));
775
+ logger3.info(
776
+ chalk.yellow(
777
+ `update: ${tickers.length} (klineConcurrency=${KLINE_CONCURRENCY_LIMIT})`
778
+ )
779
+ );
401
780
  const queue = tickers.slice();
402
781
  if (!queue.includes("BTCUSDT")) {
403
782
  queue.unshift("BTCUSDT");
@@ -412,12 +791,12 @@ var update = async (connector, interval, tickers) => {
412
791
  silent: true
413
792
  });
414
793
  } catch {
415
- logger2.error("Failed loading: %s", symbol);
794
+ logger3.error("Failed loading: %s", symbol);
416
795
  } finally {
417
796
  bar.tick(1, { symbol: chalk.gray(symbol) });
418
797
  }
419
798
  });
420
- logger2.info("");
799
+ logger3.info("");
421
800
  };
422
801
  var parseSymbolsFromCLI = (symbol = "") => symbol.split(",").map((s) => {
423
802
  const ticker = s.toUpperCase();
@@ -455,7 +834,7 @@ var getTickers = async (connector, include = "", exclude = "", limit, chunk) =>
455
834
  }
456
835
  if (chunk) {
457
836
  const [currentChunk, chunksCount] = chunk.split("/").map((c) => parseInt(c));
458
- logger2.info("chunk: %d / %d", currentChunk, chunksCount);
837
+ logger3.info("chunk: %d / %d", currentChunk, chunksCount);
459
838
  const chunkSize = Math.ceil(tickers.length / chunksCount);
460
839
  const chunks = import_lodash.default.chunk(tickers, chunkSize);
461
840
  tickers = chunks[currentChunk - 1];
@@ -471,7 +850,7 @@ var makeScreenshots = async (signals, interval) => {
471
850
  width: 30
472
851
  }
473
852
  );
474
- logger2.info(chalk.yellow("screenshots:", `${interval}m`, signals.length));
853
+ logger3.info(chalk.yellow("screenshots:", `${interval}m`, signals.length));
475
854
  await runWithConcurrency(
476
855
  signals,
477
856
  SCREENSHOT_CONCURRENCY_LIMIT,
@@ -479,7 +858,7 @@ var makeScreenshots = async (signals, interval) => {
479
858
  try {
480
859
  await screenDashboard({ ...signal, interval }, projectRoot);
481
860
  } catch (error) {
482
- logger2.error(
861
+ logger3.error(
483
862
  "Failed screenshot: %s (%s)",
484
863
  signal.symbol,
485
864
  error?.message || String(error)
@@ -489,7 +868,7 @@ var makeScreenshots = async (signals, interval) => {
489
868
  }
490
869
  }
491
870
  );
492
- logger2.info("");
871
+ logger3.info("");
493
872
  };
494
873
  var sendToAI = async (signals) => {
495
874
  const bar = new ProgressBar(
@@ -499,17 +878,17 @@ var sendToAI = async (signals) => {
499
878
  width: 30
500
879
  }
501
880
  );
502
- logger2.info(chalk.yellow("AI:", signals.length));
881
+ logger3.info(chalk.yellow("AI:", signals.length));
503
882
  await runWithConcurrency(signals, AI_CONCURRENCY_LIMIT, async (signal) => {
504
883
  try {
505
884
  await askAI(signal);
506
885
  } catch {
507
- logger2.error("Failed ask: %s", signal.symbol);
886
+ logger3.error("Failed ask: %s", signal.symbol);
508
887
  } finally {
509
888
  bar.tick(1, { symbol: chalk.gray(signal.symbol) });
510
889
  }
511
890
  });
512
- logger2.info("");
891
+ logger3.info("");
513
892
  };
514
893
  var sendToTG = async (signals, imgInterval) => {
515
894
  const bar = new ProgressBar(
@@ -519,7 +898,7 @@ var sendToTG = async (signals, imgInterval) => {
519
898
  width: 30
520
899
  }
521
900
  );
522
- logger2.info(chalk.yellow("messages:", signals.length));
901
+ logger3.info(chalk.yellow("messages:", signals.length));
523
902
  await runWithConcurrency(signals, TG_CONCURRENCY_LIMIT, async (signal) => {
524
903
  try {
525
904
  const analysis = await getData2(
@@ -530,13 +909,17 @@ var sendToTG = async (signals, imgInterval) => {
530
909
  if (analysis && typeof analysis === "object" && Object.keys(analysis).length > 0) {
531
910
  await sendSignalAnalysis(signal, analysis);
532
911
  }
533
- } catch {
534
- logger2.error("Failed sent: %s", signal.symbol);
912
+ } catch (err) {
913
+ logger3.error(
914
+ "Failed sent: %s %s",
915
+ signal.symbol,
916
+ err?.message || String(err)
917
+ );
535
918
  } finally {
536
919
  bar.tick(1, { symbol: chalk.gray(signal.symbol) });
537
920
  }
538
921
  });
539
- logger2.info("");
922
+ logger3.info("");
540
923
  };
541
924
  export {
542
925
  cleanFiles,