@tradejs/node 1.0.9 → 1.0.11

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
@@ -8,12 +8,12 @@ import {
8
8
  } from "./chunk-KZDHZ56N.mjs";
9
9
  import {
10
10
  askAI
11
- } from "./chunk-2JKX3DM7.mjs";
12
- import "./chunk-WGOYR6AB.mjs";
11
+ } from "./chunk-IUZML4RK.mjs";
12
+ import "./chunk-QVSMINLG.mjs";
13
13
  import {
14
14
  getTradejsProjectCwd,
15
15
  loadTradejsConfig
16
- } from "./chunk-JU77QVJ3.mjs";
16
+ } from "./chunk-WS5DYEVZ.mjs";
17
17
  import {
18
18
  __toESM
19
19
  } from "./chunk-6DZX6EAA.mjs";
@@ -21,6 +21,7 @@ import {
21
21
  // src/cli.ts
22
22
  var import_lodash = __toESM(require_lodash());
23
23
  import fs2 from "fs/promises";
24
+ import path2 from "path";
24
25
  import ProgressBar from "progress";
25
26
  import chalk from "chalk";
26
27
  import { runWithConcurrency } from "@tradejs/core/async";
@@ -37,6 +38,7 @@ import {
37
38
  redisKeys
38
39
  } from "@tradejs/infra/redis";
39
40
  import { logger as logger3 } from "@tradejs/infra/logger";
41
+ import { getDataEdgesForSymbols } from "@tradejs/infra/timescale";
40
42
 
41
43
  // src/screenshot.ts
42
44
  import fs from "fs/promises";
@@ -45,6 +47,27 @@ import puppeteer from "puppeteer";
45
47
  import { delay } from "@tradejs/core/async";
46
48
  import { logger } from "@tradejs/infra/logger";
47
49
  import { createScreenshotSessionToken } from "@tradejs/infra/redis";
50
+
51
+ // src/dashboardUrl.ts
52
+ var buildDashboardUrl = ({
53
+ baseUrl,
54
+ provider = "bybit",
55
+ universe = "crypto",
56
+ symbol,
57
+ interval,
58
+ searchParams = {}
59
+ }) => {
60
+ const url = new URL(
61
+ `/routes/dashboard/${provider}/${universe}/${symbol}/${interval}`,
62
+ baseUrl
63
+ );
64
+ for (const [key, value] of Object.entries(searchParams)) {
65
+ url.searchParams.set(key, value);
66
+ }
67
+ return url.toString();
68
+ };
69
+
70
+ // src/screenshot.ts
48
71
  var { APP_URL } = process.env;
49
72
  var SCREENSHOT_NAVIGATION_ATTEMPTS = 3;
50
73
  var SCREENSHOT_NAVIGATION_RETRY_DELAY_MS = 2e3;
@@ -153,7 +176,18 @@ var screenDashboard = async (signal, projectRoot, userName = "root") => {
153
176
  `Failed to create screenshot session token for ${userName}`
154
177
  );
155
178
  }
156
- const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1&screenshotToken=${encodeURIComponent(screenshotToken)}`;
179
+ const dashboardUrl = buildDashboardUrl({
180
+ baseUrl: screenshotBaseUrl,
181
+ universe: signal.universe ?? "crypto",
182
+ symbol,
183
+ interval,
184
+ searchParams: {
185
+ signalId,
186
+ autoZoom: "true",
187
+ screenshot: "1",
188
+ screenshotToken
189
+ }
190
+ });
157
191
  const maskedDashboardUrl = maskTokenInUrl(dashboardUrl);
158
192
  logger.info(
159
193
  "screenshot start: %s %sm url=%s path=%s",
@@ -461,6 +495,25 @@ var describeErrorValue2 = (value) => {
461
495
  return String(value);
462
496
  };
463
497
  var normalizeQuality = (value) => typeof value === "number" ? Math.max(1, Math.min(5, Math.round(value))) : null;
498
+ var formatOrderValue = (value) => {
499
+ const rounded = Number(value.toFixed(2));
500
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(2);
501
+ };
502
+ var getDisplayDecision = (signalDirection, analysis, fallback) => {
503
+ if (!analysis && !fallback) {
504
+ return void 0;
505
+ }
506
+ if (analysis?.direction != null && analysis.direction !== signalDirection) {
507
+ return "rejected";
508
+ }
509
+ if (analysis?.needRetest === true) {
510
+ return "pending";
511
+ }
512
+ if (analysis?.direction != null && analysis.direction === signalDirection) {
513
+ return "approved";
514
+ }
515
+ return fallback;
516
+ };
464
517
  var formatAnalysisLevel = (value) => {
465
518
  if (value == null || !Number.isFinite(value)) return null;
466
519
  if (Math.abs(value) >= 1e3) {
@@ -508,24 +561,17 @@ var formatOrderSkipReason = (reason) => {
508
561
  }
509
562
  return reason;
510
563
  };
511
- var getLastNumber = (value) => {
512
- if (Array.isArray(value)) {
513
- const last = value[value.length - 1];
514
- return typeof last === "number" ? last : void 0;
515
- }
516
- return typeof value === "number" ? value : void 0;
517
- };
518
- var getAiQualityLine = (analysis) => {
564
+ var getAiQualityLine = (analysis, label = "AI Quality") => {
519
565
  const quality = normalizeQuality(analysis?.quality);
520
566
  if (!quality) return null;
521
567
  const approvedCurrentDirection = analysis?.direction != null && analysis.direction !== null;
522
568
  if (quality >= 4 && approvedCurrentDirection) {
523
- return `\u{1F7E2} AI Quality: ${quality}/5`;
569
+ return `\u{1F7E2} ${label}: ${quality}/5`;
524
570
  }
525
571
  if (quality === 3 && approvedCurrentDirection) {
526
- return `\u{1F7E1} AI Quality: ${quality}/5`;
572
+ return `\u{1F7E1} ${label}: ${quality}/5`;
527
573
  }
528
- return `\u{1F534} AI Quality: ${quality}/5`;
574
+ return `\u{1F534} ${label}: ${quality}/5`;
529
575
  };
530
576
  var getTelegramErrorReason = (data) => {
531
577
  if (data && typeof data === "object") {
@@ -651,26 +697,59 @@ var sendTextToTG = async (message, options = {}) => {
651
697
  chatId
652
698
  });
653
699
  };
654
- var formatMessage = (signal, analysis) => {
700
+ var sendDocumentToTG = async (document, options = {}) => {
701
+ const { token, chatId } = await getTelegramSettings(options.userName);
702
+ const body = new FormData();
703
+ const fileContent = typeof document.content === "string" ? document.content : (() => {
704
+ const bytes = new Uint8Array(document.content.byteLength);
705
+ bytes.set(document.content);
706
+ return bytes;
707
+ })();
708
+ body.set("chat_id", String(chatId || ""));
709
+ body.set(
710
+ "document",
711
+ new File([fileContent], document.filename, {
712
+ type: "application/json"
713
+ })
714
+ );
715
+ if (document.caption?.trim()) {
716
+ body.set("caption", document.caption.trim());
717
+ body.set("parse_mode", "HTML");
718
+ }
719
+ const data = await requestTelegram({
720
+ method: "sendDocument",
721
+ token,
722
+ init: {
723
+ method: "POST",
724
+ body
725
+ }
726
+ });
727
+ logger2.info(
728
+ "tg sendDocument: %s",
729
+ data?.ok ? "sent" : getTelegramErrorReason(data)
730
+ );
731
+ return data;
732
+ };
733
+ var formatMessage = (signal, analysis, options = {}) => {
655
734
  const {
656
735
  symbol,
657
736
  direction,
658
737
  strategy,
659
738
  orderStatus,
660
739
  orderSkipReason,
740
+ orderFailureReason,
741
+ orderQty,
742
+ orderValue,
661
743
  isConfigFromBacktest,
662
744
  ml,
663
745
  prices: { currentPrice, takeProfitPrice, stopLossPrice, riskRatio },
664
- indicators,
665
746
  additionalIndicators
666
747
  } = signal;
748
+ const userName = options.userName || "root";
667
749
  try {
668
750
  const lines = [];
669
751
  const distance = additionalIndicators?.distance;
670
752
  const touches = additionalIndicators?.touches;
671
- const correlation = getLastNumber(indicators.correlation);
672
- const atrPct = getLastNumber(indicators.atrPct);
673
- const spread = getLastNumber(indicators.spread);
674
753
  const formatPrices = () => {
675
754
  const tpPercent = Math.abs(
676
755
  (takeProfitPrice - currentPrice) / currentPrice * 100
@@ -678,8 +757,10 @@ var formatMessage = (signal, analysis) => {
678
757
  const slPercent = Math.abs((stopLossPrice - currentPrice) / currentPrice * 100).toFixed(
679
758
  2
680
759
  ) + "%";
760
+ const resolvedOrderValue = typeof orderValue === "number" && Number.isFinite(orderValue) ? orderValue : typeof orderQty === "number" && Number.isFinite(orderQty) ? orderQty * currentPrice : null;
681
761
  const prices = [
682
762
  `Price: <b>${formatNumber(currentPrice)}</b>`,
763
+ resolvedOrderValue != null ? `Value: <b>${formatOrderValue(resolvedOrderValue)}$</b>` : null,
683
764
  `TP: <b>${formatNumber(takeProfitPrice)}</b> (${tpPercent})`,
684
765
  `SL: <b>${formatNumber(stopLossPrice)}</b> (${slPercent})`,
685
766
  `R:R = <b>${riskRatio.toFixed(2)}</b>`
@@ -705,6 +786,8 @@ var formatMessage = (signal, analysis) => {
705
786
  lines.push(
706
787
  `Skip reason: <b>${escapeHtml(formatOrderSkipReason(orderSkipReason))}</b>`
707
788
  );
789
+ } else if (orderStatus === "failed" && orderFailureReason) {
790
+ lines.push(`Reason: <code>${escapeHtml(orderFailureReason)}</code>`);
708
791
  }
709
792
  }
710
793
  if (isConfigFromBacktest) {
@@ -717,17 +800,25 @@ var formatMessage = (signal, analysis) => {
717
800
  `${ml.passed ? "\u{1F7E2} ML: PASS" : "\u{1F534} ML: FAIL"} (${ml.probability.toFixed(3)} / ${ml.threshold.toFixed(2)})`
718
801
  );
719
802
  }
720
- const aiQualityLine = getAiQualityLine(analysis);
721
- if (aiQualityLine) {
722
- lines.push(aiQualityLine);
803
+ const gateAnalysis = analysis?.gateAnalysis && typeof analysis.gateAnalysis === "object" && !Array.isArray(analysis.gateAnalysis) ? analysis.gateAnalysis : null;
804
+ if (gateAnalysis) {
805
+ const gateQualityLine = getAiQualityLine(gateAnalysis, "Gate Quality");
806
+ if (gateQualityLine) {
807
+ lines.push(gateQualityLine);
808
+ }
809
+ const llmQualityLine = getAiQualityLine(analysis, "LLM Quality");
810
+ if (llmQualityLine) {
811
+ lines.push(llmQualityLine);
812
+ }
813
+ } else {
814
+ const aiQualityLine = getAiQualityLine(analysis);
815
+ if (aiQualityLine) {
816
+ lines.push(aiQualityLine);
817
+ }
723
818
  }
724
819
  lines.push("");
725
- if (correlation) {
726
- lines.push(`BTC correlation: ${correlation}`);
727
- }
728
820
  const prices = formatPrices();
729
821
  if (prices) {
730
- lines.push("");
731
822
  lines.push(prices);
732
823
  }
733
824
  };
@@ -741,9 +832,17 @@ var formatMessage = (signal, analysis) => {
741
832
  var sendSignal = async (signal, imgInterval, analysis, options = {}) => {
742
833
  const { symbol, signalId, interval } = signal;
743
834
  const { token, chatId } = await getTelegramSettings(options.userName);
744
- const message = formatMessage(signal, analysis);
835
+ const message = formatMessage(signal, analysis, {
836
+ userName: options.userName
837
+ });
745
838
  const publicAppUrl = APP_URL2?.startsWith("https") ? APP_URL2 : null;
746
- const dashboardUrl = publicAppUrl ? `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}` : null;
839
+ const dashboardUrl = publicAppUrl ? buildDashboardUrl({
840
+ baseUrl: publicAppUrl,
841
+ universe: signal.universe ?? "crypto",
842
+ symbol,
843
+ interval,
844
+ searchParams: { signalId }
845
+ }) : null;
747
846
  const actionButtons = [
748
847
  dashboardUrl ? { text: "Dashboard", url: dashboardUrl } : null
749
848
  ].filter(Boolean);
@@ -836,9 +935,19 @@ var formatAnalysisMessage = (signal, analysis) => {
836
935
  if (quality) {
837
936
  lines.push(`Quality: <b>${quality}/5</b>`);
838
937
  }
839
- if (analysis.gateDecision && analysis.llmDecision) {
938
+ const gateDecision = getDisplayDecision(
939
+ signal.direction,
940
+ analysis.gateAnalysis,
941
+ analysis.gateDecision
942
+ );
943
+ const llmDecision = getDisplayDecision(
944
+ signal.direction,
945
+ analysis,
946
+ analysis.llmDecision
947
+ );
948
+ if (gateDecision && llmDecision) {
840
949
  lines.push(
841
- `Gate vs LLM: <b>${analysis.gateContradictsLlm ? "conflict" : "aligned"}</b> (gate ${analysis.gateDecision}, LLM ${analysis.llmDecision})`
950
+ `Gate vs LLM: <b>${gateDecision === llmDecision ? "aligned" : "conflict"}</b> (gate ${gateDecision}, LLM ${llmDecision})`
842
951
  );
843
952
  }
844
953
  const happeningText = takeUniqueAnalysisText(
@@ -883,19 +992,6 @@ var formatAnalysisMessage = (signal, analysis) => {
883
992
  if (btcText) {
884
993
  lines.push(`BTC context: ${escapeHtml(btcText)}`);
885
994
  }
886
- const levels = [];
887
- if (typeof analysis.takeProfitPrice === "number") {
888
- levels.push(`TP <b>${formatAnalysisLevel(analysis.takeProfitPrice)}</b>`);
889
- }
890
- if (typeof analysis.stopLossPrice === "number") {
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(" | ")}`);
898
- }
899
995
  return lines.join("\n");
900
996
  };
901
997
  var sendSignalAnalysis = async (signal, analysis, options = {}) => {
@@ -922,10 +1018,35 @@ var sendSignalAnalysis = async (signal, analysis, options = {}) => {
922
1018
 
923
1019
  // src/cli.ts
924
1020
  var getProjectRoot2 = () => getTradejsProjectCwd();
1021
+ var escapeHtml2 = (value) => value == null ? "" : String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1022
+ var formatOptionalNumber = (value) => {
1023
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1024
+ return "n/a";
1025
+ }
1026
+ const normalized = Number(value.toFixed(8));
1027
+ return Number.isInteger(normalized) ? String(normalized) : String(normalized);
1028
+ };
1029
+ var formatOptionalTimestamp = (value) => {
1030
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1031
+ return "n/a";
1032
+ }
1033
+ return new Date(value).toISOString();
1034
+ };
1035
+ var isEnoentError = (error) => error?.code === "ENOENT";
925
1036
  var cleanFiles = async (dir) => {
926
1037
  let completed = 0;
927
1038
  const projectRoot = getProjectRoot2();
928
- const files = await getFiles(dir, projectRoot);
1039
+ let files = [];
1040
+ try {
1041
+ files = await getFiles(dir, projectRoot);
1042
+ } catch (error) {
1043
+ if (isEnoentError(error)) {
1044
+ logger3.info(chalk.yellow("clean:", dir));
1045
+ logger3.info("");
1046
+ return;
1047
+ }
1048
+ throw error;
1049
+ }
929
1050
  const bar = new ProgressBar(":current/:total [:bar][:percent] :eta(s)", {
930
1051
  total: files.length,
931
1052
  width: 30
@@ -933,12 +1054,18 @@ var cleanFiles = async (dir) => {
933
1054
  logger3.info(chalk.yellow("clean:", dir));
934
1055
  for await (const file of files) {
935
1056
  completed++;
936
- const fullPath = `${dir}/${file}`;
937
- const stat = await fs2.lstat(fullPath);
938
- if (stat.isDirectory()) {
939
- await fs2.rm(fullPath, { recursive: true, force: true });
940
- } else {
941
- await fs2.unlink(fullPath);
1057
+ const fullPath = path2.join(projectRoot, dir, file);
1058
+ try {
1059
+ const stat = await fs2.lstat(fullPath);
1060
+ if (stat.isDirectory()) {
1061
+ await fs2.rm(fullPath, { recursive: true, force: true });
1062
+ } else {
1063
+ await fs2.unlink(fullPath);
1064
+ }
1065
+ } catch (error) {
1066
+ if (!isEnoentError(error)) {
1067
+ throw error;
1068
+ }
942
1069
  }
943
1070
  if (completed % 100 === 0 || completed === files.length) {
944
1071
  bar.tick(completed === files.length ? completed % 100 : 100);
@@ -983,28 +1110,58 @@ var update = async (connector, interval, tickers, preloadDays = PRELOAD_DAYS, op
983
1110
  );
984
1111
  const preloadEnd = Math.trunc(options.preloadEnd ?? getTimestamp());
985
1112
  const connectorLabel = String(options.connectorLabel || "").trim();
986
- const preloadLabel = options.preloadStart != null || options.preloadEnd != null ? `preloadStart=${preloadStart}, preloadEnd=${preloadEnd}` : `preloadDays=${preloadDays}`;
1113
+ const preloadLabel = options.preloadStart != null || options.preloadEnd != null ? `preloadStart=${formatOptionalTimestamp(preloadStart)}, preloadEnd=${formatOptionalTimestamp(preloadEnd)}` : `preloadDays=${preloadDays}`;
987
1114
  if (preloadStart >= preloadEnd) {
988
1115
  throw new Error(
989
1116
  `Invalid update preload window: start (${preloadStart}) must be less than end (${preloadEnd})`
990
1117
  );
991
1118
  }
1119
+ let queue = [...new Set(tickers.slice())];
1120
+ if (!queue.includes("BTCUSDT")) {
1121
+ queue.unshift("BTCUSDT");
1122
+ }
1123
+ const intervalMinutes = Number(interval);
1124
+ if (options.skipCovered && connectorLabel && Number.isFinite(intervalMinutes) && intervalMinutes > 0) {
1125
+ const edges = await getDataEdgesForSymbols(
1126
+ connectorLabel,
1127
+ queue,
1128
+ Math.floor(intervalMinutes)
1129
+ );
1130
+ const initialCount = queue.length;
1131
+ queue = queue.filter((symbol) => {
1132
+ const edge = edges.get(String(symbol).toUpperCase());
1133
+ return !edge || edge.min === void 0 || edge.max === void 0 || edge.min > preloadStart || edge.max < preloadEnd;
1134
+ });
1135
+ const skippedCount = initialCount - queue.length;
1136
+ if (skippedCount > 0) {
1137
+ logger3.info(
1138
+ chalk.gray(
1139
+ `update ${connectorLabel}: skip ${skippedCount}/${initialCount} cached symbols for interval=${interval}`
1140
+ )
1141
+ );
1142
+ }
1143
+ }
1144
+ if (queue.length === 0) {
1145
+ logger3.info(
1146
+ chalk.yellow(
1147
+ `update: 0 (connector=${connectorLabel || "unknown"}, interval=${interval}, klineConcurrency=${KLINE_CONCURRENCY_LIMIT}, ${preloadLabel})`
1148
+ )
1149
+ );
1150
+ logger3.info("");
1151
+ return;
1152
+ }
992
1153
  const bar = new ProgressBar(
993
1154
  ":current/:total [:bar][:percent] :eta(s) :symbol",
994
1155
  {
995
- total: tickers.length,
1156
+ total: queue.length,
996
1157
  width: 30
997
1158
  }
998
1159
  );
999
1160
  logger3.info(
1000
1161
  chalk.yellow(
1001
- `update: ${tickers.length} (connector=${connectorLabel || "unknown"}, klineConcurrency=${KLINE_CONCURRENCY_LIMIT}, ${preloadLabel})`
1162
+ `update: ${queue.length} (connector=${connectorLabel || "unknown"}, interval=${interval}, klineConcurrency=${KLINE_CONCURRENCY_LIMIT}, ${preloadLabel})`
1002
1163
  )
1003
1164
  );
1004
- const queue = tickers.slice();
1005
- if (!queue.includes("BTCUSDT")) {
1006
- queue.unshift("BTCUSDT");
1007
- }
1008
1165
  await runWithConcurrency(queue, KLINE_CONCURRENCY_LIMIT, async (symbol) => {
1009
1166
  try {
1010
1167
  await connector.kline({
@@ -1033,6 +1190,8 @@ var getCLILevelColor = (level) => {
1033
1190
  return chalk.green;
1034
1191
  case "warning":
1035
1192
  return chalk.yellow;
1193
+ case "neutral":
1194
+ return chalk.gray;
1036
1195
  case "error":
1037
1196
  return chalk.red;
1038
1197
  }
@@ -1044,18 +1203,18 @@ var drawStatInCLI = (stat, keys) => {
1044
1203
  return color(formatted);
1045
1204
  });
1046
1205
  };
1047
- var scanner = async (connector, limit) => {
1048
- const data = await connector.getTickers();
1206
+ var scanner = async (connector, limit, query) => {
1207
+ const data = await connector.getTickers(query);
1049
1208
  const tickers = getTopTickers(data, limit);
1050
1209
  return tickers.map(({ value }) => value);
1051
1210
  };
1052
- var getTickers = async (connector, include = "", exclude = "", limit, chunk) => {
1211
+ var getTickers = async (connector, include = "", exclude = "", limit, chunk, query) => {
1053
1212
  let tickers;
1054
1213
  const excludeTickers = parseSymbolsFromCLI(exclude);
1055
1214
  if (include) {
1056
1215
  tickers = parseSymbolsFromCLI(include);
1057
1216
  } else {
1058
- tickers = await scanner(connector, limit);
1217
+ tickers = await scanner(connector, limit, query);
1059
1218
  }
1060
1219
  if (chunk) {
1061
1220
  const [currentChunk, chunksCount] = chunk.split("/").map((c) => parseInt(c));
@@ -1115,7 +1274,79 @@ var sendToAI = async (signals, userName = "root") => {
1115
1274
  });
1116
1275
  logger3.info("");
1117
1276
  };
1277
+ var formatRuntimeCloseNotification = (event, _userName = event.userName ?? "root") => {
1278
+ const ownership = event.openedByStrategy === event.strategy ? "matched" : `mismatch: opened by ${event.openedByStrategy}`;
1279
+ return [
1280
+ "<b>Strategy self-close</b>",
1281
+ `Symbol: <b>${escapeHtml2(event.symbol)}</b>`,
1282
+ `Strategy: <b>${escapeHtml2(event.strategy)}</b>`,
1283
+ `Direction: <b>${escapeHtml2(event.direction)}</b>`,
1284
+ `Reason: <code>${escapeHtml2(event.code)}</code>`,
1285
+ "",
1286
+ `Opened by journal: <b>${escapeHtml2(event.openedByStrategy)}</b>`,
1287
+ `Ownership: <b>${escapeHtml2(ownership)}</b>`,
1288
+ "",
1289
+ `Entry: <b>${escapeHtml2(formatOptionalNumber(event.entryPrice))}</b> at <code>${escapeHtml2(formatOptionalTimestamp(event.entryTimestamp))}</code>`,
1290
+ `Exit: <b>${escapeHtml2(formatOptionalNumber(event.exitPrice))}</b> at <code>${escapeHtml2(formatOptionalTimestamp(event.exitTimestamp))}</code>`,
1291
+ `Qty: <b>${escapeHtml2(formatOptionalNumber(event.qty))}</b>`,
1292
+ `Closed PnL: <b>${escapeHtml2(formatOptionalNumber(event.closedPnl))}</b>`
1293
+ ].join("\n");
1294
+ };
1295
+ var sendRuntimeCloseNotificationsToTG = async (events, userName = "root") => {
1296
+ logger3.info(chalk.yellow("close messages:", events.length));
1297
+ if (!events.length) {
1298
+ logger3.info("");
1299
+ return;
1300
+ }
1301
+ const bar = new ProgressBar(
1302
+ ":current/:total [:bar][:percent] :eta(s) :symbol",
1303
+ {
1304
+ total: events.length,
1305
+ width: 30
1306
+ }
1307
+ );
1308
+ await runWithConcurrency(events, 1, async (event) => {
1309
+ try {
1310
+ await sendTextToTG(formatRuntimeCloseNotification(event, userName), {
1311
+ userName
1312
+ });
1313
+ } catch (err) {
1314
+ logger3.error(
1315
+ "Failed close notification: %s %s",
1316
+ event.symbol,
1317
+ err?.message || String(err)
1318
+ );
1319
+ } finally {
1320
+ bar.tick(1, { symbol: chalk.gray(event.symbol) });
1321
+ }
1322
+ });
1323
+ logger3.info("");
1324
+ };
1118
1325
  var sendToTG = async (signals, imgInterval, userName = "root") => {
1326
+ const strategyConfigCache = /* @__PURE__ */ new Map();
1327
+ const resolveStrategyConfig = async (strategyName) => {
1328
+ if (strategyConfigCache.has(strategyName)) {
1329
+ return strategyConfigCache.get(strategyName);
1330
+ }
1331
+ const config = await getData(
1332
+ `users:${userName}:strategies:${strategyName}:config`,
1333
+ null
1334
+ );
1335
+ strategyConfigCache.set(strategyName, config);
1336
+ return config;
1337
+ };
1338
+ const resolveDecision = (analysis, direction, minQuality = 4) => {
1339
+ if (analysis?.needRetest === true) {
1340
+ return "rejected";
1341
+ }
1342
+ const quality = Number(analysis?.quality);
1343
+ if (!Number.isFinite(quality)) {
1344
+ return "rejected";
1345
+ }
1346
+ const normalized = Math.round(quality);
1347
+ const resolvedQuality = analysis?.direction === direction ? normalized : 0;
1348
+ return resolvedQuality >= minQuality ? "approved" : "rejected";
1349
+ };
1119
1350
  const deliverableSignals = signals.filter(
1120
1351
  (signal) => signal.orderStatus !== "skipped" && signal.orderStatus !== "canceled"
1121
1352
  );
@@ -1133,17 +1364,56 @@ var sendToTG = async (signals, imgInterval, userName = "root") => {
1133
1364
  );
1134
1365
  await runWithConcurrency(deliverableSignals, 1, async (signal) => {
1135
1366
  try {
1136
- const analysis = await getData(
1367
+ let analysis = await getData(
1137
1368
  redisKeys.analysis(signal.symbol, signal.signalId),
1138
1369
  null
1139
1370
  );
1371
+ let shouldSendSignalAnalysis = analysis && typeof analysis === "object" && Object.keys(analysis).length > 0;
1372
+ const strategyConfig = await resolveStrategyConfig(signal.strategy);
1373
+ const aiMode = strategyConfig?.AI_MODE;
1374
+ const gateAnalysis = signal.aiAnalysis;
1375
+ if (aiMode === "gate" && gateAnalysis) {
1376
+ const minQuality = Number(strategyConfig?.MIN_AI_QUALITY) || 4;
1377
+ const gateDecision = resolveDecision(
1378
+ gateAnalysis,
1379
+ signal.direction,
1380
+ minQuality
1381
+ );
1382
+ try {
1383
+ const llmAnalysis = await askAI(signal, { userName });
1384
+ const llmDecision = resolveDecision(
1385
+ llmAnalysis,
1386
+ signal.direction,
1387
+ minQuality
1388
+ );
1389
+ analysis = {
1390
+ ...llmAnalysis ?? {},
1391
+ gateAnalysis,
1392
+ gateDecision,
1393
+ llmDecision,
1394
+ gateContradictsLlm: gateDecision !== llmDecision
1395
+ };
1396
+ shouldSendSignalAnalysis = true;
1397
+ } catch (error) {
1398
+ logger3.error(
1399
+ "LLM commentary failed: %s (%s)",
1400
+ signal.symbol,
1401
+ error?.message || String(error)
1402
+ );
1403
+ analysis = {
1404
+ gateAnalysis,
1405
+ gateDecision
1406
+ };
1407
+ shouldSendSignalAnalysis = false;
1408
+ }
1409
+ }
1140
1410
  await sendSignal(signal, imgInterval, analysis, { userName });
1141
- if (analysis && typeof analysis === "object" && Object.keys(analysis).length > 0) {
1411
+ if (shouldSendSignalAnalysis && analysis && typeof analysis === "object" && Object.keys(analysis).length > 0) {
1142
1412
  await sendSignalAnalysis(signal, analysis, { userName });
1143
1413
  }
1144
1414
  } catch (err) {
1145
1415
  logger3.error(
1146
- "Failed sent: %s %s",
1416
+ "Signal notification failed: %s (%s)",
1147
1417
  signal.symbol,
1148
1418
  err?.message || String(err)
1149
1419
  );
@@ -1157,9 +1427,12 @@ export {
1157
1427
  cleanFiles,
1158
1428
  cleanRedis,
1159
1429
  drawStatInCLI,
1430
+ formatRuntimeCloseNotification,
1160
1431
  getTickers,
1161
1432
  loadTradejsConfig,
1162
1433
  makeScreenshots,
1434
+ sendDocumentToTG,
1435
+ sendRuntimeCloseNotificationsToTG,
1163
1436
  sendTextToTG,
1164
1437
  sendToAI,
1165
1438
  sendToTG,