@tradejs/cli 1.0.6 → 1.0.9

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