@tradejs/node 1.0.8 → 1.0.10
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 +4 -1
- package/dist/ai.d.ts +4 -1
- package/dist/ai.js +601 -93
- package/dist/ai.mjs +5 -3
- package/dist/backtest.d.mts +26 -2
- package/dist/backtest.d.ts +26 -2
- package/dist/backtest.js +3368 -485
- package/dist/backtest.mjs +1839 -269
- package/dist/chunk-37VNDZVX.mjs +1040 -0
- package/dist/chunk-IUZML4RK.mjs +1136 -0
- package/dist/{chunk-WGOYR6AB.mjs → chunk-QVSMINLG.mjs} +1 -1
- package/dist/{chunk-JMDYEKIO.mjs → chunk-V3YMKE4I.mjs} +1 -1
- package/dist/{chunk-JU77QVJ3.mjs → chunk-WS5DYEVZ.mjs} +59 -5
- package/dist/cli.d.mts +13 -3
- package/dist/cli.d.ts +13 -3
- package/dist/cli.js +1016 -234
- package/dist/cli.mjs +338 -65
- package/dist/connectors.js +59 -5
- package/dist/connectors.mjs +2 -2
- package/dist/registry.js +59 -5
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +29 -8
- package/dist/strategies.d.ts +29 -8
- package/dist/strategies.js +3417 -1285
- package/dist/strategies.mjs +892 -112
- package/package.json +6 -6
- package/dist/chunk-2JKX3DM7.mjs +0 -619
- package/dist/chunk-JRRG3YQG.mjs +0 -154
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-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-IUZML4RK.mjs";
|
|
12
|
+
import "./chunk-QVSMINLG.mjs";
|
|
13
13
|
import {
|
|
14
14
|
getTradejsProjectCwd,
|
|
15
15
|
loadTradejsConfig
|
|
16
|
-
} from "./chunk-
|
|
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 =
|
|
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
|
|
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}
|
|
569
|
+
return `\u{1F7E2} ${label}: ${quality}/5`;
|
|
524
570
|
}
|
|
525
571
|
if (quality === 3 && approvedCurrentDirection) {
|
|
526
|
-
return `\u{1F7E1}
|
|
572
|
+
return `\u{1F7E1} ${label}: ${quality}/5`;
|
|
527
573
|
}
|
|
528
|
-
return `\u{1F534}
|
|
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
|
|
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
|
|
721
|
-
if (
|
|
722
|
-
|
|
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 ?
|
|
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
|
-
|
|
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>${
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
|
|
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 =
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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:
|
|
1156
|
+
total: queue.length,
|
|
996
1157
|
width: 30
|
|
997
1158
|
}
|
|
998
1159
|
);
|
|
999
1160
|
logger3.info(
|
|
1000
1161
|
chalk.yellow(
|
|
1001
|
-
`update: ${
|
|
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
|
-
|
|
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
|
-
"
|
|
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,
|