@tradejs/cli 1.0.6 → 1.0.9

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.
@@ -0,0 +1,712 @@
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_redis2 = 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/lib/runtimeSignalsLoader.ts
104
+ var import_redis = require("@tradejs/infra/redis");
105
+
106
+ // src/lib/runtimeSignalsStorage.ts
107
+ var DAY_MS = 864e5;
108
+ var RUNTIME_STORAGE_DAY_OFFSET_MS = 5 * 60 * 60 * 1e3;
109
+ var toIsoDayKey = (timestamp) => new Date(timestamp).toISOString().slice(0, 10);
110
+ var toRuntimeStorageDayTimestamp = (timestamp) => Math.floor((timestamp + RUNTIME_STORAGE_DAY_OFFSET_MS) / DAY_MS) * DAY_MS;
111
+ var getRuntimeStorageDayKeys = (startTime, endTime) => {
112
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) {
113
+ return [];
114
+ }
115
+ const start = Math.min(startTime, endTime);
116
+ const endExclusive = Math.max(startTime, endTime);
117
+ const normalizedEnd = Math.max(start, endExclusive - 1);
118
+ const keys = [];
119
+ const startDay = toRuntimeStorageDayTimestamp(start);
120
+ const endDay = toRuntimeStorageDayTimestamp(normalizedEnd);
121
+ for (let current = startDay; current <= endDay; current += DAY_MS) {
122
+ keys.push(toIsoDayKey(current));
123
+ }
124
+ return keys;
125
+ };
126
+ var isRuntimeSignalBucketRef = (value) => {
127
+ if (!value || typeof value !== "object") {
128
+ return false;
129
+ }
130
+ const record = value;
131
+ return typeof record.signalId === "string" && typeof record.symbol === "string" && typeof record.strategy === "string" && typeof record.timestamp === "number";
132
+ };
133
+ var toCount = (value) => {
134
+ const parsed = Number.parseInt(String(value ?? "0"), 10);
135
+ return Number.isFinite(parsed) ? parsed : 0;
136
+ };
137
+ var parseRuntimeSignalStatsBucket = (value) => {
138
+ const reasonGroups = /* @__PURE__ */ new Map();
139
+ for (const [field, rawCount] of Object.entries(value)) {
140
+ if (!field.startsWith("reason:")) {
141
+ continue;
142
+ }
143
+ const [, source, ...reasonParts] = field.split(":");
144
+ const reason = reasonParts.join(":");
145
+ if (!source || !reason) {
146
+ continue;
147
+ }
148
+ const reasons = reasonGroups.get(source) ?? /* @__PURE__ */ new Map();
149
+ reasons.set(reason, toCount(rawCount));
150
+ reasonGroups.set(source, reasons);
151
+ }
152
+ return {
153
+ evaluated: toCount(value.evaluated),
154
+ signals: toCount(value.signals),
155
+ reasonGroups
156
+ };
157
+ };
158
+
159
+ // src/lib/runtimeSignalsLoader.ts
160
+ var isSignalRecord = (value) => {
161
+ if (!value || typeof value !== "object") {
162
+ return false;
163
+ }
164
+ const record = value;
165
+ return typeof record.signalId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number";
166
+ };
167
+ var sortByTimestamp = (records) => [...records].sort((left, right) => left.timestamp - right.timestamp);
168
+ var loadRuntimeSignals = async (userName) => {
169
+ const bucketKeys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeSignalBuckets(userName));
170
+ const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
171
+ const seen = /* @__PURE__ */ new Set();
172
+ const payloads = await Promise.all(
173
+ refs.map(async (ref) => {
174
+ const dedupeKey = `${ref.symbol}:${ref.signalId}`;
175
+ if (seen.has(dedupeKey)) {
176
+ return null;
177
+ }
178
+ seen.add(dedupeKey);
179
+ const signal = await (0, import_redis.getData)(
180
+ import_redis.redisKeys.storeSignal(ref.symbol, ref.signalId),
181
+ null
182
+ );
183
+ return isSignalRecord(signal) ? signal : null;
184
+ })
185
+ );
186
+ return sortByTimestamp(payloads.filter(isSignalRecord));
187
+ };
188
+ var parseBucketKey = (prefix, key) => {
189
+ if (!key.startsWith(prefix)) {
190
+ return null;
191
+ }
192
+ const suffix = key.slice(prefix.length);
193
+ const firstColon = suffix.indexOf(":");
194
+ if (firstColon <= 0 || firstColon >= suffix.length - 1) {
195
+ return null;
196
+ }
197
+ return {
198
+ dayKey: suffix.slice(0, firstColon),
199
+ strategy: suffix.slice(firstColon + 1)
200
+ };
201
+ };
202
+ var loadRuntimeSignalEvaluationStatsBuckets = async (userName) => {
203
+ const prefix = import_redis.redisKeys.runtimeSignalEvaluationStatsBuckets(userName);
204
+ const keys = await (0, import_redis.getKeys)(prefix);
205
+ const entries = await Promise.all(
206
+ keys.map(async (key) => {
207
+ const parsedKey = parseBucketKey(prefix, key);
208
+ if (!parsedKey) {
209
+ return null;
210
+ }
211
+ return {
212
+ key,
213
+ ...parsedKey,
214
+ stats: parseRuntimeSignalStatsBucket(await (0, import_redis.getHashData)(key))
215
+ };
216
+ })
217
+ );
218
+ return entries.filter(Boolean);
219
+ };
220
+
221
+ // src/scripts/signalsSummary.ts
222
+ import_args.default.option(["u", "user"], "Use user config", "root");
223
+ import_args.default.option(
224
+ "connector",
225
+ "Connector provider or name for summary (e.g. bybit, binance, coinbase, custom)",
226
+ "bybit"
227
+ );
228
+ import_args.default.option(["H", "hours"], "Summary window in hours", 24);
229
+ import_args.default.option(["P", "printOnly"], "Print summary instead of Telegram", false);
230
+ var flags = import_args.default.parse(process.argv);
231
+ var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
232
+ var SUMMARY_TIMEZONE = "Europe/Moscow";
233
+ var SUMMARY_TIMEZONE_LABEL = "MSK";
234
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
235
+ var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
236
+ timeZone: SUMMARY_TIMEZONE,
237
+ year: "numeric",
238
+ month: "2-digit",
239
+ day: "2-digit",
240
+ hour: "2-digit",
241
+ minute: "2-digit",
242
+ hour12: false
243
+ }).format(new Date(timestamp));
244
+ var formatSigned = (value) => `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
245
+ var resolveSummaryTitle = (hours) => {
246
+ if (hours === 24) {
247
+ return "TradeJS daily summary";
248
+ }
249
+ if (hours === 168) {
250
+ return "TradeJS weekly summary";
251
+ }
252
+ return `TradeJS ${hours}h summary`;
253
+ };
254
+ var normalizeStatus = (value) => {
255
+ if (value === "completed" || value === "failed" || value === "skipped" || value === "canceled") {
256
+ return value;
257
+ }
258
+ return "unknown";
259
+ };
260
+ var isRuntimeTradeRecord = (value) => {
261
+ if (!value || typeof value !== "object") {
262
+ return false;
263
+ }
264
+ const record = value;
265
+ 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";
266
+ };
267
+ var resolveSummaryConnectorName = async (value) => {
268
+ const connectorName = await (0, import_connectors.resolveConnectorName)(value, projectRoot);
269
+ if (connectorName) {
270
+ return connectorName;
271
+ }
272
+ import_logger.logger.warn(
273
+ 'Unknown connector "%s". Fallback to %s.',
274
+ String(value || "").trim() || String(value),
275
+ import_connectors.DEFAULT_CONNECTOR_NAME
276
+ );
277
+ return import_connectors.DEFAULT_CONNECTOR_NAME;
278
+ };
279
+ var resolveStrategyNameByConfigKey = (userName, key) => {
280
+ const parts = key.split(":");
281
+ if (parts.length !== 5) {
282
+ return null;
283
+ }
284
+ const [users, keyUserName, strategiesKey, strategyName, configKey] = parts;
285
+ if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || configKey !== "config" || !strategyName) {
286
+ return null;
287
+ }
288
+ return strategyName;
289
+ };
290
+ var loadRuntimeStrategyNames = async (userName) => {
291
+ const keys = await (0, import_redis2.getKeys)(`${import_redis2.redisKeys.strategies(userName)}:`);
292
+ return keys.filter((key) => key.endsWith(":config")).map((key) => resolveStrategyNameByConfigKey(userName, key)).filter((value) => Boolean(value)).sort((left, right) => left.localeCompare(right));
293
+ };
294
+ var loadRuntimeTrades = async (userName) => {
295
+ const keys = await (0, import_redis2.getKeys)(import_redis2.redisKeys.runtimeTrades(userName));
296
+ const trades = await Promise.all(keys.map((key) => (0, import_redis2.getData)(key, null)));
297
+ return trades.filter(isRuntimeTradeRecord).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
298
+ };
299
+ var createRuntimeSignalStats = () => ({
300
+ evaluated: 0,
301
+ signals: 0,
302
+ reasonGroups: /* @__PURE__ */ new Map()
303
+ });
304
+ var mergeRuntimeSignalStats = (target, source) => {
305
+ target.evaluated += source.evaluated;
306
+ target.signals += source.signals;
307
+ for (const [group, reasons] of source.reasonGroups.entries()) {
308
+ const targetReasons = target.reasonGroups.get(group) ?? /* @__PURE__ */ new Map();
309
+ for (const [reason, count] of reasons.entries()) {
310
+ targetReasons.set(reason, (targetReasons.get(reason) ?? 0) + count);
311
+ }
312
+ target.reasonGroups.set(group, targetReasons);
313
+ }
314
+ };
315
+ var loadClosedPnlRows = async ({
316
+ connector,
317
+ startTime,
318
+ endTime
319
+ }) => {
320
+ if (typeof connector.getClosedPnl !== "function") {
321
+ return [];
322
+ }
323
+ try {
324
+ const rows = await connector.getClosedPnl({
325
+ startTime,
326
+ endTime,
327
+ limit: 100
328
+ });
329
+ return rows.sort((left, right) => left.closedAt - right.closedAt);
330
+ } catch (error) {
331
+ import_logger.logger.warn(
332
+ "signals summary: getClosedPnl failed: %s",
333
+ error?.message || String(error)
334
+ );
335
+ return [];
336
+ }
337
+ };
338
+ var consumeClosedPnlMatch = (buckets, trade) => {
339
+ const rows = buckets.get(trade.symbol);
340
+ if (!rows?.length) {
341
+ return null;
342
+ }
343
+ const minimumClosedAt = trade.entryTimestamp - 5 * 6e4;
344
+ const matchIndex = rows.findIndex(
345
+ (row2) => Number.isFinite(row2.closedAt) && row2.closedAt >= minimumClosedAt
346
+ );
347
+ if (matchIndex < 0) {
348
+ return null;
349
+ }
350
+ const [row] = rows.splice(matchIndex, 1);
351
+ return row ?? null;
352
+ };
353
+ var syncRuntimeTrades = async ({
354
+ userName,
355
+ connector,
356
+ trades,
357
+ startTime,
358
+ endTime
359
+ }) => {
360
+ const openPositions = typeof connector.getOpenPositionPnl === "function" ? await connector.getOpenPositionPnl() : [];
361
+ const openPositionsBySymbol = new Map(
362
+ openPositions.map((position) => [position.symbol, position])
363
+ );
364
+ const activeOrderIdBySymbol = /* @__PURE__ */ new Map();
365
+ const symbols = [...new Set(trades.map((trade) => trade.symbol))];
366
+ await Promise.all(
367
+ symbols.map(async (symbol) => {
368
+ const activeRef = await (0, import_redis2.getData)(
369
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol),
370
+ null
371
+ );
372
+ activeOrderIdBySymbol.set(
373
+ symbol,
374
+ typeof activeRef?.orderId === "string" ? activeRef.orderId : null
375
+ );
376
+ })
377
+ );
378
+ const closedPnlRows = await loadClosedPnlRows({
379
+ connector,
380
+ startTime,
381
+ endTime
382
+ });
383
+ const closedPnlBuckets = /* @__PURE__ */ new Map();
384
+ for (const row of closedPnlRows) {
385
+ const bucket = closedPnlBuckets.get(row.symbol) ?? [];
386
+ bucket.push(row);
387
+ closedPnlBuckets.set(row.symbol, bucket);
388
+ }
389
+ const syncedTrades = [];
390
+ for (const trade of trades) {
391
+ if (trade.status !== "active") {
392
+ syncedTrades.push(trade);
393
+ continue;
394
+ }
395
+ const openPosition = openPositionsBySymbol.get(trade.symbol);
396
+ const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
397
+ const isCurrentActiveTrade = activeOrderId === trade.orderId;
398
+ if (isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
399
+ const nextTrade2 = {
400
+ ...trade,
401
+ status: "active",
402
+ currentPrice: openPosition.currentPrice,
403
+ currentPnl: openPosition.unrealizedPnl,
404
+ lastSyncedAt: endTime
405
+ };
406
+ await (0, import_redis2.setData)(
407
+ import_redis2.redisKeys.runtimeTrade(userName, trade.orderId),
408
+ nextTrade2,
409
+ {
410
+ expire: 0
411
+ }
412
+ );
413
+ syncedTrades.push(nextTrade2);
414
+ continue;
415
+ }
416
+ const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
417
+ const nextTrade = {
418
+ ...trade,
419
+ status: "closed",
420
+ currentPrice: matchedClosedPnl?.exitPrice ?? trade.currentPrice ?? null,
421
+ currentPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
422
+ closedPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
423
+ exitPrice: matchedClosedPnl?.exitPrice ?? trade.exitPrice ?? null,
424
+ exitTimestamp: matchedClosedPnl?.closedAt ?? trade.exitTimestamp ?? endTime,
425
+ lastSyncedAt: endTime
426
+ };
427
+ await Promise.all([
428
+ (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
429
+ expire: import_constants.TTL_1M
430
+ }),
431
+ ...isCurrentActiveTrade ? [(0, import_redis2.delKey)(import_redis2.redisKeys.runtimeActiveTrade(userName, trade.symbol))] : []
432
+ ]);
433
+ syncedTrades.push(nextTrade);
434
+ }
435
+ return syncedTrades;
436
+ };
437
+ var buildSummaryMessage = ({
438
+ hours,
439
+ startTime,
440
+ endTime,
441
+ configuredStrategyNames,
442
+ signals,
443
+ evaluationStatsByStrategy,
444
+ trades
445
+ }) => {
446
+ const lines = [];
447
+ const statusOrder = [
448
+ "completed",
449
+ "skipped",
450
+ "failed",
451
+ "canceled",
452
+ "unknown"
453
+ ];
454
+ const strategyNames = /* @__PURE__ */ new Set();
455
+ const signalStats = /* @__PURE__ */ new Map();
456
+ const tradeStats = /* @__PURE__ */ new Map();
457
+ for (const strategyName of configuredStrategyNames) {
458
+ if (strategyName) {
459
+ strategyNames.add(strategyName);
460
+ }
461
+ }
462
+ for (const signal of signals) {
463
+ strategyNames.add(signal.strategy);
464
+ const stats = signalStats.get(signal.strategy) ?? /* @__PURE__ */ new Map();
465
+ const status = normalizeStatus(signal.orderStatus);
466
+ stats.set(status, (stats.get(status) ?? 0) + 1);
467
+ signalStats.set(signal.strategy, stats);
468
+ }
469
+ for (const trade of trades) {
470
+ strategyNames.add(trade.strategy);
471
+ const stats = tradeStats.get(trade.strategy) ?? {
472
+ total: 0,
473
+ active: 0,
474
+ closed: 0,
475
+ activePnl: 0,
476
+ activePnlKnown: 0,
477
+ closedPnl: 0,
478
+ closedPnlKnown: 0,
479
+ totalPnl: 0,
480
+ totalPnlKnown: 0,
481
+ trades: []
482
+ };
483
+ const pnl = trade.status === "active" ? trade.currentPnl : trade.closedPnl ?? trade.currentPnl;
484
+ const pnlText = typeof pnl === "number" && Number.isFinite(pnl) ? formatSigned(pnl) : "n/a";
485
+ stats.total += 1;
486
+ if (trade.status === "active") {
487
+ stats.active += 1;
488
+ if (typeof pnl === "number" && Number.isFinite(pnl)) {
489
+ stats.activePnl += pnl;
490
+ stats.activePnlKnown += 1;
491
+ stats.totalPnl += pnl;
492
+ stats.totalPnlKnown += 1;
493
+ }
494
+ } else {
495
+ stats.closed += 1;
496
+ if (typeof pnl === "number" && Number.isFinite(pnl)) {
497
+ stats.closedPnl += pnl;
498
+ stats.closedPnlKnown += 1;
499
+ stats.totalPnl += pnl;
500
+ stats.totalPnlKnown += 1;
501
+ }
502
+ }
503
+ stats.trades.push({
504
+ symbol: trade.symbol,
505
+ status: trade.status,
506
+ pnlText,
507
+ entryTimestamp: trade.entryTimestamp,
508
+ orderId: trade.orderId
509
+ });
510
+ tradeStats.set(trade.strategy, stats);
511
+ }
512
+ const sortedStrategies = [...strategyNames].sort(
513
+ (left, right) => left.localeCompare(right)
514
+ );
515
+ const totalWindowPnl = [...tradeStats.values()].reduce(
516
+ (sum, stats) => sum + stats.totalPnl,
517
+ 0
518
+ );
519
+ const totalWindowPnlKnown = [...tradeStats.values()].reduce(
520
+ (sum, stats) => sum + stats.totalPnlKnown,
521
+ 0
522
+ );
523
+ const totalWindowPnlText = totalWindowPnlKnown > 0 ? formatSigned(totalWindowPnl) : "n/a";
524
+ const windowPnlLabel = hours === 24 ? "24h PnL" : `${hours}h PnL`;
525
+ lines.push(`\u{1F4CB} <b>${escapeHtml(resolveSummaryTitle(hours))}</b>`);
526
+ lines.push("");
527
+ lines.push("\u{1F552} <b>Window</b>");
528
+ lines.push(
529
+ `<b>${escapeHtml(formatMskDateTime(startTime))} - ${escapeHtml(formatMskDateTime(endTime))} ${SUMMARY_TIMEZONE_LABEL}</b>`
530
+ );
531
+ lines.push("");
532
+ lines.push(`\u23F1 Range: <b>${hours}h</b>`);
533
+ lines.push(
534
+ `\u{1F4B0} <b>${windowPnlLabel}:</b> <b>${escapeHtml(totalWindowPnlText)}</b>`
535
+ );
536
+ lines.push("");
537
+ lines.push("\u{1F4E1} <b>Signals</b>");
538
+ if (!sortedStrategies.length) {
539
+ lines.push("\u26A0\uFE0F No runtime data for this window.");
540
+ return lines.join("\n");
541
+ }
542
+ const appendEvaluationDetails = (strategyName) => {
543
+ const evaluation = evaluationStatsByStrategy.get(strategyName);
544
+ if (!evaluation) {
545
+ return;
546
+ }
547
+ lines.push(
548
+ `evaluated=<b>${evaluation.evaluated}</b>, signals=<b>${evaluation.signals}</b>`
549
+ );
550
+ const sourceOrder = ["skip from core", "skip from AI", "skip from ML"];
551
+ const sortedReasonGroups = [...evaluation.reasonGroups.entries()].sort(
552
+ (left, right) => {
553
+ const leftOrder = sourceOrder.indexOf(left[0]);
554
+ const rightOrder = sourceOrder.indexOf(right[0]);
555
+ const normalizedLeftOrder = leftOrder >= 0 ? leftOrder : sourceOrder.length;
556
+ const normalizedRightOrder = rightOrder >= 0 ? rightOrder : sourceOrder.length;
557
+ return normalizedLeftOrder - normalizedRightOrder || left[0].localeCompare(right[0]);
558
+ }
559
+ );
560
+ for (const [source, reasons] of sortedReasonGroups) {
561
+ lines.push(`<b>${escapeHtml(source)}</b>:`);
562
+ const sortedReasons = [...reasons.entries()].sort(
563
+ (left, right) => right[1] - left[1] || left[0].localeCompare(right[0])
564
+ );
565
+ for (const [reason, count] of sortedReasons) {
566
+ lines.push(`${escapeHtml(reason)}: <b>${count}</b>`);
567
+ }
568
+ }
569
+ };
570
+ for (const strategyName of sortedStrategies) {
571
+ const stats = signalStats.get(strategyName);
572
+ if (!stats || stats.size === 0) {
573
+ const evaluation = evaluationStatsByStrategy.get(strategyName);
574
+ lines.push("");
575
+ lines.push(
576
+ evaluation?.evaluated ? `<b>${escapeHtml(strategyName)}</b>
577
+ signals=<b>0</b>` : `<b>${escapeHtml(strategyName)}</b>
578
+ none`
579
+ );
580
+ appendEvaluationDetails(strategyName);
581
+ continue;
582
+ }
583
+ const parts = statusOrder.map((status) => {
584
+ const count = stats.get(status) ?? 0;
585
+ return count > 0 ? `${status}=<b>${count}</b>` : null;
586
+ }).filter(Boolean);
587
+ lines.push("");
588
+ lines.push(`<b>${escapeHtml(strategyName)}</b>`);
589
+ lines.push(parts.join(", "));
590
+ appendEvaluationDetails(strategyName);
591
+ }
592
+ lines.push("");
593
+ lines.push("\u{1F4BC} <b>Trades</b>");
594
+ for (const strategyName of sortedStrategies) {
595
+ const stats = tradeStats.get(strategyName);
596
+ if (!stats) {
597
+ lines.push("");
598
+ lines.push(`<b>${escapeHtml(strategyName)}</b>`);
599
+ lines.push(`total=<b>0</b>`);
600
+ continue;
601
+ }
602
+ const activePnlText = stats.activePnlKnown > 0 ? formatSigned(stats.activePnl) : "n/a";
603
+ const closedPnlText = stats.closedPnlKnown > 0 ? formatSigned(stats.closedPnl) : "n/a";
604
+ const totalPnlText = stats.totalPnlKnown > 0 ? formatSigned(stats.totalPnl) : "n/a";
605
+ lines.push("");
606
+ lines.push(`<b>${escapeHtml(strategyName)}</b>`);
607
+ lines.push(
608
+ `total=<b>${stats.total}</b>, active=<b>${stats.active}</b> (PnL <b>${escapeHtml(activePnlText)}</b>)`
609
+ );
610
+ lines.push(
611
+ `closed=<b>${stats.closed}</b> (PnL <b>${escapeHtml(closedPnlText)}</b>), totalPnL=<b>${escapeHtml(totalPnlText)}</b>`
612
+ );
613
+ const sortedTrades = [...stats.trades].sort(
614
+ (left, right) => left.entryTimestamp - right.entryTimestamp || left.symbol.localeCompare(right.symbol) || left.orderId.localeCompare(right.orderId)
615
+ );
616
+ for (const trade of sortedTrades) {
617
+ lines.push(
618
+ `- ${escapeHtml(trade.symbol)}: PnL <b>${escapeHtml(trade.pnlText)}</b>, status=<b>${escapeHtml(trade.status)}</b>`
619
+ );
620
+ }
621
+ }
622
+ return lines.join("\n");
623
+ };
624
+ var signalsSummary = async () => {
625
+ const hours = Math.max(
626
+ 1,
627
+ Number.parseInt(String(flags.hours ?? 24), 10) || 24
628
+ );
629
+ const endTime = Date.now();
630
+ const startTime = endTime - hours * 60 * 60 * 1e3;
631
+ const connectorName = await resolveSummaryConnectorName(flags.connector);
632
+ const connectorFactory = await (0, import_connectors.getConnectorCreatorByName)(
633
+ connectorName,
634
+ projectRoot
635
+ );
636
+ if (!connectorFactory) {
637
+ throw new Error(`Connector "${connectorName}" is not registered`);
638
+ }
639
+ const connector = await connectorFactory({
640
+ userName: flags.user
641
+ });
642
+ const [configuredStrategyNames, signals, evaluationStatsBuckets, trades] = await Promise.all([
643
+ loadRuntimeStrategyNames(flags.user),
644
+ loadRuntimeSignals(flags.user),
645
+ loadRuntimeSignalEvaluationStatsBuckets(flags.user),
646
+ loadRuntimeTrades(flags.user)
647
+ ]);
648
+ const syncedTrades = await syncRuntimeTrades({
649
+ userName: flags.user,
650
+ connector,
651
+ trades,
652
+ startTime,
653
+ endTime
654
+ });
655
+ const windowSignals = signals.filter(
656
+ (signal) => signal.timestamp >= startTime && signal.timestamp < endTime
657
+ );
658
+ const windowDayKeys = new Set(getRuntimeStorageDayKeys(startTime, endTime));
659
+ const windowEvaluationStats = /* @__PURE__ */ new Map();
660
+ for (const entry of evaluationStatsBuckets) {
661
+ if (!windowDayKeys.has(entry.dayKey)) {
662
+ continue;
663
+ }
664
+ const stats = windowEvaluationStats.get(entry.strategy) ?? createRuntimeSignalStats();
665
+ mergeRuntimeSignalStats(stats, entry.stats);
666
+ windowEvaluationStats.set(entry.strategy, stats);
667
+ }
668
+ const signalIds = new Set(
669
+ signals.map((signal) => signal.signalId).filter(Boolean)
670
+ );
671
+ const windowTrades = syncedTrades.filter(
672
+ (trade) => trade.entryTimestamp >= startTime && trade.entryTimestamp < endTime && trade.signalId != null && signalIds.has(trade.signalId)
673
+ );
674
+ const message = buildSummaryMessage({
675
+ hours,
676
+ startTime,
677
+ endTime,
678
+ configuredStrategyNames,
679
+ signals: windowSignals,
680
+ evaluationStatsByStrategy: windowEvaluationStats,
681
+ trades: windowTrades
682
+ });
683
+ const windowEvaluationsCount = [...windowEvaluationStats.values()].reduce(
684
+ (sum, stats) => sum + stats.evaluated,
685
+ 0
686
+ );
687
+ import_logger.logger.info(
688
+ "signals summary window=%sh signals=%s evaluations=%s trades=%s connector=%s user=%s",
689
+ hours,
690
+ windowSignals.length,
691
+ windowEvaluationsCount,
692
+ windowTrades.length,
693
+ connectorName,
694
+ flags.user
695
+ );
696
+ if (flags.printOnly) {
697
+ console.log(message);
698
+ return;
699
+ }
700
+ await sendTelegramReport(message, { userName: flags.user });
701
+ };
702
+ if (process.env.NODE_ENV !== "test") {
703
+ signalsSummary().then(() => {
704
+ process.exit(0);
705
+ }).catch(() => {
706
+ process.exit(1);
707
+ });
708
+ }
709
+ // Annotate the CommonJS export names for ESM import in node:
710
+ 0 && (module.exports = {
711
+ signalsSummary
712
+ });