@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
  };
@@ -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
 
@@ -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(
@@ -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,16 @@ 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;
5191
+ const markedAtBySymbol = /* @__PURE__ */ new Map();
5192
+ const entryFacts = /* @__PURE__ */ new Map();
5193
+ let portfolioObserved = false;
5186
5194
  const orderLog = [];
5187
5195
  const positionLog = [];
5188
5196
  const orderLogByStrategy = /* @__PURE__ */ new Map();
@@ -5420,14 +5428,51 @@ var createPortfolioReplayConnector = (connector) => {
5420
5428
  const { strategyName: _strategyName, ...nextPosition } = position;
5421
5429
  return nextPosition;
5422
5430
  }),
5431
+ getPortfolioObservation: async (asOf, reservations) => {
5432
+ portfolioObserved = true;
5433
+ const pendingIds = new Set(reservations.map((r) => r.id));
5434
+ for (const id of entryFacts.keys())
5435
+ if (!pendingIds.has(id)) entryFacts.delete(id);
5436
+ return {
5437
+ asOf,
5438
+ positions: Array.from(positionsBySymbol.values()).map(
5439
+ ({ position, stopLossPrice }) => ({
5440
+ id: position.orderId ?? `${position.strategyName}:${position.symbol}:${position.timestamp}`,
5441
+ strategy: position.strategyName === UNKNOWN_STRATEGY ? null : position.strategyName,
5442
+ symbol: position.symbol,
5443
+ direction: position.direction,
5444
+ qty: position.qty,
5445
+ entryPrice: position.price,
5446
+ markPrice: currentPriceBySymbol.get(position.symbol) ?? null,
5447
+ markedAt: markedAtBySymbol.get(position.symbol) ?? null,
5448
+ stopLossPrice
5449
+ })
5450
+ ),
5451
+ orders: reservations.map(
5452
+ (r) => entryFacts.get(r.id) ?? {
5453
+ id: r.id,
5454
+ status: "pending",
5455
+ filledQty: 0
5456
+ }
5457
+ )
5458
+ };
5459
+ },
5423
5460
  getOpenPositionPnl: async () => Array.from(positionsBySymbol.values()).map(
5424
- ({ position }) => getPositionPnlSnapshot({
5425
- position,
5426
- currentPrice: currentPriceBySymbol.get(position.symbol) ?? position.price
5461
+ ({ position, stopLossPrice }) => ({
5462
+ ...getPositionPnlSnapshot({
5463
+ position,
5464
+ currentPrice: currentPriceBySymbol.get(position.symbol) ?? position.price
5465
+ }),
5466
+ observedAt: markedAtBySymbol.get(position.symbol),
5467
+ stopLossPrice
5427
5468
  })
5428
5469
  ),
5429
5470
  placeOrder: async (order) => {
5430
- const { beforeSubmit, ...submittedOrder } = order;
5471
+ const {
5472
+ beforeSubmit,
5473
+ onRejected: _onRejected,
5474
+ ...submittedOrder
5475
+ } = order;
5431
5476
  if (positionsBySymbol.has(order.symbol)) {
5432
5477
  return false;
5433
5478
  }
@@ -5471,6 +5516,14 @@ var createPortfolioReplayConnector = (connector) => {
5471
5516
  stopLossPrice: null
5472
5517
  };
5473
5518
  positionsBySymbol.set(order.symbol, positionState);
5519
+ markedAtBySymbol.set(order.symbol, executionTimestamp ?? order.timestamp);
5520
+ if (portfolioObserved && order.orderId)
5521
+ entryFacts.set(order.orderId, {
5522
+ id: order.orderId,
5523
+ status: "filled",
5524
+ filledQty: order.qty,
5525
+ positionId: order.orderId
5526
+ });
5474
5527
  logOrder({
5475
5528
  positionState,
5476
5529
  data: {
@@ -5554,11 +5607,26 @@ var createPortfolioReplayConnector = (connector) => {
5554
5607
  getTickers: connector.getTickers,
5555
5608
  advanceMarket: async ({ symbol, candle }) => {
5556
5609
  executionTimestamp = candle.timestamp;
5610
+ markedAtBySymbol.set(symbol, candle.timestamp);
5611
+ equityMarks?.set(symbol, candle.close);
5557
5612
  currentPriceBySymbol.set(symbol, candle.close);
5558
5613
  await checkSl({ symbol, candle });
5559
5614
  await checkTp({ symbol, candle });
5560
5615
  currentPriceBySymbol.set(symbol, candle.close);
5561
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
+ },
5562
5630
  getReplayArtifacts: () => ({
5563
5631
  orderLog,
5564
5632
  positionLog,
@@ -5772,6 +5840,21 @@ var executeHistoricalReplay = async (context, adapters) => {
5772
5840
  context.collectSkipEvidence
5773
5841
  );
5774
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
+ };
5775
5858
  const reportProgress = (timestamp) => {
5776
5859
  if (adapters.progress.enabled === false) return;
5777
5860
  adapters.progress.tick({
@@ -5815,6 +5898,7 @@ var executeHistoricalReplay = async (context, adapters) => {
5815
5898
  }
5816
5899
  }
5817
5900
  advanceAbortedCycle(cycleSymbols, timestamp);
5901
+ observeEquity();
5818
5902
  reportProgress(timestamp);
5819
5903
  continue;
5820
5904
  }
@@ -5858,6 +5942,21 @@ var executeHistoricalReplay = async (context, adapters) => {
5858
5942
  result
5859
5943
  });
5860
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
+ }
5861
5960
  result.runtimeLineage = strategyRuntime.runtimeLineage;
5862
5961
  await adapters.enrichSignal(result);
5863
5962
  cycleSignals.push(result);
@@ -5872,12 +5971,16 @@ var executeHistoricalReplay = async (context, adapters) => {
5872
5971
  status: "completed",
5873
5972
  durationMs: adapters.clock.now() - cycleStartedAt
5874
5973
  });
5974
+ observeEquity();
5875
5975
  reportProgress(timestamp);
5876
5976
  }
5877
5977
  } finally {
5878
5978
  releaseHistoricalReplayCaches(context.plan, adapters);
5879
5979
  }
5980
+ if (portfolioDiagnostics?.unpricedCycles)
5981
+ portfolioDiagnostics.markedMaxDrawdown = null;
5880
5982
  return {
5983
+ ...portfolioDiagnostics ? { portfolioDiagnostics } : {},
5881
5984
  signals,
5882
5985
  abortedCycles,
5883
5986
  replayLineageScopes: skipEvidence.values(),
@@ -5933,6 +6036,7 @@ var collectHistoricalReplayResult = (context) => ({
5933
6036
  abortedCycles: context.abortedCycles,
5934
6037
  runtimeLineages: context.runtimeLineages,
5935
6038
  replayLineageScopes: context.replayLineageScopes,
6039
+ ...context.portfolioDiagnostics ? { portfolioDiagnostics: context.portfolioDiagnostics } : {},
5936
6040
  ...context.replayDecisionTraces?.length ? { replayDecisionTraces: context.replayDecisionTraces } : {}
5937
6041
  });
5938
6042
  var compactHistoricalReplayResultForPortfolio = (result) => ({
@@ -6134,6 +6238,7 @@ var runHistoricalSignalsReplay = async ({
6134
6238
  showProgress = true,
6135
6239
  collectSkipEvidence = true,
6136
6240
  collectSignals = true,
6241
+ collectPortfolioDiagnostics = false,
6137
6242
  decisionTraceTargets
6138
6243
  }) => {
6139
6244
  const memory = createReplayMemoryProfile();
@@ -6143,7 +6248,8 @@ var runHistoricalSignalsReplay = async ({
6143
6248
  const projectHooks = projectConfig.hooks;
6144
6249
  const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
6145
6250
  const replayConnector = createPortfolioReplayConnector(
6146
- preparedRun.marketConnector
6251
+ preparedRun.marketConnector,
6252
+ collectPortfolioDiagnostics
6147
6253
  );
6148
6254
  const connectorName = getConnectorName(preparedRun.marketConnector);
6149
6255
  const referenceData = references ?? await loadHistoricalReplayReferences({ preparedRun, interval });
@@ -6221,6 +6327,7 @@ var runHistoricalSignalsReplay = async ({
6221
6327
  connector: replayConnector,
6222
6328
  collectSkipEvidence,
6223
6329
  collectSignals,
6330
+ collectPortfolioDiagnostics,
6224
6331
  decisionTraceTargets,
6225
6332
  hooks: projectHooks,
6226
6333
  hookContext
@@ -6259,7 +6366,8 @@ var runHistoricalSignalsReplay = async ({
6259
6366
  abortedCycles: execution.abortedCycles,
6260
6367
  runtimeLineages: plan.runtimeLineages,
6261
6368
  replayLineageScopes: execution.replayLineageScopes,
6262
- replayDecisionTraces: execution.replayDecisionTraces
6369
+ replayDecisionTraces: execution.replayDecisionTraces,
6370
+ portfolioDiagnostics: execution.portfolioDiagnostics
6263
6371
  });
6264
6372
  memory?.("result:collected");
6265
6373
  return result;
@@ -6506,16 +6614,16 @@ var findRuntimeDuplicateGroups = (entries) => {
6506
6614
  if (entry.source !== "runtime") {
6507
6615
  continue;
6508
6616
  }
6509
- const key = toRuntimeDuplicateKey(entry);
6510
- const bucket = groups.get(key) ?? [];
6617
+ const key2 = toRuntimeDuplicateKey(entry);
6618
+ const bucket = groups.get(key2) ?? [];
6511
6619
  bucket.push(entry);
6512
- groups.set(key, bucket);
6620
+ groups.set(key2, bucket);
6513
6621
  }
6514
- return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key, group]) => {
6622
+ return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key2, group]) => {
6515
6623
  const sortedGroup = [...group].sort(sortTradeParityEntries);
6516
6624
  const first = sortedGroup[0];
6517
6625
  return {
6518
- key,
6626
+ key: key2,
6519
6627
  strategy: first.strategy,
6520
6628
  symbol: first.symbol,
6521
6629
  direction: first.direction,
@@ -6554,26 +6662,26 @@ var compareTradeParityEntries = ({
6554
6662
  const groupedRuntime = /* @__PURE__ */ new Map();
6555
6663
  const groupedBacktest = /* @__PURE__ */ new Map();
6556
6664
  for (const entry of runtimeEntries) {
6557
- const key = toGroupKey(entry);
6558
- const bucket = groupedRuntime.get(key) ?? [];
6665
+ const key2 = toGroupKey(entry);
6666
+ const bucket = groupedRuntime.get(key2) ?? [];
6559
6667
  bucket.push(entry);
6560
- groupedRuntime.set(key, bucket);
6668
+ groupedRuntime.set(key2, bucket);
6561
6669
  }
6562
6670
  for (const entry of backtestEntries) {
6563
- const key = toGroupKey(entry);
6564
- const bucket = groupedBacktest.get(key) ?? [];
6671
+ const key2 = toGroupKey(entry);
6672
+ const bucket = groupedBacktest.get(key2) ?? [];
6565
6673
  bucket.push(entry);
6566
- groupedBacktest.set(key, bucket);
6674
+ groupedBacktest.set(key2, bucket);
6567
6675
  }
6568
6676
  const groupKeys = /* @__PURE__ */ new Set([
6569
6677
  ...groupedRuntime.keys(),
6570
6678
  ...groupedBacktest.keys()
6571
6679
  ]);
6572
- for (const key of groupKeys) {
6573
- const runtimeGroup = [...groupedRuntime.get(key) ?? []].sort(
6680
+ for (const key2 of groupKeys) {
6681
+ const runtimeGroup = [...groupedRuntime.get(key2) ?? []].sort(
6574
6682
  (left, right) => left.timestamp - right.timestamp
6575
6683
  );
6576
- const backtestGroup = [...groupedBacktest.get(key) ?? []].sort(
6684
+ const backtestGroup = [...groupedBacktest.get(key2) ?? []].sort(
6577
6685
  (left, right) => left.timestamp - right.timestamp
6578
6686
  );
6579
6687
  const unmatchedBacktest = backtestGroup.map((entry) => ({
@@ -6814,8 +6922,8 @@ var filterBucketKeysByWindow = (bucketKeys, prefix, startTime, endTime) => {
6814
6922
  if (!allowedDayKeys.size) {
6815
6923
  return [];
6816
6924
  }
6817
- return bucketKeys.filter((key) => {
6818
- const suffix = key.slice(prefix.length);
6925
+ return bucketKeys.filter((key2) => {
6926
+ const suffix = key2.slice(prefix.length);
6819
6927
  const firstColon = suffix.indexOf(":");
6820
6928
  if (firstColon <= 0) {
6821
6929
  return false;
@@ -6834,7 +6942,7 @@ var loadRuntimeSignals = async (userName, {
6834
6942
  startTime,
6835
6943
  endTime
6836
6944
  );
6837
- 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);
6838
6946
  const seen = /* @__PURE__ */ new Set();
6839
6947
  const payloads = await Promise.all(
6840
6948
  refs.map(async (ref) => {
@@ -6863,7 +6971,7 @@ var loadRuntimeSignalEvaluations = async (userName, {
6863
6971
  startTime,
6864
6972
  endTime
6865
6973
  );
6866
- 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);
6867
6975
  const deduped = /* @__PURE__ */ new Map();
6868
6976
  for (const evaluation of evaluations) {
6869
6977
  deduped.set(evaluation.evaluationId, evaluation);
@@ -7033,10 +7141,10 @@ var syncRuntimeTrades = async ({
7033
7141
  )
7034
7142
  ];
7035
7143
  await Promise.all(
7036
- activeTradeKeys.map(async (key) => {
7037
- const activeRef = await (0, import_redis5.getData)(key, null);
7144
+ activeTradeKeys.map(async (key2) => {
7145
+ const activeRef = await (0, import_redis5.getData)(key2, null);
7038
7146
  activeOrderIdByKey.set(
7039
- key,
7147
+ key2,
7040
7148
  typeof activeRef?.orderId === "string" ? activeRef.orderId : null
7041
7149
  );
7042
7150
  })
@@ -8103,9 +8211,9 @@ var filterReplayComparisonByLineage = ({
8103
8211
  );
8104
8212
  const deploymentWindows = /* @__PURE__ */ new Map();
8105
8213
  for (const scope of comparableRuntimeLineageScopes) {
8106
- const key = lineageScopeKey(scope);
8107
- const existing = deploymentWindows.get(key);
8108
- deploymentWindows.set(key, {
8214
+ const key2 = lineageScopeKey(scope);
8215
+ const existing = deploymentWindows.get(key2);
8216
+ deploymentWindows.set(key2, {
8109
8217
  firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
8110
8218
  lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
8111
8219
  });
@@ -8141,9 +8249,9 @@ var filterReplayComparisonByLineage = ({
8141
8249
  ...comparableRuntimeSignals,
8142
8250
  ...comparableRuntimeEvaluations
8143
8251
  ]) {
8144
- const key = lineageScopeKey(artifact);
8145
- const existing = deploymentWindows.get(key);
8146
- deploymentWindows.set(key, {
8252
+ const key2 = lineageScopeKey(artifact);
8253
+ const existing = deploymentWindows.get(key2);
8254
+ deploymentWindows.set(key2, {
8147
8255
  firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
8148
8256
  lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
8149
8257
  });
@@ -8228,16 +8336,16 @@ var compareExchangeEntriesToBacktest = ({
8228
8336
  const groupedExchange = /* @__PURE__ */ new Map();
8229
8337
  const groupedBacktest = /* @__PURE__ */ new Map();
8230
8338
  for (const entry of exchangeEntries) {
8231
- const key = `${entry.symbol}::${entry.direction}`;
8232
- const bucket = groupedExchange.get(key) ?? [];
8339
+ const key2 = `${entry.symbol}::${entry.direction}`;
8340
+ const bucket = groupedExchange.get(key2) ?? [];
8233
8341
  bucket.push(entry);
8234
- groupedExchange.set(key, bucket);
8342
+ groupedExchange.set(key2, bucket);
8235
8343
  }
8236
8344
  for (const entry of backtestEntries) {
8237
- const key = `${entry.symbol}::${entry.direction}`;
8238
- const bucket = groupedBacktest.get(key) ?? [];
8345
+ const key2 = `${entry.symbol}::${entry.direction}`;
8346
+ const bucket = groupedBacktest.get(key2) ?? [];
8239
8347
  bucket.push(entry);
8240
- groupedBacktest.set(key, bucket);
8348
+ groupedBacktest.set(key2, bucket);
8241
8349
  }
8242
8350
  const matched = [];
8243
8351
  const exchangeOnly = [];
@@ -8247,11 +8355,11 @@ var compareExchangeEntriesToBacktest = ({
8247
8355
  ...groupedExchange.keys(),
8248
8356
  ...groupedBacktest.keys()
8249
8357
  ]);
8250
- for (const key of groupKeys) {
8251
- const exchangeGroup = [...groupedExchange.get(key) ?? []].sort(
8358
+ for (const key2 of groupKeys) {
8359
+ const exchangeGroup = [...groupedExchange.get(key2) ?? []].sort(
8252
8360
  (left, right) => left.entryTimestamp - right.entryTimestamp
8253
8361
  );
8254
- 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 }));
8255
8363
  for (const exchangeEntry of exchangeGroup) {
8256
8364
  let bestIndex = -1;
8257
8365
  let bestDiff = Number.POSITIVE_INFINITY;
@@ -8672,14 +8780,14 @@ var mergeExchangeEntryExecutions = (rows) => {
8672
8780
  const mergedRows = [];
8673
8781
  const groups = /* @__PURE__ */ new Map();
8674
8782
  for (const row of rows) {
8675
- const key = getExchangeExecutionMergeKey(row);
8676
- if (!key) {
8783
+ const key2 = getExchangeExecutionMergeKey(row);
8784
+ if (!key2) {
8677
8785
  mergedRows.push(row);
8678
8786
  continue;
8679
8787
  }
8680
- const group = groups.get(key) ?? [];
8788
+ const group = groups.get(key2) ?? [];
8681
8789
  group.push(row);
8682
- groups.set(key, group);
8790
+ groups.set(key2, group);
8683
8791
  }
8684
8792
  for (const group of groups.values()) {
8685
8793
  mergedRows.push(
@@ -9170,13 +9278,13 @@ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
9170
9278
  throw new TypeError("JSON evidence cannot contain toJSON hooks");
9171
9279
  yield "{";
9172
9280
  let first = true;
9173
- for (const key of Object.keys(value)) {
9174
- const field = Reflect.get(value, key);
9281
+ for (const key2 of Object.keys(value)) {
9282
+ const field = Reflect.get(value, key2);
9175
9283
  if (field === void 0 || typeof field === "function" || typeof field === "symbol")
9176
9284
  continue;
9177
9285
  if (!first) yield ",";
9178
9286
  first = false;
9179
- yield JSON.stringify(key) + ":";
9287
+ yield JSON.stringify(key2) + ":";
9180
9288
  yield* jsonParts(field, ancestors);
9181
9289
  }
9182
9290
  yield "}";
@@ -9638,10 +9746,276 @@ var writeReplayOutputReport = async ({
9638
9746
  };
9639
9747
 
9640
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");
9641
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"));
9642
9766
  var import_node_path6 = __toESM(require("path"));
9643
- var import_node_crypto3 = require("crypto");
9644
- 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;
9645
10019
  var WINDOWS = [365, 180, 90, 30, 7];
9646
10020
  var COLORS = [
9647
10021
  "#2563eb",
@@ -9721,7 +10095,7 @@ var calculatePortfolioMetricRow = ({
9721
10095
  const pnl = selected.reduce((sum, trade) => sum + trade.pnl, 0);
9722
10096
  const wins = selected.filter((trade) => trade.pnl > 0).length;
9723
10097
  const losses = selected.filter((trade) => trade.pnl <= 0).length;
9724
- const days = Math.max(1, (end - start) / DAY_MS4);
10098
+ const days = Math.max(1, (end - start) / DAY_MS5);
9725
10099
  return {
9726
10100
  window,
9727
10101
  scope,
@@ -9741,7 +10115,7 @@ var windowBounds = (start, end) => [
9741
10115
  { label: "full", start, end },
9742
10116
  ...WINDOWS.map((days) => ({
9743
10117
  label: `${days}d`,
9744
- start: Math.max(start, end - days * DAY_MS4),
10118
+ start: Math.max(start, end - days * DAY_MS5),
9745
10119
  end
9746
10120
  }))
9747
10121
  ];
@@ -9816,10 +10190,11 @@ var writePortfolioReport = async ({
9816
10190
  replayResult,
9817
10191
  window,
9818
10192
  lineage,
9819
- command
10193
+ command,
10194
+ compareTo
9820
10195
  }) => {
9821
- const outputDir = import_node_path6.default.resolve(projectRoot, "data/replay/output");
9822
- 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 });
9823
10198
  const trades = replayResult.strategies.flatMap(
9824
10199
  (strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
9825
10200
  );
@@ -9891,13 +10266,13 @@ var writePortfolioReport = async ({
9891
10266
  });
9892
10267
  const prefix = `${timestamp}-portfolio-backtest`;
9893
10268
  const paths = {
9894
- markdown: import_node_path6.default.join(outputDir, `${prefix}.md`),
9895
- json: import_node_path6.default.join(outputDir, `${prefix}.json`),
9896
- equitySvg: import_node_path6.default.join(outputDir, `${prefix}-equity.svg`),
9897
- drawdownSvg: import_node_path6.default.join(outputDir, `${prefix}-drawdown.svg`),
9898
- 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`)
9899
10274
  };
9900
- const report = {
10275
+ const baseReport = {
9901
10276
  schema: "tradejs-portfolio-backtest/v1",
9902
10277
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
9903
10278
  command,
@@ -9919,9 +10294,17 @@ var writePortfolioReport = async ({
9919
10294
  portfolioDrawdown: drawdownCurve(portfolioEquity),
9920
10295
  strategies: strategySeries.map(({ name, points }) => ({ name, points }))
9921
10296
  },
9922
- trades
10297
+ trades,
10298
+ ...replayResult.portfolioDiagnostics ? { portfolioDiagnostics: replayResult.portfolioDiagnostics } : {}
9923
10299
  };
9924
- 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");
9925
10308
  const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
9926
10309
  const markdown = `# Portfolio Backtest
9927
10310
 
@@ -9933,9 +10316,9 @@ var writePortfolioReport = async ({
9933
10316
  - Report checksum: \`${checksum}\`
9934
10317
  - Command: \`${command}\`
9935
10318
 
9936
- ![Strategy PnL overlay](./${import_node_path6.default.basename(paths.equitySvg)})
10319
+ ![Strategy PnL overlay](./${import_node_path8.default.basename(paths.equitySvg)})
9937
10320
 
9938
- ![Portfolio drawdown](./${import_node_path6.default.basename(paths.drawdownSvg)})
10321
+ ![Portfolio drawdown](./${import_node_path8.default.basename(paths.drawdownSvg)})
9939
10322
 
9940
10323
  ## Portfolio metrics
9941
10324
 
@@ -9946,17 +10329,33 @@ ${markdownTable(portfolioRows)}
9946
10329
  ${markdownTable(strategyRows)}
9947
10330
  `;
9948
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
+ ` : "";
9949
10341
  await Promise.all([
9950
- import_promises4.default.writeFile(paths.markdown, markdown, "utf8"),
9951
- import_promises4.default.writeFile(
10342
+ import_promises5.default.writeFile(paths.markdown, markdown + comparisonMarkdown, "utf8"),
10343
+ import_promises5.default.writeFile(
9952
10344
  paths.json,
9953
10345
  `${JSON.stringify({ ...report, checksum }, null, 2)}
9954
10346
  `,
9955
10347
  "utf8"
9956
10348
  ),
9957
- import_promises4.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
9958
- import_promises4.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
9959
- 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
+ )
9960
10359
  ]);
9961
10360
  return { ...paths, checksum };
9962
10361
  };
@@ -9999,8 +10398,8 @@ var cachedTickerSymbols = async ({
9999
10398
  import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, "default"),
10000
10399
  import_redis6.redisKeys.tickerUniverse(replayUserName, name)
10001
10400
  ]);
10002
- for (const key of [...new Set(keys)]) {
10003
- 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);
10004
10403
  const symbols = (cached?.tickers ?? []).map(
10005
10404
  (ticker) => String(ticker.symbol ?? "").trim().toUpperCase()
10006
10405
  ).filter(Boolean);
@@ -10069,7 +10468,8 @@ var finishReplay = async ({
10069
10468
  replayResult,
10070
10469
  window,
10071
10470
  lineage: portfolioLineage ?? {},
10072
- command: process.argv.join(" ")
10471
+ command: process.argv.join(" "),
10472
+ compareTo: replayFlags.portfolioCompareTo
10073
10473
  }) : null;
10074
10474
  if (portfolioReport) {
10075
10475
  console.log(import_chalk13.default.green(`Portfolio report: ${portfolioReport.html}`));
@@ -10353,6 +10753,18 @@ var replayBacktest = async () => {
10353
10753
  `Invalid --portfolioBatchSize: ${String(replayFlags.portfolioBatchSize)}`
10354
10754
  );
10355
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
+ }
10356
10768
  const runtimeStrategies = replayFlags.tickers && !replayFlags.portfolioReport ? replayStrategies.map(
10357
10769
  ({ selection: _selection, ...strategy }) => strategy
10358
10770
  ) : replayStrategies;
@@ -10376,6 +10788,9 @@ var replayBacktest = async () => {
10376
10788
  showProgress: !replayFlags.portfolioReport,
10377
10789
  collectSkipEvidence: !replayFlags.portfolioReport,
10378
10790
  collectSignals: !replayFlags.portfolioReport,
10791
+ collectPortfolioDiagnostics: Boolean(
10792
+ replayFlags.portfolioReport && tickerBatches.length === 1
10793
+ ),
10379
10794
  decisionTraceTargets: replayFlags.portfolioReport ? void 0 : evidenceMetadata?.decisionTraceTargets
10380
10795
  });
10381
10796
  return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
@@ -10413,6 +10828,9 @@ var replayBacktest = async () => {
10413
10828
  cacheOnly: Boolean(replayFlags.cacheOnly),
10414
10829
  portfolioSemantics: "production strategy order and one concurrent position per symbol; fixed-risk symbol batches are merged by canonical realized PnL",
10415
10830
  portfolioBatchSize,
10831
+ batchCount: tickerBatches.length,
10832
+ connector: preparedRun.connectorName,
10833
+ interval: replayInterval,
10416
10834
  tickers: preparedRun.tickers,
10417
10835
  candleCoverage: candleCoverage ?? null,
10418
10836
  binanceMarketContextRows: portfolioMarketContextRows ?? null,