@tradejs/cli 3.1.27 → 3.1.28-beta.251

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.
@@ -39,10 +39,13 @@ __export(replayRunner_exports, {
39
39
  });
40
40
  module.exports = __toCommonJS(replayRunner_exports);
41
41
  var import_chalk13 = __toESM(require("chalk"));
42
- var import_constants6 = require("@tradejs/core/constants");
42
+ var import_constants7 = require("@tradejs/core/constants");
43
43
  var import_data4 = require("@tradejs/core/data");
44
44
  var import_time7 = require("@tradejs/core/time");
45
45
  var import_redis6 = require("@tradejs/infra/redis");
46
+ var import_candles = require("@tradejs/infra/timescale/candles");
47
+ var import_runtimeStrategies4 = require("@tradejs/node/runtimeStrategies");
48
+ var import_strategies5 = require("@tradejs/node/strategies");
46
49
 
47
50
  // src/lib/runFormatting.ts
48
51
  var import_date_fns = require("date-fns");
@@ -890,8 +893,8 @@ var buildMarketBreadthRows = ({
890
893
  let altTurnover24h = 0;
891
894
  let previousAltTurnover24h = 0;
892
895
  for (const indexed of indexedSymbols) {
893
- const { candles, byTimestamp, turnoverPrefix, closePrefix } = indexed;
894
- const index = byTimestamp.get(timestamp);
896
+ const { candles, byTimestamp: byTimestamp2, turnoverPrefix, closePrefix } = indexed;
897
+ const index = byTimestamp2.get(timestamp);
895
898
  if (index == null) continue;
896
899
  const candle = candles[index];
897
900
  const previous = candles[index - 1];
@@ -2707,27 +2710,27 @@ var createCoinMarketCapFetchStats = () => ({
2707
2710
  totalCredits: 0,
2708
2711
  byPath: /* @__PURE__ */ new Map()
2709
2712
  });
2710
- var getCoinMarketCapPathLabel = (path3) => {
2711
- if (path3.includes("global-metrics")) return "global";
2712
- if (path3.includes("cryptocurrency/quotes")) return "reference";
2713
- if (path3.includes("exchange/quotes")) return "exchangeLiquidity";
2714
- if (path3.includes("fear-and-greed")) return "fearGreed";
2715
- if (path3.includes("cmc100")) return "cmc100";
2716
- if (path3.includes("cmc20")) return "cmc20";
2717
- return path3;
2718
- };
2719
- var recordCoinMarketCapFetchStats = (stats, path3, creditCount) => {
2713
+ var getCoinMarketCapPathLabel = (path5) => {
2714
+ if (path5.includes("global-metrics")) return "global";
2715
+ if (path5.includes("cryptocurrency/quotes")) return "reference";
2716
+ if (path5.includes("exchange/quotes")) return "exchangeLiquidity";
2717
+ if (path5.includes("fear-and-greed")) return "fearGreed";
2718
+ if (path5.includes("cmc100")) return "cmc100";
2719
+ if (path5.includes("cmc20")) return "cmc20";
2720
+ return path5;
2721
+ };
2722
+ var recordCoinMarketCapFetchStats = (stats, path5, creditCount) => {
2720
2723
  if (!stats) return;
2721
- const existing = stats.byPath.get(path3) ?? { requests: 0, credits: 0 };
2724
+ const existing = stats.byPath.get(path5) ?? { requests: 0, credits: 0 };
2722
2725
  existing.requests += 1;
2723
2726
  existing.credits += creditCount ?? 0;
2724
2727
  stats.requests += 1;
2725
2728
  stats.totalCredits += creditCount ?? 0;
2726
- stats.byPath.set(path3, existing);
2729
+ stats.byPath.set(path5, existing);
2727
2730
  };
2728
2731
  var formatCoinMarketCapFetchStats = (stats) => {
2729
2732
  const byPath = [...stats.byPath.entries()].map(
2730
- ([path3, item]) => `${getCoinMarketCapPathLabel(path3)}=${item.requests}req/${item.credits}cr`
2733
+ ([path5, item]) => `${getCoinMarketCapPathLabel(path5)}=${item.requests}req/${item.credits}cr`
2731
2734
  ).join(" ");
2732
2735
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
2733
2736
  };
@@ -4323,6 +4326,29 @@ import_args.default.option(
4323
4326
  "Save compact replay chart data for the strategies UI",
4324
4327
  false
4325
4328
  );
4329
+ import_args.default.option(
4330
+ "allData",
4331
+ "Use the complete cached candle range for the selected provider, interval, and symbols",
4332
+ false
4333
+ );
4334
+ import_args.default.option(
4335
+ "maxLossValue",
4336
+ "Override MAX_LOSS_VALUE for every selected runtime strategy without changing its declaration"
4337
+ );
4338
+ import_args.default.option(
4339
+ "portfolioReport",
4340
+ "Write aggregate portfolio metrics and SVG charts; skip runtime/exchange comparison",
4341
+ false
4342
+ );
4343
+ import_args.default.option(
4344
+ ["B", "portfolioBatchSize"],
4345
+ "Maximum number of symbols held in memory by one portfolio replay batch",
4346
+ 4
4347
+ );
4348
+ import_args.default.option(
4349
+ ["X", "portfolioOutputSuffix"],
4350
+ "Optional safe suffix for parallel portfolio report shards"
4351
+ );
4326
4352
  import_args.default.option(["L", "showTickersList"], "Just show only ticker list", false);
4327
4353
  import_args.default.option(["U", "user"], "Use user config", "root");
4328
4354
  import_args.default.option(
@@ -4954,6 +4980,8 @@ var compactReplaySignal = (signal) => {
4954
4980
  // src/lib/replay/historicalSignalsReplay.ts
4955
4981
  var import_chalk9 = __toESM(require("chalk"));
4956
4982
  var import_progress4 = __toESM(require("progress"));
4983
+ var import_node_path2 = __toESM(require("path"));
4984
+ var import_node_module = require("module");
4957
4985
  var import_strategies3 = require("@tradejs/core/strategies");
4958
4986
  var import_time3 = require("@tradejs/core/time");
4959
4987
  var import_logger = require("@tradejs/infra/logger");
@@ -5037,7 +5065,8 @@ var createPortfolioReplayConnector = (connector) => {
5037
5065
  close: {
5038
5066
  timestamp,
5039
5067
  amount: (0, import_math.round)(amount)
5040
- }
5068
+ },
5069
+ netProfit: (0, import_math.round)(positionState.currentProfit)
5041
5070
  };
5042
5071
  positionLog.push(nextPositionLog);
5043
5072
  appendStrategyPositionLog(
@@ -5487,7 +5516,8 @@ var prepareHistoricalReplay = async (context, adapters) => {
5487
5516
  strategies,
5488
5517
  references
5489
5518
  } = context;
5490
- const cycleSymbolsByTimestamp = /* @__PURE__ */ new Map();
5519
+ const symbolRuntimes = [];
5520
+ const orderedTimestamps = /* @__PURE__ */ new Set();
5491
5521
  const sharedReplayKeyPrefixes = [];
5492
5522
  const runtimeLineages = [];
5493
5523
  let skippedSymbols = 0;
@@ -5594,19 +5624,18 @@ var prepareHistoricalReplay = async (context, adapters) => {
5594
5624
  currentIndex: 0,
5595
5625
  strategies: strategiesForSymbol
5596
5626
  };
5627
+ symbolRuntimes.push(symbolRuntime);
5597
5628
  adapters.progress.tick({
5598
5629
  skipped: adapters.display.skipped(skippedSymbols),
5599
5630
  symbol: adapters.display.symbol(symbol)
5600
5631
  });
5601
5632
  for (const candle of preparedData.replayData) {
5602
- const bucket = cycleSymbolsByTimestamp.get(candle.timestamp) ?? [];
5603
- bucket.push(symbolRuntime);
5604
- cycleSymbolsByTimestamp.set(candle.timestamp, bucket);
5633
+ orderedTimestamps.add(candle.timestamp);
5605
5634
  }
5606
5635
  }
5607
5636
  return {
5608
- cycleSymbolsByTimestamp,
5609
- orderedTimestamps: [...cycleSymbolsByTimestamp.keys()].sort(
5637
+ symbolRuntimes,
5638
+ orderedTimestamps: [...orderedTimestamps].sort(
5610
5639
  (left, right) => left - right
5611
5640
  ),
5612
5641
  sharedReplayKeyPrefixes,
@@ -5639,7 +5668,13 @@ var appendReplaySignalEvaluationRun = ({
5639
5668
  }
5640
5669
  ];
5641
5670
  };
5642
- var createHistoricalReplaySkipEvidence = (runtimeLineages) => {
5671
+ var createHistoricalReplaySkipEvidence = (runtimeLineages, enabled = true) => {
5672
+ if (!enabled) {
5673
+ return {
5674
+ record: () => void 0,
5675
+ values: () => []
5676
+ };
5677
+ }
5643
5678
  const lineageByStrategySymbol = new Map(
5644
5679
  runtimeLineages.map((record2) => [
5645
5680
  replayScopeKey(record2.strategy, record2.symbol),
@@ -5688,7 +5723,9 @@ var createHistoricalReplaySkipEvidence = (runtimeLineages) => {
5688
5723
 
5689
5724
  // src/lib/replay/historicalSignalsReplayExecution.ts
5690
5725
  var advanceCycleMarket = async (plan, connector, timestamp) => {
5691
- const cycleSymbols = plan.cycleSymbolsByTimestamp.get(timestamp) ?? [];
5726
+ const cycleSymbols = plan.symbolRuntimes.filter(
5727
+ (symbolRuntime) => symbolRuntime.replayData[symbolRuntime.currentIndex]?.timestamp === timestamp
5728
+ );
5692
5729
  for (const symbolRuntime of cycleSymbols) {
5693
5730
  const candle = symbolRuntime.replayData[symbolRuntime.currentIndex];
5694
5731
  if (candle?.timestamp === timestamp) {
@@ -5712,8 +5749,17 @@ var executeHistoricalReplay = async (context, adapters) => {
5712
5749
  const signals = [];
5713
5750
  let abortedCycles = 0;
5714
5751
  const skipEvidence = createHistoricalReplaySkipEvidence(
5715
- context.plan.runtimeLineages
5752
+ context.plan.runtimeLineages,
5753
+ context.collectSkipEvidence
5716
5754
  );
5755
+ const reportProgress = (timestamp) => {
5756
+ if (adapters.progress.enabled === false) return;
5757
+ adapters.progress.tick({
5758
+ signals: adapters.display.signals(signals.length),
5759
+ aborted: adapters.display.aborted(abortedCycles),
5760
+ ts: adapters.display.timestamp(timestamp)
5761
+ });
5762
+ };
5717
5763
  try {
5718
5764
  for (const timestamp of context.plan.orderedTimestamps) {
5719
5765
  const cycleStartedAt = adapters.clock.now();
@@ -5735,11 +5781,7 @@ var executeHistoricalReplay = async (context, adapters) => {
5735
5781
  durationMs: adapters.clock.now() - cycleStartedAt
5736
5782
  });
5737
5783
  advanceAbortedCycle(cycleSymbols, timestamp);
5738
- adapters.progress.tick({
5739
- signals: adapters.display.signals(signals.length),
5740
- aborted: adapters.display.aborted(abortedCycles),
5741
- ts: adapters.display.timestamp(timestamp)
5742
- });
5784
+ reportProgress(timestamp);
5743
5785
  continue;
5744
5786
  }
5745
5787
  const cycleSignals = [];
@@ -5768,7 +5810,7 @@ var executeHistoricalReplay = async (context, adapters) => {
5768
5810
  result.runtimeLineage = strategyRuntime.runtimeLineage;
5769
5811
  await adapters.enrichSignal(result);
5770
5812
  cycleSignals.push(result);
5771
- signals.push(result);
5813
+ if (context.collectSignals !== false) signals.push(result);
5772
5814
  }
5773
5815
  }
5774
5816
  symbolRuntime.currentIndex += 1;
@@ -5779,11 +5821,7 @@ var executeHistoricalReplay = async (context, adapters) => {
5779
5821
  status: "completed",
5780
5822
  durationMs: adapters.clock.now() - cycleStartedAt
5781
5823
  });
5782
- adapters.progress.tick({
5783
- signals: adapters.display.signals(signals.length),
5784
- aborted: adapters.display.aborted(abortedCycles),
5785
- ts: adapters.display.timestamp(timestamp)
5786
- });
5824
+ reportProgress(timestamp);
5787
5825
  }
5788
5826
  } finally {
5789
5827
  for (const keyPrefix of context.plan.sharedReplayKeyPrefixes) {
@@ -5800,6 +5838,7 @@ var executeHistoricalReplay = async (context, adapters) => {
5800
5838
 
5801
5839
  // src/lib/replay/historicalSignalsReplayResults.ts
5802
5840
  var import_backtest = require("@tradejs/core/backtest");
5841
+ var import_constants4 = require("@tradejs/core/constants");
5803
5842
  var emptyStat = () => ({
5804
5843
  orders: 0,
5805
5844
  wins: 0,
@@ -5807,6 +5846,25 @@ var emptyStat = () => ({
5807
5846
  netProfit: 0,
5808
5847
  amount: 0
5809
5848
  });
5849
+ var positionLogForStats = (positionLog) => {
5850
+ let amount = import_constants4.INITIAL_BACKTEST_AMOUNT;
5851
+ return [...positionLog].sort(
5852
+ (left, right) => left.close.timestamp - right.close.timestamp || left.open.timestamp - right.open.timestamp
5853
+ ).map((position) => {
5854
+ const pnl = Number.isFinite(position.netProfit) ? Number(position.netProfit) : position.close.amount - position.open.amount;
5855
+ const next = {
5856
+ ...position,
5857
+ open: { ...position.open, amount },
5858
+ close: { ...position.close, amount: amount + pnl },
5859
+ netProfit: pnl
5860
+ };
5861
+ amount += pnl;
5862
+ return next;
5863
+ });
5864
+ };
5865
+ var calculateReplayStat = (positionLog) => positionLog.length ? (0, import_backtest.calculateStatsFull)(positionLogForStats(positionLog)) : emptyStat();
5866
+ var byTimestamp = (left, right) => left.timestamp - right.timestamp;
5867
+ var byPositionClose = (left, right) => left.close.timestamp - right.close.timestamp;
5810
5868
  var collectHistoricalReplayResult = (context) => ({
5811
5869
  strategies: context.strategies.map(({ strategyName, strategyConfig }) => {
5812
5870
  const orderLog = context.artifacts.orderLogByStrategy.get(strategyName) ?? [];
@@ -5816,7 +5874,7 @@ var collectHistoricalReplayResult = (context) => ({
5816
5874
  strategyConfig,
5817
5875
  orderLog,
5818
5876
  positionLog,
5819
- stat: positionLog.length ? (0, import_backtest.calculateStatsFull)(positionLog) : emptyStat()
5877
+ stat: calculateReplayStat(positionLog)
5820
5878
  };
5821
5879
  }),
5822
5880
  signals: context.signals,
@@ -5827,8 +5885,78 @@ var collectHistoricalReplayResult = (context) => ({
5827
5885
  runtimeLineages: context.runtimeLineages,
5828
5886
  replayLineageScopes: context.replayLineageScopes
5829
5887
  });
5888
+ var compactHistoricalReplayResultForPortfolio = (result) => ({
5889
+ ...result,
5890
+ strategies: result.strategies.map((strategy) => ({
5891
+ ...strategy,
5892
+ orderLog: []
5893
+ })),
5894
+ signals: [],
5895
+ orderLog: [],
5896
+ replayLineageScopes: []
5897
+ });
5898
+ var mergeHistoricalReplayResults = (parts) => {
5899
+ if (!parts.length) {
5900
+ throw new Error("Cannot merge an empty historical replay result list");
5901
+ }
5902
+ const strategyNames = parts[0].strategies.map(
5903
+ (strategy) => strategy.strategyName
5904
+ );
5905
+ const strategies = strategyNames.map((strategyName) => {
5906
+ const entries = parts.map((part) => {
5907
+ const entry = part.strategies.find(
5908
+ (strategy) => strategy.strategyName === strategyName
5909
+ );
5910
+ if (!entry) {
5911
+ throw new Error(`Replay batch is missing strategy ${strategyName}`);
5912
+ }
5913
+ return entry;
5914
+ });
5915
+ const positionLog = entries.flatMap((entry) => entry.positionLog).sort(byPositionClose);
5916
+ return {
5917
+ strategyName,
5918
+ strategyConfig: entries[0].strategyConfig,
5919
+ orderLog: entries.flatMap((entry) => entry.orderLog).sort(byTimestamp),
5920
+ positionLog,
5921
+ stat: calculateReplayStat(positionLog)
5922
+ };
5923
+ });
5924
+ return {
5925
+ strategies,
5926
+ signals: parts.flatMap((part) => part.signals).sort(byTimestamp),
5927
+ orderLog: parts.flatMap((part) => part.orderLog).sort(byTimestamp),
5928
+ positionLog: parts.flatMap((part) => part.positionLog).sort(byPositionClose),
5929
+ cycleCount: parts.reduce((sum, part) => sum + part.cycleCount, 0),
5930
+ abortedCycles: parts.reduce((sum, part) => sum + part.abortedCycles, 0),
5931
+ runtimeLineages: parts.flatMap((part) => part.runtimeLineages),
5932
+ replayLineageScopes: parts.flatMap((part) => part.replayLineageScopes)
5933
+ };
5934
+ };
5830
5935
 
5831
5936
  // src/lib/replay/historicalSignalsReplay.ts
5937
+ var projectReplayCacheReleasers;
5938
+ var getProjectReplayCacheReleasers = () => {
5939
+ if (projectReplayCacheReleasers !== void 0) {
5940
+ return projectReplayCacheReleasers;
5941
+ }
5942
+ try {
5943
+ const requireFromProject = (0, import_node_module.createRequire)(
5944
+ import_node_path2.default.join(replayProjectRoot, "package.json")
5945
+ );
5946
+ projectReplayCacheReleasers = requireFromProject(
5947
+ "@tradejs/core/strategies"
5948
+ );
5949
+ } catch {
5950
+ projectReplayCacheReleasers = null;
5951
+ }
5952
+ return projectReplayCacheReleasers;
5953
+ };
5954
+ var releaseReplayCacheCopies = (keyPrefix, localRelease, projectRelease) => {
5955
+ localRelease(keyPrefix);
5956
+ if (projectRelease && projectRelease !== localRelease) {
5957
+ projectRelease(keyPrefix);
5958
+ }
5959
+ };
5832
5960
  var loadRuntimeStrategies = async (runtimeStrategies) => {
5833
5961
  const strategies = await Promise.all(
5834
5962
  runtimeStrategies.map(async (runtimeStrategy) => {
@@ -5843,16 +5971,19 @@ var loadRuntimeStrategies = async (runtimeStrategies) => {
5843
5971
  strategyConfig,
5844
5972
  selection
5845
5973
  } = runtimeStrategy;
5974
+ const strategyModule = strategyPackage.startsWith("@tradejs/strategy-") ? strategyPackage : void 0;
5846
5975
  const strategyCreator = await (0, import_strategies4.getStrategyCreator)(
5847
5976
  strategyName,
5848
- replayProjectRoot
5977
+ replayProjectRoot,
5978
+ strategyModule
5849
5979
  );
5850
5980
  if (!strategyCreator) {
5851
5981
  throw new Error(`Unknown strategy: ${strategyName}`);
5852
5982
  }
5853
5983
  const installed = await (0, import_runtimeStrategies3.getRuntimeStrategyPackageMetadata)({
5854
5984
  strategyName,
5855
- projectRoot: replayProjectRoot
5985
+ projectRoot: replayProjectRoot,
5986
+ strategyModule
5856
5987
  });
5857
5988
  if (installed.strategyPackage !== strategyPackage || installed.strategyPackageVersion !== strategyPackageVersion || JSON.stringify(installed.strategyDependencyVersions) !== JSON.stringify(strategyDependencyVersions) || installed.runtimePackageVersion !== runtimePackageVersion) {
5858
5989
  throw new Error(
@@ -5915,10 +6046,41 @@ var getConnectorName = (connector) => {
5915
6046
  const name = "name" in connector ? connector.name : void 0;
5916
6047
  return String(name || import_connectors6.DEFAULT_CONNECTOR_NAME).trim() || import_connectors6.DEFAULT_CONNECTOR_NAME;
5917
6048
  };
6049
+ var loadHistoricalReplayReferences = async ({
6050
+ preparedRun,
6051
+ interval
6052
+ }) => {
6053
+ const connectorName = getConnectorName(preparedRun.marketConnector);
6054
+ const binanceConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Binance") : preparedRun.marketConnector;
6055
+ const coinbaseConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Coinbase") : preparedRun.marketConnector;
6056
+ const request = {
6057
+ symbol: "BTCUSDT",
6058
+ start: preparedRun.preloadStart,
6059
+ end: preparedRun.window.end,
6060
+ cacheOnly: true,
6061
+ interval
6062
+ };
6063
+ const [btcBinanceData, btcCoinbaseData, btcMarketData, ethMarketData] = await Promise.all([
6064
+ binanceConnector.kline(request),
6065
+ coinbaseConnector.kline(request),
6066
+ preparedRun.marketConnector.kline(request),
6067
+ preparedRun.marketConnector.kline({ ...request, symbol: "ETHUSDT" })
6068
+ ]);
6069
+ return {
6070
+ btcMarketData,
6071
+ ethMarketData,
6072
+ btcBinanceData,
6073
+ btcCoinbaseData
6074
+ };
6075
+ };
5918
6076
  var runHistoricalSignalsReplay = async ({
5919
6077
  preparedRun,
5920
6078
  interval,
5921
- runtimeStrategies
6079
+ runtimeStrategies,
6080
+ references,
6081
+ showProgress = true,
6082
+ collectSkipEvidence = true,
6083
+ collectSignals = true
5922
6084
  }) => {
5923
6085
  const startedAt = Date.now();
5924
6086
  const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
@@ -5928,38 +6090,7 @@ var runHistoricalSignalsReplay = async ({
5928
6090
  preparedRun.marketConnector
5929
6091
  );
5930
6092
  const connectorName = getConnectorName(preparedRun.marketConnector);
5931
- const binanceConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Binance") : preparedRun.marketConnector;
5932
- const coinbaseConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Coinbase") : preparedRun.marketConnector;
5933
- const [btcBinanceData, btcCoinbaseData] = await Promise.all([
5934
- binanceConnector.kline({
5935
- symbol: "BTCUSDT",
5936
- start: preparedRun.preloadStart,
5937
- end: preparedRun.window.end,
5938
- cacheOnly: true,
5939
- interval
5940
- }),
5941
- coinbaseConnector.kline({
5942
- symbol: "BTCUSDT",
5943
- start: preparedRun.preloadStart,
5944
- end: preparedRun.window.end,
5945
- cacheOnly: true,
5946
- interval
5947
- })
5948
- ]);
5949
- const btcMarketData = await preparedRun.marketConnector.kline({
5950
- symbol: "BTCUSDT",
5951
- start: preparedRun.preloadStart,
5952
- end: preparedRun.window.end,
5953
- cacheOnly: true,
5954
- interval
5955
- });
5956
- const ethMarketData = await preparedRun.marketConnector.kline({
5957
- symbol: "ETHUSDT",
5958
- start: preparedRun.preloadStart,
5959
- end: preparedRun.window.end,
5960
- cacheOnly: true,
5961
- interval
5962
- });
6093
+ const referenceData = references ?? await loadHistoricalReplayReferences({ preparedRun, interval });
5963
6094
  const prepareBar = new import_progress4.default(
5964
6095
  "prepare :current/:total [:bar][:percent] skipped=:skipped :etas(s) :symbol",
5965
6096
  {
@@ -5977,10 +6108,10 @@ var runHistoricalSignalsReplay = async ({
5977
6108
  replayConnector,
5978
6109
  strategies: loadedStrategies,
5979
6110
  references: {
5980
- btcMarketData,
5981
- ethMarketData,
5982
- btcBinanceData,
5983
- btcCoinbaseData
6111
+ btcMarketData: referenceData.btcMarketData,
6112
+ ethMarketData: referenceData.ethMarketData,
6113
+ btcBinanceData: referenceData.btcBinanceData,
6114
+ btcCoinbaseData: referenceData.btcCoinbaseData
5984
6115
  }
5985
6116
  },
5986
6117
  {
@@ -6012,12 +6143,15 @@ var runHistoricalSignalsReplay = async ({
6012
6143
  {
6013
6144
  plan,
6014
6145
  connector: replayConnector,
6146
+ collectSkipEvidence,
6147
+ collectSignals,
6015
6148
  hooks: projectHooks,
6016
6149
  hookContext
6017
6150
  },
6018
6151
  {
6019
6152
  clock: { now: Date.now },
6020
6153
  progress: {
6154
+ enabled: showProgress,
6021
6155
  tick: (tokens) => cycleBar.tick(1, tokens)
6022
6156
  },
6023
6157
  display: {
@@ -6028,8 +6162,16 @@ var runHistoricalSignalsReplay = async ({
6028
6162
  invokeBeforeSignals: invokeBeforeSignalsHooks,
6029
6163
  invokeAfterSignals: invokeAfterSignalsHooks,
6030
6164
  enrichSignal: (signal) => (0, import_strategies4.enrichSignalWithBinanceMarketContext)({ signal, env: "PARITY" }),
6031
- releaseIndicatorsCache: import_strategies3.releaseStrategyIndicatorsReplayCache,
6032
- releaseReplayCache: import_strategies3.releaseStrategyReplayCache
6165
+ releaseIndicatorsCache: (keyPrefix) => releaseReplayCacheCopies(
6166
+ keyPrefix,
6167
+ import_strategies3.releaseStrategyIndicatorsReplayCache,
6168
+ getProjectReplayCacheReleasers()?.releaseStrategyIndicatorsReplayCache
6169
+ ),
6170
+ releaseReplayCache: (keyPrefix) => releaseReplayCacheCopies(
6171
+ keyPrefix,
6172
+ import_strategies3.releaseStrategyReplayCache,
6173
+ getProjectReplayCacheReleasers()?.releaseStrategyReplayCache
6174
+ )
6033
6175
  }
6034
6176
  );
6035
6177
  const artifacts = replayConnector.getReplayArtifacts();
@@ -6400,11 +6542,16 @@ var saveAndPrintReplayResultsByStrategy = async ({
6400
6542
  const backtestEntries = replayResult.strategies.flatMap(
6401
6543
  ({ orderLog }) => extractBacktestEntryParityEntries(orderLog)
6402
6544
  );
6403
- const summaries = replayResult.strategies.map(({ strategyName, strategyConfig, stat }) => {
6404
- const orders = stat?.orders ?? 0;
6405
- const wins = stat?.wins ?? 0;
6406
- const losses = stat?.losses ?? 0;
6407
- const netProfit = Number((stat?.netProfit ?? 0).toFixed(2));
6545
+ const summaries = replayResult.strategies.map(({ strategyName, strategyConfig, positionLog }) => {
6546
+ const realizedPnl = positionLog.map(
6547
+ (position) => Number.isFinite(position.netProfit) ? Number(position.netProfit) : position.close.amount - position.open.amount
6548
+ );
6549
+ const orders = realizedPnl.length;
6550
+ const wins = realizedPnl.filter((pnl) => pnl > 0).length;
6551
+ const losses = realizedPnl.filter((pnl) => pnl <= 0).length;
6552
+ const netProfit = Number(
6553
+ realizedPnl.reduce((sum, pnl) => sum + pnl, 0).toFixed(2)
6554
+ );
6408
6555
  const winRate = orders > 0 ? Number((wins / orders * 100).toFixed(2)) : 0;
6409
6556
  const avgTradeProfit = orders > 0 ? Number((netProfit / orders).toFixed(2)) : 0;
6410
6557
  const tradedSymbols = new Set(
@@ -6531,7 +6678,7 @@ var summarizeTradeParityByStrategy = ({
6531
6678
  var import_redis4 = require("@tradejs/infra/redis");
6532
6679
 
6533
6680
  // src/lib/runtimeSignalsStorage.ts
6534
- var import_constants4 = require("@tradejs/core/constants");
6681
+ var import_constants5 = require("@tradejs/core/constants");
6535
6682
  var import_data2 = require("@tradejs/core/data");
6536
6683
  var import_time4 = require("@tradejs/core/time");
6537
6684
  var isRuntimeSignalBucketRef = (value) => {
@@ -6641,7 +6788,7 @@ var loadRuntimeLineageScopes = async (userName, {
6641
6788
  };
6642
6789
 
6643
6790
  // src/lib/runtimeTradeSync.ts
6644
- var import_constants5 = require("@tradejs/core/constants");
6791
+ var import_constants6 = require("@tradejs/core/constants");
6645
6792
  var import_time5 = require("@tradejs/core/time");
6646
6793
  var import_redis5 = require("@tradejs/infra/redis");
6647
6794
  var CLOSED_PNL_LIMIT = 100;
@@ -6882,7 +7029,7 @@ var syncRuntimeTrades = async ({
6882
7029
  };
6883
7030
  await Promise.all([
6884
7031
  (0, import_redis5.setData)(import_redis5.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
6885
- expire: import_constants5.TTL_1M
7032
+ expire: import_constants6.TTL_1M
6886
7033
  }),
6887
7034
  (0, import_redis5.setHashJsonField)(
6888
7035
  import_redis5.redisKeys.runtimeTradeBucket(
@@ -6891,7 +7038,7 @@ var syncRuntimeTrades = async ({
6891
7038
  ),
6892
7039
  trade.orderId,
6893
7040
  nextTrade,
6894
- { expire: import_constants5.TTL_1M }
7041
+ { expire: import_constants6.TTL_1M }
6895
7042
  ),
6896
7043
  (0, import_redis5.setHashJsonField)(
6897
7044
  import_redis5.redisKeys.runtimeClosedTradeBucket(
@@ -6900,7 +7047,7 @@ var syncRuntimeTrades = async ({
6900
7047
  ),
6901
7048
  trade.orderId,
6902
7049
  nextTrade,
6903
- { expire: import_constants5.TTL_1M }
7050
+ { expire: import_constants6.TTL_1M }
6904
7051
  ),
6905
7052
  ...isCurrentActiveTrade ? [(0, import_redis5.delKey)(activeTradeKey)] : []
6906
7053
  ]);
@@ -8841,7 +8988,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
8841
8988
 
8842
8989
  // src/lib/replay/outputReport.ts
8843
8990
  var import_promises2 = __toESM(require("fs/promises"));
8844
- var import_node_path2 = __toESM(require("path"));
8991
+ var import_node_path3 = __toESM(require("path"));
8845
8992
  var import_data3 = require("@tradejs/core/data");
8846
8993
  var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
8847
8994
  timeZone: "Europe/Moscow",
@@ -9145,7 +9292,7 @@ var writeReplayOutputReport = async ({
9145
9292
  strategySnapshot,
9146
9293
  runtimeComparison
9147
9294
  }) => {
9148
- const outputDir = import_node_path2.default.resolve(projectRoot, "data/replay/output");
9295
+ const outputDir = import_node_path3.default.resolve(projectRoot, "data/replay/output");
9149
9296
  await import_promises2.default.mkdir(outputDir, { recursive: true });
9150
9297
  const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
9151
9298
  const report = {
@@ -9181,8 +9328,8 @@ var writeReplayOutputReport = async ({
9181
9328
  runtimeComparison,
9182
9329
  perTradeAnalysis
9183
9330
  };
9184
- const markdownPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.md`);
9185
- const jsonPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.json`);
9331
+ const markdownPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.md`);
9332
+ const jsonPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.json`);
9186
9333
  const lines = [
9187
9334
  "# Replay Report",
9188
9335
  "",
@@ -9258,6 +9405,330 @@ var writeReplayOutputReport = async ({
9258
9405
  };
9259
9406
  };
9260
9407
 
9408
+ // src/lib/replay/portfolioReport.ts
9409
+ var import_promises3 = __toESM(require("fs/promises"));
9410
+ var import_node_path4 = __toESM(require("path"));
9411
+ var import_node_crypto2 = require("crypto");
9412
+ var DAY_MS4 = 864e5;
9413
+ var WINDOWS = [365, 180, 90, 30, 7];
9414
+ var COLORS = [
9415
+ "#2563eb",
9416
+ "#dc2626",
9417
+ "#059669",
9418
+ "#7c3aed",
9419
+ "#ea580c",
9420
+ "#0891b2",
9421
+ "#c026d3",
9422
+ "#4d7c0f",
9423
+ "#be123c",
9424
+ "#4338ca",
9425
+ "#0f766e"
9426
+ ];
9427
+ var finite = (value, fallback = 0) => {
9428
+ const number = Number(value);
9429
+ return Number.isFinite(number) ? number : fallback;
9430
+ };
9431
+ var round2 = (value, digits = 8) => Number(value.toFixed(digits));
9432
+ var tradesFromPositionLog = (strategyName, positionLog) => positionLog.map((position) => ({
9433
+ strategyName,
9434
+ direction: position.direction,
9435
+ openedAt: position.open.timestamp,
9436
+ exitedAt: position.close.timestamp,
9437
+ pnl: finite(
9438
+ position.netProfit,
9439
+ finite(position.close.amount) - finite(position.open.amount)
9440
+ )
9441
+ }));
9442
+ var curveForTrades = (trades, start, end) => {
9443
+ let value = 0;
9444
+ const realized = [...trades].sort(
9445
+ (left, right) => left.exitedAt - right.exitedAt || left.strategyName.localeCompare(right.strategyName)
9446
+ ).map((trade) => ({
9447
+ timestamp: trade.exitedAt,
9448
+ value: value = round2(value + trade.pnl)
9449
+ }));
9450
+ return [
9451
+ { timestamp: start, value: 0 },
9452
+ ...realized,
9453
+ { timestamp: end, value }
9454
+ ];
9455
+ };
9456
+ var drawdownCurve = (equity) => {
9457
+ let peak = 0;
9458
+ return equity.map((point) => {
9459
+ peak = Math.max(peak, point.value);
9460
+ return { timestamp: point.timestamp, value: round2(point.value - peak) };
9461
+ });
9462
+ };
9463
+ var maxDrawdown = (trades) => {
9464
+ let equity = 0;
9465
+ let peak = 0;
9466
+ let worst = 0;
9467
+ for (const trade of [...trades].sort((a, b) => a.exitedAt - b.exitedAt)) {
9468
+ equity += trade.pnl;
9469
+ peak = Math.max(peak, equity);
9470
+ worst = Math.max(worst, peak - equity);
9471
+ }
9472
+ return worst;
9473
+ };
9474
+ var calculatePortfolioMetricRow = ({
9475
+ trades,
9476
+ window,
9477
+ scope,
9478
+ cohort,
9479
+ start,
9480
+ end
9481
+ }) => {
9482
+ const selected = trades.filter(
9483
+ (trade) => trade.exitedAt >= start && trade.exitedAt < end && (cohort === "ALL" || trade.direction === cohort)
9484
+ );
9485
+ const grossProfit = selected.filter((trade) => trade.pnl > 0).reduce((sum, trade) => sum + trade.pnl, 0);
9486
+ const grossLoss = Math.abs(
9487
+ selected.filter((trade) => trade.pnl < 0).reduce((sum, trade) => sum + trade.pnl, 0)
9488
+ );
9489
+ const pnl = selected.reduce((sum, trade) => sum + trade.pnl, 0);
9490
+ const wins = selected.filter((trade) => trade.pnl > 0).length;
9491
+ const losses = selected.filter((trade) => trade.pnl <= 0).length;
9492
+ const days = Math.max(1, (end - start) / DAY_MS4);
9493
+ return {
9494
+ window,
9495
+ scope,
9496
+ cohort,
9497
+ n: selected.length,
9498
+ wins,
9499
+ losses,
9500
+ pnl: round2(pnl),
9501
+ pnlPerTrade: selected.length ? round2(pnl / selected.length) : null,
9502
+ profitFactor: grossLoss > 0 ? round2(grossProfit / grossLoss) : null,
9503
+ winRate: selected.length ? round2(wins / selected.length * 100) : null,
9504
+ realizedMaxDrawdown: round2(maxDrawdown(selected)),
9505
+ cadencePerDay: round2(selected.length / days)
9506
+ };
9507
+ };
9508
+ var windowBounds = (start, end) => [
9509
+ { label: "full", start, end },
9510
+ ...WINDOWS.map((days) => ({
9511
+ label: `${days}d`,
9512
+ start: Math.max(start, end - days * DAY_MS4),
9513
+ end
9514
+ }))
9515
+ ];
9516
+ var escapeHtml = (value) => String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
9517
+ var display = (value, digits = 2) => value == null || !Number.isFinite(value) ? "n/a" : value.toFixed(digits);
9518
+ var displayPercent = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${value.toFixed(1)}%`;
9519
+ var downsample = (points, limit = 2e3) => {
9520
+ if (points.length <= limit) return points;
9521
+ const result = [];
9522
+ const step = (points.length - 1) / (limit - 1);
9523
+ for (let index = 0; index < limit; index += 1) {
9524
+ result.push(points[Math.round(index * step)]);
9525
+ }
9526
+ return result;
9527
+ };
9528
+ var renderSvg = ({
9529
+ title,
9530
+ series,
9531
+ start,
9532
+ end
9533
+ }) => {
9534
+ const width = 1280;
9535
+ const legendColumns = 3;
9536
+ const legendRows = Math.max(1, Math.ceil(series.length / legendColumns));
9537
+ const height = 640 + legendRows * 20;
9538
+ const left = 78;
9539
+ const right = 24;
9540
+ const top = 58;
9541
+ const bottom = 90 + legendRows * 20;
9542
+ const allValues = series.flatMap(
9543
+ ({ points }) => points.map(({ value }) => value)
9544
+ );
9545
+ const min = Math.min(0, ...allValues);
9546
+ const max = Math.max(0, ...allValues);
9547
+ const span = Math.max(1, max - min);
9548
+ const timeSpan = Math.max(1, end - start);
9549
+ const x = (timestamp) => left + (timestamp - start) / timeSpan * (width - left - right);
9550
+ const y = (value) => top + (max - value) / span * (height - top - bottom);
9551
+ const grid = Array.from({ length: 6 }, (_, index) => {
9552
+ const value = max - span * index / 5;
9553
+ const py = y(value);
9554
+ return `<line x1="${left}" y1="${py}" x2="${width - right}" y2="${py}" stroke="#e5e7eb"/><text x="${left - 10}" y="${py + 4}" text-anchor="end" font-size="12" fill="#64748b">${value.toFixed(1)}</text>`;
9555
+ }).join("");
9556
+ const paths = series.map(({ name, color, points, strokeWidth = 2 }) => {
9557
+ const sampled = downsample(points);
9558
+ const d = sampled.map(
9559
+ (point, index) => `${index ? "L" : "M"}${x(point.timestamp).toFixed(2)},${y(point.value).toFixed(2)}`
9560
+ ).join(" ");
9561
+ return `<path d="${d}" fill="none" stroke="${color}" stroke-width="${strokeWidth}"/><title>${escapeHtml(name)}</title>`;
9562
+ }).join("");
9563
+ const legend = series.map(
9564
+ ({ name, color }, index) => `<g transform="translate(${left + index % legendColumns * 390},${height - 20 - (legendRows - 1 - Math.floor(index / legendColumns)) * 20})"><line x1="0" y1="-4" x2="20" y2="-4" stroke="${color}" stroke-width="3"/><text x="26" y="0" font-size="12" fill="#334155">${escapeHtml(name)}</text></g>`
9565
+ ).join("");
9566
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="white"/><text x="${left}" y="32" font-size="22" font-family="sans-serif" fill="#0f172a">${escapeHtml(title)}</text><g font-family="sans-serif">${grid}<line x1="${left}" y1="${y(0)}" x2="${width - right}" y2="${y(0)}" stroke="#94a3b8"/>${paths}<text x="${left}" y="${height - bottom + 26}" font-size="12" fill="#64748b">${new Date(start).toISOString().slice(0, 10)}</text><text x="${width - right}" y="${height - bottom + 26}" text-anchor="end" font-size="12" fill="#64748b">${new Date(end).toISOString().slice(0, 10)}</text>${legend}</g></svg>`;
9567
+ };
9568
+ var metricTable = (rows) => {
9569
+ const body = rows.map(
9570
+ (row) => `<tr><td>${escapeHtml(row.window)}</td><td>${escapeHtml(row.scope)}</td><td>${row.cohort}</td><td>${row.n}</td><td>${display(row.pnl)}</td><td>${display(row.pnlPerTrade)}</td><td>${display(row.profitFactor)}</td><td>${displayPercent(row.winRate)}</td><td>${display(row.realizedMaxDrawdown)}</td><td>${display(row.cadencePerDay, 3)}</td></tr>`
9571
+ ).join("");
9572
+ return `<table><thead><tr><th>Window</th><th>Scope</th><th>Cohort</th><th>N</th><th>PnL</th><th>PnL/trade</th><th>PF</th><th>WR</th><th>Realized MaxDD</th><th>Cadence/day</th></tr></thead><tbody>${body}</tbody></table>`;
9573
+ };
9574
+ var markdownTable = (rows) => [
9575
+ "| Window | Scope | Cohort | N | PnL | PnL/trade | PF | WR | Realized MaxDD | Cadence/day |",
9576
+ "| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
9577
+ ...rows.map(
9578
+ (row) => `| ${row.window} | ${row.scope} | ${row.cohort} | ${row.n} | ${display(row.pnl)} | ${display(row.pnlPerTrade)} | ${display(row.profitFactor)} | ${displayPercent(row.winRate)} | ${display(row.realizedMaxDrawdown)} | ${display(row.cadencePerDay, 3)} |`
9579
+ )
9580
+ ].join("\n");
9581
+ var writePortfolioReport = async ({
9582
+ projectRoot,
9583
+ timestamp,
9584
+ replayResult,
9585
+ window,
9586
+ lineage,
9587
+ command
9588
+ }) => {
9589
+ const outputDir = import_node_path4.default.resolve(projectRoot, "data/replay/output");
9590
+ await import_promises3.default.mkdir(outputDir, { recursive: true });
9591
+ const trades = replayResult.strategies.flatMap(
9592
+ (strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
9593
+ );
9594
+ const bounds = windowBounds(window.start, window.end);
9595
+ const portfolioRows = bounds.flatMap(
9596
+ (bound) => ["ALL", "LONG", "SHORT"].map(
9597
+ (cohort) => calculatePortfolioMetricRow({
9598
+ trades,
9599
+ window: bound.label,
9600
+ scope: "PORTFOLIO",
9601
+ cohort,
9602
+ start: bound.start,
9603
+ end: bound.end
9604
+ })
9605
+ )
9606
+ );
9607
+ const strategyRows = bounds.flatMap(
9608
+ (bound) => replayResult.strategies.map(
9609
+ (strategy) => calculatePortfolioMetricRow({
9610
+ trades: trades.filter(
9611
+ (trade) => trade.strategyName === strategy.strategyName
9612
+ ),
9613
+ window: bound.label,
9614
+ scope: strategy.strategyName,
9615
+ cohort: "ALL",
9616
+ start: bound.start,
9617
+ end: bound.end
9618
+ })
9619
+ )
9620
+ );
9621
+ const portfolioEquity = curveForTrades(trades, window.start, window.end);
9622
+ const strategySeries = replayResult.strategies.flatMap((strategy, index) => {
9623
+ const strategyTrades = trades.filter(
9624
+ (trade) => trade.strategyName === strategy.strategyName
9625
+ );
9626
+ return strategyTrades.length ? [
9627
+ {
9628
+ name: strategy.strategyName,
9629
+ color: COLORS[index % COLORS.length],
9630
+ points: curveForTrades(strategyTrades, window.start, window.end)
9631
+ }
9632
+ ] : [];
9633
+ });
9634
+ const equitySvg = renderSvg({
9635
+ title: "Portfolio and strategy realized PnL overlay",
9636
+ series: [
9637
+ {
9638
+ name: "PORTFOLIO",
9639
+ color: "#111827",
9640
+ points: portfolioEquity,
9641
+ strokeWidth: 3
9642
+ },
9643
+ ...strategySeries
9644
+ ],
9645
+ start: window.start,
9646
+ end: window.end
9647
+ });
9648
+ const drawdownSvg = renderSvg({
9649
+ title: "Aggregate portfolio realized drawdown",
9650
+ series: [
9651
+ {
9652
+ name: "PORTFOLIO",
9653
+ color: "#111827",
9654
+ points: drawdownCurve(portfolioEquity)
9655
+ }
9656
+ ],
9657
+ start: window.start,
9658
+ end: window.end
9659
+ });
9660
+ const prefix = `${timestamp}-portfolio-backtest`;
9661
+ const paths = {
9662
+ markdown: import_node_path4.default.join(outputDir, `${prefix}.md`),
9663
+ json: import_node_path4.default.join(outputDir, `${prefix}.json`),
9664
+ equitySvg: import_node_path4.default.join(outputDir, `${prefix}-equity.svg`),
9665
+ drawdownSvg: import_node_path4.default.join(outputDir, `${prefix}-drawdown.svg`),
9666
+ html: import_node_path4.default.join(outputDir, `${prefix}.html`)
9667
+ };
9668
+ const report = {
9669
+ schema: "tradejs-portfolio-backtest/v1",
9670
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
9671
+ command,
9672
+ window: {
9673
+ start: window.start,
9674
+ end: window.end,
9675
+ startIso: new Date(window.start).toISOString(),
9676
+ endIso: new Date(window.end).toISOString()
9677
+ },
9678
+ lineage,
9679
+ counts: {
9680
+ strategies: replayResult.strategies.length,
9681
+ signals: replayResult.signals.length,
9682
+ completedTrades: trades.length
9683
+ },
9684
+ metrics: { portfolio: portfolioRows, strategies: strategyRows },
9685
+ curves: {
9686
+ portfolioEquity,
9687
+ portfolioDrawdown: drawdownCurve(portfolioEquity),
9688
+ strategies: strategySeries.map(({ name, points }) => ({ name, points }))
9689
+ },
9690
+ trades
9691
+ };
9692
+ const checksum = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
9693
+ const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
9694
+ const markdown = `# Portfolio Backtest
9695
+
9696
+ - Window: ${report.window.startIso} .. ${report.window.endIso}
9697
+ - Strategies: ${report.counts.strategies}
9698
+ - Signals: ${report.counts.signals}
9699
+ - Completed trades: ${report.counts.completedTrades}
9700
+ - Portfolio semantics: ${semantics}
9701
+ - Report checksum: \`${checksum}\`
9702
+ - Command: \`${command}\`
9703
+
9704
+ ![Strategy PnL overlay](./${import_node_path4.default.basename(paths.equitySvg)})
9705
+
9706
+ ![Portfolio drawdown](./${import_node_path4.default.basename(paths.drawdownSvg)})
9707
+
9708
+ ## Portfolio metrics
9709
+
9710
+ ${markdownTable(portfolioRows)}
9711
+
9712
+ ## Strategy metrics
9713
+
9714
+ ${markdownTable(strategyRows)}
9715
+ `;
9716
+ 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>`;
9717
+ await Promise.all([
9718
+ import_promises3.default.writeFile(paths.markdown, markdown, "utf8"),
9719
+ import_promises3.default.writeFile(
9720
+ paths.json,
9721
+ `${JSON.stringify({ ...report, checksum }, null, 2)}
9722
+ `,
9723
+ "utf8"
9724
+ ),
9725
+ import_promises3.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
9726
+ import_promises3.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
9727
+ import_promises3.default.writeFile(paths.html, html, "utf8")
9728
+ ]);
9729
+ return { ...paths, checksum };
9730
+ };
9731
+
9261
9732
  // src/scripts/replayRunner.ts
9262
9733
  var prepareReplayBinanceMarketContext = async (preparedRun) => {
9263
9734
  await prepareMarketContextForRun({
@@ -9276,17 +9747,47 @@ var prepareReplayBinanceMarketContext = async (preparedRun) => {
9276
9747
  log: (message) => console.log(import_chalk13.default.gray(message))
9277
9748
  });
9278
9749
  };
9750
+ var replayMarketContextInterval = () => {
9751
+ if (replayInterval === "1") return "1m";
9752
+ if (replayInterval === "5") return "5m";
9753
+ if (replayInterval === "60") return "1h";
9754
+ return "15m";
9755
+ };
9756
+ var cachedTickerSymbols = async ({
9757
+ connectorName,
9758
+ universe,
9759
+ accountId
9760
+ }) => {
9761
+ const connectorNames = [
9762
+ connectorName,
9763
+ ...connectorName.toLowerCase() === "bybit" ? ["ByBit"] : []
9764
+ ];
9765
+ const keys = connectorNames.flatMap((name) => [
9766
+ import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, accountId),
9767
+ import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, "default"),
9768
+ import_redis6.redisKeys.tickerUniverse(replayUserName, name)
9769
+ ]);
9770
+ for (const key of [...new Set(keys)]) {
9771
+ const cached = await (0, import_redis6.getData)(key, null);
9772
+ const symbols = (cached?.tickers ?? []).map(
9773
+ (ticker) => String(ticker.symbol ?? "").trim().toUpperCase()
9774
+ ).filter(Boolean);
9775
+ if (symbols.length) return [...new Set(symbols)];
9776
+ }
9777
+ return [];
9778
+ };
9279
9779
  var finishReplay = async ({
9280
9780
  replayResult,
9281
9781
  tickers,
9282
9782
  connectorName,
9283
- window
9783
+ window,
9784
+ portfolioLineage
9284
9785
  }) => {
9285
9786
  const replayStrategySnapshot = await saveAndPrintReplayResultsByStrategy({
9286
9787
  replayResult,
9287
9788
  tickers
9288
9789
  });
9289
- const replayRuntimeComparison = await saveAndPrintReplayRuntimeComparison({
9790
+ const replayRuntimeComparison = replayFlags.portfolioReport ? null : await saveAndPrintReplayRuntimeComparison({
9290
9791
  liveStrategySummaries: replayStrategySnapshot.summaries,
9291
9792
  backtestEntries: replayStrategySnapshot.backtestEntries,
9292
9793
  replaySignals: replayResult.signals,
@@ -9329,6 +9830,18 @@ var finishReplay = async ({
9329
9830
  });
9330
9831
  console.log(import_chalk13.default.green(`Replay report: ${outputReport.markdownPath}`));
9331
9832
  console.log(import_chalk13.default.green(`Replay report JSON: ${outputReport.jsonPath}`));
9833
+ const portfolioReport = replayFlags.portfolioReport ? await writePortfolioReport({
9834
+ projectRoot: replayProjectRoot,
9835
+ timestamp: `${timestamp}${replayFlags.portfolioOutputSuffix ? `-${String(replayFlags.portfolioOutputSuffix).replace(/[^a-zA-Z0-9_-]/g, "")}` : ""}`,
9836
+ replayResult,
9837
+ window,
9838
+ lineage: portfolioLineage ?? {},
9839
+ command: process.argv.join(" ")
9840
+ }) : null;
9841
+ if (portfolioReport) {
9842
+ console.log(import_chalk13.default.green(`Portfolio report: ${portfolioReport.html}`));
9843
+ console.log(import_chalk13.default.green(`Portfolio report JSON: ${portfolioReport.json}`));
9844
+ }
9332
9845
  await (0, import_redis6.setData)(
9333
9846
  replayKey,
9334
9847
  {
@@ -9352,10 +9865,11 @@ var finishReplay = async ({
9352
9865
  signalsCount: replayResult.signals.length,
9353
9866
  replayLineage: replayResult.runtimeLineages,
9354
9867
  strategyCharts: replayChartSnapshot,
9355
- outputReport
9868
+ outputReport,
9869
+ ...portfolioReport ? { portfolioReport } : {}
9356
9870
  },
9357
9871
  {
9358
- expire: import_constants6.TTL_1M
9872
+ expire: import_constants7.TTL_1M
9359
9873
  }
9360
9874
  );
9361
9875
  if (replayChartSnapshot) {
@@ -9365,7 +9879,7 @@ var finishReplay = async ({
9365
9879
  import_redis6.redisKeys.strategyChartCard(replayUserName, "replay", card.cardId),
9366
9880
  card,
9367
9881
  {
9368
- expire: import_constants6.TTL_1M
9882
+ expire: import_constants7.TTL_1M
9369
9883
  }
9370
9884
  )
9371
9885
  )
@@ -9415,7 +9929,57 @@ var replayBacktest = async () => {
9415
9929
  projectRoot: replayProjectRoot,
9416
9930
  deploymentId: replayDeploymentId
9417
9931
  });
9418
- const { deployment, strategies: replayStrategies } = replayComposition;
9932
+ const { deployment } = replayComposition;
9933
+ const maxLossValue = (() => {
9934
+ if (replayFlags.maxLossValue == null || replayFlags.maxLossValue === "") {
9935
+ return null;
9936
+ }
9937
+ const value = Number(replayFlags.maxLossValue);
9938
+ if (!Number.isFinite(value) || value <= 0) {
9939
+ throw new Error(
9940
+ `Invalid --maxLossValue: ${String(replayFlags.maxLossValue)}`
9941
+ );
9942
+ }
9943
+ return value;
9944
+ })();
9945
+ const sourceDeploymentCompositionId = replayComposition.strategies[0]?.deploymentCompositionId ?? deployment.deploymentCompositionId;
9946
+ let replayStrategies = replayComposition.strategies.map((strategy) => {
9947
+ if (maxLossValue == null) return strategy;
9948
+ const strategyConfig = {
9949
+ ...strategy.strategyConfig,
9950
+ MAX_LOSS_VALUE: maxLossValue
9951
+ };
9952
+ return {
9953
+ ...strategy,
9954
+ strategyConfig,
9955
+ strategyRevision: (0, import_runtimeStrategies4.computeStrategyRevision)({
9956
+ strategyName: strategy.strategyName,
9957
+ strategyPackage: strategy.strategyPackage,
9958
+ strategyPackageVersion: strategy.strategyPackageVersion,
9959
+ strategyDependencyVersions: strategy.strategyDependencyVersions,
9960
+ runtimePackageVersion: strategy.runtimePackageVersion,
9961
+ strategyConfig
9962
+ })
9963
+ };
9964
+ });
9965
+ const researchDeploymentCompositionId = (0, import_runtimeStrategies4.computeDeploymentCompositionId)({
9966
+ deploymentId: deployment.id,
9967
+ connectorName: deployment.connectorName,
9968
+ provider: deployment.provider,
9969
+ accountId: deployment.accountId,
9970
+ enabled: deployment.enabled,
9971
+ ...deployment.assetClasses ? { assetClasses: deployment.assetClasses } : {},
9972
+ strategies: replayStrategies.map((strategy) => ({
9973
+ strategyName: strategy.strategyName,
9974
+ strategyRevision: strategy.strategyRevision,
9975
+ enabled: true,
9976
+ ...strategy.selection ? { selection: strategy.selection } : {}
9977
+ }))
9978
+ });
9979
+ replayStrategies = replayStrategies.map((strategy) => ({
9980
+ ...strategy,
9981
+ deploymentCompositionId: researchDeploymentCompositionId
9982
+ }));
9419
9983
  if (!replayStrategies.length) {
9420
9984
  throw new Error(`No enabled strategies in deployment ${deployment.id}`);
9421
9985
  }
@@ -9440,10 +10004,26 @@ var replayBacktest = async () => {
9440
10004
  );
9441
10005
  }
9442
10006
  const replaySelection = mergeRuntimeStrategySelections(replayStrategies);
10007
+ const useAllData = Boolean(replayFlags.allData) && replayFlags.days == null && replayFlags.startTime == null && replayFlags.endTime == null;
10008
+ let candleCoverage;
10009
+ let portfolioMarketContextRows;
10010
+ let allDataTickers;
10011
+ if (useAllData && !replayFlags.tickers) {
10012
+ allDataTickers = await cachedTickerSymbols({
10013
+ connectorName: deployment.connectorName,
10014
+ universe: [...strategyUniverses][0],
10015
+ accountId: deployment.accountId
10016
+ });
10017
+ if (!allDataTickers.length) {
10018
+ throw new Error(
10019
+ `No cached ticker universe found for ${deployment.connectorName}. Pass --tickers explicitly or refresh the ticker cache first.`
10020
+ );
10021
+ }
10022
+ }
9443
10023
  const preparedRun = await prepareRunEnvironment({
9444
10024
  connector: deployment.connectorName,
9445
10025
  userName: replayUserName,
9446
- tickers: replayFlags.tickers ?? replaySelection?.tickers?.join(",") ?? deployment.tickers?.join(","),
10026
+ tickers: replayFlags.tickers ?? allDataTickers?.join(",") ?? replaySelection?.tickers?.join(",") ?? deployment.tickers?.join(","),
9447
10027
  exclude: replayFlags.exclude,
9448
10028
  tickersLimit: replayFlags.tickersLimit,
9449
10029
  showTickersList: replayFlags.showTickersList,
@@ -9463,6 +10043,45 @@ var replayBacktest = async () => {
9463
10043
  if (!preparedRun || isReplayUpdateOnlyRun) {
9464
10044
  return;
9465
10045
  }
10046
+ if (useAllData) {
10047
+ const intervalMs = (0, import_data4.intervalToMs)(replayInterval);
10048
+ const edges = await (0, import_candles.getDataEdgesForSymbols)(
10049
+ deployment.provider,
10050
+ preparedRun.tickers,
10051
+ Number(replayInterval)
10052
+ );
10053
+ const available = [...edges.values()].filter(
10054
+ (edge) => Number.isFinite(edge.min) && Number.isFinite(edge.max)
10055
+ );
10056
+ if (!available.length) {
10057
+ throw new Error(
10058
+ `No cached candles found for provider=${deployment.provider}, interval=${replayInterval}`
10059
+ );
10060
+ }
10061
+ const start = Math.min(...available.map((edge) => edge.min));
10062
+ const lastClosedEnd = Math.floor(Date.now() / intervalMs) * intervalMs - 1;
10063
+ const max = Math.min(
10064
+ Math.max(...available.map((edge) => edge.max)),
10065
+ lastClosedEnd
10066
+ );
10067
+ preparedRun.window = {
10068
+ start,
10069
+ end: max + intervalMs - 1,
10070
+ source: "explicit"
10071
+ };
10072
+ preparedRun.preloadStart = (0, import_time7.getBacktestPreloadStart)(start);
10073
+ candleCoverage = {
10074
+ symbolsRequested: preparedRun.tickers.length,
10075
+ symbolsWithData: available.length,
10076
+ min: start,
10077
+ max
10078
+ };
10079
+ console.log(
10080
+ import_chalk13.default.gray(
10081
+ `cached candle coverage: ${new Date(start).toISOString()} -> ${new Date(max).toISOString()} (${available.length}/${preparedRun.tickers.length} symbols)`
10082
+ )
10083
+ );
10084
+ }
9466
10085
  setRuntimeCompareContext({
9467
10086
  connector: preparedRun.marketConnector,
9468
10087
  connectorName: preparedRun.connectorName,
@@ -9471,16 +10090,29 @@ var replayBacktest = async () => {
9471
10090
  end: preparedRun.window.end
9472
10091
  }
9473
10092
  });
10093
+ const aiEnabled = replayStrategies.some(
10094
+ ({ strategyConfig }) => Boolean(strategyConfig.AI_ENABLED)
10095
+ );
9474
10096
  await prepareReplayBinanceMarketContext({
9475
10097
  ...preparedRun,
9476
- aiEnabled: replayStrategies.some(
9477
- ({ strategyConfig }) => Boolean(strategyConfig.AI_ENABLED)
9478
- ),
10098
+ aiEnabled,
9479
10099
  mlEnabled: replayStrategies.some(
9480
10100
  ({ strategyConfig }) => Boolean(strategyConfig.ML_ENABLED)
9481
10101
  ),
9482
10102
  strategyNames: replayStrategies.map(({ strategyName }) => strategyName)
9483
10103
  });
10104
+ if (replayFlags.portfolioReport && replayFlags.allData && aiEnabled) {
10105
+ portfolioMarketContextRows = await (0, import_strategies5.preloadBinanceMarketContextForWindow)({
10106
+ startMs: preparedRun.window.start,
10107
+ endMs: preparedRun.window.end,
10108
+ interval: replayMarketContextInterval()
10109
+ });
10110
+ console.log(
10111
+ import_chalk13.default.gray(
10112
+ `preloaded Binance market context: ${portfolioMarketContextRows.tradeFlowRows} trade-flow rows, ${portfolioMarketContextRows.breadthRows} breadth rows`
10113
+ )
10114
+ );
10115
+ }
9484
10116
  console.log(import_chalk13.default.yellow(`tickers: ${preparedRun.tickers.length}`));
9485
10117
  console.log(
9486
10118
  import_chalk13.default.gray(
@@ -9488,13 +10120,54 @@ var replayBacktest = async () => {
9488
10120
  )
9489
10121
  );
9490
10122
  markTestsStarted();
9491
- const replayResult = await runHistoricalSignalsReplay({
10123
+ const portfolioBatchSize = Number(replayFlags.portfolioBatchSize ?? 4);
10124
+ if (replayFlags.portfolioReport && (!Number.isSafeInteger(portfolioBatchSize) || portfolioBatchSize <= 0)) {
10125
+ throw new Error(
10126
+ `Invalid --portfolioBatchSize: ${String(replayFlags.portfolioBatchSize)}`
10127
+ );
10128
+ }
10129
+ const runtimeStrategies = replayFlags.tickers && !replayFlags.portfolioReport ? replayStrategies.map(
10130
+ ({ selection: _selection, ...strategy }) => strategy
10131
+ ) : replayStrategies;
10132
+ const historicalReplayReferences = replayFlags.portfolioReport ? await loadHistoricalReplayReferences({
9492
10133
  preparedRun,
9493
- interval: replayInterval,
9494
- runtimeStrategies: replayFlags.tickers ? replayStrategies.map(
9495
- ({ selection: _selection, ...strategy }) => strategy
9496
- ) : replayStrategies
9497
- });
10134
+ interval: replayInterval
10135
+ }) : void 0;
10136
+ const tickerBatches = replayFlags.portfolioReport ? Array.from(
10137
+ { length: Math.ceil(preparedRun.tickers.length / portfolioBatchSize) },
10138
+ (_, index) => preparedRun.tickers.slice(
10139
+ index * portfolioBatchSize,
10140
+ (index + 1) * portfolioBatchSize
10141
+ )
10142
+ ) : [preparedRun.tickers];
10143
+ const runReplayBatch = async (batchTickers) => {
10144
+ const result = await runHistoricalSignalsReplay({
10145
+ preparedRun: { ...preparedRun, tickers: batchTickers },
10146
+ interval: replayInterval,
10147
+ runtimeStrategies,
10148
+ references: historicalReplayReferences,
10149
+ showProgress: !replayFlags.portfolioReport,
10150
+ collectSkipEvidence: !replayFlags.portfolioReport,
10151
+ collectSignals: !replayFlags.portfolioReport
10152
+ });
10153
+ return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
10154
+ };
10155
+ const batchResults = [];
10156
+ for (const [index, batchTickers] of tickerBatches.entries()) {
10157
+ if (tickerBatches.length > 1) {
10158
+ console.log(
10159
+ import_chalk13.default.gray(
10160
+ `portfolio batch ${index + 1}/${tickerBatches.length}: ${batchTickers.length} symbols`
10161
+ )
10162
+ );
10163
+ }
10164
+ batchResults.push(await runReplayBatch(batchTickers));
10165
+ if (replayFlags.portfolioReport) {
10166
+ await new Promise((resolve) => setImmediate(resolve));
10167
+ globalThis.gc?.();
10168
+ }
10169
+ }
10170
+ const replayResult = batchResults.length === 1 ? batchResults[0] : mergeHistoricalReplayResults(batchResults);
9498
10171
  for (const _strategy of replayResult.strategies) {
9499
10172
  incrementSuccessTests();
9500
10173
  }
@@ -9502,7 +10175,32 @@ var replayBacktest = async () => {
9502
10175
  replayResult,
9503
10176
  tickers: preparedRun.tickers,
9504
10177
  connectorName: preparedRun.connectorName,
9505
- window: preparedRun.window
10178
+ window: preparedRun.window,
10179
+ portfolioLineage: replayFlags.portfolioReport ? {
10180
+ sourceDeploymentId: deployment.id,
10181
+ sourceDeploymentCompositionId,
10182
+ researchDeploymentCompositionId,
10183
+ maxLossValue,
10184
+ aiMode: "strategy runtime declaration",
10185
+ cacheOnly: Boolean(replayFlags.cacheOnly),
10186
+ portfolioSemantics: "production strategy order and one concurrent position per symbol; fixed-risk symbol batches are merged by canonical realized PnL",
10187
+ portfolioBatchSize,
10188
+ tickers: preparedRun.tickers,
10189
+ candleCoverage: candleCoverage ?? null,
10190
+ binanceMarketContextRows: portfolioMarketContextRows ?? null,
10191
+ strategies: replayStrategies.map((strategy) => ({
10192
+ strategyName: strategy.strategyName,
10193
+ strategyRevision: strategy.strategyRevision,
10194
+ strategyPackage: strategy.strategyPackage,
10195
+ strategyPackageVersion: strategy.strategyPackageVersion,
10196
+ aiEnabled: Boolean(strategy.strategyConfig.AI_ENABLED),
10197
+ minAiQuality: strategy.strategyConfig.MIN_AI_QUALITY ?? null,
10198
+ aiMode: strategy.strategyConfig.AI_MODE ?? null,
10199
+ maxLossValue: strategy.strategyConfig.MAX_LOSS_VALUE ?? null,
10200
+ selection: strategy.selection ?? null,
10201
+ effectiveConfig: strategy.strategyConfig
10202
+ }))
10203
+ } : void 0
9506
10204
  });
9507
10205
  };
9508
10206
  // Annotate the CommonJS export names for ESM import in node: