@tradejs/cli 3.1.29-beta.281 → 3.1.29-beta.283

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.
@@ -41,7 +41,7 @@ __export(replayRunner_exports, {
41
41
  module.exports = __toCommonJS(replayRunner_exports);
42
42
  var import_chalk13 = __toESM(require("chalk"));
43
43
  var import_constants7 = require("@tradejs/core/constants");
44
- var import_data4 = require("@tradejs/core/data");
44
+ var import_data3 = require("@tradejs/core/data");
45
45
  var import_time7 = require("@tradejs/core/time");
46
46
  var import_redis6 = require("@tradejs/infra/redis");
47
47
  var import_candles = require("@tradejs/infra/timescale/candles");
@@ -2711,27 +2711,27 @@ var createCoinMarketCapFetchStats = () => ({
2711
2711
  totalCredits: 0,
2712
2712
  byPath: /* @__PURE__ */ new Map()
2713
2713
  });
2714
- var getCoinMarketCapPathLabel = (path5) => {
2715
- if (path5.includes("global-metrics")) return "global";
2716
- if (path5.includes("cryptocurrency/quotes")) return "reference";
2717
- if (path5.includes("exchange/quotes")) return "exchangeLiquidity";
2718
- if (path5.includes("fear-and-greed")) return "fearGreed";
2719
- if (path5.includes("cmc100")) return "cmc100";
2720
- if (path5.includes("cmc20")) return "cmc20";
2721
- return path5;
2722
- };
2723
- var recordCoinMarketCapFetchStats = (stats, path5, creditCount) => {
2714
+ var getCoinMarketCapPathLabel = (path7) => {
2715
+ if (path7.includes("global-metrics")) return "global";
2716
+ if (path7.includes("cryptocurrency/quotes")) return "reference";
2717
+ if (path7.includes("exchange/quotes")) return "exchangeLiquidity";
2718
+ if (path7.includes("fear-and-greed")) return "fearGreed";
2719
+ if (path7.includes("cmc100")) return "cmc100";
2720
+ if (path7.includes("cmc20")) return "cmc20";
2721
+ return path7;
2722
+ };
2723
+ var recordCoinMarketCapFetchStats = (stats, path7, creditCount) => {
2724
2724
  if (!stats) return;
2725
- const existing = stats.byPath.get(path5) ?? { requests: 0, credits: 0 };
2725
+ const existing = stats.byPath.get(path7) ?? { requests: 0, credits: 0 };
2726
2726
  existing.requests += 1;
2727
2727
  existing.credits += creditCount ?? 0;
2728
2728
  stats.requests += 1;
2729
2729
  stats.totalCredits += creditCount ?? 0;
2730
- stats.byPath.set(path5, existing);
2730
+ stats.byPath.set(path7, existing);
2731
2731
  };
2732
2732
  var formatCoinMarketCapFetchStats = (stats) => {
2733
2733
  const byPath = [...stats.byPath.entries()].map(
2734
- ([path5, item]) => `${getCoinMarketCapPathLabel(path5)}=${item.requests}req/${item.credits}cr`
2734
+ ([path7, item]) => `${getCoinMarketCapPathLabel(path7)}=${item.requests}req/${item.credits}cr`
2735
2735
  ).join(" ");
2736
2736
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
2737
2737
  };
@@ -4751,10 +4751,32 @@ var compactReplaySignal = (signal) => {
4751
4751
  };
4752
4752
  };
4753
4753
 
4754
+ // src/lib/replay/memoryProfile.ts
4755
+ var import_node_fs = __toESM(require("fs"));
4756
+ var import_node_path2 = __toESM(require("path"));
4757
+ var createReplayMemoryProfile = (file = process.env.TRADEJS_REPLAY_MEMORY_PROFILE) => {
4758
+ if (!file) return void 0;
4759
+ import_node_fs.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
4760
+ return (phase, counts = {}) => {
4761
+ import_node_fs.default.appendFileSync(
4762
+ file,
4763
+ JSON.stringify({
4764
+ schemaVersion: 1,
4765
+ observedAt: Date.now(),
4766
+ pid: process.pid,
4767
+ phase,
4768
+ counts,
4769
+ ...process.memoryUsage(),
4770
+ maxRssKiB: process.resourceUsage().maxRSS
4771
+ }) + "\n"
4772
+ );
4773
+ };
4774
+ };
4775
+
4754
4776
  // src/lib/replay/historicalSignalsReplay.ts
4755
4777
  var import_chalk9 = __toESM(require("chalk"));
4756
4778
  var import_progress4 = __toESM(require("progress"));
4757
- var import_node_path2 = __toESM(require("path"));
4779
+ var import_node_path3 = __toESM(require("path"));
4758
4780
  var import_node_module = require("module");
4759
4781
  var import_strategies3 = require("@tradejs/core/strategies");
4760
4782
  var import_time3 = require("@tradejs/core/time");
@@ -5303,6 +5325,11 @@ var prepareHistoricalReplay = async (context, adapters) => {
5303
5325
  cacheOnly: true,
5304
5326
  interval
5305
5327
  });
5328
+ if (symbolRuntimes.length % 50 === 0)
5329
+ adapters.sampleMemory?.("history:loaded", {
5330
+ symbols: symbolRuntimes.length + 1,
5331
+ candles: data.length
5332
+ });
5306
5333
  const preparedData = buildPreparedData({
5307
5334
  data,
5308
5335
  btcData: references.btcMarketData,
@@ -5399,6 +5426,11 @@ var prepareHistoricalReplay = async (context, adapters) => {
5399
5426
  strategies: strategiesForSymbol
5400
5427
  };
5401
5428
  symbolRuntimes.push(symbolRuntime);
5429
+ if (symbolRuntimes.length % 50 === 0 || symbol === preparedRun.tickers.at(-1))
5430
+ adapters.sampleMemory?.("strategies:initializing", {
5431
+ symbols: symbolRuntimes.length,
5432
+ strategiesPerSymbol: strategiesForSymbol.length
5433
+ });
5402
5434
  adapters.progress.tick({
5403
5435
  skipped: adapters.display.skipped(skippedSymbols),
5404
5436
  symbol: adapters.display.symbol(symbol)
@@ -5416,6 +5448,10 @@ var prepareHistoricalReplay = async (context, adapters) => {
5416
5448
  runtimeLineages
5417
5449
  };
5418
5450
  };
5451
+ var releaseHistoricalReplayPlan = (plan) => {
5452
+ plan.symbolRuntimes.length = 0;
5453
+ plan.sharedReplayKeyPrefixes.length = 0;
5454
+ };
5419
5455
 
5420
5456
  // src/lib/replay/historicalSignalsReplaySkipEvidence.ts
5421
5457
  var import_data = require("@tradejs/core/data");
@@ -5715,7 +5751,7 @@ var getProjectReplayCacheReleasers = () => {
5715
5751
  }
5716
5752
  try {
5717
5753
  const requireFromProject = (0, import_node_module.createRequire)(
5718
- import_node_path2.default.join(replayProjectRoot, "package.json")
5754
+ import_node_path3.default.join(replayProjectRoot, "package.json")
5719
5755
  );
5720
5756
  projectReplayCacheReleasers = requireFromProject(
5721
5757
  "@tradejs/core/strategies"
@@ -5856,6 +5892,8 @@ var runHistoricalSignalsReplay = async ({
5856
5892
  collectSkipEvidence = true,
5857
5893
  collectSignals = true
5858
5894
  }) => {
5895
+ const memory = createReplayMemoryProfile();
5896
+ memory?.("prepare:start");
5859
5897
  const startedAt = Date.now();
5860
5898
  const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
5861
5899
  const projectHooks = projectConfig.hooks;
@@ -5872,6 +5910,7 @@ var runHistoricalSignalsReplay = async ({
5872
5910
  width: 30
5873
5911
  }
5874
5912
  );
5913
+ memory?.("references:loaded");
5875
5914
  const plan = await prepareHistoricalReplay(
5876
5915
  {
5877
5916
  userName: replayUserName,
@@ -5896,9 +5935,11 @@ var runHistoricalSignalsReplay = async ({
5896
5935
  skipped: (value) => import_chalk9.default.yellow(value),
5897
5936
  symbol: (value) => import_chalk9.default.gray(value)
5898
5937
  },
5899
- buildLineage: buildRuntimeLineage
5938
+ buildLineage: buildRuntimeLineage,
5939
+ sampleMemory: memory
5900
5940
  }
5901
5941
  );
5942
+ memory?.("strategies:prepared", { symbols: plan.symbolRuntimes.length });
5902
5943
  const hookContext = buildAfterSignalsContext({
5903
5944
  connector: replayConnector,
5904
5945
  connectorName: preparedRun.connectorName,
@@ -5948,13 +5989,16 @@ var runHistoricalSignalsReplay = async ({
5948
5989
  )
5949
5990
  }
5950
5991
  );
5992
+ memory?.("execution:complete", { signals: execution.signals.length });
5993
+ releaseHistoricalReplayPlan(plan);
5994
+ memory?.("execution:released");
5951
5995
  const artifacts = replayConnector.getReplayArtifacts();
5952
5996
  import_logger.logger.info(
5953
5997
  import_chalk9.default.gray(
5954
5998
  `signals replay historical cycles: ${plan.orderedTimestamps.length} (aborted=${execution.abortedCycles}) done in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
5955
5999
  )
5956
6000
  );
5957
- return collectHistoricalReplayResult({
6001
+ const result = collectHistoricalReplayResult({
5958
6002
  strategies: loadedStrategies,
5959
6003
  artifacts,
5960
6004
  signals: execution.signals,
@@ -5963,6 +6007,8 @@ var runHistoricalSignalsReplay = async ({
5963
6007
  runtimeLineages: plan.runtimeLineages,
5964
6008
  replayLineageScopes: execution.replayLineageScopes
5965
6009
  });
6010
+ memory?.("result:collected");
6011
+ return result;
5966
6012
  };
5967
6013
 
5968
6014
  // src/lib/replay/chartSnapshot.ts
@@ -8797,9 +8843,87 @@ var saveAndPrintReplayRuntimeComparison = async ({
8797
8843
  };
8798
8844
 
8799
8845
  // src/lib/replay/outputReport.ts
8846
+ var import_promises3 = __toESM(require("fs/promises"));
8847
+ var import_node_path5 = __toESM(require("path"));
8848
+
8849
+ // src/lib/replay/jsonFile.ts
8850
+ var import_node_fs2 = __toESM(require("fs"));
8800
8851
  var import_promises2 = __toESM(require("fs/promises"));
8801
- var import_node_path3 = __toESM(require("path"));
8802
- var import_data3 = require("@tradejs/core/data");
8852
+ var import_node_path4 = __toESM(require("path"));
8853
+ var import_node_events = require("events");
8854
+ var import_node_crypto2 = require("crypto");
8855
+ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
8856
+ if (value === null || typeof value !== "object") {
8857
+ yield JSON.stringify(value) ?? "null";
8858
+ return;
8859
+ }
8860
+ if (ancestors.has(value)) throw new TypeError("Circular JSON evidence");
8861
+ ancestors.add(value);
8862
+ try {
8863
+ if (Array.isArray(value)) {
8864
+ yield "[";
8865
+ for (let index = 0; index < value.length; index++) {
8866
+ if (index) yield ",";
8867
+ yield* jsonParts(value[index], ancestors);
8868
+ }
8869
+ yield "]";
8870
+ } else {
8871
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
8872
+ throw new TypeError("JSON evidence requires plain records");
8873
+ if ("toJSON" in value && typeof value.toJSON === "function")
8874
+ throw new TypeError("JSON evidence cannot contain toJSON hooks");
8875
+ yield "{";
8876
+ let first = true;
8877
+ for (const key of Object.keys(value)) {
8878
+ const field = Reflect.get(value, key);
8879
+ if (field === void 0 || typeof field === "function" || typeof field === "symbol")
8880
+ continue;
8881
+ if (!first) yield ",";
8882
+ first = false;
8883
+ yield JSON.stringify(key) + ":";
8884
+ yield* jsonParts(field, ancestors);
8885
+ }
8886
+ yield "}";
8887
+ }
8888
+ } finally {
8889
+ ancestors.delete(value);
8890
+ }
8891
+ }
8892
+ var writeReplayJsonFile = async (file, value) => {
8893
+ await import_promises2.default.mkdir(import_node_path4.default.dirname(file), { recursive: true });
8894
+ const temporary = file + ".tmp-" + (0, import_node_crypto2.randomUUID)();
8895
+ const stream = import_node_fs2.default.createWriteStream(temporary, {
8896
+ flags: "wx",
8897
+ highWaterMark: 65536
8898
+ });
8899
+ let failure;
8900
+ stream.on("error", (error) => {
8901
+ failure = error;
8902
+ });
8903
+ try {
8904
+ let buffer = "";
8905
+ for (const part of jsonParts(value)) {
8906
+ buffer += part;
8907
+ if (buffer.length >= 65536) {
8908
+ if (failure) throw failure;
8909
+ if (!stream.write(buffer)) await (0, import_node_events.once)(stream, "drain");
8910
+ buffer = "";
8911
+ }
8912
+ }
8913
+ if (failure) throw failure;
8914
+ stream.end(buffer + "\n");
8915
+ await (0, import_node_events.once)(stream, "close");
8916
+ if (failure) throw failure;
8917
+ await import_promises2.default.rename(temporary, file);
8918
+ } catch (error) {
8919
+ stream.destroy();
8920
+ if (!stream.closed) await (0, import_node_events.once)(stream, "close").catch(() => void 0);
8921
+ await import_promises2.default.rm(temporary, { force: true });
8922
+ throw error;
8923
+ }
8924
+ };
8925
+
8926
+ // src/lib/replay/outputReport.ts
8803
8927
  var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
8804
8928
  timeZone: "Europe/Moscow",
8805
8929
  dateStyle: "short",
@@ -9102,8 +9226,8 @@ var writeReplayOutputReport = async ({
9102
9226
  strategySnapshot,
9103
9227
  runtimeComparison
9104
9228
  }) => {
9105
- const outputDir = import_node_path3.default.resolve(projectRoot, "data/replay/output");
9106
- await import_promises2.default.mkdir(outputDir, { recursive: true });
9229
+ const outputDir = import_node_path5.default.resolve(projectRoot, "data/replay/output");
9230
+ await import_promises3.default.mkdir(outputDir, { recursive: true });
9107
9231
  const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
9108
9232
  const report = {
9109
9233
  reportType: "replay-output",
@@ -9138,8 +9262,8 @@ var writeReplayOutputReport = async ({
9138
9262
  runtimeComparison,
9139
9263
  perTradeAnalysis
9140
9264
  };
9141
- const markdownPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.md`);
9142
- const jsonPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.json`);
9265
+ const markdownPath = import_node_path5.default.join(outputDir, `${timestamp}-replay-report.md`);
9266
+ const jsonPath = import_node_path5.default.join(outputDir, `${timestamp}-replay-report.json`);
9143
9267
  const lines = [
9144
9268
  "# Replay Report",
9145
9269
  "",
@@ -9204,11 +9328,13 @@ var writeReplayOutputReport = async ({
9204
9328
  "Full machine-readable details are stored in the JSON sidecar.",
9205
9329
  ""
9206
9330
  ];
9331
+ const memory = createReplayMemoryProfile();
9332
+ memory?.("report:write:start");
9207
9333
  await Promise.all([
9208
- import_promises2.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
9209
- import_promises2.default.writeFile(jsonPath, `${(0, import_data3.toJson)(report, true)}
9210
- `, "utf8")
9334
+ import_promises3.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
9335
+ writeReplayJsonFile(jsonPath, report)
9211
9336
  ]);
9337
+ memory?.("report:write:complete");
9212
9338
  return {
9213
9339
  markdownPath,
9214
9340
  jsonPath
@@ -9216,9 +9342,9 @@ var writeReplayOutputReport = async ({
9216
9342
  };
9217
9343
 
9218
9344
  // src/lib/replay/portfolioReport.ts
9219
- var import_promises3 = __toESM(require("fs/promises"));
9220
- var import_node_path4 = __toESM(require("path"));
9221
- var import_node_crypto2 = require("crypto");
9345
+ var import_promises4 = __toESM(require("fs/promises"));
9346
+ var import_node_path6 = __toESM(require("path"));
9347
+ var import_node_crypto3 = require("crypto");
9222
9348
  var DAY_MS4 = 864e5;
9223
9349
  var WINDOWS = [365, 180, 90, 30, 7];
9224
9350
  var COLORS = [
@@ -9396,8 +9522,8 @@ var writePortfolioReport = async ({
9396
9522
  lineage,
9397
9523
  command
9398
9524
  }) => {
9399
- const outputDir = import_node_path4.default.resolve(projectRoot, "data/replay/output");
9400
- await import_promises3.default.mkdir(outputDir, { recursive: true });
9525
+ const outputDir = import_node_path6.default.resolve(projectRoot, "data/replay/output");
9526
+ await import_promises4.default.mkdir(outputDir, { recursive: true });
9401
9527
  const trades = replayResult.strategies.flatMap(
9402
9528
  (strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
9403
9529
  );
@@ -9469,11 +9595,11 @@ var writePortfolioReport = async ({
9469
9595
  });
9470
9596
  const prefix = `${timestamp}-portfolio-backtest`;
9471
9597
  const paths = {
9472
- markdown: import_node_path4.default.join(outputDir, `${prefix}.md`),
9473
- json: import_node_path4.default.join(outputDir, `${prefix}.json`),
9474
- equitySvg: import_node_path4.default.join(outputDir, `${prefix}-equity.svg`),
9475
- drawdownSvg: import_node_path4.default.join(outputDir, `${prefix}-drawdown.svg`),
9476
- html: import_node_path4.default.join(outputDir, `${prefix}.html`)
9598
+ markdown: import_node_path6.default.join(outputDir, `${prefix}.md`),
9599
+ json: import_node_path6.default.join(outputDir, `${prefix}.json`),
9600
+ equitySvg: import_node_path6.default.join(outputDir, `${prefix}-equity.svg`),
9601
+ drawdownSvg: import_node_path6.default.join(outputDir, `${prefix}-drawdown.svg`),
9602
+ html: import_node_path6.default.join(outputDir, `${prefix}.html`)
9477
9603
  };
9478
9604
  const report = {
9479
9605
  schema: "tradejs-portfolio-backtest/v1",
@@ -9499,7 +9625,7 @@ var writePortfolioReport = async ({
9499
9625
  },
9500
9626
  trades
9501
9627
  };
9502
- const checksum = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
9628
+ const checksum = (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
9503
9629
  const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
9504
9630
  const markdown = `# Portfolio Backtest
9505
9631
 
@@ -9511,9 +9637,9 @@ var writePortfolioReport = async ({
9511
9637
  - Report checksum: \`${checksum}\`
9512
9638
  - Command: \`${command}\`
9513
9639
 
9514
- ![Strategy PnL overlay](./${import_node_path4.default.basename(paths.equitySvg)})
9640
+ ![Strategy PnL overlay](./${import_node_path6.default.basename(paths.equitySvg)})
9515
9641
 
9516
- ![Portfolio drawdown](./${import_node_path4.default.basename(paths.drawdownSvg)})
9642
+ ![Portfolio drawdown](./${import_node_path6.default.basename(paths.drawdownSvg)})
9517
9643
 
9518
9644
  ## Portfolio metrics
9519
9645
 
@@ -9525,16 +9651,16 @@ ${markdownTable(strategyRows)}
9525
9651
  `;
9526
9652
  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>`;
9527
9653
  await Promise.all([
9528
- import_promises3.default.writeFile(paths.markdown, markdown, "utf8"),
9529
- import_promises3.default.writeFile(
9654
+ import_promises4.default.writeFile(paths.markdown, markdown, "utf8"),
9655
+ import_promises4.default.writeFile(
9530
9656
  paths.json,
9531
9657
  `${JSON.stringify({ ...report, checksum }, null, 2)}
9532
9658
  `,
9533
9659
  "utf8"
9534
9660
  ),
9535
- import_promises3.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
9536
- import_promises3.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
9537
- import_promises3.default.writeFile(paths.html, html, "utf8")
9661
+ import_promises4.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
9662
+ import_promises4.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
9663
+ import_promises4.default.writeFile(paths.html, html, "utf8")
9538
9664
  ]);
9539
9665
  return { ...paths, checksum };
9540
9666
  };
@@ -9848,13 +9974,13 @@ var replayBacktest = async () => {
9848
9974
  deploymentId: deployment.id,
9849
9975
  assetClasses: deployment.assetClasses,
9850
9976
  deployment,
9851
- closedIntervalMs: (0, import_data4.intervalToMs)(replayInterval)
9977
+ closedIntervalMs: (0, import_data3.intervalToMs)(replayInterval)
9852
9978
  });
9853
9979
  if (!preparedRun || isReplayUpdateOnlyRun) {
9854
9980
  return;
9855
9981
  }
9856
9982
  if (useAllData) {
9857
- const intervalMs = (0, import_data4.intervalToMs)(replayInterval);
9983
+ const intervalMs = (0, import_data3.intervalToMs)(replayInterval);
9858
9984
  const edges = await (0, import_candles.getDataEdgesForSymbols)(
9859
9985
  deployment.provider,
9860
9986
  preparedRun.tickers,
@@ -35,8 +35,109 @@ __export(replayRuntimeEvidence_exports, {
35
35
  summarizeReplayComparison: () => summarizeReplayComparison
36
36
  });
37
37
  module.exports = __toCommonJS(replayRuntimeEvidence_exports);
38
+
39
+ // src/lib/replay/jsonFile.ts
40
+ var import_node_fs = __toESM(require("fs"));
38
41
  var import_promises = __toESM(require("fs/promises"));
39
42
  var import_node_path = __toESM(require("path"));
43
+ var import_node_events = require("events");
44
+ var import_node_crypto = require("crypto");
45
+ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
46
+ if (value === null || typeof value !== "object") {
47
+ yield JSON.stringify(value) ?? "null";
48
+ return;
49
+ }
50
+ if (ancestors.has(value)) throw new TypeError("Circular JSON evidence");
51
+ ancestors.add(value);
52
+ try {
53
+ if (Array.isArray(value)) {
54
+ yield "[";
55
+ for (let index = 0; index < value.length; index++) {
56
+ if (index) yield ",";
57
+ yield* jsonParts(value[index], ancestors);
58
+ }
59
+ yield "]";
60
+ } else {
61
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
62
+ throw new TypeError("JSON evidence requires plain records");
63
+ if ("toJSON" in value && typeof value.toJSON === "function")
64
+ throw new TypeError("JSON evidence cannot contain toJSON hooks");
65
+ yield "{";
66
+ let first = true;
67
+ for (const key of Object.keys(value)) {
68
+ const field = Reflect.get(value, key);
69
+ if (field === void 0 || typeof field === "function" || typeof field === "symbol")
70
+ continue;
71
+ if (!first) yield ",";
72
+ first = false;
73
+ yield JSON.stringify(key) + ":";
74
+ yield* jsonParts(field, ancestors);
75
+ }
76
+ yield "}";
77
+ }
78
+ } finally {
79
+ ancestors.delete(value);
80
+ }
81
+ }
82
+ var writeReplayJsonFile = async (file, value) => {
83
+ await import_promises.default.mkdir(import_node_path.default.dirname(file), { recursive: true });
84
+ const temporary = file + ".tmp-" + (0, import_node_crypto.randomUUID)();
85
+ const stream = import_node_fs.default.createWriteStream(temporary, {
86
+ flags: "wx",
87
+ highWaterMark: 65536
88
+ });
89
+ let failure;
90
+ stream.on("error", (error) => {
91
+ failure = error;
92
+ });
93
+ try {
94
+ let buffer = "";
95
+ for (const part of jsonParts(value)) {
96
+ buffer += part;
97
+ if (buffer.length >= 65536) {
98
+ if (failure) throw failure;
99
+ if (!stream.write(buffer)) await (0, import_node_events.once)(stream, "drain");
100
+ buffer = "";
101
+ }
102
+ }
103
+ if (failure) throw failure;
104
+ stream.end(buffer + "\n");
105
+ await (0, import_node_events.once)(stream, "close");
106
+ if (failure) throw failure;
107
+ await import_promises.default.rename(temporary, file);
108
+ } catch (error) {
109
+ stream.destroy();
110
+ if (!stream.closed) await (0, import_node_events.once)(stream, "close").catch(() => void 0);
111
+ await import_promises.default.rm(temporary, { force: true });
112
+ throw error;
113
+ }
114
+ };
115
+
116
+ // src/lib/replay/memoryProfile.ts
117
+ var import_node_fs2 = __toESM(require("fs"));
118
+ var import_node_path2 = __toESM(require("path"));
119
+ var createReplayMemoryProfile = (file = process.env.TRADEJS_REPLAY_MEMORY_PROFILE) => {
120
+ if (!file) return void 0;
121
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
122
+ return (phase, counts = {}) => {
123
+ import_node_fs2.default.appendFileSync(
124
+ file,
125
+ JSON.stringify({
126
+ schemaVersion: 1,
127
+ observedAt: Date.now(),
128
+ pid: process.pid,
129
+ phase,
130
+ counts,
131
+ ...process.memoryUsage(),
132
+ maxRssKiB: process.resourceUsage().maxRSS
133
+ }) + "\n"
134
+ );
135
+ };
136
+ };
137
+
138
+ // src/scripts/replayRuntimeEvidence.ts
139
+ var import_promises2 = __toESM(require("fs/promises"));
140
+ var import_node_path3 = __toESM(require("path"));
40
141
  var import_args = __toESM(require("args"));
41
142
  var import_chalk2 = __toESM(require("chalk"));
42
143
  var import_redis4 = require("@tradejs/infra/redis");
@@ -55,39 +156,6 @@ var isRuntimeTradeRecord = (value) => {
55
156
  const record = value;
56
157
  return typeof record.orderId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.entryTimestamp === "number" && typeof record.entryPrice === "number" && typeof record.qty === "number" && (record.direction === "LONG" || record.direction === "SHORT");
57
158
  };
58
- var loadRuntimeTrades = async (userName, {
59
- startTime,
60
- endTime
61
- } = {}) => {
62
- const hasWindow = Number.isFinite(startTime) && Number.isFinite(endTime) && startTime != null && endTime != null;
63
- const filterByWindow = (trade) => !hasWindow || trade.entryTimestamp >= startTime && trade.entryTimestamp <= endTime;
64
- if (hasWindow) {
65
- const dayKeys = (0, import_time.getRuntimeStorageDayKeys)(startTime, endTime);
66
- const bucketTrades = (await Promise.all(
67
- dayKeys.map(
68
- (dayKey) => (0, import_redis.getHashJsonValues)(
69
- import_redis.redisKeys.runtimeTradeBucket(userName, dayKey)
70
- )
71
- )
72
- )).flat();
73
- const dedupedBucketTrades = /* @__PURE__ */ new Map();
74
- for (const trade of bucketTrades) {
75
- if (!isRuntimeTradeRecord(trade)) {
76
- continue;
77
- }
78
- dedupedBucketTrades.set(trade.orderId, trade);
79
- }
80
- const windowedTrades = [...dedupedBucketTrades.values()].filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
81
- return windowedTrades;
82
- }
83
- const tradePrefix = import_redis.redisKeys.runtimeTrades(userName);
84
- const bucketPrefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
85
- const keys = (await (0, import_redis.getKeys)(tradePrefix)).filter(
86
- (key) => !key.startsWith(bucketPrefix)
87
- );
88
- const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
89
- return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
90
- };
91
159
  var loadRuntimeClosedTrades = async (userName, {
92
160
  startTime,
93
161
  endTime
@@ -111,6 +179,49 @@ var loadRuntimeClosedTrades = async (userName, {
111
179
  (left, right) => left.exitTimestamp - right.exitTimestamp
112
180
  );
113
181
  };
182
+ var loadRuntimeEvidenceTrades = async (userName, { startTime, endTime }) => {
183
+ const prefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
184
+ const lastDay = (0, import_time.getRuntimeStorageDayKey)(endTime - 1);
185
+ const keys = (await (0, import_redis.getKeys)(prefix)).filter((key) => {
186
+ const day = key.slice(prefix.length);
187
+ return key.startsWith(prefix) && /^\d{4}-\d{2}-\d{2}$/.test(day) && day <= lastDay;
188
+ });
189
+ const retained = /* @__PURE__ */ new Map();
190
+ for (let offset = 0; offset < keys.length; offset += 16) {
191
+ const buckets = await Promise.all(
192
+ keys.slice(offset, offset + 16).map((key) => (0, import_redis.getHashJsonValues)(key))
193
+ );
194
+ for (const row of buckets.flat())
195
+ if (isRuntimeTradeRecord(row)) retained.set(row.orderId, row);
196
+ }
197
+ for (const row of await loadRuntimeClosedTrades(userName, {
198
+ startTime,
199
+ endTime
200
+ }))
201
+ retained.set(row.orderId, row);
202
+ return [...retained.values()].filter(
203
+ (row) => row.entryTimestamp < endTime && (row.status === "active" || Number.isFinite(row.exitTimestamp) && row.exitTimestamp >= startTime)
204
+ ).map((row) => {
205
+ const closesAfterCutoff = row.status === "closed" && row.exitTimestamp >= endTime;
206
+ if (!closesAfterCutoff && !(row.status === "active" && row.lastSyncedAt >= endTime))
207
+ return row;
208
+ return {
209
+ ...row,
210
+ status: "active",
211
+ currentPrice: null,
212
+ currentPnl: null,
213
+ closedPnl: null,
214
+ exitPrice: null,
215
+ actualExitPrice: null,
216
+ exitTimestamp: null,
217
+ exitType: null,
218
+ closeFee: null,
219
+ fundingFee: null,
220
+ totalFee: null,
221
+ lastSyncedAt: void 0
222
+ };
223
+ }).sort((a, b) => a.entryTimestamp - b.entryTimestamp);
224
+ };
114
225
 
115
226
  // src/lib/runtimeSignalsLoader.ts
116
227
  var import_redis2 = require("@tradejs/infra/redis");
@@ -313,30 +424,13 @@ var collectRuntimeDebugEvidence = async ({
313
424
  }) => {
314
425
  const dayKeys = (0, import_time2.getRuntimeStorageDayKeys)(startTime, endTime);
315
426
  const dayKeySet = new Set(dayKeys);
316
- const [
317
- entryTrades,
318
- closedTrades,
319
- signals,
320
- evaluations,
321
- evaluationStatsBuckets,
322
- lineageScopes
323
- ] = await Promise.all([
324
- loadRuntimeTrades(userName, { startTime, endTime }),
325
- loadRuntimeClosedTrades(userName, { startTime, endTime }),
427
+ const [trades, signals, evaluations, evaluationStatsBuckets, lineageScopes] = await Promise.all([
428
+ loadRuntimeEvidenceTrades(userName, { startTime, endTime }),
326
429
  loadRuntimeSignals(userName, { startTime, endTime }),
327
430
  loadRuntimeSignalEvaluations(userName, { startTime, endTime }),
328
431
  loadRuntimeSignalEvaluationStatsBuckets(userName),
329
432
  loadRuntimeLineageScopes(userName, { startTime, endTime })
330
433
  ]);
331
- const tradesByOrderId = new Map(
332
- entryTrades.map((trade) => [trade.orderId, trade])
333
- );
334
- for (const trade of closedTrades) {
335
- tradesByOrderId.set(trade.orderId, trade);
336
- }
337
- const trades = [...tradesByOrderId.values()].sort(
338
- (left, right) => left.entryTimestamp - right.entryTimestamp
339
- );
340
434
  const filteredStats = evaluationStatsBuckets.filter(
341
435
  (entry) => dayKeySet.has(entry.dayKey) && (!deploymentId || entry.deploymentId === deploymentId) && (!strategies?.length || strategies.includes(entry.strategy))
342
436
  );
@@ -666,10 +760,10 @@ var loadRuntimePayload = async ({
666
760
  runtimeEvidenceFile,
667
761
  projectRoot
668
762
  }) => {
669
- const runtimeEvidencePath = typeof runtimeEvidenceFile === "string" && runtimeEvidenceFile.trim() ? import_node_path.default.resolve(projectRoot, runtimeEvidenceFile.trim()) : null;
763
+ const runtimeEvidencePath = typeof runtimeEvidenceFile === "string" && runtimeEvidenceFile.trim() ? import_node_path3.default.resolve(projectRoot, runtimeEvidenceFile.trim()) : null;
670
764
  if (runtimeEvidencePath) {
671
765
  const artifact = JSON.parse(
672
- await import_promises.default.readFile(runtimeEvidencePath, "utf8")
766
+ await import_promises2.default.readFile(runtimeEvidencePath, "utf8")
673
767
  );
674
768
  return {
675
769
  source: "file",
@@ -755,10 +849,12 @@ var replayRuntimeEvidence = async () => {
755
849
  },
756
850
  runtime: runtime.payload
757
851
  };
758
- const outPath = import_node_path.default.resolve(projectRoot, String(flags.out));
759
- await import_promises.default.mkdir(import_node_path.default.dirname(outPath), { recursive: true });
760
- await import_promises.default.writeFile(outPath, `${JSON.stringify(artifact, null, 2)}
761
- `, "utf8");
852
+ const outPath = import_node_path3.default.resolve(projectRoot, String(flags.out));
853
+ await import_promises2.default.mkdir(import_node_path3.default.dirname(outPath), { recursive: true });
854
+ const memory = createReplayMemoryProfile();
855
+ memory?.("evidence:write:start");
856
+ await writeReplayJsonFile(outPath, artifact);
857
+ memory?.("evidence:write:complete");
762
858
  console.log(import_chalk2.default.green(`Wrote ${outPath}`));
763
859
  console.log(
764
860
  JSON.stringify(