@tradejs/cli 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +13 -4
  2. package/dist/cli.js +29531 -13520
  3. package/dist/lib/aiPocketSearch.js +1316 -0
  4. package/dist/lib/aiTrainCharts.js +711 -0
  5. package/dist/lib/aiTrainDataset.js +40 -0
  6. package/dist/lib/aiTrainEvaluationDump.js +76 -0
  7. package/dist/lib/aiTrainMetrics.js +148 -8
  8. package/dist/lib/aiTrainOptions.js +130 -0
  9. package/dist/lib/aiTrainQuarantine.js +199 -0
  10. package/dist/lib/aiTrainResearch.js +467 -0
  11. package/dist/lib/binanceMarketContextBackfill.js +841 -0
  12. package/dist/lib/binanceMarketData.js +392 -0
  13. package/dist/lib/coinMarketCapContextBackfill.js +1126 -0
  14. package/dist/lib/continuity.js +60 -0
  15. package/dist/lib/derivativesContextBackfill.js +490 -77
  16. package/dist/lib/executionCalibration.js +705 -0
  17. package/dist/lib/marketContextPrepare.js +4441 -0
  18. package/dist/lib/paritySummary.js +101 -0
  19. package/dist/lib/runBot.js +1 -0
  20. package/dist/lib/runEnvironment.js +2259 -0
  21. package/dist/lib/runFormatting.js +1657 -0
  22. package/dist/lib/runtimeDebugEvidence.js +526 -0
  23. package/dist/lib/runtimeModeConfig.js +53 -0
  24. package/dist/lib/runtimeParity.js +88 -11
  25. package/dist/lib/runtimeParityDetails.js +751 -0
  26. package/dist/lib/runtimeRedis.js +157 -0
  27. package/dist/lib/runtimeSignalsLoader.js +40 -6
  28. package/dist/lib/runtimeSignalsStorage.js +45 -22
  29. package/dist/lib/runtimeStrategyBacktest.js +134 -0
  30. package/dist/lib/runtimeTradeSync.js +263 -0
  31. package/dist/lib/telegramReports.js +9 -0
  32. package/dist/lib/tickerUniverseCache.js +116 -0
  33. package/dist/lib/timeWindow.js +3 -3
  34. package/dist/scripts/agentRun.js +14 -8
  35. package/dist/scripts/aiExport.js +1721 -7
  36. package/dist/scripts/aiExportSelect.js +16 -4
  37. package/dist/scripts/aiPocketSearch.js +4190 -0
  38. package/dist/scripts/aiTrain.js +1866 -87
  39. package/dist/scripts/backtest.js +4534 -803
  40. package/dist/scripts/binanceMarketIngest.js +565 -0
  41. package/dist/scripts/bot.js +16 -2
  42. package/dist/scripts/candlesMigrateProvider.js +32 -22
  43. package/dist/scripts/cleanDir.js +15 -2
  44. package/dist/scripts/cleanRedis.js +15 -2
  45. package/dist/scripts/cleanTests.js +38 -2
  46. package/dist/scripts/cleanupMarketContext.js +69 -0
  47. package/dist/scripts/continuity.js +148 -67
  48. package/dist/scripts/derivativesIngest.js +14 -4
  49. package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
  50. package/dist/scripts/doctor.js +14 -4
  51. package/dist/scripts/executionCalibration.js +792 -0
  52. package/dist/scripts/infraCommon.js +18 -5
  53. package/dist/scripts/infraDown.js +14 -4
  54. package/dist/scripts/infraInit.js +32 -9
  55. package/dist/scripts/infraUp.js +24 -6
  56. package/dist/scripts/marketWs.js +222 -0
  57. package/dist/scripts/migration.js +15 -2
  58. package/dist/scripts/mlExport.js +1698 -10
  59. package/dist/scripts/mlExportSelect.js +16 -4
  60. package/dist/scripts/mlInspect.js +13 -3
  61. package/dist/scripts/mlTrainLatestSelect.js +14 -4
  62. package/dist/scripts/replay.js +8867 -0
  63. package/dist/scripts/replayRunner.js +8870 -0
  64. package/dist/scripts/replayRuntimeEvidence.js +868 -0
  65. package/dist/scripts/researchAuto.js +286 -59
  66. package/dist/scripts/results.js +3 -3
  67. package/dist/scripts/runtimeEvidence.js +605 -0
  68. package/dist/scripts/runtimeParity.js +6400 -1043
  69. package/dist/scripts/serverHealth.js +588 -0
  70. package/dist/scripts/signals.js +5628 -511
  71. package/dist/scripts/signalsDaemon.js +6205 -0
  72. package/dist/scripts/signalsSummary.js +833 -248
  73. package/dist/scripts/test-ml.js +25 -3
  74. package/dist/scripts/test.js +26 -1
  75. package/dist/scripts/user-add.js +15 -2
  76. package/dist/workers/testerWorker.js +130 -21
  77. 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_redis2 = 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,32 +399,16 @@ ${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
 
103
406
  // src/lib/runtimeSignalsLoader.ts
104
- var import_redis = require("@tradejs/infra/redis");
407
+ var import_redis3 = require("@tradejs/infra/redis");
105
408
 
106
409
  // 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
- };
410
+ var import_constants2 = require("@tradejs/core/constants");
411
+ var import_time3 = require("@tradejs/core/time");
126
412
  var isRuntimeSignalBucketRef = (value) => {
127
413
  if (!value || typeof value !== "object") {
128
414
  return false;
@@ -165,9 +451,35 @@ var isSignalRecord = (value) => {
165
451
  return typeof record.signalId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number";
166
452
  };
167
453
  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);
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);
171
483
  const seen = /* @__PURE__ */ new Set();
172
484
  const payloads = await Promise.all(
173
485
  refs.map(async (ref) => {
@@ -176,8 +488,8 @@ var loadRuntimeSignals = async (userName) => {
176
488
  return null;
177
489
  }
178
490
  seen.add(dedupeKey);
179
- const signal = await (0, import_redis.getData)(
180
- import_redis.redisKeys.storeSignal(ref.symbol, ref.signalId),
491
+ const signal = await (0, import_redis3.getData)(
492
+ import_redis3.redisKeys.storeSignal(ref.symbol, ref.signalId),
181
493
  null
182
494
  );
183
495
  return isSignalRecord(signal) ? signal : null;
@@ -200,8 +512,8 @@ var parseBucketKey = (prefix, key) => {
200
512
  };
201
513
  };
202
514
  var loadRuntimeSignalEvaluationStatsBuckets = async (userName) => {
203
- const prefix = import_redis.redisKeys.runtimeSignalEvaluationStatsBuckets(userName);
204
- const keys = await (0, import_redis.getKeys)(prefix);
515
+ const prefix = import_redis3.redisKeys.runtimeSignalEvaluationStatsBuckets(userName);
516
+ const keys = await (0, import_redis3.getKeys)(prefix);
205
517
  const entries = await Promise.all(
206
518
  keys.map(async (key) => {
207
519
  const parsedKey = parseBucketKey(prefix, key);
@@ -211,13 +523,219 @@ var loadRuntimeSignalEvaluationStatsBuckets = async (userName) => {
211
523
  return {
212
524
  key,
213
525
  ...parsedKey,
214
- stats: parseRuntimeSignalStatsBucket(await (0, import_redis.getHashData)(key))
526
+ stats: parseRuntimeSignalStatsBucket(await (0, import_redis3.getHashData)(key))
215
527
  };
216
528
  })
217
529
  );
218
530
  return entries.filter(Boolean);
219
531
  };
220
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
+
221
739
  // src/scripts/signalsSummary.ts
222
740
  import_args.default.option(["u", "user"], "Use user config", "root");
223
741
  import_args.default.option(
@@ -227,6 +745,11 @@ import_args.default.option(
227
745
  );
228
746
  import_args.default.option(["H", "hours"], "Summary window in hours", 24);
229
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
+ );
230
753
  var flags = import_args.default.parse(process.argv);
231
754
  var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
232
755
  var SUMMARY_TIMEZONE = "Europe/Moscow";
@@ -242,6 +765,8 @@ var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
242
765
  hour12: false
243
766
  }).format(new Date(timestamp));
244
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";
245
770
  var resolveSummaryTitle = (hours) => {
246
771
  if (hours === 24) {
247
772
  return "TradeJS daily summary";
@@ -249,6 +774,9 @@ var resolveSummaryTitle = (hours) => {
249
774
  if (hours === 168) {
250
775
  return "TradeJS weekly summary";
251
776
  }
777
+ if (hours === 720) {
778
+ return "TradeJS monthly summary";
779
+ }
252
780
  return `TradeJS ${hours}h summary`;
253
781
  };
254
782
  var normalizeStatus = (value) => {
@@ -257,12 +785,57 @@ var normalizeStatus = (value) => {
257
785
  }
258
786
  return "unknown";
259
787
  };
260
- var isRuntimeTradeRecord = (value) => {
261
- if (!value || typeof value !== "object") {
262
- return false;
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);
809
+ return {
810
+ filename: `tradejs-signals-summary-${userName}-${dayKey}.txt`,
811
+ content: stripTelegramHtml(content),
812
+ caption: `Signals summary ${dayKey} ${SUMMARY_TIMEZONE_LABEL}`
813
+ };
814
+ };
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
+ );
263
837
  }
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";
838
+ return lines.join("\n");
266
839
  };
267
840
  var resolveSummaryConnectorName = async (value) => {
268
841
  const connectorName = await (0, import_connectors.resolveConnectorName)(value, projectRoot);
@@ -276,26 +849,6 @@ var resolveSummaryConnectorName = async (value) => {
276
849
  );
277
850
  return import_connectors.DEFAULT_CONNECTOR_NAME;
278
851
  };
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
852
  var createRuntimeSignalStats = () => ({
300
853
  evaluated: 0,
301
854
  signals: 0,
@@ -312,129 +865,49 @@ var mergeRuntimeSignalStats = (target, source) => {
312
865
  target.reasonGroups.set(group, targetReasons);
313
866
  }
314
867
  };
315
- var loadClosedPnlRows = async ({
316
- connector,
868
+ var buildSummaryPrelude = ({
869
+ hours,
317
870
  startTime,
318
- endTime
871
+ endTime,
872
+ realizedWindowPnlText,
873
+ winRateText,
874
+ longCount,
875
+ shortCount,
876
+ openCount,
877
+ openPnlText
319
878
  }) => {
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
- }
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
+ ];
337
892
  };
338
- var consumeClosedPnlMatch = (buckets, trade) => {
339
- const rows = buckets.get(trade.symbol);
340
- if (!rows?.length) {
341
- 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}";
342
896
  }
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;
897
+ if (status === "closed") {
898
+ return typeof pnl === "number" && Number.isFinite(pnl) && pnl < 0 ? "\u274C" : "\u2705";
349
899
  }
350
- const [row] = rows.splice(matchIndex, 1);
351
- return row ?? null;
900
+ return "\u2754";
352
901
  };
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);
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;
434
906
  }
435
- 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;
436
909
  };
437
- var buildSummaryMessage = ({
910
+ var buildSummaryMessages = ({
438
911
  hours,
439
912
  startTime,
440
913
  endTime,
@@ -443,7 +916,8 @@ var buildSummaryMessage = ({
443
916
  evaluationStatsByStrategy,
444
917
  trades
445
918
  }) => {
446
- const lines = [];
919
+ const signalLines = [];
920
+ const tradeLines = [];
447
921
  const statusOrder = [
448
922
  "completed",
449
923
  "skipped",
@@ -466,43 +940,49 @@ var buildSummaryMessage = ({
466
940
  stats.set(status, (stats.get(status) ?? 0) + 1);
467
941
  signalStats.set(signal.strategy, stats);
468
942
  }
469
- 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) {
470
951
  strategyNames.add(trade.strategy);
471
952
  const stats = tradeStats.get(trade.strategy) ?? {
472
- total: 0,
473
953
  active: 0,
474
954
  closed: 0,
475
955
  activePnl: 0,
476
956
  activePnlKnown: 0,
477
957
  closedPnl: 0,
478
958
  closedPnlKnown: 0,
479
- totalPnl: 0,
480
- totalPnlKnown: 0,
481
- trades: []
959
+ trades: [],
960
+ closedWins: 0,
961
+ closedKnown: 0
482
962
  };
483
963
  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;
964
+ const pnlText = typeof pnl === "number" && Number.isFinite(pnl) ? `${formatSigned(pnl)}$` : "n/a";
486
965
  if (trade.status === "active") {
487
966
  stats.active += 1;
488
967
  if (typeof pnl === "number" && Number.isFinite(pnl)) {
489
968
  stats.activePnl += pnl;
490
969
  stats.activePnlKnown += 1;
491
- stats.totalPnl += pnl;
492
- stats.totalPnlKnown += 1;
493
970
  }
494
971
  } else {
495
972
  stats.closed += 1;
496
973
  if (typeof pnl === "number" && Number.isFinite(pnl)) {
497
974
  stats.closedPnl += pnl;
498
975
  stats.closedPnlKnown += 1;
499
- stats.totalPnl += pnl;
500
- stats.totalPnlKnown += 1;
976
+ stats.closedKnown += 1;
977
+ if (pnl > 0) {
978
+ stats.closedWins += 1;
979
+ }
501
980
  }
502
981
  }
503
982
  stats.trades.push({
504
983
  symbol: trade.symbol,
505
984
  status: trade.status,
985
+ pnl: typeof pnl === "number" && Number.isFinite(pnl) ? pnl : null,
506
986
  pnlText,
507
987
  entryTimestamp: trade.entryTimestamp,
508
988
  orderId: trade.orderId
@@ -512,39 +992,70 @@ var buildSummaryMessage = ({
512
992
  const sortedStrategies = [...strategyNames].sort(
513
993
  (left, right) => left.localeCompare(right)
514
994
  );
515
- const totalWindowPnl = [...tradeStats.values()].reduce(
516
- (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,
517
1001
  0
518
1002
  );
519
- const totalWindowPnlKnown = [...tradeStats.values()].reduce(
520
- (sum, stats) => sum + stats.totalPnlKnown,
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,
521
1012
  0
522
1013
  );
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>`
1014
+ const openPnlKnown = [...tradeStats.values()].reduce(
1015
+ (sum, stats) => sum + stats.activePnlKnown,
1016
+ 0
530
1017
  );
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>`
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
1022
+ );
1023
+ const closedKnown = [...tradeStats.values()].reduce(
1024
+ (sum, stats) => sum + stats.closedKnown,
1025
+ 0
535
1026
  );
536
- lines.push("");
537
- 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>");
538
1045
  if (!sortedStrategies.length) {
539
- lines.push("\u26A0\uFE0F No runtime data for this window.");
540
- 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
+ };
541
1052
  }
542
1053
  const appendEvaluationDetails = (strategyName) => {
543
1054
  const evaluation = evaluationStatsByStrategy.get(strategyName);
544
1055
  if (!evaluation) {
545
1056
  return;
546
1057
  }
547
- lines.push(
1058
+ signalLines.push(
548
1059
  `evaluated=<b>${evaluation.evaluated}</b>, signals=<b>${evaluation.signals}</b>`
549
1060
  );
550
1061
  const sourceOrder = ["skip from core", "skip from AI", "skip from ML"];
@@ -558,12 +1069,12 @@ var buildSummaryMessage = ({
558
1069
  }
559
1070
  );
560
1071
  for (const [source, reasons] of sortedReasonGroups) {
561
- lines.push(`<b>${escapeHtml(source)}</b>:`);
1072
+ signalLines.push(`<b>${escapeHtml(source)}</b>:`);
562
1073
  const sortedReasons = [...reasons.entries()].sort(
563
1074
  (left, right) => right[1] - left[1] || left[0].localeCompare(right[0])
564
1075
  );
565
1076
  for (const [reason, count] of sortedReasons) {
566
- lines.push(`${escapeHtml(reason)}: <b>${count}</b>`);
1077
+ signalLines.push(`${escapeHtml(reason)}: <b>${count}</b>`);
567
1078
  }
568
1079
  }
569
1080
  };
@@ -571,8 +1082,8 @@ var buildSummaryMessage = ({
571
1082
  const stats = signalStats.get(strategyName);
572
1083
  if (!stats || stats.size === 0) {
573
1084
  const evaluation = evaluationStatsByStrategy.get(strategyName);
574
- lines.push("");
575
- lines.push(
1085
+ signalLines.push("");
1086
+ signalLines.push(
576
1087
  evaluation?.evaluated ? `<b>${escapeHtml(strategyName)}</b>
577
1088
  signals=<b>0</b>` : `<b>${escapeHtml(strategyName)}</b>
578
1089
  none`
@@ -584,42 +1095,62 @@ none`
584
1095
  const count = stats.get(status) ?? 0;
585
1096
  return count > 0 ? `${status}=<b>${count}</b>` : null;
586
1097
  }).filter(Boolean);
587
- lines.push("");
588
- lines.push(`<b>${escapeHtml(strategyName)}</b>`);
589
- lines.push(parts.join(", "));
1098
+ signalLines.push("");
1099
+ signalLines.push(`<b>${escapeHtml(strategyName)}</b>`);
1100
+ signalLines.push(parts.join(", "));
590
1101
  appendEvaluationDetails(strategyName);
591
1102
  }
592
- lines.push("");
593
- lines.push("\u{1F4BC} <b>Trades</b>");
594
- 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) {
595
1108
  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;
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
+ );
601
1124
  }
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>)`
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>)`
609
1137
  );
610
- lines.push(
611
- `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>)`
612
1140
  );
613
- const sortedTrades = [...stats.trades].sort(
1141
+ const sortedTrades = stats.trades.filter((trade) => trade.status === "active").sort(
614
1142
  (left, right) => left.entryTimestamp - right.entryTimestamp || left.symbol.localeCompare(right.symbol) || left.orderId.localeCompare(right.orderId)
615
1143
  );
616
1144
  for (const trade of sortedTrades) {
617
- lines.push(
618
- `- ${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)}`
619
1147
  );
620
1148
  }
621
1149
  }
622
- return lines.join("\n");
1150
+ return {
1151
+ signalsMessage: signalLines.join("\n"),
1152
+ tradesMessage: tradeLines.join("\n")
1153
+ };
623
1154
  };
624
1155
  var signalsSummary = async () => {
625
1156
  const hours = Math.max(
@@ -641,7 +1172,7 @@ var signalsSummary = async () => {
641
1172
  });
642
1173
  const [configuredStrategyNames, signals, evaluationStatsBuckets, trades] = await Promise.all([
643
1174
  loadRuntimeStrategyNames(flags.user),
644
- loadRuntimeSignals(flags.user),
1175
+ loadRuntimeSignals(flags.user, { startTime, endTime }),
645
1176
  loadRuntimeSignalEvaluationStatsBuckets(flags.user),
646
1177
  loadRuntimeTrades(flags.user)
647
1178
  ]);
@@ -650,12 +1181,28 @@ var signalsSummary = async () => {
650
1181
  connector,
651
1182
  trades,
652
1183
  startTime,
653
- 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
+ }
654
1201
  });
655
1202
  const windowSignals = signals.filter(
656
1203
  (signal) => signal.timestamp >= startTime && signal.timestamp < endTime
657
1204
  );
658
- const windowDayKeys = new Set(getRuntimeStorageDayKeys(startTime, endTime));
1205
+ const windowDayKeys = new Set((0, import_time3.getRuntimeStorageDayKeys)(startTime, endTime));
659
1206
  const windowEvaluationStats = /* @__PURE__ */ new Map();
660
1207
  for (const entry of evaluationStatsBuckets) {
661
1208
  if (!windowDayKeys.has(entry.dayKey)) {
@@ -665,13 +1212,20 @@ var signalsSummary = async () => {
665
1212
  mergeRuntimeSignalStats(stats, entry.stats);
666
1213
  windowEvaluationStats.set(entry.strategy, stats);
667
1214
  }
668
- const signalIds = new Set(
669
- signals.map((signal) => signal.signalId).filter(Boolean)
1215
+ const windowClosedTrades = syncedTrades.filter(
1216
+ (trade) => isRuntimeTradeClosedInWindow(trade, startTime, endTime)
670
1217
  );
671
- const windowTrades = syncedTrades.filter(
672
- (trade) => trade.entryTimestamp >= startTime && trade.entryTimestamp < endTime && trade.signalId != null && signalIds.has(trade.signalId)
1218
+ const openSnapshotTrades = syncedTrades.filter(
1219
+ (trade) => trade.status === "active" && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp) && trade.entryTimestamp < endTime
673
1220
  );
674
- const message = buildSummaryMessage({
1221
+ const windowTrades = [...windowClosedTrades, ...openSnapshotTrades];
1222
+ const windowTradeSignalIds = new Set(
1223
+ windowTrades.map((trade) => trade.signalId).filter((signalId) => typeof signalId === "string")
1224
+ );
1225
+ const debugSignals = windowSignals.filter(
1226
+ (signal) => windowTradeSignalIds.has(signal.signalId)
1227
+ );
1228
+ const { signalsMessage, tradesMessage } = buildSummaryMessages({
675
1229
  hours,
676
1230
  startTime,
677
1231
  endTime,
@@ -689,24 +1243,55 @@ var signalsSummary = async () => {
689
1243
  hours,
690
1244
  windowSignals.length,
691
1245
  windowEvaluationsCount,
692
- windowTrades.length,
1246
+ windowClosedTrades.length,
693
1247
  connectorName,
694
1248
  flags.user
695
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
+ ];
696
1275
  if (flags.printOnly) {
697
- 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
+ }
698
1285
  return;
699
1286
  }
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);
1287
+ await sendTelegramReport(tradesMessageWithAttachmentSummary, {
1288
+ userName: flags.user,
1289
+ attachments
707
1290
  });
708
- }
1291
+ };
1292
+ var main = signalsSummary;
709
1293
  // Annotate the CommonJS export names for ESM import in node:
710
1294
  0 && (module.exports = {
1295
+ main,
711
1296
  signalsSummary
712
1297
  });