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