@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.js CHANGED
@@ -3933,7 +3933,7 @@ var require_lodash = __commonJS({
3933
3933
  var defer = baseRest(function(func, args) {
3934
3934
  return baseDelay(func, 1, args);
3935
3935
  });
3936
- var delay2 = baseRest(function(func, wait, args) {
3936
+ var delay3 = baseRest(function(func, wait, args) {
3937
3937
  return baseDelay(func, toNumber(wait) || 0, args);
3938
3938
  });
3939
3939
  function flip(func) {
@@ -5047,7 +5047,7 @@ var require_lodash = __commonJS({
5047
5047
  lodash.defaults = defaults;
5048
5048
  lodash.defaultsDeep = defaultsDeep;
5049
5049
  lodash.defer = defer;
5050
- lodash.delay = delay2;
5050
+ lodash.delay = delay3;
5051
5051
  lodash.difference = difference;
5052
5052
  lodash.differenceBy = differenceBy;
5053
5053
  lodash.differenceWith = differenceWith;
@@ -5528,7 +5528,7 @@ var import_promises2 = __toESM(require("fs/promises"));
5528
5528
  var import_lodash = __toESM(require_lodash());
5529
5529
  var import_progress = __toESM(require("progress"));
5530
5530
  var import_chalk = __toESM(require("chalk"));
5531
- var import_async2 = require("@tradejs/core/async");
5531
+ var import_async3 = require("@tradejs/core/async");
5532
5532
  var import_backtest = require("@tradejs/core/backtest");
5533
5533
  var import_constants = require("@tradejs/core/constants");
5534
5534
  var import_tickers = require("@tradejs/core/tickers");
@@ -5536,7 +5536,16 @@ var import_time = require("@tradejs/core/time");
5536
5536
 
5537
5537
  // src/constants.ts
5538
5538
  var { NODE_ENV } = process.env;
5539
- var KLINE_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 5 : 10;
5539
+ var getPositiveIntegerEnv = (name, fallback) => {
5540
+ const rawValue = process.env[name];
5541
+ const parsedValue = Number(rawValue);
5542
+ return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : fallback;
5543
+ };
5544
+ var DEFAULT_KLINE_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 1 : 10;
5545
+ var KLINE_CONCURRENCY_LIMIT = getPositiveIntegerEnv(
5546
+ "KLINE_CONCURRENCY_LIMIT",
5547
+ DEFAULT_KLINE_CONCURRENCY_LIMIT
5548
+ );
5540
5549
  var TG_CONCURRENCY_LIMIT = 3;
5541
5550
  var AI_CONCURRENCY_LIMIT = 3;
5542
5551
  var SCREENSHOT_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 1 : 2;
@@ -5544,7 +5553,7 @@ var SCREENSHOT_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 1 : 2;
5544
5553
  // src/cli.ts
5545
5554
  var import_files = require("@tradejs/infra/files");
5546
5555
  var import_redis3 = require("@tradejs/infra/redis");
5547
- var import_logger4 = require("@tradejs/infra/logger");
5556
+ var import_logger5 = require("@tradejs/infra/logger");
5548
5557
 
5549
5558
  // src/ai.ts
5550
5559
  var import_redis = require("@tradejs/infra/redis");
@@ -5897,65 +5906,325 @@ var import_promises = __toESM(require("fs/promises"));
5897
5906
  var import_path2 = __toESM(require("path"));
5898
5907
  var import_puppeteer = __toESM(require("puppeteer"));
5899
5908
  var import_async = require("@tradejs/core/async");
5909
+ var import_logger3 = require("@tradejs/infra/logger");
5900
5910
  var import_redis2 = require("@tradejs/infra/redis");
5901
5911
  var { APP_URL } = process.env;
5912
+ var SCREENSHOT_NAVIGATION_ATTEMPTS = 3;
5913
+ var SCREENSHOT_NAVIGATION_RETRY_DELAY_MS = 2e3;
5914
+ var SCREENSHOT_CAPTURE_ATTEMPTS = 2;
5915
+ var SCREENSHOT_CAPTURE_RETRY_DELAY_MS = 2e3;
5916
+ var SCREENSHOT_NAVIGATION_TIMEOUT_MS = 3e4;
5917
+ var SCREENSHOT_RENDER_DELAY_MS = 1e4;
5918
+ var SCREENSHOT_CONSOLE_LOG_LIMIT = 20;
5919
+ var SCREENSHOT_CONSOLE_TEXT_LIMIT = 500;
5920
+ var SCREENSHOT_BROWSER_STDERR = process.env.SCREENSHOT_BROWSER_STDERR === "1";
5921
+ var PUPPETEER_DUMPIO = process.env.PUPPETEER_DUMPIO === "1";
5922
+ var SCREENSHOT_VIEWPORT = {
5923
+ width: 1280,
5924
+ height: 800,
5925
+ deviceScaleFactor: process.env.NODE_ENV === "production" ? 1 : 2
5926
+ };
5902
5927
  var getProjectRoot = (projectRoot) => import_path2.default.resolve(getTradejsProjectCwd(projectRoot));
5903
5928
  var getScreenshotsDir = (projectRoot) => import_path2.default.join(getProjectRoot(projectRoot), "data", "screenshots");
5904
- var getImageUrl = ({ symbol, signalId, interval }) => `${APP_URL}/api/files/screenshot/${symbol}_${signalId}_${interval}`;
5929
+ var maskTokenInUrl = (url) => url.replace(/([?&]token=)[^&]+/i, "$1<hidden>");
5930
+ var getErrorMessage = (error) => {
5931
+ const maybeError = error;
5932
+ const message = maybeError?.message || String(error);
5933
+ if (maybeError?.cause == null) {
5934
+ return message;
5935
+ }
5936
+ const cause = maybeError.cause instanceof Error ? maybeError.cause.message : String(maybeError.cause);
5937
+ return `${message}; cause: ${cause}`;
5938
+ };
5939
+ var truncateText = (value, maxLength = SCREENSHOT_CONSOLE_TEXT_LIMIT) => value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
5940
+ var shouldLogConsoleMessage = (type) => ["error", "warning", "assert"].includes(type);
5941
+ var getScreenshotBuffer = async (signal, projectRoot) => {
5942
+ const screenshotPath = getScreenshotPath(signal, projectRoot);
5943
+ return import_promises.default.readFile(screenshotPath);
5944
+ };
5945
+ var getScreenshotFilename = ({
5946
+ symbol,
5947
+ signalId,
5948
+ interval
5949
+ }) => `${symbol}_${signalId}_${interval}.png`;
5950
+ var getScreenshotRenderBaseUrl = () => {
5951
+ const fallback = String(APP_URL || "").trim();
5952
+ if (fallback) {
5953
+ return fallback;
5954
+ }
5955
+ throw new Error("APP_URL is required for screenshots");
5956
+ };
5905
5957
  var getScreenshotPath = ({ symbol, signalId, interval }, projectRoot) => {
5906
5958
  return import_path2.default.join(
5907
5959
  getScreenshotsDir(projectRoot),
5908
- `${symbol}_${signalId}_${interval}.png`
5960
+ getScreenshotFilename({ symbol, signalId, interval })
5909
5961
  );
5910
5962
  };
5911
5963
  var screenDashboard = async (signal, projectRoot) => {
5912
5964
  const { symbol, signalId, interval } = signal;
5965
+ const screenshotBaseUrl = getScreenshotRenderBaseUrl();
5966
+ const screenshotPath = getScreenshotPath(signal, projectRoot);
5913
5967
  const rootUser = await (0, import_redis2.getData)(import_redis2.redisKeys.user("root"), null);
5914
5968
  const token2 = rootUser && typeof rootUser === "object" ? rootUser.token : null;
5915
5969
  const tokenParam = typeof token2 === "string" && token2.length > 0 ? `&token=${encodeURIComponent(token2)}` : "";
5916
- const browser = await import_puppeteer.default.launch({
5917
- headless: true,
5918
- executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
5919
- args: [
5920
- "--no-sandbox",
5921
- "--disable-setuid-sandbox",
5922
- "--disable-dev-shm-usage",
5923
- "--disable-gpu",
5924
- "--font-render-hinting=medium"
5925
- ]
5926
- });
5927
- try {
5928
- const page = await browser.newPage();
5970
+ const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1${tokenParam}`;
5971
+ const maskedDashboardUrl = maskTokenInUrl(dashboardUrl);
5972
+ import_logger3.logger.info(
5973
+ "screenshot start: %s %sm url=%s path=%s",
5974
+ symbol,
5975
+ interval,
5976
+ maskedDashboardUrl,
5977
+ screenshotPath
5978
+ );
5979
+ let captureError = null;
5980
+ for (let captureAttempt = 1; captureAttempt <= SCREENSHOT_CAPTURE_ATTEMPTS; captureAttempt += 1) {
5981
+ const browser = await import_puppeteer.default.launch({
5982
+ headless: true,
5983
+ dumpio: PUPPETEER_DUMPIO,
5984
+ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
5985
+ args: [
5986
+ "--no-sandbox",
5987
+ "--disable-setuid-sandbox",
5988
+ "--disable-dev-shm-usage",
5989
+ "--disable-gpu",
5990
+ "--font-render-hinting=medium"
5991
+ ]
5992
+ });
5993
+ const browserProcess = browser.process();
5929
5994
  try {
5930
- await page.setViewport({
5931
- width: 1400,
5932
- height: 960,
5933
- deviceScaleFactor: 2
5934
- });
5935
- await page.goto(
5936
- `${APP_URL}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true${tokenParam}`
5995
+ if (browserProcess) {
5996
+ import_logger3.logger.info(
5997
+ "screenshot browser: %s %sm captureAttempt=%d pid=%s exec=%s",
5998
+ symbol,
5999
+ interval,
6000
+ captureAttempt,
6001
+ String(browserProcess.pid ?? "unknown"),
6002
+ browserProcess.spawnfile || "unknown"
6003
+ );
6004
+ }
6005
+ import_logger3.logger.info(
6006
+ "screenshot browser version: %s %sm captureAttempt=%d version=%s",
6007
+ symbol,
6008
+ interval,
6009
+ captureAttempt,
6010
+ await browser.version()
5937
6011
  );
5938
- await (0, import_async.delay)(1e4);
5939
- await import_promises.default.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
5940
- await page.screenshot({
5941
- path: getScreenshotPath(signal, projectRoot)
6012
+ browser.on("disconnected", () => {
6013
+ import_logger3.logger.error(
6014
+ "screenshot browser disconnected: %s %sm captureAttempt=%d",
6015
+ symbol,
6016
+ interval,
6017
+ captureAttempt
6018
+ );
5942
6019
  });
6020
+ if (SCREENSHOT_BROWSER_STDERR && browserProcess?.stderr) {
6021
+ browserProcess.stderr.on("data", (chunk) => {
6022
+ const text = String(chunk).trim();
6023
+ if (!text) {
6024
+ return;
6025
+ }
6026
+ import_logger3.logger.error(
6027
+ "screenshot browser stderr: %s %sm captureAttempt=%d (%s)",
6028
+ symbol,
6029
+ interval,
6030
+ captureAttempt,
6031
+ truncateText(text)
6032
+ );
6033
+ });
6034
+ }
6035
+ const page = await browser.newPage();
6036
+ try {
6037
+ let consoleMessagesLogged = 0;
6038
+ page.on("requestfailed", (request) => {
6039
+ if (!request.isNavigationRequest() && request.resourceType() !== "document") {
6040
+ return;
6041
+ }
6042
+ import_logger3.logger.error(
6043
+ "screenshot request failed: %s %sm %s (%s)",
6044
+ symbol,
6045
+ interval,
6046
+ maskTokenInUrl(request.url()),
6047
+ request.failure()?.errorText || "unknown"
6048
+ );
6049
+ });
6050
+ page.on("pageerror", (error) => {
6051
+ import_logger3.logger.error(
6052
+ "screenshot page error: %s %sm (%s)",
6053
+ symbol,
6054
+ interval,
6055
+ error.message || String(error)
6056
+ );
6057
+ });
6058
+ page.on("error", (error) => {
6059
+ import_logger3.logger.error(
6060
+ "screenshot target error: %s %sm (%s)",
6061
+ symbol,
6062
+ interval,
6063
+ error.message || String(error)
6064
+ );
6065
+ });
6066
+ page.on("close", () => {
6067
+ import_logger3.logger.info(
6068
+ "screenshot page closed: %s %sm captureAttempt=%d",
6069
+ symbol,
6070
+ interval,
6071
+ captureAttempt
6072
+ );
6073
+ });
6074
+ page.on("console", (message) => {
6075
+ if (!shouldLogConsoleMessage(message.type())) {
6076
+ return;
6077
+ }
6078
+ if (consoleMessagesLogged >= SCREENSHOT_CONSOLE_LOG_LIMIT) {
6079
+ return;
6080
+ }
6081
+ consoleMessagesLogged += 1;
6082
+ const location = message.location();
6083
+ const suffix = location.url ? ` ${maskTokenInUrl(location.url)}:${location.lineNumber ?? 0}:${location.columnNumber ?? 0}` : "";
6084
+ import_logger3.logger.error(
6085
+ "screenshot console %s: %s %sm captureAttempt=%d (%s%s)",
6086
+ message.type(),
6087
+ symbol,
6088
+ interval,
6089
+ captureAttempt,
6090
+ truncateText(message.text()),
6091
+ suffix
6092
+ );
6093
+ });
6094
+ page.on("response", (response) => {
6095
+ const status = response.status();
6096
+ if (status < 400) {
6097
+ return;
6098
+ }
6099
+ const request = response.request();
6100
+ const resourceType = request.resourceType();
6101
+ if (!["document", "script", "stylesheet", "fetch", "xhr"].includes(
6102
+ resourceType
6103
+ )) {
6104
+ return;
6105
+ }
6106
+ import_logger3.logger.error(
6107
+ "screenshot response failed: %s %sm captureAttempt=%d status=%d type=%s url=%s",
6108
+ symbol,
6109
+ interval,
6110
+ captureAttempt,
6111
+ status,
6112
+ resourceType,
6113
+ maskTokenInUrl(response.url())
6114
+ );
6115
+ });
6116
+ await page.setViewport(SCREENSHOT_VIEWPORT);
6117
+ let gotoError = null;
6118
+ import_logger3.logger.info(
6119
+ "screenshot render: %s %sm captureAttempt=%d viewport=%dx%d@%d",
6120
+ symbol,
6121
+ interval,
6122
+ captureAttempt,
6123
+ SCREENSHOT_VIEWPORT.width,
6124
+ SCREENSHOT_VIEWPORT.height,
6125
+ SCREENSHOT_VIEWPORT.deviceScaleFactor
6126
+ );
6127
+ for (let attempt = 1; attempt <= SCREENSHOT_NAVIGATION_ATTEMPTS; attempt += 1) {
6128
+ try {
6129
+ import_logger3.logger.info(
6130
+ "screenshot goto: %s %sm captureAttempt=%d attempt=%d url=%s",
6131
+ symbol,
6132
+ interval,
6133
+ captureAttempt,
6134
+ attempt,
6135
+ maskedDashboardUrl
6136
+ );
6137
+ const response = await page.goto(dashboardUrl, {
6138
+ waitUntil: "domcontentloaded",
6139
+ timeout: SCREENSHOT_NAVIGATION_TIMEOUT_MS
6140
+ });
6141
+ import_logger3.logger.info(
6142
+ "screenshot goto ok: %s %sm captureAttempt=%d attempt=%d status=%s finalUrl=%s",
6143
+ symbol,
6144
+ interval,
6145
+ captureAttempt,
6146
+ attempt,
6147
+ response ? String(response.status()) : "null",
6148
+ maskTokenInUrl(page.url())
6149
+ );
6150
+ import_logger3.logger.info(
6151
+ "screenshot page info: %s %sm captureAttempt=%d title=%s ua=%s",
6152
+ symbol,
6153
+ interval,
6154
+ captureAttempt,
6155
+ truncateText(await page.title(), 120),
6156
+ truncateText(await page.evaluate(() => navigator.userAgent), 240)
6157
+ );
6158
+ gotoError = null;
6159
+ break;
6160
+ } catch (error) {
6161
+ gotoError = error;
6162
+ import_logger3.logger.error(
6163
+ "screenshot goto failed: %s %sm captureAttempt=%d attempt=%d url=%s (%s)",
6164
+ symbol,
6165
+ interval,
6166
+ captureAttempt,
6167
+ attempt,
6168
+ maskedDashboardUrl,
6169
+ gotoError.message || String(gotoError)
6170
+ );
6171
+ if (attempt >= SCREENSHOT_NAVIGATION_ATTEMPTS) {
6172
+ break;
6173
+ }
6174
+ await (0, import_async.delay)(SCREENSHOT_NAVIGATION_RETRY_DELAY_MS);
6175
+ }
6176
+ }
6177
+ if (gotoError) {
6178
+ throw new Error(
6179
+ `Failed to open dashboard ${dashboardUrl}: ${gotoError.message || String(gotoError)}`
6180
+ );
6181
+ }
6182
+ await (0, import_async.delay)(SCREENSHOT_RENDER_DELAY_MS);
6183
+ await import_promises.default.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
6184
+ await page.screenshot({
6185
+ path: screenshotPath,
6186
+ type: "png"
6187
+ });
6188
+ import_logger3.logger.info(
6189
+ "screenshot saved: %s %sm path=%s",
6190
+ symbol,
6191
+ interval,
6192
+ screenshotPath
6193
+ );
6194
+ return;
6195
+ } finally {
6196
+ await page.close().catch(() => void 0);
6197
+ }
6198
+ } catch (error) {
6199
+ captureError = error;
6200
+ import_logger3.logger.error(
6201
+ "screenshot capture failed: %s %sm captureAttempt=%d (%s)",
6202
+ symbol,
6203
+ interval,
6204
+ captureAttempt,
6205
+ getErrorMessage(captureError)
6206
+ );
6207
+ if (captureAttempt < SCREENSHOT_CAPTURE_ATTEMPTS) {
6208
+ await (0, import_async.delay)(SCREENSHOT_CAPTURE_RETRY_DELAY_MS);
6209
+ }
5943
6210
  } finally {
5944
- await page.close();
6211
+ await browser.close().catch(() => void 0);
5945
6212
  }
5946
- } finally {
5947
- await browser.close();
5948
6213
  }
6214
+ throw captureError || new Error(`Failed screenshot: ${symbol} ${interval}`);
5949
6215
  };
5950
6216
 
5951
6217
  // src/signals.ts
6218
+ var import_async2 = require("@tradejs/core/async");
5952
6219
  var import_math = require("@tradejs/core/math");
5953
- var import_logger3 = require("@tradejs/infra/logger");
6220
+ var import_logger4 = require("@tradejs/infra/logger");
5954
6221
  var escapeHtml = (s) => {
5955
6222
  if (!s) return "";
5956
6223
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5957
6224
  };
5958
6225
  var { APP_URL: APP_URL2, TG_BOT_TOKEN: token, TG_CHAT_ID: chatId } = process.env;
6226
+ var TG_REQUEST_ATTEMPTS = 3;
6227
+ var TG_REQUEST_RETRY_DELAY_MS = 2e3;
5959
6228
  var normalizeQuality = (value) => typeof value === "number" ? Math.max(1, Math.min(5, Math.round(value))) : null;
5960
6229
  var getLastNumber = (value) => {
5961
6230
  if (Array.isArray(value)) {
@@ -5976,6 +6245,99 @@ var getAiQualityLine = (analysis) => {
5976
6245
  }
5977
6246
  return `\u{1F534} AI Quality: ${quality}/5`;
5978
6247
  };
6248
+ var getTelegramErrorReason = (data) => {
6249
+ if (data && typeof data === "object") {
6250
+ const record = data;
6251
+ const description = typeof record.description === "string" ? record.description : "";
6252
+ const errorCode = typeof record.error_code === "number" ? record.error_code : void 0;
6253
+ if (description && errorCode != null) {
6254
+ return `${errorCode}: ${description}`;
6255
+ }
6256
+ if (description) {
6257
+ return description;
6258
+ }
6259
+ }
6260
+ try {
6261
+ return JSON.stringify(data);
6262
+ } catch {
6263
+ return String(data);
6264
+ }
6265
+ };
6266
+ var getErrorMessage2 = (error) => {
6267
+ const maybeError = error;
6268
+ const message = maybeError?.message || String(error);
6269
+ if (maybeError?.cause == null) {
6270
+ return message;
6271
+ }
6272
+ const cause = maybeError.cause instanceof Error ? maybeError.cause.message : String(maybeError.cause);
6273
+ return `${message}; cause: ${cause}`;
6274
+ };
6275
+ var parseTelegramResponse = async (response) => {
6276
+ const fallback = {
6277
+ ok: response.ok,
6278
+ error_code: response.status,
6279
+ description: response.statusText
6280
+ };
6281
+ try {
6282
+ return await response.json();
6283
+ } catch {
6284
+ try {
6285
+ const text = await response.text();
6286
+ return text ? {
6287
+ ...fallback,
6288
+ description: text
6289
+ } : fallback;
6290
+ } catch {
6291
+ return fallback;
6292
+ }
6293
+ }
6294
+ };
6295
+ var requestTelegram = async (method, init) => {
6296
+ let lastError = null;
6297
+ for (let attempt = 1; attempt <= TG_REQUEST_ATTEMPTS; attempt += 1) {
6298
+ try {
6299
+ const res = await fetch(
6300
+ `https://api.telegram.org/bot${token}/${method}`,
6301
+ {
6302
+ ...init
6303
+ }
6304
+ );
6305
+ return await parseTelegramResponse(res);
6306
+ } catch (error) {
6307
+ lastError = error;
6308
+ import_logger4.logger.error(
6309
+ "tg %s network failed: attempt=%d (%s)",
6310
+ method,
6311
+ attempt,
6312
+ getErrorMessage2(error)
6313
+ );
6314
+ if (attempt < TG_REQUEST_ATTEMPTS) {
6315
+ await (0, import_async2.delay)(TG_REQUEST_RETRY_DELAY_MS);
6316
+ }
6317
+ }
6318
+ }
6319
+ throw lastError || new Error(`Telegram ${method} request failed`);
6320
+ };
6321
+ var sendTelegramMessage = async ({
6322
+ message,
6323
+ markup
6324
+ }) => {
6325
+ const data = await requestTelegram("sendMessage", {
6326
+ method: "POST",
6327
+ headers: { "Content-Type": "application/json" },
6328
+ body: JSON.stringify({
6329
+ chat_id: chatId,
6330
+ text: message,
6331
+ reply_markup: markup,
6332
+ parse_mode: "HTML"
6333
+ })
6334
+ });
6335
+ import_logger4.logger.info(
6336
+ "tg sendMessage: %s",
6337
+ data?.ok ? "sent" : getTelegramErrorReason(data)
6338
+ );
6339
+ return data;
6340
+ };
5979
6341
  var formatMessage = (signal, analysis) => {
5980
6342
  const {
5981
6343
  symbol,
@@ -6076,53 +6438,76 @@ var formatMessage = (signal, analysis) => {
6076
6438
  var sendSignal = async (signal, imgInterval, analysis) => {
6077
6439
  const { symbol, signalId, interval } = signal;
6078
6440
  const message = formatMessage(signal, analysis);
6079
- const markup = {
6080
- inline_keyboard: [
6081
- [
6082
- {
6083
- text: "Dashboard",
6084
- url: `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}`
6085
- }
6086
- ]
6087
- ]
6088
- };
6089
- if (!APP_URL2?.startsWith("https")) {
6090
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
6441
+ const publicAppUrl = APP_URL2?.startsWith("https") ? APP_URL2 : null;
6442
+ const dashboardUrl = publicAppUrl ? `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}` : null;
6443
+ const actionButtons = [
6444
+ dashboardUrl ? { text: "Dashboard", url: dashboardUrl } : null
6445
+ ].filter(Boolean);
6446
+ const markup = actionButtons.length ? {
6447
+ inline_keyboard: [actionButtons]
6448
+ } : void 0;
6449
+ let screenshotBytes = null;
6450
+ try {
6451
+ const screenshot = await getScreenshotBuffer({
6452
+ ...signal,
6453
+ interval: imgInterval
6454
+ });
6455
+ screenshotBytes = screenshot.buffer.slice(
6456
+ screenshot.byteOffset,
6457
+ screenshot.byteOffset + screenshot.byteLength
6458
+ );
6459
+ } catch (error) {
6460
+ import_logger4.logger.error(
6461
+ "tg screenshot unavailable: %s (%s)",
6462
+ symbol,
6463
+ error?.message || String(error)
6464
+ );
6465
+ await sendTelegramMessage({ message, markup });
6466
+ return;
6467
+ }
6468
+ const photoBody = new FormData();
6469
+ photoBody.set("chat_id", String(chatId || ""));
6470
+ photoBody.set(
6471
+ "photo",
6472
+ new Blob([screenshotBytes], { type: "image/png" }),
6473
+ getScreenshotFilename({ ...signal, interval: imgInterval })
6474
+ );
6475
+ photoBody.set("caption", message);
6476
+ photoBody.set("parse_mode", "HTML");
6477
+ if (markup) {
6478
+ photoBody.set("reply_markup", JSON.stringify(markup));
6479
+ }
6480
+ let data;
6481
+ try {
6482
+ data = await requestTelegram("sendPhoto", {
6091
6483
  method: "POST",
6092
- headers: { "Content-Type": "application/json" },
6093
- body: JSON.stringify({
6094
- chat_id: chatId,
6095
- text: message,
6096
- parse_mode: "HTML"
6097
- })
6484
+ body: photoBody
6485
+ });
6486
+ } catch (error) {
6487
+ const reason = getErrorMessage2(error);
6488
+ import_logger4.logger.error("tg sendPhoto request failed: %s", reason);
6489
+ await sendTelegramMessage({
6490
+ message: `${message}
6491
+
6492
+ \u26A0\uFE0F <b>Photo delivery failed</b>
6493
+ Reason: <code>${escapeHtml(reason)}</code>`,
6494
+ markup
6098
6495
  });
6099
6496
  return;
6100
6497
  }
6101
- const imageUrl = getImageUrl({ ...signal, interval: imgInterval });
6102
- const res = await fetch(`https://api.telegram.org/bot${token}/sendPhoto`, {
6103
- method: "POST",
6104
- headers: { "Content-Type": "application/json" },
6105
- body: JSON.stringify({
6106
- chat_id: chatId,
6107
- photo: imageUrl,
6108
- caption: message,
6109
- reply_markup: markup,
6110
- parse_mode: "HTML"
6111
- })
6112
- });
6113
- const data = await res.json();
6114
6498
  if (!data?.ok) {
6115
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
6116
- method: "POST",
6117
- headers: { "Content-Type": "application/json" },
6118
- body: JSON.stringify({
6119
- chat_id: chatId,
6120
- text: JSON.stringify(data),
6121
- parse_mode: "HTML"
6122
- })
6499
+ const reason = getTelegramErrorReason(data);
6500
+ import_logger4.logger.error("tg sendPhoto failed: %s", JSON.stringify(data));
6501
+ await sendTelegramMessage({
6502
+ message: `${message}
6503
+
6504
+ \u26A0\uFE0F <b>Photo delivery failed</b>
6505
+ Reason: <code>${escapeHtml(reason)}</code>`,
6506
+ markup
6123
6507
  });
6508
+ return;
6124
6509
  }
6125
- import_logger3.logger.info("tg sendPhoto: %s", data?.ok ? "sent" : JSON.stringify(data));
6510
+ import_logger4.logger.info("tg sendPhoto: sent");
6126
6511
  };
6127
6512
  var formatAnalysisMessage = (signal, analysis) => {
6128
6513
  const lines = [];
@@ -6168,7 +6553,7 @@ ${escapeHtml(clean)}`);
6168
6553
  };
6169
6554
  var sendSignalAnalysis = async (signal, analysis) => {
6170
6555
  const message = formatAnalysisMessage(signal, analysis);
6171
- const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
6556
+ const data = await requestTelegram("sendMessage", {
6172
6557
  method: "POST",
6173
6558
  headers: { "Content-Type": "application/json" },
6174
6559
  body: JSON.stringify({
@@ -6177,10 +6562,9 @@ var sendSignalAnalysis = async (signal, analysis) => {
6177
6562
  parse_mode: "HTML"
6178
6563
  })
6179
6564
  });
6180
- const data = await res.json();
6181
- import_logger3.logger.info(
6565
+ import_logger4.logger.info(
6182
6566
  "tg sendMessage (analysis): %s",
6183
- data?.ok ? "sent" : JSON.stringify(data)
6567
+ data?.ok ? "sent" : getTelegramErrorReason(data)
6184
6568
  );
6185
6569
  };
6186
6570
 
@@ -6194,7 +6578,7 @@ var cleanFiles = async (dir) => {
6194
6578
  total: files.length,
6195
6579
  width: 30
6196
6580
  });
6197
- import_logger4.logger.info(import_chalk.default.yellow("clean:", dir));
6581
+ import_logger5.logger.info(import_chalk.default.yellow("clean:", dir));
6198
6582
  for await (const file of files) {
6199
6583
  completed++;
6200
6584
  const fullPath = `${dir}/${file}`;
@@ -6208,7 +6592,7 @@ var cleanFiles = async (dir) => {
6208
6592
  bar.tick(completed === files.length ? completed % 100 : 100);
6209
6593
  }
6210
6594
  }
6211
- import_logger4.logger.info("");
6595
+ import_logger5.logger.info("");
6212
6596
  };
6213
6597
  var cleanRedis = async (area) => {
6214
6598
  let completed = 0;
@@ -6217,7 +6601,7 @@ var cleanRedis = async (area) => {
6217
6601
  total: keys.length,
6218
6602
  width: 30
6219
6603
  });
6220
- import_logger4.logger.info(import_chalk.default.yellow("clean:", area));
6604
+ import_logger5.logger.info(import_chalk.default.yellow("clean:", area));
6221
6605
  try {
6222
6606
  for await (const key of keys) {
6223
6607
  completed++;
@@ -6228,18 +6612,18 @@ var cleanRedis = async (area) => {
6228
6612
  }
6229
6613
  } catch (e) {
6230
6614
  if (e instanceof import_redis3.RedisWriteBlockedError || String(e).includes("MISCONF")) {
6231
- import_logger4.logger.error(
6615
+ import_logger5.logger.error(
6232
6616
  "Redis write is blocked by MISCONF (RDB save failure). Cleanup stopped early."
6233
6617
  );
6234
- import_logger4.logger.error(
6618
+ import_logger5.logger.error(
6235
6619
  "Check Redis logs and fix persistence/memory pressure before retry."
6236
6620
  );
6237
- import_logger4.logger.info("");
6621
+ import_logger5.logger.info("");
6238
6622
  return;
6239
6623
  }
6240
6624
  throw e;
6241
6625
  }
6242
- import_logger4.logger.info("");
6626
+ import_logger5.logger.info("");
6243
6627
  };
6244
6628
  var update = async (connector, interval, tickers) => {
6245
6629
  const PRELOAD_START = (0, import_time.getTimestamp)(import_constants.PRELOAD_DAYS);
@@ -6251,12 +6635,16 @@ var update = async (connector, interval, tickers) => {
6251
6635
  width: 30
6252
6636
  }
6253
6637
  );
6254
- import_logger4.logger.info(import_chalk.default.yellow("update:", tickers.length));
6638
+ import_logger5.logger.info(
6639
+ import_chalk.default.yellow(
6640
+ `update: ${tickers.length} (klineConcurrency=${KLINE_CONCURRENCY_LIMIT})`
6641
+ )
6642
+ );
6255
6643
  const queue = tickers.slice();
6256
6644
  if (!queue.includes("BTCUSDT")) {
6257
6645
  queue.unshift("BTCUSDT");
6258
6646
  }
6259
- await (0, import_async2.runWithConcurrency)(queue, KLINE_CONCURRENCY_LIMIT, async (symbol) => {
6647
+ await (0, import_async3.runWithConcurrency)(queue, KLINE_CONCURRENCY_LIMIT, async (symbol) => {
6260
6648
  try {
6261
6649
  await connector.kline({
6262
6650
  symbol,
@@ -6266,12 +6654,12 @@ var update = async (connector, interval, tickers) => {
6266
6654
  silent: true
6267
6655
  });
6268
6656
  } catch {
6269
- import_logger4.logger.error("Failed loading: %s", symbol);
6657
+ import_logger5.logger.error("Failed loading: %s", symbol);
6270
6658
  } finally {
6271
6659
  bar.tick(1, { symbol: import_chalk.default.gray(symbol) });
6272
6660
  }
6273
6661
  });
6274
- import_logger4.logger.info("");
6662
+ import_logger5.logger.info("");
6275
6663
  };
6276
6664
  var parseSymbolsFromCLI = (symbol = "") => symbol.split(",").map((s) => {
6277
6665
  const ticker = s.toUpperCase();
@@ -6309,7 +6697,7 @@ var getTickers = async (connector, include = "", exclude = "", limit, chunk) =>
6309
6697
  }
6310
6698
  if (chunk) {
6311
6699
  const [currentChunk, chunksCount] = chunk.split("/").map((c) => parseInt(c));
6312
- import_logger4.logger.info("chunk: %d / %d", currentChunk, chunksCount);
6700
+ import_logger5.logger.info("chunk: %d / %d", currentChunk, chunksCount);
6313
6701
  const chunkSize = Math.ceil(tickers.length / chunksCount);
6314
6702
  const chunks = import_lodash.default.chunk(tickers, chunkSize);
6315
6703
  tickers = chunks[currentChunk - 1];
@@ -6325,15 +6713,15 @@ var makeScreenshots = async (signals, interval) => {
6325
6713
  width: 30
6326
6714
  }
6327
6715
  );
6328
- import_logger4.logger.info(import_chalk.default.yellow("screenshots:", `${interval}m`, signals.length));
6329
- await (0, import_async2.runWithConcurrency)(
6716
+ import_logger5.logger.info(import_chalk.default.yellow("screenshots:", `${interval}m`, signals.length));
6717
+ await (0, import_async3.runWithConcurrency)(
6330
6718
  signals,
6331
6719
  SCREENSHOT_CONCURRENCY_LIMIT,
6332
6720
  async (signal) => {
6333
6721
  try {
6334
6722
  await screenDashboard({ ...signal, interval }, projectRoot);
6335
6723
  } catch (error) {
6336
- import_logger4.logger.error(
6724
+ import_logger5.logger.error(
6337
6725
  "Failed screenshot: %s (%s)",
6338
6726
  signal.symbol,
6339
6727
  error?.message || String(error)
@@ -6343,7 +6731,7 @@ var makeScreenshots = async (signals, interval) => {
6343
6731
  }
6344
6732
  }
6345
6733
  );
6346
- import_logger4.logger.info("");
6734
+ import_logger5.logger.info("");
6347
6735
  };
6348
6736
  var sendToAI = async (signals) => {
6349
6737
  const bar = new import_progress.default(
@@ -6353,17 +6741,17 @@ var sendToAI = async (signals) => {
6353
6741
  width: 30
6354
6742
  }
6355
6743
  );
6356
- import_logger4.logger.info(import_chalk.default.yellow("AI:", signals.length));
6357
- await (0, import_async2.runWithConcurrency)(signals, AI_CONCURRENCY_LIMIT, async (signal) => {
6744
+ import_logger5.logger.info(import_chalk.default.yellow("AI:", signals.length));
6745
+ await (0, import_async3.runWithConcurrency)(signals, AI_CONCURRENCY_LIMIT, async (signal) => {
6358
6746
  try {
6359
6747
  await askAI(signal);
6360
6748
  } catch {
6361
- import_logger4.logger.error("Failed ask: %s", signal.symbol);
6749
+ import_logger5.logger.error("Failed ask: %s", signal.symbol);
6362
6750
  } finally {
6363
6751
  bar.tick(1, { symbol: import_chalk.default.gray(signal.symbol) });
6364
6752
  }
6365
6753
  });
6366
- import_logger4.logger.info("");
6754
+ import_logger5.logger.info("");
6367
6755
  };
6368
6756
  var sendToTG = async (signals, imgInterval) => {
6369
6757
  const bar = new import_progress.default(
@@ -6373,8 +6761,8 @@ var sendToTG = async (signals, imgInterval) => {
6373
6761
  width: 30
6374
6762
  }
6375
6763
  );
6376
- import_logger4.logger.info(import_chalk.default.yellow("messages:", signals.length));
6377
- await (0, import_async2.runWithConcurrency)(signals, TG_CONCURRENCY_LIMIT, async (signal) => {
6764
+ import_logger5.logger.info(import_chalk.default.yellow("messages:", signals.length));
6765
+ await (0, import_async3.runWithConcurrency)(signals, TG_CONCURRENCY_LIMIT, async (signal) => {
6378
6766
  try {
6379
6767
  const analysis = await (0, import_redis3.getData)(
6380
6768
  import_redis3.redisKeys.analysis(signal.symbol, signal.signalId),
@@ -6384,13 +6772,17 @@ var sendToTG = async (signals, imgInterval) => {
6384
6772
  if (analysis && typeof analysis === "object" && Object.keys(analysis).length > 0) {
6385
6773
  await sendSignalAnalysis(signal, analysis);
6386
6774
  }
6387
- } catch {
6388
- import_logger4.logger.error("Failed sent: %s", signal.symbol);
6775
+ } catch (err) {
6776
+ import_logger5.logger.error(
6777
+ "Failed sent: %s %s",
6778
+ signal.symbol,
6779
+ err?.message || String(err)
6780
+ );
6389
6781
  } finally {
6390
6782
  bar.tick(1, { symbol: import_chalk.default.gray(signal.symbol) });
6391
6783
  }
6392
6784
  });
6393
- import_logger4.logger.info("");
6785
+ import_logger5.logger.info("");
6394
6786
  };
6395
6787
  // Annotate the CommonJS export names for ESM import in node:
6396
6788
  0 && (module.exports = {