@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/README.md +1 -1
- package/dist/{ai-MDMDKAEE.mjs → ai-G7ATN4YL.mjs} +3 -3
- package/dist/backtest.js +41 -8
- package/dist/backtest.mjs +4 -4
- package/dist/{chunk-MGFEID6K.mjs → chunk-CIY64D57.mjs} +7 -4
- package/dist/{chunk-QDYCJ2OK.mjs → chunk-LMAKIC3C.mjs} +1 -1
- package/dist/{chunk-3C76HVLA.mjs → chunk-P2ZUWONT.mjs} +29 -2
- package/dist/{chunk-H4HXW3EZ.mjs → chunk-RBE4PZER.mjs} +1 -1
- package/dist/chunk-SCMBUEGK.mjs +22 -0
- package/dist/{chunk-CK2PW4L5.mjs → chunk-ZY6ULOWK.mjs} +7 -4
- package/dist/cli.js +496 -104
- package/dist/cli.mjs +483 -100
- package/dist/connectors.js +35 -5
- package/dist/connectors.mjs +2 -2
- package/dist/constants.js +10 -1
- package/dist/constants.mjs +1 -1
- package/dist/registry.js +35 -5
- package/dist/registry.mjs +2 -2
- package/dist/strategies.js +321 -81
- package/dist/strategies.mjs +290 -80
- package/package.json +15 -6
- package/dist/chunk-4ZPGZWO7.mjs +0 -13
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-
|
|
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-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-LMAKIC3C.mjs";
|
|
13
|
+
import "./chunk-ZY6ULOWK.mjs";
|
|
14
14
|
import {
|
|
15
15
|
getTradejsProjectCwd
|
|
16
|
-
} from "./chunk-
|
|
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
|
|
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,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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
327
|
-
logger.info(
|
|
702
|
+
logger2.info(
|
|
328
703
|
"tg sendMessage (analysis): %s",
|
|
329
|
-
data?.ok ? "sent" :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
752
|
+
logger3.error(
|
|
378
753
|
"Redis write is blocked by MISCONF (RDB save failure). Cleanup stopped early."
|
|
379
754
|
);
|
|
380
|
-
|
|
755
|
+
logger3.error(
|
|
381
756
|
"Check Redis logs and fix persistence/memory pressure before retry."
|
|
382
757
|
);
|
|
383
|
-
|
|
758
|
+
logger3.info("");
|
|
384
759
|
return;
|
|
385
760
|
}
|
|
386
761
|
throw e;
|
|
387
762
|
}
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
794
|
+
logger3.error("Failed loading: %s", symbol);
|
|
416
795
|
} finally {
|
|
417
796
|
bar.tick(1, { symbol: chalk.gray(symbol) });
|
|
418
797
|
}
|
|
419
798
|
});
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
922
|
+
logger3.info("");
|
|
540
923
|
};
|
|
541
924
|
export {
|
|
542
925
|
cleanFiles,
|