@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/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
- TG_CONCURRENCY_LIMIT
6
- } from "./chunk-SCMBUEGK.mjs";
4
+ SCREENSHOT_CONCURRENCY_LIMIT
5
+ } from "./chunk-KMJQQ53K.mjs";
7
6
  import {
8
7
  require_lodash
9
- } from "./chunk-GKDBAF3A.mjs";
8
+ } from "./chunk-KZDHZ56N.mjs";
10
9
  import {
11
10
  askAI
12
- } from "./chunk-LMAKIC3C.mjs";
13
- import "./chunk-ZY6ULOWK.mjs";
11
+ } from "./chunk-2JKX3DM7.mjs";
12
+ import "./chunk-WGOYR6AB.mjs";
14
13
  import {
15
- getTradejsProjectCwd
16
- } from "./chunk-P2ZUWONT.mjs";
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 as getData2,
37
- redisKeys as redisKeys2
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 { getData, redisKeys } from "@tradejs/infra/redis";
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(/([?&]token=)[^&]+/i, "$1<hidden>");
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 = maybeError?.message || String(error);
112
+ const message = describeErrorValue(error) || String(error);
70
113
  if (maybeError?.cause == null) {
71
114
  return message;
72
115
  }
73
- const cause = maybeError.cause instanceof Error ? maybeError.cause.message : String(maybeError.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 rootUser = await getData(redisKeys.user("root"), null);
105
- const token2 = rootUser && typeof rootUser === "object" ? rootUser.token : null;
106
- const tokenParam = typeof token2 === "string" && token2.length > 0 ? `&token=${encodeURIComponent(token2)}` : "";
107
- const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1${tokenParam}`;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
361
416
  };
362
- var { APP_URL: APP_URL2, TG_BOT_TOKEN: token, TG_CHAT_ID: chatId } = process.env;
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 = maybeError?.message || String(error);
550
+ const message = describeErrorValue2(error) || String(error);
406
551
  if (maybeError?.cause == null) {
407
552
  return message;
408
553
  }
409
- const cause = maybeError.cause instanceof Error ? maybeError.cause.message : String(maybeError.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 requestTelegram = async (method, init) => {
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("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
- })
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(`Skip reason: <b>${escapeHtml(orderSkipReason)}</b>`);
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("sendPhoto", {
620
- method: "POST",
621
- body: photoBody
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(`Signal direction: <b>${signal.direction}</b>`);
655
- lines.push(`AI direction: <b>${analysis.direction ?? "NO TRADE"}</b>`);
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 (typeof analysis.needRetest === "boolean") {
660
- lines.push(`Need retest: <b>${analysis.needRetest ? "YES" : "NO"}</b>`);
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
- if (typeof analysis.retestPrice === "number") {
663
- lines.push(`Retest price: <b>${formatNumber(analysis.retestPrice)}</b>`);
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
- lines.push(`AI TP: <b>${formatNumber(analysis.takeProfitPrice)}</b>`);
888
+ levels.push(`TP <b>${formatAnalysisLevel(analysis.takeProfitPrice)}</b>`);
667
889
  }
668
890
  if (typeof analysis.stopLossPrice === "number") {
669
- lines.push(`AI SL: <b>${formatNumber(analysis.stopLossPrice)}</b>`);
670
- }
671
- const pushBlock = (title, value) => {
672
- if (!value) return;
673
- const clean = value.trim();
674
- if (!clean) return;
675
- blocks.push(`<b>${title}:</b>
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("sendMessage", {
694
- method: "POST",
695
- headers: { "Content-Type": "application/json" },
696
- body: JSON.stringify({
697
- chat_id: chatId,
698
- text: message,
699
- parse_mode: "HTML"
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 PRELOAD_START = getTimestamp(PRELOAD_DAYS);
767
- const PRELOAD_END = getTimestamp();
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: PRELOAD_START,
789
- end: PRELOAD_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: signals.length,
1130
+ total: deliverableSignals.length,
898
1131
  width: 30
899
1132
  }
900
1133
  );
901
- logger3.info(chalk.yellow("messages:", signals.length));
902
- await runWithConcurrency(signals, TG_CONCURRENCY_LIMIT, async (signal) => {
1134
+ await runWithConcurrency(deliverableSignals, 1, async (signal) => {
903
1135
  try {
904
- const analysis = await getData2(
905
- redisKeys2.analysis(signal.symbol, signal.signalId),
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