@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();
@@ -1631,8 +1646,9 @@ __export(replayRunner_exports, {
1631
1646
  module.exports = __toCommonJS(replayRunner_exports);
1632
1647
  var import_chalk12 = __toESM(require("chalk"));
1633
1648
  var import_constants6 = require("@tradejs/core/constants");
1649
+ var import_data2 = require("@tradejs/core/data");
1634
1650
  var import_time7 = require("@tradejs/core/time");
1635
- var import_redis5 = require("@tradejs/infra/redis");
1651
+ var import_redis6 = require("@tradejs/infra/redis");
1636
1652
 
1637
1653
  // src/lib/runFormatting.ts
1638
1654
  var import_date_fns = require("date-fns");
@@ -1694,11 +1710,30 @@ var resolveTimeWindow = ({
1694
1710
  endTime,
1695
1711
  nowMs = Date.now(),
1696
1712
  defaultStartMs,
1697
- defaultEndMs
1713
+ defaultEndMs,
1714
+ closedIntervalMs
1698
1715
  }) => {
1699
1716
  const explicitStart = toEpochMs(startTime);
1700
1717
  const explicitEnd = toEpochMs(endTime);
1701
1718
  const resolvedDays = toPositiveNumber(days);
1719
+ const alignToLastClosedInterval = (window2, preserveDuration) => {
1720
+ if (typeof closedIntervalMs !== "number" || !Number.isFinite(closedIntervalMs) || closedIntervalMs <= 0) {
1721
+ return window2;
1722
+ }
1723
+ const alignedEnd = Math.floor(window2.end / closedIntervalMs) * closedIntervalMs - 1;
1724
+ const delta = window2.end - alignedEnd;
1725
+ const alignedStart = preserveDuration ? window2.start - delta : window2.start;
1726
+ if (alignedStart >= alignedEnd) {
1727
+ throw new Error(
1728
+ `Invalid closed-candle window: start (${alignedStart}) must be less than end (${alignedEnd})`
1729
+ );
1730
+ }
1731
+ return {
1732
+ ...window2,
1733
+ start: Math.trunc(alignedStart),
1734
+ end: Math.trunc(alignedEnd)
1735
+ };
1736
+ };
1702
1737
  if (explicitStart != null || explicitEnd != null) {
1703
1738
  const resolvedStart = explicitStart ?? (resolvedDays != null && explicitEnd != null ? explicitEnd - resolvedDays * ONE_DAY_MS : defaultStartMs);
1704
1739
  const resolvedEnd2 = explicitEnd ?? (resolvedDays != null && explicitStart != null ? explicitStart + resolvedDays * ONE_DAY_MS : defaultEndMs ?? nowMs);
@@ -1707,11 +1742,14 @@ var resolveTimeWindow = ({
1707
1742
  `Invalid time window: startTime (${resolvedStart}) must be less than endTime (${resolvedEnd2})`
1708
1743
  );
1709
1744
  }
1710
- return {
1711
- start: resolvedStart,
1712
- end: resolvedEnd2,
1713
- source: "explicit"
1714
- };
1745
+ return alignToLastClosedInterval(
1746
+ {
1747
+ start: resolvedStart,
1748
+ end: resolvedEnd2,
1749
+ source: "explicit"
1750
+ },
1751
+ explicitStart == null && resolvedDays != null
1752
+ );
1715
1753
  }
1716
1754
  if (resolvedDays != null) {
1717
1755
  const resolvedEnd2 = defaultEndMs ?? nowMs;
@@ -1721,11 +1759,14 @@ var resolveTimeWindow = ({
1721
1759
  `Invalid time window: computed start (${resolvedStart}) must be less than end (${resolvedEnd2})`
1722
1760
  );
1723
1761
  }
1724
- return {
1725
- start: Math.trunc(resolvedStart),
1726
- end: Math.trunc(resolvedEnd2),
1727
- source: "days"
1728
- };
1762
+ return alignToLastClosedInterval(
1763
+ {
1764
+ start: Math.trunc(resolvedStart),
1765
+ end: Math.trunc(resolvedEnd2),
1766
+ source: "days"
1767
+ },
1768
+ true
1769
+ );
1729
1770
  }
1730
1771
  const resolvedEnd = defaultEndMs ?? nowMs;
1731
1772
  if (defaultStartMs >= resolvedEnd) {
@@ -1733,11 +1774,14 @@ var resolveTimeWindow = ({
1733
1774
  `Invalid default time window: start (${defaultStartMs}) must be less than end (${resolvedEnd})`
1734
1775
  );
1735
1776
  }
1736
- return {
1737
- start: Math.trunc(defaultStartMs),
1738
- end: Math.trunc(resolvedEnd),
1739
- source: "default"
1740
- };
1777
+ return alignToLastClosedInterval(
1778
+ {
1779
+ start: Math.trunc(defaultStartMs),
1780
+ end: Math.trunc(resolvedEnd),
1781
+ source: "default"
1782
+ },
1783
+ true
1784
+ );
1741
1785
  };
1742
1786
 
1743
1787
  // src/lib/runtimeStrategyBacktest.ts
@@ -2177,7 +2221,8 @@ var prepareRunEnvironment = async ({
2177
2221
  accountId,
2178
2222
  deploymentId,
2179
2223
  assetClasses,
2180
- deployment
2224
+ deployment,
2225
+ closedIntervalMs
2181
2226
  }) => {
2182
2227
  const connectorName = await resolveRunConnectorName({
2183
2228
  value: connector,
@@ -2240,7 +2285,8 @@ var prepareRunEnvironment = async ({
2240
2285
  startTime,
2241
2286
  endTime,
2242
2287
  defaultStartMs: (0, import_time2.getTimestamp)(import_constants.BACKTEST_DEFAULT_DAYS),
2243
- defaultEndMs: (0, import_time2.getTimestamp)()
2288
+ defaultEndMs: (0, import_time2.getTimestamp)(),
2289
+ closedIntervalMs
2244
2290
  });
2245
2291
  const preloadStart = (0, import_time2.getBacktestPreloadStart)(
2246
2292
  window2.start,
@@ -2309,6 +2355,7 @@ var import_progress = __toESM(require("progress"));
2309
2355
  var import_chalk4 = __toESM(require("chalk"));
2310
2356
  var import_connectors4 = require("@tradejs/connectors");
2311
2357
  var import_connectors5 = require("@tradejs/node/connectors");
2358
+ var import_strategies = require("@tradejs/node/strategies");
2312
2359
  var import_timescale = require("@tradejs/infra/timescale");
2313
2360
 
2314
2361
  // src/lib/binanceMarketData.ts
@@ -2318,14 +2365,6 @@ var MARKET_FEATURE_INTERVAL_MS = {
2318
2365
  "15m": 9e5,
2319
2366
  "1h": 36e5
2320
2367
  };
2321
- var STABLE_QUOTE_SYMBOLS = /* @__PURE__ */ new Set([
2322
- "USDCUSDT",
2323
- "FDUSDUSDT",
2324
- "TUSDUSDT",
2325
- "BUSDUSDT",
2326
- "USDPUSDT",
2327
- "DAIUSDT"
2328
- ]);
2329
2368
  var safeDivide = (num, den) => Number.isFinite(num) && Number.isFinite(den) && den !== 0 ? num / den : null;
2330
2369
  var mean = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
2331
2370
  var standardDeviation = (values) => {
@@ -2369,9 +2408,6 @@ var classifyBtcAltRegime = ({
2369
2408
  if (btcVsAltReturn24h < -5e-3) return "alt_lead";
2370
2409
  return "neutral";
2371
2410
  };
2372
- var selectBreadthUniverseFromTickers = (tickers, limit) => tickers.filter(
2373
- (ticker) => ticker.symbol.endsWith("USDT") && ticker.symbol !== "BTCUSDT" && !STABLE_QUOTE_SYMBOLS.has(ticker.symbol) && Number.isFinite(ticker.turnover24h) && ticker.turnover24h > 0
2374
- ).sort((a, b) => b.turnover24h - a.turnover24h).slice(0, Math.max(0, limit)).map((ticker) => ticker.symbol);
2375
2411
  var aggregateAggTradesToRows = ({
2376
2412
  symbol,
2377
2413
  interval,
@@ -2853,10 +2889,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2853
2889
  process.env.BINANCE_MARKET_CONTEXT_TRADE_FLOW_SYMBOL_LIMIT,
2854
2890
  2
2855
2891
  );
2856
- const breadthLimit = asInt(
2857
- process.env.BINANCE_MARKET_CONTEXT_BREADTH_LIMIT,
2858
- 30
2859
- );
2860
2892
  const breadthChunkDays = asFloat(
2861
2893
  process.env.BINANCE_MARKET_CONTEXT_BREADTH_CHUNK_DAYS,
2862
2894
  30
@@ -2979,88 +3011,113 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2979
3011
  }
2980
3012
  }
2981
3013
  if (includeBreadth) {
2982
- const tickers = await connector.getTickers();
2983
- const breadthSymbols = selectBreadthUniverseFromTickers(
2984
- tickers,
2985
- breadthLimit
2986
- );
2987
- const universe = `binance_top${breadthSymbols.length}_usdt`;
2988
- const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
2989
- universe,
2990
- interval,
2991
- startMs: breadthStartMs,
2992
- endMs
2993
- });
2994
- const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
2995
- if (!hasCoverage({
2996
- coverage,
3014
+ const breadthUniverses = (0, import_strategies.getBinanceBreadthUniverses)();
3015
+ const breadthSymbols = breadthUniverses.at(-1)?.symbols ?? [];
3016
+ const connectorInterval = marketIntervalToConnectorInterval(interval);
3017
+ const chunks = buildBreadthBackfillChunks({
2997
3018
  startMs: breadthStartMs,
2998
3019
  endMs,
2999
- intervalMs
3000
- }) || !hasBtcAltMetrics) {
3001
- const connectorInterval = marketIntervalToConnectorInterval(interval);
3002
- const chunks = buildBreadthBackfillChunks({
3003
- startMs: breadthStartMs,
3004
- endMs,
3005
- intervalMs,
3006
- chunkDays: breadthChunkDays
3007
- });
3008
- const missingChunks = filterMissingBreadthBackfillChunks({
3009
- chunks,
3010
- coverage,
3011
- intervalMs
3012
- });
3013
- const bar = new import_progress.default(
3014
- "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
3015
- {
3016
- total: Math.max(1, missingChunks.length * breadthSymbols.length),
3017
- width: 24
3018
- }
3019
- );
3020
- let candlesRead = 0;
3021
- let skippedBreadthChunks = chunks.length - missingChunks.length;
3022
- if (!missingChunks.length) {
3023
- bar.tick(1, {
3024
- candles: 0,
3025
- skip: skippedBreadthChunks,
3026
- chunk: "cached",
3027
- symbol: universe
3020
+ intervalMs,
3021
+ chunkDays: breadthChunkDays
3022
+ });
3023
+ const plans = await Promise.all(
3024
+ breadthUniverses.map(async (definition) => {
3025
+ const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
3026
+ universe: definition.universe,
3027
+ interval,
3028
+ startMs: breadthStartMs,
3029
+ endMs
3028
3030
  });
3029
- }
3030
- for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
3031
- const chunk = missingChunks[chunkIndex];
3032
- const originalChunkIndex = chunks.findIndex(
3033
- (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
3034
- );
3035
- const candlesBySymbol = {};
3036
- for (const symbol of breadthSymbols) {
3037
- const candles = await connector.kline({
3038
- symbol,
3039
- interval: connectorInterval,
3040
- start: chunk.fetchStartMs,
3041
- end: chunk.endMs,
3042
- silent: true
3043
- });
3044
- candlesBySymbol[symbol] = candles;
3045
- candlesRead += candles.length;
3046
- bar.tick(1, {
3047
- candles: candlesRead,
3048
- skip: skippedBreadthChunks,
3049
- chunk: `${originalChunkIndex + 1}/${chunks.length}`,
3050
- symbol
3051
- });
3031
+ const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
3032
+ const missingChunks2 = !hasCoverage({
3033
+ coverage,
3034
+ startMs: breadthStartMs,
3035
+ endMs,
3036
+ intervalMs
3037
+ }) || !hasBtcAltMetrics ? !hasBtcAltMetrics ? chunks : filterMissingBreadthBackfillChunks({
3038
+ chunks,
3039
+ coverage,
3040
+ intervalMs
3041
+ }) : [];
3042
+ if (!missingChunks2.length) {
3043
+ console.log(
3044
+ import_chalk4.default.gray(`breadth cached: universe=${definition.universe}`)
3045
+ );
3052
3046
  }
3053
- const btcCandles = await connector.kline({
3054
- symbol: "BTCUSDT",
3047
+ return {
3048
+ definition,
3049
+ missingChunkKeys: new Set(
3050
+ missingChunks2.map((chunk) => `${chunk.startMs}:${chunk.endMs}`)
3051
+ )
3052
+ };
3053
+ })
3054
+ );
3055
+ const missingChunks = chunks.filter((chunk) => {
3056
+ const key = `${chunk.startMs}:${chunk.endMs}`;
3057
+ return plans.some((plan) => plan.missingChunkKeys.has(key));
3058
+ });
3059
+ const bar = new import_progress.default(
3060
+ "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
3061
+ {
3062
+ total: Math.max(1, missingChunks.length * breadthSymbols.length),
3063
+ width: 24
3064
+ }
3065
+ );
3066
+ let candlesRead = 0;
3067
+ const skippedBreadthChunks = chunks.length - missingChunks.length;
3068
+ if (!missingChunks.length) {
3069
+ bar.tick(1, {
3070
+ candles: 0,
3071
+ skip: skippedBreadthChunks,
3072
+ chunk: "cached",
3073
+ symbol: "top5/top10/top30/top50/top100"
3074
+ });
3075
+ }
3076
+ for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
3077
+ const chunk = missingChunks[chunkIndex];
3078
+ const originalChunkIndex = chunks.findIndex(
3079
+ (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
3080
+ );
3081
+ const candlesBySymbol = {};
3082
+ for (const symbol of breadthSymbols) {
3083
+ const candles = await connector.kline({
3084
+ symbol,
3055
3085
  interval: connectorInterval,
3056
3086
  start: chunk.fetchStartMs,
3057
3087
  end: chunk.endMs,
3058
3088
  silent: true
3059
3089
  });
3090
+ candlesBySymbol[symbol] = candles;
3091
+ candlesRead += candles.length;
3092
+ bar.tick(1, {
3093
+ candles: candlesRead,
3094
+ skip: skippedBreadthChunks,
3095
+ chunk: `${originalChunkIndex + 1}/${chunks.length}`,
3096
+ symbol
3097
+ });
3098
+ }
3099
+ const btcCandles = await connector.kline({
3100
+ symbol: "BTCUSDT",
3101
+ interval: connectorInterval,
3102
+ start: chunk.fetchStartMs,
3103
+ end: chunk.endMs,
3104
+ silent: true
3105
+ });
3106
+ const chunkKey = `${chunk.startMs}:${chunk.endMs}`;
3107
+ for (const plan of plans) {
3108
+ if (!plan.missingChunkKeys.has(chunkKey)) {
3109
+ continue;
3110
+ }
3111
+ const symbolSet = new Set(plan.definition.symbols);
3112
+ const universeCandles = Object.fromEntries(
3113
+ Object.entries(candlesBySymbol).filter(
3114
+ ([symbol]) => symbolSet.has(symbol)
3115
+ )
3116
+ );
3060
3117
  const rows = buildMarketBreadthRows({
3061
- universe,
3118
+ universe: plan.definition.universe,
3062
3119
  interval,
3063
- candlesBySymbol,
3120
+ candlesBySymbol: universeCandles,
3064
3121
  btcCandles
3065
3122
  }).filter((row) => {
3066
3123
  const ts = row.ts.getTime();
@@ -3069,8 +3126,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
3069
3126
  await (0, import_timescale.upsertMarketBreadthRows)(rows);
3070
3127
  breadthRows += rows.length;
3071
3128
  }
3072
- } else {
3073
- console.log(import_chalk4.default.gray(`breadth cached: universe=${universe}`));
3074
3129
  }
3075
3130
  }
3076
3131
  console.log(
@@ -4295,27 +4350,27 @@ var createCoinMarketCapFetchStats = () => ({
4295
4350
  totalCredits: 0,
4296
4351
  byPath: /* @__PURE__ */ new Map()
4297
4352
  });
4298
- var getCoinMarketCapPathLabel = (path2) => {
4299
- if (path2.includes("global-metrics")) return "global";
4300
- if (path2.includes("cryptocurrency/quotes")) return "reference";
4301
- if (path2.includes("exchange/quotes")) return "exchangeLiquidity";
4302
- if (path2.includes("fear-and-greed")) return "fearGreed";
4303
- if (path2.includes("cmc100")) return "cmc100";
4304
- if (path2.includes("cmc20")) return "cmc20";
4305
- return path2;
4306
- };
4307
- var recordCoinMarketCapFetchStats = (stats, path2, creditCount) => {
4353
+ var getCoinMarketCapPathLabel = (path3) => {
4354
+ if (path3.includes("global-metrics")) return "global";
4355
+ if (path3.includes("cryptocurrency/quotes")) return "reference";
4356
+ if (path3.includes("exchange/quotes")) return "exchangeLiquidity";
4357
+ if (path3.includes("fear-and-greed")) return "fearGreed";
4358
+ if (path3.includes("cmc100")) return "cmc100";
4359
+ if (path3.includes("cmc20")) return "cmc20";
4360
+ return path3;
4361
+ };
4362
+ var recordCoinMarketCapFetchStats = (stats, path3, creditCount) => {
4308
4363
  if (!stats) return;
4309
- const existing = stats.byPath.get(path2) ?? { requests: 0, credits: 0 };
4364
+ const existing = stats.byPath.get(path3) ?? { requests: 0, credits: 0 };
4310
4365
  existing.requests += 1;
4311
4366
  existing.credits += creditCount ?? 0;
4312
4367
  stats.requests += 1;
4313
4368
  stats.totalCredits += creditCount ?? 0;
4314
- stats.byPath.set(path2, existing);
4369
+ stats.byPath.set(path3, existing);
4315
4370
  };
4316
4371
  var formatCoinMarketCapFetchStats = (stats) => {
4317
4372
  const byPath = [...stats.byPath.entries()].map(
4318
- ([path2, item]) => `${getCoinMarketCapPathLabel(path2)}=${item.requests}req/${item.credits}cr`
4373
+ ([path3, item]) => `${getCoinMarketCapPathLabel(path3)}=${item.requests}req/${item.credits}cr`
4319
4374
  ).join(" ");
4320
4375
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
4321
4376
  };
@@ -5231,8 +5286,12 @@ var prepareMarketContextForRun = async (params) => {
5231
5286
  };
5232
5287
 
5233
5288
  // src/lib/backtest/runState.ts
5289
+ var import_types = require("@tradejs/types");
5234
5290
  var successTests = 0;
5235
5291
  var errorTests = 0;
5292
+ var warningCounts = {
5293
+ [import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
5294
+ };
5236
5295
  var errorMessages = [];
5237
5296
  var topResults = [];
5238
5297
  var bestTickerResults = /* @__PURE__ */ new Map();
@@ -5277,6 +5336,9 @@ var getRuntimeCompareContext = () => ({
5277
5336
  var resetRunState = () => {
5278
5337
  successTests = 0;
5279
5338
  errorTests = 0;
5339
+ for (const code of Object.values(import_types.BACKTEST_WARNING_CODES)) {
5340
+ warningCounts[code] = 0;
5341
+ }
5280
5342
  errorMessages.length = 0;
5281
5343
  topResults = [];
5282
5344
  bestTickerResults.clear();
@@ -5421,13 +5483,16 @@ var REPLAY_RUNTIME_COMPARISON_HEADERS = [
5421
5483
  import_chalk8.default.yellow("RT TRADES"),
5422
5484
  import_chalk8.default.yellow("RT PNL"),
5423
5485
  import_chalk8.default.green("MATCHED"),
5486
+ import_chalk8.default.red("ORDER FAILED"),
5424
5487
  import_chalk8.default.yellow("RT ONLY"),
5425
- import_chalk8.default.magenta("BT ONLY")
5488
+ import_chalk8.default.magenta("BT ONLY"),
5489
+ import_chalk8.default.red("UNMATCHED")
5426
5490
  ];
5427
5491
  var REPLAY_RESULTS_CONFIG = "replay";
5428
5492
  var REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS = 1;
5429
5493
  var REPLAY_RUNTIME_COMPARE_TOLERANCE_MS = REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS * 15 * 60 * 1e3;
5430
5494
  var formatReplayRuntimeCompareTolerance = () => `${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS} ${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS === 1 ? "bar" : "bars"}`;
5495
+ var getReplayRuntimeUnmatchedCount = (row) => row.orderFailed + row.runtimeOnly + row.backtestOnly;
5431
5496
  var buildReplayStrategyConfig = ({
5432
5497
  strategyConfig,
5433
5498
  interval
@@ -5492,12 +5557,13 @@ var compactReplaySignal = (signal) => {
5492
5557
  var import_chalk9 = __toESM(require("chalk"));
5493
5558
  var import_progress3 = __toESM(require("progress"));
5494
5559
  var import_backtest = require("@tradejs/core/backtest");
5495
- var import_strategies = require("@tradejs/core/strategies");
5560
+ var import_strategies2 = require("@tradejs/core/strategies");
5496
5561
  var import_time3 = require("@tradejs/core/time");
5497
5562
  var import_logger = require("@tradejs/infra/logger");
5563
+ var import_redis3 = require("@tradejs/infra/redis");
5498
5564
  var import_connectors6 = require("@tradejs/node/connectors");
5499
5565
  var import_cli4 = require("@tradejs/node/cli");
5500
- var import_strategies2 = require("@tradejs/node/strategies");
5566
+ var import_strategies3 = require("@tradejs/node/strategies");
5501
5567
 
5502
5568
  // src/lib/replay/portfolioReplayConnector.ts
5503
5569
  var import_constants3 = require("@tradejs/core/constants");
@@ -5947,21 +6013,231 @@ var invokeAfterSignalsHooks = async (hooks, params) => {
5947
6013
  }
5948
6014
  };
5949
6015
 
6016
+ // src/lib/runtimeLineage.ts
6017
+ var import_node_child_process = require("child_process");
6018
+ var import_node_crypto = require("crypto");
6019
+ var import_promises = __toESM(require("fs/promises"));
6020
+ var import_node_path = __toESM(require("path"));
6021
+ var RUNTIME_CONTEXT_ENV_KEYS = [
6022
+ "AI_MODE",
6023
+ "MIN_AI_QUALITY",
6024
+ "INTERVAL",
6025
+ "DERIVATIVES_CONTEXT_ENABLED",
6026
+ "DERIVATIVES_CONTEXT_TARGET_ENABLED",
6027
+ "DERIVATIVES_CONTEXT_LOOKBACK_HOURS",
6028
+ "DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS",
6029
+ "DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY",
6030
+ "COINMARKETCAP_CONTEXT_ENABLED",
6031
+ "COINMARKETCAP_CONTEXT_MAX_AGE_MS",
6032
+ "COINMARKETCAP_CONTEXT_EXCHANGE_LIQUIDITY_ENABLED",
6033
+ "COINMARKETCAP_CONTEXT_EXCHANGE_SLUGS",
6034
+ "COINMARKETCAP_CONTEXT_FEAR_GREED_ENABLED",
6035
+ "COINMARKETCAP_CONTEXT_HISTORICAL_ACCESS_MONTHS",
6036
+ "COINMARKETCAP_CONTEXT_BACKFILL_ENABLED",
6037
+ "COINMARKETCAP_CONTEXT_BACKFILL_MAX_DAYS",
6038
+ "COINMARKETCAP_CONTEXT_BACKFILL_WARMUP_DAYS"
6039
+ ];
6040
+ var RUNTIME_CONTEXT_DATA_MODEL = {
6041
+ derivativesSourceIntervals: "15m",
6042
+ derivativesDerivedIntervals: "1h",
6043
+ derivativesHourlyFallback: "stored-1h",
6044
+ derivativesDataModelVersion: 2,
6045
+ coinMarketCapAvailabilityModel: "closed-interval-all-daily",
6046
+ coinMarketCapDataModelVersion: 3,
6047
+ binanceBreadthUniverseModel: "fixed-versioned-top5-top10-top30-top50-top100",
6048
+ binanceBreadthDataModelVersion: 2
6049
+ };
6050
+ var normalizeForStableJson = (value) => {
6051
+ if (Array.isArray(value)) {
6052
+ return value.map(normalizeForStableJson);
6053
+ }
6054
+ if (value && typeof value === "object") {
6055
+ return Object.fromEntries(
6056
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
6057
+ );
6058
+ }
6059
+ return value;
6060
+ };
6061
+ var fingerprintRuntimeValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
6062
+ var gitLineageCache = /* @__PURE__ */ new Map();
6063
+ var gateFingerprintCache = /* @__PURE__ */ new Map();
6064
+ var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
6065
+ var resolveGitLineage = (projectRoot) => {
6066
+ const cached = gitLineageCache.get(projectRoot);
6067
+ if (cached) {
6068
+ return cached;
6069
+ }
6070
+ const envSha = String(process.env.TRADEJS_GIT_SHA ?? "").trim();
6071
+ if (envSha && envSha !== "unknown") {
6072
+ const result = {
6073
+ gitSha: envSha,
6074
+ gitDirty: false
6075
+ };
6076
+ gitLineageCache.set(projectRoot, result);
6077
+ return result;
6078
+ }
6079
+ try {
6080
+ const gitSha = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "HEAD"], {
6081
+ cwd: projectRoot,
6082
+ encoding: "utf8",
6083
+ stdio: ["ignore", "pipe", "ignore"]
6084
+ }).trim();
6085
+ const status = (0, import_node_child_process.execFileSync)(
6086
+ "git",
6087
+ ["status", "--porcelain", "--untracked-files=normal"],
6088
+ {
6089
+ cwd: projectRoot,
6090
+ encoding: "utf8",
6091
+ stdio: ["ignore", "pipe", "ignore"]
6092
+ }
6093
+ ).trim();
6094
+ const result = {
6095
+ gitSha: gitSha || null,
6096
+ gitDirty: status.length > 0
6097
+ };
6098
+ gitLineageCache.set(projectRoot, result);
6099
+ return result;
6100
+ } catch {
6101
+ const result = {
6102
+ gitSha: null,
6103
+ gitDirty: null
6104
+ };
6105
+ gitLineageCache.set(projectRoot, result);
6106
+ return result;
6107
+ }
6108
+ };
6109
+ var readOptionalFile = async (filePath) => {
6110
+ try {
6111
+ return await import_promises.default.readFile(filePath);
6112
+ } catch {
6113
+ return null;
6114
+ }
6115
+ };
6116
+ var resolveBinanceBreadthFingerprint = async (projectRoot) => {
6117
+ if (binanceBreadthFingerprintCache.has(projectRoot)) {
6118
+ return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
6119
+ }
6120
+ const content = await readOptionalFile(
6121
+ import_node_path.default.join(
6122
+ projectRoot,
6123
+ "packages/node/src/config/binanceBreadthUniverses.json"
6124
+ )
6125
+ );
6126
+ let result = null;
6127
+ if (content) {
6128
+ try {
6129
+ const parsed = JSON.parse(content.toString("utf8"));
6130
+ result = typeof parsed.fingerprint === "string" && parsed.fingerprint.trim() ? parsed.fingerprint.trim() : null;
6131
+ } catch {
6132
+ result = null;
6133
+ }
6134
+ }
6135
+ binanceBreadthFingerprintCache.set(projectRoot, result);
6136
+ return result;
6137
+ };
6138
+ var resolveGateFingerprint = async ({
6139
+ projectRoot,
6140
+ strategyName,
6141
+ gitSha
6142
+ }) => {
6143
+ const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
6144
+ const cached = gateFingerprintCache.get(cacheKey);
6145
+ if (cached) {
6146
+ return cached;
6147
+ }
6148
+ const relativeCandidates = [
6149
+ `packages/strategies/src/${strategyName}/adapters/ai.ts`,
6150
+ `packages/strategies/src/${strategyName}/guardrails.ts`,
6151
+ `packages/strategies/src/${strategyName}/pockets.ts`,
6152
+ `packages/strategies/src/${strategyName}/config.ts`,
6153
+ "packages/node/src/ai.ts"
6154
+ ];
6155
+ const optionalEntries = await Promise.all(
6156
+ relativeCandidates.map(async (relativePath) => {
6157
+ const content = await readOptionalFile(
6158
+ import_node_path.default.join(projectRoot, relativePath)
6159
+ );
6160
+ return content == null ? null : { relativePath, content };
6161
+ })
6162
+ );
6163
+ const sourceEntries = [];
6164
+ for (const entry of optionalEntries) {
6165
+ if (entry != null) {
6166
+ sourceEntries.push(entry);
6167
+ }
6168
+ }
6169
+ const hash = (0, import_node_crypto.createHash)("sha256");
6170
+ hash.update(strategyName);
6171
+ if (sourceEntries.length) {
6172
+ for (const entry of sourceEntries) {
6173
+ hash.update(entry.relativePath);
6174
+ hash.update(entry.content);
6175
+ }
6176
+ } else {
6177
+ hash.update(gitSha ?? "unknown-git-sha");
6178
+ }
6179
+ const fingerprint = hash.digest("hex").slice(0, 16);
6180
+ gateFingerprintCache.set(cacheKey, fingerprint);
6181
+ return fingerprint;
6182
+ };
6183
+ var buildRuntimeLineage = async ({
6184
+ projectRoot,
6185
+ strategyName,
6186
+ config,
6187
+ runContext,
6188
+ env = process.env
6189
+ }) => {
6190
+ const git = resolveGitLineage(projectRoot);
6191
+ const context = {
6192
+ ...Object.fromEntries(
6193
+ RUNTIME_CONTEXT_ENV_KEYS.map((key) => [key, env[key] ?? null])
6194
+ ),
6195
+ ...RUNTIME_CONTEXT_DATA_MODEL,
6196
+ binanceBreadthUniverseFingerprint: await resolveBinanceBreadthFingerprint(projectRoot),
6197
+ ...runContext
6198
+ };
6199
+ return {
6200
+ schemaVersion: 1,
6201
+ ...git,
6202
+ gateFingerprint: await resolveGateFingerprint({
6203
+ projectRoot,
6204
+ strategyName,
6205
+ gitSha: git.gitSha
6206
+ }),
6207
+ configFingerprint: fingerprintRuntimeValue(config),
6208
+ contextFingerprint: fingerprintRuntimeValue(context)
6209
+ };
6210
+ };
6211
+ var runtimeLineageKey = (lineage) => [
6212
+ lineage.schemaVersion,
6213
+ lineage.gitSha ?? "unknown",
6214
+ lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
6215
+ lineage.gateFingerprint,
6216
+ lineage.configFingerprint,
6217
+ lineage.contextFingerprint
6218
+ ].join(":");
6219
+ var runtimeLineagesMatch = (left, right) => left != null && right != null && runtimeLineageKey(left) === runtimeLineageKey(right);
6220
+
5950
6221
  // src/lib/replay/historicalSignalsReplay.ts
5951
6222
  var loadRuntimeStrategies = async (runtimeStrategies) => {
5952
6223
  const strategies = await Promise.all(
5953
6224
  runtimeStrategies.map(async ({ strategyName, strategyConfig }) => {
5954
- const strategyCreator = await (0, import_strategies2.getStrategyCreator)(
6225
+ const strategyCreator = await (0, import_strategies3.getStrategyCreator)(
5955
6226
  strategyName,
5956
6227
  replayProjectRoot
5957
6228
  );
5958
6229
  if (!strategyCreator) {
5959
6230
  throw new Error(`Unknown strategy: ${strategyName}`);
5960
6231
  }
6232
+ const strategyResults = await (0, import_redis3.getData)(
6233
+ import_redis3.redisKeys.strategyResults(replayUserName, strategyName),
6234
+ {}
6235
+ );
5961
6236
  return {
5962
6237
  strategyName,
5963
6238
  strategyCreator,
5964
- strategyConfig
6239
+ strategyConfig,
6240
+ strategyResults
5965
6241
  };
5966
6242
  })
5967
6243
  );
@@ -6062,6 +6338,7 @@ var runHistoricalSignalsReplay = async ({
6062
6338
  }) => {
6063
6339
  const startedAt = Date.now();
6064
6340
  const signals = [];
6341
+ const runtimeLineages = [];
6065
6342
  const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
6066
6343
  const projectHooks = projectConfig.hooks;
6067
6344
  const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
@@ -6149,26 +6426,54 @@ var runHistoricalSignalsReplay = async ({
6149
6426
  sharedReplayKeyPrefixes.push(sharedIndicatorsReplayKey);
6150
6427
  const strategiesForSymbol = await Promise.all(
6151
6428
  loadedStrategies.map(
6152
- async ({ strategyName, strategyCreator, strategyConfig }) => ({
6429
+ async ({
6153
6430
  strategyName,
6431
+ strategyCreator,
6154
6432
  strategyConfig,
6155
- run: await strategyCreator({
6156
- userName: replayUserName,
6157
- connectorName,
6158
- config: buildReplayStrategyConfig({
6433
+ strategyResults
6434
+ }) => {
6435
+ const runtimeLineage = await buildRuntimeLineage({
6436
+ projectRoot: replayProjectRoot,
6437
+ strategyName,
6438
+ config: {
6439
+ configId: "config",
6159
6440
  strategyConfig,
6160
- interval
6161
- }),
6441
+ symbolResultConfig: strategyResults?.[symbol]?.config ?? null
6442
+ },
6443
+ runContext: {
6444
+ connectorName: connectorName.toLowerCase(),
6445
+ interval: String(interval),
6446
+ universe: preparedRun.universe ?? null
6447
+ }
6448
+ });
6449
+ runtimeLineages.push({
6450
+ strategy: strategyName,
6162
6451
  symbol,
6163
- data: preparedData.prevData,
6164
- btcData: preparedData.btcPrevData,
6165
- ethData: preparedData.ethPrevData,
6166
- btcBinanceData: preparedData.btcBinancePrevData,
6167
- btcCoinbaseData: preparedData.btcCoinbasePrevData,
6168
- connector: replayConnector,
6169
- sharedIndicatorsReplayKey
6170
- })
6171
- })
6452
+ lineage: runtimeLineage
6453
+ });
6454
+ return {
6455
+ strategyName,
6456
+ strategyConfig,
6457
+ runtimeLineage,
6458
+ run: await strategyCreator({
6459
+ userName: replayUserName,
6460
+ connectorName,
6461
+ runtimeConfigId: "config",
6462
+ config: buildReplayStrategyConfig({
6463
+ strategyConfig,
6464
+ interval
6465
+ }),
6466
+ symbol,
6467
+ data: preparedData.prevData,
6468
+ btcData: preparedData.btcPrevData,
6469
+ ethData: preparedData.ethPrevData,
6470
+ btcBinanceData: preparedData.btcBinancePrevData,
6471
+ btcCoinbaseData: preparedData.btcCoinbasePrevData,
6472
+ connector: replayConnector,
6473
+ sharedIndicatorsReplayKey
6474
+ })
6475
+ };
6476
+ }
6172
6477
  )
6173
6478
  );
6174
6479
  const symbolRuntime = {
@@ -6262,7 +6567,8 @@ var runHistoricalSignalsReplay = async ({
6262
6567
  ethCandle?.timestamp === timestamp ? ethCandle : void 0
6263
6568
  );
6264
6569
  if (result && typeof result !== "string") {
6265
- await (0, import_strategies2.enrichSignalWithBinanceMarketContext)({
6570
+ result.runtimeLineage = strategyRuntime.runtimeLineage;
6571
+ await (0, import_strategies3.enrichSignalWithBinanceMarketContext)({
6266
6572
  signal: result,
6267
6573
  env: "PARITY"
6268
6574
  });
@@ -6286,8 +6592,8 @@ var runHistoricalSignalsReplay = async ({
6286
6592
  }
6287
6593
  } finally {
6288
6594
  for (const keyPrefix of sharedReplayKeyPrefixes) {
6289
- (0, import_strategies.releaseStrategyIndicatorsReplayCache)(keyPrefix);
6290
- (0, import_strategies.releaseStrategyReplayCache)(keyPrefix);
6595
+ (0, import_strategies2.releaseStrategyIndicatorsReplayCache)(keyPrefix);
6596
+ (0, import_strategies2.releaseStrategyReplayCache)(keyPrefix);
6291
6597
  }
6292
6598
  }
6293
6599
  const artifacts = replayConnector.getReplayArtifacts();
@@ -6321,7 +6627,8 @@ var runHistoricalSignalsReplay = async ({
6321
6627
  orderLog: artifacts.orderLog,
6322
6628
  positionLog: artifacts.positionLog,
6323
6629
  cycleCount: orderedTimestamps.length,
6324
- abortedCycles
6630
+ abortedCycles,
6631
+ runtimeLineages
6325
6632
  };
6326
6633
  };
6327
6634
 
@@ -6805,7 +7112,7 @@ var summarizeTradeParityByStrategy = ({
6805
7112
  };
6806
7113
 
6807
7114
  // src/lib/runtimeSignalsLoader.ts
6808
- var import_redis3 = require("@tradejs/infra/redis");
7115
+ var import_redis4 = require("@tradejs/infra/redis");
6809
7116
 
6810
7117
  // src/lib/runtimeSignalsStorage.ts
6811
7118
  var import_constants4 = require("@tradejs/core/constants");
@@ -6855,14 +7162,14 @@ var loadRuntimeSignals = async (userName, {
6855
7162
  startTime,
6856
7163
  endTime
6857
7164
  } = {}) => {
6858
- const bucketPrefix = import_redis3.redisKeys.runtimeSignalBuckets(userName);
7165
+ const bucketPrefix = import_redis4.redisKeys.runtimeSignalBuckets(userName);
6859
7166
  const bucketKeys = filterBucketKeysByWindow(
6860
- await (0, import_redis3.getKeys)(bucketPrefix),
7167
+ await (0, import_redis4.getKeys)(bucketPrefix),
6861
7168
  bucketPrefix,
6862
7169
  startTime,
6863
7170
  endTime
6864
7171
  );
6865
- const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis3.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
7172
+ const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
6866
7173
  const seen = /* @__PURE__ */ new Set();
6867
7174
  const payloads = await Promise.all(
6868
7175
  refs.map(async (ref) => {
@@ -6871,8 +7178,8 @@ var loadRuntimeSignals = async (userName, {
6871
7178
  return null;
6872
7179
  }
6873
7180
  seen.add(dedupeKey);
6874
- const signal = await (0, import_redis3.getData)(
6875
- import_redis3.redisKeys.storeSignal(ref.symbol, ref.signalId),
7181
+ const signal = await (0, import_redis4.getData)(
7182
+ import_redis4.redisKeys.storeSignal(ref.symbol, ref.signalId),
6876
7183
  null
6877
7184
  );
6878
7185
  return isSignalRecord(signal) ? signal : null;
@@ -6884,25 +7191,42 @@ var loadRuntimeSignalEvaluations = async (userName, {
6884
7191
  startTime,
6885
7192
  endTime
6886
7193
  } = {}) => {
6887
- const bucketPrefix = import_redis3.redisKeys.runtimeSignalEvaluationBuckets(userName);
7194
+ const bucketPrefix = import_redis4.redisKeys.runtimeSignalEvaluationBuckets(userName);
6888
7195
  const bucketKeys = filterBucketKeysByWindow(
6889
- await (0, import_redis3.getKeys)(bucketPrefix),
7196
+ await (0, import_redis4.getKeys)(bucketPrefix),
6890
7197
  bucketPrefix,
6891
7198
  startTime,
6892
7199
  endTime
6893
7200
  );
6894
- const evaluations = (await Promise.all(bucketKeys.map((key) => (0, import_redis3.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalEvaluationRecord);
7201
+ const evaluations = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalEvaluationRecord);
6895
7202
  const deduped = /* @__PURE__ */ new Map();
6896
7203
  for (const evaluation of evaluations) {
6897
7204
  deduped.set(evaluation.evaluationId, evaluation);
6898
7205
  }
6899
7206
  return sortByTimestamp([...deduped.values()]);
6900
7207
  };
7208
+ var loadRuntimeLineageScopes = async (userName, {
7209
+ startTime,
7210
+ endTime
7211
+ }) => {
7212
+ const records = (await Promise.all(
7213
+ (0, import_time4.getRuntimeStorageDayKeys)(startTime, endTime).map(
7214
+ (dayKey) => (0, import_redis4.getHashJsonValues)(
7215
+ import_redis4.redisKeys.runtimeLineageScopeBucket(userName, dayKey)
7216
+ )
7217
+ )
7218
+ )).flat().filter(
7219
+ (record) => record != null && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.firstTimestamp === "number" && typeof record.lastTimestamp === "number" && record.lineage != null
7220
+ );
7221
+ return records.sort(
7222
+ (left, right) => left.firstTimestamp - right.firstTimestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol)
7223
+ );
7224
+ };
6901
7225
 
6902
7226
  // src/lib/runtimeTradeSync.ts
6903
7227
  var import_constants5 = require("@tradejs/core/constants");
6904
7228
  var import_time5 = require("@tradejs/core/time");
6905
- var import_redis4 = require("@tradejs/infra/redis");
7229
+ var import_redis5 = require("@tradejs/infra/redis");
6906
7230
  var CLOSED_PNL_LIMIT = 100;
6907
7231
  var CLOSED_PNL_RECONCILIATION_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
6908
7232
  var EXCHANGE_HISTORY_MAX_RANGE_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -6996,6 +7320,18 @@ var consumeClosedPnlMatch = (buckets, trade) => {
6996
7320
  const [row] = rows.splice(matchIndex, 1);
6997
7321
  return row ?? null;
6998
7322
  };
7323
+ var isRuntimeTradeInConnectorScope = (trade, connector) => {
7324
+ if (trade.deploymentId && trade.deploymentId !== connector.deploymentId) {
7325
+ return false;
7326
+ }
7327
+ if (trade.accountId && trade.accountId !== connector.accountId) {
7328
+ return false;
7329
+ }
7330
+ if (trade.universe && connector.universe && trade.universe !== connector.universe) {
7331
+ return false;
7332
+ }
7333
+ return true;
7334
+ };
6999
7335
  var syncRuntimeTrades = async ({
7000
7336
  userName,
7001
7337
  connector,
@@ -7019,16 +7355,23 @@ var syncRuntimeTrades = async ({
7019
7355
  const openPositionsBySymbol = new Map(
7020
7356
  openPositions.map((position) => [position.symbol, position])
7021
7357
  );
7022
- const activeOrderIdBySymbol = /* @__PURE__ */ new Map();
7023
- const symbols = [...new Set(trades.map((trade) => trade.symbol))];
7358
+ const activeOrderIdByKey = /* @__PURE__ */ new Map();
7359
+ const activeTradeKeys = [
7360
+ ...new Set(
7361
+ trades.filter((trade) => isRuntimeTradeInConnectorScope(trade, connector)).map(
7362
+ (trade) => import_redis5.redisKeys.runtimeActiveTrade(
7363
+ userName,
7364
+ trade.symbol,
7365
+ trade.deploymentId ?? trade.accountId
7366
+ )
7367
+ )
7368
+ )
7369
+ ];
7024
7370
  await Promise.all(
7025
- symbols.map(async (symbol) => {
7026
- const activeRef = await (0, import_redis4.getData)(
7027
- import_redis4.redisKeys.runtimeActiveTrade(userName, symbol),
7028
- null
7029
- );
7030
- activeOrderIdBySymbol.set(
7031
- symbol,
7371
+ activeTradeKeys.map(async (key) => {
7372
+ const activeRef = await (0, import_redis5.getData)(key, null);
7373
+ activeOrderIdByKey.set(
7374
+ key,
7032
7375
  typeof activeRef?.orderId === "string" ? activeRef.orderId : null
7033
7376
  );
7034
7377
  })
@@ -7047,12 +7390,21 @@ var syncRuntimeTrades = async ({
7047
7390
  }
7048
7391
  const syncedTrades = [];
7049
7392
  for (const trade of trades) {
7393
+ if (!isRuntimeTradeInConnectorScope(trade, connector)) {
7394
+ syncedTrades.push(trade);
7395
+ continue;
7396
+ }
7050
7397
  if (trade.status !== "active" && !isRuntimeTradeSyncFallbackClose(trade)) {
7051
7398
  syncedTrades.push(trade);
7052
7399
  continue;
7053
7400
  }
7401
+ const activeTradeKey = import_redis5.redisKeys.runtimeActiveTrade(
7402
+ userName,
7403
+ trade.symbol,
7404
+ trade.deploymentId ?? trade.accountId
7405
+ );
7054
7406
  const openPosition = openPositionsBySymbol.get(trade.symbol);
7055
- const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
7407
+ const activeOrderId = activeOrderIdByKey.get(activeTradeKey);
7056
7408
  const isCurrentActiveTrade = activeOrderId === trade.orderId;
7057
7409
  if (trade.status === "active" && !openPositionsReliable) {
7058
7410
  syncedTrades.push({
@@ -7070,15 +7422,15 @@ var syncRuntimeTrades = async ({
7070
7422
  currentPnl: openPosition.unrealizedPnl,
7071
7423
  lastSyncedAt: endTime
7072
7424
  };
7073
- await (0, import_redis4.setData)(
7074
- import_redis4.redisKeys.runtimeTrade(userName, trade.orderId),
7425
+ await (0, import_redis5.setData)(
7426
+ import_redis5.redisKeys.runtimeTrade(userName, trade.orderId),
7075
7427
  nextTrade2,
7076
7428
  {
7077
7429
  expire: 0
7078
7430
  }
7079
7431
  );
7080
- await (0, import_redis4.setHashJsonField)(
7081
- import_redis4.redisKeys.runtimeTradeBucket(
7432
+ await (0, import_redis5.setHashJsonField)(
7433
+ import_redis5.redisKeys.runtimeTradeBucket(
7082
7434
  userName,
7083
7435
  (0, import_time5.getRuntimeStorageDayKey)(trade.entryTimestamp)
7084
7436
  ),
@@ -7091,6 +7443,7 @@ var syncRuntimeTrades = async ({
7091
7443
  }
7092
7444
  const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
7093
7445
  if (!matchedClosedPnl) {
7446
+ syncedTrades.push(trade);
7094
7447
  continue;
7095
7448
  }
7096
7449
  const nextTrade = {
@@ -7111,11 +7464,11 @@ var syncRuntimeTrades = async ({
7111
7464
  lastSyncedAt: endTime
7112
7465
  };
7113
7466
  await Promise.all([
7114
- (0, import_redis4.setData)(import_redis4.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
7467
+ (0, import_redis5.setData)(import_redis5.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
7115
7468
  expire: import_constants5.TTL_1M
7116
7469
  }),
7117
- (0, import_redis4.setHashJsonField)(
7118
- import_redis4.redisKeys.runtimeTradeBucket(
7470
+ (0, import_redis5.setHashJsonField)(
7471
+ import_redis5.redisKeys.runtimeTradeBucket(
7119
7472
  userName,
7120
7473
  (0, import_time5.getRuntimeStorageDayKey)(trade.entryTimestamp)
7121
7474
  ),
@@ -7123,7 +7476,7 @@ var syncRuntimeTrades = async ({
7123
7476
  nextTrade,
7124
7477
  { expire: import_constants5.TTL_1M }
7125
7478
  ),
7126
- ...isCurrentActiveTrade ? [(0, import_redis4.delKey)(import_redis4.redisKeys.runtimeActiveTrade(userName, trade.symbol))] : []
7479
+ ...isCurrentActiveTrade ? [(0, import_redis5.delKey)(activeTradeKey)] : []
7127
7480
  ]);
7128
7481
  syncedTrades.push(nextTrade);
7129
7482
  }
@@ -7733,6 +8086,7 @@ var buildReplayRuntimeComparisonDetails = ({
7733
8086
  ),
7734
8087
  limit
7735
8088
  ),
8089
+ orderFailed: [],
7736
8090
  runtimeOnly: cappedRuntimeOnlyDetails,
7737
8091
  backtestOnly: cappedBacktestOnlyDetails,
7738
8092
  nearestCandidates: cappedNearestCandidates,
@@ -7752,6 +8106,7 @@ var buildReplayRuntimeComparisonDetails = ({
7752
8106
  };
7753
8107
  var buildReplayExchangeComparisonDetails = ({
7754
8108
  matched,
8109
+ orderFailed = [],
7755
8110
  exchangeOnly,
7756
8111
  backtestOnly,
7757
8112
  exchangeEntries,
@@ -7802,7 +8157,7 @@ var buildReplayExchangeComparisonDetails = ({
7802
8157
  limit
7803
8158
  );
7804
8159
  return {
7805
- capped: matched.length > limit || exchangeOnlyDetails.length > limit || backtestOnlyDetails.length > limit || nearestCandidates.length > limit,
8160
+ capped: matched.length > limit || orderFailed.length > limit || exchangeOnlyDetails.length > limit || backtestOnlyDetails.length > limit || nearestCandidates.length > limit,
7806
8161
  limit,
7807
8162
  matched: capReplayDetails(
7808
8163
  matched.map(
@@ -7818,6 +8173,27 @@ var buildReplayExchangeComparisonDetails = ({
7818
8173
  ),
7819
8174
  limit
7820
8175
  ),
8176
+ orderFailed: capReplayDetails(
8177
+ orderFailed.map((item) => {
8178
+ const detail = buildMatchedReplayDetail({
8179
+ runtime: toExchangeDetail(item.exchange),
8180
+ backtest: toBacktestParityDetail(
8181
+ item.backtest,
8182
+ backtestTimestampOffsetMs
8183
+ ),
8184
+ timestampDiffMs: item.timestampDiffMs,
8185
+ priceDeltaPct: item.priceDeltaPct
8186
+ });
8187
+ return {
8188
+ runtime: detail.runtime,
8189
+ backtest: detail.backtest,
8190
+ timestampDiffMs: detail.timestampDiffMs,
8191
+ priceDeltaPct: detail.priceDeltaPct,
8192
+ reason: item.reason
8193
+ };
8194
+ }),
8195
+ limit
8196
+ ),
7821
8197
  runtimeOnly: cappedExchangeOnlyDetails,
7822
8198
  backtestOnly: cappedBacktestOnlyDetails,
7823
8199
  nearestCandidates: cappedNearestCandidates,
@@ -7853,11 +8229,147 @@ var buildReplaySignalEvaluations = (signals) => signals.map((signal) => ({
7853
8229
  reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
7854
8230
  signalId: signal.signalId,
7855
8231
  direction: signal.direction,
8232
+ runtimeLineage: signal.runtimeLineage,
7856
8233
  orderStatus: signal.orderStatus,
7857
8234
  orderSkipReason: signal.orderSkipReason,
7858
8235
  ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
7859
8236
  ...signal.ml ? { ml: signal.ml } : {}
7860
8237
  }));
8238
+ var lineageScopeKey = (strategy, symbol) => `${strategy}::${symbol}`;
8239
+ var buildExpectedLineageByScope = (replayLineages) => new Map(
8240
+ replayLineages.map((record) => [
8241
+ lineageScopeKey(record.strategy, record.symbol),
8242
+ record.lineage
8243
+ ])
8244
+ );
8245
+ var hasExpectedLineage = ({
8246
+ strategy,
8247
+ symbol,
8248
+ lineage,
8249
+ expectedByScope
8250
+ }) => runtimeLineagesMatch(
8251
+ lineage,
8252
+ expectedByScope.get(lineageScopeKey(strategy, symbol))
8253
+ );
8254
+ var filterReplayComparisonByLineage = ({
8255
+ replayLineages,
8256
+ runtimeTrades,
8257
+ runtimeSignals,
8258
+ runtimeSignalEvaluations,
8259
+ runtimeLineageScopes,
8260
+ backtestEntries
8261
+ }) => {
8262
+ const expectedByScope = buildExpectedLineageByScope(replayLineages);
8263
+ const comparableRuntimeSignals = runtimeSignals.filter(
8264
+ (signal) => hasExpectedLineage({
8265
+ strategy: signal.strategy,
8266
+ symbol: signal.symbol,
8267
+ lineage: signal.runtimeLineage,
8268
+ expectedByScope
8269
+ })
8270
+ );
8271
+ const comparableRuntimeEvaluations = runtimeSignalEvaluations.filter(
8272
+ (evaluation) => hasExpectedLineage({
8273
+ strategy: evaluation.strategy,
8274
+ symbol: evaluation.symbol,
8275
+ lineage: evaluation.runtimeLineage,
8276
+ expectedByScope
8277
+ })
8278
+ );
8279
+ const comparableRuntimeLineageScopes = runtimeLineageScopes.filter(
8280
+ (scope) => hasExpectedLineage({
8281
+ strategy: scope.strategy,
8282
+ symbol: scope.symbol,
8283
+ lineage: scope.lineage,
8284
+ expectedByScope
8285
+ })
8286
+ );
8287
+ const deploymentWindows = /* @__PURE__ */ new Map();
8288
+ for (const scope of comparableRuntimeLineageScopes) {
8289
+ const key = lineageScopeKey(scope.strategy, scope.symbol);
8290
+ const existing = deploymentWindows.get(key);
8291
+ deploymentWindows.set(key, {
8292
+ firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
8293
+ lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
8294
+ });
8295
+ }
8296
+ const lineageBySignalId = /* @__PURE__ */ new Map();
8297
+ for (const signal of runtimeSignals) {
8298
+ if (signal.runtimeLineage) {
8299
+ lineageBySignalId.set(signal.signalId, signal.runtimeLineage);
8300
+ }
8301
+ }
8302
+ for (const evaluation of runtimeSignalEvaluations) {
8303
+ if (evaluation.signalId && evaluation.runtimeLineage) {
8304
+ lineageBySignalId.set(evaluation.signalId, evaluation.runtimeLineage);
8305
+ }
8306
+ }
8307
+ const comparableRuntimeTrades = runtimeTrades.filter((trade) => {
8308
+ const lineage = trade.signalId ? lineageBySignalId.get(trade.signalId) : void 0;
8309
+ if (lineage) {
8310
+ return hasExpectedLineage({
8311
+ strategy: trade.strategy,
8312
+ symbol: trade.symbol,
8313
+ lineage,
8314
+ expectedByScope
8315
+ });
8316
+ }
8317
+ const window2 = deploymentWindows.get(
8318
+ lineageScopeKey(trade.strategy, trade.symbol)
8319
+ );
8320
+ const signalTimestamp = trade.signalTimestamp ?? trade.entryTimestamp;
8321
+ return window2 != null && signalTimestamp >= window2.firstTimestamp && signalTimestamp <= window2.lastTimestamp;
8322
+ });
8323
+ for (const artifact of [
8324
+ ...comparableRuntimeSignals,
8325
+ ...comparableRuntimeEvaluations
8326
+ ]) {
8327
+ const key = lineageScopeKey(artifact.strategy, artifact.symbol);
8328
+ const existing = deploymentWindows.get(key);
8329
+ deploymentWindows.set(key, {
8330
+ firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
8331
+ lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
8332
+ });
8333
+ }
8334
+ const comparableBacktestEntries = backtestEntries.filter((entry) => {
8335
+ const window2 = deploymentWindows.get(
8336
+ lineageScopeKey(entry.strategy, entry.symbol)
8337
+ );
8338
+ const signalTimestamp = entry.signalTimestamp ?? entry.timestamp;
8339
+ return window2 != null && signalTimestamp >= window2.firstTimestamp && signalTimestamp <= window2.lastTimestamp;
8340
+ });
8341
+ const comparableScopeKeys = new Set(deploymentWindows.keys());
8342
+ return {
8343
+ runtimeTrades: comparableRuntimeTrades,
8344
+ runtimeSignals: comparableRuntimeSignals,
8345
+ runtimeSignalEvaluations: comparableRuntimeEvaluations,
8346
+ backtestEntries: comparableBacktestEntries,
8347
+ lineage: {
8348
+ enforced: true,
8349
+ replayScopes: expectedByScope.size,
8350
+ comparableScopes: comparableScopeKeys.size,
8351
+ excludedRuntimeTrades: runtimeTrades.length - comparableRuntimeTrades.length,
8352
+ excludedRuntimeSignals: runtimeSignals.length - comparableRuntimeSignals.length,
8353
+ excludedRuntimeEvaluations: runtimeSignalEvaluations.length - comparableRuntimeEvaluations.length,
8354
+ excludedRuntimeLineageScopes: runtimeLineageScopes.length - comparableRuntimeLineageScopes.length,
8355
+ excludedExchangeEntries: 0,
8356
+ excludedBacktestEntries: backtestEntries.length - comparableBacktestEntries.length,
8357
+ reason: comparableScopeKeys.size > 0 ? null : "no_runtime_artifacts_with_matching_lineage",
8358
+ replay: [...expectedByScope.entries()].map(([scope, lineage]) => {
8359
+ const separator = scope.indexOf("::");
8360
+ return {
8361
+ strategy: scope.slice(0, separator),
8362
+ symbol: scope.slice(separator + 2),
8363
+ lineage
8364
+ };
8365
+ }).sort(
8366
+ (left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || runtimeLineageKey(left.lineage).localeCompare(
8367
+ runtimeLineageKey(right.lineage)
8368
+ )
8369
+ )
8370
+ }
8371
+ };
8372
+ };
7861
8373
  var formatDrilldownSummary = (summary) => Object.entries(summary).filter(([, count]) => count > 0).map(([classification, count]) => `${classification}=${count}`).join(", ");
7862
8374
  var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
7863
8375
  var sumOptionalNumbers = (values) => {
@@ -7881,6 +8393,14 @@ var firstFiniteNumber = (values) => {
7881
8393
  return null;
7882
8394
  };
7883
8395
  var firstString = (values) => values.find((value) => typeof value === "string" && value.trim());
8396
+ var summarizeBacktestPnlByStrategy = (entries) => {
8397
+ const totals = /* @__PURE__ */ new Map();
8398
+ for (const entry of entries) {
8399
+ const pnl = typeof entry.expectedPnl === "number" && Number.isFinite(entry.expectedPnl) ? entry.expectedPnl : 0;
8400
+ totals.set(entry.strategy, (totals.get(entry.strategy) ?? 0) + pnl);
8401
+ }
8402
+ return totals;
8403
+ };
7884
8404
  var getExchangeExecutionMergeKey = (entry) => {
7885
8405
  const orderId = firstString([entry.orderId, entry.orderLinkId]);
7886
8406
  return orderId ? `${entry.symbol}::${entry.direction}::${orderId}` : null;
@@ -8147,16 +8667,89 @@ var compareExchangeEntriesToBacktest = ({
8147
8667
  backtestOnly
8148
8668
  };
8149
8669
  };
8670
+ var findExchangeRuntimeOutcome = ({
8671
+ exchangeEntry,
8672
+ strategyName,
8673
+ runtimeSignals,
8674
+ runtimeSignalEvaluations,
8675
+ toleranceMs,
8676
+ signalTimestampOffsetMs
8677
+ }) => {
8678
+ const candidates = [];
8679
+ for (const signal of runtimeSignals) {
8680
+ if (signal.strategy === strategyName && signal.symbol === exchangeEntry.symbol && signal.direction === exchangeEntry.direction) {
8681
+ candidates.push({
8682
+ timestamp: signal.timestamp,
8683
+ orderStatus: signal.orderStatus,
8684
+ reason: signal.orderFailureReason || signal.orderSkipReason || signal.orderStatus
8685
+ });
8686
+ }
8687
+ }
8688
+ for (const evaluation of runtimeSignalEvaluations) {
8689
+ if (evaluation.strategy === strategyName && evaluation.symbol === exchangeEntry.symbol && (evaluation.direction == null || evaluation.direction === exchangeEntry.direction)) {
8690
+ candidates.push({
8691
+ timestamp: evaluation.timestamp,
8692
+ orderStatus: evaluation.orderStatus,
8693
+ reason: evaluation.orderSkipReason || evaluation.reason || evaluation.orderStatus
8694
+ });
8695
+ }
8696
+ }
8697
+ return candidates.map((candidate) => ({
8698
+ ...candidate,
8699
+ timestampDiffMs: Math.abs(
8700
+ candidate.timestamp + signalTimestampOffsetMs - exchangeEntry.entryTimestamp
8701
+ )
8702
+ })).filter((candidate) => candidate.timestampDiffMs <= toleranceMs).sort(
8703
+ (left, right) => left.timestampDiffMs - right.timestampDiffMs || Number(right.orderStatus === "failed") - Number(left.orderStatus === "failed")
8704
+ )[0] ?? null;
8705
+ };
8706
+ var splitExchangeMatchesByRuntimeOrderStatus = ({
8707
+ matched,
8708
+ strategyNameByOrderLinkKey,
8709
+ runtimeSignals,
8710
+ runtimeSignalEvaluations,
8711
+ toleranceMs,
8712
+ signalTimestampOffsetMs
8713
+ }) => {
8714
+ const orderFailed = [];
8715
+ const completed = matched.filter((item) => {
8716
+ const strategyName = resolveReplayStrategyNameFromExchangeEntry({
8717
+ exchangeEntry: item.exchange,
8718
+ strategyNameByOrderLinkKey
8719
+ });
8720
+ if (!strategyName) {
8721
+ return true;
8722
+ }
8723
+ const outcome = findExchangeRuntimeOutcome({
8724
+ exchangeEntry: item.exchange,
8725
+ strategyName,
8726
+ runtimeSignals,
8727
+ runtimeSignalEvaluations,
8728
+ toleranceMs,
8729
+ signalTimestampOffsetMs
8730
+ });
8731
+ if (outcome?.orderStatus !== "failed") {
8732
+ return true;
8733
+ }
8734
+ orderFailed.push({
8735
+ ...item,
8736
+ reason: outcome.reason || "orderStatus=failed"
8737
+ });
8738
+ return false;
8739
+ });
8740
+ return { completed, orderFailed };
8741
+ };
8150
8742
  var saveAndPrintReplayExchangeComparison = async ({
8151
8743
  liveStrategySummaries,
8152
8744
  backtestEntries,
8153
8745
  replaySignals,
8154
8746
  replaySignalEvaluations,
8155
8747
  runtimeSignals,
8156
- runtimeSignalEvaluations
8748
+ runtimeSignalEvaluations,
8749
+ lineage
8157
8750
  }) => {
8158
8751
  const { connector, connectorName, window: window2 } = getRuntimeCompareContext();
8159
- const exchangeEntries = await loadExchangeEntriesForComparison({
8752
+ const rawExchangeEntries = await loadExchangeEntriesForComparison({
8160
8753
  connector,
8161
8754
  startTime: window2.start,
8162
8755
  endTime: window2.end
@@ -8164,11 +8757,36 @@ var saveAndPrintReplayExchangeComparison = async ({
8164
8757
  const strategyNameByOrderLinkKey = buildStrategyNameByOrderLinkKey(
8165
8758
  liveStrategySummaries.map((summary) => summary.strategyName)
8166
8759
  );
8760
+ const exchangeOutcomeByEntry = /* @__PURE__ */ new Map();
8761
+ const exchangeEntries = rawExchangeEntries.filter((entry) => {
8762
+ const strategyName = resolveReplayStrategyNameFromExchangeEntry({
8763
+ exchangeEntry: entry,
8764
+ strategyNameByOrderLinkKey
8765
+ });
8766
+ if (!strategyName) {
8767
+ return false;
8768
+ }
8769
+ const outcome = findExchangeRuntimeOutcome({
8770
+ exchangeEntry: entry,
8771
+ strategyName,
8772
+ runtimeSignals,
8773
+ runtimeSignalEvaluations,
8774
+ toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8775
+ signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8776
+ });
8777
+ exchangeOutcomeByEntry.set(entry, outcome);
8778
+ return outcome != null;
8779
+ });
8780
+ const comparisonLineage = {
8781
+ ...lineage,
8782
+ excludedExchangeEntries: lineage.excludedExchangeEntries + rawExchangeEntries.length - exchangeEntries.length
8783
+ };
8784
+ const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
8167
8785
  if (!exchangeEntries.length) {
8168
8786
  console.log("");
8169
8787
  console.log(
8170
8788
  import_chalk11.default.yellow(
8171
- `SIGNALS REPLAY VS EXCHANGE: no exchange entry executions found for ${connectorName} in ${(0, import_time6.formatUnix)(
8789
+ `SIGNALS REPLAY VS EXCHANGE: no lineage-linked exchange entry executions found for ${connectorName} in ${(0, import_time6.formatUnix)(
8172
8790
  window2.start
8173
8791
  )} -> ${(0, import_time6.formatUnix)(window2.end)}`
8174
8792
  )
@@ -8195,18 +8813,21 @@ var saveAndPrintReplayExchangeComparison = async ({
8195
8813
  runtimeEntriesCount: 0,
8196
8814
  backtestEntriesCount: backtestEntries.length,
8197
8815
  matchedCount: 0,
8816
+ orderFailedCount: 0,
8198
8817
  runtimeOnlyCount: 0,
8199
8818
  backtestOnlyCount: backtestEntries.length,
8200
8819
  details: details2,
8820
+ lineage: comparisonLineage,
8201
8821
  rows: liveStrategySummaries.map((summary) => ({
8202
8822
  strategyName: summary.strategyName,
8203
8823
  backtestEntries: backtestEntries.filter(
8204
8824
  (entry) => entry.strategy === summary.strategyName
8205
8825
  ).length,
8206
- backtestNetProfit: summary.netProfit,
8826
+ backtestNetProfit: backtestPnlByStrategy.get(summary.strategyName) ?? 0,
8207
8827
  runtimeTrades: 0,
8208
8828
  runtimePnl: 0,
8209
8829
  matched: 0,
8830
+ orderFailed: 0,
8210
8831
  runtimeOnly: 0,
8211
8832
  backtestOnly: backtestEntries.filter(
8212
8833
  (entry) => entry.strategy === summary.strategyName
@@ -8220,6 +8841,14 @@ var saveAndPrintReplayExchangeComparison = async ({
8220
8841
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8221
8842
  backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8222
8843
  });
8844
+ const { completed: matched, orderFailed } = splitExchangeMatchesByRuntimeOrderStatus({
8845
+ matched: comparison.matched,
8846
+ strategyNameByOrderLinkKey,
8847
+ runtimeSignals,
8848
+ runtimeSignalEvaluations,
8849
+ toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8850
+ signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8851
+ });
8223
8852
  const liveSummaryByStrategy = new Map(
8224
8853
  liveStrategySummaries.map((summary) => [summary.strategyName, summary])
8225
8854
  );
@@ -8232,10 +8861,11 @@ var saveAndPrintReplayExchangeComparison = async ({
8232
8861
  const next = {
8233
8862
  strategyName,
8234
8863
  backtestEntries: 0,
8235
- backtestNetProfit: liveSummaryByStrategy.get(strategyName)?.netProfit ?? 0,
8864
+ backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
8236
8865
  runtimeTrades: 0,
8237
8866
  runtimePnl: 0,
8238
8867
  matched: 0,
8868
+ orderFailed: 0,
8239
8869
  runtimeOnly: 0,
8240
8870
  backtestOnly: 0
8241
8871
  };
@@ -8248,7 +8878,7 @@ var saveAndPrintReplayExchangeComparison = async ({
8248
8878
  for (const entry of backtestEntries) {
8249
8879
  ensureRow(entry.strategy).backtestEntries += 1;
8250
8880
  }
8251
- for (const item of comparison.matched) {
8881
+ for (const item of matched) {
8252
8882
  const row = ensureRow(item.backtest.strategy);
8253
8883
  row.runtimeTrades += 1;
8254
8884
  row.matched += 1;
@@ -8256,6 +8886,11 @@ var saveAndPrintReplayExchangeComparison = async ({
8256
8886
  row.runtimePnl += item.exchange.closedPnl;
8257
8887
  }
8258
8888
  }
8889
+ for (const item of orderFailed) {
8890
+ const row = ensureRow(item.backtest.strategy);
8891
+ row.runtimeTrades += 1;
8892
+ row.orderFailed += 1;
8893
+ }
8259
8894
  for (const entry of comparison.backtestOnly) {
8260
8895
  ensureRow(entry.strategy).backtestOnly += 1;
8261
8896
  }
@@ -8274,7 +8909,8 @@ var saveAndPrintReplayExchangeComparison = async ({
8274
8909
  }
8275
8910
  }
8276
8911
  const details = buildReplayExchangeComparisonDetails({
8277
- matched: comparison.matched,
8912
+ matched,
8913
+ orderFailed,
8278
8914
  exchangeOnly: comparison.exchangeOnly,
8279
8915
  backtestOnly: comparison.backtestOnly,
8280
8916
  exchangeEntries,
@@ -8301,8 +8937,10 @@ var saveAndPrintReplayExchangeComparison = async ({
8301
8937
  import_chalk11.default.yellow(String(row.runtimeTrades)),
8302
8938
  rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
8303
8939
  import_chalk11.default.green(String(row.matched)),
8940
+ import_chalk11.default.red(String(row.orderFailed)),
8304
8941
  import_chalk11.default.yellow(String(row.runtimeOnly)),
8305
- import_chalk11.default.magenta(String(row.backtestOnly))
8942
+ import_chalk11.default.magenta(String(row.backtestOnly)),
8943
+ import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
8306
8944
  ];
8307
8945
  });
8308
8946
  console.log("");
@@ -8330,17 +8968,20 @@ var saveAndPrintReplayExchangeComparison = async ({
8330
8968
  windowTradesCount: exchangeEntries.length,
8331
8969
  runtimeEntriesCount: exchangeEntries.length,
8332
8970
  backtestEntriesCount: backtestEntries.length,
8333
- matchedCount: comparison.matched.length,
8971
+ matchedCount: matched.length,
8972
+ orderFailedCount: orderFailed.length,
8334
8973
  runtimeOnlyCount: comparison.exchangeOnly.length,
8335
8974
  backtestOnlyCount: comparison.backtestOnly.length,
8336
8975
  rows,
8337
- details
8976
+ details,
8977
+ lineage: comparisonLineage
8338
8978
  };
8339
8979
  };
8340
8980
  var saveAndPrintReplayRuntimeComparison = async ({
8341
8981
  liveStrategySummaries,
8342
8982
  backtestEntries,
8343
- replaySignals
8983
+ replaySignals,
8984
+ replayLineages
8344
8985
  }) => {
8345
8986
  const { connector, connectorName, window: window2 } = getRuntimeCompareContext();
8346
8987
  if (!connector || !window2) {
@@ -8350,8 +8991,16 @@ var saveAndPrintReplayRuntimeComparison = async ({
8350
8991
  liveStrategySummaries.map((summary) => summary.strategyName)
8351
8992
  );
8352
8993
  const replaySignalEvaluations = buildReplaySignalEvaluations(replaySignals);
8353
- const [rawRuntimeTrades, runtimeSignals, runtimeSignalEvaluations] = await Promise.all([
8354
- loadRuntimeTrades(replayUserName),
8994
+ const [
8995
+ rawRuntimeTrades,
8996
+ runtimeSignals,
8997
+ runtimeSignalEvaluations,
8998
+ runtimeLineageScopes
8999
+ ] = await Promise.all([
9000
+ loadRuntimeTrades(replayUserName, {
9001
+ startTime: window2.start,
9002
+ endTime: window2.end
9003
+ }),
8355
9004
  loadRuntimeSignals(replayUserName, {
8356
9005
  startTime: window2.start,
8357
9006
  endTime: window2.end
@@ -8359,6 +9008,10 @@ var saveAndPrintReplayRuntimeComparison = async ({
8359
9008
  loadRuntimeSignalEvaluations(replayUserName, {
8360
9009
  startTime: window2.start,
8361
9010
  endTime: window2.end
9011
+ }),
9012
+ loadRuntimeLineageScopes(replayUserName, {
9013
+ startTime: window2.start,
9014
+ endTime: window2.end
8362
9015
  })
8363
9016
  ]);
8364
9017
  const syncedRuntimeTrades = await syncRuntimeTrades({
@@ -8400,7 +9053,16 @@ var saveAndPrintReplayRuntimeComparison = async ({
8400
9053
  }
8401
9054
  }
8402
9055
  });
8403
- const windowRuntimeTrades = syncedRuntimeTrades.filter(
9056
+ const comparable = filterReplayComparisonByLineage({
9057
+ replayLineages,
9058
+ runtimeTrades: syncedRuntimeTrades,
9059
+ runtimeSignals,
9060
+ runtimeSignalEvaluations,
9061
+ runtimeLineageScopes,
9062
+ backtestEntries
9063
+ });
9064
+ const comparableBacktestEntries = comparable.backtestEntries;
9065
+ const windowRuntimeTrades = comparable.runtimeTrades.filter(
8404
9066
  (trade) => trade.entryTimestamp >= window2.start && trade.entryTimestamp < window2.end && relevantStrategies.has(trade.strategy)
8405
9067
  );
8406
9068
  if (!windowRuntimeTrades.length) {
@@ -8409,17 +9071,18 @@ var saveAndPrintReplayRuntimeComparison = async ({
8409
9071
  import_chalk11.default.yellow(
8410
9072
  `SIGNALS REPLAY VS RUNTIME: no local runtime trades found for ${connectorName} in ${(0, import_time6.formatUnix)(
8411
9073
  window2.start
8412
- )} -> ${(0, import_time6.formatUnix)(window2.end)}; falling back to direct exchange comparison`
9074
+ )} -> ${(0, import_time6.formatUnix)(window2.end)} with matching lineage; checking lineage-linked exchange executions`
8413
9075
  )
8414
9076
  );
8415
9077
  console.log("");
8416
9078
  return saveAndPrintReplayExchangeComparison({
8417
9079
  liveStrategySummaries,
8418
- backtestEntries,
9080
+ backtestEntries: comparableBacktestEntries,
8419
9081
  replaySignals,
8420
9082
  replaySignalEvaluations,
8421
- runtimeSignals,
8422
- runtimeSignalEvaluations
9083
+ runtimeSignals: comparable.runtimeSignals,
9084
+ runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
9085
+ lineage: comparable.lineage
8423
9086
  });
8424
9087
  }
8425
9088
  const runtimeSummaries = summarizeRuntimeTradesByStrategy(windowRuntimeTrades);
@@ -8430,7 +9093,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
8430
9093
  const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
8431
9094
  const comparison = compareTradeParityEntries({
8432
9095
  runtimeEntries: runtimeDedupe.entries,
8433
- backtestEntries,
9096
+ backtestEntries: comparableBacktestEntries,
8434
9097
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8435
9098
  backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
8436
9099
  });
@@ -8439,18 +9102,18 @@ var saveAndPrintReplayRuntimeComparison = async ({
8439
9102
  runtimeOnly: comparison.runtimeOnly,
8440
9103
  backtestOnly: comparison.backtestOnly,
8441
9104
  runtimeEntries: runtimeDedupe.entries,
8442
- backtestEntries,
9105
+ backtestEntries: comparableBacktestEntries,
8443
9106
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
8444
9107
  backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs(),
8445
- runtimeSignals,
8446
- runtimeSignalEvaluations,
9108
+ runtimeSignals: comparable.runtimeSignals,
9109
+ runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
8447
9110
  replaySignals,
8448
9111
  replaySignalEvaluations
8449
9112
  });
8450
9113
  const parityRows = summarizeTradeParityByStrategy({
8451
9114
  runtimeEntries: runtimeDedupe.entries,
8452
9115
  runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
8453
- backtestEntries,
9116
+ backtestEntries: comparableBacktestEntries,
8454
9117
  matchedEntries: comparison.matched,
8455
9118
  runtimeOnlyEntries: comparison.runtimeOnly,
8456
9119
  backtestOnlyEntries: comparison.backtestOnly
@@ -8459,22 +9122,25 @@ var saveAndPrintReplayRuntimeComparison = async ({
8459
9122
  const liveSummaryByStrategy = new Map(
8460
9123
  liveStrategySummaries.map((summary) => [summary.strategyName, summary])
8461
9124
  );
9125
+ const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(
9126
+ comparableBacktestEntries
9127
+ );
8462
9128
  const strategyNames = /* @__PURE__ */ new Set([
8463
9129
  ...liveSummaryByStrategy.keys(),
8464
9130
  ...runtimeSummaryByStrategy.keys(),
8465
9131
  ...parityByStrategy.keys()
8466
9132
  ]);
8467
9133
  const rows = [...strategyNames].sort((left, right) => left.localeCompare(right)).map((strategyName) => {
8468
- const liveSummary = liveSummaryByStrategy.get(strategyName);
8469
9134
  const runtimeSummary = runtimeSummaryByStrategy.get(strategyName);
8470
9135
  const parity = parityByStrategy.get(strategyName);
8471
9136
  return {
8472
9137
  strategyName,
8473
9138
  backtestEntries: parity?.backtest ?? 0,
8474
- backtestNetProfit: liveSummary?.netProfit ?? 0,
9139
+ backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
8475
9140
  runtimeTrades: runtimeSummary?.trades ?? 0,
8476
9141
  runtimePnl: runtimeSummary?.totalPnl ?? 0,
8477
9142
  matched: parity?.matched ?? 0,
9143
+ orderFailed: 0,
8478
9144
  runtimeOnly: parity?.runtimeOnly ?? 0,
8479
9145
  backtestOnly: parity?.backtestOnly ?? 0
8480
9146
  };
@@ -8489,8 +9155,10 @@ var saveAndPrintReplayRuntimeComparison = async ({
8489
9155
  import_chalk11.default.yellow(String(row.runtimeTrades)),
8490
9156
  rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
8491
9157
  import_chalk11.default.green(String(row.matched)),
9158
+ import_chalk11.default.red(String(row.orderFailed)),
8492
9159
  import_chalk11.default.yellow(String(row.runtimeOnly)),
8493
- import_chalk11.default.magenta(String(row.backtestOnly))
9160
+ import_chalk11.default.magenta(String(row.backtestOnly)),
9161
+ import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
8494
9162
  ];
8495
9163
  });
8496
9164
  console.log("");
@@ -8517,18 +9185,20 @@ var saveAndPrintReplayRuntimeComparison = async ({
8517
9185
  syncedTradesCount: syncedRuntimeTrades.length,
8518
9186
  windowTradesCount: windowRuntimeTrades.length,
8519
9187
  runtimeEntriesCount: runtimeDedupe.entries.length,
8520
- backtestEntriesCount: backtestEntries.length,
9188
+ backtestEntriesCount: comparableBacktestEntries.length,
8521
9189
  matchedCount: comparison.matched.length,
9190
+ orderFailedCount: 0,
8522
9191
  runtimeOnlyCount: comparison.runtimeOnly.length,
8523
9192
  backtestOnlyCount: comparison.backtestOnly.length,
8524
9193
  rows,
8525
- details
9194
+ details,
9195
+ lineage: comparable.lineage
8526
9196
  };
8527
9197
  };
8528
9198
 
8529
9199
  // src/lib/replay/outputReport.ts
8530
- var import_promises = __toESM(require("fs/promises"));
8531
- var import_node_path = __toESM(require("path"));
9200
+ var import_promises2 = __toESM(require("fs/promises"));
9201
+ var import_node_path2 = __toESM(require("path"));
8532
9202
  var import_data = require("@tradejs/core/data");
8533
9203
  var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
8534
9204
  timeZone: "Europe/Moscow",
@@ -8563,6 +9233,7 @@ var buildPerTradeAnalysis = (runtimeComparison) => {
8563
9233
  if (!details) {
8564
9234
  return {
8565
9235
  matched: [],
9236
+ orderFailed: [],
8566
9237
  runtimeOnly: [],
8567
9238
  backtestOnly: []
8568
9239
  };
@@ -8591,6 +9262,17 @@ var buildPerTradeAnalysis = (runtimeComparison) => {
8591
9262
  slippage: item.slippage
8592
9263
  }
8593
9264
  })),
9265
+ orderFailed: details.orderFailed.map((item) => ({
9266
+ status: "orderFailed",
9267
+ strategy: entryStrategy(item.backtest),
9268
+ symbol: item.backtest.symbol,
9269
+ direction: item.backtest.direction,
9270
+ runtime: item.runtime,
9271
+ backtest: item.backtest,
9272
+ reason: item.reason,
9273
+ timestampDiffMs: item.timestampDiffMs,
9274
+ entryPriceDeltaPct: item.priceDeltaPct
9275
+ })),
8594
9276
  runtimeOnly: details.runtimeOnly.map((entry) => ({
8595
9277
  status: "runtimeOnly",
8596
9278
  strategy: entryStrategy(entry),
@@ -8674,6 +9356,26 @@ var renderRuntimeOnlyRows = (perTrade) => perTrade.length ? createMarkdownTable(
8674
9356
  ];
8675
9357
  })
8676
9358
  ) : "_No runtime-only trades._";
9359
+ var renderOrderFailedRows = (perTrade) => perTrade.length ? createMarkdownTable(
9360
+ [
9361
+ "Strategy",
9362
+ "Symbol",
9363
+ "Dir",
9364
+ "BT time",
9365
+ "RT time",
9366
+ "Entry delta",
9367
+ "Reason"
9368
+ ],
9369
+ perTrade.map((item) => [
9370
+ item.strategy,
9371
+ item.symbol,
9372
+ item.direction,
9373
+ formatMsk(item.backtest.timestamp),
9374
+ formatMsk(item.runtime.timestamp),
9375
+ formatPct(item.entryPriceDeltaPct),
9376
+ item.reason
9377
+ ])
9378
+ ) : "_No failed runtime orders._";
8677
9379
  var renderBacktestOnlyRows = (perTrade) => perTrade.length ? createMarkdownTable(
8678
9380
  [
8679
9381
  "Strategy",
@@ -8711,8 +9413,10 @@ var renderStrategyRows = (runtimeComparison) => runtimeComparison?.rows.length ?
8711
9413
  "RT trades",
8712
9414
  "RT PnL",
8713
9415
  "Matched",
9416
+ "Order failed",
8714
9417
  "RT only",
8715
- "BT only"
9418
+ "BT only",
9419
+ "Unmatched"
8716
9420
  ],
8717
9421
  runtimeComparison.rows.map((row) => [
8718
9422
  row.strategyName,
@@ -8721,10 +9425,69 @@ var renderStrategyRows = (runtimeComparison) => runtimeComparison?.rows.length ?
8721
9425
  row.runtimeTrades,
8722
9426
  formatMoney(row.runtimePnl),
8723
9427
  row.matched,
9428
+ row.orderFailed,
8724
9429
  row.runtimeOnly,
8725
- row.backtestOnly
9430
+ row.backtestOnly,
9431
+ getReplayRuntimeUnmatchedCount(row)
8726
9432
  ])
8727
9433
  ) : "_No runtime comparison rows._";
9434
+ var renderLineageRows = (runtimeComparison) => {
9435
+ const grouped = /* @__PURE__ */ new Map();
9436
+ for (const record of runtimeComparison?.lineage.replay ?? []) {
9437
+ const existing = grouped.get(record.strategy) ?? {
9438
+ strategy: record.strategy,
9439
+ scopes: 0,
9440
+ gitShas: /* @__PURE__ */ new Set(),
9441
+ dirty: /* @__PURE__ */ new Set(),
9442
+ gates: /* @__PURE__ */ new Set(),
9443
+ configs: /* @__PURE__ */ new Set(),
9444
+ contexts: /* @__PURE__ */ new Set()
9445
+ };
9446
+ existing.scopes += 1;
9447
+ existing.gitShas.add(record.lineage.gitSha ?? "unknown");
9448
+ existing.dirty.add(
9449
+ record.lineage.gitDirty == null ? "unknown" : String(record.lineage.gitDirty)
9450
+ );
9451
+ existing.gates.add(record.lineage.gateFingerprint);
9452
+ existing.configs.add(record.lineage.configFingerprint);
9453
+ existing.contexts.add(record.lineage.contextFingerprint);
9454
+ grouped.set(record.strategy, existing);
9455
+ }
9456
+ const formatSet = (values) => {
9457
+ const sorted = [...values].sort();
9458
+ return sorted.length <= 3 ? sorted.join(", ") : `${sorted.slice(0, 3).join(", ")} \u2026 (+${sorted.length - 3})`;
9459
+ };
9460
+ return grouped.size ? createMarkdownTable(
9461
+ [
9462
+ "Strategy",
9463
+ "Scopes",
9464
+ "Git SHA",
9465
+ "Dirty",
9466
+ "Gate",
9467
+ "Config variants",
9468
+ "Context"
9469
+ ],
9470
+ [...grouped.values()].sort((left, right) => left.strategy.localeCompare(right.strategy)).map(
9471
+ ({
9472
+ strategy,
9473
+ scopes,
9474
+ gitShas,
9475
+ dirty,
9476
+ gates,
9477
+ configs,
9478
+ contexts
9479
+ }) => [
9480
+ strategy,
9481
+ scopes,
9482
+ formatSet(gitShas),
9483
+ formatSet(dirty),
9484
+ formatSet(gates),
9485
+ `${configs.size}: ${formatSet(configs)}`,
9486
+ formatSet(contexts)
9487
+ ]
9488
+ )
9489
+ ) : "_No replay lineage records._";
9490
+ };
8728
9491
  var writeReplayOutputReport = async ({
8729
9492
  projectRoot,
8730
9493
  timestamp,
@@ -8739,8 +9502,8 @@ var writeReplayOutputReport = async ({
8739
9502
  strategySnapshot,
8740
9503
  runtimeComparison
8741
9504
  }) => {
8742
- const outputDir = import_node_path.default.resolve(projectRoot, "data/replay/output");
8743
- await import_promises.default.mkdir(outputDir, { recursive: true });
9505
+ const outputDir = import_node_path2.default.resolve(projectRoot, "data/replay/output");
9506
+ await import_promises2.default.mkdir(outputDir, { recursive: true });
8744
9507
  const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
8745
9508
  const report = {
8746
9509
  reportType: "replay-output",
@@ -8766,15 +9529,17 @@ var writeReplayOutputReport = async ({
8766
9529
  cycleCount: replayResult.cycleCount,
8767
9530
  abortedCycles: replayResult.abortedCycles,
8768
9531
  matched: runtimeComparison?.matchedCount ?? 0,
9532
+ orderFailed: runtimeComparison?.orderFailedCount ?? 0,
8769
9533
  runtimeOnly: runtimeComparison?.runtimeOnlyCount ?? 0,
8770
9534
  backtestOnly: runtimeComparison?.backtestOnlyCount ?? 0
8771
9535
  },
8772
9536
  resultsByStrategies: strategySnapshot.summaries,
9537
+ replayLineage: replayResult.runtimeLineages,
8773
9538
  runtimeComparison,
8774
9539
  perTradeAnalysis
8775
9540
  };
8776
- const markdownPath = import_node_path.default.join(outputDir, `${timestamp}-replay-report.md`);
8777
- const jsonPath = import_node_path.default.join(outputDir, `${timestamp}-replay-report.json`);
9541
+ const markdownPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.md`);
9542
+ const jsonPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.json`);
8778
9543
  const lines = [
8779
9544
  "# Replay Report",
8780
9545
  "",
@@ -8794,13 +9559,26 @@ var writeReplayOutputReport = async ({
8794
9559
  ["Window", `${formatMsk(window2.start)} - ${formatMsk(window2.end)} MSK`],
8795
9560
  ["Runtime compare mode", runtimeComparison?.mode ?? "none"],
8796
9561
  ["Matched", runtimeComparison?.matchedCount ?? 0],
9562
+ ["Order failed", runtimeComparison?.orderFailedCount ?? 0],
8797
9563
  ["Runtime-only", runtimeComparison?.runtimeOnlyCount ?? 0],
8798
9564
  ["Backtest-only", runtimeComparison?.backtestOnlyCount ?? 0],
9565
+ [
9566
+ "Comparable lineage scopes",
9567
+ runtimeComparison ? `${runtimeComparison.lineage.comparableScopes}/${runtimeComparison.lineage.replayScopes}` : "0/0"
9568
+ ],
9569
+ [
9570
+ "Lineage exclusions",
9571
+ runtimeComparison ? `runtime=${runtimeComparison.lineage.excludedRuntimeTrades}, exchange=${runtimeComparison.lineage.excludedExchangeEntries}, backtest=${runtimeComparison.lineage.excludedBacktestEntries}` : ""
9572
+ ],
8799
9573
  ["Command", `\`${process.argv.join(" ")}\``],
8800
9574
  ["JSON report", jsonPath]
8801
9575
  ]
8802
9576
  ),
8803
9577
  "",
9578
+ "## Replay Lineage",
9579
+ "",
9580
+ renderLineageRows(runtimeComparison),
9581
+ "",
8804
9582
  "## Strategy Comparison",
8805
9583
  "",
8806
9584
  renderStrategyRows(runtimeComparison),
@@ -8809,6 +9587,10 @@ var writeReplayOutputReport = async ({
8809
9587
  "",
8810
9588
  renderMatchedRows(perTradeAnalysis.matched),
8811
9589
  "",
9590
+ "## Failed Runtime Orders",
9591
+ "",
9592
+ renderOrderFailedRows(perTradeAnalysis.orderFailed),
9593
+ "",
8812
9594
  "## Runtime-Only Trades",
8813
9595
  "",
8814
9596
  renderRuntimeOnlyRows(perTradeAnalysis.runtimeOnly),
@@ -8823,8 +9605,8 @@ var writeReplayOutputReport = async ({
8823
9605
  ""
8824
9606
  ];
8825
9607
  await Promise.all([
8826
- import_promises.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
8827
- import_promises.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
9608
+ import_promises2.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
9609
+ import_promises2.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
8828
9610
  `, "utf8")
8829
9611
  ]);
8830
9612
  return {
@@ -8863,7 +9645,8 @@ var finishReplay = async ({
8863
9645
  const replayRuntimeComparison = await saveAndPrintReplayRuntimeComparison({
8864
9646
  liveStrategySummaries: replayStrategySnapshot.summaries,
8865
9647
  backtestEntries: replayStrategySnapshot.backtestEntries,
8866
- replaySignals: replayResult.signals
9648
+ replaySignals: replayResult.signals,
9649
+ replayLineages: replayResult.runtimeLineages
8867
9650
  });
8868
9651
  const finishedAt = /* @__PURE__ */ new Date();
8869
9652
  const durationSeconds = Number(
@@ -8879,7 +9662,7 @@ var finishReplay = async ({
8879
9662
  replayResult.orderLog[replayResult.orderLog.length - 1]?.timestamp ?? finishedAt.getTime()
8880
9663
  )}`
8881
9664
  }) : null;
8882
- const replayKey = import_redis5.redisKeys.backtestResults(
9665
+ const replayKey = import_redis6.redisKeys.backtestResults(
8883
9666
  replayUserName,
8884
9667
  REPLAY_RESULTS_CONFIG,
8885
9668
  timestamp
@@ -8900,7 +9683,7 @@ var finishReplay = async ({
8900
9683
  });
8901
9684
  console.log(import_chalk12.default.green(`Replay report: ${outputReport.markdownPath}`));
8902
9685
  console.log(import_chalk12.default.green(`Replay report JSON: ${outputReport.jsonPath}`));
8903
- await (0, import_redis5.setData)(
9686
+ await (0, import_redis6.setData)(
8904
9687
  replayKey,
8905
9688
  {
8906
9689
  config: REPLAY_RESULTS_CONFIG,
@@ -8921,6 +9704,7 @@ var finishReplay = async ({
8921
9704
  cycleCount: replayResult.cycleCount,
8922
9705
  abortedCycles: replayResult.abortedCycles,
8923
9706
  signalsCount: replayResult.signals.length,
9707
+ replayLineage: replayResult.runtimeLineages,
8924
9708
  strategyCharts: replayChartSnapshot,
8925
9709
  outputReport
8926
9710
  },
@@ -8931,8 +9715,8 @@ var finishReplay = async ({
8931
9715
  if (replayChartSnapshot) {
8932
9716
  await Promise.all(
8933
9717
  replayChartSnapshot.strategies.map(
8934
- (card) => (0, import_redis5.setData)(
8935
- import_redis5.redisKeys.strategyChartCard(replayUserName, "replay", card.cardId),
9718
+ (card) => (0, import_redis6.setData)(
9719
+ import_redis6.redisKeys.strategyChartCard(replayUserName, "replay", card.cardId),
8936
9720
  card,
8937
9721
  {
8938
9722
  expire: import_constants6.TTL_1M
@@ -8957,7 +9741,8 @@ var replayBacktest = async () => {
8957
9741
  endTime: replayFlags.endTime,
8958
9742
  cacheOnly: replayFlags.cacheOnly,
8959
9743
  interval: replayInterval,
8960
- projectRoot: replayProjectRoot
9744
+ projectRoot: replayProjectRoot,
9745
+ closedIntervalMs: (0, import_data2.intervalToMs)(replayInterval)
8961
9746
  });
8962
9747
  if (!preparedRun || isReplayUpdateOnlyRun) {
8963
9748
  return;