@tradejs/cli 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.
Files changed (76) hide show
  1. package/dist/cli.js +29748 -13641
  2. package/dist/lib/aiPocketSearch.js +1316 -0
  3. package/dist/lib/aiTrainCharts.js +711 -0
  4. package/dist/lib/aiTrainDataset.js +40 -0
  5. package/dist/lib/aiTrainEvaluationDump.js +76 -0
  6. package/dist/lib/aiTrainMetrics.js +148 -8
  7. package/dist/lib/aiTrainOptions.js +130 -0
  8. package/dist/lib/aiTrainQuarantine.js +199 -0
  9. package/dist/lib/aiTrainResearch.js +467 -0
  10. package/dist/lib/binanceMarketContextBackfill.js +841 -0
  11. package/dist/lib/binanceMarketData.js +392 -0
  12. package/dist/lib/coinMarketCapContextBackfill.js +1088 -0
  13. package/dist/lib/continuity.js +60 -0
  14. package/dist/lib/derivativesContextBackfill.js +490 -77
  15. package/dist/lib/executionCalibration.js +705 -0
  16. package/dist/lib/marketContextPrepare.js +4403 -0
  17. package/dist/lib/paritySummary.js +101 -0
  18. package/dist/lib/runBot.js +1 -0
  19. package/dist/lib/runEnvironment.js +2254 -0
  20. package/dist/lib/runFormatting.js +1657 -0
  21. package/dist/lib/runtimeDebugEvidence.js +526 -0
  22. package/dist/lib/runtimeModeConfig.js +53 -0
  23. package/dist/lib/runtimeParity.js +88 -11
  24. package/dist/lib/runtimeParityDetails.js +751 -0
  25. package/dist/lib/runtimeRedis.js +157 -0
  26. package/dist/lib/runtimeSignalsLoader.js +187 -0
  27. package/dist/lib/runtimeSignalsStorage.js +185 -0
  28. package/dist/lib/runtimeStrategyBacktest.js +134 -0
  29. package/dist/lib/runtimeTradeSync.js +263 -0
  30. package/dist/lib/telegramReports.js +9 -0
  31. package/dist/lib/tickerUniverseCache.js +116 -0
  32. package/dist/lib/timeWindow.js +3 -3
  33. package/dist/scripts/agentRun.js +28 -8
  34. package/dist/scripts/aiExport.js +1721 -7
  35. package/dist/scripts/aiExportSelect.js +16 -4
  36. package/dist/scripts/aiPocketSearch.js +4190 -0
  37. package/dist/scripts/aiTrain.js +1866 -87
  38. package/dist/scripts/backtest.js +4509 -818
  39. package/dist/scripts/binanceMarketIngest.js +565 -0
  40. package/dist/scripts/bot.js +16 -2
  41. package/dist/scripts/candlesMigrateProvider.js +32 -22
  42. package/dist/scripts/cleanDir.js +15 -2
  43. package/dist/scripts/cleanRedis.js +15 -2
  44. package/dist/scripts/cleanTests.js +38 -2
  45. package/dist/scripts/cleanupMarketContext.js +69 -0
  46. package/dist/scripts/continuity.js +148 -67
  47. package/dist/scripts/derivativesIngest.js +14 -4
  48. package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
  49. package/dist/scripts/doctor.js +14 -4
  50. package/dist/scripts/executionCalibration.js +792 -0
  51. package/dist/scripts/infraCommon.js +10 -0
  52. package/dist/scripts/infraDown.js +14 -4
  53. package/dist/scripts/infraInit.js +24 -4
  54. package/dist/scripts/infraUp.js +14 -4
  55. package/dist/scripts/marketWs.js +222 -0
  56. package/dist/scripts/migration.js +15 -2
  57. package/dist/scripts/mlExport.js +1698 -10
  58. package/dist/scripts/mlExportSelect.js +16 -4
  59. package/dist/scripts/mlInspect.js +13 -3
  60. package/dist/scripts/mlTrainLatestSelect.js +14 -4
  61. package/dist/scripts/replay.js +8824 -0
  62. package/dist/scripts/replayRunner.js +8827 -0
  63. package/dist/scripts/replayRuntimeEvidence.js +868 -0
  64. package/dist/scripts/researchAuto.js +289 -60
  65. package/dist/scripts/results.js +3 -3
  66. package/dist/scripts/runtimeEvidence.js +605 -0
  67. package/dist/scripts/runtimeParity.js +6360 -1004
  68. package/dist/scripts/serverHealth.js +588 -0
  69. package/dist/scripts/signals.js +5623 -486
  70. package/dist/scripts/signalsDaemon.js +6167 -0
  71. package/dist/scripts/signalsSummary.js +954 -312
  72. package/dist/scripts/test-ml.js +25 -3
  73. package/dist/scripts/test.js +26 -1
  74. package/dist/scripts/user-add.js +15 -2
  75. package/dist/workers/testerWorker.js +130 -21
  76. package/package.json +18 -12
@@ -30,16 +30,310 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/scripts/signalsSummary.ts
31
31
  var signalsSummary_exports = {};
32
32
  __export(signalsSummary_exports, {
33
+ main: () => main,
33
34
  signalsSummary: () => signalsSummary
34
35
  });
35
36
  module.exports = __toCommonJS(signalsSummary_exports);
36
37
  var import_config = require("dotenv/config");
37
38
  var import_args = __toESM(require("args"));
38
- var import_constants = require("@tradejs/core/constants");
39
39
  var import_logger = require("@tradejs/infra/logger");
40
- var import_redis = require("@tradejs/infra/redis");
41
40
  var import_connectors = require("@tradejs/node/connectors");
42
41
 
42
+ // src/lib/runtimeRedis.ts
43
+ var import_redis = require("@tradejs/infra/redis");
44
+ var import_time = require("@tradejs/core/time");
45
+ var RESERVED_STRATEGY_NAMES = /* @__PURE__ */ new Set(["charts"]);
46
+ var RESERVED_CONFIG_IDS = /* @__PURE__ */ new Set(["results"]);
47
+ var resolveStrategyConfigIdentityByKey = (userName, key) => {
48
+ const parts = key.split(":");
49
+ if (parts.length !== 5) return null;
50
+ const [users, keyUserName, strategiesKey, strategyName, configId] = parts;
51
+ if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || !strategyName || !configId || RESERVED_STRATEGY_NAMES.has(strategyName) || RESERVED_CONFIG_IDS.has(configId) || !/^[a-zA-Z0-9_-]+$/.test(configId)) {
52
+ return null;
53
+ }
54
+ return { strategyName, configId };
55
+ };
56
+ var isRuntimeTradeRecord = (value) => {
57
+ if (!value || typeof value !== "object") {
58
+ return false;
59
+ }
60
+ const record = value;
61
+ 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" && (record.direction === "LONG" || record.direction === "SHORT");
62
+ };
63
+ var resolveStrategyNameByConfigKey = (userName, key) => {
64
+ return resolveStrategyConfigIdentityByKey(userName, key)?.strategyName ?? null;
65
+ };
66
+ var getRuntimeStrategyConfigKeys = async (userName) => {
67
+ const keys = await (0, import_redis.getKeys)(`${import_redis.redisKeys.strategies(userName)}:`);
68
+ return keys.filter((key) => resolveStrategyConfigIdentityByKey(userName, key)).sort((left, right) => left.localeCompare(right));
69
+ };
70
+ var loadRuntimeStrategyNames = async (userName) => [
71
+ ...new Set(
72
+ (await getRuntimeStrategyConfigKeys(userName)).map((key) => resolveStrategyNameByConfigKey(userName, key)).filter((value) => Boolean(value))
73
+ )
74
+ ].sort((left, right) => left.localeCompare(right));
75
+ var loadRuntimeTrades = async (userName, {
76
+ startTime,
77
+ endTime
78
+ } = {}) => {
79
+ const hasWindow = Number.isFinite(startTime) && Number.isFinite(endTime) && startTime != null && endTime != null;
80
+ const filterByWindow = (trade) => !hasWindow || trade.entryTimestamp >= startTime && trade.entryTimestamp <= endTime;
81
+ if (hasWindow) {
82
+ const dayKeys = (0, import_time.getRuntimeStorageDayKeys)(startTime, endTime);
83
+ const bucketTrades = (await Promise.all(
84
+ dayKeys.map(
85
+ (dayKey) => (0, import_redis.getHashJsonValues)(
86
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey)
87
+ )
88
+ )
89
+ )).flat();
90
+ const dedupedBucketTrades = /* @__PURE__ */ new Map();
91
+ for (const trade of bucketTrades) {
92
+ if (!isRuntimeTradeRecord(trade)) {
93
+ continue;
94
+ }
95
+ dedupedBucketTrades.set(trade.orderId, trade);
96
+ }
97
+ const windowedTrades = [...dedupedBucketTrades.values()].filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
98
+ if (windowedTrades.length > 0 || dayKeys.length === 0) {
99
+ return windowedTrades;
100
+ }
101
+ }
102
+ const tradePrefix = import_redis.redisKeys.runtimeTrades(userName);
103
+ const bucketPrefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
104
+ const keys = (await (0, import_redis.getKeys)(tradePrefix)).filter(
105
+ (key) => !key.startsWith(bucketPrefix)
106
+ );
107
+ const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
108
+ return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
109
+ };
110
+
111
+ // src/lib/runtimeTradeSync.ts
112
+ var import_constants = require("@tradejs/core/constants");
113
+ var import_time2 = require("@tradejs/core/time");
114
+ var import_redis2 = require("@tradejs/infra/redis");
115
+ var CLOSED_PNL_LIMIT = 100;
116
+ var EXCHANGE_HISTORY_MAX_RANGE_MS = 7 * 24 * 60 * 60 * 1e3;
117
+ var splitExchangeHistoryTimeRange = ({
118
+ startTime,
119
+ endTime,
120
+ maxRangeMs = EXCHANGE_HISTORY_MAX_RANGE_MS
121
+ }) => {
122
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || !Number.isFinite(maxRangeMs) || maxRangeMs <= 0 || endTime <= startTime) {
123
+ return [{ startTime, endTime }];
124
+ }
125
+ const chunks = [];
126
+ let chunkStart = Math.trunc(startTime);
127
+ const finalEnd = Math.trunc(endTime);
128
+ const maxRange = Math.trunc(maxRangeMs);
129
+ while (chunkStart < finalEnd) {
130
+ const chunkEnd = Math.min(finalEnd, chunkStart + maxRange);
131
+ chunks.push({ startTime: chunkStart, endTime: chunkEnd });
132
+ if (chunkEnd >= finalEnd) {
133
+ break;
134
+ }
135
+ chunkStart = chunkEnd + 1;
136
+ }
137
+ return chunks;
138
+ };
139
+ var formatRuntimeTradeSyncError = (error) => {
140
+ if (!error || typeof error !== "object") {
141
+ return String(error || "unknown error");
142
+ }
143
+ const record = error;
144
+ const message = typeof record.message === "string" && record.message.trim() ? record.message.trim() : String(error);
145
+ const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : null;
146
+ const isAxiosError = record.isAxiosError === true;
147
+ const config = record.config && typeof record.config === "object" ? record.config : null;
148
+ const response = record.response && typeof record.response === "object" ? record.response : null;
149
+ const method = typeof config?.method === "string" && config.method.trim() ? config.method.toUpperCase() : null;
150
+ const url = typeof config?.url === "string" && config.url.trim() ? config.url.trim() : null;
151
+ const timeout = typeof config?.timeout === "number" && Number.isFinite(config.timeout) ? config.timeout : null;
152
+ const status = typeof response?.status === "number" && Number.isFinite(response.status) ? response.status : null;
153
+ const details = [
154
+ isAxiosError ? "axios" : null,
155
+ code,
156
+ status == null ? null : `status=${status}`,
157
+ method && url ? `${method} ${url}` : url,
158
+ timeout == null ? null : `timeout=${timeout}ms`
159
+ ].filter(Boolean);
160
+ return details.length > 0 ? `${message} (${details.join(", ")})` : message;
161
+ };
162
+ var loadClosedPnlRows = async ({
163
+ connector,
164
+ startTime,
165
+ endTime,
166
+ callbacks
167
+ }) => {
168
+ if (typeof connector.getClosedPnl !== "function") {
169
+ callbacks?.onUnsupported?.();
170
+ return [];
171
+ }
172
+ const rows = [];
173
+ const chunks = splitExchangeHistoryTimeRange({ startTime, endTime });
174
+ for (const chunk of chunks) {
175
+ try {
176
+ const chunkRows = await connector.getClosedPnl({
177
+ startTime: chunk.startTime,
178
+ endTime: chunk.endTime,
179
+ limit: CLOSED_PNL_LIMIT
180
+ });
181
+ if (chunkRows.length >= CLOSED_PNL_LIMIT) {
182
+ callbacks?.onCapped?.(chunkRows.length);
183
+ }
184
+ rows.push(...chunkRows);
185
+ } catch (error) {
186
+ callbacks?.onError?.(error);
187
+ }
188
+ }
189
+ return rows.sort((left, right) => left.closedAt - right.closedAt);
190
+ };
191
+ var consumeClosedPnlMatch = (buckets, trade) => {
192
+ const rows = buckets.get(trade.symbol);
193
+ if (!rows?.length) {
194
+ return null;
195
+ }
196
+ const minimumClosedAt = trade.entryTimestamp - 5 * 6e4;
197
+ const matchIndex = rows.findIndex(
198
+ (row2) => Number.isFinite(row2.closedAt) && row2.closedAt >= minimumClosedAt
199
+ );
200
+ if (matchIndex < 0) {
201
+ return null;
202
+ }
203
+ const [row] = rows.splice(matchIndex, 1);
204
+ return row ?? null;
205
+ };
206
+ var syncRuntimeTrades = async ({
207
+ userName,
208
+ connector,
209
+ trades,
210
+ startTime,
211
+ endTime,
212
+ closedPnlCallbacks,
213
+ openPositionCallbacks
214
+ }) => {
215
+ let openPositions = [];
216
+ let openPositionsReliable = true;
217
+ if (typeof connector.getOpenPositionPnl === "function") {
218
+ try {
219
+ openPositions = await connector.getOpenPositionPnl();
220
+ } catch (error) {
221
+ openPositionCallbacks?.onError?.(error);
222
+ openPositionsReliable = false;
223
+ openPositions = [];
224
+ }
225
+ }
226
+ const openPositionsBySymbol = new Map(
227
+ openPositions.map((position) => [position.symbol, position])
228
+ );
229
+ const activeOrderIdBySymbol = /* @__PURE__ */ new Map();
230
+ const symbols = [...new Set(trades.map((trade) => trade.symbol))];
231
+ await Promise.all(
232
+ symbols.map(async (symbol) => {
233
+ const activeRef = await (0, import_redis2.getData)(
234
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol),
235
+ null
236
+ );
237
+ activeOrderIdBySymbol.set(
238
+ symbol,
239
+ typeof activeRef?.orderId === "string" ? activeRef.orderId : null
240
+ );
241
+ })
242
+ );
243
+ const closedPnlRows = await loadClosedPnlRows({
244
+ connector,
245
+ startTime,
246
+ endTime,
247
+ callbacks: closedPnlCallbacks
248
+ });
249
+ const closedPnlBuckets = /* @__PURE__ */ new Map();
250
+ for (const row of closedPnlRows) {
251
+ const bucket = closedPnlBuckets.get(row.symbol) ?? [];
252
+ bucket.push(row);
253
+ closedPnlBuckets.set(row.symbol, bucket);
254
+ }
255
+ const syncedTrades = [];
256
+ for (const trade of trades) {
257
+ if (trade.status !== "active") {
258
+ syncedTrades.push(trade);
259
+ continue;
260
+ }
261
+ const openPosition = openPositionsBySymbol.get(trade.symbol);
262
+ const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
263
+ const isCurrentActiveTrade = activeOrderId === trade.orderId;
264
+ if (!openPositionsReliable) {
265
+ syncedTrades.push({
266
+ ...trade,
267
+ status: "active",
268
+ lastSyncedAt: endTime
269
+ });
270
+ continue;
271
+ }
272
+ if (isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
273
+ const nextTrade2 = {
274
+ ...trade,
275
+ status: "active",
276
+ currentPrice: openPosition.currentPrice,
277
+ currentPnl: openPosition.unrealizedPnl,
278
+ lastSyncedAt: endTime
279
+ };
280
+ await (0, import_redis2.setData)(
281
+ import_redis2.redisKeys.runtimeTrade(userName, trade.orderId),
282
+ nextTrade2,
283
+ {
284
+ expire: 0
285
+ }
286
+ );
287
+ await (0, import_redis2.setHashJsonField)(
288
+ import_redis2.redisKeys.runtimeTradeBucket(
289
+ userName,
290
+ (0, import_time2.getRuntimeStorageDayKey)(trade.entryTimestamp)
291
+ ),
292
+ trade.orderId,
293
+ nextTrade2,
294
+ { expire: 0 }
295
+ );
296
+ syncedTrades.push(nextTrade2);
297
+ continue;
298
+ }
299
+ const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
300
+ const nextTrade = {
301
+ ...trade,
302
+ status: "closed",
303
+ currentPrice: matchedClosedPnl?.exitPrice ?? trade.currentPrice ?? null,
304
+ currentPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
305
+ closedPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
306
+ actualEntryPrice: matchedClosedPnl?.entryPrice ?? trade.actualEntryPrice ?? null,
307
+ exitPrice: matchedClosedPnl?.exitPrice ?? trade.exitPrice ?? null,
308
+ actualExitPrice: matchedClosedPnl?.exitPrice ?? trade.actualExitPrice ?? null,
309
+ exitTimestamp: matchedClosedPnl?.closedAt ?? trade.exitTimestamp ?? endTime,
310
+ exitType: trade.exitType ?? null,
311
+ openFee: matchedClosedPnl?.openFee ?? trade.openFee ?? null,
312
+ closeFee: matchedClosedPnl?.closeFee ?? trade.closeFee ?? null,
313
+ fundingFee: matchedClosedPnl?.fundingFee ?? trade.fundingFee ?? null,
314
+ totalFee: matchedClosedPnl?.totalFee ?? trade.totalFee ?? null,
315
+ lastSyncedAt: endTime
316
+ };
317
+ await Promise.all([
318
+ (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
319
+ expire: import_constants.TTL_1M
320
+ }),
321
+ (0, import_redis2.setHashJsonField)(
322
+ import_redis2.redisKeys.runtimeTradeBucket(
323
+ userName,
324
+ (0, import_time2.getRuntimeStorageDayKey)(trade.entryTimestamp)
325
+ ),
326
+ trade.orderId,
327
+ nextTrade,
328
+ { expire: import_constants.TTL_1M }
329
+ ),
330
+ ...isCurrentActiveTrade ? [(0, import_redis2.delKey)(import_redis2.redisKeys.runtimeActiveTrade(userName, trade.symbol))] : []
331
+ ]);
332
+ syncedTrades.push(nextTrade);
333
+ }
334
+ return syncedTrades;
335
+ };
336
+
43
337
  // src/lib/telegramReports.ts
44
338
  var import_cli = require("@tradejs/node/cli");
45
339
  var TELEGRAM_REPORT_CHUNK_LIMIT = 3900;
@@ -84,8 +378,16 @@ ${line}` : line;
84
378
  };
85
379
  var sendTelegramReport = async (message, options = {}) => {
86
380
  const chunks = splitTelegramReport(message);
381
+ const sendAttachments = async () => {
382
+ for (const attachment of options.attachments || []) {
383
+ await (0, import_cli.sendDocumentToTG)(attachment, {
384
+ userName: options.userName
385
+ });
386
+ }
387
+ };
87
388
  if (chunks.length === 1) {
88
389
  await (0, import_cli.sendTextToTG)(chunks[0], options);
390
+ await sendAttachments();
89
391
  return chunks.length;
90
392
  }
91
393
  for (const [index, chunk] of chunks.entries()) {
@@ -97,9 +399,343 @@ ${chunk}`, {
97
399
  markup: index === chunks.length - 1 ? options.markup : void 0
98
400
  });
99
401
  }
402
+ await sendAttachments();
100
403
  return chunks.length;
101
404
  };
102
405
 
406
+ // src/lib/runtimeSignalsLoader.ts
407
+ var import_redis3 = require("@tradejs/infra/redis");
408
+
409
+ // src/lib/runtimeSignalsStorage.ts
410
+ var import_constants2 = require("@tradejs/core/constants");
411
+ var import_time3 = require("@tradejs/core/time");
412
+ var isRuntimeSignalBucketRef = (value) => {
413
+ if (!value || typeof value !== "object") {
414
+ return false;
415
+ }
416
+ const record = value;
417
+ return typeof record.signalId === "string" && typeof record.symbol === "string" && typeof record.strategy === "string" && typeof record.timestamp === "number";
418
+ };
419
+ var toCount = (value) => {
420
+ const parsed = Number.parseInt(String(value ?? "0"), 10);
421
+ return Number.isFinite(parsed) ? parsed : 0;
422
+ };
423
+ var parseRuntimeSignalStatsBucket = (value) => {
424
+ const reasonGroups = /* @__PURE__ */ new Map();
425
+ for (const [field, rawCount] of Object.entries(value)) {
426
+ if (!field.startsWith("reason:")) {
427
+ continue;
428
+ }
429
+ const [, source, ...reasonParts] = field.split(":");
430
+ const reason = reasonParts.join(":");
431
+ if (!source || !reason) {
432
+ continue;
433
+ }
434
+ const reasons = reasonGroups.get(source) ?? /* @__PURE__ */ new Map();
435
+ reasons.set(reason, toCount(rawCount));
436
+ reasonGroups.set(source, reasons);
437
+ }
438
+ return {
439
+ evaluated: toCount(value.evaluated),
440
+ signals: toCount(value.signals),
441
+ reasonGroups
442
+ };
443
+ };
444
+
445
+ // src/lib/runtimeSignalsLoader.ts
446
+ var isSignalRecord = (value) => {
447
+ if (!value || typeof value !== "object") {
448
+ return false;
449
+ }
450
+ const record = value;
451
+ return typeof record.signalId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number";
452
+ };
453
+ var sortByTimestamp = (records) => [...records].sort((left, right) => left.timestamp - right.timestamp);
454
+ var filterBucketKeysByWindow = (bucketKeys, prefix, startTime, endTime) => {
455
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime == null || endTime == null) {
456
+ return bucketKeys;
457
+ }
458
+ const allowedDayKeys = new Set((0, import_time3.getRuntimeStorageDayKeys)(startTime, endTime));
459
+ if (!allowedDayKeys.size) {
460
+ return [];
461
+ }
462
+ return bucketKeys.filter((key) => {
463
+ const suffix = key.slice(prefix.length);
464
+ const firstColon = suffix.indexOf(":");
465
+ if (firstColon <= 0) {
466
+ return false;
467
+ }
468
+ return allowedDayKeys.has(suffix.slice(0, firstColon));
469
+ });
470
+ };
471
+ var loadRuntimeSignals = async (userName, {
472
+ startTime,
473
+ endTime
474
+ } = {}) => {
475
+ const bucketPrefix = import_redis3.redisKeys.runtimeSignalBuckets(userName);
476
+ const bucketKeys = filterBucketKeysByWindow(
477
+ await (0, import_redis3.getKeys)(bucketPrefix),
478
+ bucketPrefix,
479
+ startTime,
480
+ endTime
481
+ );
482
+ const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis3.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
483
+ const seen = /* @__PURE__ */ new Set();
484
+ const payloads = await Promise.all(
485
+ refs.map(async (ref) => {
486
+ const dedupeKey = `${ref.symbol}:${ref.signalId}`;
487
+ if (seen.has(dedupeKey)) {
488
+ return null;
489
+ }
490
+ seen.add(dedupeKey);
491
+ const signal = await (0, import_redis3.getData)(
492
+ import_redis3.redisKeys.storeSignal(ref.symbol, ref.signalId),
493
+ null
494
+ );
495
+ return isSignalRecord(signal) ? signal : null;
496
+ })
497
+ );
498
+ return sortByTimestamp(payloads.filter(isSignalRecord));
499
+ };
500
+ var parseBucketKey = (prefix, key) => {
501
+ if (!key.startsWith(prefix)) {
502
+ return null;
503
+ }
504
+ const suffix = key.slice(prefix.length);
505
+ const firstColon = suffix.indexOf(":");
506
+ if (firstColon <= 0 || firstColon >= suffix.length - 1) {
507
+ return null;
508
+ }
509
+ return {
510
+ dayKey: suffix.slice(0, firstColon),
511
+ strategy: suffix.slice(firstColon + 1)
512
+ };
513
+ };
514
+ var loadRuntimeSignalEvaluationStatsBuckets = async (userName) => {
515
+ const prefix = import_redis3.redisKeys.runtimeSignalEvaluationStatsBuckets(userName);
516
+ const keys = await (0, import_redis3.getKeys)(prefix);
517
+ const entries = await Promise.all(
518
+ keys.map(async (key) => {
519
+ const parsedKey = parseBucketKey(prefix, key);
520
+ if (!parsedKey) {
521
+ return null;
522
+ }
523
+ return {
524
+ key,
525
+ ...parsedKey,
526
+ stats: parseRuntimeSignalStatsBucket(await (0, import_redis3.getHashData)(key))
527
+ };
528
+ })
529
+ );
530
+ return entries.filter(Boolean);
531
+ };
532
+
533
+ // src/lib/runtimeDebugEvidence.ts
534
+ var import_redis4 = require("@tradejs/infra/redis");
535
+ var RUNTIME_DEBUG_TIMEZONE = "Europe/Moscow";
536
+ var RUNTIME_DEBUG_TIMEZONE_LABEL = "MSK";
537
+ var formatRuntimeDebugDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
538
+ timeZone: RUNTIME_DEBUG_TIMEZONE,
539
+ year: "numeric",
540
+ month: "2-digit",
541
+ day: "2-digit",
542
+ hour: "2-digit",
543
+ minute: "2-digit",
544
+ hour12: false
545
+ }).format(new Date(timestamp));
546
+ var getRuntimeDebugDateParts = (timestamp) => Object.fromEntries(
547
+ new Intl.DateTimeFormat("en-US", {
548
+ timeZone: RUNTIME_DEBUG_TIMEZONE,
549
+ year: "numeric",
550
+ month: "2-digit",
551
+ day: "2-digit"
552
+ }).formatToParts(new Date(timestamp)).map((part) => [part.type, part.value])
553
+ );
554
+ var formatRuntimeDebugIsoDayKey = (timestamp) => {
555
+ const parts = getRuntimeDebugDateParts(timestamp);
556
+ return `${parts.year}-${parts.month}-${parts.day}`;
557
+ };
558
+ var buildRuntimeDebugEvaluationId = ({
559
+ strategy,
560
+ symbol,
561
+ timestamp
562
+ }) => `${strategy}:${symbol}:${timestamp}`;
563
+ var buildRuntimeDebugReportPayload = async ({
564
+ userName,
565
+ startTime,
566
+ endTime,
567
+ signals,
568
+ evaluations,
569
+ trades,
570
+ strategyConfigs,
571
+ evaluationStatsBuckets
572
+ }) => {
573
+ const signalById = new Map(
574
+ signals.map((signal) => [signal.signalId, signal])
575
+ );
576
+ const evaluationById = new Map(
577
+ evaluations.map((evaluation) => [evaluation.evaluationId, evaluation])
578
+ );
579
+ const evaluationBySignalShape = new Map(
580
+ evaluations.map((evaluation) => [
581
+ buildRuntimeDebugEvaluationId(evaluation),
582
+ evaluation
583
+ ])
584
+ );
585
+ const dayKeys = (0, import_time3.getRuntimeStorageDayKeys)(startTime, endTime);
586
+ const debugTrades = await Promise.all(
587
+ trades.map(async (trade) => {
588
+ const signal = trade.signalId != null ? signalById.get(trade.signalId) : void 0;
589
+ const evaluationId = buildRuntimeDebugEvaluationId({
590
+ strategy: trade.strategy,
591
+ symbol: trade.symbol,
592
+ timestamp: signal?.timestamp ?? trade.entryTimestamp
593
+ });
594
+ const knownEvaluation = evaluationById.get(evaluationId) ?? evaluationBySignalShape.get(evaluationId);
595
+ const tradeDayKey = (0, import_time3.getRuntimeStorageDayKey)(trade.entryTimestamp);
596
+ const signalDayKey = signal != null ? (0, import_time3.getRuntimeStorageDayKey)(signal.timestamp) : tradeDayKey;
597
+ const evaluationDayKey = knownEvaluation != null ? (0, import_time3.getRuntimeStorageDayKey)(knownEvaluation.timestamp) : signalDayKey;
598
+ const tradeKey = import_redis4.redisKeys.runtimeTrade(userName, trade.orderId);
599
+ const tradeBucketKey = import_redis4.redisKeys.runtimeTradeBucket(
600
+ userName,
601
+ tradeDayKey
602
+ );
603
+ const activeTradeKey = import_redis4.redisKeys.runtimeActiveTrade(
604
+ userName,
605
+ trade.symbol
606
+ );
607
+ const signalKey = trade.signalId != null ? import_redis4.redisKeys.storeSignal(trade.symbol, trade.signalId) : null;
608
+ const evaluationBucketKey = import_redis4.redisKeys.runtimeSignalEvaluationBucket(
609
+ userName,
610
+ evaluationDayKey,
611
+ knownEvaluation?.strategy ?? trade.strategy
612
+ );
613
+ const evaluationField = knownEvaluation?.evaluationId ?? evaluationId;
614
+ const evaluationKey = import_redis4.redisKeys.runtimeSignalEvaluation(
615
+ userName,
616
+ evaluationField
617
+ );
618
+ const [
619
+ tradeValue,
620
+ activeTradeValue,
621
+ signalValue,
622
+ directEvaluationValue,
623
+ bucketEvaluationValue
624
+ ] = await Promise.all([
625
+ (0, import_redis4.getData)(tradeKey, null),
626
+ (0, import_redis4.getData)(activeTradeKey, null),
627
+ signalKey ? (0, import_redis4.getData)(signalKey, null) : Promise.resolve(null),
628
+ (0, import_redis4.getData)(evaluationKey, null),
629
+ knownEvaluation ? Promise.resolve(knownEvaluation) : (0, import_redis4.getHashJsonField)(
630
+ evaluationBucketKey,
631
+ evaluationField,
632
+ null
633
+ )
634
+ ]);
635
+ const evaluationValue = directEvaluationValue ?? bucketEvaluationValue ?? knownEvaluation ?? null;
636
+ return {
637
+ redisDebug: {
638
+ trade: tradeKey,
639
+ tradeBucket: {
640
+ key: tradeBucketKey,
641
+ field: trade.orderId
642
+ },
643
+ activeTrade: activeTradeKey,
644
+ signal: signalKey,
645
+ evaluation: {
646
+ key: evaluationBucketKey,
647
+ field: evaluationField,
648
+ directKey: evaluationKey
649
+ }
650
+ },
651
+ trade,
652
+ redisValues: {
653
+ trade: tradeValue,
654
+ tradeBucket: trade,
655
+ activeTrade: activeTradeValue,
656
+ signal: signalValue ?? signal ?? null,
657
+ evaluation: evaluationValue ?? knownEvaluation ?? null
658
+ }
659
+ };
660
+ })
661
+ );
662
+ const linkedEvaluations = /* @__PURE__ */ new Map();
663
+ for (const trade of debugTrades) {
664
+ const evaluation = trade.redisValues.evaluation;
665
+ if (evaluation && typeof evaluation === "object" && "evaluationId" in evaluation && typeof evaluation.evaluationId === "string") {
666
+ linkedEvaluations.set(
667
+ evaluation.evaluationId,
668
+ evaluation
669
+ );
670
+ }
671
+ }
672
+ const reportEvaluations = evaluations.length > 0 ? evaluations : [...linkedEvaluations.values()];
673
+ return {
674
+ reportType: "runtime-daily-debug",
675
+ generatedAt: Date.now(),
676
+ generatedAtMsk: formatRuntimeDebugDateTime(Date.now()),
677
+ userName,
678
+ window: {
679
+ startTime,
680
+ endTime,
681
+ startMsk: formatRuntimeDebugDateTime(startTime),
682
+ endMsk: formatRuntimeDebugDateTime(endTime),
683
+ dayKeys
684
+ },
685
+ redisPrefixes: {
686
+ runtimeTrades: import_redis4.redisKeys.runtimeTrades(userName),
687
+ runtimeTradeBuckets: import_redis4.redisKeys.runtimeTradeBuckets(userName),
688
+ runtimeActiveTrades: import_redis4.redisKeys.runtimeActiveTrades(userName),
689
+ runtimeSignals: import_redis4.redisKeys.runtimeSignalBuckets(userName),
690
+ runtimeSignalEvaluations: import_redis4.redisKeys.runtimeSignalEvaluationBuckets(userName),
691
+ runtimeSignalEvaluationStats: import_redis4.redisKeys.runtimeSignalEvaluationStatsBuckets(userName)
692
+ },
693
+ counts: {
694
+ trades: trades.length,
695
+ signals: signals.length,
696
+ evaluations: reportEvaluations.length,
697
+ evaluationStatsBuckets: evaluationStatsBuckets?.length ?? 0,
698
+ strategyConfigs: strategyConfigs?.length ?? 0
699
+ },
700
+ trades: debugTrades,
701
+ signals: signals.map((signal) => ({
702
+ redisKey: import_redis4.redisKeys.storeSignal(signal.symbol, signal.signalId),
703
+ signal
704
+ })),
705
+ evaluations: reportEvaluations.map((evaluation) => ({
706
+ redisDebug: {
707
+ key: import_redis4.redisKeys.runtimeSignalEvaluationBucket(
708
+ userName,
709
+ (0, import_time3.getRuntimeStorageDayKey)(evaluation.timestamp),
710
+ evaluation.strategy
711
+ ),
712
+ field: evaluation.evaluationId,
713
+ directKey: import_redis4.redisKeys.runtimeSignalEvaluation(
714
+ userName,
715
+ evaluation.evaluationId
716
+ )
717
+ },
718
+ evaluation
719
+ })),
720
+ evaluationStatsBuckets: evaluationStatsBuckets ?? [],
721
+ strategyConfigs: strategyConfigs ?? []
722
+ };
723
+ };
724
+ var buildRuntimeDebugReportAttachment = async (params) => {
725
+ const payload = await buildRuntimeDebugReportPayload(params);
726
+ const dayKey = formatRuntimeDebugIsoDayKey(params.endTime);
727
+ return {
728
+ filename: `tradejs-runtime-debug-${params.userName}-${dayKey}.json`,
729
+ content: JSON.stringify(payload, null, 2),
730
+ caption: `Runtime debug ${dayKey} ${RUNTIME_DEBUG_TIMEZONE_LABEL}`,
731
+ summary: {
732
+ trades: payload.counts.trades,
733
+ signals: payload.counts.signals,
734
+ evaluations: payload.counts.evaluations
735
+ }
736
+ };
737
+ };
738
+
103
739
  // src/scripts/signalsSummary.ts
104
740
  import_args.default.option(["u", "user"], "Use user config", "root");
105
741
  import_args.default.option(
@@ -109,6 +745,11 @@ import_args.default.option(
109
745
  );
110
746
  import_args.default.option(["H", "hours"], "Summary window in hours", 24);
111
747
  import_args.default.option(["P", "printOnly"], "Print summary instead of Telegram", false);
748
+ import_args.default.option(
749
+ "debugAttachment",
750
+ "Attach runtime Redis debug JSON for replay diagnostics",
751
+ true
752
+ );
112
753
  var flags = import_args.default.parse(process.argv);
113
754
  var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
114
755
  var SUMMARY_TIMEZONE = "Europe/Moscow";
@@ -124,6 +765,8 @@ var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
124
765
  hour12: false
125
766
  }).format(new Date(timestamp));
126
767
  var formatSigned = (value) => `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
768
+ var formatPnlMoneyText = (value, knownCount) => knownCount > 0 ? `${formatSigned(value)}$` : "n/a";
769
+ var formatWinRateText = (wins, closedKnown) => closedKnown > 0 ? `${(wins / closedKnown * 100).toFixed(2)}% (${wins}/${closedKnown})` : "n/a";
127
770
  var resolveSummaryTitle = (hours) => {
128
771
  if (hours === 24) {
129
772
  return "TradeJS daily summary";
@@ -131,6 +774,9 @@ var resolveSummaryTitle = (hours) => {
131
774
  if (hours === 168) {
132
775
  return "TradeJS weekly summary";
133
776
  }
777
+ if (hours === 720) {
778
+ return "TradeJS monthly summary";
779
+ }
134
780
  return `TradeJS ${hours}h summary`;
135
781
  };
136
782
  var normalizeStatus = (value) => {
@@ -139,57 +785,57 @@ var normalizeStatus = (value) => {
139
785
  }
140
786
  return "unknown";
141
787
  };
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
- }
788
+ var shouldAttachDebugReport = (value) => value !== false && String(value).toLowerCase() !== "false";
789
+ var getSummaryDateParts = (timestamp) => Object.fromEntries(
790
+ new Intl.DateTimeFormat("en-US", {
791
+ timeZone: SUMMARY_TIMEZONE,
792
+ year: "numeric",
793
+ month: "2-digit",
794
+ day: "2-digit"
795
+ }).formatToParts(new Date(timestamp)).map((part) => [part.type, part.value])
796
+ );
797
+ var formatSummaryIsoDayKey = (timestamp) => {
798
+ const parts = getSummaryDateParts(timestamp);
799
+ return `${parts.year}-${parts.month}-${parts.day}`;
800
+ };
801
+ var unescapeHtml = (value) => value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
802
+ var stripTelegramHtml = (value) => unescapeHtml(value.replace(/<\/?(?:b|code)>/g, ""));
803
+ var buildSignalsSummaryAttachment = ({
804
+ userName,
805
+ endTime,
806
+ content
807
+ }) => {
808
+ const dayKey = formatSummaryIsoDayKey(endTime);
168
809
  return {
169
- source: `skip from ${source}`,
170
- reason: normalizedReason
810
+ filename: `tradejs-signals-summary-${userName}-${dayKey}.txt`,
811
+ content: stripTelegramHtml(content),
812
+ caption: `Signals summary ${dayKey} ${SUMMARY_TIMEZONE_LABEL}`
171
813
  };
172
814
  };
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;
815
+ var appendReportAttachmentSummary = ({
816
+ message,
817
+ signalsFilename,
818
+ filename,
819
+ tradesCount,
820
+ signalsCount,
821
+ evaluationsCount
822
+ }) => {
823
+ const lines = [
824
+ message,
825
+ "",
826
+ "\u{1F4CE} <b>Signal report file</b>",
827
+ `File: <code>${escapeHtml(signalsFilename)}</code>`
828
+ ];
829
+ if (filename) {
830
+ lines.push(
831
+ "",
832
+ "\u{1F4CE} <b>Replay debug file</b>",
833
+ `File: <code>${escapeHtml(filename)}</code>`,
834
+ `Inside: trades=<b>${tradesCount ?? 0}</b>, signals=<b>${signalsCount ?? 0}</b>, evaluations=<b>${evaluationsCount ?? 0}</b>`,
835
+ "Redis refs: <code>trade</code>, <code>tradeBucket</code>, <code>activeTrade</code>, <code>signal</code>, <code>evaluation</code>"
836
+ );
190
837
  }
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";
838
+ return lines.join("\n");
193
839
  };
194
840
  var resolveSummaryConnectorName = async (value) => {
195
841
  const connectorName = await (0, import_connectors.resolveConnectorName)(value, projectRoot);
@@ -203,168 +849,75 @@ var resolveSummaryConnectorName = async (value) => {
203
849
  );
204
850
  return import_connectors.DEFAULT_CONNECTOR_NAME;
205
851
  };
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;
852
+ var createRuntimeSignalStats = () => ({
853
+ evaluated: 0,
854
+ signals: 0,
855
+ reasonGroups: /* @__PURE__ */ new Map()
856
+ });
857
+ var mergeRuntimeSignalStats = (target, source) => {
858
+ target.evaluated += source.evaluated;
859
+ target.signals += source.signals;
860
+ for (const [group, reasons] of source.reasonGroups.entries()) {
861
+ const targetReasons = target.reasonGroups.get(group) ?? /* @__PURE__ */ new Map();
862
+ for (const [reason, count] of reasons.entries()) {
863
+ targetReasons.set(reason, (targetReasons.get(reason) ?? 0) + count);
864
+ }
865
+ target.reasonGroups.set(group, targetReasons);
224
866
  }
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
867
  };
236
- var loadClosedPnlRows = async ({
237
- connector,
868
+ var buildSummaryPrelude = ({
869
+ hours,
238
870
  startTime,
239
- endTime
871
+ endTime,
872
+ realizedWindowPnlText,
873
+ winRateText,
874
+ longCount,
875
+ shortCount,
876
+ openCount,
877
+ openPnlText
240
878
  }) => {
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
- }
879
+ const windowPnlLabel = hours === 24 ? "24h Realized PnL" : `${hours}h Realized PnL`;
880
+ return [
881
+ `\u{1F4CB} <b>${escapeHtml(resolveSummaryTitle(hours))}</b>`,
882
+ "",
883
+ "\u{1F552} <b>Window</b>",
884
+ `<b>${escapeHtml(formatMskDateTime(startTime))} - ${escapeHtml(formatMskDateTime(endTime))} ${SUMMARY_TIMEZONE_LABEL}</b>`,
885
+ "",
886
+ `\u23F1 Range: <b>${hours}h</b>`,
887
+ `\u{1F4B0} <b>${windowPnlLabel}:</b> <b>${escapeHtml(realizedWindowPnlText)}</b>`,
888
+ `\u{1F3C6} <b>WinRate:</b> <b>${escapeHtml(winRateText)}</b>`,
889
+ `\u2197\uFE0F <b>LONG:</b> <b>${longCount}</b>, \u2198\uFE0F <b>SHORT:</b> <b>${shortCount}</b>`,
890
+ `\u{1F4CD} <b>Open positions:</b> <b>${openCount}</b>, Unrealized PnL: <b>${escapeHtml(openPnlText)}</b>`
891
+ ];
258
892
  };
259
- var consumeClosedPnlMatch = (buckets, trade) => {
260
- const rows = buckets.get(trade.symbol);
261
- if (!rows?.length) {
262
- return null;
893
+ var resolveTradeStatusEmoji = (status, pnl) => {
894
+ if (status === "active") {
895
+ return typeof pnl === "number" && Number.isFinite(pnl) && pnl < 0 ? "\u{1F534}" : "\u{1F7E2}";
263
896
  }
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;
897
+ if (status === "closed") {
898
+ return typeof pnl === "number" && Number.isFinite(pnl) && pnl < 0 ? "\u274C" : "\u2705";
270
899
  }
271
- const [row] = rows.splice(matchIndex, 1);
272
- return row ?? null;
900
+ return "\u2754";
273
901
  };
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);
902
+ var isTimestampInWindow = (timestamp, startTime, endTime) => typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp >= startTime && timestamp < endTime;
903
+ var isRuntimeTradeClosedInWindow = (trade, startTime, endTime) => {
904
+ if (trade.status !== "closed" || !isTimestampInWindow(trade.exitTimestamp, startTime, endTime)) {
905
+ return false;
355
906
  }
356
- return syncedTrades;
907
+ const looksLikeSyncFallbackClose = trade.exitTimestamp === trade.lastSyncedAt && trade.exitPrice == null && trade.actualExitPrice == null && trade.closeFee == null && trade.fundingFee == null && trade.totalFee == null;
908
+ return !looksLikeSyncFallbackClose;
357
909
  };
358
- var buildSummaryMessage = ({
910
+ var buildSummaryMessages = ({
359
911
  hours,
360
912
  startTime,
361
913
  endTime,
362
914
  configuredStrategyNames,
363
915
  signals,
364
- signalEvaluations,
916
+ evaluationStatsByStrategy,
365
917
  trades
366
918
  }) => {
367
- const lines = [];
919
+ const signalLines = [];
920
+ const tradeLines = [];
368
921
  const statusOrder = [
369
922
  "completed",
370
923
  "skipped",
@@ -374,7 +927,6 @@ var buildSummaryMessage = ({
374
927
  ];
375
928
  const strategyNames = /* @__PURE__ */ new Set();
376
929
  const signalStats = /* @__PURE__ */ new Map();
377
- const evaluationStats = /* @__PURE__ */ new Map();
378
930
  const tradeStats = /* @__PURE__ */ new Map();
379
931
  for (const strategyName of configuredStrategyNames) {
380
932
  if (strategyName) {
@@ -388,75 +940,49 @@ var buildSummaryMessage = ({
388
940
  stats.set(status, (stats.get(status) ?? 0) + 1);
389
941
  signalStats.set(signal.strategy, stats);
390
942
  }
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) {
943
+ const realizedTrades = trades.filter(
944
+ (trade) => isRuntimeTradeClosedInWindow(trade, startTime, endTime)
945
+ );
946
+ const openTrades = trades.filter(
947
+ (trade) => trade.status === "active" && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp) && trade.entryTimestamp < endTime
948
+ );
949
+ const reportTrades = [...realizedTrades, ...openTrades];
950
+ for (const trade of reportTrades) {
424
951
  strategyNames.add(trade.strategy);
425
952
  const stats = tradeStats.get(trade.strategy) ?? {
426
- total: 0,
427
953
  active: 0,
428
954
  closed: 0,
429
955
  activePnl: 0,
430
956
  activePnlKnown: 0,
431
957
  closedPnl: 0,
432
958
  closedPnlKnown: 0,
433
- totalPnl: 0,
434
- totalPnlKnown: 0,
435
- trades: []
959
+ trades: [],
960
+ closedWins: 0,
961
+ closedKnown: 0
436
962
  };
437
963
  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;
964
+ const pnlText = typeof pnl === "number" && Number.isFinite(pnl) ? `${formatSigned(pnl)}$` : "n/a";
440
965
  if (trade.status === "active") {
441
966
  stats.active += 1;
442
967
  if (typeof pnl === "number" && Number.isFinite(pnl)) {
443
968
  stats.activePnl += pnl;
444
969
  stats.activePnlKnown += 1;
445
- stats.totalPnl += pnl;
446
- stats.totalPnlKnown += 1;
447
970
  }
448
971
  } else {
449
972
  stats.closed += 1;
450
973
  if (typeof pnl === "number" && Number.isFinite(pnl)) {
451
974
  stats.closedPnl += pnl;
452
975
  stats.closedPnlKnown += 1;
453
- stats.totalPnl += pnl;
454
- stats.totalPnlKnown += 1;
976
+ stats.closedKnown += 1;
977
+ if (pnl > 0) {
978
+ stats.closedWins += 1;
979
+ }
455
980
  }
456
981
  }
457
982
  stats.trades.push({
458
983
  symbol: trade.symbol,
459
984
  status: trade.status,
985
+ pnl: typeof pnl === "number" && Number.isFinite(pnl) ? pnl : null,
460
986
  pnlText,
461
987
  entryTimestamp: trade.entryTimestamp,
462
988
  orderId: trade.orderId
@@ -466,39 +992,70 @@ var buildSummaryMessage = ({
466
992
  const sortedStrategies = [...strategyNames].sort(
467
993
  (left, right) => left.localeCompare(right)
468
994
  );
469
- const totalWindowPnl = [...tradeStats.values()].reduce(
470
- (sum, stats) => sum + stats.totalPnl,
995
+ const realizedWindowPnl = [...tradeStats.values()].reduce(
996
+ (sum, stats) => sum + stats.closedPnl,
997
+ 0
998
+ );
999
+ const realizedWindowPnlKnown = [...tradeStats.values()].reduce(
1000
+ (sum, stats) => sum + stats.closedPnlKnown,
1001
+ 0
1002
+ );
1003
+ const realizedWindowPnlText = realizedWindowPnlKnown > 0 ? formatSigned(realizedWindowPnl) : "n/a";
1004
+ const longCount = realizedTrades.filter(
1005
+ (trade) => trade.direction === "LONG"
1006
+ ).length;
1007
+ const shortCount = realizedTrades.filter(
1008
+ (trade) => trade.direction === "SHORT"
1009
+ ).length;
1010
+ const openPnl = [...tradeStats.values()].reduce(
1011
+ (sum, stats) => sum + stats.activePnl,
471
1012
  0
472
1013
  );
473
- const totalWindowPnlKnown = [...tradeStats.values()].reduce(
474
- (sum, stats) => sum + stats.totalPnlKnown,
1014
+ const openPnlKnown = [...tradeStats.values()].reduce(
1015
+ (sum, stats) => sum + stats.activePnlKnown,
475
1016
  0
476
1017
  );
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>`
1018
+ const openPnlText = openTrades.length > 0 ? formatPnlMoneyText(openPnl, openPnlKnown) : "n/a";
1019
+ const closedWins = [...tradeStats.values()].reduce(
1020
+ (sum, stats) => sum + stats.closedWins,
1021
+ 0
484
1022
  );
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>`
1023
+ const closedKnown = [...tradeStats.values()].reduce(
1024
+ (sum, stats) => sum + stats.closedKnown,
1025
+ 0
489
1026
  );
490
- lines.push("");
491
- lines.push("\u{1F4E1} <b>Signals</b>");
1027
+ const winRateText = formatWinRateText(closedWins, closedKnown);
1028
+ const prelude = buildSummaryPrelude({
1029
+ hours,
1030
+ startTime,
1031
+ endTime,
1032
+ realizedWindowPnlText,
1033
+ winRateText,
1034
+ longCount,
1035
+ shortCount,
1036
+ openCount: openTrades.length,
1037
+ openPnlText
1038
+ });
1039
+ signalLines.push(...prelude);
1040
+ signalLines.push("");
1041
+ signalLines.push("\u{1F4E1} <b>Signals</b>");
1042
+ tradeLines.push(...prelude);
1043
+ tradeLines.push("");
1044
+ tradeLines.push("\u{1F4BC} <b>Closed Trades</b>");
492
1045
  if (!sortedStrategies.length) {
493
- lines.push("\u26A0\uFE0F No runtime data for this window.");
494
- return lines.join("\n");
1046
+ signalLines.push("\u26A0\uFE0F No runtime data for this window.");
1047
+ tradeLines.push("\u26A0\uFE0F No runtime data for this window.");
1048
+ return {
1049
+ signalsMessage: signalLines.join("\n"),
1050
+ tradesMessage: tradeLines.join("\n")
1051
+ };
495
1052
  }
496
1053
  const appendEvaluationDetails = (strategyName) => {
497
- const evaluation = evaluationStats.get(strategyName);
1054
+ const evaluation = evaluationStatsByStrategy.get(strategyName);
498
1055
  if (!evaluation) {
499
1056
  return;
500
1057
  }
501
- lines.push(
1058
+ signalLines.push(
502
1059
  `evaluated=<b>${evaluation.evaluated}</b>, signals=<b>${evaluation.signals}</b>`
503
1060
  );
504
1061
  const sourceOrder = ["skip from core", "skip from AI", "skip from ML"];
@@ -512,21 +1069,21 @@ var buildSummaryMessage = ({
512
1069
  }
513
1070
  );
514
1071
  for (const [source, reasons] of sortedReasonGroups) {
515
- lines.push(`<b>${escapeHtml(source)}</b>:`);
1072
+ signalLines.push(`<b>${escapeHtml(source)}</b>:`);
516
1073
  const sortedReasons = [...reasons.entries()].sort(
517
1074
  (left, right) => right[1] - left[1] || left[0].localeCompare(right[0])
518
1075
  );
519
1076
  for (const [reason, count] of sortedReasons) {
520
- lines.push(`${escapeHtml(reason)}: <b>${count}</b>`);
1077
+ signalLines.push(`${escapeHtml(reason)}: <b>${count}</b>`);
521
1078
  }
522
1079
  }
523
1080
  };
524
1081
  for (const strategyName of sortedStrategies) {
525
1082
  const stats = signalStats.get(strategyName);
526
1083
  if (!stats || stats.size === 0) {
527
- const evaluation = evaluationStats.get(strategyName);
528
- lines.push("");
529
- lines.push(
1084
+ const evaluation = evaluationStatsByStrategy.get(strategyName);
1085
+ signalLines.push("");
1086
+ signalLines.push(
530
1087
  evaluation?.evaluated ? `<b>${escapeHtml(strategyName)}</b>
531
1088
  signals=<b>0</b>` : `<b>${escapeHtml(strategyName)}</b>
532
1089
  none`
@@ -538,42 +1095,62 @@ none`
538
1095
  const count = stats.get(status) ?? 0;
539
1096
  return count > 0 ? `${status}=<b>${count}</b>` : null;
540
1097
  }).filter(Boolean);
541
- lines.push("");
542
- lines.push(`<b>${escapeHtml(strategyName)}</b>`);
543
- lines.push(parts.join(", "));
1098
+ signalLines.push("");
1099
+ signalLines.push(`<b>${escapeHtml(strategyName)}</b>`);
1100
+ signalLines.push(parts.join(", "));
544
1101
  appendEvaluationDetails(strategyName);
545
1102
  }
546
- lines.push("");
547
- lines.push("\u{1F4BC} <b>Trades</b>");
548
- for (const strategyName of sortedStrategies) {
1103
+ const sortedTradeStrategies = [...tradeStats.entries()].filter(([, stats]) => stats.closed > 0).map(([strategyName]) => strategyName).sort((left, right) => left.localeCompare(right));
1104
+ if (sortedTradeStrategies.length === 0) {
1105
+ tradeLines.push("none");
1106
+ }
1107
+ for (const strategyName of sortedTradeStrategies) {
549
1108
  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;
1109
+ if (!stats) continue;
1110
+ tradeLines.push("");
1111
+ tradeLines.push(
1112
+ `<b>${escapeHtml(strategyName)}</b> (<b>${stats.closed}</b>)`
1113
+ );
1114
+ tradeLines.push(
1115
+ `Closed PnL: <b>${escapeHtml(formatPnlMoneyText(stats.closedPnl, stats.closedPnlKnown))}</b> (<b>${stats.closed}</b>)`
1116
+ );
1117
+ const sortedTrades = stats.trades.filter((trade) => trade.status === "closed").sort(
1118
+ (left, right) => left.entryTimestamp - right.entryTimestamp || left.symbol.localeCompare(right.symbol) || left.orderId.localeCompare(right.orderId)
1119
+ );
1120
+ for (const trade of sortedTrades) {
1121
+ tradeLines.push(
1122
+ `- ${escapeHtml(trade.symbol)}: PnL <b>${escapeHtml(trade.pnlText)}</b> ${resolveTradeStatusEmoji(trade.status, trade.pnl)}`
1123
+ );
555
1124
  }
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>)`
1125
+ }
1126
+ const sortedOpenStrategies = [...tradeStats.entries()].filter(([, stats]) => stats.active > 0).map(([strategyName]) => strategyName).sort((left, right) => left.localeCompare(right));
1127
+ if (sortedOpenStrategies.length > 0) {
1128
+ tradeLines.push("");
1129
+ tradeLines.push("\u{1F4CD} <b>Open Positions</b>");
1130
+ }
1131
+ for (const strategyName of sortedOpenStrategies) {
1132
+ const stats = tradeStats.get(strategyName);
1133
+ if (!stats) continue;
1134
+ tradeLines.push("");
1135
+ tradeLines.push(
1136
+ `<b>${escapeHtml(strategyName)}</b> (<b>${stats.active}</b>)`
563
1137
  );
564
- lines.push(
565
- `closed=<b>${stats.closed}</b> (PnL <b>${escapeHtml(closedPnlText)}</b>), totalPnL=<b>${escapeHtml(totalPnlText)}</b>`
1138
+ tradeLines.push(
1139
+ `Unrealized PnL: <b>${escapeHtml(formatPnlMoneyText(stats.activePnl, stats.activePnlKnown))}</b> (<b>${stats.active}</b>)`
566
1140
  );
567
- const sortedTrades = [...stats.trades].sort(
1141
+ const sortedTrades = stats.trades.filter((trade) => trade.status === "active").sort(
568
1142
  (left, right) => left.entryTimestamp - right.entryTimestamp || left.symbol.localeCompare(right.symbol) || left.orderId.localeCompare(right.orderId)
569
1143
  );
570
1144
  for (const trade of sortedTrades) {
571
- lines.push(
572
- `- ${escapeHtml(trade.symbol)}: PnL <b>${escapeHtml(trade.pnlText)}</b>, status=<b>${escapeHtml(trade.status)}</b>`
1145
+ tradeLines.push(
1146
+ `- ${escapeHtml(trade.symbol)}: PnL <b>${escapeHtml(trade.pnlText)}</b> ${resolveTradeStatusEmoji(trade.status, trade.pnl)}`
573
1147
  );
574
1148
  }
575
1149
  }
576
- return lines.join("\n");
1150
+ return {
1151
+ signalsMessage: signalLines.join("\n"),
1152
+ tradesMessage: tradeLines.join("\n")
1153
+ };
577
1154
  };
578
1155
  var signalsSummary = async () => {
579
1156
  const hours = Math.max(
@@ -593,10 +1170,10 @@ var signalsSummary = async () => {
593
1170
  const connector = await connectorFactory({
594
1171
  userName: flags.user
595
1172
  });
596
- const [configuredStrategyNames, signals, signalEvaluations, trades] = await Promise.all([
1173
+ const [configuredStrategyNames, signals, evaluationStatsBuckets, trades] = await Promise.all([
597
1174
  loadRuntimeStrategyNames(flags.user),
598
- loadRuntimeSignals(flags.user),
599
- loadRuntimeSignalEvaluations(flags.user),
1175
+ loadRuntimeSignals(flags.user, { startTime, endTime }),
1176
+ loadRuntimeSignalEvaluationStatsBuckets(flags.user),
600
1177
  loadRuntimeTrades(flags.user)
601
1178
  ]);
602
1179
  const syncedTrades = await syncRuntimeTrades({
@@ -604,52 +1181,117 @@ var signalsSummary = async () => {
604
1181
  connector,
605
1182
  trades,
606
1183
  startTime,
607
- endTime
1184
+ endTime,
1185
+ openPositionCallbacks: {
1186
+ onError: (error) => {
1187
+ import_logger.logger.warn(
1188
+ "signals summary: getOpenPositionPnl failed: %s",
1189
+ formatRuntimeTradeSyncError(error)
1190
+ );
1191
+ }
1192
+ },
1193
+ closedPnlCallbacks: {
1194
+ onError: (error) => {
1195
+ import_logger.logger.warn(
1196
+ "signals summary: getClosedPnl failed: %s",
1197
+ formatRuntimeTradeSyncError(error)
1198
+ );
1199
+ }
1200
+ }
608
1201
  });
609
1202
  const windowSignals = signals.filter(
610
- (signal) => signal.timestamp >= startTime && signal.timestamp <= endTime
1203
+ (signal) => signal.timestamp >= startTime && signal.timestamp < endTime
611
1204
  );
612
- const windowSignalEvaluations = signalEvaluations.filter(
613
- (evaluation) => evaluation.timestamp >= startTime && evaluation.timestamp <= endTime
1205
+ const windowDayKeys = new Set((0, import_time3.getRuntimeStorageDayKeys)(startTime, endTime));
1206
+ const windowEvaluationStats = /* @__PURE__ */ new Map();
1207
+ for (const entry of evaluationStatsBuckets) {
1208
+ if (!windowDayKeys.has(entry.dayKey)) {
1209
+ continue;
1210
+ }
1211
+ const stats = windowEvaluationStats.get(entry.strategy) ?? createRuntimeSignalStats();
1212
+ mergeRuntimeSignalStats(stats, entry.stats);
1213
+ windowEvaluationStats.set(entry.strategy, stats);
1214
+ }
1215
+ const windowClosedTrades = syncedTrades.filter(
1216
+ (trade) => isRuntimeTradeClosedInWindow(trade, startTime, endTime)
1217
+ );
1218
+ const openSnapshotTrades = syncedTrades.filter(
1219
+ (trade) => trade.status === "active" && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp) && trade.entryTimestamp < endTime
614
1220
  );
615
- const signalIds = new Set(
616
- signals.map((signal) => signal.signalId).filter(Boolean)
1221
+ const windowTrades = [...windowClosedTrades, ...openSnapshotTrades];
1222
+ const windowTradeSignalIds = new Set(
1223
+ windowTrades.map((trade) => trade.signalId).filter((signalId) => typeof signalId === "string")
617
1224
  );
618
- const windowTrades = syncedTrades.filter(
619
- (trade) => trade.entryTimestamp >= startTime && trade.entryTimestamp <= endTime && trade.signalId != null && signalIds.has(trade.signalId)
1225
+ const debugSignals = windowSignals.filter(
1226
+ (signal) => windowTradeSignalIds.has(signal.signalId)
620
1227
  );
621
- const message = buildSummaryMessage({
1228
+ const { signalsMessage, tradesMessage } = buildSummaryMessages({
622
1229
  hours,
623
1230
  startTime,
624
1231
  endTime,
625
1232
  configuredStrategyNames,
626
1233
  signals: windowSignals,
627
- signalEvaluations: windowSignalEvaluations,
1234
+ evaluationStatsByStrategy: windowEvaluationStats,
628
1235
  trades: windowTrades
629
1236
  });
1237
+ const windowEvaluationsCount = [...windowEvaluationStats.values()].reduce(
1238
+ (sum, stats) => sum + stats.evaluated,
1239
+ 0
1240
+ );
630
1241
  import_logger.logger.info(
631
1242
  "signals summary window=%sh signals=%s evaluations=%s trades=%s connector=%s user=%s",
632
1243
  hours,
633
1244
  windowSignals.length,
634
- windowSignalEvaluations.length,
635
- windowTrades.length,
1245
+ windowEvaluationsCount,
1246
+ windowClosedTrades.length,
636
1247
  connectorName,
637
1248
  flags.user
638
1249
  );
1250
+ const debugAttachment = shouldAttachDebugReport(flags.debugAttachment) ? await buildRuntimeDebugReportAttachment({
1251
+ userName: flags.user,
1252
+ startTime,
1253
+ endTime,
1254
+ signals: debugSignals,
1255
+ evaluations: [],
1256
+ trades: windowTrades
1257
+ }) : null;
1258
+ const signalsAttachment = buildSignalsSummaryAttachment({
1259
+ userName: flags.user,
1260
+ endTime,
1261
+ content: signalsMessage
1262
+ });
1263
+ const tradesMessageWithAttachmentSummary = appendReportAttachmentSummary({
1264
+ message: tradesMessage,
1265
+ signalsFilename: signalsAttachment.filename,
1266
+ filename: debugAttachment?.filename,
1267
+ tradesCount: debugAttachment ? windowTrades.length : void 0,
1268
+ signalsCount: debugAttachment ? debugAttachment.summary?.signals ?? debugSignals.length : void 0,
1269
+ evaluationsCount: debugAttachment ? debugAttachment.summary?.evaluations ?? 0 : void 0
1270
+ });
1271
+ const attachments = [
1272
+ signalsAttachment,
1273
+ ...debugAttachment ? [debugAttachment] : []
1274
+ ];
639
1275
  if (flags.printOnly) {
640
- console.log(message);
1276
+ console.log(tradesMessageWithAttachmentSummary);
1277
+ console.log("");
1278
+ console.log(`Signals attachment: ${signalsAttachment.filename}`);
1279
+ console.log(signalsAttachment.content);
1280
+ if (debugAttachment) {
1281
+ console.log("");
1282
+ console.log(`Debug attachment: ${debugAttachment.filename}`);
1283
+ console.log(debugAttachment.content);
1284
+ }
641
1285
  return;
642
1286
  }
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);
1287
+ await sendTelegramReport(tradesMessageWithAttachmentSummary, {
1288
+ userName: flags.user,
1289
+ attachments
650
1290
  });
651
- }
1291
+ };
1292
+ var main = signalsSummary;
652
1293
  // Annotate the CommonJS export names for ESM import in node:
653
1294
  0 && (module.exports = {
1295
+ main,
654
1296
  signalsSummary
655
1297
  });