@tradejs/cli 3.1.29-beta.291 → 3.1.29-beta.296

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.
@@ -11,9 +11,9 @@ var __export = (target, all) => {
11
11
  };
12
12
  var __copyProps = (to, from, except, desc) => {
13
13
  if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ for (let key2 of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key2) && key2 !== except)
16
+ __defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
17
17
  }
18
18
  return to;
19
19
  };
@@ -591,9 +591,9 @@ var loadRuntimeTrades = async (userName, {
591
591
  const tradePrefix = import_redis.redisKeys.runtimeTrades(userName);
592
592
  const bucketPrefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
593
593
  const keys = (await (0, import_redis.getKeys)(tradePrefix)).filter(
594
- (key) => !key.startsWith(bucketPrefix)
594
+ (key2) => !key2.startsWith(bucketPrefix)
595
595
  );
596
- const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
596
+ const trades = await Promise.all(keys.map((key2) => (0, import_redis.getData)(key2, null)));
597
597
  return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
598
598
  };
599
599
 
@@ -1515,13 +1515,12 @@ var getReferenceSymbols = () => {
1515
1515
  };
1516
1516
  var intervalToMarketFeatureInterval = (interval) => {
1517
1517
  const normalized = String(interval).trim().toLowerCase();
1518
- if (normalized === "1" || normalized === "1m") return "1m";
1518
+ if (normalized === "1" || normalized === "1m") return null;
1519
1519
  if (normalized === "5" || normalized === "5m") return "5m";
1520
1520
  if (normalized === "60" || normalized === "1h") return "1h";
1521
1521
  return "15m";
1522
1522
  };
1523
1523
  var marketIntervalToConnectorInterval = (interval) => {
1524
- if (interval === "1m") return "1";
1525
1524
  if (interval === "5m") return "5";
1526
1525
  if (interval === "1h") return "60";
1527
1526
  return "15";
@@ -1686,6 +1685,7 @@ var backfillBinanceMarketContext = async (params, enabled) => {
1686
1685
  return skippedBackfillResult();
1687
1686
  }
1688
1687
  const interval = intervalToMarketFeatureInterval(params.interval);
1688
+ if (interval === null) return skippedBackfillResult();
1689
1689
  const intervalMs = MARKET_FEATURE_INTERVAL_MS[interval];
1690
1690
  const { breadthStartMs, tradeFlowStartMs, endMs } = resolveBinanceMarketContextBackfillWindow(params);
1691
1691
  if (endMs <= Math.min(breadthStartMs, tradeFlowStartMs)) {
@@ -1880,8 +1880,8 @@ var backfillBinanceMarketContext = async (params, enabled) => {
1880
1880
  })
1881
1881
  );
1882
1882
  const missingChunks = chunks.filter((chunk) => {
1883
- const key = `${chunk.startMs}:${chunk.endMs}`;
1884
- return plans.some((plan) => plan.missingChunkKeys.has(key));
1883
+ const key2 = `${chunk.startMs}:${chunk.endMs}`;
1884
+ return plans.some((plan) => plan.missingChunkKeys.has(key2));
1885
1885
  });
1886
1886
  const bar = new import_progress.default(
1887
1887
  "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
@@ -2237,13 +2237,13 @@ var networkErrorCodes = /* @__PURE__ */ new Set([
2237
2237
  "EAI_AGAIN",
2238
2238
  "EHOSTUNREACH"
2239
2239
  ]);
2240
- var getNestedErrorValue = (error, key) => {
2240
+ var getNestedErrorValue = (error, key2) => {
2241
2241
  let current = error;
2242
2242
  const seen = /* @__PURE__ */ new Set();
2243
2243
  while (current && typeof current === "object" && !seen.has(current)) {
2244
2244
  seen.add(current);
2245
2245
  const record = current;
2246
- const value = record[key];
2246
+ const value = record[key2];
2247
2247
  if (typeof value === "string" && value.trim()) {
2248
2248
  return value.trim();
2249
2249
  }
@@ -2376,11 +2376,11 @@ var selectBestMarket = (candidates, exchangePriority) => {
2376
2376
  var buildMatches = (tickers, markets, exchangePriority) => {
2377
2377
  const marketByTicker = /* @__PURE__ */ new Map();
2378
2378
  for (const market of markets) {
2379
- const key = String(market.symbol_on_exchange || "").trim().toUpperCase();
2380
- if (!key) continue;
2381
- const list = marketByTicker.get(key) ?? [];
2379
+ const key2 = String(market.symbol_on_exchange || "").trim().toUpperCase();
2380
+ if (!key2) continue;
2381
+ const list = marketByTicker.get(key2) ?? [];
2382
2382
  list.push(market);
2383
- marketByTicker.set(key, list);
2383
+ marketByTicker.set(key2, list);
2384
2384
  }
2385
2385
  const matches = [];
2386
2386
  const unmatched = [];
@@ -2712,7 +2712,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2712
2712
  while (cursor < window.toMs) {
2713
2713
  const toMs = Math.min(window.toMs, cursor + batchMs);
2714
2714
  const missingRanges = batch.map((item) => {
2715
- const key = coverageKey({
2715
+ const key2 = coverageKey({
2716
2716
  symbol: item.symbol,
2717
2717
  interval,
2718
2718
  fromMs: cursor,
@@ -2727,7 +2727,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2727
2727
  fromMs: cursor,
2728
2728
  toMs,
2729
2729
  nowMs: coverageNowMs,
2730
- dataCoverageKeyExists: coverageKeys.has(key),
2730
+ dataCoverageKeyExists: coverageKeys.has(key2),
2731
2731
  dataCoverageRanges: coverageRangesBySymbol.get(normalizedSymbol),
2732
2732
  liquidationCoverageRanges: liquidationCoverageRangesBySymbol.get(normalizedSymbol),
2733
2733
  edges: edgesBySymbol.get(normalizedSymbol)
@@ -3026,7 +3026,7 @@ var toIntOrNull = (value) => {
3026
3026
  return numeric == null ? null : Math.trunc(numeric);
3027
3027
  };
3028
3028
  var getRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3029
- var getNestedRecord = (value, key) => getRecord(value[key]);
3029
+ var getNestedRecord = (value, key2) => getRecord(value[key2]);
3030
3030
  var safeDivide2 = (numerator, denominator) => numerator != null && denominator != null && denominator > 0 ? numerator / denominator : null;
3031
3031
  var sumFinite = (values) => values.reduce((sum, value) => sum + (value ?? 0), 0);
3032
3032
  var toExchangeLiquidityRegime = ({
@@ -3133,7 +3133,7 @@ var coverageRowsToKeySet = (rows) => new Set(
3133
3133
  );
3134
3134
  var hasBackfillCoverage = (coverageKeys, params) => coverageKeys.has(coverageKey2(params));
3135
3135
  var hasRecentBackfillCoverage = (rows, params) => {
3136
- const key = coverageKey2(params);
3136
+ const key2 = coverageKey2(params);
3137
3137
  return rows.some((row) => {
3138
3138
  if (!isMarketContextBackfillCoverageDataBearing(row)) return false;
3139
3139
  if (coverageKey2({
@@ -3142,7 +3142,7 @@ var hasRecentBackfillCoverage = (rows, params) => {
3142
3142
  interval: row.interval,
3143
3143
  fromMs: row.fromMs,
3144
3144
  toMs: row.toMs
3145
- }) !== key) {
3145
+ }) !== key2) {
3146
3146
  return false;
3147
3147
  }
3148
3148
  const checkedAtMs = Number(row.checkedAtMs);
@@ -3179,27 +3179,27 @@ var createCoinMarketCapFetchStats = () => ({
3179
3179
  totalCredits: 0,
3180
3180
  byPath: /* @__PURE__ */ new Map()
3181
3181
  });
3182
- var getCoinMarketCapPathLabel = (path7) => {
3183
- if (path7.includes("global-metrics")) return "global";
3184
- if (path7.includes("cryptocurrency/quotes")) return "reference";
3185
- if (path7.includes("exchange/quotes")) return "exchangeLiquidity";
3186
- if (path7.includes("fear-and-greed")) return "fearGreed";
3187
- if (path7.includes("cmc100")) return "cmc100";
3188
- if (path7.includes("cmc20")) return "cmc20";
3189
- return path7;
3190
- };
3191
- var recordCoinMarketCapFetchStats = (stats, path7, creditCount) => {
3182
+ var getCoinMarketCapPathLabel = (path9) => {
3183
+ if (path9.includes("global-metrics")) return "global";
3184
+ if (path9.includes("cryptocurrency/quotes")) return "reference";
3185
+ if (path9.includes("exchange/quotes")) return "exchangeLiquidity";
3186
+ if (path9.includes("fear-and-greed")) return "fearGreed";
3187
+ if (path9.includes("cmc100")) return "cmc100";
3188
+ if (path9.includes("cmc20")) return "cmc20";
3189
+ return path9;
3190
+ };
3191
+ var recordCoinMarketCapFetchStats = (stats, path9, creditCount) => {
3192
3192
  if (!stats) return;
3193
- const existing = stats.byPath.get(path7) ?? { requests: 0, credits: 0 };
3193
+ const existing = stats.byPath.get(path9) ?? { requests: 0, credits: 0 };
3194
3194
  existing.requests += 1;
3195
3195
  existing.credits += creditCount ?? 0;
3196
3196
  stats.requests += 1;
3197
3197
  stats.totalCredits += creditCount ?? 0;
3198
- stats.byPath.set(path7, existing);
3198
+ stats.byPath.set(path9, existing);
3199
3199
  };
3200
3200
  var formatCoinMarketCapFetchStats = (stats) => {
3201
3201
  const byPath = [...stats.byPath.entries()].map(
3202
- ([path7, item]) => `${getCoinMarketCapPathLabel(path7)}=${item.requests}req/${item.credits}cr`
3202
+ ([path9, item]) => `${getCoinMarketCapPathLabel(path9)}=${item.requests}req/${item.credits}cr`
3203
3203
  ).join(" ");
3204
3204
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
3205
3205
  };
@@ -3221,8 +3221,8 @@ var logBackfillSourceDone = (params) => {
3221
3221
  var markBackfillCoverage = (rows) => (0, import_marketContext2.upsertMarketContextBackfillCoverage)(rows);
3222
3222
  var coinMarketCapFetch = async (params) => {
3223
3223
  const url = new URL(`${getBaseUrl()}${params.path}`);
3224
- for (const [key, value] of Object.entries(params.searchParams)) {
3225
- url.searchParams.set(key, value);
3224
+ for (const [key2, value] of Object.entries(params.searchParams)) {
3225
+ url.searchParams.set(key2, value);
3226
3226
  }
3227
3227
  for (let attempt = 0; attempt <= getMaxRetries(); attempt += 1) {
3228
3228
  const waitMs = Math.max(
@@ -3266,7 +3266,7 @@ var coinMarketCapFetch = async (params) => {
3266
3266
  };
3267
3267
  var fetchCoinMarketCapIndexRows = async (params) => {
3268
3268
  const rows = [];
3269
- const maxChunkDays = 9;
3269
+ const maxChunkDays = 4;
3270
3270
  const totalChunks = Math.max(
3271
3271
  1,
3272
3272
  Math.ceil(
@@ -3403,10 +3403,10 @@ var coinMarketCapExchangeQuotesPayloadToLiquidityRows = (payload, interval = "1d
3403
3403
  const ts = typeof tsRaw === "string" ? new Date(tsRaw) : null;
3404
3404
  if (!ts || Number.isNaN(ts.getTime())) continue;
3405
3405
  const volumeUsd = toFiniteNumberOrNull(usd.volume_24h) ?? toFiniteNumberOrNull(usd.volume_24h_adjusted) ?? toFiniteNumberOrNull(usd.spot_volume_usd) ?? toFiniteNumberOrNull(usd.reported_volume_24h);
3406
- const key = ts.getTime();
3407
- const rows = rowsByTs.get(key) ?? [];
3406
+ const key2 = ts.getTime();
3407
+ const rows = rowsByTs.get(key2) ?? [];
3408
3408
  rows.push({ slug, name, volumeUsd });
3409
- rowsByTs.set(key, rows);
3409
+ rowsByTs.set(key2, rows);
3410
3410
  }
3411
3411
  }
3412
3412
  return [...rowsByTs.entries()].sort(([left], [right]) => left - right).map(([ts, exchanges]) => {
@@ -4809,6 +4809,10 @@ import_args.default.option(
4809
4809
  "Write aggregate portfolio metrics and SVG charts; skip runtime/exchange comparison",
4810
4810
  false
4811
4811
  );
4812
+ import_args.default.option(
4813
+ "portfolioCompareTo",
4814
+ "Compare a full shared replay with a prior portfolio report JSON"
4815
+ );
4812
4816
  import_args.default.option(
4813
4817
  ["B", "portfolioBatchSize"],
4814
4818
  "Maximum number of symbols held in memory by one portfolio replay batch",
@@ -4854,7 +4858,7 @@ var normalizeForStableJson = (value) => {
4854
4858
  }
4855
4859
  if (value && typeof value === "object") {
4856
4860
  return Object.fromEntries(
4857
- Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
4861
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key2, entry]) => [key2, normalizeForStableJson(entry)])
4858
4862
  );
4859
4863
  }
4860
4864
  return value;
@@ -5012,9 +5016,9 @@ var buildRuntimeEvidenceLineageScopes = ({
5012
5016
  if (!lineage || !Number.isFinite(timestamp)) {
5013
5017
  return;
5014
5018
  }
5015
- const key = `${deploymentId ?? "default"}::${accountId ?? "default"}::${strategy}::${symbol}::${runtimeLineageKey(lineage)}`;
5016
- const existing = scopes.get(key);
5017
- scopes.set(key, {
5019
+ const key2 = `${deploymentId ?? "default"}::${accountId ?? "default"}::${strategy}::${symbol}::${runtimeLineageKey(lineage)}`;
5020
+ const existing = scopes.get(key2);
5021
+ scopes.set(key2, {
5018
5022
  strategy,
5019
5023
  symbol,
5020
5024
  ...deploymentId ? { deploymentId } : {},
@@ -5177,12 +5181,13 @@ var getPositionPnlSnapshot = ({
5177
5181
  direction: position.direction
5178
5182
  };
5179
5183
  };
5180
- var createPortfolioReplayConnector = (connector) => {
5184
+ var createPortfolioReplayConnector = (connector, observeEquity = false) => {
5181
5185
  let state = {};
5182
5186
  let amount = import_constants3.INITIAL_BACKTEST_AMOUNT;
5183
5187
  let executionTimestamp = null;
5184
5188
  const positionsBySymbol = /* @__PURE__ */ new Map();
5185
5189
  const currentPriceBySymbol = /* @__PURE__ */ new Map();
5190
+ const equityMarks = observeEquity ? /* @__PURE__ */ new Map() : void 0;
5186
5191
  const markedAtBySymbol = /* @__PURE__ */ new Map();
5187
5192
  const entryFacts = /* @__PURE__ */ new Map();
5188
5193
  let portfolioObserved = false;
@@ -5603,11 +5608,25 @@ var createPortfolioReplayConnector = (connector) => {
5603
5608
  advanceMarket: async ({ symbol, candle }) => {
5604
5609
  executionTimestamp = candle.timestamp;
5605
5610
  markedAtBySymbol.set(symbol, candle.timestamp);
5611
+ equityMarks?.set(symbol, candle.close);
5606
5612
  currentPriceBySymbol.set(symbol, candle.close);
5607
5613
  await checkSl({ symbol, candle });
5608
5614
  await checkTp({ symbol, candle });
5609
5615
  currentPriceBySymbol.set(symbol, candle.close);
5610
5616
  },
5617
+ getMarkedEquity: () => {
5618
+ if (!equityMarks) return null;
5619
+ let equity = amount - import_constants3.INITIAL_BACKTEST_AMOUNT;
5620
+ for (const { position } of positionsBySymbol.values()) {
5621
+ const mark = equityMarks.get(position.symbol);
5622
+ if (mark == null || !Number.isFinite(mark)) return null;
5623
+ equity += getPositionPnlSnapshot({
5624
+ position,
5625
+ currentPrice: mark
5626
+ }).unrealizedPnl;
5627
+ }
5628
+ return (0, import_math.round)(equity);
5629
+ },
5611
5630
  getReplayArtifacts: () => ({
5612
5631
  orderLog,
5613
5632
  positionLog,
@@ -5821,6 +5840,21 @@ var executeHistoricalReplay = async (context, adapters) => {
5821
5840
  context.collectSkipEvidence
5822
5841
  );
5823
5842
  const decisionTrace = createReplayDecisionTrace(context.decisionTraceTargets);
5843
+ const portfolioDiagnostics = context.collectPortfolioDiagnostics ? { markedMaxDrawdown: 0, unpricedCycles: 0, admissions: [] } : void 0;
5844
+ let equityPeak = 0;
5845
+ const observeEquity = () => {
5846
+ if (!portfolioDiagnostics) return;
5847
+ const equity = context.connector.getMarkedEquity();
5848
+ if (equity === null) {
5849
+ portfolioDiagnostics.unpricedCycles++;
5850
+ return;
5851
+ }
5852
+ equityPeak = Math.max(equityPeak, equity);
5853
+ portfolioDiagnostics.markedMaxDrawdown = Math.max(
5854
+ portfolioDiagnostics.markedMaxDrawdown ?? 0,
5855
+ equityPeak - equity
5856
+ );
5857
+ };
5824
5858
  const reportProgress = (timestamp) => {
5825
5859
  if (adapters.progress.enabled === false) return;
5826
5860
  adapters.progress.tick({
@@ -5864,6 +5898,7 @@ var executeHistoricalReplay = async (context, adapters) => {
5864
5898
  }
5865
5899
  }
5866
5900
  advanceAbortedCycle(cycleSymbols, timestamp);
5901
+ observeEquity();
5867
5902
  reportProgress(timestamp);
5868
5903
  continue;
5869
5904
  }
@@ -5907,6 +5942,21 @@ var executeHistoricalReplay = async (context, adapters) => {
5907
5942
  result
5908
5943
  });
5909
5944
  if (result && typeof result !== "string") {
5945
+ if (portfolioDiagnostics) {
5946
+ const status = result.orderFailureReason === "PORTFOLIO_POSITION_LIMIT" ? "capacity-rejected" : result.orderStatus === "completed" ? "filled" : void 0;
5947
+ if (status)
5948
+ portfolioDiagnostics.admissions.push({
5949
+ strategy: strategyRuntime.strategyName,
5950
+ status,
5951
+ key: JSON.stringify([
5952
+ strategyRuntime.strategyName,
5953
+ result.symbol,
5954
+ result.direction,
5955
+ result.timestamp,
5956
+ result.signalId
5957
+ ])
5958
+ });
5959
+ }
5910
5960
  result.runtimeLineage = strategyRuntime.runtimeLineage;
5911
5961
  await adapters.enrichSignal(result);
5912
5962
  cycleSignals.push(result);
@@ -5921,12 +5971,16 @@ var executeHistoricalReplay = async (context, adapters) => {
5921
5971
  status: "completed",
5922
5972
  durationMs: adapters.clock.now() - cycleStartedAt
5923
5973
  });
5974
+ observeEquity();
5924
5975
  reportProgress(timestamp);
5925
5976
  }
5926
5977
  } finally {
5927
5978
  releaseHistoricalReplayCaches(context.plan, adapters);
5928
5979
  }
5980
+ if (portfolioDiagnostics?.unpricedCycles)
5981
+ portfolioDiagnostics.markedMaxDrawdown = null;
5929
5982
  return {
5983
+ ...portfolioDiagnostics ? { portfolioDiagnostics } : {},
5930
5984
  signals,
5931
5985
  abortedCycles,
5932
5986
  replayLineageScopes: skipEvidence.values(),
@@ -5982,6 +6036,7 @@ var collectHistoricalReplayResult = (context) => ({
5982
6036
  abortedCycles: context.abortedCycles,
5983
6037
  runtimeLineages: context.runtimeLineages,
5984
6038
  replayLineageScopes: context.replayLineageScopes,
6039
+ ...context.portfolioDiagnostics ? { portfolioDiagnostics: context.portfolioDiagnostics } : {},
5985
6040
  ...context.replayDecisionTraces?.length ? { replayDecisionTraces: context.replayDecisionTraces } : {}
5986
6041
  });
5987
6042
  var compactHistoricalReplayResultForPortfolio = (result) => ({
@@ -6183,6 +6238,7 @@ var runHistoricalSignalsReplay = async ({
6183
6238
  showProgress = true,
6184
6239
  collectSkipEvidence = true,
6185
6240
  collectSignals = true,
6241
+ collectPortfolioDiagnostics = false,
6186
6242
  decisionTraceTargets
6187
6243
  }) => {
6188
6244
  const memory = createReplayMemoryProfile();
@@ -6192,7 +6248,8 @@ var runHistoricalSignalsReplay = async ({
6192
6248
  const projectHooks = projectConfig.hooks;
6193
6249
  const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
6194
6250
  const replayConnector = createPortfolioReplayConnector(
6195
- preparedRun.marketConnector
6251
+ preparedRun.marketConnector,
6252
+ collectPortfolioDiagnostics
6196
6253
  );
6197
6254
  const connectorName = getConnectorName(preparedRun.marketConnector);
6198
6255
  const referenceData = references ?? await loadHistoricalReplayReferences({ preparedRun, interval });
@@ -6270,6 +6327,7 @@ var runHistoricalSignalsReplay = async ({
6270
6327
  connector: replayConnector,
6271
6328
  collectSkipEvidence,
6272
6329
  collectSignals,
6330
+ collectPortfolioDiagnostics,
6273
6331
  decisionTraceTargets,
6274
6332
  hooks: projectHooks,
6275
6333
  hookContext
@@ -6308,7 +6366,8 @@ var runHistoricalSignalsReplay = async ({
6308
6366
  abortedCycles: execution.abortedCycles,
6309
6367
  runtimeLineages: plan.runtimeLineages,
6310
6368
  replayLineageScopes: execution.replayLineageScopes,
6311
- replayDecisionTraces: execution.replayDecisionTraces
6369
+ replayDecisionTraces: execution.replayDecisionTraces,
6370
+ portfolioDiagnostics: execution.portfolioDiagnostics
6312
6371
  });
6313
6372
  memory?.("result:collected");
6314
6373
  return result;
@@ -6555,16 +6614,16 @@ var findRuntimeDuplicateGroups = (entries) => {
6555
6614
  if (entry.source !== "runtime") {
6556
6615
  continue;
6557
6616
  }
6558
- const key = toRuntimeDuplicateKey(entry);
6559
- const bucket = groups.get(key) ?? [];
6617
+ const key2 = toRuntimeDuplicateKey(entry);
6618
+ const bucket = groups.get(key2) ?? [];
6560
6619
  bucket.push(entry);
6561
- groups.set(key, bucket);
6620
+ groups.set(key2, bucket);
6562
6621
  }
6563
- return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key, group]) => {
6622
+ return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key2, group]) => {
6564
6623
  const sortedGroup = [...group].sort(sortTradeParityEntries);
6565
6624
  const first = sortedGroup[0];
6566
6625
  return {
6567
- key,
6626
+ key: key2,
6568
6627
  strategy: first.strategy,
6569
6628
  symbol: first.symbol,
6570
6629
  direction: first.direction,
@@ -6603,26 +6662,26 @@ var compareTradeParityEntries = ({
6603
6662
  const groupedRuntime = /* @__PURE__ */ new Map();
6604
6663
  const groupedBacktest = /* @__PURE__ */ new Map();
6605
6664
  for (const entry of runtimeEntries) {
6606
- const key = toGroupKey(entry);
6607
- const bucket = groupedRuntime.get(key) ?? [];
6665
+ const key2 = toGroupKey(entry);
6666
+ const bucket = groupedRuntime.get(key2) ?? [];
6608
6667
  bucket.push(entry);
6609
- groupedRuntime.set(key, bucket);
6668
+ groupedRuntime.set(key2, bucket);
6610
6669
  }
6611
6670
  for (const entry of backtestEntries) {
6612
- const key = toGroupKey(entry);
6613
- const bucket = groupedBacktest.get(key) ?? [];
6671
+ const key2 = toGroupKey(entry);
6672
+ const bucket = groupedBacktest.get(key2) ?? [];
6614
6673
  bucket.push(entry);
6615
- groupedBacktest.set(key, bucket);
6674
+ groupedBacktest.set(key2, bucket);
6616
6675
  }
6617
6676
  const groupKeys = /* @__PURE__ */ new Set([
6618
6677
  ...groupedRuntime.keys(),
6619
6678
  ...groupedBacktest.keys()
6620
6679
  ]);
6621
- for (const key of groupKeys) {
6622
- const runtimeGroup = [...groupedRuntime.get(key) ?? []].sort(
6680
+ for (const key2 of groupKeys) {
6681
+ const runtimeGroup = [...groupedRuntime.get(key2) ?? []].sort(
6623
6682
  (left, right) => left.timestamp - right.timestamp
6624
6683
  );
6625
- const backtestGroup = [...groupedBacktest.get(key) ?? []].sort(
6684
+ const backtestGroup = [...groupedBacktest.get(key2) ?? []].sort(
6626
6685
  (left, right) => left.timestamp - right.timestamp
6627
6686
  );
6628
6687
  const unmatchedBacktest = backtestGroup.map((entry) => ({
@@ -6863,8 +6922,8 @@ var filterBucketKeysByWindow = (bucketKeys, prefix, startTime, endTime) => {
6863
6922
  if (!allowedDayKeys.size) {
6864
6923
  return [];
6865
6924
  }
6866
- return bucketKeys.filter((key) => {
6867
- const suffix = key.slice(prefix.length);
6925
+ return bucketKeys.filter((key2) => {
6926
+ const suffix = key2.slice(prefix.length);
6868
6927
  const firstColon = suffix.indexOf(":");
6869
6928
  if (firstColon <= 0) {
6870
6929
  return false;
@@ -6883,7 +6942,7 @@ var loadRuntimeSignals = async (userName, {
6883
6942
  startTime,
6884
6943
  endTime
6885
6944
  );
6886
- const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
6945
+ const refs = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalBucketRef);
6887
6946
  const seen = /* @__PURE__ */ new Set();
6888
6947
  const payloads = await Promise.all(
6889
6948
  refs.map(async (ref) => {
@@ -6912,7 +6971,7 @@ var loadRuntimeSignalEvaluations = async (userName, {
6912
6971
  startTime,
6913
6972
  endTime
6914
6973
  );
6915
- const evaluations = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalEvaluationRecord);
6974
+ const evaluations = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalEvaluationRecord);
6916
6975
  const deduped = /* @__PURE__ */ new Map();
6917
6976
  for (const evaluation of evaluations) {
6918
6977
  deduped.set(evaluation.evaluationId, evaluation);
@@ -7082,10 +7141,10 @@ var syncRuntimeTrades = async ({
7082
7141
  )
7083
7142
  ];
7084
7143
  await Promise.all(
7085
- activeTradeKeys.map(async (key) => {
7086
- const activeRef = await (0, import_redis5.getData)(key, null);
7144
+ activeTradeKeys.map(async (key2) => {
7145
+ const activeRef = await (0, import_redis5.getData)(key2, null);
7087
7146
  activeOrderIdByKey.set(
7088
- key,
7147
+ key2,
7089
7148
  typeof activeRef?.orderId === "string" ? activeRef.orderId : null
7090
7149
  );
7091
7150
  })
@@ -8152,9 +8211,9 @@ var filterReplayComparisonByLineage = ({
8152
8211
  );
8153
8212
  const deploymentWindows = /* @__PURE__ */ new Map();
8154
8213
  for (const scope of comparableRuntimeLineageScopes) {
8155
- const key = lineageScopeKey(scope);
8156
- const existing = deploymentWindows.get(key);
8157
- deploymentWindows.set(key, {
8214
+ const key2 = lineageScopeKey(scope);
8215
+ const existing = deploymentWindows.get(key2);
8216
+ deploymentWindows.set(key2, {
8158
8217
  firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
8159
8218
  lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
8160
8219
  });
@@ -8190,9 +8249,9 @@ var filterReplayComparisonByLineage = ({
8190
8249
  ...comparableRuntimeSignals,
8191
8250
  ...comparableRuntimeEvaluations
8192
8251
  ]) {
8193
- const key = lineageScopeKey(artifact);
8194
- const existing = deploymentWindows.get(key);
8195
- deploymentWindows.set(key, {
8252
+ const key2 = lineageScopeKey(artifact);
8253
+ const existing = deploymentWindows.get(key2);
8254
+ deploymentWindows.set(key2, {
8196
8255
  firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
8197
8256
  lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
8198
8257
  });
@@ -8277,16 +8336,16 @@ var compareExchangeEntriesToBacktest = ({
8277
8336
  const groupedExchange = /* @__PURE__ */ new Map();
8278
8337
  const groupedBacktest = /* @__PURE__ */ new Map();
8279
8338
  for (const entry of exchangeEntries) {
8280
- const key = `${entry.symbol}::${entry.direction}`;
8281
- const bucket = groupedExchange.get(key) ?? [];
8339
+ const key2 = `${entry.symbol}::${entry.direction}`;
8340
+ const bucket = groupedExchange.get(key2) ?? [];
8282
8341
  bucket.push(entry);
8283
- groupedExchange.set(key, bucket);
8342
+ groupedExchange.set(key2, bucket);
8284
8343
  }
8285
8344
  for (const entry of backtestEntries) {
8286
- const key = `${entry.symbol}::${entry.direction}`;
8287
- const bucket = groupedBacktest.get(key) ?? [];
8345
+ const key2 = `${entry.symbol}::${entry.direction}`;
8346
+ const bucket = groupedBacktest.get(key2) ?? [];
8288
8347
  bucket.push(entry);
8289
- groupedBacktest.set(key, bucket);
8348
+ groupedBacktest.set(key2, bucket);
8290
8349
  }
8291
8350
  const matched = [];
8292
8351
  const exchangeOnly = [];
@@ -8296,11 +8355,11 @@ var compareExchangeEntriesToBacktest = ({
8296
8355
  ...groupedExchange.keys(),
8297
8356
  ...groupedBacktest.keys()
8298
8357
  ]);
8299
- for (const key of groupKeys) {
8300
- const exchangeGroup = [...groupedExchange.get(key) ?? []].sort(
8358
+ for (const key2 of groupKeys) {
8359
+ const exchangeGroup = [...groupedExchange.get(key2) ?? []].sort(
8301
8360
  (left, right) => left.entryTimestamp - right.entryTimestamp
8302
8361
  );
8303
- const unmatchedBacktest = [...groupedBacktest.get(key) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
8362
+ const unmatchedBacktest = [...groupedBacktest.get(key2) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
8304
8363
  for (const exchangeEntry of exchangeGroup) {
8305
8364
  let bestIndex = -1;
8306
8365
  let bestDiff = Number.POSITIVE_INFINITY;
@@ -8721,14 +8780,14 @@ var mergeExchangeEntryExecutions = (rows) => {
8721
8780
  const mergedRows = [];
8722
8781
  const groups = /* @__PURE__ */ new Map();
8723
8782
  for (const row of rows) {
8724
- const key = getExchangeExecutionMergeKey(row);
8725
- if (!key) {
8783
+ const key2 = getExchangeExecutionMergeKey(row);
8784
+ if (!key2) {
8726
8785
  mergedRows.push(row);
8727
8786
  continue;
8728
8787
  }
8729
- const group = groups.get(key) ?? [];
8788
+ const group = groups.get(key2) ?? [];
8730
8789
  group.push(row);
8731
- groups.set(key, group);
8790
+ groups.set(key2, group);
8732
8791
  }
8733
8792
  for (const group of groups.values()) {
8734
8793
  mergedRows.push(
@@ -9219,13 +9278,13 @@ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
9219
9278
  throw new TypeError("JSON evidence cannot contain toJSON hooks");
9220
9279
  yield "{";
9221
9280
  let first = true;
9222
- for (const key of Object.keys(value)) {
9223
- const field = Reflect.get(value, key);
9281
+ for (const key2 of Object.keys(value)) {
9282
+ const field = Reflect.get(value, key2);
9224
9283
  if (field === void 0 || typeof field === "function" || typeof field === "symbol")
9225
9284
  continue;
9226
9285
  if (!first) yield ",";
9227
9286
  first = false;
9228
- yield JSON.stringify(key) + ":";
9287
+ yield JSON.stringify(key2) + ":";
9229
9288
  yield* jsonParts(field, ancestors);
9230
9289
  }
9231
9290
  yield "}";
@@ -9687,10 +9746,276 @@ var writeReplayOutputReport = async ({
9687
9746
  };
9688
9747
 
9689
9748
  // src/lib/replay/portfolioReport.ts
9749
+ var import_promises5 = __toESM(require("fs/promises"));
9750
+ var import_node_path8 = __toESM(require("path"));
9751
+ var import_node_crypto6 = require("crypto");
9752
+
9753
+ // src/lib/replay/portfolioComparison.ts
9754
+ var import_node_crypto5 = require("crypto");
9755
+
9756
+ // src/lib/coreResearch/io.ts
9757
+ var import_node_crypto3 = require("crypto");
9758
+ var import_node_fs4 = require("fs");
9690
9759
  var import_promises4 = __toESM(require("fs/promises"));
9760
+ var import_backtest2 = require("@tradejs/core/backtest");
9761
+ var import_node_path7 = __toESM(require("path"));
9762
+
9763
+ // src/lib/coreResearch/trace.ts
9764
+ var import_node_fs3 = require("fs");
9765
+ var import_node_readline = __toESM(require("readline"));
9691
9766
  var import_node_path6 = __toESM(require("path"));
9692
- var import_node_crypto3 = require("crypto");
9693
- var DAY_MS4 = 864e5;
9767
+
9768
+ // src/lib/coreResearch/io.ts
9769
+ var canonicalJson = (value) => {
9770
+ const normalize = (current) => {
9771
+ if (Array.isArray(current)) return current.map(normalize);
9772
+ if (current && typeof current === "object") {
9773
+ return Object.fromEntries(
9774
+ Object.entries(current).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key2, entry]) => [key2, normalize(entry)])
9775
+ );
9776
+ }
9777
+ return current;
9778
+ };
9779
+ return JSON.stringify(normalize(value));
9780
+ };
9781
+
9782
+ // src/lib/coreResearch/statistics.ts
9783
+ var import_node_crypto4 = require("crypto");
9784
+
9785
+ // src/lib/coreResearch/metrics.ts
9786
+ var DAY_MS4 = 24 * 60 * 60 * 1e3;
9787
+
9788
+ // src/lib/coreResearch/statistics.ts
9789
+ var createSeededRandom = (seedText) => {
9790
+ const digest = (0, import_node_crypto4.createHash)("sha256").update(seedText).digest();
9791
+ let state = digest.readUInt32LE(0) || 1;
9792
+ return () => {
9793
+ state ^= state << 13;
9794
+ state ^= state >>> 17;
9795
+ state ^= state << 5;
9796
+ return (state >>> 0) / 4294967296;
9797
+ };
9798
+ };
9799
+ var percentile = (sorted, probability) => {
9800
+ if (!sorted.length) return Number.NaN;
9801
+ const index = Math.min(
9802
+ sorted.length - 1,
9803
+ Math.max(0, Math.floor(probability * sorted.length))
9804
+ );
9805
+ return sorted[index];
9806
+ };
9807
+ var calendarClusterBootstrap = (params) => {
9808
+ const clusterMs = params.clusterDays * DAY_MS4;
9809
+ const buckets = /* @__PURE__ */ new Map();
9810
+ if (params.start != null && params.end != null && params.end > params.start) {
9811
+ const first = Math.floor(params.start / clusterMs);
9812
+ const last = Math.ceil(params.end / clusterMs);
9813
+ for (let bucket = first; bucket < last; bucket += 1) {
9814
+ buckets.set(bucket, { control: 0, candidate: 0 });
9815
+ }
9816
+ }
9817
+ const add = (trades, field) => {
9818
+ for (const trade of trades) {
9819
+ const bucket = Math.floor(trade.exitTimestamp / clusterMs);
9820
+ const current = buckets.get(bucket) ?? { control: 0, candidate: 0 };
9821
+ current[field] += trade.netProfit;
9822
+ buckets.set(bucket, current);
9823
+ }
9824
+ };
9825
+ add(params.control, "control");
9826
+ add(params.candidate, "candidate");
9827
+ const deltas = [...buckets.entries()].sort(([left], [right]) => left - right).map(([, value]) => value.candidate - value.control);
9828
+ if (!deltas.length) {
9829
+ return {
9830
+ method: "calendar-cluster-bootstrap",
9831
+ clusterDays: params.clusterDays,
9832
+ iterations: params.iterations,
9833
+ confidenceLevel: params.confidenceLevel,
9834
+ observedMeanPnlDelta: null,
9835
+ confidenceInterval: null,
9836
+ probabilityPositive: null,
9837
+ oneSidedPValue: null,
9838
+ holmAdjustedPValue: null
9839
+ };
9840
+ }
9841
+ const random = createSeededRandom(params.seed);
9842
+ const samples = [];
9843
+ for (let iteration = 0; iteration < params.iterations; iteration += 1) {
9844
+ let sum = 0;
9845
+ for (let index = 0; index < deltas.length; index += 1) {
9846
+ sum += deltas[Math.floor(random() * deltas.length)];
9847
+ }
9848
+ samples.push(sum / deltas.length);
9849
+ }
9850
+ samples.sort((left, right) => left - right);
9851
+ const alpha = 1 - params.confidenceLevel;
9852
+ const positive = samples.filter((value) => value > 0).length;
9853
+ const probabilityPositive = positive / samples.length;
9854
+ const observed = deltas.reduce((sum, value) => sum + value, 0) / deltas.length;
9855
+ const oneSidedPValue = (1 + samples.filter((value) => value - observed >= observed - 1e-12).length) / (samples.length + 1);
9856
+ return {
9857
+ method: "calendar-cluster-bootstrap",
9858
+ clusterDays: params.clusterDays,
9859
+ iterations: params.iterations,
9860
+ confidenceLevel: params.confidenceLevel,
9861
+ observedMeanPnlDelta: deltas.reduce((sum, value) => sum + value, 0) / deltas.length,
9862
+ confidenceInterval: [
9863
+ percentile(samples, alpha / 2),
9864
+ percentile(samples, 1 - alpha / 2)
9865
+ ],
9866
+ probabilityPositive,
9867
+ oneSidedPValue,
9868
+ holmAdjustedPValue: null
9869
+ };
9870
+ };
9871
+
9872
+ // src/lib/replay/portfolioComparison.ts
9873
+ var key = (value) => canonicalJson(value);
9874
+ function longestRecovery(points) {
9875
+ let start;
9876
+ let lastPeak;
9877
+ let longestMs = 0;
9878
+ for (const point of points) {
9879
+ if (point.value === 0) {
9880
+ if (start !== void 0)
9881
+ longestMs = Math.max(longestMs, point.timestamp - start);
9882
+ start = void 0;
9883
+ lastPeak = point.timestamp;
9884
+ } else if (start === void 0) start = lastPeak ?? point.timestamp;
9885
+ }
9886
+ return { longestCompletedMs: longestMs, unresolvedSince: start ?? null };
9887
+ }
9888
+ function compareCapacityAdmissions(before, after, changed) {
9889
+ const count = (filled, rejected) => {
9890
+ const keys = new Set(
9891
+ filled.admissions.filter(
9892
+ (event) => event.strategy !== changed && event.status === "filled"
9893
+ ).map((event) => event.key)
9894
+ );
9895
+ return new Set(
9896
+ rejected.admissions.filter(
9897
+ (event) => event.status === "capacity-rejected" && keys.has(event.key)
9898
+ ).map((event) => event.key)
9899
+ ).size;
9900
+ };
9901
+ return { displaced: count(before, after), restored: count(after, before) };
9902
+ }
9903
+ function comparePortfolioReports(input, candidate) {
9904
+ if (!input || typeof input !== "object" || !("checksum" in input)) {
9905
+ throw new Error("Expected a checksum-bound portfolio report");
9906
+ }
9907
+ const { checksum, ...payload } = input;
9908
+ if (checksum !== (0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(payload)).digest("hex") || payload.schema !== "tradejs-portfolio-backtest/v1") {
9909
+ throw new Error("Control portfolio report checksum or schema mismatch");
9910
+ }
9911
+ const control = payload;
9912
+ if (control.lineage?.batchCount !== 1 || candidate.lineage.batchCount !== 1 || control.lineage.cacheOnly !== true || candidate.lineage.cacheOnly !== true) {
9913
+ throw new Error(
9914
+ "Portfolio comparison requires two cache-only reports from a single shared batch"
9915
+ );
9916
+ }
9917
+ for (const field of [
9918
+ "connector",
9919
+ "interval",
9920
+ "tickers",
9921
+ "candleCoverage",
9922
+ "binanceMarketContextRows",
9923
+ "maxLossValue"
9924
+ ]) {
9925
+ if (candidate.lineage[field] === void 0 || key(control.lineage[field]) !== key(candidate.lineage[field])) {
9926
+ throw new Error(`Portfolio comparison scope mismatch: ${field}`);
9927
+ }
9928
+ }
9929
+ if (key(control.window) !== key(candidate.window))
9930
+ throw new Error("Portfolio comparison window mismatch");
9931
+ const before = control.lineage.strategies;
9932
+ const after = candidate.lineage.strategies;
9933
+ if (!Array.isArray(before) || !Array.isArray(after) || key(before.map((s) => s.strategyName)) !== key(after.map((s) => s.strategyName))) {
9934
+ throw new Error(
9935
+ "Portfolio comparison requires the same ordered strategies"
9936
+ );
9937
+ }
9938
+ const changed = after.filter((s, index) => key(s) !== key(before[index]));
9939
+ if (changed.length > 1)
9940
+ throw new Error(
9941
+ "Portfolio comparison allows at most one strategy replacement"
9942
+ );
9943
+ if (after.some(
9944
+ (s, index) => s.maxLossValue !== before[index].maxLossValue || key(s.effectiveConfig.PORTFOLIO_POLICY) !== key(before[index].effectiveConfig.PORTFOLIO_POLICY)
9945
+ )) {
9946
+ throw new Error(
9947
+ "Portfolio comparison requires unchanged risk and portfolio policies"
9948
+ );
9949
+ }
9950
+ const all = (report) => {
9951
+ const row = report.metrics.portfolio.find(
9952
+ (r) => r.window === "full" && r.cohort === "ALL"
9953
+ );
9954
+ if (!row) throw new Error("Missing full ALL portfolio metrics");
9955
+ return row;
9956
+ };
9957
+ const a = all(control), b = all(candidate);
9958
+ const toPnl = (report) => report.trades.map((trade) => ({
9959
+ exitTimestamp: trade.exitedAt,
9960
+ netProfit: trade.pnl
9961
+ }));
9962
+ const strategyDelta = candidate.metrics.strategies.filter((row) => row.window === "full").map((row) => {
9963
+ const previous = control.metrics.strategies.find(
9964
+ (r) => r.window === row.window && r.scope === row.scope && r.cohort === row.cohort
9965
+ );
9966
+ return {
9967
+ strategy: row.scope,
9968
+ cohort: row.cohort,
9969
+ trades: previous ? row.n - previous.n : null,
9970
+ pnl: previous ? row.pnl - previous.pnl : null
9971
+ };
9972
+ });
9973
+ const markBefore = control.portfolioDiagnostics?.markedMaxDrawdown ?? null;
9974
+ const markAfter = candidate.portfolioDiagnostics?.markedMaxDrawdown ?? null;
9975
+ const displacement = control.portfolioDiagnostics && candidate.portfolioDiagnostics ? compareCapacityAdmissions(
9976
+ control.portfolioDiagnostics,
9977
+ candidate.portfolioDiagnostics,
9978
+ changed[0]?.strategyName
9979
+ ) : null;
9980
+ return {
9981
+ controlChecksum: checksum,
9982
+ markedDrawdown: {
9983
+ control: markBefore,
9984
+ candidate: markAfter,
9985
+ delta: markBefore === null || markAfter === null ? null : markAfter - markBefore
9986
+ },
9987
+ capacityDisplacement: displacement,
9988
+ changedStrategy: changed[0]?.strategyName ?? null,
9989
+ delta: {
9990
+ pnl: b.pnl - a.pnl,
9991
+ trades: b.n - a.n,
9992
+ realizedMaxDrawdown: b.realizedMaxDrawdown - a.realizedMaxDrawdown
9993
+ },
9994
+ recovery: {
9995
+ control: longestRecovery(control.curves.portfolioDrawdown),
9996
+ candidate: longestRecovery(candidate.curves.portfolioDrawdown)
9997
+ },
9998
+ strategyDelta,
9999
+ bootstrap: calendarClusterBootstrap({
10000
+ control: toPnl(control),
10001
+ candidate: toPnl(candidate),
10002
+ clusterDays: 7,
10003
+ iterations: 1e3,
10004
+ confidenceLevel: 0.95,
10005
+ seed: String(checksum),
10006
+ start: candidate.window.start,
10007
+ end: candidate.window.end
10008
+ }),
10009
+ limitations: [
10010
+ "Scope and recorded coverage match. Report lineage does not prove equality of every historical source byte.",
10011
+ "Marked drawdown uses last observed candle closes at replay cycle boundaries. It does not measure intra-candle excursions.",
10012
+ "Capacity displacement counts only identical signals of unchanged strategies filled in one run and explicitly rejected by the position cap in the other."
10013
+ ]
10014
+ };
10015
+ }
10016
+
10017
+ // src/lib/replay/portfolioReport.ts
10018
+ var DAY_MS5 = 864e5;
9694
10019
  var WINDOWS = [365, 180, 90, 30, 7];
9695
10020
  var COLORS = [
9696
10021
  "#2563eb",
@@ -9770,7 +10095,7 @@ var calculatePortfolioMetricRow = ({
9770
10095
  const pnl = selected.reduce((sum, trade) => sum + trade.pnl, 0);
9771
10096
  const wins = selected.filter((trade) => trade.pnl > 0).length;
9772
10097
  const losses = selected.filter((trade) => trade.pnl <= 0).length;
9773
- const days = Math.max(1, (end - start) / DAY_MS4);
10098
+ const days = Math.max(1, (end - start) / DAY_MS5);
9774
10099
  return {
9775
10100
  window,
9776
10101
  scope,
@@ -9790,7 +10115,7 @@ var windowBounds = (start, end) => [
9790
10115
  { label: "full", start, end },
9791
10116
  ...WINDOWS.map((days) => ({
9792
10117
  label: `${days}d`,
9793
- start: Math.max(start, end - days * DAY_MS4),
10118
+ start: Math.max(start, end - days * DAY_MS5),
9794
10119
  end
9795
10120
  }))
9796
10121
  ];
@@ -9865,10 +10190,11 @@ var writePortfolioReport = async ({
9865
10190
  replayResult,
9866
10191
  window,
9867
10192
  lineage,
9868
- command
10193
+ command,
10194
+ compareTo
9869
10195
  }) => {
9870
- const outputDir = import_node_path6.default.resolve(projectRoot, "data/replay/output");
9871
- await import_promises4.default.mkdir(outputDir, { recursive: true });
10196
+ const outputDir = import_node_path8.default.resolve(projectRoot, "data/replay/output");
10197
+ await import_promises5.default.mkdir(outputDir, { recursive: true });
9872
10198
  const trades = replayResult.strategies.flatMap(
9873
10199
  (strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
9874
10200
  );
@@ -9940,13 +10266,13 @@ var writePortfolioReport = async ({
9940
10266
  });
9941
10267
  const prefix = `${timestamp}-portfolio-backtest`;
9942
10268
  const paths = {
9943
- markdown: import_node_path6.default.join(outputDir, `${prefix}.md`),
9944
- json: import_node_path6.default.join(outputDir, `${prefix}.json`),
9945
- equitySvg: import_node_path6.default.join(outputDir, `${prefix}-equity.svg`),
9946
- drawdownSvg: import_node_path6.default.join(outputDir, `${prefix}-drawdown.svg`),
9947
- html: import_node_path6.default.join(outputDir, `${prefix}.html`)
10269
+ markdown: import_node_path8.default.join(outputDir, `${prefix}.md`),
10270
+ json: import_node_path8.default.join(outputDir, `${prefix}.json`),
10271
+ equitySvg: import_node_path8.default.join(outputDir, `${prefix}-equity.svg`),
10272
+ drawdownSvg: import_node_path8.default.join(outputDir, `${prefix}-drawdown.svg`),
10273
+ html: import_node_path8.default.join(outputDir, `${prefix}.html`)
9948
10274
  };
9949
- const report = {
10275
+ const baseReport = {
9950
10276
  schema: "tradejs-portfolio-backtest/v1",
9951
10277
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
9952
10278
  command,
@@ -9968,9 +10294,17 @@ var writePortfolioReport = async ({
9968
10294
  portfolioDrawdown: drawdownCurve(portfolioEquity),
9969
10295
  strategies: strategySeries.map(({ name, points }) => ({ name, points }))
9970
10296
  },
9971
- trades
10297
+ trades,
10298
+ ...replayResult.portfolioDiagnostics ? { portfolioDiagnostics: replayResult.portfolioDiagnostics } : {}
9972
10299
  };
9973
- const checksum = (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
10300
+ const comparison = compareTo ? comparePortfolioReports(
10301
+ JSON.parse(
10302
+ await import_promises5.default.readFile(import_node_path8.default.resolve(projectRoot, compareTo), "utf8")
10303
+ ),
10304
+ baseReport
10305
+ ) : void 0;
10306
+ const report = { ...baseReport, ...comparison ? { comparison } : {} };
10307
+ const checksum = (0, import_node_crypto6.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
9974
10308
  const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
9975
10309
  const markdown = `# Portfolio Backtest
9976
10310
 
@@ -9982,9 +10316,9 @@ var writePortfolioReport = async ({
9982
10316
  - Report checksum: \`${checksum}\`
9983
10317
  - Command: \`${command}\`
9984
10318
 
9985
- ![Strategy PnL overlay](./${import_node_path6.default.basename(paths.equitySvg)})
10319
+ ![Strategy PnL overlay](./${import_node_path8.default.basename(paths.equitySvg)})
9986
10320
 
9987
- ![Portfolio drawdown](./${import_node_path6.default.basename(paths.drawdownSvg)})
10321
+ ![Portfolio drawdown](./${import_node_path8.default.basename(paths.drawdownSvg)})
9988
10322
 
9989
10323
  ## Portfolio metrics
9990
10324
 
@@ -9995,17 +10329,33 @@ ${markdownTable(portfolioRows)}
9995
10329
  ${markdownTable(strategyRows)}
9996
10330
  `;
9997
10331
  const html = `<!doctype html><html><head><meta charset="utf-8"><title>Portfolio Backtest</title><style>body{font-family:Inter,system-ui,sans-serif;margin:32px;color:#0f172a}h1,h2{margin-top:32px}.meta{color:#475569}table{border-collapse:collapse;width:100%;font-size:13px}th,td{padding:8px;border:1px solid #e2e8f0;text-align:right}th:nth-child(-n+3),td:nth-child(-n+3){text-align:left}.chart{overflow:auto;border:1px solid #e2e8f0;margin:16px 0}</style></head><body><h1>Portfolio Backtest</h1><p class="meta">${escapeHtml(report.window.startIso)} .. ${escapeHtml(report.window.endIso)} \xB7 ${report.counts.strategies} strategies \xB7 ${trades.length} completed trades \xB7 MAX_LOSS_VALUE=${escapeHtml(String(lineage.maxLossValue))}</p><p class="meta">${escapeHtml(semantics)}</p><div class="chart">${equitySvg}</div><div class="chart">${drawdownSvg}</div><h2>Portfolio metrics</h2>${metricTable(portfolioRows)}<h2>Strategy metrics</h2>${metricTable(strategyRows)}<h2>Lineage</h2><pre>${escapeHtml(JSON.stringify(lineage, null, 2))}</pre></body></html>`;
10332
+ const comparisonMarkdown = comparison ? `
10333
+ ## Paired portfolio comparison
10334
+
10335
+ - Net PnL delta: ${display(comparison.delta.pnl)}
10336
+ - Closed trades delta: ${comparison.delta.trades}
10337
+ - Realized MaxDD delta: ${display(comparison.delta.realizedMaxDrawdown)}
10338
+
10339
+ ${comparison.limitations.join(" ")}
10340
+ ` : "";
9998
10341
  await Promise.all([
9999
- import_promises4.default.writeFile(paths.markdown, markdown, "utf8"),
10000
- import_promises4.default.writeFile(
10342
+ import_promises5.default.writeFile(paths.markdown, markdown + comparisonMarkdown, "utf8"),
10343
+ import_promises5.default.writeFile(
10001
10344
  paths.json,
10002
10345
  `${JSON.stringify({ ...report, checksum }, null, 2)}
10003
10346
  `,
10004
10347
  "utf8"
10005
10348
  ),
10006
- import_promises4.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
10007
- import_promises4.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
10008
- import_promises4.default.writeFile(paths.html, html, "utf8")
10349
+ import_promises5.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
10350
+ import_promises5.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
10351
+ import_promises5.default.writeFile(
10352
+ paths.html,
10353
+ comparison ? html.replace(
10354
+ "</body>",
10355
+ `<h2>Paired portfolio comparison</h2><pre>${escapeHtml(JSON.stringify(comparison, null, 2))}</pre></body>`
10356
+ ) : html,
10357
+ "utf8"
10358
+ )
10009
10359
  ]);
10010
10360
  return { ...paths, checksum };
10011
10361
  };
@@ -10048,8 +10398,8 @@ var cachedTickerSymbols = async ({
10048
10398
  import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, "default"),
10049
10399
  import_redis6.redisKeys.tickerUniverse(replayUserName, name)
10050
10400
  ]);
10051
- for (const key of [...new Set(keys)]) {
10052
- const cached = await (0, import_redis6.getData)(key, null);
10401
+ for (const key2 of [...new Set(keys)]) {
10402
+ const cached = await (0, import_redis6.getData)(key2, null);
10053
10403
  const symbols = (cached?.tickers ?? []).map(
10054
10404
  (ticker) => String(ticker.symbol ?? "").trim().toUpperCase()
10055
10405
  ).filter(Boolean);
@@ -10118,7 +10468,8 @@ var finishReplay = async ({
10118
10468
  replayResult,
10119
10469
  window,
10120
10470
  lineage: portfolioLineage ?? {},
10121
- command: process.argv.join(" ")
10471
+ command: process.argv.join(" "),
10472
+ compareTo: replayFlags.portfolioCompareTo
10122
10473
  }) : null;
10123
10474
  if (portfolioReport) {
10124
10475
  console.log(import_chalk13.default.green(`Portfolio report: ${portfolioReport.html}`));
@@ -10402,6 +10753,18 @@ var replayBacktest = async () => {
10402
10753
  `Invalid --portfolioBatchSize: ${String(replayFlags.portfolioBatchSize)}`
10403
10754
  );
10404
10755
  }
10756
+ if (replayFlags.portfolioCompareTo && (!replayFlags.portfolioReport || !replayFlags.cacheOnly)) {
10757
+ throw new Error(
10758
+ "--portfolioCompareTo requires --portfolioReport and --cacheOnly"
10759
+ );
10760
+ }
10761
+ if (replayFlags.portfolioReport && preparedRun.tickers.length > portfolioBatchSize && (replayFlags.portfolioCompareTo || replayStrategies.some(
10762
+ ({ strategyConfig }) => Boolean(strategyConfig.PORTFOLIO_POLICY)
10763
+ ))) {
10764
+ throw new Error(
10765
+ "Portfolio policies and comparisons require a single shared batch; set --portfolioBatchSize to at least the number of tickers"
10766
+ );
10767
+ }
10405
10768
  const runtimeStrategies = replayFlags.tickers && !replayFlags.portfolioReport ? replayStrategies.map(
10406
10769
  ({ selection: _selection, ...strategy }) => strategy
10407
10770
  ) : replayStrategies;
@@ -10425,6 +10788,9 @@ var replayBacktest = async () => {
10425
10788
  showProgress: !replayFlags.portfolioReport,
10426
10789
  collectSkipEvidence: !replayFlags.portfolioReport,
10427
10790
  collectSignals: !replayFlags.portfolioReport,
10791
+ collectPortfolioDiagnostics: Boolean(
10792
+ replayFlags.portfolioReport && tickerBatches.length === 1
10793
+ ),
10428
10794
  decisionTraceTargets: replayFlags.portfolioReport ? void 0 : evidenceMetadata?.decisionTraceTargets
10429
10795
  });
10430
10796
  return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
@@ -10462,6 +10828,9 @@ var replayBacktest = async () => {
10462
10828
  cacheOnly: Boolean(replayFlags.cacheOnly),
10463
10829
  portfolioSemantics: "production strategy order and one concurrent position per symbol; fixed-risk symbol batches are merged by canonical realized PnL",
10464
10830
  portfolioBatchSize,
10831
+ batchCount: tickerBatches.length,
10832
+ connector: preparedRun.connectorName,
10833
+ interval: replayInterval,
10465
10834
  tickers: preparedRun.tickers,
10466
10835
  candleCoverage: candleCoverage ?? null,
10467
10836
  binanceMarketContextRows: portfolioMarketContextRows ?? null,