@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.
@@ -26,6 +26,7 @@ __export(runtimeRedis_exports, {
26
26
  loadRuntimeActiveTradeOrderIds: () => loadRuntimeActiveTradeOrderIds,
27
27
  loadRuntimeActiveTrades: () => loadRuntimeActiveTrades,
28
28
  loadRuntimeClosedTrades: () => loadRuntimeClosedTrades,
29
+ loadRuntimeEvidenceTrades: () => loadRuntimeEvidenceTrades,
29
30
  loadRuntimeStrategyConfigs: () => import_runtimeStrategyConfigs.loadRuntimeStrategyConfigs,
30
31
  loadRuntimeStrategyNames: () => import_runtimeStrategyConfigs.loadRuntimeStrategyNames,
31
32
  loadRuntimeTrades: () => loadRuntimeTrades,
@@ -104,6 +105,49 @@ var loadRuntimeClosedTrades = async (userName, {
104
105
  (left, right) => left.exitTimestamp - right.exitTimestamp
105
106
  );
106
107
  };
108
+ var loadRuntimeEvidenceTrades = async (userName, { startTime, endTime }) => {
109
+ const prefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
110
+ const lastDay = (0, import_time.getRuntimeStorageDayKey)(endTime - 1);
111
+ const keys = (await (0, import_redis.getKeys)(prefix)).filter((key) => {
112
+ const day = key.slice(prefix.length);
113
+ return key.startsWith(prefix) && /^\d{4}-\d{2}-\d{2}$/.test(day) && day <= lastDay;
114
+ });
115
+ const retained = /* @__PURE__ */ new Map();
116
+ for (let offset = 0; offset < keys.length; offset += 16) {
117
+ const buckets = await Promise.all(
118
+ keys.slice(offset, offset + 16).map((key) => (0, import_redis.getHashJsonValues)(key))
119
+ );
120
+ for (const row of buckets.flat())
121
+ if (isRuntimeTradeRecord(row)) retained.set(row.orderId, row);
122
+ }
123
+ for (const row of await loadRuntimeClosedTrades(userName, {
124
+ startTime,
125
+ endTime
126
+ }))
127
+ retained.set(row.orderId, row);
128
+ return [...retained.values()].filter(
129
+ (row) => row.entryTimestamp < endTime && (row.status === "active" || Number.isFinite(row.exitTimestamp) && row.exitTimestamp >= startTime)
130
+ ).map((row) => {
131
+ const closesAfterCutoff = row.status === "closed" && row.exitTimestamp >= endTime;
132
+ if (!closesAfterCutoff && !(row.status === "active" && row.lastSyncedAt >= endTime))
133
+ return row;
134
+ return {
135
+ ...row,
136
+ status: "active",
137
+ currentPrice: null,
138
+ currentPnl: null,
139
+ closedPnl: null,
140
+ exitPrice: null,
141
+ actualExitPrice: null,
142
+ exitTimestamp: null,
143
+ exitType: null,
144
+ closeFee: null,
145
+ fundingFee: null,
146
+ totalFee: null,
147
+ lastSyncedAt: void 0
148
+ };
149
+ }).sort((a, b) => a.entryTimestamp - b.entryTimestamp);
150
+ };
107
151
  var loadRuntimeActiveTradeOrderIds = async (userName) => {
108
152
  const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeActiveTrades(userName));
109
153
  const refs = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
@@ -144,6 +188,7 @@ var loadStrategyResultSymbols = async ({
144
188
  loadRuntimeActiveTradeOrderIds,
145
189
  loadRuntimeActiveTrades,
146
190
  loadRuntimeClosedTrades,
191
+ loadRuntimeEvidenceTrades,
147
192
  loadRuntimeStrategyConfigs,
148
193
  loadRuntimeStrategyNames,
149
194
  loadRuntimeTrades,
@@ -66,8 +66,21 @@ const timer = setInterval(() => {
66
66
  try {
67
67
  const result = JSON.parse(fs.readFileSync(file + '.result', 'utf8'));
68
68
  console.log(result.message);
69
+ if (result.exitCode !== 0) {
70
+ let fd;
71
+ try {
72
+ fd = fs.openSync(file + '.log', fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
73
+ const size = fs.fstatSync(fd).size;
74
+ const tail = Buffer.alloc(Math.min(size, 65536));
75
+ fs.readSync(fd, tail, 0, tail.length, Math.max(0, size - tail.length));
76
+ // The worker redacts each line before persisting or returning it through MCP.
77
+ for (const line of tail.toString('utf8').split(/[\\r\\n]+/).filter(Boolean).slice(-150)) console.error('Host replay: ' + line);
78
+ } catch (error) {
79
+ console.error('Host replay log unavailable: ' + error.code);
80
+ } finally { if (fd !== undefined) fs.closeSync(fd); }
81
+ }
69
82
  clearInterval(timer);
70
- process.exit(result.exitCode === 0 ? 0 : 1);
83
+ process.exitCode = result.exitCode === 0 ? 0 : 1;
71
84
  } catch (error) { if (error.code !== 'ENOENT') throw error; }
72
85
  }, 1000);
73
86
  `;
@@ -90,7 +103,7 @@ var buildMcpReplayProcess = async ({
90
103
  const heartbeat = JSON.parse(
91
104
  await import_promises.default.readFile(import_node_path.default.join(root, "heartbeat.json"), "utf8")
92
105
  );
93
- if (!Number.isFinite(heartbeat.observedAt) || Date.now() - heartbeat.observedAt < 0 || Date.now() - heartbeat.observedAt > 12e4)
106
+ if (heartbeat.protocolVersion !== 2 || !Number.isFinite(heartbeat.observedAt) || Date.now() - heartbeat.observedAt < 0 || Date.now() - heartbeat.observedAt > 12e4)
94
107
  throw new Error("Host replay runner heartbeat is stale");
95
108
  const bytes = await import_promises.default.readFile(evidencePath);
96
109
  const request = JSON.stringify({
@@ -123,14 +136,17 @@ var main = async (options = {}) => {
123
136
  let leaseLost = false;
124
137
  let manifestHash = "";
125
138
  let replayReady = false;
139
+ let replayReadiness = "unknown";
126
140
  const updateReplayReady = async () => {
127
141
  try {
128
142
  const root = process.env.MCP_REPLAY_REQUEST_ROOT || `${process.env.PROJECT_CWD || process.cwd()}/data/mcp-replay-requests`;
129
143
  const heartbeat2 = JSON.parse(
130
144
  await (0, import_promises2.readFile)(`${root}/heartbeat.json`, "utf8")
131
145
  );
132
- replayReady = Number.isFinite(heartbeat2.observedAt) && Date.now() - heartbeat2.observedAt >= 0 && Date.now() - heartbeat2.observedAt < 12e4;
146
+ replayReadiness = heartbeat2.readiness === "ready" || heartbeat2.readiness === "waiting_resources" ? heartbeat2.readiness : "unknown";
147
+ replayReady = heartbeat2.protocolVersion === 2 && Number.isFinite(heartbeat2.observedAt) && Date.now() - heartbeat2.observedAt >= 0 && Date.now() - heartbeat2.observedAt < 12e4;
133
148
  } catch {
149
+ replayReadiness = "unknown";
134
150
  replayReady = false;
135
151
  }
136
152
  };
@@ -147,7 +163,8 @@ var main = async (options = {}) => {
147
163
  const heartbeatValue = () => ({
148
164
  observedAt: Date.now(),
149
165
  packageManifestSha256: manifestHash,
150
- replayAvailable: replayReady
166
+ replayAvailable: replayReady,
167
+ replayReadiness
151
168
  });
152
169
  const stop = () => {
153
170
  stopping = true;
@@ -208,7 +225,9 @@ var main = async (options = {}) => {
208
225
  cursor = page.cursor;
209
226
  for (const key of page.ids) {
210
227
  const job = await import_redis.mcpStorage.get(key);
211
- if (job?.status === "queued" && (!queued || job.createdAt < queued.createdAt))
228
+ if (job && ["queued", "waiting_resources", "waiting_executor"].includes(
229
+ job.status
230
+ ) && (!queued || job.createdAt < queued.createdAt))
212
231
  queued = job;
213
232
  }
214
233
  } while (cursor !== "0" && !stopping);
@@ -219,13 +238,13 @@ var main = async (options = {}) => {
219
238
  const jobKey = (0, import_mcp.mcpJobStorageId)(queued.userName, queued.id);
220
239
  const started = {
221
240
  ...queued,
222
- status: "running",
223
- startedAt: Date.now(),
241
+ status: queued.kind === "runtime-feedback-replay" ? "waiting_executor" : "running",
242
+ ...queued.kind === "runtime-feedback-replay" ? {} : { startedAt: Date.now() },
224
243
  updatedAt: Date.now()
225
244
  };
226
245
  if (!await import_redis.mcpStorage.compare(jobKey, queued, started, import_mcp.MCP_JOB_TTL))
227
246
  continue;
228
- if (started.packageManifestSha256 !== manifestHash) {
247
+ if (started.kind !== "runtime-feedback-replay" && started.packageManifestSha256 !== manifestHash) {
229
248
  await import_redis.mcpStorage.compare(
230
249
  jobKey,
231
250
  started,
@@ -299,7 +318,7 @@ var main = async (options = {}) => {
299
318
  status: "failed",
300
319
  finishedAt: Date.now(),
301
320
  updatedAt: Date.now(),
302
- error: error instanceof Error ? error.message : "Replay preparation failed"
321
+ error: error instanceof Error ? (0, import_mcp.redactMcpJobLog)(error.message) : "Replay preparation failed"
303
322
  },
304
323
  import_mcp.MCP_JOB_TTL
305
324
  );
@@ -313,21 +332,15 @@ var main = async (options = {}) => {
313
332
  });
314
333
  const currentChild = child;
315
334
  let timedOut = false;
316
- const timeout = setTimeout(() => {
335
+ let replayStartedAt;
336
+ const timeout = started.kind === "runtime-feedback-replay" ? void 0 : setTimeout(() => {
317
337
  timedOut = true;
318
338
  signalChild("SIGTERM");
319
339
  }, JOB_TIMEOUT_MS);
320
340
  const logs = [];
321
341
  const log = (chunk) => {
322
342
  const lines = chunk.toString().replace(/\u001b\[[0-9;]*m/g, "").split(/[\r\n]+/).filter(Boolean);
323
- logs.push(
324
- ...lines.map(
325
- (line) => line.slice(0, 1e3).replace(
326
- /((?:secret|password|api.?key|token|authorization)\s*[:=]\s*)\S+/gi,
327
- "$1[redacted]"
328
- )
329
- )
330
- );
343
+ logs.push(...lines.map(import_mcp.redactMcpJobLog));
331
344
  if (logs.length > 150) logs.splice(0, logs.length - 150);
332
345
  };
333
346
  currentChild.stdout?.on("data", log);
@@ -340,11 +353,38 @@ var main = async (options = {}) => {
340
353
  killAt ??= Date.now();
341
354
  signalChild(Date.now() - killAt > 8e3 ? "SIGKILL" : "SIGTERM");
342
355
  }
356
+ let replayStatus;
357
+ if (latest?.kind === "runtime-feedback-replay") {
358
+ try {
359
+ const root = process.env.MCP_REPLAY_REQUEST_ROOT || `${process.env.PROJECT_CWD || process.cwd()}/data/mcp-replay-requests`;
360
+ const state = JSON.parse(
361
+ await (0, import_promises2.readFile)(`${root}/${latest.id}.state`, "utf8")
362
+ );
363
+ if (state && typeof state === "object" && "status" in state && (state.status === "waiting_resources" || state.status === "waiting_executor" || state.status === "running"))
364
+ replayStatus = state.status;
365
+ } catch {
366
+ }
367
+ }
368
+ if (replayStatus === "running") {
369
+ replayStartedAt ??= latest?.startedAt ?? Date.now();
370
+ if (Date.now() - replayStartedAt > JOB_TIMEOUT_MS) {
371
+ timedOut = true;
372
+ signalChild("SIGTERM");
373
+ }
374
+ }
343
375
  if (latest && !leaseLost)
344
376
  await import_redis.mcpStorage.compare(
345
377
  jobKey,
346
378
  latest,
347
- { ...latest, logs: [...logs], updatedAt: Date.now() },
379
+ {
380
+ ...latest,
381
+ ...replayStatus && !latest.cancelRequested ? {
382
+ status: replayStatus,
383
+ ...replayStatus === "running" && !latest.startedAt ? { startedAt: Date.now() } : {}
384
+ } : {},
385
+ logs: [...logs],
386
+ updatedAt: Date.now()
387
+ },
348
388
  import_mcp.MCP_JOB_TTL
349
389
  );
350
390
  } catch {
@@ -357,7 +397,7 @@ var main = async (options = {}) => {
357
397
  "error",
358
398
  () => resolve({ code: null, error: "Unable to start research process" })
359
399
  );
360
- currentChild.once("exit", (code) => resolve({ code }));
400
+ currentChild.once("close", (code) => resolve({ code }));
361
401
  });
362
402
  clearTimeout(timeout);
363
403
  clearInterval(monitor);
@@ -374,7 +414,11 @@ var main = async (options = {}) => {
374
414
  exitCode: outcome.code,
375
415
  updatedAt: Date.now(),
376
416
  finishedAt: Date.now(),
377
- ...cancelled ? {} : timedOut ? { error: "Job exceeded one hour" } : stopping ? { error: "Worker stopped; this job was not replayed" } : outcome.code !== 0 ? { error: outcome.error || "Research process failed" } : {}
417
+ ...cancelled ? {} : timedOut ? { error: "Job exceeded one hour" } : stopping ? { error: "Worker stopped; this job was not replayed" } : outcome.code !== 0 ? {
418
+ error: outcome.error || (started.kind === "runtime-feedback-replay" ? logs.find(
419
+ (line) => line.startsWith("Host replay: ")
420
+ ) || logs.at(-1) : void 0) || "Research process failed"
421
+ } : {}
378
422
  };
379
423
  if (await import_redis.mcpStorage.compare(jobKey, latest, next, import_mcp.MCP_JOB_TTL))
380
424
  break;
@@ -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,