@tradejs/node 1.0.5 → 1.0.8
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/ai.d.mts +31 -0
- package/dist/ai.d.ts +31 -0
- package/dist/ai.js +1074 -0
- package/dist/ai.mjs +35 -0
- package/dist/backtest.js +713 -5689
- package/dist/backtest.mjs +418 -180
- package/dist/chunk-2JKX3DM7.mjs +619 -0
- package/dist/{chunk-7ICOZAKA.mjs → chunk-H6LIYHU4.mjs} +29 -11
- package/dist/{chunk-CIY64D57.mjs → chunk-JMDYEKIO.mjs} +1 -1
- package/dist/chunk-JRRG3YQG.mjs +154 -0
- package/dist/{chunk-P2ZUWONT.mjs → chunk-JU77QVJ3.mjs} +9 -2
- package/dist/{chunk-SCMBUEGK.mjs → chunk-KMJQQ53K.mjs} +1 -1
- package/dist/{chunk-GKDBAF3A.mjs → chunk-KZDHZ56N.mjs} +25 -7
- package/dist/{chunk-ZY6ULOWK.mjs → chunk-WGOYR6AB.mjs} +1 -1
- package/dist/cli.d.mts +23 -5
- package/dist/cli.d.ts +23 -5
- package/dist/cli.js +1091 -232
- package/dist/cli.mjs +342 -108
- package/dist/connectors.js +7 -2
- package/dist/connectors.mjs +2 -2
- package/dist/constants.js +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/pine.d.mts +10 -6
- package/dist/pine.d.ts +10 -6
- package/dist/pine.js +40 -18
- package/dist/pine.mjs +21 -13
- package/dist/registry.js +7 -2
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +28 -19
- package/dist/strategies.d.ts +28 -19
- package/dist/strategies.js +7690 -6349
- package/dist/strategies.mjs +1060 -169
- package/package.json +11 -6
- package/dist/ai-G7ATN4YL.mjs +0 -19
- package/dist/chunk-LMAKIC3C.mjs +0 -294
- package/dist/chunk-RBE4PZER.mjs +0 -49
package/dist/cli.mjs
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AI_CONCURRENCY_LIMIT,
|
|
3
3
|
KLINE_CONCURRENCY_LIMIT,
|
|
4
|
-
SCREENSHOT_CONCURRENCY_LIMIT
|
|
5
|
-
|
|
6
|
-
} from "./chunk-SCMBUEGK.mjs";
|
|
4
|
+
SCREENSHOT_CONCURRENCY_LIMIT
|
|
5
|
+
} from "./chunk-KMJQQ53K.mjs";
|
|
7
6
|
import {
|
|
8
7
|
require_lodash
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-KZDHZ56N.mjs";
|
|
10
9
|
import {
|
|
11
10
|
askAI
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-2JKX3DM7.mjs";
|
|
12
|
+
import "./chunk-WGOYR6AB.mjs";
|
|
14
13
|
import {
|
|
15
|
-
getTradejsProjectCwd
|
|
16
|
-
|
|
14
|
+
getTradejsProjectCwd,
|
|
15
|
+
loadTradejsConfig
|
|
16
|
+
} from "./chunk-JU77QVJ3.mjs";
|
|
17
17
|
import {
|
|
18
18
|
__toESM
|
|
19
19
|
} from "./chunk-6DZX6EAA.mjs";
|
|
@@ -33,8 +33,8 @@ import {
|
|
|
33
33
|
RedisWriteBlockedError,
|
|
34
34
|
delKeyWithOptions,
|
|
35
35
|
getKeys,
|
|
36
|
-
getData
|
|
37
|
-
redisKeys
|
|
36
|
+
getData,
|
|
37
|
+
redisKeys
|
|
38
38
|
} from "@tradejs/infra/redis";
|
|
39
39
|
import { logger as logger3 } from "@tradejs/infra/logger";
|
|
40
40
|
|
|
@@ -44,7 +44,7 @@ import path from "path";
|
|
|
44
44
|
import puppeteer from "puppeteer";
|
|
45
45
|
import { delay } from "@tradejs/core/async";
|
|
46
46
|
import { logger } from "@tradejs/infra/logger";
|
|
47
|
-
import {
|
|
47
|
+
import { createScreenshotSessionToken } from "@tradejs/infra/redis";
|
|
48
48
|
var { APP_URL } = process.env;
|
|
49
49
|
var SCREENSHOT_NAVIGATION_ATTEMPTS = 3;
|
|
50
50
|
var SCREENSHOT_NAVIGATION_RETRY_DELAY_MS = 2e3;
|
|
@@ -63,14 +63,60 @@ var SCREENSHOT_VIEWPORT = {
|
|
|
63
63
|
};
|
|
64
64
|
var getProjectRoot = (projectRoot) => path.resolve(getTradejsProjectCwd(projectRoot));
|
|
65
65
|
var getScreenshotsDir = (projectRoot) => path.join(getProjectRoot(projectRoot), "data", "screenshots");
|
|
66
|
-
var maskTokenInUrl = (url) => url.replace(/([?&]
|
|
66
|
+
var maskTokenInUrl = (url) => url.replace(/([?&]screenshotToken=)[^&]+/i, "$1<hidden>");
|
|
67
|
+
var getErrorFields = (value) => {
|
|
68
|
+
if (!value || typeof value !== "object") {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const record = value;
|
|
72
|
+
const fields = [
|
|
73
|
+
["name", record.name],
|
|
74
|
+
["code", record.code],
|
|
75
|
+
["errno", record.errno],
|
|
76
|
+
["type", record.type],
|
|
77
|
+
["syscall", record.syscall],
|
|
78
|
+
["hostname", record.hostname],
|
|
79
|
+
["host", record.host],
|
|
80
|
+
["address", record.address],
|
|
81
|
+
["port", record.port]
|
|
82
|
+
];
|
|
83
|
+
return fields.filter(([, fieldValue]) => fieldValue != null && String(fieldValue).trim()).map(([key, fieldValue]) => `${key}=${String(fieldValue)}`);
|
|
84
|
+
};
|
|
85
|
+
var describeErrorValue = (value) => {
|
|
86
|
+
if (value == null) {
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
if (value instanceof Error) {
|
|
93
|
+
const message = value.message?.trim() || "";
|
|
94
|
+
const fields = getErrorFields(value);
|
|
95
|
+
return [message, ...fields].filter(Boolean).join(" ");
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === "object") {
|
|
98
|
+
const fields = getErrorFields(value);
|
|
99
|
+
if (fields.length) {
|
|
100
|
+
return fields.join(" ");
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return JSON.stringify(value);
|
|
104
|
+
} catch {
|
|
105
|
+
return String(value);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return String(value);
|
|
109
|
+
};
|
|
67
110
|
var getErrorMessage = (error) => {
|
|
68
111
|
const maybeError = error;
|
|
69
|
-
const message =
|
|
112
|
+
const message = describeErrorValue(error) || String(error);
|
|
70
113
|
if (maybeError?.cause == null) {
|
|
71
114
|
return message;
|
|
72
115
|
}
|
|
73
|
-
const cause =
|
|
116
|
+
const cause = describeErrorValue(maybeError.cause);
|
|
117
|
+
if (!cause) {
|
|
118
|
+
return message;
|
|
119
|
+
}
|
|
74
120
|
return `${message}; cause: ${cause}`;
|
|
75
121
|
};
|
|
76
122
|
var truncateText = (value, maxLength = SCREENSHOT_CONSOLE_TEXT_LIMIT) => value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
@@ -97,14 +143,17 @@ var getScreenshotPath = ({ symbol, signalId, interval }, projectRoot) => {
|
|
|
97
143
|
getScreenshotFilename({ symbol, signalId, interval })
|
|
98
144
|
);
|
|
99
145
|
};
|
|
100
|
-
var screenDashboard = async (signal, projectRoot) => {
|
|
146
|
+
var screenDashboard = async (signal, projectRoot, userName = "root") => {
|
|
101
147
|
const { symbol, signalId, interval } = signal;
|
|
102
148
|
const screenshotBaseUrl = getScreenshotRenderBaseUrl();
|
|
103
149
|
const screenshotPath = getScreenshotPath(signal, projectRoot);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
const screenshotToken = await createScreenshotSessionToken(userName);
|
|
151
|
+
if (!screenshotToken) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Failed to create screenshot session token for ${userName}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1&screenshotToken=${encodeURIComponent(screenshotToken)}`;
|
|
108
157
|
const maskedDashboardUrl = maskTokenInUrl(dashboardUrl);
|
|
109
158
|
logger.info(
|
|
110
159
|
"screenshot start: %s %sm url=%s path=%s",
|
|
@@ -128,6 +177,7 @@ var screenDashboard = async (signal, projectRoot) => {
|
|
|
128
177
|
]
|
|
129
178
|
});
|
|
130
179
|
const browserProcess = browser.process();
|
|
180
|
+
let browserCloseRequested = false;
|
|
131
181
|
try {
|
|
132
182
|
if (browserProcess) {
|
|
133
183
|
logger.info(
|
|
@@ -147,6 +197,9 @@ var screenDashboard = async (signal, projectRoot) => {
|
|
|
147
197
|
await browser.version()
|
|
148
198
|
);
|
|
149
199
|
browser.on("disconnected", () => {
|
|
200
|
+
if (browserCloseRequested) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
150
203
|
logger.error(
|
|
151
204
|
"screenshot browser disconnected: %s %sm captureAttempt=%d",
|
|
152
205
|
symbol,
|
|
@@ -345,6 +398,7 @@ var screenDashboard = async (signal, projectRoot) => {
|
|
|
345
398
|
await delay(SCREENSHOT_CAPTURE_RETRY_DELAY_MS);
|
|
346
399
|
}
|
|
347
400
|
} finally {
|
|
401
|
+
browserCloseRequested = true;
|
|
348
402
|
await browser.close().catch(() => void 0);
|
|
349
403
|
}
|
|
350
404
|
}
|
|
@@ -355,14 +409,105 @@ var screenDashboard = async (signal, projectRoot) => {
|
|
|
355
409
|
import { delay as delay2 } from "@tradejs/core/async";
|
|
356
410
|
import { formatNumber } from "@tradejs/core/math";
|
|
357
411
|
import { logger as logger2 } from "@tradejs/infra/logger";
|
|
412
|
+
import { getUserSettings } from "@tradejs/infra/userSettings";
|
|
358
413
|
var escapeHtml = (s) => {
|
|
359
414
|
if (!s) return "";
|
|
360
415
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
361
416
|
};
|
|
362
|
-
var { APP_URL: APP_URL2
|
|
417
|
+
var { APP_URL: APP_URL2 } = process.env;
|
|
363
418
|
var TG_REQUEST_ATTEMPTS = 3;
|
|
364
419
|
var TG_REQUEST_RETRY_DELAY_MS = 2e3;
|
|
420
|
+
var getErrorFields2 = (value) => {
|
|
421
|
+
if (!value || typeof value !== "object") {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
const record = value;
|
|
425
|
+
const fields = [
|
|
426
|
+
["name", record.name],
|
|
427
|
+
["code", record.code],
|
|
428
|
+
["errno", record.errno],
|
|
429
|
+
["type", record.type],
|
|
430
|
+
["syscall", record.syscall],
|
|
431
|
+
["hostname", record.hostname],
|
|
432
|
+
["host", record.host],
|
|
433
|
+
["address", record.address],
|
|
434
|
+
["port", record.port]
|
|
435
|
+
];
|
|
436
|
+
return fields.filter(([, fieldValue]) => fieldValue != null && String(fieldValue).trim()).map(([key, fieldValue]) => `${key}=${String(fieldValue)}`);
|
|
437
|
+
};
|
|
438
|
+
var describeErrorValue2 = (value) => {
|
|
439
|
+
if (value == null) {
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
if (typeof value === "string") {
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
if (value instanceof Error) {
|
|
446
|
+
const message = value.message?.trim() || "";
|
|
447
|
+
const fields = getErrorFields2(value);
|
|
448
|
+
return [message, ...fields].filter(Boolean).join(" ");
|
|
449
|
+
}
|
|
450
|
+
if (typeof value === "object") {
|
|
451
|
+
const fields = getErrorFields2(value);
|
|
452
|
+
if (fields.length) {
|
|
453
|
+
return fields.join(" ");
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
return JSON.stringify(value);
|
|
457
|
+
} catch {
|
|
458
|
+
return String(value);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return String(value);
|
|
462
|
+
};
|
|
365
463
|
var normalizeQuality = (value) => typeof value === "number" ? Math.max(1, Math.min(5, Math.round(value))) : null;
|
|
464
|
+
var formatAnalysisLevel = (value) => {
|
|
465
|
+
if (value == null || !Number.isFinite(value)) return null;
|
|
466
|
+
if (Math.abs(value) >= 1e3) {
|
|
467
|
+
return value.toFixed(0);
|
|
468
|
+
}
|
|
469
|
+
const digits = Math.abs(value) < 1 ? 8 : 6;
|
|
470
|
+
return value.toFixed(digits).replace(/\.?0+$/, "");
|
|
471
|
+
};
|
|
472
|
+
var normalizeAnalysisText = (value) => {
|
|
473
|
+
if (typeof value !== "string") return null;
|
|
474
|
+
const clean = value.replace(/\s+/g, " ").trim();
|
|
475
|
+
return clean || null;
|
|
476
|
+
};
|
|
477
|
+
var getReadableAnalysisComment = (value) => {
|
|
478
|
+
const clean = normalizeAnalysisText(value);
|
|
479
|
+
if (!clean) return null;
|
|
480
|
+
if (/^(ok|approved|approved by ai|historical approval|historical reject)$/i.test(
|
|
481
|
+
clean
|
|
482
|
+
)) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
return clean;
|
|
486
|
+
};
|
|
487
|
+
var takeUniqueAnalysisText = (usedValues, ...values) => {
|
|
488
|
+
for (const value of values) {
|
|
489
|
+
const clean = normalizeAnalysisText(value);
|
|
490
|
+
if (!clean) continue;
|
|
491
|
+
const key = clean.toLowerCase();
|
|
492
|
+
if (usedValues.has(key)) continue;
|
|
493
|
+
usedValues.add(key);
|
|
494
|
+
return clean;
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
};
|
|
498
|
+
var formatOrderSkipReason = (reason) => {
|
|
499
|
+
if (!reason) return "";
|
|
500
|
+
if (reason.startsWith("AI_QUALITY_BELOW_MIN")) {
|
|
501
|
+
return "AI_QUALITY_BELOW_MIN";
|
|
502
|
+
}
|
|
503
|
+
if (reason.startsWith("ML_THRESHOLD_NOT_MET")) {
|
|
504
|
+
return "ML_THRESHOLD_NOT_MET";
|
|
505
|
+
}
|
|
506
|
+
if (reason === "ML_RESULT_UNAVAILABLE") {
|
|
507
|
+
return "ML_RESULT_UNAVAILABLE";
|
|
508
|
+
}
|
|
509
|
+
return reason;
|
|
510
|
+
};
|
|
366
511
|
var getLastNumber = (value) => {
|
|
367
512
|
if (Array.isArray(value)) {
|
|
368
513
|
const last = value[value.length - 1];
|
|
@@ -402,11 +547,14 @@ var getTelegramErrorReason = (data) => {
|
|
|
402
547
|
};
|
|
403
548
|
var getErrorMessage2 = (error) => {
|
|
404
549
|
const maybeError = error;
|
|
405
|
-
const message =
|
|
550
|
+
const message = describeErrorValue2(error) || String(error);
|
|
406
551
|
if (maybeError?.cause == null) {
|
|
407
552
|
return message;
|
|
408
553
|
}
|
|
409
|
-
const cause =
|
|
554
|
+
const cause = describeErrorValue2(maybeError.cause);
|
|
555
|
+
if (!cause) {
|
|
556
|
+
return message;
|
|
557
|
+
}
|
|
410
558
|
return `${message}; cause: ${cause}`;
|
|
411
559
|
};
|
|
412
560
|
var parseTelegramResponse = async (response) => {
|
|
@@ -429,7 +577,20 @@ var parseTelegramResponse = async (response) => {
|
|
|
429
577
|
}
|
|
430
578
|
}
|
|
431
579
|
};
|
|
432
|
-
var
|
|
580
|
+
var getTelegramSettings = async (userName = "root") => {
|
|
581
|
+
const settings = await getUserSettings(userName);
|
|
582
|
+
const token = settings.TG_BOT_TOKEN;
|
|
583
|
+
const chatId = settings.TG_CHAT_ID;
|
|
584
|
+
if (!token || !chatId) {
|
|
585
|
+
throw new Error(`Telegram settings are incomplete for user ${userName}`);
|
|
586
|
+
}
|
|
587
|
+
return { token, chatId };
|
|
588
|
+
};
|
|
589
|
+
var requestTelegram = async ({
|
|
590
|
+
method,
|
|
591
|
+
token,
|
|
592
|
+
init
|
|
593
|
+
}) => {
|
|
433
594
|
let lastError = null;
|
|
434
595
|
for (let attempt = 1; attempt <= TG_REQUEST_ATTEMPTS; attempt += 1) {
|
|
435
596
|
try {
|
|
@@ -457,17 +618,23 @@ var requestTelegram = async (method, init) => {
|
|
|
457
618
|
};
|
|
458
619
|
var sendTelegramMessage = async ({
|
|
459
620
|
message,
|
|
460
|
-
markup
|
|
621
|
+
markup,
|
|
622
|
+
token,
|
|
623
|
+
chatId
|
|
461
624
|
}) => {
|
|
462
|
-
const data = await requestTelegram(
|
|
463
|
-
method: "
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
625
|
+
const data = await requestTelegram({
|
|
626
|
+
method: "sendMessage",
|
|
627
|
+
token,
|
|
628
|
+
init: {
|
|
629
|
+
method: "POST",
|
|
630
|
+
headers: { "Content-Type": "application/json" },
|
|
631
|
+
body: JSON.stringify({
|
|
632
|
+
chat_id: chatId,
|
|
633
|
+
text: message,
|
|
634
|
+
reply_markup: markup,
|
|
635
|
+
parse_mode: "HTML"
|
|
636
|
+
})
|
|
637
|
+
}
|
|
471
638
|
});
|
|
472
639
|
logger2.info(
|
|
473
640
|
"tg sendMessage: %s",
|
|
@@ -475,6 +642,15 @@ var sendTelegramMessage = async ({
|
|
|
475
642
|
);
|
|
476
643
|
return data;
|
|
477
644
|
};
|
|
645
|
+
var sendTextToTG = async (message, options = {}) => {
|
|
646
|
+
const { token, chatId } = await getTelegramSettings(options.userName);
|
|
647
|
+
return sendTelegramMessage({
|
|
648
|
+
message,
|
|
649
|
+
markup: options.markup,
|
|
650
|
+
token,
|
|
651
|
+
chatId
|
|
652
|
+
});
|
|
653
|
+
};
|
|
478
654
|
var formatMessage = (signal, analysis) => {
|
|
479
655
|
const {
|
|
480
656
|
symbol,
|
|
@@ -526,7 +702,9 @@ var formatMessage = (signal, analysis) => {
|
|
|
526
702
|
}
|
|
527
703
|
lines.push(orderStatusText);
|
|
528
704
|
if ((orderStatus === "skipped" || orderStatus === "canceled") && orderSkipReason) {
|
|
529
|
-
lines.push(
|
|
705
|
+
lines.push(
|
|
706
|
+
`Skip reason: <b>${escapeHtml(formatOrderSkipReason(orderSkipReason))}</b>`
|
|
707
|
+
);
|
|
530
708
|
}
|
|
531
709
|
}
|
|
532
710
|
if (isConfigFromBacktest) {
|
|
@@ -544,21 +722,9 @@ var formatMessage = (signal, analysis) => {
|
|
|
544
722
|
lines.push(aiQualityLine);
|
|
545
723
|
}
|
|
546
724
|
lines.push("");
|
|
547
|
-
if (touches) {
|
|
548
|
-
lines.push(`Points: ${touches}`);
|
|
549
|
-
}
|
|
550
|
-
if (atrPct != null && Number.isFinite(atrPct)) {
|
|
551
|
-
lines.push(`ATR: ${atrPct.toFixed(2)}`);
|
|
552
|
-
}
|
|
553
|
-
if (distance) {
|
|
554
|
-
lines.push(`Distance: ${distance}`);
|
|
555
|
-
}
|
|
556
725
|
if (correlation) {
|
|
557
726
|
lines.push(`BTC correlation: ${correlation}`);
|
|
558
727
|
}
|
|
559
|
-
if (spread != null && Number.isFinite(spread)) {
|
|
560
|
-
lines.push(`BTC spread (CB-BN)/BN: ${spread.toFixed(6)}`);
|
|
561
|
-
}
|
|
562
728
|
const prices = formatPrices();
|
|
563
729
|
if (prices) {
|
|
564
730
|
lines.push("");
|
|
@@ -572,8 +738,9 @@ var formatMessage = (signal, analysis) => {
|
|
|
572
738
|
\u0414\u0435\u0442\u0430\u043B\u0438: ${err.message || String(err)}`;
|
|
573
739
|
}
|
|
574
740
|
};
|
|
575
|
-
var sendSignal = async (signal, imgInterval, analysis) => {
|
|
741
|
+
var sendSignal = async (signal, imgInterval, analysis, options = {}) => {
|
|
576
742
|
const { symbol, signalId, interval } = signal;
|
|
743
|
+
const { token, chatId } = await getTelegramSettings(options.userName);
|
|
577
744
|
const message = formatMessage(signal, analysis);
|
|
578
745
|
const publicAppUrl = APP_URL2?.startsWith("https") ? APP_URL2 : null;
|
|
579
746
|
const dashboardUrl = publicAppUrl ? `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}` : null;
|
|
@@ -599,7 +766,7 @@ var sendSignal = async (signal, imgInterval, analysis) => {
|
|
|
599
766
|
symbol,
|
|
600
767
|
error?.message || String(error)
|
|
601
768
|
);
|
|
602
|
-
await sendTelegramMessage({ message, markup });
|
|
769
|
+
await sendTelegramMessage({ message, markup, token, chatId });
|
|
603
770
|
return;
|
|
604
771
|
}
|
|
605
772
|
const photoBody = new FormData();
|
|
@@ -616,9 +783,13 @@ var sendSignal = async (signal, imgInterval, analysis) => {
|
|
|
616
783
|
}
|
|
617
784
|
let data;
|
|
618
785
|
try {
|
|
619
|
-
data = await requestTelegram(
|
|
620
|
-
method: "
|
|
621
|
-
|
|
786
|
+
data = await requestTelegram({
|
|
787
|
+
method: "sendPhoto",
|
|
788
|
+
token,
|
|
789
|
+
init: {
|
|
790
|
+
method: "POST",
|
|
791
|
+
body: photoBody
|
|
792
|
+
}
|
|
622
793
|
});
|
|
623
794
|
} catch (error) {
|
|
624
795
|
const reason = getErrorMessage2(error);
|
|
@@ -628,7 +799,9 @@ var sendSignal = async (signal, imgInterval, analysis) => {
|
|
|
628
799
|
|
|
629
800
|
\u26A0\uFE0F <b>Photo delivery failed</b>
|
|
630
801
|
Reason: <code>${escapeHtml(reason)}</code>`,
|
|
631
|
-
markup
|
|
802
|
+
markup,
|
|
803
|
+
token,
|
|
804
|
+
chatId
|
|
632
805
|
});
|
|
633
806
|
return;
|
|
634
807
|
}
|
|
@@ -640,7 +813,9 @@ Reason: <code>${escapeHtml(reason)}</code>`,
|
|
|
640
813
|
|
|
641
814
|
\u26A0\uFE0F <b>Photo delivery failed</b>
|
|
642
815
|
Reason: <code>${escapeHtml(reason)}</code>`,
|
|
643
|
-
markup
|
|
816
|
+
markup,
|
|
817
|
+
token,
|
|
818
|
+
chatId
|
|
644
819
|
});
|
|
645
820
|
return;
|
|
646
821
|
}
|
|
@@ -648,56 +823,96 @@ Reason: <code>${escapeHtml(reason)}</code>`,
|
|
|
648
823
|
};
|
|
649
824
|
var formatAnalysisMessage = (signal, analysis) => {
|
|
650
825
|
const lines = [];
|
|
651
|
-
const blocks = [];
|
|
652
826
|
const quality = normalizeQuality(analysis.quality);
|
|
827
|
+
const readableComment = getReadableAnalysisComment(analysis.comment);
|
|
828
|
+
const approvedDirection = analysis.direction != null && analysis.direction === signal.direction && analysis.needRetest !== true;
|
|
829
|
+
const oppositeDirection = analysis.direction != null && analysis.direction !== signal.direction;
|
|
830
|
+
const verdict = approvedDirection ? `Approved ${signal.direction}` : oppositeDirection ? `Not approved: AI prefers ${analysis.direction}` : analysis.needRetest ? "Not approved yet" : "Not approved";
|
|
831
|
+
const summary = approvedDirection ? `AI approves this ${signal.direction} setup right now.` : oppositeDirection ? `AI does not approve this ${signal.direction} setup and currently leans ${analysis.direction}.` : analysis.needRetest ? `The ${signal.direction} setup is visible, but confirmation is still missing before entry.` : `AI does not approve this ${signal.direction} setup right now.`;
|
|
832
|
+
const usedValues = /* @__PURE__ */ new Set();
|
|
653
833
|
lines.push(`<b>AI analysis ${signal.symbol}</b>`);
|
|
654
|
-
lines.push(`
|
|
655
|
-
lines.push(
|
|
834
|
+
lines.push(`Verdict: <b>${escapeHtml(verdict)}</b>`);
|
|
835
|
+
lines.push(escapeHtml(summary));
|
|
656
836
|
if (quality) {
|
|
657
837
|
lines.push(`Quality: <b>${quality}/5</b>`);
|
|
658
838
|
}
|
|
659
|
-
if (
|
|
660
|
-
lines.push(
|
|
839
|
+
if (analysis.gateDecision && analysis.llmDecision) {
|
|
840
|
+
lines.push(
|
|
841
|
+
`Gate vs LLM: <b>${analysis.gateContradictsLlm ? "conflict" : "aligned"}</b> (gate ${analysis.gateDecision}, LLM ${analysis.llmDecision})`
|
|
842
|
+
);
|
|
661
843
|
}
|
|
662
|
-
|
|
663
|
-
|
|
844
|
+
const happeningText = takeUniqueAnalysisText(
|
|
845
|
+
usedValues,
|
|
846
|
+
analysis.setup,
|
|
847
|
+
readableComment
|
|
848
|
+
);
|
|
849
|
+
if (happeningText) {
|
|
850
|
+
lines.push(`What's happening: ${escapeHtml(happeningText)}`);
|
|
664
851
|
}
|
|
852
|
+
const reasonText = approvedDirection ? takeUniqueAnalysisText(
|
|
853
|
+
usedValues,
|
|
854
|
+
analysis.qualityReason,
|
|
855
|
+
analysis.confirmations,
|
|
856
|
+
readableComment
|
|
857
|
+
) : takeUniqueAnalysisText(
|
|
858
|
+
usedValues,
|
|
859
|
+
analysis.qualityReason,
|
|
860
|
+
analysis.confirmations,
|
|
861
|
+
readableComment,
|
|
862
|
+
analysis.btcContext
|
|
863
|
+
);
|
|
864
|
+
if (reasonText) {
|
|
865
|
+
lines.push(
|
|
866
|
+
`${approvedDirection ? "Why approved" : "Why not approved"}: ${escapeHtml(reasonText)}`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
const nextText = analysis.needRetest ? takeUniqueAnalysisText(
|
|
870
|
+
usedValues,
|
|
871
|
+
analysis.retestPlan,
|
|
872
|
+
analysis.triggerInvalidation,
|
|
873
|
+
typeof analysis.retestPrice === "number" ? `Wait for confirmation around ${formatAnalysisLevel(analysis.retestPrice)}.` : null
|
|
874
|
+
) : takeUniqueAnalysisText(
|
|
875
|
+
usedValues,
|
|
876
|
+
analysis.triggerInvalidation,
|
|
877
|
+
analysis.retestPlan
|
|
878
|
+
);
|
|
879
|
+
if (nextText) {
|
|
880
|
+
lines.push(`Next: ${escapeHtml(nextText)}`);
|
|
881
|
+
}
|
|
882
|
+
const btcText = takeUniqueAnalysisText(usedValues, analysis.btcContext);
|
|
883
|
+
if (btcText) {
|
|
884
|
+
lines.push(`BTC context: ${escapeHtml(btcText)}`);
|
|
885
|
+
}
|
|
886
|
+
const levels = [];
|
|
665
887
|
if (typeof analysis.takeProfitPrice === "number") {
|
|
666
|
-
|
|
888
|
+
levels.push(`TP <b>${formatAnalysisLevel(analysis.takeProfitPrice)}</b>`);
|
|
667
889
|
}
|
|
668
890
|
if (typeof analysis.stopLossPrice === "number") {
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
${escapeHtml(clean)}`);
|
|
677
|
-
};
|
|
678
|
-
pushBlock("Setup", analysis.setup);
|
|
679
|
-
pushBlock("Confirmations", analysis.confirmations);
|
|
680
|
-
pushBlock("BTC", analysis.btcContext);
|
|
681
|
-
pushBlock("Retest", analysis.retestPlan);
|
|
682
|
-
pushBlock("Risk/Levels", analysis.riskLevels);
|
|
683
|
-
pushBlock("Why Quality", analysis.qualityReason);
|
|
684
|
-
pushBlock("Trigger/Invalidation", analysis.triggerInvalidation);
|
|
685
|
-
if (blocks.length > 0) {
|
|
686
|
-
lines.push("");
|
|
687
|
-
lines.push(blocks.join("\n\n"));
|
|
891
|
+
levels.push(`SL <b>${formatAnalysisLevel(analysis.stopLossPrice)}</b>`);
|
|
892
|
+
}
|
|
893
|
+
if (typeof analysis.retestPrice === "number") {
|
|
894
|
+
levels.push(`Retest <b>${formatAnalysisLevel(analysis.retestPrice)}</b>`);
|
|
895
|
+
}
|
|
896
|
+
if (levels.length > 0) {
|
|
897
|
+
lines.push(`Levels: ${levels.join(" | ")}`);
|
|
688
898
|
}
|
|
689
899
|
return lines.join("\n");
|
|
690
900
|
};
|
|
691
|
-
var sendSignalAnalysis = async (signal, analysis) => {
|
|
901
|
+
var sendSignalAnalysis = async (signal, analysis, options = {}) => {
|
|
902
|
+
const { token, chatId } = await getTelegramSettings(options.userName);
|
|
692
903
|
const message = formatAnalysisMessage(signal, analysis);
|
|
693
|
-
const data = await requestTelegram(
|
|
694
|
-
method: "
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
904
|
+
const data = await requestTelegram({
|
|
905
|
+
method: "sendMessage",
|
|
906
|
+
token,
|
|
907
|
+
init: {
|
|
908
|
+
method: "POST",
|
|
909
|
+
headers: { "Content-Type": "application/json" },
|
|
910
|
+
body: JSON.stringify({
|
|
911
|
+
chat_id: chatId,
|
|
912
|
+
text: message,
|
|
913
|
+
parse_mode: "HTML"
|
|
914
|
+
})
|
|
915
|
+
}
|
|
701
916
|
});
|
|
702
917
|
logger2.info(
|
|
703
918
|
"tg sendMessage (analysis): %s",
|
|
@@ -762,9 +977,18 @@ var cleanRedis = async (area) => {
|
|
|
762
977
|
}
|
|
763
978
|
logger3.info("");
|
|
764
979
|
};
|
|
765
|
-
var update = async (connector, interval, tickers) => {
|
|
766
|
-
const
|
|
767
|
-
|
|
980
|
+
var update = async (connector, interval, tickers, preloadDays = PRELOAD_DAYS, options = {}) => {
|
|
981
|
+
const preloadStart = Math.trunc(
|
|
982
|
+
options.preloadStart ?? getTimestamp(preloadDays)
|
|
983
|
+
);
|
|
984
|
+
const preloadEnd = Math.trunc(options.preloadEnd ?? getTimestamp());
|
|
985
|
+
const connectorLabel = String(options.connectorLabel || "").trim();
|
|
986
|
+
const preloadLabel = options.preloadStart != null || options.preloadEnd != null ? `preloadStart=${preloadStart}, preloadEnd=${preloadEnd}` : `preloadDays=${preloadDays}`;
|
|
987
|
+
if (preloadStart >= preloadEnd) {
|
|
988
|
+
throw new Error(
|
|
989
|
+
`Invalid update preload window: start (${preloadStart}) must be less than end (${preloadEnd})`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
768
992
|
const bar = new ProgressBar(
|
|
769
993
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
770
994
|
{
|
|
@@ -774,7 +998,7 @@ var update = async (connector, interval, tickers) => {
|
|
|
774
998
|
);
|
|
775
999
|
logger3.info(
|
|
776
1000
|
chalk.yellow(
|
|
777
|
-
`update: ${tickers.length} (klineConcurrency=${KLINE_CONCURRENCY_LIMIT})`
|
|
1001
|
+
`update: ${tickers.length} (connector=${connectorLabel || "unknown"}, klineConcurrency=${KLINE_CONCURRENCY_LIMIT}, ${preloadLabel})`
|
|
778
1002
|
)
|
|
779
1003
|
);
|
|
780
1004
|
const queue = tickers.slice();
|
|
@@ -785,10 +1009,11 @@ var update = async (connector, interval, tickers) => {
|
|
|
785
1009
|
try {
|
|
786
1010
|
await connector.kline({
|
|
787
1011
|
symbol,
|
|
788
|
-
start:
|
|
789
|
-
end:
|
|
1012
|
+
start: preloadStart,
|
|
1013
|
+
end: preloadEnd,
|
|
790
1014
|
interval,
|
|
791
|
-
silent: true
|
|
1015
|
+
silent: true,
|
|
1016
|
+
warmOnly: true
|
|
792
1017
|
});
|
|
793
1018
|
} catch {
|
|
794
1019
|
logger3.error("Failed loading: %s", symbol);
|
|
@@ -841,7 +1066,7 @@ var getTickers = async (connector, include = "", exclude = "", limit, chunk) =>
|
|
|
841
1066
|
}
|
|
842
1067
|
return tickers.filter((t) => !excludeTickers.includes(t));
|
|
843
1068
|
};
|
|
844
|
-
var makeScreenshots = async (signals, interval) => {
|
|
1069
|
+
var makeScreenshots = async (signals, interval, userName = "root") => {
|
|
845
1070
|
const projectRoot = getProjectRoot2();
|
|
846
1071
|
const bar = new ProgressBar(
|
|
847
1072
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
@@ -856,7 +1081,7 @@ var makeScreenshots = async (signals, interval) => {
|
|
|
856
1081
|
SCREENSHOT_CONCURRENCY_LIMIT,
|
|
857
1082
|
async (signal) => {
|
|
858
1083
|
try {
|
|
859
|
-
await screenDashboard({ ...signal, interval }, projectRoot);
|
|
1084
|
+
await screenDashboard({ ...signal, interval }, projectRoot, userName);
|
|
860
1085
|
} catch (error) {
|
|
861
1086
|
logger3.error(
|
|
862
1087
|
"Failed screenshot: %s (%s)",
|
|
@@ -870,7 +1095,7 @@ var makeScreenshots = async (signals, interval) => {
|
|
|
870
1095
|
);
|
|
871
1096
|
logger3.info("");
|
|
872
1097
|
};
|
|
873
|
-
var sendToAI = async (signals) => {
|
|
1098
|
+
var sendToAI = async (signals, userName = "root") => {
|
|
874
1099
|
const bar = new ProgressBar(
|
|
875
1100
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
876
1101
|
{
|
|
@@ -881,7 +1106,7 @@ var sendToAI = async (signals) => {
|
|
|
881
1106
|
logger3.info(chalk.yellow("AI:", signals.length));
|
|
882
1107
|
await runWithConcurrency(signals, AI_CONCURRENCY_LIMIT, async (signal) => {
|
|
883
1108
|
try {
|
|
884
|
-
await askAI(signal);
|
|
1109
|
+
await askAI(signal, { userName });
|
|
885
1110
|
} catch {
|
|
886
1111
|
logger3.error("Failed ask: %s", signal.symbol);
|
|
887
1112
|
} finally {
|
|
@@ -890,24 +1115,31 @@ var sendToAI = async (signals) => {
|
|
|
890
1115
|
});
|
|
891
1116
|
logger3.info("");
|
|
892
1117
|
};
|
|
893
|
-
var sendToTG = async (signals, imgInterval) => {
|
|
1118
|
+
var sendToTG = async (signals, imgInterval, userName = "root") => {
|
|
1119
|
+
const deliverableSignals = signals.filter(
|
|
1120
|
+
(signal) => signal.orderStatus !== "skipped" && signal.orderStatus !== "canceled"
|
|
1121
|
+
);
|
|
1122
|
+
logger3.info(chalk.yellow("messages:", deliverableSignals.length));
|
|
1123
|
+
if (!deliverableSignals.length) {
|
|
1124
|
+
logger3.info("");
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
894
1127
|
const bar = new ProgressBar(
|
|
895
1128
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
896
1129
|
{
|
|
897
|
-
total:
|
|
1130
|
+
total: deliverableSignals.length,
|
|
898
1131
|
width: 30
|
|
899
1132
|
}
|
|
900
1133
|
);
|
|
901
|
-
|
|
902
|
-
await runWithConcurrency(signals, TG_CONCURRENCY_LIMIT, async (signal) => {
|
|
1134
|
+
await runWithConcurrency(deliverableSignals, 1, async (signal) => {
|
|
903
1135
|
try {
|
|
904
|
-
const analysis = await
|
|
905
|
-
|
|
1136
|
+
const analysis = await getData(
|
|
1137
|
+
redisKeys.analysis(signal.symbol, signal.signalId),
|
|
906
1138
|
null
|
|
907
1139
|
);
|
|
908
|
-
await sendSignal(signal, imgInterval, analysis);
|
|
1140
|
+
await sendSignal(signal, imgInterval, analysis, { userName });
|
|
909
1141
|
if (analysis && typeof analysis === "object" && Object.keys(analysis).length > 0) {
|
|
910
|
-
await sendSignalAnalysis(signal, analysis);
|
|
1142
|
+
await sendSignalAnalysis(signal, analysis, { userName });
|
|
911
1143
|
}
|
|
912
1144
|
} catch (err) {
|
|
913
1145
|
logger3.error(
|
|
@@ -926,7 +1158,9 @@ export {
|
|
|
926
1158
|
cleanRedis,
|
|
927
1159
|
drawStatInCLI,
|
|
928
1160
|
getTickers,
|
|
1161
|
+
loadTradejsConfig,
|
|
929
1162
|
makeScreenshots,
|
|
1163
|
+
sendTextToTG,
|
|
930
1164
|
sendToAI,
|
|
931
1165
|
sendToTG,
|
|
932
1166
|
update
|