@tradejs/cli 3.1.29-beta.282 → 3.1.29-beta.284

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.
@@ -38,7 +38,7 @@ module.exports = __toCommonJS(replay_exports);
38
38
  // src/scripts/replayRunner.ts
39
39
  var import_chalk13 = __toESM(require("chalk"));
40
40
  var import_constants7 = require("@tradejs/core/constants");
41
- var import_data4 = require("@tradejs/core/data");
41
+ var import_data3 = require("@tradejs/core/data");
42
42
  var import_time7 = require("@tradejs/core/time");
43
43
  var import_redis6 = require("@tradejs/infra/redis");
44
44
  var import_candles = require("@tradejs/infra/timescale/candles");
@@ -2708,27 +2708,27 @@ var createCoinMarketCapFetchStats = () => ({
2708
2708
  totalCredits: 0,
2709
2709
  byPath: /* @__PURE__ */ new Map()
2710
2710
  });
2711
- var getCoinMarketCapPathLabel = (path5) => {
2712
- if (path5.includes("global-metrics")) return "global";
2713
- if (path5.includes("cryptocurrency/quotes")) return "reference";
2714
- if (path5.includes("exchange/quotes")) return "exchangeLiquidity";
2715
- if (path5.includes("fear-and-greed")) return "fearGreed";
2716
- if (path5.includes("cmc100")) return "cmc100";
2717
- if (path5.includes("cmc20")) return "cmc20";
2718
- return path5;
2719
- };
2720
- var recordCoinMarketCapFetchStats = (stats, path5, creditCount) => {
2711
+ var getCoinMarketCapPathLabel = (path7) => {
2712
+ if (path7.includes("global-metrics")) return "global";
2713
+ if (path7.includes("cryptocurrency/quotes")) return "reference";
2714
+ if (path7.includes("exchange/quotes")) return "exchangeLiquidity";
2715
+ if (path7.includes("fear-and-greed")) return "fearGreed";
2716
+ if (path7.includes("cmc100")) return "cmc100";
2717
+ if (path7.includes("cmc20")) return "cmc20";
2718
+ return path7;
2719
+ };
2720
+ var recordCoinMarketCapFetchStats = (stats, path7, creditCount) => {
2721
2721
  if (!stats) return;
2722
- const existing = stats.byPath.get(path5) ?? { requests: 0, credits: 0 };
2722
+ const existing = stats.byPath.get(path7) ?? { requests: 0, credits: 0 };
2723
2723
  existing.requests += 1;
2724
2724
  existing.credits += creditCount ?? 0;
2725
2725
  stats.requests += 1;
2726
2726
  stats.totalCredits += creditCount ?? 0;
2727
- stats.byPath.set(path5, existing);
2727
+ stats.byPath.set(path7, existing);
2728
2728
  };
2729
2729
  var formatCoinMarketCapFetchStats = (stats) => {
2730
2730
  const byPath = [...stats.byPath.entries()].map(
2731
- ([path5, item]) => `${getCoinMarketCapPathLabel(path5)}=${item.requests}req/${item.credits}cr`
2731
+ ([path7, item]) => `${getCoinMarketCapPathLabel(path7)}=${item.requests}req/${item.credits}cr`
2732
2732
  ).join(" ");
2733
2733
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
2734
2734
  };
@@ -4748,10 +4748,32 @@ var compactReplaySignal = (signal) => {
4748
4748
  };
4749
4749
  };
4750
4750
 
4751
+ // src/lib/replay/memoryProfile.ts
4752
+ var import_node_fs = __toESM(require("fs"));
4753
+ var import_node_path2 = __toESM(require("path"));
4754
+ var createReplayMemoryProfile = (file = process.env.TRADEJS_REPLAY_MEMORY_PROFILE) => {
4755
+ if (!file) return void 0;
4756
+ import_node_fs.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
4757
+ return (phase, counts = {}) => {
4758
+ import_node_fs.default.appendFileSync(
4759
+ file,
4760
+ JSON.stringify({
4761
+ schemaVersion: 1,
4762
+ observedAt: Date.now(),
4763
+ pid: process.pid,
4764
+ phase,
4765
+ counts,
4766
+ ...process.memoryUsage(),
4767
+ maxRssKiB: process.resourceUsage().maxRSS
4768
+ }) + "\n"
4769
+ );
4770
+ };
4771
+ };
4772
+
4751
4773
  // src/lib/replay/historicalSignalsReplay.ts
4752
4774
  var import_chalk9 = __toESM(require("chalk"));
4753
4775
  var import_progress4 = __toESM(require("progress"));
4754
- var import_node_path2 = __toESM(require("path"));
4776
+ var import_node_path3 = __toESM(require("path"));
4755
4777
  var import_node_module = require("module");
4756
4778
  var import_strategies3 = require("@tradejs/core/strategies");
4757
4779
  var import_time3 = require("@tradejs/core/time");
@@ -5300,6 +5322,11 @@ var prepareHistoricalReplay = async (context, adapters) => {
5300
5322
  cacheOnly: true,
5301
5323
  interval
5302
5324
  });
5325
+ if (symbolRuntimes.length % 50 === 0)
5326
+ adapters.sampleMemory?.("history:loaded", {
5327
+ symbols: symbolRuntimes.length + 1,
5328
+ candles: data.length
5329
+ });
5303
5330
  const preparedData = buildPreparedData({
5304
5331
  data,
5305
5332
  btcData: references.btcMarketData,
@@ -5396,6 +5423,11 @@ var prepareHistoricalReplay = async (context, adapters) => {
5396
5423
  strategies: strategiesForSymbol
5397
5424
  };
5398
5425
  symbolRuntimes.push(symbolRuntime);
5426
+ if (symbolRuntimes.length % 50 === 0 || symbol === preparedRun.tickers.at(-1))
5427
+ adapters.sampleMemory?.("strategies:initializing", {
5428
+ symbols: symbolRuntimes.length,
5429
+ strategiesPerSymbol: strategiesForSymbol.length
5430
+ });
5399
5431
  adapters.progress.tick({
5400
5432
  skipped: adapters.display.skipped(skippedSymbols),
5401
5433
  symbol: adapters.display.symbol(symbol)
@@ -5413,6 +5445,10 @@ var prepareHistoricalReplay = async (context, adapters) => {
5413
5445
  runtimeLineages
5414
5446
  };
5415
5447
  };
5448
+ var releaseHistoricalReplayPlan = (plan) => {
5449
+ plan.symbolRuntimes.length = 0;
5450
+ plan.sharedReplayKeyPrefixes.length = 0;
5451
+ };
5416
5452
 
5417
5453
  // src/lib/replay/historicalSignalsReplaySkipEvidence.ts
5418
5454
  var import_data = require("@tradejs/core/data");
@@ -5712,7 +5748,7 @@ var getProjectReplayCacheReleasers = () => {
5712
5748
  }
5713
5749
  try {
5714
5750
  const requireFromProject = (0, import_node_module.createRequire)(
5715
- import_node_path2.default.join(replayProjectRoot, "package.json")
5751
+ import_node_path3.default.join(replayProjectRoot, "package.json")
5716
5752
  );
5717
5753
  projectReplayCacheReleasers = requireFromProject(
5718
5754
  "@tradejs/core/strategies"
@@ -5853,6 +5889,8 @@ var runHistoricalSignalsReplay = async ({
5853
5889
  collectSkipEvidence = true,
5854
5890
  collectSignals = true
5855
5891
  }) => {
5892
+ const memory = createReplayMemoryProfile();
5893
+ memory?.("prepare:start");
5856
5894
  const startedAt = Date.now();
5857
5895
  const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
5858
5896
  const projectHooks = projectConfig.hooks;
@@ -5869,6 +5907,7 @@ var runHistoricalSignalsReplay = async ({
5869
5907
  width: 30
5870
5908
  }
5871
5909
  );
5910
+ memory?.("references:loaded");
5872
5911
  const plan = await prepareHistoricalReplay(
5873
5912
  {
5874
5913
  userName: replayUserName,
@@ -5893,9 +5932,11 @@ var runHistoricalSignalsReplay = async ({
5893
5932
  skipped: (value) => import_chalk9.default.yellow(value),
5894
5933
  symbol: (value) => import_chalk9.default.gray(value)
5895
5934
  },
5896
- buildLineage: buildRuntimeLineage
5935
+ buildLineage: buildRuntimeLineage,
5936
+ sampleMemory: memory
5897
5937
  }
5898
5938
  );
5939
+ memory?.("strategies:prepared", { symbols: plan.symbolRuntimes.length });
5899
5940
  const hookContext = buildAfterSignalsContext({
5900
5941
  connector: replayConnector,
5901
5942
  connectorName: preparedRun.connectorName,
@@ -5945,13 +5986,16 @@ var runHistoricalSignalsReplay = async ({
5945
5986
  )
5946
5987
  }
5947
5988
  );
5989
+ memory?.("execution:complete", { signals: execution.signals.length });
5990
+ releaseHistoricalReplayPlan(plan);
5991
+ memory?.("execution:released");
5948
5992
  const artifacts = replayConnector.getReplayArtifacts();
5949
5993
  import_logger.logger.info(
5950
5994
  import_chalk9.default.gray(
5951
5995
  `signals replay historical cycles: ${plan.orderedTimestamps.length} (aborted=${execution.abortedCycles}) done in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
5952
5996
  )
5953
5997
  );
5954
- return collectHistoricalReplayResult({
5998
+ const result = collectHistoricalReplayResult({
5955
5999
  strategies: loadedStrategies,
5956
6000
  artifacts,
5957
6001
  signals: execution.signals,
@@ -5960,6 +6004,8 @@ var runHistoricalSignalsReplay = async ({
5960
6004
  runtimeLineages: plan.runtimeLineages,
5961
6005
  replayLineageScopes: execution.replayLineageScopes
5962
6006
  });
6007
+ memory?.("result:collected");
6008
+ return result;
5963
6009
  };
5964
6010
 
5965
6011
  // src/lib/replay/chartSnapshot.ts
@@ -8794,9 +8840,87 @@ var saveAndPrintReplayRuntimeComparison = async ({
8794
8840
  };
8795
8841
 
8796
8842
  // src/lib/replay/outputReport.ts
8843
+ var import_promises3 = __toESM(require("fs/promises"));
8844
+ var import_node_path5 = __toESM(require("path"));
8845
+
8846
+ // src/lib/replay/jsonFile.ts
8847
+ var import_node_fs2 = __toESM(require("fs"));
8797
8848
  var import_promises2 = __toESM(require("fs/promises"));
8798
- var import_node_path3 = __toESM(require("path"));
8799
- var import_data3 = require("@tradejs/core/data");
8849
+ var import_node_path4 = __toESM(require("path"));
8850
+ var import_node_events = require("events");
8851
+ var import_node_crypto2 = require("crypto");
8852
+ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
8853
+ if (value === null || typeof value !== "object") {
8854
+ yield JSON.stringify(value) ?? "null";
8855
+ return;
8856
+ }
8857
+ if (ancestors.has(value)) throw new TypeError("Circular JSON evidence");
8858
+ ancestors.add(value);
8859
+ try {
8860
+ if (Array.isArray(value)) {
8861
+ yield "[";
8862
+ for (let index = 0; index < value.length; index++) {
8863
+ if (index) yield ",";
8864
+ yield* jsonParts(value[index], ancestors);
8865
+ }
8866
+ yield "]";
8867
+ } else {
8868
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
8869
+ throw new TypeError("JSON evidence requires plain records");
8870
+ if ("toJSON" in value && typeof value.toJSON === "function")
8871
+ throw new TypeError("JSON evidence cannot contain toJSON hooks");
8872
+ yield "{";
8873
+ let first = true;
8874
+ for (const key of Object.keys(value)) {
8875
+ const field = Reflect.get(value, key);
8876
+ if (field === void 0 || typeof field === "function" || typeof field === "symbol")
8877
+ continue;
8878
+ if (!first) yield ",";
8879
+ first = false;
8880
+ yield JSON.stringify(key) + ":";
8881
+ yield* jsonParts(field, ancestors);
8882
+ }
8883
+ yield "}";
8884
+ }
8885
+ } finally {
8886
+ ancestors.delete(value);
8887
+ }
8888
+ }
8889
+ var writeReplayJsonFile = async (file, value) => {
8890
+ await import_promises2.default.mkdir(import_node_path4.default.dirname(file), { recursive: true });
8891
+ const temporary = file + ".tmp-" + (0, import_node_crypto2.randomUUID)();
8892
+ const stream = import_node_fs2.default.createWriteStream(temporary, {
8893
+ flags: "wx",
8894
+ highWaterMark: 65536
8895
+ });
8896
+ let failure;
8897
+ stream.on("error", (error) => {
8898
+ failure = error;
8899
+ });
8900
+ try {
8901
+ let buffer = "";
8902
+ for (const part of jsonParts(value)) {
8903
+ buffer += part;
8904
+ if (buffer.length >= 65536) {
8905
+ if (failure) throw failure;
8906
+ if (!stream.write(buffer)) await (0, import_node_events.once)(stream, "drain");
8907
+ buffer = "";
8908
+ }
8909
+ }
8910
+ if (failure) throw failure;
8911
+ stream.end(buffer + "\n");
8912
+ await (0, import_node_events.once)(stream, "close");
8913
+ if (failure) throw failure;
8914
+ await import_promises2.default.rename(temporary, file);
8915
+ } catch (error) {
8916
+ stream.destroy();
8917
+ if (!stream.closed) await (0, import_node_events.once)(stream, "close").catch(() => void 0);
8918
+ await import_promises2.default.rm(temporary, { force: true });
8919
+ throw error;
8920
+ }
8921
+ };
8922
+
8923
+ // src/lib/replay/outputReport.ts
8800
8924
  var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
8801
8925
  timeZone: "Europe/Moscow",
8802
8926
  dateStyle: "short",
@@ -9099,8 +9223,8 @@ var writeReplayOutputReport = async ({
9099
9223
  strategySnapshot,
9100
9224
  runtimeComparison
9101
9225
  }) => {
9102
- const outputDir = import_node_path3.default.resolve(projectRoot, "data/replay/output");
9103
- await import_promises2.default.mkdir(outputDir, { recursive: true });
9226
+ const outputDir = import_node_path5.default.resolve(projectRoot, "data/replay/output");
9227
+ await import_promises3.default.mkdir(outputDir, { recursive: true });
9104
9228
  const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
9105
9229
  const report = {
9106
9230
  reportType: "replay-output",
@@ -9135,8 +9259,8 @@ var writeReplayOutputReport = async ({
9135
9259
  runtimeComparison,
9136
9260
  perTradeAnalysis
9137
9261
  };
9138
- const markdownPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.md`);
9139
- const jsonPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.json`);
9262
+ const markdownPath = import_node_path5.default.join(outputDir, `${timestamp}-replay-report.md`);
9263
+ const jsonPath = import_node_path5.default.join(outputDir, `${timestamp}-replay-report.json`);
9140
9264
  const lines = [
9141
9265
  "# Replay Report",
9142
9266
  "",
@@ -9201,11 +9325,13 @@ var writeReplayOutputReport = async ({
9201
9325
  "Full machine-readable details are stored in the JSON sidecar.",
9202
9326
  ""
9203
9327
  ];
9328
+ const memory = createReplayMemoryProfile();
9329
+ memory?.("report:write:start");
9204
9330
  await Promise.all([
9205
- import_promises2.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
9206
- import_promises2.default.writeFile(jsonPath, `${(0, import_data3.toJson)(report, true)}
9207
- `, "utf8")
9331
+ import_promises3.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
9332
+ writeReplayJsonFile(jsonPath, report)
9208
9333
  ]);
9334
+ memory?.("report:write:complete");
9209
9335
  return {
9210
9336
  markdownPath,
9211
9337
  jsonPath
@@ -9213,9 +9339,9 @@ var writeReplayOutputReport = async ({
9213
9339
  };
9214
9340
 
9215
9341
  // src/lib/replay/portfolioReport.ts
9216
- var import_promises3 = __toESM(require("fs/promises"));
9217
- var import_node_path4 = __toESM(require("path"));
9218
- var import_node_crypto2 = require("crypto");
9342
+ var import_promises4 = __toESM(require("fs/promises"));
9343
+ var import_node_path6 = __toESM(require("path"));
9344
+ var import_node_crypto3 = require("crypto");
9219
9345
  var DAY_MS4 = 864e5;
9220
9346
  var WINDOWS = [365, 180, 90, 30, 7];
9221
9347
  var COLORS = [
@@ -9393,8 +9519,8 @@ var writePortfolioReport = async ({
9393
9519
  lineage,
9394
9520
  command
9395
9521
  }) => {
9396
- const outputDir = import_node_path4.default.resolve(projectRoot, "data/replay/output");
9397
- await import_promises3.default.mkdir(outputDir, { recursive: true });
9522
+ const outputDir = import_node_path6.default.resolve(projectRoot, "data/replay/output");
9523
+ await import_promises4.default.mkdir(outputDir, { recursive: true });
9398
9524
  const trades = replayResult.strategies.flatMap(
9399
9525
  (strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
9400
9526
  );
@@ -9466,11 +9592,11 @@ var writePortfolioReport = async ({
9466
9592
  });
9467
9593
  const prefix = `${timestamp}-portfolio-backtest`;
9468
9594
  const paths = {
9469
- markdown: import_node_path4.default.join(outputDir, `${prefix}.md`),
9470
- json: import_node_path4.default.join(outputDir, `${prefix}.json`),
9471
- equitySvg: import_node_path4.default.join(outputDir, `${prefix}-equity.svg`),
9472
- drawdownSvg: import_node_path4.default.join(outputDir, `${prefix}-drawdown.svg`),
9473
- html: import_node_path4.default.join(outputDir, `${prefix}.html`)
9595
+ markdown: import_node_path6.default.join(outputDir, `${prefix}.md`),
9596
+ json: import_node_path6.default.join(outputDir, `${prefix}.json`),
9597
+ equitySvg: import_node_path6.default.join(outputDir, `${prefix}-equity.svg`),
9598
+ drawdownSvg: import_node_path6.default.join(outputDir, `${prefix}-drawdown.svg`),
9599
+ html: import_node_path6.default.join(outputDir, `${prefix}.html`)
9474
9600
  };
9475
9601
  const report = {
9476
9602
  schema: "tradejs-portfolio-backtest/v1",
@@ -9496,7 +9622,7 @@ var writePortfolioReport = async ({
9496
9622
  },
9497
9623
  trades
9498
9624
  };
9499
- const checksum = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
9625
+ const checksum = (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
9500
9626
  const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
9501
9627
  const markdown = `# Portfolio Backtest
9502
9628
 
@@ -9508,9 +9634,9 @@ var writePortfolioReport = async ({
9508
9634
  - Report checksum: \`${checksum}\`
9509
9635
  - Command: \`${command}\`
9510
9636
 
9511
- ![Strategy PnL overlay](./${import_node_path4.default.basename(paths.equitySvg)})
9637
+ ![Strategy PnL overlay](./${import_node_path6.default.basename(paths.equitySvg)})
9512
9638
 
9513
- ![Portfolio drawdown](./${import_node_path4.default.basename(paths.drawdownSvg)})
9639
+ ![Portfolio drawdown](./${import_node_path6.default.basename(paths.drawdownSvg)})
9514
9640
 
9515
9641
  ## Portfolio metrics
9516
9642
 
@@ -9522,16 +9648,16 @@ ${markdownTable(strategyRows)}
9522
9648
  `;
9523
9649
  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>`;
9524
9650
  await Promise.all([
9525
- import_promises3.default.writeFile(paths.markdown, markdown, "utf8"),
9526
- import_promises3.default.writeFile(
9651
+ import_promises4.default.writeFile(paths.markdown, markdown, "utf8"),
9652
+ import_promises4.default.writeFile(
9527
9653
  paths.json,
9528
9654
  `${JSON.stringify({ ...report, checksum }, null, 2)}
9529
9655
  `,
9530
9656
  "utf8"
9531
9657
  ),
9532
- import_promises3.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
9533
- import_promises3.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
9534
- import_promises3.default.writeFile(paths.html, html, "utf8")
9658
+ import_promises4.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
9659
+ import_promises4.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
9660
+ import_promises4.default.writeFile(paths.html, html, "utf8")
9535
9661
  ]);
9536
9662
  return { ...paths, checksum };
9537
9663
  };
@@ -9845,13 +9971,13 @@ var replayBacktest = async () => {
9845
9971
  deploymentId: deployment.id,
9846
9972
  assetClasses: deployment.assetClasses,
9847
9973
  deployment,
9848
- closedIntervalMs: (0, import_data4.intervalToMs)(replayInterval)
9974
+ closedIntervalMs: (0, import_data3.intervalToMs)(replayInterval)
9849
9975
  });
9850
9976
  if (!preparedRun || isReplayUpdateOnlyRun) {
9851
9977
  return;
9852
9978
  }
9853
9979
  if (useAllData) {
9854
- const intervalMs = (0, import_data4.intervalToMs)(replayInterval);
9980
+ const intervalMs = (0, import_data3.intervalToMs)(replayInterval);
9855
9981
  const edges = await (0, import_candles.getDataEdgesForSymbols)(
9856
9982
  deployment.provider,
9857
9983
  preparedRun.tickers,