@tradejs/cli 1.0.5 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +13791 -7652
  3. package/dist/lib/aiTrainMetrics.js +173 -0
  4. package/dist/lib/candlesProviderMigration.js +266 -0
  5. package/dist/lib/cliArgs.js +40 -0
  6. package/dist/lib/derivativesContextBackfill.js +487 -0
  7. package/dist/lib/researchAgent.js +195 -0
  8. package/dist/lib/runtimeParity.js +234 -0
  9. package/dist/lib/telegramReports.js +91 -0
  10. package/dist/lib/timeWindow.js +101 -0
  11. package/dist/scripts/agentRun.js +895 -0
  12. package/dist/scripts/aiExport.js +192 -0
  13. package/dist/scripts/aiExportSelect.js +135 -0
  14. package/dist/scripts/aiTrain.js +2321 -0
  15. package/dist/scripts/backtest.js +998 -111
  16. package/dist/scripts/candlesMigrateProvider.js +5302 -0
  17. package/dist/scripts/continuity.js +1 -1
  18. package/dist/scripts/derivativesIngest.js +5 -0
  19. package/dist/scripts/derivativesIngestCoinalyzeAll.js +21 -12
  20. package/dist/scripts/migration.js +1 -1
  21. package/dist/scripts/mlExport.js +112 -13
  22. package/dist/scripts/mlExportSelect.js +43 -9
  23. package/dist/scripts/mlTrainLatestSelect.js +4 -3
  24. package/dist/scripts/researchAuto.js +845 -0
  25. package/dist/scripts/resolveExportStrategy.js +133 -0
  26. package/dist/scripts/results.js +46 -22
  27. package/dist/scripts/runtimeParity.js +1651 -0
  28. package/dist/scripts/selectStrategy.js +4 -3
  29. package/dist/scripts/signals.js +799 -62
  30. package/dist/scripts/signalsSummary.js +655 -0
  31. package/dist/scripts/spawnSiblingScript.js +71 -0
  32. package/dist/scripts/user-add.js +0 -9
  33. package/dist/workers/testerWorker.js +13 -2
  34. package/package.json +18 -12
@@ -0,0 +1,1651 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/scripts/runtimeParity.ts
31
+ var runtimeParity_exports = {};
32
+ __export(runtimeParity_exports, {
33
+ buildRuntimeParityMessage: () => buildRuntimeParityMessage,
34
+ runtimeParity: () => runtimeParity
35
+ });
36
+ module.exports = __toCommonJS(runtimeParity_exports);
37
+ var import_config = require("dotenv/config");
38
+ var import_node_crypto = require("crypto");
39
+ var import_args = __toESM(require("args"));
40
+ var import_chalk = __toESM(require("chalk"));
41
+ var import_connectors = require("@tradejs/connectors");
42
+ var import_constants = require("@tradejs/core/constants");
43
+ var import_time = require("@tradejs/core/time");
44
+ var import_logger = require("@tradejs/infra/logger");
45
+ var import_redis = require("@tradejs/infra/redis");
46
+ var import_cli2 = require("@tradejs/node/cli");
47
+ var import_backtest = require("@tradejs/node/backtest");
48
+ var import_connectors2 = require("@tradejs/node/connectors");
49
+
50
+ // src/lib/runtimeParity.ts
51
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
52
+ var toGroupKey = (entry) => `${entry.strategy}::${entry.symbol}::${entry.direction}`;
53
+ var toRuntimeDuplicateKey = (entry) => `${toGroupKey(entry)}::${entry.timestamp}`;
54
+ var sortTradeParityEntries = (left, right) => left.timestamp - right.timestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.direction.localeCompare(right.direction) || left.id.localeCompare(right.id);
55
+ var buildPriceDeltaPct = (leftPrice, rightPrice) => {
56
+ if (leftPrice == null || rightPrice == null || !Number.isFinite(leftPrice) || !Number.isFinite(rightPrice) || leftPrice === 0) {
57
+ return null;
58
+ }
59
+ return Math.abs((rightPrice - leftPrice) / leftPrice * 100);
60
+ };
61
+ var extractBacktestEntryParityEntries = (orderLog) => {
62
+ if (!Array.isArray(orderLog)) {
63
+ return [];
64
+ }
65
+ return orderLog.filter(
66
+ (item) => (item.type === "OPEN_LONG" || item.type === "OPEN_SHORT") && item.signal && typeof item.signal.strategy === "string" && typeof item.signal.symbol === "string" && (item.signal.direction === "LONG" || item.signal.direction === "SHORT")
67
+ ).map((item, index) => {
68
+ const signal = item.signal;
69
+ const timestamp = typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : item.timestamp;
70
+ return {
71
+ id: typeof signal.signalId === "string" && signal.signalId.trim() ? signal.signalId : `backtest-${index}-${timestamp}`,
72
+ source: "backtest",
73
+ strategy: signal.strategy,
74
+ symbol: signal.symbol,
75
+ direction: signal.direction,
76
+ timestamp,
77
+ price: toFiniteNumberOrNull(item.price),
78
+ signalId: typeof signal.signalId === "string" ? signal.signalId : void 0
79
+ };
80
+ }).sort((left, right) => left.timestamp - right.timestamp);
81
+ };
82
+ var extractRuntimeParityEntries = (trades) => trades.filter(
83
+ (trade) => typeof trade.strategy === "string" && typeof trade.symbol === "string" && (trade.direction === "LONG" || trade.direction === "SHORT") && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp)
84
+ ).map((trade) => ({
85
+ id: trade.orderId,
86
+ source: "runtime",
87
+ strategy: trade.strategy,
88
+ symbol: trade.symbol,
89
+ direction: trade.direction,
90
+ timestamp: trade.entryTimestamp,
91
+ price: toFiniteNumberOrNull(trade.entryPrice),
92
+ orderId: trade.orderId,
93
+ signalId: typeof trade.signalId === "string" ? trade.signalId : void 0
94
+ })).sort((left, right) => left.timestamp - right.timestamp);
95
+ var findRuntimeDuplicateGroups = (entries) => {
96
+ const groups = /* @__PURE__ */ new Map();
97
+ for (const entry of entries) {
98
+ if (entry.source !== "runtime") {
99
+ continue;
100
+ }
101
+ const key = toRuntimeDuplicateKey(entry);
102
+ const bucket = groups.get(key) ?? [];
103
+ bucket.push(entry);
104
+ groups.set(key, bucket);
105
+ }
106
+ return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key, group]) => {
107
+ const sortedGroup = [...group].sort(sortTradeParityEntries);
108
+ const first = sortedGroup[0];
109
+ return {
110
+ key,
111
+ strategy: first.strategy,
112
+ symbol: first.symbol,
113
+ direction: first.direction,
114
+ timestamp: first.timestamp,
115
+ entries: sortedGroup
116
+ };
117
+ }).sort(
118
+ (left, right) => left.timestamp - right.timestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.direction.localeCompare(right.direction)
119
+ );
120
+ };
121
+ var dedupeRuntimeParityEntries = (entries) => {
122
+ const duplicateGroups = findRuntimeDuplicateGroups(entries);
123
+ const duplicateEntriesSet = /* @__PURE__ */ new Set();
124
+ for (const group of duplicateGroups) {
125
+ for (const duplicate of group.entries.slice(1)) {
126
+ duplicateEntriesSet.add(duplicate);
127
+ }
128
+ }
129
+ const dedupedEntries = entries.filter((entry) => !duplicateEntriesSet.has(entry)).sort(sortTradeParityEntries);
130
+ const duplicateEntries = duplicateGroups.flatMap((group) => group.entries.slice(1)).sort(sortTradeParityEntries);
131
+ return {
132
+ entries: dedupedEntries,
133
+ duplicateGroups,
134
+ duplicateEntries
135
+ };
136
+ };
137
+ var compareTradeParityEntries = ({
138
+ runtimeEntries,
139
+ backtestEntries,
140
+ toleranceMs
141
+ }) => {
142
+ const matched = [];
143
+ const runtimeOnly = [];
144
+ const backtestOnly = [];
145
+ const groupedRuntime = /* @__PURE__ */ new Map();
146
+ const groupedBacktest = /* @__PURE__ */ new Map();
147
+ for (const entry of runtimeEntries) {
148
+ const key = toGroupKey(entry);
149
+ const bucket = groupedRuntime.get(key) ?? [];
150
+ bucket.push(entry);
151
+ groupedRuntime.set(key, bucket);
152
+ }
153
+ for (const entry of backtestEntries) {
154
+ const key = toGroupKey(entry);
155
+ const bucket = groupedBacktest.get(key) ?? [];
156
+ bucket.push(entry);
157
+ groupedBacktest.set(key, bucket);
158
+ }
159
+ const groupKeys = /* @__PURE__ */ new Set([
160
+ ...groupedRuntime.keys(),
161
+ ...groupedBacktest.keys()
162
+ ]);
163
+ for (const key of groupKeys) {
164
+ const runtimeGroup = [...groupedRuntime.get(key) ?? []].sort(
165
+ (left, right) => left.timestamp - right.timestamp
166
+ );
167
+ const backtestGroup = [...groupedBacktest.get(key) ?? []].sort(
168
+ (left, right) => left.timestamp - right.timestamp
169
+ );
170
+ const unmatchedBacktest = backtestGroup.map((entry) => ({
171
+ entry,
172
+ used: false
173
+ }));
174
+ for (const runtimeEntry of runtimeGroup) {
175
+ let bestIndex = -1;
176
+ let bestDiff = Number.POSITIVE_INFINITY;
177
+ for (let index = 0; index < unmatchedBacktest.length; index += 1) {
178
+ const candidate = unmatchedBacktest[index];
179
+ if (candidate.used) {
180
+ continue;
181
+ }
182
+ const diff = Math.abs(
183
+ candidate.entry.timestamp - runtimeEntry.timestamp
184
+ );
185
+ if (diff > toleranceMs || diff >= bestDiff) {
186
+ continue;
187
+ }
188
+ bestIndex = index;
189
+ bestDiff = diff;
190
+ }
191
+ if (bestIndex < 0) {
192
+ runtimeOnly.push(runtimeEntry);
193
+ continue;
194
+ }
195
+ unmatchedBacktest[bestIndex].used = true;
196
+ const backtestEntry = unmatchedBacktest[bestIndex].entry;
197
+ matched.push({
198
+ runtime: runtimeEntry,
199
+ backtest: backtestEntry,
200
+ timestampDiffMs: bestDiff,
201
+ priceDeltaPct: buildPriceDeltaPct(
202
+ runtimeEntry.price,
203
+ backtestEntry.price
204
+ )
205
+ });
206
+ }
207
+ for (const candidate of unmatchedBacktest) {
208
+ if (!candidate.used) {
209
+ backtestOnly.push(candidate.entry);
210
+ }
211
+ }
212
+ }
213
+ matched.sort(
214
+ (left, right) => left.runtime.timestamp - right.runtime.timestamp
215
+ );
216
+ runtimeOnly.sort((left, right) => left.timestamp - right.timestamp);
217
+ backtestOnly.sort((left, right) => left.timestamp - right.timestamp);
218
+ return {
219
+ matched,
220
+ runtimeOnly,
221
+ backtestOnly
222
+ };
223
+ };
224
+ var summarizeMatchedParity = (matched) => {
225
+ if (!matched.length) {
226
+ return {
227
+ avgPriceDeltaPct: null,
228
+ maxPriceDeltaPct: null,
229
+ avgTimestampDiffMs: null,
230
+ maxTimestampDiffMs: null
231
+ };
232
+ }
233
+ const priceDeltas = matched.map((item) => item.priceDeltaPct).filter(
234
+ (value) => value != null && Number.isFinite(value)
235
+ );
236
+ const timestampDiffs = matched.map((item) => item.timestampDiffMs).filter((value) => Number.isFinite(value));
237
+ const average = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
238
+ const maximum = (values) => values.length ? Math.max(...values) : null;
239
+ return {
240
+ avgPriceDeltaPct: average(priceDeltas),
241
+ maxPriceDeltaPct: maximum(priceDeltas),
242
+ avgTimestampDiffMs: average(timestampDiffs),
243
+ maxTimestampDiffMs: maximum(timestampDiffs)
244
+ };
245
+ };
246
+
247
+ // src/lib/cliArgs.ts
248
+ var normalizeCliArgv = (argv, legacyFlagMap) => argv.map((arg) => {
249
+ const [flag, ...rest] = String(arg).split("=");
250
+ const normalizedFlag = legacyFlagMap[flag];
251
+ if (!normalizedFlag) {
252
+ return arg;
253
+ }
254
+ if (!rest.length) {
255
+ return normalizedFlag;
256
+ }
257
+ return `${normalizedFlag}=${rest.join("=")}`;
258
+ });
259
+
260
+ // src/lib/telegramReports.ts
261
+ var import_cli = require("@tradejs/node/cli");
262
+ var TELEGRAM_REPORT_CHUNK_LIMIT = 3900;
263
+ var buildPartHeader = (index, total) => `\u{1F4E9} <b>Part ${index}/${total}</b>`;
264
+ var splitLongLine = (line, limit) => {
265
+ const chunks = [];
266
+ for (let index = 0; index < line.length; index += limit) {
267
+ chunks.push(line.slice(index, index + limit));
268
+ }
269
+ return chunks;
270
+ };
271
+ var splitTelegramReport = (message, limit = TELEGRAM_REPORT_CHUNK_LIMIT) => {
272
+ const normalizedLimit = Math.max(100, Math.floor(limit));
273
+ if (message.length <= normalizedLimit) {
274
+ return [message];
275
+ }
276
+ const chunks = [];
277
+ let current = "";
278
+ const flush = () => {
279
+ if (!current) {
280
+ return;
281
+ }
282
+ chunks.push(current);
283
+ current = "";
284
+ };
285
+ for (const line of message.split("\n")) {
286
+ const next = current ? `${current}
287
+ ${line}` : line;
288
+ if (next.length <= normalizedLimit) {
289
+ current = next;
290
+ continue;
291
+ }
292
+ flush();
293
+ if (line.length <= normalizedLimit) {
294
+ current = line;
295
+ continue;
296
+ }
297
+ chunks.push(...splitLongLine(line, normalizedLimit));
298
+ }
299
+ flush();
300
+ return chunks;
301
+ };
302
+ var sendTelegramReport = async (message, options = {}) => {
303
+ const chunks = splitTelegramReport(message);
304
+ if (chunks.length === 1) {
305
+ await (0, import_cli.sendTextToTG)(chunks[0], options);
306
+ return chunks.length;
307
+ }
308
+ for (const [index, chunk] of chunks.entries()) {
309
+ const header = buildPartHeader(index + 1, chunks.length);
310
+ await (0, import_cli.sendTextToTG)(`${header}
311
+
312
+ ${chunk}`, {
313
+ userName: options.userName,
314
+ markup: index === chunks.length - 1 ? options.markup : void 0
315
+ });
316
+ }
317
+ return chunks.length;
318
+ };
319
+
320
+ // src/lib/timeWindow.ts
321
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
322
+ var toEpochMs = (value) => {
323
+ if (value == null || value === "") {
324
+ return null;
325
+ }
326
+ const parsed = Number(value);
327
+ if (!Number.isFinite(parsed) || parsed <= 0) {
328
+ return null;
329
+ }
330
+ return parsed < 1e12 ? Math.trunc(parsed * 1e3) : Math.trunc(parsed);
331
+ };
332
+ var toPositiveNumber = (value) => {
333
+ if (value == null || value === "") {
334
+ return null;
335
+ }
336
+ const parsed = Number(value);
337
+ if (!Number.isFinite(parsed) || parsed <= 0) {
338
+ return null;
339
+ }
340
+ return parsed;
341
+ };
342
+ var resolveTimeWindow = ({
343
+ days,
344
+ startTime,
345
+ endTime,
346
+ nowMs = Date.now(),
347
+ defaultStartMs,
348
+ defaultEndMs
349
+ }) => {
350
+ const explicitStart = toEpochMs(startTime);
351
+ const explicitEnd = toEpochMs(endTime);
352
+ if (explicitStart != null || explicitEnd != null) {
353
+ const resolvedEnd2 = explicitEnd ?? defaultEndMs ?? nowMs;
354
+ const resolvedStart = explicitStart ?? defaultStartMs;
355
+ if (resolvedStart >= resolvedEnd2) {
356
+ throw new Error(
357
+ `Invalid time window: startTime (${resolvedStart}) must be less than endTime (${resolvedEnd2})`
358
+ );
359
+ }
360
+ return {
361
+ start: resolvedStart,
362
+ end: resolvedEnd2,
363
+ source: "explicit"
364
+ };
365
+ }
366
+ const resolvedDays = toPositiveNumber(days);
367
+ if (resolvedDays != null) {
368
+ const resolvedEnd2 = defaultEndMs ?? nowMs;
369
+ const resolvedStart = resolvedEnd2 - resolvedDays * ONE_DAY_MS;
370
+ if (resolvedStart >= resolvedEnd2) {
371
+ throw new Error(
372
+ `Invalid time window: computed start (${resolvedStart}) must be less than end (${resolvedEnd2})`
373
+ );
374
+ }
375
+ return {
376
+ start: Math.trunc(resolvedStart),
377
+ end: Math.trunc(resolvedEnd2),
378
+ source: "days"
379
+ };
380
+ }
381
+ const resolvedEnd = defaultEndMs ?? nowMs;
382
+ if (defaultStartMs >= resolvedEnd) {
383
+ throw new Error(
384
+ `Invalid default time window: start (${defaultStartMs}) must be less than end (${resolvedEnd})`
385
+ );
386
+ }
387
+ return {
388
+ start: Math.trunc(defaultStartMs),
389
+ end: Math.trunc(resolvedEnd),
390
+ source: "default"
391
+ };
392
+ };
393
+
394
+ // src/scripts/runtimeParity.ts
395
+ import_args.default.option(["u", "user"], "Use user config", "root");
396
+ import_args.default.option(
397
+ ["o", "connector"],
398
+ "Connector provider or name for parity replay (e.g. bybit, binance, coinbase, custom)",
399
+ "bybit"
400
+ );
401
+ import_args.default.option(["d", "days"], "Replay window in days", 3);
402
+ import_args.default.option(
403
+ ["b", "startTime"],
404
+ "Explicit replay start timestamp (ms or seconds)"
405
+ );
406
+ import_args.default.option(["e", "endTime"], "Explicit replay end timestamp (ms or seconds)");
407
+ import_args.default.option(["s", "strategy"], "Only compare one strategy");
408
+ import_args.default.option(
409
+ ["t", "tickers"],
410
+ "Replay comma-separated symbols for all configured strategies"
411
+ );
412
+ import_args.default.option(
413
+ ["C", "cacheOnly"],
414
+ "Do not refresh market history before replay",
415
+ false
416
+ );
417
+ import_args.default.option(
418
+ ["a", "toleranceBars"],
419
+ "Allowed entry timestamp drift in bars when matching runtime vs backtest",
420
+ 1
421
+ );
422
+ import_args.default.option(
423
+ "fullUniverse",
424
+ "Replay every configured strategy across the full connector universe. By default parity replays only runtime-traded and strategy-results symbols unless --tickers is provided.",
425
+ false
426
+ );
427
+ import_args.default.option(
428
+ "runtimeGates",
429
+ "Replay with runtime AI/ML gates enabled when configured. This may call external AI providers and ML inference.",
430
+ false
431
+ );
432
+ import_args.default.option(["N", "notify"], "Send parity summary to Telegram", false);
433
+ import_args.default.option(["D", "details"], "Print unmatched entry details (capped)", false);
434
+ process.argv = normalizeCliArgv(process.argv, {
435
+ "-C": "--cacheOnly",
436
+ "-D": "--details",
437
+ "-E": "--endTime",
438
+ "-N": "--notify",
439
+ "-S": "--startTime",
440
+ "-T": "--toleranceBars"
441
+ });
442
+ var flags = import_args.default.parse(process.argv);
443
+ var interval = "15";
444
+ var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
445
+ var DEFAULT_LOOKBACK_DAYS = 3;
446
+ var DETAIL_LIMIT = 10;
447
+ var TELEGRAM_DETAIL_LIMIT = 5;
448
+ var SUMMARY_TIMEZONE = "Europe/Moscow";
449
+ var SUMMARY_TIMEZONE_LABEL = "MSK";
450
+ var REPLAY_ENV = flags.runtimeGates ? "PARITY" : "BACKTEST";
451
+ var formatDuration = (startedAt) => {
452
+ const seconds = Math.max(0, (Date.now() - startedAt) / 1e3);
453
+ if (seconds < 60) {
454
+ return `${seconds.toFixed(1)}s`;
455
+ }
456
+ const minutes = Math.floor(seconds / 60);
457
+ const restSeconds = Math.round(seconds % 60);
458
+ return `${minutes}m ${restSeconds}s`;
459
+ };
460
+ var BACKTEST_ONLY_CLASSIFICATIONS = [
461
+ "gated_out",
462
+ "order_failed",
463
+ "core_skipped",
464
+ "not_evaluated",
465
+ "true_mismatch"
466
+ ];
467
+ var RUNTIME_ONLY_CLASSIFICATIONS = [
468
+ "gated_out",
469
+ "order_failed",
470
+ "core_skipped",
471
+ "backtest_drift",
472
+ "not_evaluated",
473
+ "true_mismatch"
474
+ ];
475
+ var parseSymbolsFromCLI = (symbols = "") => symbols.split(",").map((symbol) => symbol.trim().toUpperCase()).filter(Boolean).map((symbol) => symbol.endsWith("USDT") ? symbol : `${symbol}USDT`);
476
+ var toTargetKey = (target) => `${target.strategy}::${target.symbol}`;
477
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
478
+ var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
479
+ timeZone: SUMMARY_TIMEZONE,
480
+ year: "numeric",
481
+ month: "2-digit",
482
+ day: "2-digit",
483
+ hour: "2-digit",
484
+ minute: "2-digit",
485
+ hour12: false
486
+ }).format(new Date(timestamp));
487
+ var normalizeSignalOrderStatus = (value) => {
488
+ if (value === "completed" || value === "failed" || value === "skipped" || value === "canceled") {
489
+ return value;
490
+ }
491
+ return "unknown";
492
+ };
493
+ var isRuntimeTradeRecord = (value) => {
494
+ if (!value || typeof value !== "object") {
495
+ return false;
496
+ }
497
+ const record = value;
498
+ 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");
499
+ };
500
+ var isSignalRecord = (value) => {
501
+ if (!value || typeof value !== "object") {
502
+ return false;
503
+ }
504
+ const record = value;
505
+ return typeof record.signalId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number" && (record.direction === "LONG" || record.direction === "SHORT");
506
+ };
507
+ var isRuntimeSignalEvaluationRecord = (value) => {
508
+ if (!value || typeof value !== "object") {
509
+ return false;
510
+ }
511
+ const record = value;
512
+ 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");
513
+ };
514
+ var resolveParityConnectorName = async (value) => {
515
+ const connectorName = await (0, import_connectors2.resolveConnectorName)(value, projectRoot);
516
+ if (connectorName) {
517
+ return connectorName;
518
+ }
519
+ import_logger.logger.warn(
520
+ 'Unknown connector "%s". Fallback to %s.',
521
+ String(value || "").trim() || String(value),
522
+ import_connectors2.DEFAULT_CONNECTOR_NAME
523
+ );
524
+ return import_connectors2.DEFAULT_CONNECTOR_NAME;
525
+ };
526
+ var loadRuntimeTrades = async (userName) => {
527
+ const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeTrades(userName));
528
+ const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
529
+ return trades.filter(isRuntimeTradeRecord).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
530
+ };
531
+ var loadRuntimeSignals = async (userName) => {
532
+ const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeSignals(userName));
533
+ const signals = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
534
+ return signals.filter(isSignalRecord).sort((left, right) => left.timestamp - right.timestamp);
535
+ };
536
+ var loadRuntimeSignalEvaluations = async (userName) => {
537
+ const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeSignalEvaluations(userName));
538
+ const evaluations = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
539
+ return evaluations.filter(isRuntimeSignalEvaluationRecord).sort((left, right) => left.timestamp - right.timestamp);
540
+ };
541
+ var resolveStrategyNameByConfigKey = (userName, key) => {
542
+ const parts = key.split(":");
543
+ if (parts.length !== 5) {
544
+ return null;
545
+ }
546
+ const [users, keyUserName, strategiesKey, strategyName, configKey] = parts;
547
+ if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || configKey !== "config" || !strategyName) {
548
+ return null;
549
+ }
550
+ return strategyName;
551
+ };
552
+ var loadConfiguredStrategyNames = async (userName) => {
553
+ const keys = await (0, import_redis.getKeys)(`${import_redis.redisKeys.strategies(userName)}:`);
554
+ return keys.filter((key) => key.endsWith(":config")).map((key) => resolveStrategyNameByConfigKey(userName, key)).filter((value) => Boolean(value)).sort((left, right) => left.localeCompare(right));
555
+ };
556
+ var loadStrategyResultSymbols = async ({
557
+ userName,
558
+ strategy
559
+ }) => {
560
+ const results = await (0, import_redis.getData)(
561
+ import_redis.redisKeys.strategyResults(userName, strategy),
562
+ {}
563
+ );
564
+ return Object.keys(results ?? {}).sort(
565
+ (left, right) => left.localeCompare(right)
566
+ );
567
+ };
568
+ var addReplayTarget = (targets, target, source) => {
569
+ const key = toTargetKey(target);
570
+ const existing = targets.get(key);
571
+ if (existing) {
572
+ if (!existing.sources.includes(source)) {
573
+ existing.sources.push(source);
574
+ }
575
+ return;
576
+ }
577
+ targets.set(key, {
578
+ strategy: target.strategy,
579
+ symbol: target.symbol,
580
+ sources: [source]
581
+ });
582
+ };
583
+ var countReplayTargetSources = (targets) => ({
584
+ runtime: targets.filter((target) => target.sources.includes("runtime")).length,
585
+ connectorUniverse: targets.filter(
586
+ (target) => target.sources.includes("connectorUniverse")
587
+ ).length,
588
+ explicitTickers: targets.filter(
589
+ (target) => target.sources.includes("explicitTickers")
590
+ ).length,
591
+ strategyResults: targets.filter(
592
+ (target) => target.sources.includes("strategyResults")
593
+ ).length
594
+ });
595
+ var buildReplayTargets = async ({
596
+ userName,
597
+ runtimeTrades,
598
+ connectorSymbols,
599
+ strategyFilter,
600
+ explicitSymbols,
601
+ includeConnectorUniverse
602
+ }) => {
603
+ const targets = /* @__PURE__ */ new Map();
604
+ let configuredStrategies = (await loadConfiguredStrategyNames(userName)).filter((strategy) => !strategyFilter || strategy === strategyFilter);
605
+ if (strategyFilter && !configuredStrategies.some((strategy) => strategy === strategyFilter)) {
606
+ configuredStrategies = [strategyFilter];
607
+ }
608
+ const explicitSymbolSet = explicitSymbols.length ? new Set(explicitSymbols) : null;
609
+ for (const trade of runtimeTrades) {
610
+ if (strategyFilter && trade.strategy !== strategyFilter) {
611
+ continue;
612
+ }
613
+ if (explicitSymbolSet && !explicitSymbolSet.has(trade.symbol)) {
614
+ continue;
615
+ }
616
+ addReplayTarget(
617
+ targets,
618
+ { strategy: trade.strategy, symbol: trade.symbol },
619
+ "runtime"
620
+ );
621
+ }
622
+ if (explicitSymbols.length || includeConnectorUniverse) {
623
+ const universeSymbols = explicitSymbols.length ? explicitSymbols : connectorSymbols;
624
+ const universeSource = explicitSymbols.length ? "explicitTickers" : "connectorUniverse";
625
+ for (const strategy of configuredStrategies) {
626
+ for (const symbol of universeSymbols) {
627
+ addReplayTarget(targets, { strategy, symbol }, universeSource);
628
+ }
629
+ }
630
+ }
631
+ const strategySymbolsEntries = await Promise.all(
632
+ configuredStrategies.map(
633
+ async (strategy) => [
634
+ strategy,
635
+ await loadStrategyResultSymbols({ userName, strategy })
636
+ ]
637
+ )
638
+ );
639
+ for (const [strategy, symbols] of strategySymbolsEntries) {
640
+ for (const symbol of symbols) {
641
+ if (explicitSymbolSet && !explicitSymbolSet.has(symbol)) {
642
+ continue;
643
+ }
644
+ addReplayTarget(targets, { strategy, symbol }, "strategyResults");
645
+ }
646
+ }
647
+ return [...targets.values()].sort(
648
+ (left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol)
649
+ );
650
+ };
651
+ var createConnector = async ({
652
+ userName,
653
+ connectorName
654
+ }) => {
655
+ const connectorFactory = await (0, import_connectors2.getConnectorCreatorByName)(
656
+ connectorName,
657
+ projectRoot
658
+ );
659
+ if (!connectorFactory) {
660
+ throw new Error(`Connector "${connectorName}" is not registered`);
661
+ }
662
+ return await connectorFactory({ userName });
663
+ };
664
+ var buildReplayAiAnalyses = ({
665
+ runtimeTrades,
666
+ runtimeSignalEvaluations,
667
+ strategy,
668
+ symbol,
669
+ toleranceMs
670
+ }) => {
671
+ const snapshots = [
672
+ ...runtimeTrades.filter(
673
+ (trade) => trade.strategy === strategy && trade.symbol === symbol && trade.aiAnalysis && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp)
674
+ ).map((trade) => ({
675
+ strategy: trade.strategy,
676
+ symbol: trade.symbol,
677
+ direction: trade.direction,
678
+ timestamp: trade.entryTimestamp,
679
+ toleranceMs,
680
+ analysis: trade.aiAnalysis
681
+ })),
682
+ ...runtimeSignalEvaluations.filter(
683
+ (evaluation) => evaluation.strategy === strategy && evaluation.symbol === symbol && evaluation.aiAnalysis && evaluation.direction && typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp)
684
+ ).map((evaluation) => ({
685
+ strategy: evaluation.strategy,
686
+ symbol: evaluation.symbol,
687
+ direction: evaluation.direction,
688
+ timestamp: evaluation.timestamp,
689
+ toleranceMs,
690
+ analysis: evaluation.aiAnalysis
691
+ }))
692
+ ];
693
+ const seen = /* @__PURE__ */ new Set();
694
+ return snapshots.filter((snapshot) => {
695
+ const key = `${snapshot.strategy}:${snapshot.symbol}:${snapshot.direction}:${snapshot.timestamp}`;
696
+ if (seen.has(key)) {
697
+ return false;
698
+ }
699
+ seen.add(key);
700
+ return true;
701
+ });
702
+ };
703
+ var buildReplayConfig = async ({
704
+ userName,
705
+ strategy,
706
+ symbol,
707
+ runtimeTrades,
708
+ runtimeSignalEvaluations,
709
+ toleranceMs
710
+ }) => {
711
+ const [userConfig, strategyResults] = await Promise.all([
712
+ (0, import_redis.getData)(import_redis.redisKeys.strategyConfig(userName, strategy), {}),
713
+ (0, import_redis.getData)(import_redis.redisKeys.strategyResults(userName, strategy), {})
714
+ ]);
715
+ const typedResults = strategyResults ?? {};
716
+ const symbolResult = typedResults?.[symbol];
717
+ const symbolConfig = symbolResult && symbolResult.config && typeof symbolResult.config === "object" ? symbolResult.config : {};
718
+ const aiReplayAnalyses = buildReplayAiAnalyses({
719
+ runtimeTrades,
720
+ runtimeSignalEvaluations,
721
+ strategy,
722
+ symbol,
723
+ toleranceMs
724
+ });
725
+ return {
726
+ ...userConfig,
727
+ ...symbolConfig,
728
+ ENV: REPLAY_ENV,
729
+ MAKE_ORDERS: true,
730
+ INTERVAL: interval,
731
+ RECORD_RUNTIME_TRADES: false,
732
+ ...aiReplayAnalyses.length ? { AI_REPLAY_ANALYSES: aiReplayAnalyses } : {}
733
+ };
734
+ };
735
+ var warmReplayHistory = async ({
736
+ userName,
737
+ connectorName,
738
+ targets,
739
+ preloadStart,
740
+ preloadEnd
741
+ }) => {
742
+ const connector = await createConnector({ userName, connectorName });
743
+ const symbols = [...new Set(targets.map((target) => target.symbol))];
744
+ const warmStartedAt = Date.now();
745
+ console.log(
746
+ import_chalk.default.gray(
747
+ `Warm history: ${symbols.length} symbol(s) on ${connectorName}, preload ${(0, import_time.formatUnix)(preloadStart)} -> ${(0, import_time.formatUnix)(preloadEnd)}`
748
+ )
749
+ );
750
+ await (0, import_cli2.update)(connector, interval, symbols, void 0, {
751
+ connectorLabel: connectorName,
752
+ preloadStart,
753
+ preloadEnd
754
+ });
755
+ const [binanceFactory, coinbaseFactory] = await Promise.all([
756
+ (0, import_connectors2.getConnectorCreatorByName)(import_connectors.ConnectorNames.Binance, projectRoot),
757
+ (0, import_connectors2.getConnectorCreatorByName)(import_connectors.ConnectorNames.Coinbase, projectRoot)
758
+ ]);
759
+ if (binanceFactory) {
760
+ const binanceConnector = await binanceFactory({
761
+ userName
762
+ });
763
+ await (0, import_cli2.update)(binanceConnector, interval, ["BTCUSDT"], void 0, {
764
+ connectorLabel: import_connectors.ConnectorNames.Binance,
765
+ preloadStart,
766
+ preloadEnd
767
+ });
768
+ } else {
769
+ import_logger.logger.warn(
770
+ "Binance connector is unavailable. BTC reference replay may drift."
771
+ );
772
+ }
773
+ if (coinbaseFactory) {
774
+ const coinbaseConnector = await coinbaseFactory({
775
+ userName
776
+ });
777
+ await (0, import_cli2.update)(coinbaseConnector, interval, ["BTCUSDT"], void 0, {
778
+ connectorLabel: import_connectors.ConnectorNames.Coinbase,
779
+ preloadStart,
780
+ preloadEnd
781
+ });
782
+ } else {
783
+ import_logger.logger.warn(
784
+ "Coinbase connector is unavailable. BTC reference replay may drift."
785
+ );
786
+ }
787
+ console.log(
788
+ import_chalk.default.gray(`Warm history: done in ${formatDuration(warmStartedAt)}`)
789
+ );
790
+ };
791
+ var formatPercent = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${value.toFixed(2)}%`;
792
+ var formatMinutes = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${(value / 6e4).toFixed(2)}m`;
793
+ var formatEntryLabel = (entry) => `${entry.strategy} ${entry.symbol} ${entry.direction} ${(0, import_time.formatUnix)(entry.timestamp)}`;
794
+ var printRuntimeDuplicateDetails = (groups) => {
795
+ if (!groups.length) {
796
+ return;
797
+ }
798
+ console.log("");
799
+ console.log(import_chalk.default.yellow("Runtime duplicates"));
800
+ for (const group of groups.slice(0, DETAIL_LIMIT)) {
801
+ const ids = group.entries.map((entry) => entry.orderId ?? entry.id);
802
+ console.log(
803
+ `- ${formatEntryLabel(group.entries[0])} count=${group.entries.length}, duplicateEntries=${group.entries.length - 1}, ids=${ids.join(",")}`
804
+ );
805
+ }
806
+ if (groups.length > DETAIL_LIMIT) {
807
+ console.log(`- ... ${groups.length - DETAIL_LIMIT} more`);
808
+ }
809
+ };
810
+ var findNearestRuntimeSignal = ({
811
+ entry,
812
+ runtimeSignals,
813
+ toleranceMs
814
+ }) => {
815
+ let bestSignal = null;
816
+ let bestDiff = Number.POSITIVE_INFINITY;
817
+ for (const signal of runtimeSignals) {
818
+ if (signal.strategy !== entry.strategy || signal.symbol !== entry.symbol || signal.direction !== entry.direction) {
819
+ continue;
820
+ }
821
+ const diff = Math.abs(signal.timestamp - entry.timestamp);
822
+ if (diff > toleranceMs || diff >= bestDiff) {
823
+ continue;
824
+ }
825
+ bestSignal = signal;
826
+ bestDiff = diff;
827
+ }
828
+ return bestSignal ? {
829
+ signal: bestSignal,
830
+ timestampDiffMs: bestDiff
831
+ } : null;
832
+ };
833
+ var findNearestRuntimeSignalEvaluation = ({
834
+ entry,
835
+ runtimeSignalEvaluations,
836
+ toleranceMs
837
+ }) => {
838
+ let bestEvaluation = null;
839
+ let bestDiff = Number.POSITIVE_INFINITY;
840
+ for (const evaluation of runtimeSignalEvaluations) {
841
+ if (evaluation.strategy !== entry.strategy || evaluation.symbol !== entry.symbol) {
842
+ continue;
843
+ }
844
+ const diff = Math.abs(evaluation.timestamp - entry.timestamp);
845
+ if (diff > toleranceMs || diff >= bestDiff) {
846
+ continue;
847
+ }
848
+ bestEvaluation = evaluation;
849
+ bestDiff = diff;
850
+ }
851
+ return bestEvaluation ? {
852
+ evaluation: bestEvaluation,
853
+ timestampDiffMs: bestDiff
854
+ } : null;
855
+ };
856
+ var findNearestBacktestEntryForRuntimeOnly = ({
857
+ entry,
858
+ backtestEntries
859
+ }) => {
860
+ let bestEntry = null;
861
+ let bestDiff = Number.POSITIVE_INFINITY;
862
+ for (const backtestEntry of backtestEntries) {
863
+ if (backtestEntry.strategy !== entry.strategy || backtestEntry.symbol !== entry.symbol || backtestEntry.direction !== entry.direction) {
864
+ continue;
865
+ }
866
+ const diff = Math.abs(backtestEntry.timestamp - entry.timestamp);
867
+ if (diff >= bestDiff) {
868
+ continue;
869
+ }
870
+ bestEntry = backtestEntry;
871
+ bestDiff = diff;
872
+ }
873
+ return bestEntry ? {
874
+ entry: bestEntry,
875
+ timestampDiffMs: bestDiff
876
+ } : null;
877
+ };
878
+ var buildSignalClassificationReason = ({
879
+ signal,
880
+ orderStatus,
881
+ classification
882
+ }) => {
883
+ const skipReason = typeof signal.orderSkipReason === "string" ? signal.orderSkipReason.trim() : "";
884
+ if (skipReason) {
885
+ return skipReason;
886
+ }
887
+ if (classification === "gated_out" && signal.ml?.passed === false) {
888
+ return `ml_probability=${signal.ml.probability.toFixed(4)} threshold=${signal.ml.threshold.toFixed(4)}`;
889
+ }
890
+ if (orderStatus === "completed") {
891
+ return "completed_signal_without_runtime_trade";
892
+ }
893
+ if (orderStatus !== "unknown") {
894
+ return `orderStatus=${orderStatus}`;
895
+ }
896
+ return "runtime_signal_without_completed_trade";
897
+ };
898
+ var buildEvaluationClassification = ({
899
+ entry,
900
+ evaluation
901
+ }) => {
902
+ if (evaluation.status === "skip") {
903
+ return {
904
+ classification: "core_skipped",
905
+ reason: evaluation.reason || "NO_SIGNAL"
906
+ };
907
+ }
908
+ if (evaluation.status === "error") {
909
+ return {
910
+ classification: "true_mismatch",
911
+ reason: evaluation.reason || "runtime_evaluation_error"
912
+ };
913
+ }
914
+ if (evaluation.direction && evaluation.direction !== entry.direction) {
915
+ return {
916
+ classification: "core_skipped",
917
+ reason: `runtime_signal_direction=${evaluation.direction}`
918
+ };
919
+ }
920
+ const orderStatus = normalizeSignalOrderStatus(evaluation.orderStatus);
921
+ if (orderStatus === "failed") {
922
+ return {
923
+ classification: "order_failed",
924
+ reason: evaluation.reason || "orderStatus=failed"
925
+ };
926
+ }
927
+ if (orderStatus === "skipped" || orderStatus === "canceled" || typeof evaluation.orderSkipReason === "string" && evaluation.orderSkipReason.trim()) {
928
+ return {
929
+ classification: "gated_out",
930
+ reason: evaluation.orderSkipReason || evaluation.reason || `orderStatus=${orderStatus}`
931
+ };
932
+ }
933
+ if (orderStatus === "completed") {
934
+ return {
935
+ classification: "true_mismatch",
936
+ reason: "completed_signal_without_runtime_trade"
937
+ };
938
+ }
939
+ return {
940
+ classification: "true_mismatch",
941
+ reason: evaluation.reason || "runtime_signal_without_completed_trade"
942
+ };
943
+ };
944
+ var buildRuntimeOnlyEvaluationClassification = ({
945
+ entry,
946
+ evaluation
947
+ }) => {
948
+ if (evaluation.status === "skip") {
949
+ return {
950
+ classification: "core_skipped",
951
+ reason: evaluation.reason || "NO_SIGNAL"
952
+ };
953
+ }
954
+ if (evaluation.status === "error") {
955
+ return {
956
+ classification: "true_mismatch",
957
+ reason: evaluation.reason || "replay_evaluation_error"
958
+ };
959
+ }
960
+ if (evaluation.direction && evaluation.direction !== entry.direction) {
961
+ return {
962
+ classification: "true_mismatch",
963
+ reason: `replay_signal_direction=${evaluation.direction}`
964
+ };
965
+ }
966
+ const orderStatus = normalizeSignalOrderStatus(evaluation.orderStatus);
967
+ if (orderStatus === "failed") {
968
+ return {
969
+ classification: "order_failed",
970
+ reason: evaluation.reason || "orderStatus=failed"
971
+ };
972
+ }
973
+ if (orderStatus === "skipped" || orderStatus === "canceled" || typeof evaluation.orderSkipReason === "string" && evaluation.orderSkipReason.trim()) {
974
+ return {
975
+ classification: "gated_out",
976
+ reason: evaluation.orderSkipReason || evaluation.reason || `orderStatus=${orderStatus}`
977
+ };
978
+ }
979
+ if (orderStatus === "completed") {
980
+ return {
981
+ classification: "true_mismatch",
982
+ reason: "completed_replay_signal_without_backtest_trade"
983
+ };
984
+ }
985
+ return {
986
+ classification: "true_mismatch",
987
+ reason: evaluation.reason || "runtime_trade_without_replay_trade"
988
+ };
989
+ };
990
+ var classifyBacktestOnlyEntries = ({
991
+ entries,
992
+ runtimeSignals,
993
+ runtimeSignalEvaluations,
994
+ toleranceMs
995
+ }) => entries.map((entry) => {
996
+ const nearestSignal = findNearestRuntimeSignal({
997
+ entry,
998
+ runtimeSignals,
999
+ toleranceMs
1000
+ });
1001
+ if (!nearestSignal) {
1002
+ const nearestEvaluation = findNearestRuntimeSignalEvaluation({
1003
+ entry,
1004
+ runtimeSignalEvaluations,
1005
+ toleranceMs
1006
+ });
1007
+ if (nearestEvaluation) {
1008
+ const evaluationClassification = buildEvaluationClassification({
1009
+ entry,
1010
+ evaluation: nearestEvaluation.evaluation
1011
+ });
1012
+ return {
1013
+ entry,
1014
+ ...evaluationClassification,
1015
+ evaluation: nearestEvaluation.evaluation,
1016
+ evaluationTimestampDiffMs: nearestEvaluation.timestampDiffMs
1017
+ };
1018
+ }
1019
+ return {
1020
+ entry,
1021
+ classification: "not_evaluated",
1022
+ reason: "no_runtime_evaluation"
1023
+ };
1024
+ }
1025
+ const orderStatus = normalizeSignalOrderStatus(
1026
+ nearestSignal.signal.orderStatus
1027
+ );
1028
+ let classification;
1029
+ if (orderStatus === "failed") {
1030
+ classification = "order_failed";
1031
+ } else if (orderStatus === "skipped" || orderStatus === "canceled" || nearestSignal.signal.ml?.passed === false || typeof nearestSignal.signal.orderSkipReason === "string" && nearestSignal.signal.orderSkipReason.trim()) {
1032
+ classification = "gated_out";
1033
+ } else {
1034
+ classification = "true_mismatch";
1035
+ }
1036
+ return {
1037
+ entry,
1038
+ classification,
1039
+ reason: buildSignalClassificationReason({
1040
+ signal: nearestSignal.signal,
1041
+ orderStatus,
1042
+ classification
1043
+ }),
1044
+ signal: nearestSignal.signal,
1045
+ signalTimestampDiffMs: nearestSignal.timestampDiffMs
1046
+ };
1047
+ });
1048
+ var classifyRuntimeOnlyEntries = ({
1049
+ entries,
1050
+ replaySignalEvaluations,
1051
+ backtestEntries,
1052
+ toleranceMs
1053
+ }) => entries.map((entry) => {
1054
+ const nearestBacktestEntry = findNearestBacktestEntryForRuntimeOnly({
1055
+ entry,
1056
+ backtestEntries
1057
+ });
1058
+ if (nearestBacktestEntry && nearestBacktestEntry.timestampDiffMs > toleranceMs) {
1059
+ return {
1060
+ entry,
1061
+ classification: "backtest_drift",
1062
+ reason: `nearest_backtest_entry_drift=${formatMinutes(nearestBacktestEntry.timestampDiffMs)}`,
1063
+ nearestBacktestEntry: nearestBacktestEntry.entry,
1064
+ nearestBacktestEntryTimestampDiffMs: nearestBacktestEntry.timestampDiffMs
1065
+ };
1066
+ }
1067
+ const nearestEvaluation = findNearestRuntimeSignalEvaluation({
1068
+ entry,
1069
+ runtimeSignalEvaluations: replaySignalEvaluations,
1070
+ toleranceMs
1071
+ });
1072
+ if (!nearestEvaluation) {
1073
+ return {
1074
+ entry,
1075
+ classification: "not_evaluated",
1076
+ reason: "no_replay_evaluation"
1077
+ };
1078
+ }
1079
+ return {
1080
+ entry,
1081
+ ...buildRuntimeOnlyEvaluationClassification({
1082
+ entry,
1083
+ evaluation: nearestEvaluation.evaluation
1084
+ }),
1085
+ evaluation: nearestEvaluation.evaluation,
1086
+ evaluationTimestampDiffMs: nearestEvaluation.timestampDiffMs
1087
+ };
1088
+ });
1089
+ var summarizeBacktestOnlyClassifications = (classifiedEntries) => {
1090
+ const counts = new Map(
1091
+ BACKTEST_ONLY_CLASSIFICATIONS.map((classification) => [classification, 0])
1092
+ );
1093
+ for (const item of classifiedEntries) {
1094
+ counts.set(item.classification, (counts.get(item.classification) ?? 0) + 1);
1095
+ }
1096
+ return BACKTEST_ONLY_CLASSIFICATIONS.map(
1097
+ (classification) => `${classification}=${counts.get(classification) ?? 0}`
1098
+ ).join(", ");
1099
+ };
1100
+ var summarizeRuntimeOnlyClassifications = (classifiedEntries) => {
1101
+ const counts = new Map(
1102
+ RUNTIME_ONLY_CLASSIFICATIONS.map((classification) => [classification, 0])
1103
+ );
1104
+ for (const item of classifiedEntries) {
1105
+ counts.set(item.classification, (counts.get(item.classification) ?? 0) + 1);
1106
+ }
1107
+ return RUNTIME_ONLY_CLASSIFICATIONS.map(
1108
+ (classification) => `${classification}=${counts.get(classification) ?? 0}`
1109
+ ).join(", ");
1110
+ };
1111
+ var printClassifiedBacktestOnlyDetails = (classifiedEntries) => {
1112
+ if (!classifiedEntries.length) {
1113
+ return;
1114
+ }
1115
+ console.log("");
1116
+ console.log(import_chalk.default.yellow("Backtest only"));
1117
+ for (const item of classifiedEntries.slice(0, DETAIL_LIMIT)) {
1118
+ const evidenceSuffix = item.signal ? ` signalId=${item.signal.signalId} signalDrift=${formatMinutes(item.signalTimestampDiffMs ?? null)}` : item.evaluation ? ` evaluationId=${item.evaluation.evaluationId} evaluationDrift=${formatMinutes(item.evaluationTimestampDiffMs ?? null)}` : "";
1119
+ const price = item.entry.price == null ? "n/a" : item.entry.price.toFixed(6);
1120
+ console.log(
1121
+ `- [${item.classification}] ${formatEntryLabel(item.entry)} price=${price} id=${item.entry.id} reason=${item.reason}${evidenceSuffix}`
1122
+ );
1123
+ }
1124
+ if (classifiedEntries.length > DETAIL_LIMIT) {
1125
+ console.log(`- ... ${classifiedEntries.length - DETAIL_LIMIT} more`);
1126
+ }
1127
+ };
1128
+ var printClassifiedRuntimeOnlyDetails = (classifiedEntries) => {
1129
+ if (!classifiedEntries.length) {
1130
+ return;
1131
+ }
1132
+ console.log("");
1133
+ console.log(import_chalk.default.yellow("Runtime only"));
1134
+ for (const item of classifiedEntries.slice(0, DETAIL_LIMIT)) {
1135
+ const evidenceSuffix = item.nearestBacktestEntry ? ` nearestBacktestId=${item.nearestBacktestEntry.id} backtestDrift=${formatMinutes(item.nearestBacktestEntryTimestampDiffMs ?? null)}` : item.evaluation ? ` replayEvaluationId=${item.evaluation.evaluationId} replayDrift=${formatMinutes(item.evaluationTimestampDiffMs ?? null)} status=${item.evaluation.status}` : "";
1136
+ const price = item.entry.price == null ? "n/a" : item.entry.price.toFixed(6);
1137
+ console.log(
1138
+ `- [${item.classification}] ${formatEntryLabel(item.entry)} price=${price} id=${item.entry.id} reason=${item.reason}${evidenceSuffix}`
1139
+ );
1140
+ }
1141
+ if (classifiedEntries.length > DETAIL_LIMIT) {
1142
+ console.log(`- ... ${classifiedEntries.length - DETAIL_LIMIT} more`);
1143
+ }
1144
+ };
1145
+ var summarizeByStrategy = ({
1146
+ targets,
1147
+ successfulTargetKeys,
1148
+ replayErrors,
1149
+ runtimeEntries,
1150
+ runtimeDuplicateEntries,
1151
+ backtestEntries,
1152
+ matchedEntries,
1153
+ runtimeOnlyEntries,
1154
+ backtestOnlyEntries
1155
+ }) => {
1156
+ const rows = /* @__PURE__ */ new Map();
1157
+ const ensureRow = (strategy) => {
1158
+ const row = rows.get(strategy) ?? {
1159
+ runtime: 0,
1160
+ runtimeDuplicates: 0,
1161
+ backtest: 0,
1162
+ matched: 0,
1163
+ runtimeOnly: 0,
1164
+ backtestOnly: 0,
1165
+ targets: 0,
1166
+ compared: 0,
1167
+ errors: 0
1168
+ };
1169
+ rows.set(strategy, row);
1170
+ return row;
1171
+ };
1172
+ for (const target of targets) {
1173
+ const row = ensureRow(target.strategy);
1174
+ row.targets += 1;
1175
+ if (successfulTargetKeys.has(toTargetKey(target))) {
1176
+ row.compared += 1;
1177
+ }
1178
+ }
1179
+ for (const error of replayErrors) {
1180
+ ensureRow(error.strategy).errors += 1;
1181
+ }
1182
+ for (const entry of runtimeEntries) {
1183
+ ensureRow(entry.strategy).runtime += 1;
1184
+ }
1185
+ for (const entry of runtimeDuplicateEntries) {
1186
+ ensureRow(entry.strategy).runtimeDuplicates += 1;
1187
+ }
1188
+ for (const entry of backtestEntries) {
1189
+ ensureRow(entry.strategy).backtest += 1;
1190
+ }
1191
+ for (const entry of matchedEntries) {
1192
+ ensureRow(entry.runtime.strategy).matched += 1;
1193
+ }
1194
+ for (const entry of runtimeOnlyEntries) {
1195
+ ensureRow(entry.strategy).runtimeOnly += 1;
1196
+ }
1197
+ for (const entry of backtestOnlyEntries) {
1198
+ ensureRow(entry.strategy).backtestOnly += 1;
1199
+ }
1200
+ return [...rows.entries()].sort(
1201
+ ([left], [right]) => left.localeCompare(right)
1202
+ );
1203
+ };
1204
+ var buildRuntimeParityMessage = ({
1205
+ window,
1206
+ connectorName,
1207
+ replayEnv,
1208
+ runtimeGatesEnabled,
1209
+ toleranceBars,
1210
+ toleranceMs,
1211
+ replayTargetsCount,
1212
+ comparedTargetsCount,
1213
+ replayErrors,
1214
+ sourceCounts,
1215
+ rawRuntimeEntriesCount,
1216
+ runtimeEntriesCount,
1217
+ runtimeDuplicateEntriesCount,
1218
+ backtestEntriesCount,
1219
+ matchedCount,
1220
+ runtimeOnlyCount,
1221
+ backtestOnlyCount,
1222
+ matchedSummary,
1223
+ classifiedRuntimeOnly,
1224
+ classifiedBacktestOnly,
1225
+ runtimeSignalEvaluationsCount,
1226
+ strategyRows,
1227
+ runtimeGateWarningCounts
1228
+ }) => {
1229
+ const lines = [];
1230
+ lines.push("\u{1F9EA} <b>TradeJS runtime parity</b>");
1231
+ lines.push("");
1232
+ lines.push("\u{1F552} <b>Window</b>");
1233
+ lines.push(
1234
+ `<b>${escapeHtml(formatMskDateTime(window.start))} - ${escapeHtml(formatMskDateTime(window.end))} ${SUMMARY_TIMEZONE_LABEL}</b>`
1235
+ );
1236
+ lines.push("");
1237
+ lines.push(`\u{1F50C} Connector: <b>${escapeHtml(connectorName)}</b>`);
1238
+ lines.push(`\u{1F9EC} Replay env: <b>${escapeHtml(replayEnv)}</b>`);
1239
+ lines.push(
1240
+ `\u{1F6A6} Runtime gates: <b>${runtimeGatesEnabled ? "\u2705 enabled" : "\u26D4 disabled"}</b>`
1241
+ );
1242
+ lines.push(
1243
+ `\u{1F3AF} Tolerance: <b>${toleranceBars} bar(s) / ${(toleranceMs / 6e4).toFixed(0)}m</b>`
1244
+ );
1245
+ lines.push("");
1246
+ lines.push("\u{1F4CC} <b>Overview</b>");
1247
+ lines.push(
1248
+ `\u2022 Targets: <b>${replayTargetsCount}</b> / compared <b>${comparedTargetsCount}</b> / errors <b>${replayErrors.length}</b>`
1249
+ );
1250
+ lines.push(
1251
+ `\u2022 Sources: runtime=<b>${sourceCounts.runtime}</b>, universe=<b>${sourceCounts.connectorUniverse}</b>, explicit=<b>${sourceCounts.explicitTickers}</b>, results=<b>${sourceCounts.strategyResults}</b>`
1252
+ );
1253
+ lines.push("");
1254
+ lines.push("\u{1F4C8} <b>Entries</b>");
1255
+ lines.push(
1256
+ `\u2022 Runtime: <b>${rawRuntimeEntriesCount}</b> (deduped <b>${runtimeEntriesCount}</b>, dup <b>${runtimeDuplicateEntriesCount}</b>)`
1257
+ );
1258
+ lines.push(`\u2022 Backtest: <b>${backtestEntriesCount}</b>`);
1259
+ lines.push(`\u2022 Matched: <b>${matchedCount}</b>`);
1260
+ lines.push(
1261
+ `\u2022 Runtime only: <b>${runtimeOnlyCount}</b> / Backtest only: <b>${backtestOnlyCount}</b>`
1262
+ );
1263
+ lines.push(
1264
+ `\u2022 Deltas: price <b>${escapeHtml(formatPercent(matchedSummary.avgPriceDeltaPct))} / ${escapeHtml(formatPercent(matchedSummary.maxPriceDeltaPct))}</b>, drift <b>${escapeHtml(formatMinutes(matchedSummary.avgTimestampDiffMs))} / ${escapeHtml(formatMinutes(matchedSummary.maxTimestampDiffMs))}</b>`
1265
+ );
1266
+ if (classifiedRuntimeOnly.length) {
1267
+ lines.push(
1268
+ `\u2022 Runtime-only classes: <code>${escapeHtml(summarizeRuntimeOnlyClassifications(classifiedRuntimeOnly))}</code>`
1269
+ );
1270
+ }
1271
+ if (classifiedBacktestOnly.length) {
1272
+ lines.push(
1273
+ `\u2022 Backtest-only classes: <code>${escapeHtml(summarizeBacktestOnlyClassifications(classifiedBacktestOnly))}</code>`
1274
+ );
1275
+ }
1276
+ if (runtimeSignalEvaluationsCount) {
1277
+ lines.push(
1278
+ `\u2022 Runtime evaluations: <b>${runtimeSignalEvaluationsCount}</b>`
1279
+ );
1280
+ }
1281
+ if (strategyRows.length) {
1282
+ lines.push("");
1283
+ lines.push("\u{1F4CA} <b>By strategy</b>");
1284
+ for (const [strategy, row] of strategyRows) {
1285
+ lines.push("");
1286
+ lines.push(`<b>${escapeHtml(strategy)}</b>`);
1287
+ lines.push(
1288
+ `\u2022 targets=<b>${row.targets}</b>, compared=<b>${row.compared}</b>, errors=<b>${row.errors}</b>`
1289
+ );
1290
+ lines.push(
1291
+ `\u2022 runtime=<b>${row.runtime}</b> (dup <b>${row.runtimeDuplicates}</b>), backtest=<b>${row.backtest}</b>, matched=<b>${row.matched}</b>`
1292
+ );
1293
+ lines.push(
1294
+ `\u2022 runtimeOnly=<b>${row.runtimeOnly}</b>, backtestOnly=<b>${row.backtestOnly}</b>`
1295
+ );
1296
+ }
1297
+ }
1298
+ if (runtimeGateWarningCounts.size && !runtimeGatesEnabled) {
1299
+ lines.push("");
1300
+ lines.push("\u26A0\uFE0F <b>Warnings</b>");
1301
+ for (const [strategy, count] of [
1302
+ ...runtimeGateWarningCounts.entries()
1303
+ ].sort(([left], [right]) => left.localeCompare(right))) {
1304
+ lines.push(
1305
+ `\u2022 <b>${escapeHtml(strategy)}</b>: AI/ML runtime gates configured on <b>${count}</b> target(s); BACKTEST replay covers core execution only.`
1306
+ );
1307
+ }
1308
+ }
1309
+ if (replayErrors.length) {
1310
+ lines.push("");
1311
+ lines.push("\u274C <b>Replay errors</b>");
1312
+ for (const error of replayErrors.slice(0, TELEGRAM_DETAIL_LIMIT)) {
1313
+ lines.push(
1314
+ `\u2022 <b>${escapeHtml(error.strategy)}</b> ${escapeHtml(error.symbol)}: <code>${escapeHtml(error.message)}</code>`
1315
+ );
1316
+ }
1317
+ if (replayErrors.length > TELEGRAM_DETAIL_LIMIT) {
1318
+ lines.push(
1319
+ `... <b>${replayErrors.length - TELEGRAM_DETAIL_LIMIT}</b> more`
1320
+ );
1321
+ }
1322
+ }
1323
+ return lines.join("\n");
1324
+ };
1325
+ var buildRuntimeParityNoTargetsMessage = ({
1326
+ window,
1327
+ connectorName,
1328
+ replayEnv,
1329
+ runtimeGatesEnabled,
1330
+ userName
1331
+ }) => [
1332
+ "\u{1F9EA} <b>TradeJS runtime parity</b>",
1333
+ "",
1334
+ "\u{1F552} <b>Window</b>",
1335
+ `<b>${escapeHtml(formatMskDateTime(window.start))} - ${escapeHtml(formatMskDateTime(window.end))} ${SUMMARY_TIMEZONE_LABEL}</b>`,
1336
+ "",
1337
+ `\u{1F50C} Connector: <b>${escapeHtml(connectorName)}</b>`,
1338
+ `\u{1F9EC} Replay env: <b>${escapeHtml(replayEnv)}</b>`,
1339
+ `\u{1F6A6} Runtime gates: <b>${runtimeGatesEnabled ? "\u2705 enabled" : "\u26D4 disabled"}</b>`,
1340
+ "",
1341
+ `\u26A0\uFE0F No replay targets found for user <b>${escapeHtml(userName)}</b>.`
1342
+ ].join("\n");
1343
+ var runtimeParity = async () => {
1344
+ const window = resolveTimeWindow({
1345
+ days: flags.days ?? DEFAULT_LOOKBACK_DAYS,
1346
+ startTime: flags.startTime,
1347
+ endTime: flags.endTime,
1348
+ defaultStartMs: Date.now() - DEFAULT_LOOKBACK_DAYS * 24 * 60 * 60 * 1e3,
1349
+ defaultEndMs: Date.now()
1350
+ });
1351
+ const toleranceBars = Math.max(
1352
+ 0,
1353
+ Number.parseInt(String(flags.toleranceBars ?? 1), 10) || 0
1354
+ );
1355
+ const toleranceMs = toleranceBars * 15 * 60 * 1e3;
1356
+ const connectorName = await resolveParityConnectorName(flags.connector);
1357
+ const requestedSymbols = parseSymbolsFromCLI(String(flags.tickers || ""));
1358
+ const requestedSymbolSet = requestedSymbols.length ? new Set(requestedSymbols) : null;
1359
+ let replayErrors = [];
1360
+ try {
1361
+ const parityConnector = await createConnector({
1362
+ userName: flags.user,
1363
+ connectorName
1364
+ });
1365
+ const [allRuntimeTrades, allRuntimeSignals, allRuntimeSignalEvaluations] = await Promise.all([
1366
+ loadRuntimeTrades(flags.user),
1367
+ loadRuntimeSignals(flags.user),
1368
+ loadRuntimeSignalEvaluations(flags.user)
1369
+ ]);
1370
+ const runtimeTrades = allRuntimeTrades.filter(
1371
+ (trade) => trade.entryTimestamp >= window.start && trade.entryTimestamp <= window.end && (!flags.strategy || trade.strategy === flags.strategy)
1372
+ );
1373
+ const runtimeSignals = allRuntimeSignals.filter(
1374
+ (signal) => signal.timestamp >= window.start && signal.timestamp <= window.end && (!flags.strategy || signal.strategy === flags.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(signal.symbol))
1375
+ );
1376
+ const runtimeSignalEvaluations = allRuntimeSignalEvaluations.filter(
1377
+ (evaluation) => evaluation.timestamp >= window.start && evaluation.timestamp <= window.end && (!flags.strategy || evaluation.strategy === flags.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(evaluation.symbol))
1378
+ );
1379
+ const connectorSymbols = requestedSymbols.length ? requestedSymbols : await (0, import_cli2.getTickers)(parityConnector);
1380
+ const replayTargets = await buildReplayTargets({
1381
+ userName: flags.user,
1382
+ runtimeTrades,
1383
+ connectorSymbols,
1384
+ strategyFilter: String(flags.strategy || "").trim() || void 0,
1385
+ explicitSymbols: requestedSymbols,
1386
+ includeConnectorUniverse: Boolean(flags.fullUniverse)
1387
+ });
1388
+ if (!replayTargets.length) {
1389
+ console.log(
1390
+ import_chalk.default.yellow(
1391
+ `No replay targets found for ${flags.user} in ${(0, import_time.formatUnix)(window.start)} -> ${(0, import_time.formatUnix)(window.end)}.`
1392
+ )
1393
+ );
1394
+ if (flags.notify) {
1395
+ await sendTelegramReport(
1396
+ buildRuntimeParityNoTargetsMessage({
1397
+ window,
1398
+ connectorName,
1399
+ replayEnv: REPLAY_ENV,
1400
+ runtimeGatesEnabled: Boolean(flags.runtimeGates),
1401
+ userName: String(flags.user)
1402
+ }),
1403
+ { userName: flags.user }
1404
+ );
1405
+ }
1406
+ return;
1407
+ }
1408
+ const sourceCounts = countReplayTargetSources(replayTargets);
1409
+ const preloadStart = (0, import_time.getBacktestPreloadStart)(
1410
+ window.start,
1411
+ import_constants.BACKTEST_PRELOAD_DAYS
1412
+ );
1413
+ const replayStartedAt = Date.now();
1414
+ const replaySymbolsCount = new Set(
1415
+ replayTargets.map((target) => target.symbol)
1416
+ ).size;
1417
+ console.log(
1418
+ import_chalk.default.gray(
1419
+ `Replay queue: ${replayTargets.length} target(s), ${replaySymbolsCount} symbol(s), mode=${flags.fullUniverse ? "full-universe" : requestedSymbols.length ? "explicit-tickers" : "runtime+results"}`
1420
+ )
1421
+ );
1422
+ if (!flags.cacheOnly) {
1423
+ await warmReplayHistory({
1424
+ userName: flags.user,
1425
+ connectorName,
1426
+ targets: replayTargets,
1427
+ preloadStart,
1428
+ preloadEnd: window.end
1429
+ });
1430
+ }
1431
+ const backtestEntries = [];
1432
+ const replaySignalEvaluations = [];
1433
+ const successfulTargetKeys = /* @__PURE__ */ new Set();
1434
+ const runtimeGateWarningCounts = /* @__PURE__ */ new Map();
1435
+ for (const [targetIndex, target] of replayTargets.entries()) {
1436
+ const progressPosition = targetIndex + 1;
1437
+ try {
1438
+ const replayConfig = await buildReplayConfig({
1439
+ userName: flags.user,
1440
+ strategy: target.strategy,
1441
+ symbol: target.symbol,
1442
+ runtimeTrades,
1443
+ runtimeSignalEvaluations,
1444
+ toleranceMs
1445
+ });
1446
+ if (replayConfig.AI_ENABLED === true || replayConfig.ML_ENABLED === true) {
1447
+ runtimeGateWarningCounts.set(
1448
+ target.strategy,
1449
+ (runtimeGateWarningCounts.get(target.strategy) ?? 0) + 1
1450
+ );
1451
+ }
1452
+ const result = await (0, import_backtest.testing)({
1453
+ userName: flags.user,
1454
+ symbol: target.symbol,
1455
+ options: {
1456
+ start: window.start,
1457
+ end: window.end
1458
+ },
1459
+ name: `${target.symbol}_${target.strategy}_${(0, import_node_crypto.randomUUID)().slice(0, 8)}`,
1460
+ testId: (0, import_node_crypto.randomUUID)().slice(0, 8),
1461
+ testSuiteId: (0, import_node_crypto.randomUUID)().slice(0, 8),
1462
+ strategyName: target.strategy,
1463
+ strategyConfig: replayConfig,
1464
+ connectorName,
1465
+ timeoutMs: 12e4
1466
+ });
1467
+ backtestEntries.push(
1468
+ ...extractBacktestEntryParityEntries(result?.inlineOrderLog)
1469
+ );
1470
+ replaySignalEvaluations.push(
1471
+ ...result?.inlineReplaySignalEvaluations ?? []
1472
+ );
1473
+ successfulTargetKeys.add(toTargetKey(target));
1474
+ } catch (error) {
1475
+ replayErrors.push({
1476
+ ...target,
1477
+ message: error?.message || String(error)
1478
+ });
1479
+ }
1480
+ if (progressPosition === 1 || progressPosition === replayTargets.length || progressPosition % 25 === 0) {
1481
+ console.log(
1482
+ import_chalk.default.gray(
1483
+ `Replay progress: ${progressPosition}/${replayTargets.length}, ok=${successfulTargetKeys.size}, errors=${replayErrors.length}, current=${target.strategy} ${target.symbol}, elapsed=${formatDuration(replayStartedAt)}`
1484
+ )
1485
+ );
1486
+ }
1487
+ }
1488
+ const comparableRuntimeTrades = runtimeTrades.filter(
1489
+ (trade) => successfulTargetKeys.has(toTargetKey(trade))
1490
+ );
1491
+ const rawRuntimeEntries = extractRuntimeParityEntries(
1492
+ comparableRuntimeTrades
1493
+ );
1494
+ const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
1495
+ const runtimeEntries = runtimeDedupe.entries;
1496
+ const comparison = compareTradeParityEntries({
1497
+ runtimeEntries,
1498
+ backtestEntries,
1499
+ toleranceMs
1500
+ });
1501
+ const classifiedBacktestOnly = classifyBacktestOnlyEntries({
1502
+ entries: comparison.backtestOnly,
1503
+ runtimeSignals,
1504
+ runtimeSignalEvaluations,
1505
+ toleranceMs
1506
+ });
1507
+ const classifiedRuntimeOnly = classifyRuntimeOnlyEntries({
1508
+ entries: comparison.runtimeOnly,
1509
+ replaySignalEvaluations,
1510
+ backtestEntries,
1511
+ toleranceMs
1512
+ });
1513
+ const summary = summarizeMatchedParity(comparison.matched);
1514
+ console.log(import_chalk.default.cyan("TradeJS runtime parity"));
1515
+ console.log(
1516
+ `Window: ${(0, import_time.formatUnix)(window.start)} -> ${(0, import_time.formatUnix)(window.end)} (${window.source})`
1517
+ );
1518
+ console.log(`Connector: ${connectorName}`);
1519
+ console.log(
1520
+ `Replay env: ${REPLAY_ENV}${flags.runtimeGates ? " (runtime AI/ML gates enabled)" : " (core/backtest gates only)"}`
1521
+ );
1522
+ console.log(
1523
+ `Tolerance: ${toleranceBars} bar(s) / ${(toleranceMs / 6e4).toFixed(0)}m`
1524
+ );
1525
+ console.log(
1526
+ `Targets: ${replayTargets.length}, compared: ${successfulTargetKeys.size}, replayErrors: ${replayErrors.length}`
1527
+ );
1528
+ console.log(
1529
+ `Target sources: runtime=${sourceCounts.runtime}, connectorUniverse=${sourceCounts.connectorUniverse}, explicitTickers=${sourceCounts.explicitTickers}, strategyResults=${sourceCounts.strategyResults}`
1530
+ );
1531
+ console.log("");
1532
+ console.log(
1533
+ `Runtime entries: ${rawRuntimeEntries.length} (deduped: ${runtimeEntries.length}, duplicates: ${runtimeDedupe.duplicateEntries.length}), backtest entries: ${backtestEntries.length}, matched: ${comparison.matched.length}, runtimeOnly: ${comparison.runtimeOnly.length}, backtestOnly: ${comparison.backtestOnly.length}`
1534
+ );
1535
+ console.log(
1536
+ `Matched price delta avg/max: ${formatPercent(summary.avgPriceDeltaPct)} / ${formatPercent(summary.maxPriceDeltaPct)}`
1537
+ );
1538
+ console.log(
1539
+ `Matched timestamp drift avg/max: ${formatMinutes(summary.avgTimestampDiffMs)} / ${formatMinutes(summary.maxTimestampDiffMs)}`
1540
+ );
1541
+ if (runtimeDedupe.duplicateGroups.length) {
1542
+ console.log(
1543
+ `Runtime duplicate groups: ${runtimeDedupe.duplicateGroups.length}, duplicate entries: ${runtimeDedupe.duplicateEntries.length}`
1544
+ );
1545
+ }
1546
+ if (classifiedRuntimeOnly.length) {
1547
+ console.log(
1548
+ `Runtime only classifications: ${summarizeRuntimeOnlyClassifications(classifiedRuntimeOnly)}`
1549
+ );
1550
+ }
1551
+ if (classifiedBacktestOnly.length) {
1552
+ console.log(
1553
+ `Backtest only classifications: ${summarizeBacktestOnlyClassifications(classifiedBacktestOnly)}`
1554
+ );
1555
+ }
1556
+ if (runtimeSignalEvaluations.length) {
1557
+ console.log(`Runtime evaluations: ${runtimeSignalEvaluations.length}`);
1558
+ }
1559
+ const strategyRows = summarizeByStrategy({
1560
+ targets: replayTargets,
1561
+ successfulTargetKeys,
1562
+ replayErrors,
1563
+ runtimeEntries,
1564
+ runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
1565
+ backtestEntries,
1566
+ matchedEntries: comparison.matched,
1567
+ runtimeOnlyEntries: comparison.runtimeOnly,
1568
+ backtestOnlyEntries: comparison.backtestOnly
1569
+ });
1570
+ if (strategyRows.length) {
1571
+ console.log("");
1572
+ console.log(import_chalk.default.cyan("By strategy"));
1573
+ for (const [strategy, row] of strategyRows) {
1574
+ console.log(
1575
+ `- ${strategy}: targets=${row.targets}, compared=${row.compared}, errors=${row.errors}, runtime=${row.runtime}, runtimeDuplicates=${row.runtimeDuplicates}, backtest=${row.backtest}, matched=${row.matched}, runtimeOnly=${row.runtimeOnly}, backtestOnly=${row.backtestOnly}`
1576
+ );
1577
+ }
1578
+ }
1579
+ if (runtimeGateWarningCounts.size && !flags.runtimeGates) {
1580
+ console.log("");
1581
+ console.log(import_chalk.default.yellow("Warnings"));
1582
+ for (const [strategy, count] of [
1583
+ ...runtimeGateWarningCounts.entries()
1584
+ ].sort(([left], [right]) => left.localeCompare(right))) {
1585
+ console.log(
1586
+ `- ${strategy} uses AI/ML runtime gates on ${count} replay target(s); BACKTEST replay covers core execution, not live gating.`
1587
+ );
1588
+ }
1589
+ }
1590
+ if (replayErrors.length) {
1591
+ console.log("");
1592
+ console.log(import_chalk.default.red("Replay errors"));
1593
+ for (const error of replayErrors.slice(0, DETAIL_LIMIT)) {
1594
+ console.log(`- ${error.strategy} ${error.symbol}: ${error.message}`);
1595
+ }
1596
+ if (replayErrors.length > DETAIL_LIMIT) {
1597
+ console.log(`- ... ${replayErrors.length - DETAIL_LIMIT} more`);
1598
+ }
1599
+ }
1600
+ if (flags.details) {
1601
+ printRuntimeDuplicateDetails(runtimeDedupe.duplicateGroups);
1602
+ printClassifiedRuntimeOnlyDetails(classifiedRuntimeOnly);
1603
+ printClassifiedBacktestOnlyDetails(classifiedBacktestOnly);
1604
+ }
1605
+ if (flags.notify) {
1606
+ await sendTelegramReport(
1607
+ buildRuntimeParityMessage({
1608
+ window,
1609
+ connectorName,
1610
+ replayEnv: REPLAY_ENV,
1611
+ runtimeGatesEnabled: Boolean(flags.runtimeGates),
1612
+ toleranceBars,
1613
+ toleranceMs,
1614
+ replayTargetsCount: replayTargets.length,
1615
+ comparedTargetsCount: successfulTargetKeys.size,
1616
+ replayErrors,
1617
+ sourceCounts,
1618
+ rawRuntimeEntriesCount: rawRuntimeEntries.length,
1619
+ runtimeEntriesCount: runtimeEntries.length,
1620
+ runtimeDuplicateEntriesCount: runtimeDedupe.duplicateEntries.length,
1621
+ backtestEntriesCount: backtestEntries.length,
1622
+ matchedCount: comparison.matched.length,
1623
+ runtimeOnlyCount: comparison.runtimeOnly.length,
1624
+ backtestOnlyCount: comparison.backtestOnly.length,
1625
+ matchedSummary: summary,
1626
+ classifiedRuntimeOnly,
1627
+ classifiedBacktestOnly,
1628
+ runtimeSignalEvaluationsCount: runtimeSignalEvaluations.length,
1629
+ strategyRows,
1630
+ runtimeGateWarningCounts
1631
+ }),
1632
+ { userName: flags.user }
1633
+ );
1634
+ }
1635
+ } finally {
1636
+ (0, import_backtest.resetTestingKlineCache)(projectRoot);
1637
+ }
1638
+ };
1639
+ if (process.env.NODE_ENV !== "test") {
1640
+ runtimeParity().then(() => {
1641
+ process.exit(0);
1642
+ }).catch((error) => {
1643
+ console.error(error);
1644
+ process.exit(1);
1645
+ });
1646
+ }
1647
+ // Annotate the CommonJS export names for ESM import in node:
1648
+ 0 && (module.exports = {
1649
+ buildRuntimeParityMessage,
1650
+ runtimeParity
1651
+ });