@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/README.md +1 -1
- package/dist/chunk-SCMBUEGK.mjs +22 -0
- package/dist/cli.js +491 -105
- package/dist/cli.mjs +475 -98
- package/dist/constants.js +10 -1
- package/dist/constants.mjs +1 -1
- package/package.json +13 -5
- package/dist/chunk-4ZPGZWO7.mjs +0 -13
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
329
|
-
logger.info(
|
|
702
|
+
logger2.info(
|
|
330
703
|
"tg sendMessage (analysis): %s",
|
|
331
|
-
data?.ok ? "sent" :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
752
|
+
logger3.error(
|
|
380
753
|
"Redis write is blocked by MISCONF (RDB save failure). Cleanup stopped early."
|
|
381
754
|
);
|
|
382
|
-
|
|
755
|
+
logger3.error(
|
|
383
756
|
"Check Redis logs and fix persistence/memory pressure before retry."
|
|
384
757
|
);
|
|
385
|
-
|
|
758
|
+
logger3.info("");
|
|
386
759
|
return;
|
|
387
760
|
}
|
|
388
761
|
throw e;
|
|
389
762
|
}
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
794
|
+
logger3.error("Failed loading: %s", symbol);
|
|
418
795
|
} finally {
|
|
419
796
|
bar.tick(1, { symbol: chalk.gray(symbol) });
|
|
420
797
|
}
|
|
421
798
|
});
|
|
422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
922
|
+
logger3.info("");
|
|
546
923
|
};
|
|
547
924
|
export {
|
|
548
925
|
cleanFiles,
|