@tradejs/cli 3.1.29-beta.290 → 3.1.29-beta.292

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
  };
@@ -596,9 +596,9 @@ var loadRuntimeTrades = async (userName, {
596
596
  const tradePrefix = import_redis.redisKeys.runtimeTrades(userName);
597
597
  const bucketPrefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
598
598
  const keys = (await (0, import_redis.getKeys)(tradePrefix)).filter(
599
- (key) => !key.startsWith(bucketPrefix)
599
+ (key2) => !key2.startsWith(bucketPrefix)
600
600
  );
601
- const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
601
+ const trades = await Promise.all(keys.map((key2) => (0, import_redis.getData)(key2, null)));
602
602
  return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
603
603
  };
604
604
 
@@ -1885,8 +1885,8 @@ var backfillBinanceMarketContext = async (params, enabled) => {
1885
1885
  })
1886
1886
  );
1887
1887
  const missingChunks = chunks.filter((chunk) => {
1888
- const key = `${chunk.startMs}:${chunk.endMs}`;
1889
- return plans.some((plan) => plan.missingChunkKeys.has(key));
1888
+ const key2 = `${chunk.startMs}:${chunk.endMs}`;
1889
+ return plans.some((plan) => plan.missingChunkKeys.has(key2));
1890
1890
  });
1891
1891
  const bar = new import_progress.default(
1892
1892
  "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
@@ -2242,13 +2242,13 @@ var networkErrorCodes = /* @__PURE__ */ new Set([
2242
2242
  "EAI_AGAIN",
2243
2243
  "EHOSTUNREACH"
2244
2244
  ]);
2245
- var getNestedErrorValue = (error, key) => {
2245
+ var getNestedErrorValue = (error, key2) => {
2246
2246
  let current = error;
2247
2247
  const seen = /* @__PURE__ */ new Set();
2248
2248
  while (current && typeof current === "object" && !seen.has(current)) {
2249
2249
  seen.add(current);
2250
2250
  const record = current;
2251
- const value = record[key];
2251
+ const value = record[key2];
2252
2252
  if (typeof value === "string" && value.trim()) {
2253
2253
  return value.trim();
2254
2254
  }
@@ -2381,11 +2381,11 @@ var selectBestMarket = (candidates, exchangePriority) => {
2381
2381
  var buildMatches = (tickers, markets, exchangePriority) => {
2382
2382
  const marketByTicker = /* @__PURE__ */ new Map();
2383
2383
  for (const market of markets) {
2384
- const key = String(market.symbol_on_exchange || "").trim().toUpperCase();
2385
- if (!key) continue;
2386
- const list = marketByTicker.get(key) ?? [];
2384
+ const key2 = String(market.symbol_on_exchange || "").trim().toUpperCase();
2385
+ if (!key2) continue;
2386
+ const list = marketByTicker.get(key2) ?? [];
2387
2387
  list.push(market);
2388
- marketByTicker.set(key, list);
2388
+ marketByTicker.set(key2, list);
2389
2389
  }
2390
2390
  const matches = [];
2391
2391
  const unmatched = [];
@@ -2717,7 +2717,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2717
2717
  while (cursor < window.toMs) {
2718
2718
  const toMs = Math.min(window.toMs, cursor + batchMs);
2719
2719
  const missingRanges = batch.map((item) => {
2720
- const key = coverageKey({
2720
+ const key2 = coverageKey({
2721
2721
  symbol: item.symbol,
2722
2722
  interval,
2723
2723
  fromMs: cursor,
@@ -2732,7 +2732,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2732
2732
  fromMs: cursor,
2733
2733
  toMs,
2734
2734
  nowMs: coverageNowMs,
2735
- dataCoverageKeyExists: coverageKeys.has(key),
2735
+ dataCoverageKeyExists: coverageKeys.has(key2),
2736
2736
  dataCoverageRanges: coverageRangesBySymbol.get(normalizedSymbol),
2737
2737
  liquidationCoverageRanges: liquidationCoverageRangesBySymbol.get(normalizedSymbol),
2738
2738
  edges: edgesBySymbol.get(normalizedSymbol)
@@ -3031,7 +3031,7 @@ var toIntOrNull = (value) => {
3031
3031
  return numeric == null ? null : Math.trunc(numeric);
3032
3032
  };
3033
3033
  var getRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3034
- var getNestedRecord = (value, key) => getRecord(value[key]);
3034
+ var getNestedRecord = (value, key2) => getRecord(value[key2]);
3035
3035
  var safeDivide2 = (numerator, denominator) => numerator != null && denominator != null && denominator > 0 ? numerator / denominator : null;
3036
3036
  var sumFinite = (values) => values.reduce((sum, value) => sum + (value ?? 0), 0);
3037
3037
  var toExchangeLiquidityRegime = ({
@@ -3138,7 +3138,7 @@ var coverageRowsToKeySet = (rows) => new Set(
3138
3138
  );
3139
3139
  var hasBackfillCoverage = (coverageKeys, params) => coverageKeys.has(coverageKey2(params));
3140
3140
  var hasRecentBackfillCoverage = (rows, params) => {
3141
- const key = coverageKey2(params);
3141
+ const key2 = coverageKey2(params);
3142
3142
  return rows.some((row) => {
3143
3143
  if (!isMarketContextBackfillCoverageDataBearing(row)) return false;
3144
3144
  if (coverageKey2({
@@ -3147,7 +3147,7 @@ var hasRecentBackfillCoverage = (rows, params) => {
3147
3147
  interval: row.interval,
3148
3148
  fromMs: row.fromMs,
3149
3149
  toMs: row.toMs
3150
- }) !== key) {
3150
+ }) !== key2) {
3151
3151
  return false;
3152
3152
  }
3153
3153
  const checkedAtMs = Number(row.checkedAtMs);
@@ -3184,27 +3184,27 @@ var createCoinMarketCapFetchStats = () => ({
3184
3184
  totalCredits: 0,
3185
3185
  byPath: /* @__PURE__ */ new Map()
3186
3186
  });
3187
- var getCoinMarketCapPathLabel = (path7) => {
3188
- if (path7.includes("global-metrics")) return "global";
3189
- if (path7.includes("cryptocurrency/quotes")) return "reference";
3190
- if (path7.includes("exchange/quotes")) return "exchangeLiquidity";
3191
- if (path7.includes("fear-and-greed")) return "fearGreed";
3192
- if (path7.includes("cmc100")) return "cmc100";
3193
- if (path7.includes("cmc20")) return "cmc20";
3194
- return path7;
3195
- };
3196
- var recordCoinMarketCapFetchStats = (stats, path7, creditCount) => {
3187
+ var getCoinMarketCapPathLabel = (path9) => {
3188
+ if (path9.includes("global-metrics")) return "global";
3189
+ if (path9.includes("cryptocurrency/quotes")) return "reference";
3190
+ if (path9.includes("exchange/quotes")) return "exchangeLiquidity";
3191
+ if (path9.includes("fear-and-greed")) return "fearGreed";
3192
+ if (path9.includes("cmc100")) return "cmc100";
3193
+ if (path9.includes("cmc20")) return "cmc20";
3194
+ return path9;
3195
+ };
3196
+ var recordCoinMarketCapFetchStats = (stats, path9, creditCount) => {
3197
3197
  if (!stats) return;
3198
- const existing = stats.byPath.get(path7) ?? { requests: 0, credits: 0 };
3198
+ const existing = stats.byPath.get(path9) ?? { requests: 0, credits: 0 };
3199
3199
  existing.requests += 1;
3200
3200
  existing.credits += creditCount ?? 0;
3201
3201
  stats.requests += 1;
3202
3202
  stats.totalCredits += creditCount ?? 0;
3203
- stats.byPath.set(path7, existing);
3203
+ stats.byPath.set(path9, existing);
3204
3204
  };
3205
3205
  var formatCoinMarketCapFetchStats = (stats) => {
3206
3206
  const byPath = [...stats.byPath.entries()].map(
3207
- ([path7, item]) => `${getCoinMarketCapPathLabel(path7)}=${item.requests}req/${item.credits}cr`
3207
+ ([path9, item]) => `${getCoinMarketCapPathLabel(path9)}=${item.requests}req/${item.credits}cr`
3208
3208
  ).join(" ");
3209
3209
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
3210
3210
  };
@@ -3226,8 +3226,8 @@ var logBackfillSourceDone = (params) => {
3226
3226
  var markBackfillCoverage = (rows) => (0, import_marketContext2.upsertMarketContextBackfillCoverage)(rows);
3227
3227
  var coinMarketCapFetch = async (params) => {
3228
3228
  const url = new URL(`${getBaseUrl()}${params.path}`);
3229
- for (const [key, value] of Object.entries(params.searchParams)) {
3230
- url.searchParams.set(key, value);
3229
+ for (const [key2, value] of Object.entries(params.searchParams)) {
3230
+ url.searchParams.set(key2, value);
3231
3231
  }
3232
3232
  for (let attempt = 0; attempt <= getMaxRetries(); attempt += 1) {
3233
3233
  const waitMs = Math.max(
@@ -3408,10 +3408,10 @@ var coinMarketCapExchangeQuotesPayloadToLiquidityRows = (payload, interval = "1d
3408
3408
  const ts = typeof tsRaw === "string" ? new Date(tsRaw) : null;
3409
3409
  if (!ts || Number.isNaN(ts.getTime())) continue;
3410
3410
  const volumeUsd = toFiniteNumberOrNull(usd.volume_24h) ?? toFiniteNumberOrNull(usd.volume_24h_adjusted) ?? toFiniteNumberOrNull(usd.spot_volume_usd) ?? toFiniteNumberOrNull(usd.reported_volume_24h);
3411
- const key = ts.getTime();
3412
- const rows = rowsByTs.get(key) ?? [];
3411
+ const key2 = ts.getTime();
3412
+ const rows = rowsByTs.get(key2) ?? [];
3413
3413
  rows.push({ slug, name, volumeUsd });
3414
- rowsByTs.set(key, rows);
3414
+ rowsByTs.set(key2, rows);
3415
3415
  }
3416
3416
  }
3417
3417
  return [...rowsByTs.entries()].sort(([left], [right]) => left - right).map(([ts, exchanges]) => {
@@ -4814,6 +4814,10 @@ import_args.default.option(
4814
4814
  "Write aggregate portfolio metrics and SVG charts; skip runtime/exchange comparison",
4815
4815
  false
4816
4816
  );
4817
+ import_args.default.option(
4818
+ "portfolioCompareTo",
4819
+ "Compare a full shared replay with a prior portfolio report JSON"
4820
+ );
4817
4821
  import_args.default.option(
4818
4822
  ["B", "portfolioBatchSize"],
4819
4823
  "Maximum number of symbols held in memory by one portfolio replay batch",
@@ -4859,7 +4863,7 @@ var normalizeForStableJson = (value) => {
4859
4863
  }
4860
4864
  if (value && typeof value === "object") {
4861
4865
  return Object.fromEntries(
4862
- Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
4866
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key2, entry]) => [key2, normalizeForStableJson(entry)])
4863
4867
  );
4864
4868
  }
4865
4869
  return value;
@@ -5017,9 +5021,9 @@ var buildRuntimeEvidenceLineageScopes = ({
5017
5021
  if (!lineage || !Number.isFinite(timestamp)) {
5018
5022
  return;
5019
5023
  }
5020
- const key = `${deploymentId ?? "default"}::${accountId ?? "default"}::${strategy}::${symbol}::${runtimeLineageKey(lineage)}`;
5021
- const existing = scopes.get(key);
5022
- scopes.set(key, {
5024
+ const key2 = `${deploymentId ?? "default"}::${accountId ?? "default"}::${strategy}::${symbol}::${runtimeLineageKey(lineage)}`;
5025
+ const existing = scopes.get(key2);
5026
+ scopes.set(key2, {
5023
5027
  strategy,
5024
5028
  symbol,
5025
5029
  ...deploymentId ? { deploymentId } : {},
@@ -5182,12 +5186,16 @@ var getPositionPnlSnapshot = ({
5182
5186
  direction: position.direction
5183
5187
  };
5184
5188
  };
5185
- var createPortfolioReplayConnector = (connector) => {
5189
+ var createPortfolioReplayConnector = (connector, observeEquity = false) => {
5186
5190
  let state = {};
5187
5191
  let amount = import_constants3.INITIAL_BACKTEST_AMOUNT;
5188
5192
  let executionTimestamp = null;
5189
5193
  const positionsBySymbol = /* @__PURE__ */ new Map();
5190
5194
  const currentPriceBySymbol = /* @__PURE__ */ new Map();
5195
+ const equityMarks = observeEquity ? /* @__PURE__ */ new Map() : void 0;
5196
+ const markedAtBySymbol = /* @__PURE__ */ new Map();
5197
+ const entryFacts = /* @__PURE__ */ new Map();
5198
+ let portfolioObserved = false;
5191
5199
  const orderLog = [];
5192
5200
  const positionLog = [];
5193
5201
  const orderLogByStrategy = /* @__PURE__ */ new Map();
@@ -5425,14 +5433,51 @@ var createPortfolioReplayConnector = (connector) => {
5425
5433
  const { strategyName: _strategyName, ...nextPosition } = position;
5426
5434
  return nextPosition;
5427
5435
  }),
5436
+ getPortfolioObservation: async (asOf, reservations) => {
5437
+ portfolioObserved = true;
5438
+ const pendingIds = new Set(reservations.map((r) => r.id));
5439
+ for (const id of entryFacts.keys())
5440
+ if (!pendingIds.has(id)) entryFacts.delete(id);
5441
+ return {
5442
+ asOf,
5443
+ positions: Array.from(positionsBySymbol.values()).map(
5444
+ ({ position, stopLossPrice }) => ({
5445
+ id: position.orderId ?? `${position.strategyName}:${position.symbol}:${position.timestamp}`,
5446
+ strategy: position.strategyName === UNKNOWN_STRATEGY ? null : position.strategyName,
5447
+ symbol: position.symbol,
5448
+ direction: position.direction,
5449
+ qty: position.qty,
5450
+ entryPrice: position.price,
5451
+ markPrice: currentPriceBySymbol.get(position.symbol) ?? null,
5452
+ markedAt: markedAtBySymbol.get(position.symbol) ?? null,
5453
+ stopLossPrice
5454
+ })
5455
+ ),
5456
+ orders: reservations.map(
5457
+ (r) => entryFacts.get(r.id) ?? {
5458
+ id: r.id,
5459
+ status: "pending",
5460
+ filledQty: 0
5461
+ }
5462
+ )
5463
+ };
5464
+ },
5428
5465
  getOpenPositionPnl: async () => Array.from(positionsBySymbol.values()).map(
5429
- ({ position }) => getPositionPnlSnapshot({
5430
- position,
5431
- currentPrice: currentPriceBySymbol.get(position.symbol) ?? position.price
5466
+ ({ position, stopLossPrice }) => ({
5467
+ ...getPositionPnlSnapshot({
5468
+ position,
5469
+ currentPrice: currentPriceBySymbol.get(position.symbol) ?? position.price
5470
+ }),
5471
+ observedAt: markedAtBySymbol.get(position.symbol),
5472
+ stopLossPrice
5432
5473
  })
5433
5474
  ),
5434
5475
  placeOrder: async (order) => {
5435
- const { beforeSubmit, ...submittedOrder } = order;
5476
+ const {
5477
+ beforeSubmit,
5478
+ onRejected: _onRejected,
5479
+ ...submittedOrder
5480
+ } = order;
5436
5481
  if (positionsBySymbol.has(order.symbol)) {
5437
5482
  return false;
5438
5483
  }
@@ -5476,6 +5521,14 @@ var createPortfolioReplayConnector = (connector) => {
5476
5521
  stopLossPrice: null
5477
5522
  };
5478
5523
  positionsBySymbol.set(order.symbol, positionState);
5524
+ markedAtBySymbol.set(order.symbol, executionTimestamp ?? order.timestamp);
5525
+ if (portfolioObserved && order.orderId)
5526
+ entryFacts.set(order.orderId, {
5527
+ id: order.orderId,
5528
+ status: "filled",
5529
+ filledQty: order.qty,
5530
+ positionId: order.orderId
5531
+ });
5479
5532
  logOrder({
5480
5533
  positionState,
5481
5534
  data: {
@@ -5559,11 +5612,26 @@ var createPortfolioReplayConnector = (connector) => {
5559
5612
  getTickers: connector.getTickers,
5560
5613
  advanceMarket: async ({ symbol, candle }) => {
5561
5614
  executionTimestamp = candle.timestamp;
5615
+ markedAtBySymbol.set(symbol, candle.timestamp);
5616
+ equityMarks?.set(symbol, candle.close);
5562
5617
  currentPriceBySymbol.set(symbol, candle.close);
5563
5618
  await checkSl({ symbol, candle });
5564
5619
  await checkTp({ symbol, candle });
5565
5620
  currentPriceBySymbol.set(symbol, candle.close);
5566
5621
  },
5622
+ getMarkedEquity: () => {
5623
+ if (!equityMarks) return null;
5624
+ let equity = amount - import_constants3.INITIAL_BACKTEST_AMOUNT;
5625
+ for (const { position } of positionsBySymbol.values()) {
5626
+ const mark = equityMarks.get(position.symbol);
5627
+ if (mark == null || !Number.isFinite(mark)) return null;
5628
+ equity += getPositionPnlSnapshot({
5629
+ position,
5630
+ currentPrice: mark
5631
+ }).unrealizedPnl;
5632
+ }
5633
+ return (0, import_math.round)(equity);
5634
+ },
5567
5635
  getReplayArtifacts: () => ({
5568
5636
  orderLog,
5569
5637
  positionLog,
@@ -5777,6 +5845,21 @@ var executeHistoricalReplay = async (context, adapters) => {
5777
5845
  context.collectSkipEvidence
5778
5846
  );
5779
5847
  const decisionTrace = createReplayDecisionTrace(context.decisionTraceTargets);
5848
+ const portfolioDiagnostics = context.collectPortfolioDiagnostics ? { markedMaxDrawdown: 0, unpricedCycles: 0, admissions: [] } : void 0;
5849
+ let equityPeak = 0;
5850
+ const observeEquity = () => {
5851
+ if (!portfolioDiagnostics) return;
5852
+ const equity = context.connector.getMarkedEquity();
5853
+ if (equity === null) {
5854
+ portfolioDiagnostics.unpricedCycles++;
5855
+ return;
5856
+ }
5857
+ equityPeak = Math.max(equityPeak, equity);
5858
+ portfolioDiagnostics.markedMaxDrawdown = Math.max(
5859
+ portfolioDiagnostics.markedMaxDrawdown ?? 0,
5860
+ equityPeak - equity
5861
+ );
5862
+ };
5780
5863
  const reportProgress = (timestamp) => {
5781
5864
  if (adapters.progress.enabled === false) return;
5782
5865
  adapters.progress.tick({
@@ -5820,6 +5903,7 @@ var executeHistoricalReplay = async (context, adapters) => {
5820
5903
  }
5821
5904
  }
5822
5905
  advanceAbortedCycle(cycleSymbols, timestamp);
5906
+ observeEquity();
5823
5907
  reportProgress(timestamp);
5824
5908
  continue;
5825
5909
  }
@@ -5863,6 +5947,21 @@ var executeHistoricalReplay = async (context, adapters) => {
5863
5947
  result
5864
5948
  });
5865
5949
  if (result && typeof result !== "string") {
5950
+ if (portfolioDiagnostics) {
5951
+ const status = result.orderFailureReason === "PORTFOLIO_POSITION_LIMIT" ? "capacity-rejected" : result.orderStatus === "completed" ? "filled" : void 0;
5952
+ if (status)
5953
+ portfolioDiagnostics.admissions.push({
5954
+ strategy: strategyRuntime.strategyName,
5955
+ status,
5956
+ key: JSON.stringify([
5957
+ strategyRuntime.strategyName,
5958
+ result.symbol,
5959
+ result.direction,
5960
+ result.timestamp,
5961
+ result.signalId
5962
+ ])
5963
+ });
5964
+ }
5866
5965
  result.runtimeLineage = strategyRuntime.runtimeLineage;
5867
5966
  await adapters.enrichSignal(result);
5868
5967
  cycleSignals.push(result);
@@ -5877,12 +5976,16 @@ var executeHistoricalReplay = async (context, adapters) => {
5877
5976
  status: "completed",
5878
5977
  durationMs: adapters.clock.now() - cycleStartedAt
5879
5978
  });
5979
+ observeEquity();
5880
5980
  reportProgress(timestamp);
5881
5981
  }
5882
5982
  } finally {
5883
5983
  releaseHistoricalReplayCaches(context.plan, adapters);
5884
5984
  }
5985
+ if (portfolioDiagnostics?.unpricedCycles)
5986
+ portfolioDiagnostics.markedMaxDrawdown = null;
5885
5987
  return {
5988
+ ...portfolioDiagnostics ? { portfolioDiagnostics } : {},
5886
5989
  signals,
5887
5990
  abortedCycles,
5888
5991
  replayLineageScopes: skipEvidence.values(),
@@ -5938,6 +6041,7 @@ var collectHistoricalReplayResult = (context) => ({
5938
6041
  abortedCycles: context.abortedCycles,
5939
6042
  runtimeLineages: context.runtimeLineages,
5940
6043
  replayLineageScopes: context.replayLineageScopes,
6044
+ ...context.portfolioDiagnostics ? { portfolioDiagnostics: context.portfolioDiagnostics } : {},
5941
6045
  ...context.replayDecisionTraces?.length ? { replayDecisionTraces: context.replayDecisionTraces } : {}
5942
6046
  });
5943
6047
  var compactHistoricalReplayResultForPortfolio = (result) => ({
@@ -6139,6 +6243,7 @@ var runHistoricalSignalsReplay = async ({
6139
6243
  showProgress = true,
6140
6244
  collectSkipEvidence = true,
6141
6245
  collectSignals = true,
6246
+ collectPortfolioDiagnostics = false,
6142
6247
  decisionTraceTargets
6143
6248
  }) => {
6144
6249
  const memory = createReplayMemoryProfile();
@@ -6148,7 +6253,8 @@ var runHistoricalSignalsReplay = async ({
6148
6253
  const projectHooks = projectConfig.hooks;
6149
6254
  const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
6150
6255
  const replayConnector = createPortfolioReplayConnector(
6151
- preparedRun.marketConnector
6256
+ preparedRun.marketConnector,
6257
+ collectPortfolioDiagnostics
6152
6258
  );
6153
6259
  const connectorName = getConnectorName(preparedRun.marketConnector);
6154
6260
  const referenceData = references ?? await loadHistoricalReplayReferences({ preparedRun, interval });
@@ -6226,6 +6332,7 @@ var runHistoricalSignalsReplay = async ({
6226
6332
  connector: replayConnector,
6227
6333
  collectSkipEvidence,
6228
6334
  collectSignals,
6335
+ collectPortfolioDiagnostics,
6229
6336
  decisionTraceTargets,
6230
6337
  hooks: projectHooks,
6231
6338
  hookContext
@@ -6264,7 +6371,8 @@ var runHistoricalSignalsReplay = async ({
6264
6371
  abortedCycles: execution.abortedCycles,
6265
6372
  runtimeLineages: plan.runtimeLineages,
6266
6373
  replayLineageScopes: execution.replayLineageScopes,
6267
- replayDecisionTraces: execution.replayDecisionTraces
6374
+ replayDecisionTraces: execution.replayDecisionTraces,
6375
+ portfolioDiagnostics: execution.portfolioDiagnostics
6268
6376
  });
6269
6377
  memory?.("result:collected");
6270
6378
  return result;
@@ -6511,16 +6619,16 @@ var findRuntimeDuplicateGroups = (entries) => {
6511
6619
  if (entry.source !== "runtime") {
6512
6620
  continue;
6513
6621
  }
6514
- const key = toRuntimeDuplicateKey(entry);
6515
- const bucket = groups.get(key) ?? [];
6622
+ const key2 = toRuntimeDuplicateKey(entry);
6623
+ const bucket = groups.get(key2) ?? [];
6516
6624
  bucket.push(entry);
6517
- groups.set(key, bucket);
6625
+ groups.set(key2, bucket);
6518
6626
  }
6519
- return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key, group]) => {
6627
+ return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key2, group]) => {
6520
6628
  const sortedGroup = [...group].sort(sortTradeParityEntries);
6521
6629
  const first = sortedGroup[0];
6522
6630
  return {
6523
- key,
6631
+ key: key2,
6524
6632
  strategy: first.strategy,
6525
6633
  symbol: first.symbol,
6526
6634
  direction: first.direction,
@@ -6559,26 +6667,26 @@ var compareTradeParityEntries = ({
6559
6667
  const groupedRuntime = /* @__PURE__ */ new Map();
6560
6668
  const groupedBacktest = /* @__PURE__ */ new Map();
6561
6669
  for (const entry of runtimeEntries) {
6562
- const key = toGroupKey(entry);
6563
- const bucket = groupedRuntime.get(key) ?? [];
6670
+ const key2 = toGroupKey(entry);
6671
+ const bucket = groupedRuntime.get(key2) ?? [];
6564
6672
  bucket.push(entry);
6565
- groupedRuntime.set(key, bucket);
6673
+ groupedRuntime.set(key2, bucket);
6566
6674
  }
6567
6675
  for (const entry of backtestEntries) {
6568
- const key = toGroupKey(entry);
6569
- const bucket = groupedBacktest.get(key) ?? [];
6676
+ const key2 = toGroupKey(entry);
6677
+ const bucket = groupedBacktest.get(key2) ?? [];
6570
6678
  bucket.push(entry);
6571
- groupedBacktest.set(key, bucket);
6679
+ groupedBacktest.set(key2, bucket);
6572
6680
  }
6573
6681
  const groupKeys = /* @__PURE__ */ new Set([
6574
6682
  ...groupedRuntime.keys(),
6575
6683
  ...groupedBacktest.keys()
6576
6684
  ]);
6577
- for (const key of groupKeys) {
6578
- const runtimeGroup = [...groupedRuntime.get(key) ?? []].sort(
6685
+ for (const key2 of groupKeys) {
6686
+ const runtimeGroup = [...groupedRuntime.get(key2) ?? []].sort(
6579
6687
  (left, right) => left.timestamp - right.timestamp
6580
6688
  );
6581
- const backtestGroup = [...groupedBacktest.get(key) ?? []].sort(
6689
+ const backtestGroup = [...groupedBacktest.get(key2) ?? []].sort(
6582
6690
  (left, right) => left.timestamp - right.timestamp
6583
6691
  );
6584
6692
  const unmatchedBacktest = backtestGroup.map((entry) => ({
@@ -6819,8 +6927,8 @@ var filterBucketKeysByWindow = (bucketKeys, prefix, startTime, endTime) => {
6819
6927
  if (!allowedDayKeys.size) {
6820
6928
  return [];
6821
6929
  }
6822
- return bucketKeys.filter((key) => {
6823
- const suffix = key.slice(prefix.length);
6930
+ return bucketKeys.filter((key2) => {
6931
+ const suffix = key2.slice(prefix.length);
6824
6932
  const firstColon = suffix.indexOf(":");
6825
6933
  if (firstColon <= 0) {
6826
6934
  return false;
@@ -6839,7 +6947,7 @@ var loadRuntimeSignals = async (userName, {
6839
6947
  startTime,
6840
6948
  endTime
6841
6949
  );
6842
- const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
6950
+ const refs = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalBucketRef);
6843
6951
  const seen = /* @__PURE__ */ new Set();
6844
6952
  const payloads = await Promise.all(
6845
6953
  refs.map(async (ref) => {
@@ -6868,7 +6976,7 @@ var loadRuntimeSignalEvaluations = async (userName, {
6868
6976
  startTime,
6869
6977
  endTime
6870
6978
  );
6871
- const evaluations = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalEvaluationRecord);
6979
+ const evaluations = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalEvaluationRecord);
6872
6980
  const deduped = /* @__PURE__ */ new Map();
6873
6981
  for (const evaluation of evaluations) {
6874
6982
  deduped.set(evaluation.evaluationId, evaluation);
@@ -7038,10 +7146,10 @@ var syncRuntimeTrades = async ({
7038
7146
  )
7039
7147
  ];
7040
7148
  await Promise.all(
7041
- activeTradeKeys.map(async (key) => {
7042
- const activeRef = await (0, import_redis5.getData)(key, null);
7149
+ activeTradeKeys.map(async (key2) => {
7150
+ const activeRef = await (0, import_redis5.getData)(key2, null);
7043
7151
  activeOrderIdByKey.set(
7044
- key,
7152
+ key2,
7045
7153
  typeof activeRef?.orderId === "string" ? activeRef.orderId : null
7046
7154
  );
7047
7155
  })
@@ -8108,9 +8216,9 @@ var filterReplayComparisonByLineage = ({
8108
8216
  );
8109
8217
  const deploymentWindows = /* @__PURE__ */ new Map();
8110
8218
  for (const scope of comparableRuntimeLineageScopes) {
8111
- const key = lineageScopeKey(scope);
8112
- const existing = deploymentWindows.get(key);
8113
- deploymentWindows.set(key, {
8219
+ const key2 = lineageScopeKey(scope);
8220
+ const existing = deploymentWindows.get(key2);
8221
+ deploymentWindows.set(key2, {
8114
8222
  firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
8115
8223
  lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
8116
8224
  });
@@ -8146,9 +8254,9 @@ var filterReplayComparisonByLineage = ({
8146
8254
  ...comparableRuntimeSignals,
8147
8255
  ...comparableRuntimeEvaluations
8148
8256
  ]) {
8149
- const key = lineageScopeKey(artifact);
8150
- const existing = deploymentWindows.get(key);
8151
- deploymentWindows.set(key, {
8257
+ const key2 = lineageScopeKey(artifact);
8258
+ const existing = deploymentWindows.get(key2);
8259
+ deploymentWindows.set(key2, {
8152
8260
  firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
8153
8261
  lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
8154
8262
  });
@@ -8233,16 +8341,16 @@ var compareExchangeEntriesToBacktest = ({
8233
8341
  const groupedExchange = /* @__PURE__ */ new Map();
8234
8342
  const groupedBacktest = /* @__PURE__ */ new Map();
8235
8343
  for (const entry of exchangeEntries) {
8236
- const key = `${entry.symbol}::${entry.direction}`;
8237
- const bucket = groupedExchange.get(key) ?? [];
8344
+ const key2 = `${entry.symbol}::${entry.direction}`;
8345
+ const bucket = groupedExchange.get(key2) ?? [];
8238
8346
  bucket.push(entry);
8239
- groupedExchange.set(key, bucket);
8347
+ groupedExchange.set(key2, bucket);
8240
8348
  }
8241
8349
  for (const entry of backtestEntries) {
8242
- const key = `${entry.symbol}::${entry.direction}`;
8243
- const bucket = groupedBacktest.get(key) ?? [];
8350
+ const key2 = `${entry.symbol}::${entry.direction}`;
8351
+ const bucket = groupedBacktest.get(key2) ?? [];
8244
8352
  bucket.push(entry);
8245
- groupedBacktest.set(key, bucket);
8353
+ groupedBacktest.set(key2, bucket);
8246
8354
  }
8247
8355
  const matched = [];
8248
8356
  const exchangeOnly = [];
@@ -8252,11 +8360,11 @@ var compareExchangeEntriesToBacktest = ({
8252
8360
  ...groupedExchange.keys(),
8253
8361
  ...groupedBacktest.keys()
8254
8362
  ]);
8255
- for (const key of groupKeys) {
8256
- const exchangeGroup = [...groupedExchange.get(key) ?? []].sort(
8363
+ for (const key2 of groupKeys) {
8364
+ const exchangeGroup = [...groupedExchange.get(key2) ?? []].sort(
8257
8365
  (left, right) => left.entryTimestamp - right.entryTimestamp
8258
8366
  );
8259
- const unmatchedBacktest = [...groupedBacktest.get(key) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
8367
+ const unmatchedBacktest = [...groupedBacktest.get(key2) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
8260
8368
  for (const exchangeEntry of exchangeGroup) {
8261
8369
  let bestIndex = -1;
8262
8370
  let bestDiff = Number.POSITIVE_INFINITY;
@@ -8677,14 +8785,14 @@ var mergeExchangeEntryExecutions = (rows) => {
8677
8785
  const mergedRows = [];
8678
8786
  const groups = /* @__PURE__ */ new Map();
8679
8787
  for (const row of rows) {
8680
- const key = getExchangeExecutionMergeKey(row);
8681
- if (!key) {
8788
+ const key2 = getExchangeExecutionMergeKey(row);
8789
+ if (!key2) {
8682
8790
  mergedRows.push(row);
8683
8791
  continue;
8684
8792
  }
8685
- const group = groups.get(key) ?? [];
8793
+ const group = groups.get(key2) ?? [];
8686
8794
  group.push(row);
8687
- groups.set(key, group);
8795
+ groups.set(key2, group);
8688
8796
  }
8689
8797
  for (const group of groups.values()) {
8690
8798
  mergedRows.push(
@@ -9175,13 +9283,13 @@ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
9175
9283
  throw new TypeError("JSON evidence cannot contain toJSON hooks");
9176
9284
  yield "{";
9177
9285
  let first = true;
9178
- for (const key of Object.keys(value)) {
9179
- const field = Reflect.get(value, key);
9286
+ for (const key2 of Object.keys(value)) {
9287
+ const field = Reflect.get(value, key2);
9180
9288
  if (field === void 0 || typeof field === "function" || typeof field === "symbol")
9181
9289
  continue;
9182
9290
  if (!first) yield ",";
9183
9291
  first = false;
9184
- yield JSON.stringify(key) + ":";
9292
+ yield JSON.stringify(key2) + ":";
9185
9293
  yield* jsonParts(field, ancestors);
9186
9294
  }
9187
9295
  yield "}";
@@ -9643,10 +9751,276 @@ var writeReplayOutputReport = async ({
9643
9751
  };
9644
9752
 
9645
9753
  // src/lib/replay/portfolioReport.ts
9754
+ var import_promises5 = __toESM(require("fs/promises"));
9755
+ var import_node_path8 = __toESM(require("path"));
9756
+ var import_node_crypto6 = require("crypto");
9757
+
9758
+ // src/lib/replay/portfolioComparison.ts
9759
+ var import_node_crypto5 = require("crypto");
9760
+
9761
+ // src/lib/coreResearch/io.ts
9762
+ var import_node_crypto3 = require("crypto");
9763
+ var import_node_fs4 = require("fs");
9646
9764
  var import_promises4 = __toESM(require("fs/promises"));
9765
+ var import_backtest2 = require("@tradejs/core/backtest");
9766
+ var import_node_path7 = __toESM(require("path"));
9767
+
9768
+ // src/lib/coreResearch/trace.ts
9769
+ var import_node_fs3 = require("fs");
9770
+ var import_node_readline = __toESM(require("readline"));
9647
9771
  var import_node_path6 = __toESM(require("path"));
9648
- var import_node_crypto3 = require("crypto");
9649
- var DAY_MS4 = 864e5;
9772
+
9773
+ // src/lib/coreResearch/io.ts
9774
+ var canonicalJson = (value) => {
9775
+ const normalize = (current) => {
9776
+ if (Array.isArray(current)) return current.map(normalize);
9777
+ if (current && typeof current === "object") {
9778
+ return Object.fromEntries(
9779
+ Object.entries(current).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key2, entry]) => [key2, normalize(entry)])
9780
+ );
9781
+ }
9782
+ return current;
9783
+ };
9784
+ return JSON.stringify(normalize(value));
9785
+ };
9786
+
9787
+ // src/lib/coreResearch/statistics.ts
9788
+ var import_node_crypto4 = require("crypto");
9789
+
9790
+ // src/lib/coreResearch/metrics.ts
9791
+ var DAY_MS4 = 24 * 60 * 60 * 1e3;
9792
+
9793
+ // src/lib/coreResearch/statistics.ts
9794
+ var createSeededRandom = (seedText) => {
9795
+ const digest = (0, import_node_crypto4.createHash)("sha256").update(seedText).digest();
9796
+ let state = digest.readUInt32LE(0) || 1;
9797
+ return () => {
9798
+ state ^= state << 13;
9799
+ state ^= state >>> 17;
9800
+ state ^= state << 5;
9801
+ return (state >>> 0) / 4294967296;
9802
+ };
9803
+ };
9804
+ var percentile = (sorted, probability) => {
9805
+ if (!sorted.length) return Number.NaN;
9806
+ const index = Math.min(
9807
+ sorted.length - 1,
9808
+ Math.max(0, Math.floor(probability * sorted.length))
9809
+ );
9810
+ return sorted[index];
9811
+ };
9812
+ var calendarClusterBootstrap = (params) => {
9813
+ const clusterMs = params.clusterDays * DAY_MS4;
9814
+ const buckets = /* @__PURE__ */ new Map();
9815
+ if (params.start != null && params.end != null && params.end > params.start) {
9816
+ const first = Math.floor(params.start / clusterMs);
9817
+ const last = Math.ceil(params.end / clusterMs);
9818
+ for (let bucket = first; bucket < last; bucket += 1) {
9819
+ buckets.set(bucket, { control: 0, candidate: 0 });
9820
+ }
9821
+ }
9822
+ const add = (trades, field) => {
9823
+ for (const trade of trades) {
9824
+ const bucket = Math.floor(trade.exitTimestamp / clusterMs);
9825
+ const current = buckets.get(bucket) ?? { control: 0, candidate: 0 };
9826
+ current[field] += trade.netProfit;
9827
+ buckets.set(bucket, current);
9828
+ }
9829
+ };
9830
+ add(params.control, "control");
9831
+ add(params.candidate, "candidate");
9832
+ const deltas = [...buckets.entries()].sort(([left], [right]) => left - right).map(([, value]) => value.candidate - value.control);
9833
+ if (!deltas.length) {
9834
+ return {
9835
+ method: "calendar-cluster-bootstrap",
9836
+ clusterDays: params.clusterDays,
9837
+ iterations: params.iterations,
9838
+ confidenceLevel: params.confidenceLevel,
9839
+ observedMeanPnlDelta: null,
9840
+ confidenceInterval: null,
9841
+ probabilityPositive: null,
9842
+ oneSidedPValue: null,
9843
+ holmAdjustedPValue: null
9844
+ };
9845
+ }
9846
+ const random = createSeededRandom(params.seed);
9847
+ const samples = [];
9848
+ for (let iteration = 0; iteration < params.iterations; iteration += 1) {
9849
+ let sum = 0;
9850
+ for (let index = 0; index < deltas.length; index += 1) {
9851
+ sum += deltas[Math.floor(random() * deltas.length)];
9852
+ }
9853
+ samples.push(sum / deltas.length);
9854
+ }
9855
+ samples.sort((left, right) => left - right);
9856
+ const alpha = 1 - params.confidenceLevel;
9857
+ const positive = samples.filter((value) => value > 0).length;
9858
+ const probabilityPositive = positive / samples.length;
9859
+ const observed = deltas.reduce((sum, value) => sum + value, 0) / deltas.length;
9860
+ const oneSidedPValue = (1 + samples.filter((value) => value - observed >= observed - 1e-12).length) / (samples.length + 1);
9861
+ return {
9862
+ method: "calendar-cluster-bootstrap",
9863
+ clusterDays: params.clusterDays,
9864
+ iterations: params.iterations,
9865
+ confidenceLevel: params.confidenceLevel,
9866
+ observedMeanPnlDelta: deltas.reduce((sum, value) => sum + value, 0) / deltas.length,
9867
+ confidenceInterval: [
9868
+ percentile(samples, alpha / 2),
9869
+ percentile(samples, 1 - alpha / 2)
9870
+ ],
9871
+ probabilityPositive,
9872
+ oneSidedPValue,
9873
+ holmAdjustedPValue: null
9874
+ };
9875
+ };
9876
+
9877
+ // src/lib/replay/portfolioComparison.ts
9878
+ var key = (value) => canonicalJson(value);
9879
+ function longestRecovery(points) {
9880
+ let start;
9881
+ let lastPeak;
9882
+ let longestMs = 0;
9883
+ for (const point of points) {
9884
+ if (point.value === 0) {
9885
+ if (start !== void 0)
9886
+ longestMs = Math.max(longestMs, point.timestamp - start);
9887
+ start = void 0;
9888
+ lastPeak = point.timestamp;
9889
+ } else if (start === void 0) start = lastPeak ?? point.timestamp;
9890
+ }
9891
+ return { longestCompletedMs: longestMs, unresolvedSince: start ?? null };
9892
+ }
9893
+ function compareCapacityAdmissions(before, after, changed) {
9894
+ const count = (filled, rejected) => {
9895
+ const keys = new Set(
9896
+ filled.admissions.filter(
9897
+ (event) => event.strategy !== changed && event.status === "filled"
9898
+ ).map((event) => event.key)
9899
+ );
9900
+ return new Set(
9901
+ rejected.admissions.filter(
9902
+ (event) => event.status === "capacity-rejected" && keys.has(event.key)
9903
+ ).map((event) => event.key)
9904
+ ).size;
9905
+ };
9906
+ return { displaced: count(before, after), restored: count(after, before) };
9907
+ }
9908
+ function comparePortfolioReports(input, candidate) {
9909
+ if (!input || typeof input !== "object" || !("checksum" in input)) {
9910
+ throw new Error("Expected a checksum-bound portfolio report");
9911
+ }
9912
+ const { checksum, ...payload } = input;
9913
+ if (checksum !== (0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(payload)).digest("hex") || payload.schema !== "tradejs-portfolio-backtest/v1") {
9914
+ throw new Error("Control portfolio report checksum or schema mismatch");
9915
+ }
9916
+ const control = payload;
9917
+ if (control.lineage?.batchCount !== 1 || candidate.lineage.batchCount !== 1 || control.lineage.cacheOnly !== true || candidate.lineage.cacheOnly !== true) {
9918
+ throw new Error(
9919
+ "Portfolio comparison requires two cache-only reports from a single shared batch"
9920
+ );
9921
+ }
9922
+ for (const field of [
9923
+ "connector",
9924
+ "interval",
9925
+ "tickers",
9926
+ "candleCoverage",
9927
+ "binanceMarketContextRows",
9928
+ "maxLossValue"
9929
+ ]) {
9930
+ if (candidate.lineage[field] === void 0 || key(control.lineage[field]) !== key(candidate.lineage[field])) {
9931
+ throw new Error(`Portfolio comparison scope mismatch: ${field}`);
9932
+ }
9933
+ }
9934
+ if (key(control.window) !== key(candidate.window))
9935
+ throw new Error("Portfolio comparison window mismatch");
9936
+ const before = control.lineage.strategies;
9937
+ const after = candidate.lineage.strategies;
9938
+ if (!Array.isArray(before) || !Array.isArray(after) || key(before.map((s) => s.strategyName)) !== key(after.map((s) => s.strategyName))) {
9939
+ throw new Error(
9940
+ "Portfolio comparison requires the same ordered strategies"
9941
+ );
9942
+ }
9943
+ const changed = after.filter((s, index) => key(s) !== key(before[index]));
9944
+ if (changed.length > 1)
9945
+ throw new Error(
9946
+ "Portfolio comparison allows at most one strategy replacement"
9947
+ );
9948
+ if (after.some(
9949
+ (s, index) => s.maxLossValue !== before[index].maxLossValue || key(s.effectiveConfig.PORTFOLIO_POLICY) !== key(before[index].effectiveConfig.PORTFOLIO_POLICY)
9950
+ )) {
9951
+ throw new Error(
9952
+ "Portfolio comparison requires unchanged risk and portfolio policies"
9953
+ );
9954
+ }
9955
+ const all = (report) => {
9956
+ const row = report.metrics.portfolio.find(
9957
+ (r) => r.window === "full" && r.cohort === "ALL"
9958
+ );
9959
+ if (!row) throw new Error("Missing full ALL portfolio metrics");
9960
+ return row;
9961
+ };
9962
+ const a = all(control), b = all(candidate);
9963
+ const toPnl = (report) => report.trades.map((trade) => ({
9964
+ exitTimestamp: trade.exitedAt,
9965
+ netProfit: trade.pnl
9966
+ }));
9967
+ const strategyDelta = candidate.metrics.strategies.filter((row) => row.window === "full").map((row) => {
9968
+ const previous = control.metrics.strategies.find(
9969
+ (r) => r.window === row.window && r.scope === row.scope && r.cohort === row.cohort
9970
+ );
9971
+ return {
9972
+ strategy: row.scope,
9973
+ cohort: row.cohort,
9974
+ trades: previous ? row.n - previous.n : null,
9975
+ pnl: previous ? row.pnl - previous.pnl : null
9976
+ };
9977
+ });
9978
+ const markBefore = control.portfolioDiagnostics?.markedMaxDrawdown ?? null;
9979
+ const markAfter = candidate.portfolioDiagnostics?.markedMaxDrawdown ?? null;
9980
+ const displacement = control.portfolioDiagnostics && candidate.portfolioDiagnostics ? compareCapacityAdmissions(
9981
+ control.portfolioDiagnostics,
9982
+ candidate.portfolioDiagnostics,
9983
+ changed[0]?.strategyName
9984
+ ) : null;
9985
+ return {
9986
+ controlChecksum: checksum,
9987
+ markedDrawdown: {
9988
+ control: markBefore,
9989
+ candidate: markAfter,
9990
+ delta: markBefore === null || markAfter === null ? null : markAfter - markBefore
9991
+ },
9992
+ capacityDisplacement: displacement,
9993
+ changedStrategy: changed[0]?.strategyName ?? null,
9994
+ delta: {
9995
+ pnl: b.pnl - a.pnl,
9996
+ trades: b.n - a.n,
9997
+ realizedMaxDrawdown: b.realizedMaxDrawdown - a.realizedMaxDrawdown
9998
+ },
9999
+ recovery: {
10000
+ control: longestRecovery(control.curves.portfolioDrawdown),
10001
+ candidate: longestRecovery(candidate.curves.portfolioDrawdown)
10002
+ },
10003
+ strategyDelta,
10004
+ bootstrap: calendarClusterBootstrap({
10005
+ control: toPnl(control),
10006
+ candidate: toPnl(candidate),
10007
+ clusterDays: 7,
10008
+ iterations: 1e3,
10009
+ confidenceLevel: 0.95,
10010
+ seed: String(checksum),
10011
+ start: candidate.window.start,
10012
+ end: candidate.window.end
10013
+ }),
10014
+ limitations: [
10015
+ "Scope and recorded coverage match. Report lineage does not prove equality of every historical source byte.",
10016
+ "Marked drawdown uses last observed candle closes at replay cycle boundaries. It does not measure intra-candle excursions.",
10017
+ "Capacity displacement counts only identical signals of unchanged strategies filled in one run and explicitly rejected by the position cap in the other."
10018
+ ]
10019
+ };
10020
+ }
10021
+
10022
+ // src/lib/replay/portfolioReport.ts
10023
+ var DAY_MS5 = 864e5;
9650
10024
  var WINDOWS = [365, 180, 90, 30, 7];
9651
10025
  var COLORS = [
9652
10026
  "#2563eb",
@@ -9726,7 +10100,7 @@ var calculatePortfolioMetricRow = ({
9726
10100
  const pnl = selected.reduce((sum, trade) => sum + trade.pnl, 0);
9727
10101
  const wins = selected.filter((trade) => trade.pnl > 0).length;
9728
10102
  const losses = selected.filter((trade) => trade.pnl <= 0).length;
9729
- const days = Math.max(1, (end - start) / DAY_MS4);
10103
+ const days = Math.max(1, (end - start) / DAY_MS5);
9730
10104
  return {
9731
10105
  window,
9732
10106
  scope,
@@ -9746,7 +10120,7 @@ var windowBounds = (start, end) => [
9746
10120
  { label: "full", start, end },
9747
10121
  ...WINDOWS.map((days) => ({
9748
10122
  label: `${days}d`,
9749
- start: Math.max(start, end - days * DAY_MS4),
10123
+ start: Math.max(start, end - days * DAY_MS5),
9750
10124
  end
9751
10125
  }))
9752
10126
  ];
@@ -9821,10 +10195,11 @@ var writePortfolioReport = async ({
9821
10195
  replayResult,
9822
10196
  window,
9823
10197
  lineage,
9824
- command
10198
+ command,
10199
+ compareTo
9825
10200
  }) => {
9826
- const outputDir = import_node_path6.default.resolve(projectRoot, "data/replay/output");
9827
- await import_promises4.default.mkdir(outputDir, { recursive: true });
10201
+ const outputDir = import_node_path8.default.resolve(projectRoot, "data/replay/output");
10202
+ await import_promises5.default.mkdir(outputDir, { recursive: true });
9828
10203
  const trades = replayResult.strategies.flatMap(
9829
10204
  (strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
9830
10205
  );
@@ -9896,13 +10271,13 @@ var writePortfolioReport = async ({
9896
10271
  });
9897
10272
  const prefix = `${timestamp}-portfolio-backtest`;
9898
10273
  const paths = {
9899
- markdown: import_node_path6.default.join(outputDir, `${prefix}.md`),
9900
- json: import_node_path6.default.join(outputDir, `${prefix}.json`),
9901
- equitySvg: import_node_path6.default.join(outputDir, `${prefix}-equity.svg`),
9902
- drawdownSvg: import_node_path6.default.join(outputDir, `${prefix}-drawdown.svg`),
9903
- html: import_node_path6.default.join(outputDir, `${prefix}.html`)
10274
+ markdown: import_node_path8.default.join(outputDir, `${prefix}.md`),
10275
+ json: import_node_path8.default.join(outputDir, `${prefix}.json`),
10276
+ equitySvg: import_node_path8.default.join(outputDir, `${prefix}-equity.svg`),
10277
+ drawdownSvg: import_node_path8.default.join(outputDir, `${prefix}-drawdown.svg`),
10278
+ html: import_node_path8.default.join(outputDir, `${prefix}.html`)
9904
10279
  };
9905
- const report = {
10280
+ const baseReport = {
9906
10281
  schema: "tradejs-portfolio-backtest/v1",
9907
10282
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
9908
10283
  command,
@@ -9924,9 +10299,17 @@ var writePortfolioReport = async ({
9924
10299
  portfolioDrawdown: drawdownCurve(portfolioEquity),
9925
10300
  strategies: strategySeries.map(({ name, points }) => ({ name, points }))
9926
10301
  },
9927
- trades
10302
+ trades,
10303
+ ...replayResult.portfolioDiagnostics ? { portfolioDiagnostics: replayResult.portfolioDiagnostics } : {}
9928
10304
  };
9929
- const checksum = (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
10305
+ const comparison = compareTo ? comparePortfolioReports(
10306
+ JSON.parse(
10307
+ await import_promises5.default.readFile(import_node_path8.default.resolve(projectRoot, compareTo), "utf8")
10308
+ ),
10309
+ baseReport
10310
+ ) : void 0;
10311
+ const report = { ...baseReport, ...comparison ? { comparison } : {} };
10312
+ const checksum = (0, import_node_crypto6.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
9930
10313
  const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
9931
10314
  const markdown = `# Portfolio Backtest
9932
10315
 
@@ -9938,9 +10321,9 @@ var writePortfolioReport = async ({
9938
10321
  - Report checksum: \`${checksum}\`
9939
10322
  - Command: \`${command}\`
9940
10323
 
9941
- ![Strategy PnL overlay](./${import_node_path6.default.basename(paths.equitySvg)})
10324
+ ![Strategy PnL overlay](./${import_node_path8.default.basename(paths.equitySvg)})
9942
10325
 
9943
- ![Portfolio drawdown](./${import_node_path6.default.basename(paths.drawdownSvg)})
10326
+ ![Portfolio drawdown](./${import_node_path8.default.basename(paths.drawdownSvg)})
9944
10327
 
9945
10328
  ## Portfolio metrics
9946
10329
 
@@ -9951,17 +10334,33 @@ ${markdownTable(portfolioRows)}
9951
10334
  ${markdownTable(strategyRows)}
9952
10335
  `;
9953
10336
  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>`;
10337
+ const comparisonMarkdown = comparison ? `
10338
+ ## Paired portfolio comparison
10339
+
10340
+ - Net PnL delta: ${display(comparison.delta.pnl)}
10341
+ - Closed trades delta: ${comparison.delta.trades}
10342
+ - Realized MaxDD delta: ${display(comparison.delta.realizedMaxDrawdown)}
10343
+
10344
+ ${comparison.limitations.join(" ")}
10345
+ ` : "";
9954
10346
  await Promise.all([
9955
- import_promises4.default.writeFile(paths.markdown, markdown, "utf8"),
9956
- import_promises4.default.writeFile(
10347
+ import_promises5.default.writeFile(paths.markdown, markdown + comparisonMarkdown, "utf8"),
10348
+ import_promises5.default.writeFile(
9957
10349
  paths.json,
9958
10350
  `${JSON.stringify({ ...report, checksum }, null, 2)}
9959
10351
  `,
9960
10352
  "utf8"
9961
10353
  ),
9962
- import_promises4.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
9963
- import_promises4.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
9964
- import_promises4.default.writeFile(paths.html, html, "utf8")
10354
+ import_promises5.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
10355
+ import_promises5.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
10356
+ import_promises5.default.writeFile(
10357
+ paths.html,
10358
+ comparison ? html.replace(
10359
+ "</body>",
10360
+ `<h2>Paired portfolio comparison</h2><pre>${escapeHtml(JSON.stringify(comparison, null, 2))}</pre></body>`
10361
+ ) : html,
10362
+ "utf8"
10363
+ )
9965
10364
  ]);
9966
10365
  return { ...paths, checksum };
9967
10366
  };
@@ -10004,8 +10403,8 @@ var cachedTickerSymbols = async ({
10004
10403
  import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, "default"),
10005
10404
  import_redis6.redisKeys.tickerUniverse(replayUserName, name)
10006
10405
  ]);
10007
- for (const key of [...new Set(keys)]) {
10008
- const cached = await (0, import_redis6.getData)(key, null);
10406
+ for (const key2 of [...new Set(keys)]) {
10407
+ const cached = await (0, import_redis6.getData)(key2, null);
10009
10408
  const symbols = (cached?.tickers ?? []).map(
10010
10409
  (ticker) => String(ticker.symbol ?? "").trim().toUpperCase()
10011
10410
  ).filter(Boolean);
@@ -10074,7 +10473,8 @@ var finishReplay = async ({
10074
10473
  replayResult,
10075
10474
  window,
10076
10475
  lineage: portfolioLineage ?? {},
10077
- command: process.argv.join(" ")
10476
+ command: process.argv.join(" "),
10477
+ compareTo: replayFlags.portfolioCompareTo
10078
10478
  }) : null;
10079
10479
  if (portfolioReport) {
10080
10480
  console.log(import_chalk13.default.green(`Portfolio report: ${portfolioReport.html}`));
@@ -10358,6 +10758,18 @@ var replayBacktest = async () => {
10358
10758
  `Invalid --portfolioBatchSize: ${String(replayFlags.portfolioBatchSize)}`
10359
10759
  );
10360
10760
  }
10761
+ if (replayFlags.portfolioCompareTo && (!replayFlags.portfolioReport || !replayFlags.cacheOnly)) {
10762
+ throw new Error(
10763
+ "--portfolioCompareTo requires --portfolioReport and --cacheOnly"
10764
+ );
10765
+ }
10766
+ if (replayFlags.portfolioReport && preparedRun.tickers.length > portfolioBatchSize && (replayFlags.portfolioCompareTo || replayStrategies.some(
10767
+ ({ strategyConfig }) => Boolean(strategyConfig.PORTFOLIO_POLICY)
10768
+ ))) {
10769
+ throw new Error(
10770
+ "Portfolio policies and comparisons require a single shared batch; set --portfolioBatchSize to at least the number of tickers"
10771
+ );
10772
+ }
10361
10773
  const runtimeStrategies = replayFlags.tickers && !replayFlags.portfolioReport ? replayStrategies.map(
10362
10774
  ({ selection: _selection, ...strategy }) => strategy
10363
10775
  ) : replayStrategies;
@@ -10381,6 +10793,9 @@ var replayBacktest = async () => {
10381
10793
  showProgress: !replayFlags.portfolioReport,
10382
10794
  collectSkipEvidence: !replayFlags.portfolioReport,
10383
10795
  collectSignals: !replayFlags.portfolioReport,
10796
+ collectPortfolioDiagnostics: Boolean(
10797
+ replayFlags.portfolioReport && tickerBatches.length === 1
10798
+ ),
10384
10799
  decisionTraceTargets: replayFlags.portfolioReport ? void 0 : evidenceMetadata?.decisionTraceTargets
10385
10800
  });
10386
10801
  return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
@@ -10418,6 +10833,9 @@ var replayBacktest = async () => {
10418
10833
  cacheOnly: Boolean(replayFlags.cacheOnly),
10419
10834
  portfolioSemantics: "production strategy order and one concurrent position per symbol; fixed-risk symbol batches are merged by canonical realized PnL",
10420
10835
  portfolioBatchSize,
10836
+ batchCount: tickerBatches.length,
10837
+ connector: preparedRun.connectorName,
10838
+ interval: replayInterval,
10421
10839
  tickers: preparedRun.tickers,
10422
10840
  candleCoverage: candleCoverage ?? null,
10423
10841
  binanceMarketContextRows: portfolioMarketContextRows ?? null,