@tradejs/cli 2.0.5 → 2.0.7

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.
@@ -34,9 +34,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
34
34
  ));
35
35
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
36
 
37
- // ../../node_modules/debug/node_modules/ms/index.js
37
+ // ../../node_modules/ms/index.js
38
38
  var require_ms = __commonJS({
39
- "../../node_modules/debug/node_modules/ms/index.js"(exports2, module2) {
39
+ "../../node_modules/ms/index.js"(exports2, module2) {
40
40
  "use strict";
41
41
  var s = 1e3;
42
42
  var m = s * 60;
@@ -151,9 +151,9 @@ var require_ms = __commonJS({
151
151
  }
152
152
  });
153
153
 
154
- // ../../node_modules/debug/src/common.js
154
+ // ../../node_modules/list-it/node_modules/debug/src/common.js
155
155
  var require_common = __commonJS({
156
- "../../node_modules/debug/src/common.js"(exports2, module2) {
156
+ "../../node_modules/list-it/node_modules/debug/src/common.js"(exports2, module2) {
157
157
  "use strict";
158
158
  function setup(env) {
159
159
  createDebug.debug = createDebug;
@@ -255,50 +255,64 @@ var require_common = __commonJS({
255
255
  createDebug.namespaces = namespaces;
256
256
  createDebug.names = [];
257
257
  createDebug.skips = [];
258
- let i;
259
- const split = (typeof namespaces === "string" ? namespaces : "").split(/[\s,]+/);
260
- const len = split.length;
261
- for (i = 0; i < len; i++) {
262
- if (!split[i]) {
263
- continue;
258
+ const split = (typeof namespaces === "string" ? namespaces : "").trim().replace(/\s+/g, ",").split(",").filter(Boolean);
259
+ for (const ns of split) {
260
+ if (ns[0] === "-") {
261
+ createDebug.skips.push(ns.slice(1));
262
+ } else {
263
+ createDebug.names.push(ns);
264
264
  }
265
- namespaces = split[i].replace(/\*/g, ".*?");
266
- if (namespaces[0] === "-") {
267
- createDebug.skips.push(new RegExp("^" + namespaces.slice(1) + "$"));
265
+ }
266
+ }
267
+ function matchesTemplate(search, template) {
268
+ let searchIndex = 0;
269
+ let templateIndex = 0;
270
+ let starIndex = -1;
271
+ let matchIndex = 0;
272
+ while (searchIndex < search.length) {
273
+ if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) {
274
+ if (template[templateIndex] === "*") {
275
+ starIndex = templateIndex;
276
+ matchIndex = searchIndex;
277
+ templateIndex++;
278
+ } else {
279
+ searchIndex++;
280
+ templateIndex++;
281
+ }
282
+ } else if (starIndex !== -1) {
283
+ templateIndex = starIndex + 1;
284
+ matchIndex++;
285
+ searchIndex = matchIndex;
268
286
  } else {
269
- createDebug.names.push(new RegExp("^" + namespaces + "$"));
287
+ return false;
270
288
  }
271
289
  }
290
+ while (templateIndex < template.length && template[templateIndex] === "*") {
291
+ templateIndex++;
292
+ }
293
+ return templateIndex === template.length;
272
294
  }
273
295
  function disable() {
274
296
  const namespaces = [
275
- ...createDebug.names.map(toNamespace),
276
- ...createDebug.skips.map(toNamespace).map((namespace) => "-" + namespace)
297
+ ...createDebug.names,
298
+ ...createDebug.skips.map((namespace) => "-" + namespace)
277
299
  ].join(",");
278
300
  createDebug.enable("");
279
301
  return namespaces;
280
302
  }
281
303
  function enabled(name) {
282
- if (name[name.length - 1] === "*") {
283
- return true;
284
- }
285
- let i;
286
- let len;
287
- for (i = 0, len = createDebug.skips.length; i < len; i++) {
288
- if (createDebug.skips[i].test(name)) {
304
+ for (const skip of createDebug.skips) {
305
+ if (matchesTemplate(name, skip)) {
289
306
  return false;
290
307
  }
291
308
  }
292
- for (i = 0, len = createDebug.names.length; i < len; i++) {
293
- if (createDebug.names[i].test(name)) {
309
+ for (const ns of createDebug.names) {
310
+ if (matchesTemplate(name, ns)) {
294
311
  return true;
295
312
  }
296
313
  }
297
314
  return false;
298
315
  }
299
- function toNamespace(regexp) {
300
- return regexp.toString().substring(2, regexp.toString().length - 2).replace(/\.\*\?$/, "*");
301
- }
302
316
  function coerce(val) {
303
317
  if (val instanceof Error) {
304
318
  return val.stack || val.message;
@@ -315,9 +329,9 @@ var require_common = __commonJS({
315
329
  }
316
330
  });
317
331
 
318
- // ../../node_modules/debug/src/browser.js
332
+ // ../../node_modules/list-it/node_modules/debug/src/browser.js
319
333
  var require_browser = __commonJS({
320
- "../../node_modules/debug/src/browser.js"(exports2, module2) {
334
+ "../../node_modules/list-it/node_modules/debug/src/browser.js"(exports2, module2) {
321
335
  "use strict";
322
336
  exports2.formatArgs = formatArgs;
323
337
  exports2.save = save;
@@ -418,10 +432,11 @@ var require_browser = __commonJS({
418
432
  if (typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) {
419
433
  return false;
420
434
  }
435
+ let m;
421
436
  return typeof document !== "undefined" && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || // Is firebug? http://stackoverflow.com/a/398120/376773
422
437
  typeof window !== "undefined" && window.console && (window.console.firebug || window.console.exception && window.console.table) || // Is firefox >= v31?
423
438
  // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
424
- typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31 || // Double check webkit in userAgent just in case we are in a worker
439
+ typeof navigator !== "undefined" && navigator.userAgent && (m = navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)) && parseInt(m[1], 10) >= 31 || // Double check webkit in userAgent just in case we are in a worker
425
440
  typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/);
426
441
  }
427
442
  function formatArgs(args2) {
@@ -459,7 +474,7 @@ var require_browser = __commonJS({
459
474
  function load() {
460
475
  let r;
461
476
  try {
462
- r = exports2.storage.getItem("debug");
477
+ r = exports2.storage.getItem("debug") || exports2.storage.getItem("DEBUG");
463
478
  } catch (error) {
464
479
  }
465
480
  if (!r && typeof process !== "undefined" && "env" in process) {
@@ -600,9 +615,9 @@ var require_supports_color = __commonJS({
600
615
  }
601
616
  });
602
617
 
603
- // ../../node_modules/debug/src/node.js
618
+ // ../../node_modules/list-it/node_modules/debug/src/node.js
604
619
  var require_node = __commonJS({
605
- "../../node_modules/debug/src/node.js"(exports2, module2) {
620
+ "../../node_modules/list-it/node_modules/debug/src/node.js"(exports2, module2) {
606
621
  "use strict";
607
622
  var tty = require("tty");
608
623
  var util = require("util");
@@ -743,7 +758,7 @@ var require_node = __commonJS({
743
758
  return (/* @__PURE__ */ new Date()).toISOString() + " ";
744
759
  }
745
760
  function log(...args2) {
746
- return process.stderr.write(util.format(...args2) + "\n");
761
+ return process.stderr.write(util.formatWithOptions(exports2.inspectOpts, ...args2) + "\n");
747
762
  }
748
763
  function save(namespaces) {
749
764
  if (namespaces) {
@@ -775,9 +790,9 @@ var require_node = __commonJS({
775
790
  }
776
791
  });
777
792
 
778
- // ../../node_modules/debug/src/index.js
793
+ // ../../node_modules/list-it/node_modules/debug/src/index.js
779
794
  var require_src = __commonJS({
780
- "../../node_modules/debug/src/index.js"(exports2, module2) {
795
+ "../../node_modules/list-it/node_modules/debug/src/index.js"(exports2, module2) {
781
796
  "use strict";
782
797
  if (typeof process === "undefined" || process.type === "renderer" || process.browser === true || process.__nwjs) {
783
798
  module2.exports = require_browser();
@@ -1628,8 +1643,9 @@ module.exports = __toCommonJS(replay_exports);
1628
1643
  // src/scripts/replayRunner.ts
1629
1644
  var import_chalk12 = __toESM(require("chalk"));
1630
1645
  var import_constants6 = require("@tradejs/core/constants");
1646
+ var import_data2 = require("@tradejs/core/data");
1631
1647
  var import_time7 = require("@tradejs/core/time");
1632
- var import_redis5 = require("@tradejs/infra/redis");
1648
+ var import_redis6 = require("@tradejs/infra/redis");
1633
1649
 
1634
1650
  // src/lib/runFormatting.ts
1635
1651
  var import_date_fns = require("date-fns");
@@ -1691,11 +1707,30 @@ var resolveTimeWindow = ({
1691
1707
  endTime,
1692
1708
  nowMs = Date.now(),
1693
1709
  defaultStartMs,
1694
- defaultEndMs
1710
+ defaultEndMs,
1711
+ closedIntervalMs
1695
1712
  }) => {
1696
1713
  const explicitStart = toEpochMs(startTime);
1697
1714
  const explicitEnd = toEpochMs(endTime);
1698
1715
  const resolvedDays = toPositiveNumber(days);
1716
+ const alignToLastClosedInterval = (window2, preserveDuration) => {
1717
+ if (typeof closedIntervalMs !== "number" || !Number.isFinite(closedIntervalMs) || closedIntervalMs <= 0) {
1718
+ return window2;
1719
+ }
1720
+ const alignedEnd = Math.floor(window2.end / closedIntervalMs) * closedIntervalMs - 1;
1721
+ const delta = window2.end - alignedEnd;
1722
+ const alignedStart = preserveDuration ? window2.start - delta : window2.start;
1723
+ if (alignedStart >= alignedEnd) {
1724
+ throw new Error(
1725
+ `Invalid closed-candle window: start (${alignedStart}) must be less than end (${alignedEnd})`
1726
+ );
1727
+ }
1728
+ return {
1729
+ ...window2,
1730
+ start: Math.trunc(alignedStart),
1731
+ end: Math.trunc(alignedEnd)
1732
+ };
1733
+ };
1699
1734
  if (explicitStart != null || explicitEnd != null) {
1700
1735
  const resolvedStart = explicitStart ?? (resolvedDays != null && explicitEnd != null ? explicitEnd - resolvedDays * ONE_DAY_MS : defaultStartMs);
1701
1736
  const resolvedEnd2 = explicitEnd ?? (resolvedDays != null && explicitStart != null ? explicitStart + resolvedDays * ONE_DAY_MS : defaultEndMs ?? nowMs);
@@ -1704,11 +1739,14 @@ var resolveTimeWindow = ({
1704
1739
  `Invalid time window: startTime (${resolvedStart}) must be less than endTime (${resolvedEnd2})`
1705
1740
  );
1706
1741
  }
1707
- return {
1708
- start: resolvedStart,
1709
- end: resolvedEnd2,
1710
- source: "explicit"
1711
- };
1742
+ return alignToLastClosedInterval(
1743
+ {
1744
+ start: resolvedStart,
1745
+ end: resolvedEnd2,
1746
+ source: "explicit"
1747
+ },
1748
+ explicitStart == null && resolvedDays != null
1749
+ );
1712
1750
  }
1713
1751
  if (resolvedDays != null) {
1714
1752
  const resolvedEnd2 = defaultEndMs ?? nowMs;
@@ -1718,11 +1756,14 @@ var resolveTimeWindow = ({
1718
1756
  `Invalid time window: computed start (${resolvedStart}) must be less than end (${resolvedEnd2})`
1719
1757
  );
1720
1758
  }
1721
- return {
1722
- start: Math.trunc(resolvedStart),
1723
- end: Math.trunc(resolvedEnd2),
1724
- source: "days"
1725
- };
1759
+ return alignToLastClosedInterval(
1760
+ {
1761
+ start: Math.trunc(resolvedStart),
1762
+ end: Math.trunc(resolvedEnd2),
1763
+ source: "days"
1764
+ },
1765
+ true
1766
+ );
1726
1767
  }
1727
1768
  const resolvedEnd = defaultEndMs ?? nowMs;
1728
1769
  if (defaultStartMs >= resolvedEnd) {
@@ -1730,11 +1771,14 @@ var resolveTimeWindow = ({
1730
1771
  `Invalid default time window: start (${defaultStartMs}) must be less than end (${resolvedEnd})`
1731
1772
  );
1732
1773
  }
1733
- return {
1734
- start: Math.trunc(defaultStartMs),
1735
- end: Math.trunc(resolvedEnd),
1736
- source: "default"
1737
- };
1774
+ return alignToLastClosedInterval(
1775
+ {
1776
+ start: Math.trunc(defaultStartMs),
1777
+ end: Math.trunc(resolvedEnd),
1778
+ source: "default"
1779
+ },
1780
+ true
1781
+ );
1738
1782
  };
1739
1783
 
1740
1784
  // src/lib/runtimeStrategyBacktest.ts
@@ -2174,7 +2218,8 @@ var prepareRunEnvironment = async ({
2174
2218
  accountId,
2175
2219
  deploymentId,
2176
2220
  assetClasses,
2177
- deployment
2221
+ deployment,
2222
+ closedIntervalMs
2178
2223
  }) => {
2179
2224
  const connectorName = await resolveRunConnectorName({
2180
2225
  value: connector,
@@ -2237,7 +2282,8 @@ var prepareRunEnvironment = async ({
2237
2282
  startTime,
2238
2283
  endTime,
2239
2284
  defaultStartMs: (0, import_time2.getTimestamp)(import_constants.BACKTEST_DEFAULT_DAYS),
2240
- defaultEndMs: (0, import_time2.getTimestamp)()
2285
+ defaultEndMs: (0, import_time2.getTimestamp)(),
2286
+ closedIntervalMs
2241
2287
  });
2242
2288
  const preloadStart = (0, import_time2.getBacktestPreloadStart)(
2243
2289
  window2.start,
@@ -2306,6 +2352,7 @@ var import_progress = __toESM(require("progress"));
2306
2352
  var import_chalk4 = __toESM(require("chalk"));
2307
2353
  var import_connectors4 = require("@tradejs/connectors");
2308
2354
  var import_connectors5 = require("@tradejs/node/connectors");
2355
+ var import_strategies = require("@tradejs/node/strategies");
2309
2356
  var import_timescale = require("@tradejs/infra/timescale");
2310
2357
 
2311
2358
  // src/lib/binanceMarketData.ts
@@ -2315,14 +2362,6 @@ var MARKET_FEATURE_INTERVAL_MS = {
2315
2362
  "15m": 9e5,
2316
2363
  "1h": 36e5
2317
2364
  };
2318
- var STABLE_QUOTE_SYMBOLS = /* @__PURE__ */ new Set([
2319
- "USDCUSDT",
2320
- "FDUSDUSDT",
2321
- "TUSDUSDT",
2322
- "BUSDUSDT",
2323
- "USDPUSDT",
2324
- "DAIUSDT"
2325
- ]);
2326
2365
  var safeDivide = (num, den) => Number.isFinite(num) && Number.isFinite(den) && den !== 0 ? num / den : null;
2327
2366
  var mean = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
2328
2367
  var standardDeviation = (values) => {
@@ -2366,9 +2405,6 @@ var classifyBtcAltRegime = ({
2366
2405
  if (btcVsAltReturn24h < -5e-3) return "alt_lead";
2367
2406
  return "neutral";
2368
2407
  };
2369
- var selectBreadthUniverseFromTickers = (tickers, limit) => tickers.filter(
2370
- (ticker) => ticker.symbol.endsWith("USDT") && ticker.symbol !== "BTCUSDT" && !STABLE_QUOTE_SYMBOLS.has(ticker.symbol) && Number.isFinite(ticker.turnover24h) && ticker.turnover24h > 0
2371
- ).sort((a, b) => b.turnover24h - a.turnover24h).slice(0, Math.max(0, limit)).map((ticker) => ticker.symbol);
2372
2408
  var aggregateAggTradesToRows = ({
2373
2409
  symbol,
2374
2410
  interval,
@@ -2850,10 +2886,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2850
2886
  process.env.BINANCE_MARKET_CONTEXT_TRADE_FLOW_SYMBOL_LIMIT,
2851
2887
  2
2852
2888
  );
2853
- const breadthLimit = asInt(
2854
- process.env.BINANCE_MARKET_CONTEXT_BREADTH_LIMIT,
2855
- 30
2856
- );
2857
2889
  const breadthChunkDays = asFloat(
2858
2890
  process.env.BINANCE_MARKET_CONTEXT_BREADTH_CHUNK_DAYS,
2859
2891
  30
@@ -2976,88 +3008,113 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2976
3008
  }
2977
3009
  }
2978
3010
  if (includeBreadth) {
2979
- const tickers = await connector.getTickers();
2980
- const breadthSymbols = selectBreadthUniverseFromTickers(
2981
- tickers,
2982
- breadthLimit
2983
- );
2984
- const universe = `binance_top${breadthSymbols.length}_usdt`;
2985
- const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
2986
- universe,
2987
- interval,
2988
- startMs: breadthStartMs,
2989
- endMs
2990
- });
2991
- const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
2992
- if (!hasCoverage({
2993
- coverage,
3011
+ const breadthUniverses = (0, import_strategies.getBinanceBreadthUniverses)();
3012
+ const breadthSymbols = breadthUniverses.at(-1)?.symbols ?? [];
3013
+ const connectorInterval = marketIntervalToConnectorInterval(interval);
3014
+ const chunks = buildBreadthBackfillChunks({
2994
3015
  startMs: breadthStartMs,
2995
3016
  endMs,
2996
- intervalMs
2997
- }) || !hasBtcAltMetrics) {
2998
- const connectorInterval = marketIntervalToConnectorInterval(interval);
2999
- const chunks = buildBreadthBackfillChunks({
3000
- startMs: breadthStartMs,
3001
- endMs,
3002
- intervalMs,
3003
- chunkDays: breadthChunkDays
3004
- });
3005
- const missingChunks = filterMissingBreadthBackfillChunks({
3006
- chunks,
3007
- coverage,
3008
- intervalMs
3009
- });
3010
- const bar = new import_progress.default(
3011
- "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
3012
- {
3013
- total: Math.max(1, missingChunks.length * breadthSymbols.length),
3014
- width: 24
3015
- }
3016
- );
3017
- let candlesRead = 0;
3018
- let skippedBreadthChunks = chunks.length - missingChunks.length;
3019
- if (!missingChunks.length) {
3020
- bar.tick(1, {
3021
- candles: 0,
3022
- skip: skippedBreadthChunks,
3023
- chunk: "cached",
3024
- symbol: universe
3017
+ intervalMs,
3018
+ chunkDays: breadthChunkDays
3019
+ });
3020
+ const plans = await Promise.all(
3021
+ breadthUniverses.map(async (definition) => {
3022
+ const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
3023
+ universe: definition.universe,
3024
+ interval,
3025
+ startMs: breadthStartMs,
3026
+ endMs
3025
3027
  });
3026
- }
3027
- for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
3028
- const chunk = missingChunks[chunkIndex];
3029
- const originalChunkIndex = chunks.findIndex(
3030
- (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
3031
- );
3032
- const candlesBySymbol = {};
3033
- for (const symbol of breadthSymbols) {
3034
- const candles = await connector.kline({
3035
- symbol,
3036
- interval: connectorInterval,
3037
- start: chunk.fetchStartMs,
3038
- end: chunk.endMs,
3039
- silent: true
3040
- });
3041
- candlesBySymbol[symbol] = candles;
3042
- candlesRead += candles.length;
3043
- bar.tick(1, {
3044
- candles: candlesRead,
3045
- skip: skippedBreadthChunks,
3046
- chunk: `${originalChunkIndex + 1}/${chunks.length}`,
3047
- symbol
3048
- });
3028
+ const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
3029
+ const missingChunks2 = !hasCoverage({
3030
+ coverage,
3031
+ startMs: breadthStartMs,
3032
+ endMs,
3033
+ intervalMs
3034
+ }) || !hasBtcAltMetrics ? !hasBtcAltMetrics ? chunks : filterMissingBreadthBackfillChunks({
3035
+ chunks,
3036
+ coverage,
3037
+ intervalMs
3038
+ }) : [];
3039
+ if (!missingChunks2.length) {
3040
+ console.log(
3041
+ import_chalk4.default.gray(`breadth cached: universe=${definition.universe}`)
3042
+ );
3049
3043
  }
3050
- const btcCandles = await connector.kline({
3051
- symbol: "BTCUSDT",
3044
+ return {
3045
+ definition,
3046
+ missingChunkKeys: new Set(
3047
+ missingChunks2.map((chunk) => `${chunk.startMs}:${chunk.endMs}`)
3048
+ )
3049
+ };
3050
+ })
3051
+ );
3052
+ const missingChunks = chunks.filter((chunk) => {
3053
+ const key = `${chunk.startMs}:${chunk.endMs}`;
3054
+ return plans.some((plan) => plan.missingChunkKeys.has(key));
3055
+ });
3056
+ const bar = new import_progress.default(
3057
+ "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
3058
+ {
3059
+ total: Math.max(1, missingChunks.length * breadthSymbols.length),
3060
+ width: 24
3061
+ }
3062
+ );
3063
+ let candlesRead = 0;
3064
+ const skippedBreadthChunks = chunks.length - missingChunks.length;
3065
+ if (!missingChunks.length) {
3066
+ bar.tick(1, {
3067
+ candles: 0,
3068
+ skip: skippedBreadthChunks,
3069
+ chunk: "cached",
3070
+ symbol: "top5/top10/top30/top50/top100"
3071
+ });
3072
+ }
3073
+ for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
3074
+ const chunk = missingChunks[chunkIndex];
3075
+ const originalChunkIndex = chunks.findIndex(
3076
+ (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
3077
+ );
3078
+ const candlesBySymbol = {};
3079
+ for (const symbol of breadthSymbols) {
3080
+ const candles = await connector.kline({
3081
+ symbol,
3052
3082
  interval: connectorInterval,
3053
3083
  start: chunk.fetchStartMs,
3054
3084
  end: chunk.endMs,
3055
3085
  silent: true
3056
3086
  });
3087
+ candlesBySymbol[symbol] = candles;
3088
+ candlesRead += candles.length;
3089
+ bar.tick(1, {
3090
+ candles: candlesRead,
3091
+ skip: skippedBreadthChunks,
3092
+ chunk: `${originalChunkIndex + 1}/${chunks.length}`,
3093
+ symbol
3094
+ });
3095
+ }
3096
+ const btcCandles = await connector.kline({
3097
+ symbol: "BTCUSDT",
3098
+ interval: connectorInterval,
3099
+ start: chunk.fetchStartMs,
3100
+ end: chunk.endMs,
3101
+ silent: true
3102
+ });
3103
+ const chunkKey = `${chunk.startMs}:${chunk.endMs}`;
3104
+ for (const plan of plans) {
3105
+ if (!plan.missingChunkKeys.has(chunkKey)) {
3106
+ continue;
3107
+ }
3108
+ const symbolSet = new Set(plan.definition.symbols);
3109
+ const universeCandles = Object.fromEntries(
3110
+ Object.entries(candlesBySymbol).filter(
3111
+ ([symbol]) => symbolSet.has(symbol)
3112
+ )
3113
+ );
3057
3114
  const rows = buildMarketBreadthRows({
3058
- universe,
3115
+ universe: plan.definition.universe,
3059
3116
  interval,
3060
- candlesBySymbol,
3117
+ candlesBySymbol: universeCandles,
3061
3118
  btcCandles
3062
3119
  }).filter((row) => {
3063
3120
  const ts = row.ts.getTime();
@@ -3066,8 +3123,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
3066
3123
  await (0, import_timescale.upsertMarketBreadthRows)(rows);
3067
3124
  breadthRows += rows.length;
3068
3125
  }
3069
- } else {
3070
- console.log(import_chalk4.default.gray(`breadth cached: universe=${universe}`));
3071
3126
  }
3072
3127
  }
3073
3128
  console.log(
@@ -4292,27 +4347,27 @@ var createCoinMarketCapFetchStats = () => ({
4292
4347
  totalCredits: 0,
4293
4348
  byPath: /* @__PURE__ */ new Map()
4294
4349
  });
4295
- var getCoinMarketCapPathLabel = (path2) => {
4296
- if (path2.includes("global-metrics")) return "global";
4297
- if (path2.includes("cryptocurrency/quotes")) return "reference";
4298
- if (path2.includes("exchange/quotes")) return "exchangeLiquidity";
4299
- if (path2.includes("fear-and-greed")) return "fearGreed";
4300
- if (path2.includes("cmc100")) return "cmc100";
4301
- if (path2.includes("cmc20")) return "cmc20";
4302
- return path2;
4303
- };
4304
- var recordCoinMarketCapFetchStats = (stats, path2, creditCount) => {
4350
+ var getCoinMarketCapPathLabel = (path3) => {
4351
+ if (path3.includes("global-metrics")) return "global";
4352
+ if (path3.includes("cryptocurrency/quotes")) return "reference";
4353
+ if (path3.includes("exchange/quotes")) return "exchangeLiquidity";
4354
+ if (path3.includes("fear-and-greed")) return "fearGreed";
4355
+ if (path3.includes("cmc100")) return "cmc100";
4356
+ if (path3.includes("cmc20")) return "cmc20";
4357
+ return path3;
4358
+ };
4359
+ var recordCoinMarketCapFetchStats = (stats, path3, creditCount) => {
4305
4360
  if (!stats) return;
4306
- const existing = stats.byPath.get(path2) ?? { requests: 0, credits: 0 };
4361
+ const existing = stats.byPath.get(path3) ?? { requests: 0, credits: 0 };
4307
4362
  existing.requests += 1;
4308
4363
  existing.credits += creditCount ?? 0;
4309
4364
  stats.requests += 1;
4310
4365
  stats.totalCredits += creditCount ?? 0;
4311
- stats.byPath.set(path2, existing);
4366
+ stats.byPath.set(path3, existing);
4312
4367
  };
4313
4368
  var formatCoinMarketCapFetchStats = (stats) => {
4314
4369
  const byPath = [...stats.byPath.entries()].map(
4315
- ([path2, item]) => `${getCoinMarketCapPathLabel(path2)}=${item.requests}req/${item.credits}cr`
4370
+ ([path3, item]) => `${getCoinMarketCapPathLabel(path3)}=${item.requests}req/${item.credits}cr`
4316
4371
  ).join(" ");
4317
4372
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
4318
4373
  };
@@ -5228,8 +5283,12 @@ var prepareMarketContextForRun = async (params) => {
5228
5283
  };
5229
5284
 
5230
5285
  // src/lib/backtest/runState.ts
5286
+ var import_types = require("@tradejs/types");
5231
5287
  var successTests = 0;
5232
5288
  var errorTests = 0;
5289
+ var warningCounts = {
5290
+ [import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
5291
+ };
5233
5292
  var errorMessages = [];
5234
5293
  var topResults = [];
5235
5294
  var bestTickerResults = /* @__PURE__ */ new Map();
@@ -5274,6 +5333,9 @@ var getRuntimeCompareContext = () => ({
5274
5333
  var resetRunState = () => {
5275
5334
  successTests = 0;
5276
5335
  errorTests = 0;
5336
+ for (const code of Object.values(import_types.BACKTEST_WARNING_CODES)) {
5337
+ warningCounts[code] = 0;
5338
+ }
5277
5339
  errorMessages.length = 0;
5278
5340
  topResults = [];
5279
5341
  bestTickerResults.clear();
@@ -5418,13 +5480,16 @@ var REPLAY_RUNTIME_COMPARISON_HEADERS = [
5418
5480
  import_chalk8.default.yellow("RT TRADES"),
5419
5481
  import_chalk8.default.yellow("RT PNL"),
5420
5482
  import_chalk8.default.green("MATCHED"),
5483
+ import_chalk8.default.red("ORDER FAILED"),
5421
5484
  import_chalk8.default.yellow("RT ONLY"),
5422
- import_chalk8.default.magenta("BT ONLY")
5485
+ import_chalk8.default.magenta("BT ONLY"),
5486
+ import_chalk8.default.red("UNMATCHED")
5423
5487
  ];
5424
5488
  var REPLAY_RESULTS_CONFIG = "replay";
5425
5489
  var REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS = 1;
5426
5490
  var REPLAY_RUNTIME_COMPARE_TOLERANCE_MS = REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS * 15 * 60 * 1e3;
5427
5491
  var formatReplayRuntimeCompareTolerance = () => `${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS} ${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS === 1 ? "bar" : "bars"}`;
5492
+ var getReplayRuntimeUnmatchedCount = (row) => row.orderFailed + row.runtimeOnly + row.backtestOnly;
5428
5493
  var buildReplayStrategyConfig = ({
5429
5494
  strategyConfig,
5430
5495
  interval
@@ -5489,12 +5554,13 @@ var compactReplaySignal = (signal) => {
5489
5554
  var import_chalk9 = __toESM(require("chalk"));
5490
5555
  var import_progress3 = __toESM(require("progress"));
5491
5556
  var import_backtest = require("@tradejs/core/backtest");
5492
- var import_strategies = require("@tradejs/core/strategies");
5557
+ var import_strategies2 = require("@tradejs/core/strategies");
5493
5558
  var import_time3 = require("@tradejs/core/time");
5494
5559
  var import_logger = require("@tradejs/infra/logger");
5560
+ var import_redis3 = require("@tradejs/infra/redis");
5495
5561
  var import_connectors6 = require("@tradejs/node/connectors");
5496
5562
  var import_cli4 = require("@tradejs/node/cli");
5497
- var import_strategies2 = require("@tradejs/node/strategies");
5563
+ var import_strategies3 = require("@tradejs/node/strategies");
5498
5564
 
5499
5565
  // src/lib/replay/portfolioReplayConnector.ts
5500
5566
  var import_constants3 = require("@tradejs/core/constants");
@@ -5944,21 +6010,231 @@ var invokeAfterSignalsHooks = async (hooks, params) => {
5944
6010
  }
5945
6011
  };
5946
6012
 
6013
+ // src/lib/runtimeLineage.ts
6014
+ var import_node_child_process = require("child_process");
6015
+ var import_node_crypto = require("crypto");
6016
+ var import_promises = __toESM(require("fs/promises"));
6017
+ var import_node_path = __toESM(require("path"));
6018
+ var RUNTIME_CONTEXT_ENV_KEYS = [
6019
+ "AI_MODE",
6020
+ "MIN_AI_QUALITY",
6021
+ "INTERVAL",
6022
+ "DERIVATIVES_CONTEXT_ENABLED",
6023
+ "DERIVATIVES_CONTEXT_TARGET_ENABLED",
6024
+ "DERIVATIVES_CONTEXT_LOOKBACK_HOURS",
6025
+ "DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS",
6026
+ "DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY",
6027
+ "COINMARKETCAP_CONTEXT_ENABLED",
6028
+ "COINMARKETCAP_CONTEXT_MAX_AGE_MS",
6029
+ "COINMARKETCAP_CONTEXT_EXCHANGE_LIQUIDITY_ENABLED",
6030
+ "COINMARKETCAP_CONTEXT_EXCHANGE_SLUGS",
6031
+ "COINMARKETCAP_CONTEXT_FEAR_GREED_ENABLED",
6032
+ "COINMARKETCAP_CONTEXT_HISTORICAL_ACCESS_MONTHS",
6033
+ "COINMARKETCAP_CONTEXT_BACKFILL_ENABLED",
6034
+ "COINMARKETCAP_CONTEXT_BACKFILL_MAX_DAYS",
6035
+ "COINMARKETCAP_CONTEXT_BACKFILL_WARMUP_DAYS"
6036
+ ];
6037
+ var RUNTIME_CONTEXT_DATA_MODEL = {
6038
+ derivativesSourceIntervals: "15m",
6039
+ derivativesDerivedIntervals: "1h",
6040
+ derivativesHourlyFallback: "stored-1h",
6041
+ derivativesDataModelVersion: 2,
6042
+ coinMarketCapAvailabilityModel: "closed-interval-all-daily",
6043
+ coinMarketCapDataModelVersion: 3,
6044
+ binanceBreadthUniverseModel: "fixed-versioned-top5-top10-top30-top50-top100",
6045
+ binanceBreadthDataModelVersion: 2
6046
+ };
6047
+ var normalizeForStableJson = (value) => {
6048
+ if (Array.isArray(value)) {
6049
+ return value.map(normalizeForStableJson);
6050
+ }
6051
+ if (value && typeof value === "object") {
6052
+ return Object.fromEntries(
6053
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
6054
+ );
6055
+ }
6056
+ return value;
6057
+ };
6058
+ var fingerprintRuntimeValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
6059
+ var gitLineageCache = /* @__PURE__ */ new Map();
6060
+ var gateFingerprintCache = /* @__PURE__ */ new Map();
6061
+ var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
6062
+ var resolveGitLineage = (projectRoot) => {
6063
+ const cached = gitLineageCache.get(projectRoot);
6064
+ if (cached) {
6065
+ return cached;
6066
+ }
6067
+ const envSha = String(process.env.TRADEJS_GIT_SHA ?? "").trim();
6068
+ if (envSha && envSha !== "unknown") {
6069
+ const result = {
6070
+ gitSha: envSha,
6071
+ gitDirty: false
6072
+ };
6073
+ gitLineageCache.set(projectRoot, result);
6074
+ return result;
6075
+ }
6076
+ try {
6077
+ const gitSha = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "HEAD"], {
6078
+ cwd: projectRoot,
6079
+ encoding: "utf8",
6080
+ stdio: ["ignore", "pipe", "ignore"]
6081
+ }).trim();
6082
+ const status = (0, import_node_child_process.execFileSync)(
6083
+ "git",
6084
+ ["status", "--porcelain", "--untracked-files=normal"],
6085
+ {
6086
+ cwd: projectRoot,
6087
+ encoding: "utf8",
6088
+ stdio: ["ignore", "pipe", "ignore"]
6089
+ }
6090
+ ).trim();
6091
+ const result = {
6092
+ gitSha: gitSha || null,
6093
+ gitDirty: status.length > 0
6094
+ };
6095
+ gitLineageCache.set(projectRoot, result);
6096
+ return result;
6097
+ } catch {
6098
+ const result = {
6099
+ gitSha: null,
6100
+ gitDirty: null
6101
+ };
6102
+ gitLineageCache.set(projectRoot, result);
6103
+ return result;
6104
+ }
6105
+ };
6106
+ var readOptionalFile = async (filePath) => {
6107
+ try {
6108
+ return await import_promises.default.readFile(filePath);
6109
+ } catch {
6110
+ return null;
6111
+ }
6112
+ };
6113
+ var resolveBinanceBreadthFingerprint = async (projectRoot) => {
6114
+ if (binanceBreadthFingerprintCache.has(projectRoot)) {
6115
+ return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
6116
+ }
6117
+ const content = await readOptionalFile(
6118
+ import_node_path.default.join(
6119
+ projectRoot,
6120
+ "packages/node/src/config/binanceBreadthUniverses.json"
6121
+ )
6122
+ );
6123
+ let result = null;
6124
+ if (content) {
6125
+ try {
6126
+ const parsed = JSON.parse(content.toString("utf8"));
6127
+ result = typeof parsed.fingerprint === "string" && parsed.fingerprint.trim() ? parsed.fingerprint.trim() : null;
6128
+ } catch {
6129
+ result = null;
6130
+ }
6131
+ }
6132
+ binanceBreadthFingerprintCache.set(projectRoot, result);
6133
+ return result;
6134
+ };
6135
+ var resolveGateFingerprint = async ({
6136
+ projectRoot,
6137
+ strategyName,
6138
+ gitSha
6139
+ }) => {
6140
+ const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
6141
+ const cached = gateFingerprintCache.get(cacheKey);
6142
+ if (cached) {
6143
+ return cached;
6144
+ }
6145
+ const relativeCandidates = [
6146
+ `packages/strategies/src/${strategyName}/adapters/ai.ts`,
6147
+ `packages/strategies/src/${strategyName}/guardrails.ts`,
6148
+ `packages/strategies/src/${strategyName}/pockets.ts`,
6149
+ `packages/strategies/src/${strategyName}/config.ts`,
6150
+ "packages/node/src/ai.ts"
6151
+ ];
6152
+ const optionalEntries = await Promise.all(
6153
+ relativeCandidates.map(async (relativePath) => {
6154
+ const content = await readOptionalFile(
6155
+ import_node_path.default.join(projectRoot, relativePath)
6156
+ );
6157
+ return content == null ? null : { relativePath, content };
6158
+ })
6159
+ );
6160
+ const sourceEntries = [];
6161
+ for (const entry of optionalEntries) {
6162
+ if (entry != null) {
6163
+ sourceEntries.push(entry);
6164
+ }
6165
+ }
6166
+ const hash = (0, import_node_crypto.createHash)("sha256");
6167
+ hash.update(strategyName);
6168
+ if (sourceEntries.length) {
6169
+ for (const entry of sourceEntries) {
6170
+ hash.update(entry.relativePath);
6171
+ hash.update(entry.content);
6172
+ }
6173
+ } else {
6174
+ hash.update(gitSha ?? "unknown-git-sha");
6175
+ }
6176
+ const fingerprint = hash.digest("hex").slice(0, 16);
6177
+ gateFingerprintCache.set(cacheKey, fingerprint);
6178
+ return fingerprint;
6179
+ };
6180
+ var buildRuntimeLineage = async ({
6181
+ projectRoot,
6182
+ strategyName,
6183
+ config,
6184
+ runContext,
6185
+ env = process.env
6186
+ }) => {
6187
+ const git = resolveGitLineage(projectRoot);
6188
+ const context = {
6189
+ ...Object.fromEntries(
6190
+ RUNTIME_CONTEXT_ENV_KEYS.map((key) => [key, env[key] ?? null])
6191
+ ),
6192
+ ...RUNTIME_CONTEXT_DATA_MODEL,
6193
+ binanceBreadthUniverseFingerprint: await resolveBinanceBreadthFingerprint(projectRoot),
6194
+ ...runContext
6195
+ };
6196
+ return {
6197
+ schemaVersion: 1,
6198
+ ...git,
6199
+ gateFingerprint: await resolveGateFingerprint({
6200
+ projectRoot,
6201
+ strategyName,
6202
+ gitSha: git.gitSha
6203
+ }),
6204
+ configFingerprint: fingerprintRuntimeValue(config),
6205
+ contextFingerprint: fingerprintRuntimeValue(context)
6206
+ };
6207
+ };
6208
+ var runtimeLineageKey = (lineage) => [
6209
+ lineage.schemaVersion,
6210
+ lineage.gitSha ?? "unknown",
6211
+ lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
6212
+ lineage.gateFingerprint,
6213
+ lineage.configFingerprint,
6214
+ lineage.contextFingerprint
6215
+ ].join(":");
6216
+ var runtimeLineagesMatch = (left, right) => left != null && right != null && runtimeLineageKey(left) === runtimeLineageKey(right);
6217
+
5947
6218
  // src/lib/replay/historicalSignalsReplay.ts
5948
6219
  var loadRuntimeStrategies = async (runtimeStrategies) => {
5949
6220
  const strategies = await Promise.all(
5950
6221
  runtimeStrategies.map(async ({ strategyName, strategyConfig }) => {
5951
- const strategyCreator = await (0, import_strategies2.getStrategyCreator)(
6222
+ const strategyCreator = await (0, import_strategies3.getStrategyCreator)(
5952
6223
  strategyName,
5953
6224
  replayProjectRoot
5954
6225
  );
5955
6226
  if (!strategyCreator) {
5956
6227
  throw new Error(`Unknown strategy: ${strategyName}`);
5957
6228
  }
6229
+ const strategyResults = await (0, import_redis3.getData)(
6230
+ import_redis3.redisKeys.strategyResults(replayUserName, strategyName),
6231
+ {}
6232
+ );
5958
6233
  return {
5959
6234
  strategyName,
5960
6235
  strategyCreator,
5961
- strategyConfig
6236
+ strategyConfig,
6237
+ strategyResults
5962
6238
  };
5963
6239
  })
5964
6240
  );
@@ -6059,6 +6335,7 @@ var runHistoricalSignalsReplay = async ({
6059
6335
  }) => {
6060
6336
  const startedAt = Date.now();
6061
6337
  const signals = [];
6338
+ const runtimeLineages = [];
6062
6339
  const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
6063
6340
  const projectHooks = projectConfig.hooks;
6064
6341
  const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
@@ -6146,26 +6423,54 @@ var runHistoricalSignalsReplay = async ({
6146
6423
  sharedReplayKeyPrefixes.push(sharedIndicatorsReplayKey);
6147
6424
  const strategiesForSymbol = await Promise.all(
6148
6425
  loadedStrategies.map(
6149
- async ({ strategyName, strategyCreator, strategyConfig }) => ({
6426
+ async ({
6150
6427
  strategyName,
6428
+ strategyCreator,
6151
6429
  strategyConfig,
6152
- run: await strategyCreator({
6153
- userName: replayUserName,
6154
- connectorName,
6155
- config: buildReplayStrategyConfig({
6430
+ strategyResults
6431
+ }) => {
6432
+ const runtimeLineage = await buildRuntimeLineage({
6433
+ projectRoot: replayProjectRoot,
6434
+ strategyName,
6435
+ config: {
6436
+ configId: "config",
6156
6437
  strategyConfig,
6157
- interval
6158
- }),
6438
+ symbolResultConfig: strategyResults?.[symbol]?.config ?? null
6439
+ },
6440
+ runContext: {
6441
+ connectorName: connectorName.toLowerCase(),
6442
+ interval: String(interval),
6443
+ universe: preparedRun.universe ?? null
6444
+ }
6445
+ });
6446
+ runtimeLineages.push({
6447
+ strategy: strategyName,
6159
6448
  symbol,
6160
- data: preparedData.prevData,
6161
- btcData: preparedData.btcPrevData,
6162
- ethData: preparedData.ethPrevData,
6163
- btcBinanceData: preparedData.btcBinancePrevData,
6164
- btcCoinbaseData: preparedData.btcCoinbasePrevData,
6165
- connector: replayConnector,
6166
- sharedIndicatorsReplayKey
6167
- })
6168
- })
6449
+ lineage: runtimeLineage
6450
+ });
6451
+ return {
6452
+ strategyName,
6453
+ strategyConfig,
6454
+ runtimeLineage,
6455
+ run: await strategyCreator({
6456
+ userName: replayUserName,
6457
+ connectorName,
6458
+ runtimeConfigId: "config",
6459
+ config: buildReplayStrategyConfig({
6460
+ strategyConfig,
6461
+ interval
6462
+ }),
6463
+ symbol,
6464
+ data: preparedData.prevData,
6465
+ btcData: preparedData.btcPrevData,
6466
+ ethData: preparedData.ethPrevData,
6467
+ btcBinanceData: preparedData.btcBinancePrevData,
6468
+ btcCoinbaseData: preparedData.btcCoinbasePrevData,
6469
+ connector: replayConnector,
6470
+ sharedIndicatorsReplayKey
6471
+ })
6472
+ };
6473
+ }
6169
6474
  )
6170
6475
  );
6171
6476
  const symbolRuntime = {
@@ -6259,7 +6564,8 @@ var runHistoricalSignalsReplay = async ({
6259
6564
  ethCandle?.timestamp === timestamp ? ethCandle : void 0
6260
6565
  );
6261
6566
  if (result && typeof result !== "string") {
6262
- await (0, import_strategies2.enrichSignalWithBinanceMarketContext)({
6567
+ result.runtimeLineage = strategyRuntime.runtimeLineage;
6568
+ await (0, import_strategies3.enrichSignalWithBinanceMarketContext)({
6263
6569
  signal: result,
6264
6570
  env: "PARITY"
6265
6571
  });
@@ -6283,8 +6589,8 @@ var runHistoricalSignalsReplay = async ({
6283
6589
  }
6284
6590
  } finally {
6285
6591
  for (const keyPrefix of sharedReplayKeyPrefixes) {
6286
- (0, import_strategies.releaseStrategyIndicatorsReplayCache)(keyPrefix);
6287
- (0, import_strategies.releaseStrategyReplayCache)(keyPrefix);
6592
+ (0, import_strategies2.releaseStrategyIndicatorsReplayCache)(keyPrefix);
6593
+ (0, import_strategies2.releaseStrategyReplayCache)(keyPrefix);
6288
6594
  }
6289
6595
  }
6290
6596
  const artifacts = replayConnector.getReplayArtifacts();
@@ -6318,7 +6624,8 @@ var runHistoricalSignalsReplay = async ({
6318
6624
  orderLog: artifacts.orderLog,
6319
6625
  positionLog: artifacts.positionLog,
6320
6626
  cycleCount: orderedTimestamps.length,
6321
- abortedCycles
6627
+ abortedCycles,
6628
+ runtimeLineages
6322
6629
  };
6323
6630
  };
6324
6631
 
@@ -6802,7 +7109,7 @@ var summarizeTradeParityByStrategy = ({
6802
7109
  };
6803
7110
 
6804
7111
  // src/lib/runtimeSignalsLoader.ts
6805
- var import_redis3 = require("@tradejs/infra/redis");
7112
+ var import_redis4 = require("@tradejs/infra/redis");
6806
7113
 
6807
7114
  // src/lib/runtimeSignalsStorage.ts
6808
7115
  var import_constants4 = require("@tradejs/core/constants");
@@ -6852,14 +7159,14 @@ var loadRuntimeSignals = async (userName, {
6852
7159
  startTime,
6853
7160
  endTime
6854
7161
  } = {}) => {
6855
- const bucketPrefix = import_redis3.redisKeys.runtimeSignalBuckets(userName);
7162
+ const bucketPrefix = import_redis4.redisKeys.runtimeSignalBuckets(userName);
6856
7163
  const bucketKeys = filterBucketKeysByWindow(
6857
- await (0, import_redis3.getKeys)(bucketPrefix),
7164
+ await (0, import_redis4.getKeys)(bucketPrefix),
6858
7165
  bucketPrefix,
6859
7166
  startTime,
6860
7167
  endTime
6861
7168
  );
6862
- const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis3.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
7169
+ const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
6863
7170
  const seen = /* @__PURE__ */ new Set();
6864
7171
  const payloads = await Promise.all(
6865
7172
  refs.map(async (ref) => {
@@ -6868,8 +7175,8 @@ var loadRuntimeSignals = async (userName, {
6868
7175
  return null;
6869
7176
  }
6870
7177
  seen.add(dedupeKey);
6871
- const signal = await (0, import_redis3.getData)(
6872
- import_redis3.redisKeys.storeSignal(ref.symbol, ref.signalId),
7178
+ const signal = await (0, import_redis4.getData)(
7179
+ import_redis4.redisKeys.storeSignal(ref.symbol, ref.signalId),
6873
7180
  null
6874
7181
  );
6875
7182
  return isSignalRecord(signal) ? signal : null;
@@ -6881,25 +7188,42 @@ var loadRuntimeSignalEvaluations = async (userName, {
6881
7188
  startTime,
6882
7189
  endTime
6883
7190
  } = {}) => {
6884
- const bucketPrefix = import_redis3.redisKeys.runtimeSignalEvaluationBuckets(userName);
7191
+ const bucketPrefix = import_redis4.redisKeys.runtimeSignalEvaluationBuckets(userName);
6885
7192
  const bucketKeys = filterBucketKeysByWindow(
6886
- await (0, import_redis3.getKeys)(bucketPrefix),
7193
+ await (0, import_redis4.getKeys)(bucketPrefix),
6887
7194
  bucketPrefix,
6888
7195
  startTime,
6889
7196
  endTime
6890
7197
  );
6891
- const evaluations = (await Promise.all(bucketKeys.map((key) => (0, import_redis3.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalEvaluationRecord);
7198
+ const evaluations = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalEvaluationRecord);
6892
7199
  const deduped = /* @__PURE__ */ new Map();
6893
7200
  for (const evaluation of evaluations) {
6894
7201
  deduped.set(evaluation.evaluationId, evaluation);
6895
7202
  }
6896
7203
  return sortByTimestamp([...deduped.values()]);
6897
7204
  };
7205
+ var loadRuntimeLineageScopes = async (userName, {
7206
+ startTime,
7207
+ endTime
7208
+ }) => {
7209
+ const records = (await Promise.all(
7210
+ (0, import_time4.getRuntimeStorageDayKeys)(startTime, endTime).map(
7211
+ (dayKey) => (0, import_redis4.getHashJsonValues)(
7212
+ import_redis4.redisKeys.runtimeLineageScopeBucket(userName, dayKey)
7213
+ )
7214
+ )
7215
+ )).flat().filter(
7216
+ (record) => record != null && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.firstTimestamp === "number" && typeof record.lastTimestamp === "number" && record.lineage != null
7217
+ );
7218
+ return records.sort(
7219
+ (left, right) => left.firstTimestamp - right.firstTimestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol)
7220
+ );
7221
+ };
6898
7222
 
6899
7223
  // src/lib/runtimeTradeSync.ts
6900
7224
  var import_constants5 = require("@tradejs/core/constants");
6901
7225
  var import_time5 = require("@tradejs/core/time");
6902
- var import_redis4 = require("@tradejs/infra/redis");
7226
+ var import_redis5 = require("@tradejs/infra/redis");
6903
7227
  var CLOSED_PNL_LIMIT = 100;
6904
7228
  var CLOSED_PNL_RECONCILIATION_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
6905
7229
  var EXCHANGE_HISTORY_MAX_RANGE_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -6993,6 +7317,18 @@ var consumeClosedPnlMatch = (buckets, trade) => {
6993
7317
  const [row] = rows.splice(matchIndex, 1);
6994
7318
  return row ?? null;
6995
7319
  };
7320
+ var isRuntimeTradeInConnectorScope = (trade, connector) => {
7321
+ if (trade.deploymentId && trade.deploymentId !== connector.deploymentId) {
7322
+ return false;
7323
+ }
7324
+ if (trade.accountId && trade.accountId !== connector.accountId) {
7325
+ return false;
7326
+ }
7327
+ if (trade.universe && connector.universe && trade.universe !== connector.universe) {
7328
+ return false;
7329
+ }
7330
+ return true;
7331
+ };
6996
7332
  var syncRuntimeTrades = async ({
6997
7333
  userName,
6998
7334
  connector,
@@ -7016,16 +7352,23 @@ var syncRuntimeTrades = async ({
7016
7352
  const openPositionsBySymbol = new Map(
7017
7353
  openPositions.map((position) => [position.symbol, position])
7018
7354
  );
7019
- const activeOrderIdBySymbol = /* @__PURE__ */ new Map();
7020
- const symbols = [...new Set(trades.map((trade) => trade.symbol))];
7355
+ const activeOrderIdByKey = /* @__PURE__ */ new Map();
7356
+ const activeTradeKeys = [
7357
+ ...new Set(
7358
+ trades.filter((trade) => isRuntimeTradeInConnectorScope(trade, connector)).map(
7359
+ (trade) => import_redis5.redisKeys.runtimeActiveTrade(
7360
+ userName,
7361
+ trade.symbol,
7362
+ trade.deploymentId ?? trade.accountId
7363
+ )
7364
+ )
7365
+ )
7366
+ ];
7021
7367
  await Promise.all(
7022
- symbols.map(async (symbol) => {
7023
- const activeRef = await (0, import_redis4.getData)(
7024
- import_redis4.redisKeys.runtimeActiveTrade(userName, symbol),
7025
- null
7026
- );
7027
- activeOrderIdBySymbol.set(
7028
- symbol,
7368
+ activeTradeKeys.map(async (key) => {
7369
+ const activeRef = await (0, import_redis5.getData)(key, null);
7370
+ activeOrderIdByKey.set(
7371
+ key,
7029
7372
  typeof activeRef?.orderId === "string" ? activeRef.orderId : null
7030
7373
  );
7031
7374
  })
@@ -7044,12 +7387,21 @@ var syncRuntimeTrades = async ({
7044
7387
  }
7045
7388
  const syncedTrades = [];
7046
7389
  for (const trade of trades) {
7390
+ if (!isRuntimeTradeInConnectorScope(trade, connector)) {
7391
+ syncedTrades.push(trade);
7392
+ continue;
7393
+ }
7047
7394
  if (trade.status !== "active" && !isRuntimeTradeSyncFallbackClose(trade)) {
7048
7395
  syncedTrades.push(trade);
7049
7396
  continue;
7050
7397
  }
7398
+ const activeTradeKey = import_redis5.redisKeys.runtimeActiveTrade(
7399
+ userName,
7400
+ trade.symbol,
7401
+ trade.deploymentId ?? trade.accountId
7402
+ );
7051
7403
  const openPosition = openPositionsBySymbol.get(trade.symbol);
7052
- const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
7404
+ const activeOrderId = activeOrderIdByKey.get(activeTradeKey);
7053
7405
  const isCurrentActiveTrade = activeOrderId === trade.orderId;
7054
7406
  if (trade.status === "active" && !openPositionsReliable) {
7055
7407
  syncedTrades.push({
@@ -7067,15 +7419,15 @@ var syncRuntimeTrades = async ({
7067
7419
  currentPnl: openPosition.unrealizedPnl,
7068
7420
  lastSyncedAt: endTime
7069
7421
  };
7070
- await (0, import_redis4.setData)(
7071
- import_redis4.redisKeys.runtimeTrade(userName, trade.orderId),
7422
+ await (0, import_redis5.setData)(
7423
+ import_redis5.redisKeys.runtimeTrade(userName, trade.orderId),
7072
7424
  nextTrade2,
7073
7425
  {
7074
7426
  expire: 0
7075
7427
  }
7076
7428
  );
7077
- await (0, import_redis4.setHashJsonField)(
7078
- import_redis4.redisKeys.runtimeTradeBucket(
7429
+ await (0, import_redis5.setHashJsonField)(
7430
+ import_redis5.redisKeys.runtimeTradeBucket(
7079
7431
  userName,
7080
7432
  (0, import_time5.getRuntimeStorageDayKey)(trade.entryTimestamp)
7081
7433
  ),
@@ -7088,6 +7440,7 @@ var syncRuntimeTrades = async ({
7088
7440
  }
7089
7441
  const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
7090
7442
  if (!matchedClosedPnl) {
7443
+ syncedTrades.push(trade);
7091
7444
  continue;
7092
7445
  }
7093
7446
  const nextTrade = {
@@ -7108,11 +7461,11 @@ var syncRuntimeTrades = async ({
7108
7461
  lastSyncedAt: endTime
7109
7462
  };
7110
7463
  await Promise.all([
7111
- (0, import_redis4.setData)(import_redis4.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
7464
+ (0, import_redis5.setData)(import_redis5.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
7112
7465
  expire: import_constants5.TTL_1M
7113
7466
  }),
7114
- (0, import_redis4.setHashJsonField)(
7115
- import_redis4.redisKeys.runtimeTradeBucket(
7467
+ (0, import_redis5.setHashJsonField)(
7468
+ import_redis5.redisKeys.runtimeTradeBucket(
7116
7469
  userName,
7117
7470
  (0, import_time5.getRuntimeStorageDayKey)(trade.entryTimestamp)
7118
7471
  ),
@@ -7120,7 +7473,7 @@ var syncRuntimeTrades = async ({
7120
7473
  nextTrade,
7121
7474
  { expire: import_constants5.TTL_1M }
7122
7475
  ),
7123
- ...isCurrentActiveTrade ? [(0, import_redis4.delKey)(import_redis4.redisKeys.runtimeActiveTrade(userName, trade.symbol))] : []
7476
+ ...isCurrentActiveTrade ? [(0, import_redis5.delKey)(activeTradeKey)] : []
7124
7477
  ]);
7125
7478
  syncedTrades.push(nextTrade);
7126
7479
  }
@@ -7730,6 +8083,7 @@ var buildReplayRuntimeComparisonDetails = ({
7730
8083
  ),
7731
8084
  limit
7732
8085
  ),
8086
+ orderFailed: [],
7733
8087
  runtimeOnly: cappedRuntimeOnlyDetails,
7734
8088
  backtestOnly: cappedBacktestOnlyDetails,
7735
8089
  nearestCandidates: cappedNearestCandidates,
@@ -7749,6 +8103,7 @@ var buildReplayRuntimeComparisonDetails = ({
7749
8103
  };
7750
8104
  var buildReplayExchangeComparisonDetails = ({
7751
8105
  matched,
8106
+ orderFailed = [],
7752
8107
  exchangeOnly,
7753
8108
  backtestOnly,
7754
8109
  exchangeEntries,
@@ -7799,7 +8154,7 @@ var buildReplayExchangeComparisonDetails = ({
7799
8154
  limit
7800
8155
  );
7801
8156
  return {
7802
- capped: matched.length > limit || exchangeOnlyDetails.length > limit || backtestOnlyDetails.length > limit || nearestCandidates.length > limit,
8157
+ capped: matched.length > limit || orderFailed.length > limit || exchangeOnlyDetails.length > limit || backtestOnlyDetails.length > limit || nearestCandidates.length > limit,
7803
8158
  limit,
7804
8159
  matched: capReplayDetails(
7805
8160
  matched.map(
@@ -7815,6 +8170,27 @@ var buildReplayExchangeComparisonDetails = ({
7815
8170
  ),
7816
8171
  limit
7817
8172
  ),
8173
+ orderFailed: capReplayDetails(
8174
+ orderFailed.map((item) => {
8175
+ const detail = buildMatchedReplayDetail({
8176
+ runtime: toExchangeDetail(item.exchange),
8177
+ backtest: toBacktestParityDetail(
8178
+ item.backtest,
8179
+ backtestTimestampOffsetMs
8180
+ ),
8181
+ timestampDiffMs: item.timestampDiffMs,
8182
+ priceDeltaPct: item.priceDeltaPct
8183
+ });
8184
+ return {
8185
+ runtime: detail.runtime,
8186
+ backtest: detail.backtest,
8187
+ timestampDiffMs: detail.timestampDiffMs,
8188
+ priceDeltaPct: detail.priceDeltaPct,
8189
+ reason: item.reason
8190
+ };
8191
+ }),
8192
+ limit
8193
+ ),
7818
8194
  runtimeOnly: cappedExchangeOnlyDetails,
7819
8195
  backtestOnly: cappedBacktestOnlyDetails,
7820
8196
  nearestCandidates: cappedNearestCandidates,
@@ -7850,11 +8226,147 @@ var buildReplaySignalEvaluations = (signals) => signals.map((signal) => ({
7850
8226
  reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
7851
8227
  signalId: signal.signalId,
7852
8228
  direction: signal.direction,
8229
+ runtimeLineage: signal.runtimeLineage,
7853
8230
  orderStatus: signal.orderStatus,
7854
8231
  orderSkipReason: signal.orderSkipReason,
7855
8232
  ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
7856
8233
  ...signal.ml ? { ml: signal.ml } : {}
7857
8234
  }));
8235
+ var lineageScopeKey = (strategy, symbol) => `${strategy}::${symbol}`;
8236
+ var buildExpectedLineageByScope = (replayLineages) => new Map(
8237
+ replayLineages.map((record) => [
8238
+ lineageScopeKey(record.strategy, record.symbol),
8239
+ record.lineage
8240
+ ])
8241
+ );
8242
+ var hasExpectedLineage = ({
8243
+ strategy,
8244
+ symbol,
8245
+ lineage,
8246
+ expectedByScope
8247
+ }) => runtimeLineagesMatch(
8248
+ lineage,
8249
+ expectedByScope.get(lineageScopeKey(strategy, symbol))
8250
+ );
8251
+ var filterReplayComparisonByLineage = ({
8252
+ replayLineages,
8253
+ runtimeTrades,
8254
+ runtimeSignals,
8255
+ runtimeSignalEvaluations,
8256
+ runtimeLineageScopes,
8257
+ backtestEntries
8258
+ }) => {
8259
+ const expectedByScope = buildExpectedLineageByScope(replayLineages);
8260
+ const comparableRuntimeSignals = runtimeSignals.filter(
8261
+ (signal) => hasExpectedLineage({
8262
+ strategy: signal.strategy,
8263
+ symbol: signal.symbol,
8264
+ lineage: signal.runtimeLineage,
8265
+ expectedByScope
8266
+ })
8267
+ );
8268
+ const comparableRuntimeEvaluations = runtimeSignalEvaluations.filter(
8269
+ (evaluation) => hasExpectedLineage({
8270
+ strategy: evaluation.strategy,
8271
+ symbol: evaluation.symbol,
8272
+ lineage: evaluation.runtimeLineage,
8273
+ expectedByScope
8274
+ })
8275
+ );
8276
+ const comparableRuntimeLineageScopes = runtimeLineageScopes.filter(
8277
+ (scope) => hasExpectedLineage({
8278
+ strategy: scope.strategy,
8279
+ symbol: scope.symbol,
8280
+ lineage: scope.lineage,
8281
+ expectedByScope
8282
+ })
8283
+ );
8284
+ const deploymentWindows = /* @__PURE__ */ new Map();
8285
+ for (const scope of comparableRuntimeLineageScopes) {
8286
+ const key = lineageScopeKey(scope.strategy, scope.symbol);
8287
+ const existing = deploymentWindows.get(key);
8288
+ deploymentWindows.set(key, {
8289
+ firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
8290
+ lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
8291
+ });
8292
+ }
8293
+ const lineageBySignalId = /* @__PURE__ */ new Map();
8294
+ for (const signal of runtimeSignals) {
8295
+ if (signal.runtimeLineage) {
8296
+ lineageBySignalId.set(signal.signalId, signal.runtimeLineage);
8297
+ }
8298
+ }
8299
+ for (const evaluation of runtimeSignalEvaluations) {
8300
+ if (evaluation.signalId && evaluation.runtimeLineage) {
8301
+ lineageBySignalId.set(evaluation.signalId, evaluation.runtimeLineage);
8302
+ }
8303
+ }
8304
+ const comparableRuntimeTrades = runtimeTrades.filter((trade) => {
8305
+ const lineage = trade.signalId ? lineageBySignalId.get(trade.signalId) : void 0;
8306
+ if (lineage) {
8307
+ return hasExpectedLineage({
8308
+ strategy: trade.strategy,
8309
+ symbol: trade.symbol,
8310
+ lineage,
8311
+ expectedByScope
8312
+ });
8313
+ }
8314
+ const window2 = deploymentWindows.get(
8315
+ lineageScopeKey(trade.strategy, trade.symbol)
8316
+ );
8317
+ const signalTimestamp = trade.signalTimestamp ?? trade.entryTimestamp;
8318
+ return window2 != null && signalTimestamp >= window2.firstTimestamp && signalTimestamp <= window2.lastTimestamp;
8319
+ });
8320
+ for (const artifact of [
8321
+ ...comparableRuntimeSignals,
8322
+ ...comparableRuntimeEvaluations
8323
+ ]) {
8324
+ const key = lineageScopeKey(artifact.strategy, artifact.symbol);
8325
+ const existing = deploymentWindows.get(key);
8326
+ deploymentWindows.set(key, {
8327
+ firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
8328
+ lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
8329
+ });
8330
+ }
8331
+ const comparableBacktestEntries = backtestEntries.filter((entry) => {
8332
+ const window2 = deploymentWindows.get(
8333
+ lineageScopeKey(entry.strategy, entry.symbol)
8334
+ );
8335
+ const signalTimestamp = entry.signalTimestamp ?? entry.timestamp;
8336
+ return window2 != null && signalTimestamp >= window2.firstTimestamp && signalTimestamp <= window2.lastTimestamp;
8337
+ });
8338
+ const comparableScopeKeys = new Set(deploymentWindows.keys());
8339
+ return {
8340
+ runtimeTrades: comparableRuntimeTrades,
8341
+ runtimeSignals: comparableRuntimeSignals,
8342
+ runtimeSignalEvaluations: comparableRuntimeEvaluations,
8343
+ backtestEntries: comparableBacktestEntries,
8344
+ lineage: {
8345
+ enforced: true,
8346
+ replayScopes: expectedByScope.size,
8347
+ comparableScopes: comparableScopeKeys.size,
8348
+ excludedRuntimeTrades: runtimeTrades.length - comparableRuntimeTrades.length,
8349
+ excludedRuntimeSignals: runtimeSignals.length - comparableRuntimeSignals.length,
8350
+ excludedRuntimeEvaluations: runtimeSignalEvaluations.length - comparableRuntimeEvaluations.length,
8351
+ excludedRuntimeLineageScopes: runtimeLineageScopes.length - comparableRuntimeLineageScopes.length,
8352
+ excludedExchangeEntries: 0,
8353
+ excludedBacktestEntries: backtestEntries.length - comparableBacktestEntries.length,
8354
+ reason: comparableScopeKeys.size > 0 ? null : "no_runtime_artifacts_with_matching_lineage",
8355
+ replay: [...expectedByScope.entries()].map(([scope, lineage]) => {
8356
+ const separator = scope.indexOf("::");
8357
+ return {
8358
+ strategy: scope.slice(0, separator),
8359
+ symbol: scope.slice(separator + 2),
8360
+ lineage
8361
+ };
8362
+ }).sort(
8363
+ (left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || runtimeLineageKey(left.lineage).localeCompare(
8364
+ runtimeLineageKey(right.lineage)
8365
+ )
8366
+ )
8367
+ }
8368
+ };
8369
+ };
7858
8370
  var formatDrilldownSummary = (summary) => Object.entries(summary).filter(([, count]) => count > 0).map(([classification, count]) => `${classification}=${count}`).join(", ");
7859
8371
  var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
7860
8372
  var sumOptionalNumbers = (values) => {
@@ -7878,6 +8390,14 @@ var firstFiniteNumber = (values) => {
7878
8390
  return null;
7879
8391
  };
7880
8392
  var firstString = (values) => values.find((value) => typeof value === "string" && value.trim());
8393
+ var summarizeBacktestPnlByStrategy = (entries) => {
8394
+ const totals = /* @__PURE__ */ new Map();
8395
+ for (const entry of entries) {
8396
+ const pnl = typeof entry.expectedPnl === "number" && Number.isFinite(entry.expectedPnl) ? entry.expectedPnl : 0;
8397
+ totals.set(entry.strategy, (totals.get(entry.strategy) ?? 0) + pnl);
8398
+ }
8399
+ return totals;
8400
+ };
7881
8401
  var getExchangeExecutionMergeKey = (entry) => {
7882
8402
  const orderId = firstString([entry.orderId, entry.orderLinkId]);
7883
8403
  return orderId ? `${entry.symbol}::${entry.direction}::${orderId}` : null;
@@ -8144,16 +8664,89 @@ var compareExchangeEntriesToBacktest = ({
8144
8664
  backtestOnly
8145
8665
  };
8146
8666
  };
8667
+ var findExchangeRuntimeOutcome = ({
8668
+ exchangeEntry,
8669
+ strategyName,
8670
+ runtimeSignals,
8671
+ runtimeSignalEvaluations,
8672
+ toleranceMs,
8673
+ signalTimestampOffsetMs
8674
+ }) => {
8675
+ const candidates = [];
8676
+ for (const signal of runtimeSignals) {
8677
+ if (signal.strategy === strategyName && signal.symbol === exchangeEntry.symbol && signal.direction === exchangeEntry.direction) {
8678
+ candidates.push({
8679
+ timestamp: signal.timestamp,
8680
+ orderStatus: signal.orderStatus,
8681
+ reason: signal.orderFailureReason || signal.orderSkipReason || signal.orderStatus
8682
+ });
8683
+ }
8684
+ }
8685
+ for (const evaluation of runtimeSignalEvaluations) {
8686
+ if (evaluation.strategy === strategyName && evaluation.symbol === exchangeEntry.symbol && (evaluation.direction == null || evaluation.direction === exchangeEntry.direction)) {
8687
+ candidates.push({
8688
+ timestamp: evaluation.timestamp,
8689
+ orderStatus: evaluation.orderStatus,
8690
+ reason: evaluation.orderSkipReason || evaluation.reason || evaluation.orderStatus
8691
+ });
8692
+ }
8693
+ }
8694
+ return candidates.map((candidate) => ({
8695
+ ...candidate,
8696
+ timestampDiffMs: Math.abs(
8697
+ candidate.timestamp + signalTimestampOffsetMs - exchangeEntry.entryTimestamp
8698
+ )
8699
+ })).filter((candidate) => candidate.timestampDiffMs <= toleranceMs).sort(
8700
+ (left, right) => left.timestampDiffMs - right.timestampDiffMs || Number(right.orderStatus === "failed") - Number(left.orderStatus === "failed")
8701
+ )[0] ?? null;
8702
+ };
8703
+ var splitExchangeMatchesByRuntimeOrderStatus = ({
8704
+ matched,
8705
+ strategyNameByOrderLinkKey,
8706
+ runtimeSignals,
8707
+ runtimeSignalEvaluations,
8708
+ toleranceMs,
8709
+ signalTimestampOffsetMs
8710
+ }) => {
8711
+ const orderFailed = [];
8712
+ const completed = matched.filter((item) => {
8713
+ const strategyName = resolveReplayStrategyNameFromExchangeEntry({
8714
+ exchangeEntry: item.exchange,
8715
+ strategyNameByOrderLinkKey
8716
+ });
8717
+ if (!strategyName) {
8718
+ return true;
8719
+ }
8720
+ const outcome = findExchangeRuntimeOutcome({
8721
+ exchangeEntry: item.exchange,
8722
+ strategyName,
8723
+ runtimeSignals,
8724
+ runtimeSignalEvaluations,
8725
+ toleranceMs,
8726
+ signalTimestampOffsetMs
8727
+ });
8728
+ if (outcome?.orderStatus !== "failed") {
8729
+ return true;
8730
+ }
8731
+ orderFailed.push({
8732
+ ...item,
8733
+ reason: outcome.reason || "orderStatus=failed"
8734
+ });
8735
+ return false;
8736
+ });
8737
+ return { completed, orderFailed };
8738
+ };
8147
8739
  var saveAndPrintReplayExchangeComparison = async ({
8148
8740
  liveStrategySummaries,
8149
8741
  backtestEntries,
8150
8742
  replaySignals,
8151
8743
  replaySignalEvaluations,
8152
8744
  runtimeSignals,
8153
- runtimeSignalEvaluations
8745
+ runtimeSignalEvaluations,
8746
+ lineage
8154
8747
  }) => {
8155
8748
  const { connector, connectorName, window: window2 } = getRuntimeCompareContext();
8156
- const exchangeEntries = await loadExchangeEntriesForComparison({
8749
+ const rawExchangeEntries = await loadExchangeEntriesForComparison({
8157
8750
  connector,
8158
8751
  startTime: window2.start,
8159
8752
  endTime: window2.end
@@ -8161,11 +8754,36 @@ var saveAndPrintReplayExchangeComparison = async ({
8161
8754
  const strategyNameByOrderLinkKey = buildStrategyNameByOrderLinkKey(
8162
8755
  liveStrategySummaries.map((summary) => summary.strategyName)
8163
8756
  );
8757
+ const exchangeOutcomeByEntry = /* @__PURE__ */ new Map();
8758
+ const exchangeEntries = rawExchangeEntries.filter((entry) => {
8759
+ const strategyName = resolveReplayStrategyNameFromExchangeEntry({
8760
+ exchangeEntry: entry,
8761
+ strategyNameByOrderLinkKey
8762
+ });
8763
+ if (!strategyName) {
8764
+ return false;
8765
+ }
8766
+ const outcome = findExchangeRuntimeOutcome({
8767
+ exchangeEntry: entry,
8768
+ strategyName,
8769
+ runtimeSignals,
8770
+ runtimeSignalEvaluations,
8771
+ toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8772
+ signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8773
+ });
8774
+ exchangeOutcomeByEntry.set(entry, outcome);
8775
+ return outcome != null;
8776
+ });
8777
+ const comparisonLineage = {
8778
+ ...lineage,
8779
+ excludedExchangeEntries: lineage.excludedExchangeEntries + rawExchangeEntries.length - exchangeEntries.length
8780
+ };
8781
+ const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
8164
8782
  if (!exchangeEntries.length) {
8165
8783
  console.log("");
8166
8784
  console.log(
8167
8785
  import_chalk11.default.yellow(
8168
- `SIGNALS REPLAY VS EXCHANGE: no exchange entry executions found for ${connectorName} in ${(0, import_time6.formatUnix)(
8786
+ `SIGNALS REPLAY VS EXCHANGE: no lineage-linked exchange entry executions found for ${connectorName} in ${(0, import_time6.formatUnix)(
8169
8787
  window2.start
8170
8788
  )} -> ${(0, import_time6.formatUnix)(window2.end)}`
8171
8789
  )
@@ -8192,18 +8810,21 @@ var saveAndPrintReplayExchangeComparison = async ({
8192
8810
  runtimeEntriesCount: 0,
8193
8811
  backtestEntriesCount: backtestEntries.length,
8194
8812
  matchedCount: 0,
8813
+ orderFailedCount: 0,
8195
8814
  runtimeOnlyCount: 0,
8196
8815
  backtestOnlyCount: backtestEntries.length,
8197
8816
  details: details2,
8817
+ lineage: comparisonLineage,
8198
8818
  rows: liveStrategySummaries.map((summary) => ({
8199
8819
  strategyName: summary.strategyName,
8200
8820
  backtestEntries: backtestEntries.filter(
8201
8821
  (entry) => entry.strategy === summary.strategyName
8202
8822
  ).length,
8203
- backtestNetProfit: summary.netProfit,
8823
+ backtestNetProfit: backtestPnlByStrategy.get(summary.strategyName) ?? 0,
8204
8824
  runtimeTrades: 0,
8205
8825
  runtimePnl: 0,
8206
8826
  matched: 0,
8827
+ orderFailed: 0,
8207
8828
  runtimeOnly: 0,
8208
8829
  backtestOnly: backtestEntries.filter(
8209
8830
  (entry) => entry.strategy === summary.strategyName
@@ -8217,6 +8838,14 @@ var saveAndPrintReplayExchangeComparison = async ({
8217
8838
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8218
8839
  backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8219
8840
  });
8841
+ const { completed: matched, orderFailed } = splitExchangeMatchesByRuntimeOrderStatus({
8842
+ matched: comparison.matched,
8843
+ strategyNameByOrderLinkKey,
8844
+ runtimeSignals,
8845
+ runtimeSignalEvaluations,
8846
+ toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8847
+ signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8848
+ });
8220
8849
  const liveSummaryByStrategy = new Map(
8221
8850
  liveStrategySummaries.map((summary) => [summary.strategyName, summary])
8222
8851
  );
@@ -8229,10 +8858,11 @@ var saveAndPrintReplayExchangeComparison = async ({
8229
8858
  const next = {
8230
8859
  strategyName,
8231
8860
  backtestEntries: 0,
8232
- backtestNetProfit: liveSummaryByStrategy.get(strategyName)?.netProfit ?? 0,
8861
+ backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
8233
8862
  runtimeTrades: 0,
8234
8863
  runtimePnl: 0,
8235
8864
  matched: 0,
8865
+ orderFailed: 0,
8236
8866
  runtimeOnly: 0,
8237
8867
  backtestOnly: 0
8238
8868
  };
@@ -8245,7 +8875,7 @@ var saveAndPrintReplayExchangeComparison = async ({
8245
8875
  for (const entry of backtestEntries) {
8246
8876
  ensureRow(entry.strategy).backtestEntries += 1;
8247
8877
  }
8248
- for (const item of comparison.matched) {
8878
+ for (const item of matched) {
8249
8879
  const row = ensureRow(item.backtest.strategy);
8250
8880
  row.runtimeTrades += 1;
8251
8881
  row.matched += 1;
@@ -8253,6 +8883,11 @@ var saveAndPrintReplayExchangeComparison = async ({
8253
8883
  row.runtimePnl += item.exchange.closedPnl;
8254
8884
  }
8255
8885
  }
8886
+ for (const item of orderFailed) {
8887
+ const row = ensureRow(item.backtest.strategy);
8888
+ row.runtimeTrades += 1;
8889
+ row.orderFailed += 1;
8890
+ }
8256
8891
  for (const entry of comparison.backtestOnly) {
8257
8892
  ensureRow(entry.strategy).backtestOnly += 1;
8258
8893
  }
@@ -8271,7 +8906,8 @@ var saveAndPrintReplayExchangeComparison = async ({
8271
8906
  }
8272
8907
  }
8273
8908
  const details = buildReplayExchangeComparisonDetails({
8274
- matched: comparison.matched,
8909
+ matched,
8910
+ orderFailed,
8275
8911
  exchangeOnly: comparison.exchangeOnly,
8276
8912
  backtestOnly: comparison.backtestOnly,
8277
8913
  exchangeEntries,
@@ -8298,8 +8934,10 @@ var saveAndPrintReplayExchangeComparison = async ({
8298
8934
  import_chalk11.default.yellow(String(row.runtimeTrades)),
8299
8935
  rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
8300
8936
  import_chalk11.default.green(String(row.matched)),
8937
+ import_chalk11.default.red(String(row.orderFailed)),
8301
8938
  import_chalk11.default.yellow(String(row.runtimeOnly)),
8302
- import_chalk11.default.magenta(String(row.backtestOnly))
8939
+ import_chalk11.default.magenta(String(row.backtestOnly)),
8940
+ import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
8303
8941
  ];
8304
8942
  });
8305
8943
  console.log("");
@@ -8327,17 +8965,20 @@ var saveAndPrintReplayExchangeComparison = async ({
8327
8965
  windowTradesCount: exchangeEntries.length,
8328
8966
  runtimeEntriesCount: exchangeEntries.length,
8329
8967
  backtestEntriesCount: backtestEntries.length,
8330
- matchedCount: comparison.matched.length,
8968
+ matchedCount: matched.length,
8969
+ orderFailedCount: orderFailed.length,
8331
8970
  runtimeOnlyCount: comparison.exchangeOnly.length,
8332
8971
  backtestOnlyCount: comparison.backtestOnly.length,
8333
8972
  rows,
8334
- details
8973
+ details,
8974
+ lineage: comparisonLineage
8335
8975
  };
8336
8976
  };
8337
8977
  var saveAndPrintReplayRuntimeComparison = async ({
8338
8978
  liveStrategySummaries,
8339
8979
  backtestEntries,
8340
- replaySignals
8980
+ replaySignals,
8981
+ replayLineages
8341
8982
  }) => {
8342
8983
  const { connector, connectorName, window: window2 } = getRuntimeCompareContext();
8343
8984
  if (!connector || !window2) {
@@ -8347,8 +8988,16 @@ var saveAndPrintReplayRuntimeComparison = async ({
8347
8988
  liveStrategySummaries.map((summary) => summary.strategyName)
8348
8989
  );
8349
8990
  const replaySignalEvaluations = buildReplaySignalEvaluations(replaySignals);
8350
- const [rawRuntimeTrades, runtimeSignals, runtimeSignalEvaluations] = await Promise.all([
8351
- loadRuntimeTrades(replayUserName),
8991
+ const [
8992
+ rawRuntimeTrades,
8993
+ runtimeSignals,
8994
+ runtimeSignalEvaluations,
8995
+ runtimeLineageScopes
8996
+ ] = await Promise.all([
8997
+ loadRuntimeTrades(replayUserName, {
8998
+ startTime: window2.start,
8999
+ endTime: window2.end
9000
+ }),
8352
9001
  loadRuntimeSignals(replayUserName, {
8353
9002
  startTime: window2.start,
8354
9003
  endTime: window2.end
@@ -8356,6 +9005,10 @@ var saveAndPrintReplayRuntimeComparison = async ({
8356
9005
  loadRuntimeSignalEvaluations(replayUserName, {
8357
9006
  startTime: window2.start,
8358
9007
  endTime: window2.end
9008
+ }),
9009
+ loadRuntimeLineageScopes(replayUserName, {
9010
+ startTime: window2.start,
9011
+ endTime: window2.end
8359
9012
  })
8360
9013
  ]);
8361
9014
  const syncedRuntimeTrades = await syncRuntimeTrades({
@@ -8397,7 +9050,16 @@ var saveAndPrintReplayRuntimeComparison = async ({
8397
9050
  }
8398
9051
  }
8399
9052
  });
8400
- const windowRuntimeTrades = syncedRuntimeTrades.filter(
9053
+ const comparable = filterReplayComparisonByLineage({
9054
+ replayLineages,
9055
+ runtimeTrades: syncedRuntimeTrades,
9056
+ runtimeSignals,
9057
+ runtimeSignalEvaluations,
9058
+ runtimeLineageScopes,
9059
+ backtestEntries
9060
+ });
9061
+ const comparableBacktestEntries = comparable.backtestEntries;
9062
+ const windowRuntimeTrades = comparable.runtimeTrades.filter(
8401
9063
  (trade) => trade.entryTimestamp >= window2.start && trade.entryTimestamp < window2.end && relevantStrategies.has(trade.strategy)
8402
9064
  );
8403
9065
  if (!windowRuntimeTrades.length) {
@@ -8406,17 +9068,18 @@ var saveAndPrintReplayRuntimeComparison = async ({
8406
9068
  import_chalk11.default.yellow(
8407
9069
  `SIGNALS REPLAY VS RUNTIME: no local runtime trades found for ${connectorName} in ${(0, import_time6.formatUnix)(
8408
9070
  window2.start
8409
- )} -> ${(0, import_time6.formatUnix)(window2.end)}; falling back to direct exchange comparison`
9071
+ )} -> ${(0, import_time6.formatUnix)(window2.end)} with matching lineage; checking lineage-linked exchange executions`
8410
9072
  )
8411
9073
  );
8412
9074
  console.log("");
8413
9075
  return saveAndPrintReplayExchangeComparison({
8414
9076
  liveStrategySummaries,
8415
- backtestEntries,
9077
+ backtestEntries: comparableBacktestEntries,
8416
9078
  replaySignals,
8417
9079
  replaySignalEvaluations,
8418
- runtimeSignals,
8419
- runtimeSignalEvaluations
9080
+ runtimeSignals: comparable.runtimeSignals,
9081
+ runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
9082
+ lineage: comparable.lineage
8420
9083
  });
8421
9084
  }
8422
9085
  const runtimeSummaries = summarizeRuntimeTradesByStrategy(windowRuntimeTrades);
@@ -8427,7 +9090,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
8427
9090
  const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
8428
9091
  const comparison = compareTradeParityEntries({
8429
9092
  runtimeEntries: runtimeDedupe.entries,
8430
- backtestEntries,
9093
+ backtestEntries: comparableBacktestEntries,
8431
9094
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8432
9095
  backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8433
9096
  });
@@ -8436,18 +9099,18 @@ var saveAndPrintReplayRuntimeComparison = async ({
8436
9099
  runtimeOnly: comparison.runtimeOnly,
8437
9100
  backtestOnly: comparison.backtestOnly,
8438
9101
  runtimeEntries: runtimeDedupe.entries,
8439
- backtestEntries,
9102
+ backtestEntries: comparableBacktestEntries,
8440
9103
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8441
9104
  backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs(),
8442
- runtimeSignals,
8443
- runtimeSignalEvaluations,
9105
+ runtimeSignals: comparable.runtimeSignals,
9106
+ runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
8444
9107
  replaySignals,
8445
9108
  replaySignalEvaluations
8446
9109
  });
8447
9110
  const parityRows = summarizeTradeParityByStrategy({
8448
9111
  runtimeEntries: runtimeDedupe.entries,
8449
9112
  runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
8450
- backtestEntries,
9113
+ backtestEntries: comparableBacktestEntries,
8451
9114
  matchedEntries: comparison.matched,
8452
9115
  runtimeOnlyEntries: comparison.runtimeOnly,
8453
9116
  backtestOnlyEntries: comparison.backtestOnly
@@ -8456,22 +9119,25 @@ var saveAndPrintReplayRuntimeComparison = async ({
8456
9119
  const liveSummaryByStrategy = new Map(
8457
9120
  liveStrategySummaries.map((summary) => [summary.strategyName, summary])
8458
9121
  );
9122
+ const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(
9123
+ comparableBacktestEntries
9124
+ );
8459
9125
  const strategyNames = /* @__PURE__ */ new Set([
8460
9126
  ...liveSummaryByStrategy.keys(),
8461
9127
  ...runtimeSummaryByStrategy.keys(),
8462
9128
  ...parityByStrategy.keys()
8463
9129
  ]);
8464
9130
  const rows = [...strategyNames].sort((left, right) => left.localeCompare(right)).map((strategyName) => {
8465
- const liveSummary = liveSummaryByStrategy.get(strategyName);
8466
9131
  const runtimeSummary = runtimeSummaryByStrategy.get(strategyName);
8467
9132
  const parity = parityByStrategy.get(strategyName);
8468
9133
  return {
8469
9134
  strategyName,
8470
9135
  backtestEntries: parity?.backtest ?? 0,
8471
- backtestNetProfit: liveSummary?.netProfit ?? 0,
9136
+ backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
8472
9137
  runtimeTrades: runtimeSummary?.trades ?? 0,
8473
9138
  runtimePnl: runtimeSummary?.totalPnl ?? 0,
8474
9139
  matched: parity?.matched ?? 0,
9140
+ orderFailed: 0,
8475
9141
  runtimeOnly: parity?.runtimeOnly ?? 0,
8476
9142
  backtestOnly: parity?.backtestOnly ?? 0
8477
9143
  };
@@ -8486,8 +9152,10 @@ var saveAndPrintReplayRuntimeComparison = async ({
8486
9152
  import_chalk11.default.yellow(String(row.runtimeTrades)),
8487
9153
  rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
8488
9154
  import_chalk11.default.green(String(row.matched)),
9155
+ import_chalk11.default.red(String(row.orderFailed)),
8489
9156
  import_chalk11.default.yellow(String(row.runtimeOnly)),
8490
- import_chalk11.default.magenta(String(row.backtestOnly))
9157
+ import_chalk11.default.magenta(String(row.backtestOnly)),
9158
+ import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
8491
9159
  ];
8492
9160
  });
8493
9161
  console.log("");
@@ -8514,18 +9182,20 @@ var saveAndPrintReplayRuntimeComparison = async ({
8514
9182
  syncedTradesCount: syncedRuntimeTrades.length,
8515
9183
  windowTradesCount: windowRuntimeTrades.length,
8516
9184
  runtimeEntriesCount: runtimeDedupe.entries.length,
8517
- backtestEntriesCount: backtestEntries.length,
9185
+ backtestEntriesCount: comparableBacktestEntries.length,
8518
9186
  matchedCount: comparison.matched.length,
9187
+ orderFailedCount: 0,
8519
9188
  runtimeOnlyCount: comparison.runtimeOnly.length,
8520
9189
  backtestOnlyCount: comparison.backtestOnly.length,
8521
9190
  rows,
8522
- details
9191
+ details,
9192
+ lineage: comparable.lineage
8523
9193
  };
8524
9194
  };
8525
9195
 
8526
9196
  // src/lib/replay/outputReport.ts
8527
- var import_promises = __toESM(require("fs/promises"));
8528
- var import_node_path = __toESM(require("path"));
9197
+ var import_promises2 = __toESM(require("fs/promises"));
9198
+ var import_node_path2 = __toESM(require("path"));
8529
9199
  var import_data = require("@tradejs/core/data");
8530
9200
  var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
8531
9201
  timeZone: "Europe/Moscow",
@@ -8560,6 +9230,7 @@ var buildPerTradeAnalysis = (runtimeComparison) => {
8560
9230
  if (!details) {
8561
9231
  return {
8562
9232
  matched: [],
9233
+ orderFailed: [],
8563
9234
  runtimeOnly: [],
8564
9235
  backtestOnly: []
8565
9236
  };
@@ -8588,6 +9259,17 @@ var buildPerTradeAnalysis = (runtimeComparison) => {
8588
9259
  slippage: item.slippage
8589
9260
  }
8590
9261
  })),
9262
+ orderFailed: details.orderFailed.map((item) => ({
9263
+ status: "orderFailed",
9264
+ strategy: entryStrategy(item.backtest),
9265
+ symbol: item.backtest.symbol,
9266
+ direction: item.backtest.direction,
9267
+ runtime: item.runtime,
9268
+ backtest: item.backtest,
9269
+ reason: item.reason,
9270
+ timestampDiffMs: item.timestampDiffMs,
9271
+ entryPriceDeltaPct: item.priceDeltaPct
9272
+ })),
8591
9273
  runtimeOnly: details.runtimeOnly.map((entry) => ({
8592
9274
  status: "runtimeOnly",
8593
9275
  strategy: entryStrategy(entry),
@@ -8671,6 +9353,26 @@ var renderRuntimeOnlyRows = (perTrade) => perTrade.length ? createMarkdownTable(
8671
9353
  ];
8672
9354
  })
8673
9355
  ) : "_No runtime-only trades._";
9356
+ var renderOrderFailedRows = (perTrade) => perTrade.length ? createMarkdownTable(
9357
+ [
9358
+ "Strategy",
9359
+ "Symbol",
9360
+ "Dir",
9361
+ "BT time",
9362
+ "RT time",
9363
+ "Entry delta",
9364
+ "Reason"
9365
+ ],
9366
+ perTrade.map((item) => [
9367
+ item.strategy,
9368
+ item.symbol,
9369
+ item.direction,
9370
+ formatMsk(item.backtest.timestamp),
9371
+ formatMsk(item.runtime.timestamp),
9372
+ formatPct(item.entryPriceDeltaPct),
9373
+ item.reason
9374
+ ])
9375
+ ) : "_No failed runtime orders._";
8674
9376
  var renderBacktestOnlyRows = (perTrade) => perTrade.length ? createMarkdownTable(
8675
9377
  [
8676
9378
  "Strategy",
@@ -8708,8 +9410,10 @@ var renderStrategyRows = (runtimeComparison) => runtimeComparison?.rows.length ?
8708
9410
  "RT trades",
8709
9411
  "RT PnL",
8710
9412
  "Matched",
9413
+ "Order failed",
8711
9414
  "RT only",
8712
- "BT only"
9415
+ "BT only",
9416
+ "Unmatched"
8713
9417
  ],
8714
9418
  runtimeComparison.rows.map((row) => [
8715
9419
  row.strategyName,
@@ -8718,10 +9422,69 @@ var renderStrategyRows = (runtimeComparison) => runtimeComparison?.rows.length ?
8718
9422
  row.runtimeTrades,
8719
9423
  formatMoney(row.runtimePnl),
8720
9424
  row.matched,
9425
+ row.orderFailed,
8721
9426
  row.runtimeOnly,
8722
- row.backtestOnly
9427
+ row.backtestOnly,
9428
+ getReplayRuntimeUnmatchedCount(row)
8723
9429
  ])
8724
9430
  ) : "_No runtime comparison rows._";
9431
+ var renderLineageRows = (runtimeComparison) => {
9432
+ const grouped = /* @__PURE__ */ new Map();
9433
+ for (const record of runtimeComparison?.lineage.replay ?? []) {
9434
+ const existing = grouped.get(record.strategy) ?? {
9435
+ strategy: record.strategy,
9436
+ scopes: 0,
9437
+ gitShas: /* @__PURE__ */ new Set(),
9438
+ dirty: /* @__PURE__ */ new Set(),
9439
+ gates: /* @__PURE__ */ new Set(),
9440
+ configs: /* @__PURE__ */ new Set(),
9441
+ contexts: /* @__PURE__ */ new Set()
9442
+ };
9443
+ existing.scopes += 1;
9444
+ existing.gitShas.add(record.lineage.gitSha ?? "unknown");
9445
+ existing.dirty.add(
9446
+ record.lineage.gitDirty == null ? "unknown" : String(record.lineage.gitDirty)
9447
+ );
9448
+ existing.gates.add(record.lineage.gateFingerprint);
9449
+ existing.configs.add(record.lineage.configFingerprint);
9450
+ existing.contexts.add(record.lineage.contextFingerprint);
9451
+ grouped.set(record.strategy, existing);
9452
+ }
9453
+ const formatSet = (values) => {
9454
+ const sorted = [...values].sort();
9455
+ return sorted.length <= 3 ? sorted.join(", ") : `${sorted.slice(0, 3).join(", ")} \u2026 (+${sorted.length - 3})`;
9456
+ };
9457
+ return grouped.size ? createMarkdownTable(
9458
+ [
9459
+ "Strategy",
9460
+ "Scopes",
9461
+ "Git SHA",
9462
+ "Dirty",
9463
+ "Gate",
9464
+ "Config variants",
9465
+ "Context"
9466
+ ],
9467
+ [...grouped.values()].sort((left, right) => left.strategy.localeCompare(right.strategy)).map(
9468
+ ({
9469
+ strategy,
9470
+ scopes,
9471
+ gitShas,
9472
+ dirty,
9473
+ gates,
9474
+ configs,
9475
+ contexts
9476
+ }) => [
9477
+ strategy,
9478
+ scopes,
9479
+ formatSet(gitShas),
9480
+ formatSet(dirty),
9481
+ formatSet(gates),
9482
+ `${configs.size}: ${formatSet(configs)}`,
9483
+ formatSet(contexts)
9484
+ ]
9485
+ )
9486
+ ) : "_No replay lineage records._";
9487
+ };
8725
9488
  var writeReplayOutputReport = async ({
8726
9489
  projectRoot,
8727
9490
  timestamp,
@@ -8736,8 +9499,8 @@ var writeReplayOutputReport = async ({
8736
9499
  strategySnapshot,
8737
9500
  runtimeComparison
8738
9501
  }) => {
8739
- const outputDir = import_node_path.default.resolve(projectRoot, "data/replay/output");
8740
- await import_promises.default.mkdir(outputDir, { recursive: true });
9502
+ const outputDir = import_node_path2.default.resolve(projectRoot, "data/replay/output");
9503
+ await import_promises2.default.mkdir(outputDir, { recursive: true });
8741
9504
  const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
8742
9505
  const report = {
8743
9506
  reportType: "replay-output",
@@ -8763,15 +9526,17 @@ var writeReplayOutputReport = async ({
8763
9526
  cycleCount: replayResult.cycleCount,
8764
9527
  abortedCycles: replayResult.abortedCycles,
8765
9528
  matched: runtimeComparison?.matchedCount ?? 0,
9529
+ orderFailed: runtimeComparison?.orderFailedCount ?? 0,
8766
9530
  runtimeOnly: runtimeComparison?.runtimeOnlyCount ?? 0,
8767
9531
  backtestOnly: runtimeComparison?.backtestOnlyCount ?? 0
8768
9532
  },
8769
9533
  resultsByStrategies: strategySnapshot.summaries,
9534
+ replayLineage: replayResult.runtimeLineages,
8770
9535
  runtimeComparison,
8771
9536
  perTradeAnalysis
8772
9537
  };
8773
- const markdownPath = import_node_path.default.join(outputDir, `${timestamp}-replay-report.md`);
8774
- const jsonPath = import_node_path.default.join(outputDir, `${timestamp}-replay-report.json`);
9538
+ const markdownPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.md`);
9539
+ const jsonPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.json`);
8775
9540
  const lines = [
8776
9541
  "# Replay Report",
8777
9542
  "",
@@ -8791,13 +9556,26 @@ var writeReplayOutputReport = async ({
8791
9556
  ["Window", `${formatMsk(window2.start)} - ${formatMsk(window2.end)} MSK`],
8792
9557
  ["Runtime compare mode", runtimeComparison?.mode ?? "none"],
8793
9558
  ["Matched", runtimeComparison?.matchedCount ?? 0],
9559
+ ["Order failed", runtimeComparison?.orderFailedCount ?? 0],
8794
9560
  ["Runtime-only", runtimeComparison?.runtimeOnlyCount ?? 0],
8795
9561
  ["Backtest-only", runtimeComparison?.backtestOnlyCount ?? 0],
9562
+ [
9563
+ "Comparable lineage scopes",
9564
+ runtimeComparison ? `${runtimeComparison.lineage.comparableScopes}/${runtimeComparison.lineage.replayScopes}` : "0/0"
9565
+ ],
9566
+ [
9567
+ "Lineage exclusions",
9568
+ runtimeComparison ? `runtime=${runtimeComparison.lineage.excludedRuntimeTrades}, exchange=${runtimeComparison.lineage.excludedExchangeEntries}, backtest=${runtimeComparison.lineage.excludedBacktestEntries}` : ""
9569
+ ],
8796
9570
  ["Command", `\`${process.argv.join(" ")}\``],
8797
9571
  ["JSON report", jsonPath]
8798
9572
  ]
8799
9573
  ),
8800
9574
  "",
9575
+ "## Replay Lineage",
9576
+ "",
9577
+ renderLineageRows(runtimeComparison),
9578
+ "",
8801
9579
  "## Strategy Comparison",
8802
9580
  "",
8803
9581
  renderStrategyRows(runtimeComparison),
@@ -8806,6 +9584,10 @@ var writeReplayOutputReport = async ({
8806
9584
  "",
8807
9585
  renderMatchedRows(perTradeAnalysis.matched),
8808
9586
  "",
9587
+ "## Failed Runtime Orders",
9588
+ "",
9589
+ renderOrderFailedRows(perTradeAnalysis.orderFailed),
9590
+ "",
8809
9591
  "## Runtime-Only Trades",
8810
9592
  "",
8811
9593
  renderRuntimeOnlyRows(perTradeAnalysis.runtimeOnly),
@@ -8820,8 +9602,8 @@ var writeReplayOutputReport = async ({
8820
9602
  ""
8821
9603
  ];
8822
9604
  await Promise.all([
8823
- import_promises.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
8824
- import_promises.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
9605
+ import_promises2.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
9606
+ import_promises2.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
8825
9607
  `, "utf8")
8826
9608
  ]);
8827
9609
  return {
@@ -8860,7 +9642,8 @@ var finishReplay = async ({
8860
9642
  const replayRuntimeComparison = await saveAndPrintReplayRuntimeComparison({
8861
9643
  liveStrategySummaries: replayStrategySnapshot.summaries,
8862
9644
  backtestEntries: replayStrategySnapshot.backtestEntries,
8863
- replaySignals: replayResult.signals
9645
+ replaySignals: replayResult.signals,
9646
+ replayLineages: replayResult.runtimeLineages
8864
9647
  });
8865
9648
  const finishedAt = /* @__PURE__ */ new Date();
8866
9649
  const durationSeconds = Number(
@@ -8876,7 +9659,7 @@ var finishReplay = async ({
8876
9659
  replayResult.orderLog[replayResult.orderLog.length - 1]?.timestamp ?? finishedAt.getTime()
8877
9660
  )}`
8878
9661
  }) : null;
8879
- const replayKey = import_redis5.redisKeys.backtestResults(
9662
+ const replayKey = import_redis6.redisKeys.backtestResults(
8880
9663
  replayUserName,
8881
9664
  REPLAY_RESULTS_CONFIG,
8882
9665
  timestamp
@@ -8897,7 +9680,7 @@ var finishReplay = async ({
8897
9680
  });
8898
9681
  console.log(import_chalk12.default.green(`Replay report: ${outputReport.markdownPath}`));
8899
9682
  console.log(import_chalk12.default.green(`Replay report JSON: ${outputReport.jsonPath}`));
8900
- await (0, import_redis5.setData)(
9683
+ await (0, import_redis6.setData)(
8901
9684
  replayKey,
8902
9685
  {
8903
9686
  config: REPLAY_RESULTS_CONFIG,
@@ -8918,6 +9701,7 @@ var finishReplay = async ({
8918
9701
  cycleCount: replayResult.cycleCount,
8919
9702
  abortedCycles: replayResult.abortedCycles,
8920
9703
  signalsCount: replayResult.signals.length,
9704
+ replayLineage: replayResult.runtimeLineages,
8921
9705
  strategyCharts: replayChartSnapshot,
8922
9706
  outputReport
8923
9707
  },
@@ -8928,8 +9712,8 @@ var finishReplay = async ({
8928
9712
  if (replayChartSnapshot) {
8929
9713
  await Promise.all(
8930
9714
  replayChartSnapshot.strategies.map(
8931
- (card) => (0, import_redis5.setData)(
8932
- import_redis5.redisKeys.strategyChartCard(replayUserName, "replay", card.cardId),
9715
+ (card) => (0, import_redis6.setData)(
9716
+ import_redis6.redisKeys.strategyChartCard(replayUserName, "replay", card.cardId),
8933
9717
  card,
8934
9718
  {
8935
9719
  expire: import_constants6.TTL_1M
@@ -8954,7 +9738,8 @@ var replayBacktest = async () => {
8954
9738
  endTime: replayFlags.endTime,
8955
9739
  cacheOnly: replayFlags.cacheOnly,
8956
9740
  interval: replayInterval,
8957
- projectRoot: replayProjectRoot
9741
+ projectRoot: replayProjectRoot,
9742
+ closedIntervalMs: (0, import_data2.intervalToMs)(replayInterval)
8958
9743
  });
8959
9744
  if (!preparedRun || isReplayUpdateOnlyRun) {
8960
9745
  return;