@tradejs/cli 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/README.md +4 -1
- package/dist/cli.js +13791 -7652
- package/dist/lib/aiTrainMetrics.js +173 -0
- package/dist/lib/candlesProviderMigration.js +266 -0
- package/dist/lib/cliArgs.js +40 -0
- package/dist/lib/derivativesContextBackfill.js +487 -0
- package/dist/lib/researchAgent.js +195 -0
- package/dist/lib/runtimeParity.js +234 -0
- package/dist/lib/telegramReports.js +91 -0
- package/dist/lib/timeWindow.js +101 -0
- package/dist/scripts/agentRun.js +895 -0
- package/dist/scripts/aiExport.js +192 -0
- package/dist/scripts/aiExportSelect.js +135 -0
- package/dist/scripts/aiTrain.js +2321 -0
- package/dist/scripts/backtest.js +998 -111
- package/dist/scripts/candlesMigrateProvider.js +5302 -0
- package/dist/scripts/continuity.js +1 -1
- package/dist/scripts/derivativesIngest.js +5 -0
- package/dist/scripts/derivativesIngestCoinalyzeAll.js +21 -12
- package/dist/scripts/migration.js +1 -1
- package/dist/scripts/mlExport.js +112 -13
- package/dist/scripts/mlExportSelect.js +43 -9
- package/dist/scripts/mlTrainLatestSelect.js +4 -3
- package/dist/scripts/researchAuto.js +845 -0
- package/dist/scripts/resolveExportStrategy.js +133 -0
- package/dist/scripts/results.js +46 -22
- package/dist/scripts/runtimeParity.js +1651 -0
- package/dist/scripts/selectStrategy.js +4 -3
- package/dist/scripts/signals.js +799 -62
- package/dist/scripts/signalsSummary.js +655 -0
- package/dist/scripts/spawnSiblingScript.js +71 -0
- package/dist/scripts/user-add.js +0 -9
- package/dist/workers/testerWorker.js +13 -2
- package/package.json +18 -12
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/scripts/signalsSummary.ts
|
|
31
|
+
var signalsSummary_exports = {};
|
|
32
|
+
__export(signalsSummary_exports, {
|
|
33
|
+
signalsSummary: () => signalsSummary
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(signalsSummary_exports);
|
|
36
|
+
var import_config = require("dotenv/config");
|
|
37
|
+
var import_args = __toESM(require("args"));
|
|
38
|
+
var import_constants = require("@tradejs/core/constants");
|
|
39
|
+
var import_logger = require("@tradejs/infra/logger");
|
|
40
|
+
var import_redis = require("@tradejs/infra/redis");
|
|
41
|
+
var import_connectors = require("@tradejs/node/connectors");
|
|
42
|
+
|
|
43
|
+
// src/lib/telegramReports.ts
|
|
44
|
+
var import_cli = require("@tradejs/node/cli");
|
|
45
|
+
var TELEGRAM_REPORT_CHUNK_LIMIT = 3900;
|
|
46
|
+
var buildPartHeader = (index, total) => `\u{1F4E9} <b>Part ${index}/${total}</b>`;
|
|
47
|
+
var splitLongLine = (line, limit) => {
|
|
48
|
+
const chunks = [];
|
|
49
|
+
for (let index = 0; index < line.length; index += limit) {
|
|
50
|
+
chunks.push(line.slice(index, index + limit));
|
|
51
|
+
}
|
|
52
|
+
return chunks;
|
|
53
|
+
};
|
|
54
|
+
var splitTelegramReport = (message, limit = TELEGRAM_REPORT_CHUNK_LIMIT) => {
|
|
55
|
+
const normalizedLimit = Math.max(100, Math.floor(limit));
|
|
56
|
+
if (message.length <= normalizedLimit) {
|
|
57
|
+
return [message];
|
|
58
|
+
}
|
|
59
|
+
const chunks = [];
|
|
60
|
+
let current = "";
|
|
61
|
+
const flush = () => {
|
|
62
|
+
if (!current) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
chunks.push(current);
|
|
66
|
+
current = "";
|
|
67
|
+
};
|
|
68
|
+
for (const line of message.split("\n")) {
|
|
69
|
+
const next = current ? `${current}
|
|
70
|
+
${line}` : line;
|
|
71
|
+
if (next.length <= normalizedLimit) {
|
|
72
|
+
current = next;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
flush();
|
|
76
|
+
if (line.length <= normalizedLimit) {
|
|
77
|
+
current = line;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
chunks.push(...splitLongLine(line, normalizedLimit));
|
|
81
|
+
}
|
|
82
|
+
flush();
|
|
83
|
+
return chunks;
|
|
84
|
+
};
|
|
85
|
+
var sendTelegramReport = async (message, options = {}) => {
|
|
86
|
+
const chunks = splitTelegramReport(message);
|
|
87
|
+
if (chunks.length === 1) {
|
|
88
|
+
await (0, import_cli.sendTextToTG)(chunks[0], options);
|
|
89
|
+
return chunks.length;
|
|
90
|
+
}
|
|
91
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
92
|
+
const header = buildPartHeader(index + 1, chunks.length);
|
|
93
|
+
await (0, import_cli.sendTextToTG)(`${header}
|
|
94
|
+
|
|
95
|
+
${chunk}`, {
|
|
96
|
+
userName: options.userName,
|
|
97
|
+
markup: index === chunks.length - 1 ? options.markup : void 0
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return chunks.length;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/scripts/signalsSummary.ts
|
|
104
|
+
import_args.default.option(["u", "user"], "Use user config", "root");
|
|
105
|
+
import_args.default.option(
|
|
106
|
+
"connector",
|
|
107
|
+
"Connector provider or name for summary (e.g. bybit, binance, coinbase, custom)",
|
|
108
|
+
"bybit"
|
|
109
|
+
);
|
|
110
|
+
import_args.default.option(["H", "hours"], "Summary window in hours", 24);
|
|
111
|
+
import_args.default.option(["P", "printOnly"], "Print summary instead of Telegram", false);
|
|
112
|
+
var flags = import_args.default.parse(process.argv);
|
|
113
|
+
var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
114
|
+
var SUMMARY_TIMEZONE = "Europe/Moscow";
|
|
115
|
+
var SUMMARY_TIMEZONE_LABEL = "MSK";
|
|
116
|
+
var escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
117
|
+
var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
|
|
118
|
+
timeZone: SUMMARY_TIMEZONE,
|
|
119
|
+
year: "numeric",
|
|
120
|
+
month: "2-digit",
|
|
121
|
+
day: "2-digit",
|
|
122
|
+
hour: "2-digit",
|
|
123
|
+
minute: "2-digit",
|
|
124
|
+
hour12: false
|
|
125
|
+
}).format(new Date(timestamp));
|
|
126
|
+
var formatSigned = (value) => `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
|
|
127
|
+
var resolveSummaryTitle = (hours) => {
|
|
128
|
+
if (hours === 24) {
|
|
129
|
+
return "TradeJS daily summary";
|
|
130
|
+
}
|
|
131
|
+
if (hours === 168) {
|
|
132
|
+
return "TradeJS weekly summary";
|
|
133
|
+
}
|
|
134
|
+
return `TradeJS ${hours}h summary`;
|
|
135
|
+
};
|
|
136
|
+
var normalizeStatus = (value) => {
|
|
137
|
+
if (value === "completed" || value === "failed" || value === "skipped" || value === "canceled") {
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
return "unknown";
|
|
141
|
+
};
|
|
142
|
+
var normalizeSkipStatsReason = (reason, fallbackSource) => {
|
|
143
|
+
let source = fallbackSource;
|
|
144
|
+
let normalizedReason = reason;
|
|
145
|
+
if (reason.startsWith("AI_QUALITY_BELOW_MIN")) {
|
|
146
|
+
source = "AI";
|
|
147
|
+
normalizedReason = "MIN_AI_QUALITY";
|
|
148
|
+
} else if (reason === "AI_QUALITY_UNAVAILABLE") {
|
|
149
|
+
source = "AI";
|
|
150
|
+
normalizedReason = "QUALITY_UNAVAILABLE";
|
|
151
|
+
} else if (reason === "ML_RESULT_UNAVAILABLE") {
|
|
152
|
+
source = "ML";
|
|
153
|
+
normalizedReason = "RESULT_UNAVAILABLE";
|
|
154
|
+
} else if (reason.startsWith("ML_THRESHOLD_NOT_MET")) {
|
|
155
|
+
source = "ML";
|
|
156
|
+
normalizedReason = "ML_THRESHOLD";
|
|
157
|
+
} else if (reason.startsWith("HOOK_BEFORE_ENTRY_GATE:")) {
|
|
158
|
+
source = "hook";
|
|
159
|
+
normalizedReason = `BEFORE_ENTRY_GATE:${reason.slice(
|
|
160
|
+
"HOOK_BEFORE_ENTRY_GATE:".length
|
|
161
|
+
)}`;
|
|
162
|
+
} else if (reason === "HOOK_BEFORE_ENTRY_GATE") {
|
|
163
|
+
source = "hook";
|
|
164
|
+
normalizedReason = "BEFORE_ENTRY_GATE";
|
|
165
|
+
} else if (reason === "MAKE_ORDERS_DISABLED" || reason === "ENTRY_POLICY_BLOCKED") {
|
|
166
|
+
source = "policy";
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
source: `skip from ${source}`,
|
|
170
|
+
reason: normalizedReason
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
var isSignalRecord = (value) => {
|
|
174
|
+
if (!value || typeof value !== "object") {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const record = value;
|
|
178
|
+
return typeof record.signalId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number";
|
|
179
|
+
};
|
|
180
|
+
var isRuntimeSignalEvaluationRecord = (value) => {
|
|
181
|
+
if (!value || typeof value !== "object") {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const record = value;
|
|
185
|
+
return typeof record.evaluationId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number" && (record.status === "signal" || record.status === "skip" || record.status === "error");
|
|
186
|
+
};
|
|
187
|
+
var isRuntimeTradeRecord = (value) => {
|
|
188
|
+
if (!value || typeof value !== "object") {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const record = value;
|
|
192
|
+
return typeof record.orderId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.entryTimestamp === "number" && typeof record.entryPrice === "number" && typeof record.qty === "number";
|
|
193
|
+
};
|
|
194
|
+
var resolveSummaryConnectorName = async (value) => {
|
|
195
|
+
const connectorName = await (0, import_connectors.resolveConnectorName)(value, projectRoot);
|
|
196
|
+
if (connectorName) {
|
|
197
|
+
return connectorName;
|
|
198
|
+
}
|
|
199
|
+
import_logger.logger.warn(
|
|
200
|
+
'Unknown connector "%s". Fallback to %s.',
|
|
201
|
+
String(value || "").trim() || String(value),
|
|
202
|
+
import_connectors.DEFAULT_CONNECTOR_NAME
|
|
203
|
+
);
|
|
204
|
+
return import_connectors.DEFAULT_CONNECTOR_NAME;
|
|
205
|
+
};
|
|
206
|
+
var loadRuntimeSignals = async (userName) => {
|
|
207
|
+
const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeSignals(userName));
|
|
208
|
+
const signals = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
|
|
209
|
+
return signals.filter(isSignalRecord).sort((left, right) => left.timestamp - right.timestamp);
|
|
210
|
+
};
|
|
211
|
+
var loadRuntimeSignalEvaluations = async (userName) => {
|
|
212
|
+
const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeSignalEvaluations(userName));
|
|
213
|
+
const evaluations = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
|
|
214
|
+
return evaluations.filter(isRuntimeSignalEvaluationRecord).sort((left, right) => left.timestamp - right.timestamp);
|
|
215
|
+
};
|
|
216
|
+
var resolveStrategyNameByConfigKey = (userName, key) => {
|
|
217
|
+
const parts = key.split(":");
|
|
218
|
+
if (parts.length !== 5) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const [users, keyUserName, strategiesKey, strategyName, configKey] = parts;
|
|
222
|
+
if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || configKey !== "config" || !strategyName) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return strategyName;
|
|
226
|
+
};
|
|
227
|
+
var loadRuntimeStrategyNames = async (userName) => {
|
|
228
|
+
const keys = await (0, import_redis.getKeys)(`${import_redis.redisKeys.strategies(userName)}:`);
|
|
229
|
+
return keys.filter((key) => key.endsWith(":config")).map((key) => resolveStrategyNameByConfigKey(userName, key)).filter((value) => Boolean(value)).sort((left, right) => left.localeCompare(right));
|
|
230
|
+
};
|
|
231
|
+
var loadRuntimeTrades = async (userName) => {
|
|
232
|
+
const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeTrades(userName));
|
|
233
|
+
const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
|
|
234
|
+
return trades.filter(isRuntimeTradeRecord).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
235
|
+
};
|
|
236
|
+
var loadClosedPnlRows = async ({
|
|
237
|
+
connector,
|
|
238
|
+
startTime,
|
|
239
|
+
endTime
|
|
240
|
+
}) => {
|
|
241
|
+
if (typeof connector.getClosedPnl !== "function") {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const rows = await connector.getClosedPnl({
|
|
246
|
+
startTime,
|
|
247
|
+
endTime,
|
|
248
|
+
limit: 100
|
|
249
|
+
});
|
|
250
|
+
return rows.sort((left, right) => left.closedAt - right.closedAt);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
import_logger.logger.warn(
|
|
253
|
+
"signals summary: getClosedPnl failed: %s",
|
|
254
|
+
error?.message || String(error)
|
|
255
|
+
);
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var consumeClosedPnlMatch = (buckets, trade) => {
|
|
260
|
+
const rows = buckets.get(trade.symbol);
|
|
261
|
+
if (!rows?.length) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const minimumClosedAt = trade.entryTimestamp - 5 * 6e4;
|
|
265
|
+
const matchIndex = rows.findIndex(
|
|
266
|
+
(row2) => Number.isFinite(row2.closedAt) && row2.closedAt >= minimumClosedAt
|
|
267
|
+
);
|
|
268
|
+
if (matchIndex < 0) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const [row] = rows.splice(matchIndex, 1);
|
|
272
|
+
return row ?? null;
|
|
273
|
+
};
|
|
274
|
+
var syncRuntimeTrades = async ({
|
|
275
|
+
userName,
|
|
276
|
+
connector,
|
|
277
|
+
trades,
|
|
278
|
+
startTime,
|
|
279
|
+
endTime
|
|
280
|
+
}) => {
|
|
281
|
+
const openPositions = typeof connector.getOpenPositionPnl === "function" ? await connector.getOpenPositionPnl() : [];
|
|
282
|
+
const openPositionsBySymbol = new Map(
|
|
283
|
+
openPositions.map((position) => [position.symbol, position])
|
|
284
|
+
);
|
|
285
|
+
const activeOrderIdBySymbol = /* @__PURE__ */ new Map();
|
|
286
|
+
const symbols = [...new Set(trades.map((trade) => trade.symbol))];
|
|
287
|
+
await Promise.all(
|
|
288
|
+
symbols.map(async (symbol) => {
|
|
289
|
+
const activeRef = await (0, import_redis.getData)(
|
|
290
|
+
import_redis.redisKeys.runtimeActiveTrade(userName, symbol),
|
|
291
|
+
null
|
|
292
|
+
);
|
|
293
|
+
activeOrderIdBySymbol.set(
|
|
294
|
+
symbol,
|
|
295
|
+
typeof activeRef?.orderId === "string" ? activeRef.orderId : null
|
|
296
|
+
);
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
const closedPnlRows = await loadClosedPnlRows({
|
|
300
|
+
connector,
|
|
301
|
+
startTime,
|
|
302
|
+
endTime
|
|
303
|
+
});
|
|
304
|
+
const closedPnlBuckets = /* @__PURE__ */ new Map();
|
|
305
|
+
for (const row of closedPnlRows) {
|
|
306
|
+
const bucket = closedPnlBuckets.get(row.symbol) ?? [];
|
|
307
|
+
bucket.push(row);
|
|
308
|
+
closedPnlBuckets.set(row.symbol, bucket);
|
|
309
|
+
}
|
|
310
|
+
const syncedTrades = [];
|
|
311
|
+
for (const trade of trades) {
|
|
312
|
+
if (trade.status !== "active") {
|
|
313
|
+
syncedTrades.push(trade);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const openPosition = openPositionsBySymbol.get(trade.symbol);
|
|
317
|
+
const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
|
|
318
|
+
const isCurrentActiveTrade = activeOrderId === trade.orderId;
|
|
319
|
+
if (isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
|
|
320
|
+
const nextTrade2 = {
|
|
321
|
+
...trade,
|
|
322
|
+
status: "active",
|
|
323
|
+
currentPrice: openPosition.currentPrice,
|
|
324
|
+
currentPnl: openPosition.unrealizedPnl,
|
|
325
|
+
lastSyncedAt: endTime
|
|
326
|
+
};
|
|
327
|
+
await (0, import_redis.setData)(
|
|
328
|
+
import_redis.redisKeys.runtimeTrade(userName, trade.orderId),
|
|
329
|
+
nextTrade2,
|
|
330
|
+
{
|
|
331
|
+
expire: 0
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
syncedTrades.push(nextTrade2);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
|
|
338
|
+
const nextTrade = {
|
|
339
|
+
...trade,
|
|
340
|
+
status: "closed",
|
|
341
|
+
currentPrice: matchedClosedPnl?.exitPrice ?? trade.currentPrice ?? null,
|
|
342
|
+
currentPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
|
|
343
|
+
closedPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
|
|
344
|
+
exitPrice: matchedClosedPnl?.exitPrice ?? trade.exitPrice ?? null,
|
|
345
|
+
exitTimestamp: matchedClosedPnl?.closedAt ?? trade.exitTimestamp ?? endTime,
|
|
346
|
+
lastSyncedAt: endTime
|
|
347
|
+
};
|
|
348
|
+
await Promise.all([
|
|
349
|
+
(0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
|
|
350
|
+
expire: import_constants.TTL_1M
|
|
351
|
+
}),
|
|
352
|
+
...isCurrentActiveTrade ? [(0, import_redis.delKey)(import_redis.redisKeys.runtimeActiveTrade(userName, trade.symbol))] : []
|
|
353
|
+
]);
|
|
354
|
+
syncedTrades.push(nextTrade);
|
|
355
|
+
}
|
|
356
|
+
return syncedTrades;
|
|
357
|
+
};
|
|
358
|
+
var buildSummaryMessage = ({
|
|
359
|
+
hours,
|
|
360
|
+
startTime,
|
|
361
|
+
endTime,
|
|
362
|
+
configuredStrategyNames,
|
|
363
|
+
signals,
|
|
364
|
+
signalEvaluations,
|
|
365
|
+
trades
|
|
366
|
+
}) => {
|
|
367
|
+
const lines = [];
|
|
368
|
+
const statusOrder = [
|
|
369
|
+
"completed",
|
|
370
|
+
"skipped",
|
|
371
|
+
"failed",
|
|
372
|
+
"canceled",
|
|
373
|
+
"unknown"
|
|
374
|
+
];
|
|
375
|
+
const strategyNames = /* @__PURE__ */ new Set();
|
|
376
|
+
const signalStats = /* @__PURE__ */ new Map();
|
|
377
|
+
const evaluationStats = /* @__PURE__ */ new Map();
|
|
378
|
+
const tradeStats = /* @__PURE__ */ new Map();
|
|
379
|
+
for (const strategyName of configuredStrategyNames) {
|
|
380
|
+
if (strategyName) {
|
|
381
|
+
strategyNames.add(strategyName);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const signal of signals) {
|
|
385
|
+
strategyNames.add(signal.strategy);
|
|
386
|
+
const stats = signalStats.get(signal.strategy) ?? /* @__PURE__ */ new Map();
|
|
387
|
+
const status = normalizeStatus(signal.orderStatus);
|
|
388
|
+
stats.set(status, (stats.get(status) ?? 0) + 1);
|
|
389
|
+
signalStats.set(signal.strategy, stats);
|
|
390
|
+
}
|
|
391
|
+
for (const evaluation of signalEvaluations) {
|
|
392
|
+
strategyNames.add(evaluation.strategy);
|
|
393
|
+
const stats = evaluationStats.get(evaluation.strategy) ?? {
|
|
394
|
+
evaluated: 0,
|
|
395
|
+
signals: 0,
|
|
396
|
+
reasonGroups: /* @__PURE__ */ new Map()
|
|
397
|
+
};
|
|
398
|
+
stats.evaluated += 1;
|
|
399
|
+
if (evaluation.status === "signal") {
|
|
400
|
+
stats.signals += 1;
|
|
401
|
+
}
|
|
402
|
+
const orderStatus = normalizeStatus(evaluation.orderStatus);
|
|
403
|
+
let skipReason = null;
|
|
404
|
+
let fallbackSource = "core";
|
|
405
|
+
if (evaluation.status === "skip") {
|
|
406
|
+
skipReason = evaluation.reason || "NO_SIGNAL";
|
|
407
|
+
fallbackSource = "core";
|
|
408
|
+
} else if (evaluation.status === "signal" && (orderStatus === "skipped" || orderStatus === "canceled")) {
|
|
409
|
+
skipReason = evaluation.orderSkipReason || evaluation.reason || `orderStatus=${orderStatus}`;
|
|
410
|
+
fallbackSource = "runtime";
|
|
411
|
+
} else if (evaluation.status === "error") {
|
|
412
|
+
skipReason = evaluation.reason || "EVALUATION_ERROR";
|
|
413
|
+
fallbackSource = "runtime";
|
|
414
|
+
}
|
|
415
|
+
if (skipReason) {
|
|
416
|
+
const normalized = normalizeSkipStatsReason(skipReason, fallbackSource);
|
|
417
|
+
const reasons = stats.reasonGroups.get(normalized.source) ?? /* @__PURE__ */ new Map();
|
|
418
|
+
reasons.set(normalized.reason, (reasons.get(normalized.reason) ?? 0) + 1);
|
|
419
|
+
stats.reasonGroups.set(normalized.source, reasons);
|
|
420
|
+
}
|
|
421
|
+
evaluationStats.set(evaluation.strategy, stats);
|
|
422
|
+
}
|
|
423
|
+
for (const trade of trades) {
|
|
424
|
+
strategyNames.add(trade.strategy);
|
|
425
|
+
const stats = tradeStats.get(trade.strategy) ?? {
|
|
426
|
+
total: 0,
|
|
427
|
+
active: 0,
|
|
428
|
+
closed: 0,
|
|
429
|
+
activePnl: 0,
|
|
430
|
+
activePnlKnown: 0,
|
|
431
|
+
closedPnl: 0,
|
|
432
|
+
closedPnlKnown: 0,
|
|
433
|
+
totalPnl: 0,
|
|
434
|
+
totalPnlKnown: 0,
|
|
435
|
+
trades: []
|
|
436
|
+
};
|
|
437
|
+
const pnl = trade.status === "active" ? trade.currentPnl : trade.closedPnl ?? trade.currentPnl;
|
|
438
|
+
const pnlText = typeof pnl === "number" && Number.isFinite(pnl) ? formatSigned(pnl) : "n/a";
|
|
439
|
+
stats.total += 1;
|
|
440
|
+
if (trade.status === "active") {
|
|
441
|
+
stats.active += 1;
|
|
442
|
+
if (typeof pnl === "number" && Number.isFinite(pnl)) {
|
|
443
|
+
stats.activePnl += pnl;
|
|
444
|
+
stats.activePnlKnown += 1;
|
|
445
|
+
stats.totalPnl += pnl;
|
|
446
|
+
stats.totalPnlKnown += 1;
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
stats.closed += 1;
|
|
450
|
+
if (typeof pnl === "number" && Number.isFinite(pnl)) {
|
|
451
|
+
stats.closedPnl += pnl;
|
|
452
|
+
stats.closedPnlKnown += 1;
|
|
453
|
+
stats.totalPnl += pnl;
|
|
454
|
+
stats.totalPnlKnown += 1;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
stats.trades.push({
|
|
458
|
+
symbol: trade.symbol,
|
|
459
|
+
status: trade.status,
|
|
460
|
+
pnlText,
|
|
461
|
+
entryTimestamp: trade.entryTimestamp,
|
|
462
|
+
orderId: trade.orderId
|
|
463
|
+
});
|
|
464
|
+
tradeStats.set(trade.strategy, stats);
|
|
465
|
+
}
|
|
466
|
+
const sortedStrategies = [...strategyNames].sort(
|
|
467
|
+
(left, right) => left.localeCompare(right)
|
|
468
|
+
);
|
|
469
|
+
const totalWindowPnl = [...tradeStats.values()].reduce(
|
|
470
|
+
(sum, stats) => sum + stats.totalPnl,
|
|
471
|
+
0
|
|
472
|
+
);
|
|
473
|
+
const totalWindowPnlKnown = [...tradeStats.values()].reduce(
|
|
474
|
+
(sum, stats) => sum + stats.totalPnlKnown,
|
|
475
|
+
0
|
|
476
|
+
);
|
|
477
|
+
const totalWindowPnlText = totalWindowPnlKnown > 0 ? formatSigned(totalWindowPnl) : "n/a";
|
|
478
|
+
const windowPnlLabel = hours === 24 ? "24h PnL" : `${hours}h PnL`;
|
|
479
|
+
lines.push(`\u{1F4CB} <b>${escapeHtml(resolveSummaryTitle(hours))}</b>`);
|
|
480
|
+
lines.push("");
|
|
481
|
+
lines.push("\u{1F552} <b>Window</b>");
|
|
482
|
+
lines.push(
|
|
483
|
+
`<b>${escapeHtml(formatMskDateTime(startTime))} - ${escapeHtml(formatMskDateTime(endTime))} ${SUMMARY_TIMEZONE_LABEL}</b>`
|
|
484
|
+
);
|
|
485
|
+
lines.push("");
|
|
486
|
+
lines.push(`\u23F1 Range: <b>${hours}h</b>`);
|
|
487
|
+
lines.push(
|
|
488
|
+
`\u{1F4B0} <b>${windowPnlLabel}:</b> <b>${escapeHtml(totalWindowPnlText)}</b>`
|
|
489
|
+
);
|
|
490
|
+
lines.push("");
|
|
491
|
+
lines.push("\u{1F4E1} <b>Signals</b>");
|
|
492
|
+
if (!sortedStrategies.length) {
|
|
493
|
+
lines.push("\u26A0\uFE0F No runtime data for this window.");
|
|
494
|
+
return lines.join("\n");
|
|
495
|
+
}
|
|
496
|
+
const appendEvaluationDetails = (strategyName) => {
|
|
497
|
+
const evaluation = evaluationStats.get(strategyName);
|
|
498
|
+
if (!evaluation) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
lines.push(
|
|
502
|
+
`evaluated=<b>${evaluation.evaluated}</b>, signals=<b>${evaluation.signals}</b>`
|
|
503
|
+
);
|
|
504
|
+
const sourceOrder = ["skip from core", "skip from AI", "skip from ML"];
|
|
505
|
+
const sortedReasonGroups = [...evaluation.reasonGroups.entries()].sort(
|
|
506
|
+
(left, right) => {
|
|
507
|
+
const leftOrder = sourceOrder.indexOf(left[0]);
|
|
508
|
+
const rightOrder = sourceOrder.indexOf(right[0]);
|
|
509
|
+
const normalizedLeftOrder = leftOrder >= 0 ? leftOrder : sourceOrder.length;
|
|
510
|
+
const normalizedRightOrder = rightOrder >= 0 ? rightOrder : sourceOrder.length;
|
|
511
|
+
return normalizedLeftOrder - normalizedRightOrder || left[0].localeCompare(right[0]);
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
for (const [source, reasons] of sortedReasonGroups) {
|
|
515
|
+
lines.push(`<b>${escapeHtml(source)}</b>:`);
|
|
516
|
+
const sortedReasons = [...reasons.entries()].sort(
|
|
517
|
+
(left, right) => right[1] - left[1] || left[0].localeCompare(right[0])
|
|
518
|
+
);
|
|
519
|
+
for (const [reason, count] of sortedReasons) {
|
|
520
|
+
lines.push(`${escapeHtml(reason)}: <b>${count}</b>`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
for (const strategyName of sortedStrategies) {
|
|
525
|
+
const stats = signalStats.get(strategyName);
|
|
526
|
+
if (!stats || stats.size === 0) {
|
|
527
|
+
const evaluation = evaluationStats.get(strategyName);
|
|
528
|
+
lines.push("");
|
|
529
|
+
lines.push(
|
|
530
|
+
evaluation?.evaluated ? `<b>${escapeHtml(strategyName)}</b>
|
|
531
|
+
signals=<b>0</b>` : `<b>${escapeHtml(strategyName)}</b>
|
|
532
|
+
none`
|
|
533
|
+
);
|
|
534
|
+
appendEvaluationDetails(strategyName);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const parts = statusOrder.map((status) => {
|
|
538
|
+
const count = stats.get(status) ?? 0;
|
|
539
|
+
return count > 0 ? `${status}=<b>${count}</b>` : null;
|
|
540
|
+
}).filter(Boolean);
|
|
541
|
+
lines.push("");
|
|
542
|
+
lines.push(`<b>${escapeHtml(strategyName)}</b>`);
|
|
543
|
+
lines.push(parts.join(", "));
|
|
544
|
+
appendEvaluationDetails(strategyName);
|
|
545
|
+
}
|
|
546
|
+
lines.push("");
|
|
547
|
+
lines.push("\u{1F4BC} <b>Trades</b>");
|
|
548
|
+
for (const strategyName of sortedStrategies) {
|
|
549
|
+
const stats = tradeStats.get(strategyName);
|
|
550
|
+
if (!stats) {
|
|
551
|
+
lines.push("");
|
|
552
|
+
lines.push(`<b>${escapeHtml(strategyName)}</b>`);
|
|
553
|
+
lines.push(`total=<b>0</b>`);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const activePnlText = stats.activePnlKnown > 0 ? formatSigned(stats.activePnl) : "n/a";
|
|
557
|
+
const closedPnlText = stats.closedPnlKnown > 0 ? formatSigned(stats.closedPnl) : "n/a";
|
|
558
|
+
const totalPnlText = stats.totalPnlKnown > 0 ? formatSigned(stats.totalPnl) : "n/a";
|
|
559
|
+
lines.push("");
|
|
560
|
+
lines.push(`<b>${escapeHtml(strategyName)}</b>`);
|
|
561
|
+
lines.push(
|
|
562
|
+
`total=<b>${stats.total}</b>, active=<b>${stats.active}</b> (PnL <b>${escapeHtml(activePnlText)}</b>)`
|
|
563
|
+
);
|
|
564
|
+
lines.push(
|
|
565
|
+
`closed=<b>${stats.closed}</b> (PnL <b>${escapeHtml(closedPnlText)}</b>), totalPnL=<b>${escapeHtml(totalPnlText)}</b>`
|
|
566
|
+
);
|
|
567
|
+
const sortedTrades = [...stats.trades].sort(
|
|
568
|
+
(left, right) => left.entryTimestamp - right.entryTimestamp || left.symbol.localeCompare(right.symbol) || left.orderId.localeCompare(right.orderId)
|
|
569
|
+
);
|
|
570
|
+
for (const trade of sortedTrades) {
|
|
571
|
+
lines.push(
|
|
572
|
+
`- ${escapeHtml(trade.symbol)}: PnL <b>${escapeHtml(trade.pnlText)}</b>, status=<b>${escapeHtml(trade.status)}</b>`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return lines.join("\n");
|
|
577
|
+
};
|
|
578
|
+
var signalsSummary = async () => {
|
|
579
|
+
const hours = Math.max(
|
|
580
|
+
1,
|
|
581
|
+
Number.parseInt(String(flags.hours ?? 24), 10) || 24
|
|
582
|
+
);
|
|
583
|
+
const endTime = Date.now();
|
|
584
|
+
const startTime = endTime - hours * 60 * 60 * 1e3;
|
|
585
|
+
const connectorName = await resolveSummaryConnectorName(flags.connector);
|
|
586
|
+
const connectorFactory = await (0, import_connectors.getConnectorCreatorByName)(
|
|
587
|
+
connectorName,
|
|
588
|
+
projectRoot
|
|
589
|
+
);
|
|
590
|
+
if (!connectorFactory) {
|
|
591
|
+
throw new Error(`Connector "${connectorName}" is not registered`);
|
|
592
|
+
}
|
|
593
|
+
const connector = await connectorFactory({
|
|
594
|
+
userName: flags.user
|
|
595
|
+
});
|
|
596
|
+
const [configuredStrategyNames, signals, signalEvaluations, trades] = await Promise.all([
|
|
597
|
+
loadRuntimeStrategyNames(flags.user),
|
|
598
|
+
loadRuntimeSignals(flags.user),
|
|
599
|
+
loadRuntimeSignalEvaluations(flags.user),
|
|
600
|
+
loadRuntimeTrades(flags.user)
|
|
601
|
+
]);
|
|
602
|
+
const syncedTrades = await syncRuntimeTrades({
|
|
603
|
+
userName: flags.user,
|
|
604
|
+
connector,
|
|
605
|
+
trades,
|
|
606
|
+
startTime,
|
|
607
|
+
endTime
|
|
608
|
+
});
|
|
609
|
+
const windowSignals = signals.filter(
|
|
610
|
+
(signal) => signal.timestamp >= startTime && signal.timestamp <= endTime
|
|
611
|
+
);
|
|
612
|
+
const windowSignalEvaluations = signalEvaluations.filter(
|
|
613
|
+
(evaluation) => evaluation.timestamp >= startTime && evaluation.timestamp <= endTime
|
|
614
|
+
);
|
|
615
|
+
const signalIds = new Set(
|
|
616
|
+
signals.map((signal) => signal.signalId).filter(Boolean)
|
|
617
|
+
);
|
|
618
|
+
const windowTrades = syncedTrades.filter(
|
|
619
|
+
(trade) => trade.entryTimestamp >= startTime && trade.entryTimestamp <= endTime && trade.signalId != null && signalIds.has(trade.signalId)
|
|
620
|
+
);
|
|
621
|
+
const message = buildSummaryMessage({
|
|
622
|
+
hours,
|
|
623
|
+
startTime,
|
|
624
|
+
endTime,
|
|
625
|
+
configuredStrategyNames,
|
|
626
|
+
signals: windowSignals,
|
|
627
|
+
signalEvaluations: windowSignalEvaluations,
|
|
628
|
+
trades: windowTrades
|
|
629
|
+
});
|
|
630
|
+
import_logger.logger.info(
|
|
631
|
+
"signals summary window=%sh signals=%s evaluations=%s trades=%s connector=%s user=%s",
|
|
632
|
+
hours,
|
|
633
|
+
windowSignals.length,
|
|
634
|
+
windowSignalEvaluations.length,
|
|
635
|
+
windowTrades.length,
|
|
636
|
+
connectorName,
|
|
637
|
+
flags.user
|
|
638
|
+
);
|
|
639
|
+
if (flags.printOnly) {
|
|
640
|
+
console.log(message);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
await sendTelegramReport(message, { userName: flags.user });
|
|
644
|
+
};
|
|
645
|
+
if (process.env.NODE_ENV !== "test") {
|
|
646
|
+
signalsSummary().then(() => {
|
|
647
|
+
process.exit(0);
|
|
648
|
+
}).catch(() => {
|
|
649
|
+
process.exit(1);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
653
|
+
0 && (module.exports = {
|
|
654
|
+
signalsSummary
|
|
655
|
+
});
|