@tradejs/cli 2.0.16 → 2.0.18

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.
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/lib/hyperliquidWhaleBackfill.ts
@@ -24,6 +34,7 @@ __export(hyperliquidWhaleBackfill_exports, {
24
34
  fetchHyperliquidUserFillsByTime: () => fetchHyperliquidUserFillsByTime
25
35
  });
26
36
  module.exports = __toCommonJS(hyperliquidWhaleBackfill_exports);
37
+ var import_progress = __toESM(require("progress"));
27
38
  var import_timescale = require("@tradejs/infra/timescale");
28
39
  var import_strategies = require("@tradejs/node/strategies");
29
40
 
@@ -398,11 +409,36 @@ var backfillHyperliquidWhaleContext = async (params) => {
398
409
  toMs,
399
410
  ...identity
400
411
  });
412
+ const totalCoverageBuckets = Math.ceil(
413
+ (toMs - fromMs) / HYPERLIQUID_WHALE_BUCKET_MS
414
+ );
415
+ const coverageBar = new import_progress.default(
416
+ "Hyperliquid coverage :current/:total [:bar][:percent] :eta(s) rows=:rows chunk=:chunk",
417
+ {
418
+ total: Math.max(1, totalCoverageBuckets),
419
+ width: 24
420
+ }
421
+ );
422
+ let displayedCoverageBuckets = 0;
401
423
  const coverageBuckets = await (0, import_timescale.rebuildHyperliquidWhaleCoverageRows)({
402
424
  fromMs,
403
425
  toMs,
404
426
  expectedWhales: whales.addresses.length,
405
- ...identity
427
+ ...identity,
428
+ onProgress: (progress) => {
429
+ const next = Math.max(
430
+ displayedCoverageBuckets,
431
+ Math.min(progress.completedBuckets, coverageBar.total)
432
+ );
433
+ const delta = next - displayedCoverageBuckets;
434
+ if (delta > 0) {
435
+ coverageBar.tick(delta, {
436
+ rows: progress.rows,
437
+ chunk: `${progress.chunkIndex}/${progress.totalChunks}`
438
+ });
439
+ displayedCoverageBuckets = next;
440
+ }
441
+ }
406
442
  });
407
443
  const result = {
408
444
  cached: false,
@@ -4501,6 +4501,7 @@ var timeOperation = async (label, operation, log = console.log) => {
4501
4501
  };
4502
4502
 
4503
4503
  // src/lib/hyperliquidWhaleBackfill.ts
4504
+ var import_progress3 = __toESM(require("progress"));
4504
4505
  var import_timescale4 = require("@tradejs/infra/timescale");
4505
4506
  var import_strategies2 = require("@tradejs/node/strategies");
4506
4507
 
@@ -4875,11 +4876,36 @@ var backfillHyperliquidWhaleContext = async (params) => {
4875
4876
  toMs,
4876
4877
  ...identity
4877
4878
  });
4879
+ const totalCoverageBuckets = Math.ceil(
4880
+ (toMs - fromMs) / HYPERLIQUID_WHALE_BUCKET_MS
4881
+ );
4882
+ const coverageBar = new import_progress3.default(
4883
+ "Hyperliquid coverage :current/:total [:bar][:percent] :eta(s) rows=:rows chunk=:chunk",
4884
+ {
4885
+ total: Math.max(1, totalCoverageBuckets),
4886
+ width: 24
4887
+ }
4888
+ );
4889
+ let displayedCoverageBuckets = 0;
4878
4890
  const coverageBuckets = await (0, import_timescale4.rebuildHyperliquidWhaleCoverageRows)({
4879
4891
  fromMs,
4880
4892
  toMs,
4881
4893
  expectedWhales: whales.addresses.length,
4882
- ...identity
4894
+ ...identity,
4895
+ onProgress: (progress) => {
4896
+ const next = Math.max(
4897
+ displayedCoverageBuckets,
4898
+ Math.min(progress.completedBuckets, coverageBar.total)
4899
+ );
4900
+ const delta = next - displayedCoverageBuckets;
4901
+ if (delta > 0) {
4902
+ coverageBar.tick(delta, {
4903
+ rows: progress.rows,
4904
+ chunk: `${progress.chunkIndex}/${progress.totalChunks}`
4905
+ });
4906
+ displayedCoverageBuckets = next;
4907
+ }
4908
+ }
4883
4909
  });
4884
4910
  const result = {
4885
4911
  cached: false,
@@ -46,6 +46,17 @@ var RUNTIME_EVIDENCE_PAYLOAD_FILE = "runtime-evidence.json";
46
46
  var RUNTIME_EVIDENCE_COMPLETE_FILE = ".complete";
47
47
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
48
48
  var sha256 = (value) => (0, import_node_crypto.createHash)("sha256").update(value).digest("hex");
49
+ var runtimeEvidenceIdentitySha256 = (artifact) => {
50
+ const identityArtifact = { ...artifact };
51
+ delete identityArtifact.generatedAt;
52
+ const runtime = asRecord(identityArtifact.runtime);
53
+ if (runtime) {
54
+ const identityRuntime = { ...runtime };
55
+ delete identityRuntime.generatedAt;
56
+ identityArtifact.runtime = identityRuntime;
57
+ }
58
+ return sha256(JSON.stringify(identityArtifact));
59
+ };
49
60
  var isExistingDestinationError = (error) => {
50
61
  const code = error.code;
51
62
  return code === "EEXIST" || code === "ENOTEMPTY";
@@ -116,11 +127,12 @@ var publishRuntimeEvidenceBundle = async ({
116
127
  const payload = `${JSON.stringify(artifact, null, 2)}
117
128
  `;
118
129
  const payloadSha256 = sha256(payload);
130
+ const identitySha256 = runtimeEvidenceIdentitySha256(artifact);
119
131
  const safeDeploymentId = safeSegment(deploymentId, "default");
120
132
  const artifactId = [
121
133
  compactTimestamp(startTime),
122
134
  compactTimestamp(endTime),
123
- payloadSha256.slice(0, 16)
135
+ identitySha256.slice(0, 16)
124
136
  ].join("_");
125
137
  const createdAt = Date.now();
126
138
  const manifest = {
@@ -4542,6 +4542,7 @@ var backfillCoinMarketCapContextForReplay = backfillCoinMarketCapContext;
4542
4542
  var backfillCoinMarketCapContextForSignals = (params) => backfillCoinMarketCapContext({ ...params, refreshStaleFearGreed: true });
4543
4543
 
4544
4544
  // src/lib/hyperliquidWhaleBackfill.ts
4545
+ var import_progress3 = __toESM(require("progress"));
4545
4546
  var import_timescale4 = require("@tradejs/infra/timescale");
4546
4547
  var import_strategies2 = require("@tradejs/node/strategies");
4547
4548
 
@@ -4916,11 +4917,36 @@ var backfillHyperliquidWhaleContext = async (params) => {
4916
4917
  toMs,
4917
4918
  ...identity
4918
4919
  });
4920
+ const totalCoverageBuckets = Math.ceil(
4921
+ (toMs - fromMs) / HYPERLIQUID_WHALE_BUCKET_MS
4922
+ );
4923
+ const coverageBar = new import_progress3.default(
4924
+ "Hyperliquid coverage :current/:total [:bar][:percent] :eta(s) rows=:rows chunk=:chunk",
4925
+ {
4926
+ total: Math.max(1, totalCoverageBuckets),
4927
+ width: 24
4928
+ }
4929
+ );
4930
+ let displayedCoverageBuckets = 0;
4919
4931
  const coverageBuckets = await (0, import_timescale4.rebuildHyperliquidWhaleCoverageRows)({
4920
4932
  fromMs,
4921
4933
  toMs,
4922
4934
  expectedWhales: whales.addresses.length,
4923
- ...identity
4935
+ ...identity,
4936
+ onProgress: (progress) => {
4937
+ const next = Math.max(
4938
+ displayedCoverageBuckets,
4939
+ Math.min(progress.completedBuckets, coverageBar.total)
4940
+ );
4941
+ const delta = next - displayedCoverageBuckets;
4942
+ if (delta > 0) {
4943
+ coverageBar.tick(delta, {
4944
+ rows: progress.rows,
4945
+ chunk: `${progress.chunkIndex}/${progress.totalChunks}`
4946
+ });
4947
+ displayedCoverageBuckets = next;
4948
+ }
4949
+ }
4924
4950
  });
4925
4951
  const result = {
4926
4952
  cached: false,
@@ -6273,10 +6299,27 @@ var resetRunState = () => {
6273
6299
  };
6274
6300
 
6275
6301
  // src/lib/backtest/workerPool.ts
6276
- var import_progress3 = __toESM(require("progress"));
6302
+ var import_progress4 = __toESM(require("progress"));
6277
6303
  var import_child_process = require("child_process");
6278
6304
  var import_chalk8 = __toESM(require("chalk"));
6279
6305
  var import_node_crypto2 = require("crypto");
6306
+ var DEFAULT_PG_POOL_MAX = 10;
6307
+ var FAST_AI_PG_CONNECTION_BUDGET = 18;
6308
+ var MIN_FAST_AI_WORKER_PG_POOL_MAX = 3;
6309
+ var resolveFastAiWorkerPgPoolMax = (testSuite, workerCount, configuredPoolMax) => {
6310
+ if (configuredPoolMax?.trim() || workerCount <= 1 || testSuite.length === 0 || testSuite.some((test) => !test.fast || !test.ai)) {
6311
+ return void 0;
6312
+ }
6313
+ return String(
6314
+ Math.min(
6315
+ DEFAULT_PG_POOL_MAX,
6316
+ Math.max(
6317
+ MIN_FAST_AI_WORKER_PG_POOL_MAX,
6318
+ Math.floor(FAST_AI_PG_CONNECTION_BUDGET / workerCount)
6319
+ )
6320
+ )
6321
+ );
6322
+ };
6280
6323
  var executeBacktestWorkerPool = async ({
6281
6324
  testSuite,
6282
6325
  userName: userName2,
@@ -6295,6 +6338,12 @@ var executeBacktestWorkerPool = async ({
6295
6338
  totalTests
6296
6339
  }) => {
6297
6340
  const chunks = chunkTestSuite(testSuite);
6341
+ const fastAiWorkerPgPoolMax = resolveFastAiWorkerPgPoolMax(
6342
+ testSuite,
6343
+ chunks.length,
6344
+ process.env.PG_POOL_MAX
6345
+ );
6346
+ const workerEnv = fastAiWorkerPgPoolMax ? { ...process.env, PG_POOL_MAX: fastAiWorkerPgPoolMax } : void 0;
6298
6347
  const totalExpectedTests = Math.max(
6299
6348
  testSuite.length,
6300
6349
  totalTests ?? testSuite.length,
@@ -6352,7 +6401,7 @@ var executeBacktestWorkerPool = async ({
6352
6401
  console.log(line);
6353
6402
  }
6354
6403
  console.log("");
6355
- const bar = new import_progress3.default(
6404
+ const bar = new import_progress4.default(
6356
6405
  ":current/:total [:bar][:percent] avg :amount win :winRate trades :trades :eta(s)",
6357
6406
  {
6358
6407
  total: totalExpectedTests,
@@ -6380,6 +6429,7 @@ var executeBacktestWorkerPool = async ({
6380
6429
  const chunkId = runId ? `${runId}-${attemptId}` : attemptId;
6381
6430
  const chunkWithId = chunk.map((test) => ({ ...test, chunkId }));
6382
6431
  const tester = (0, import_child_process.fork)(testerWorkerPath, [], {
6432
+ ...workerEnv ? { env: workerEnv } : {},
6383
6433
  execArgv: testerNeedsTsRuntime ? [
6384
6434
  `--max-old-space-size=${workerHeapMb2}`,
6385
6435
  "-r",
@@ -36,6 +36,7 @@ module.exports = __toCommonJS(hyperliquidWhaleBackfill_exports);
36
36
  var import_args = __toESM(require("args"));
37
37
 
38
38
  // src/lib/hyperliquidWhaleBackfill.ts
39
+ var import_progress = __toESM(require("progress"));
39
40
  var import_timescale = require("@tradejs/infra/timescale");
40
41
  var import_strategies = require("@tradejs/node/strategies");
41
42
 
@@ -410,11 +411,36 @@ var backfillHyperliquidWhaleContext = async (params) => {
410
411
  toMs,
411
412
  ...identity
412
413
  });
414
+ const totalCoverageBuckets = Math.ceil(
415
+ (toMs - fromMs) / HYPERLIQUID_WHALE_BUCKET_MS
416
+ );
417
+ const coverageBar = new import_progress.default(
418
+ "Hyperliquid coverage :current/:total [:bar][:percent] :eta(s) rows=:rows chunk=:chunk",
419
+ {
420
+ total: Math.max(1, totalCoverageBuckets),
421
+ width: 24
422
+ }
423
+ );
424
+ let displayedCoverageBuckets = 0;
413
425
  const coverageBuckets = await (0, import_timescale.rebuildHyperliquidWhaleCoverageRows)({
414
426
  fromMs,
415
427
  toMs,
416
428
  expectedWhales: whales.addresses.length,
417
- ...identity
429
+ ...identity,
430
+ onProgress: (progress) => {
431
+ const next = Math.max(
432
+ displayedCoverageBuckets,
433
+ Math.min(progress.completedBuckets, coverageBar.total)
434
+ );
435
+ const delta = next - displayedCoverageBuckets;
436
+ if (delta > 0) {
437
+ coverageBar.tick(delta, {
438
+ rows: progress.rows,
439
+ chunk: `${progress.chunkIndex}/${progress.totalChunks}`
440
+ });
441
+ displayedCoverageBuckets = next;
442
+ }
443
+ }
418
444
  });
419
445
  const result = {
420
446
  cached: false,
@@ -39,6 +39,7 @@ var import_node_crypto = require("crypto");
39
39
  var import_strategies2 = require("@tradejs/node/strategies");
40
40
 
41
41
  // src/lib/hyperliquidWhaleBackfill.ts
42
+ var import_progress = __toESM(require("progress"));
42
43
  var import_timescale = require("@tradejs/infra/timescale");
43
44
  var import_strategies = require("@tradejs/node/strategies");
44
45
 
@@ -4349,27 +4349,27 @@ var createCoinMarketCapFetchStats = () => ({
4349
4349
  totalCredits: 0,
4350
4350
  byPath: /* @__PURE__ */ new Map()
4351
4351
  });
4352
- var getCoinMarketCapPathLabel = (path3) => {
4353
- if (path3.includes("global-metrics")) return "global";
4354
- if (path3.includes("cryptocurrency/quotes")) return "reference";
4355
- if (path3.includes("exchange/quotes")) return "exchangeLiquidity";
4356
- if (path3.includes("fear-and-greed")) return "fearGreed";
4357
- if (path3.includes("cmc100")) return "cmc100";
4358
- if (path3.includes("cmc20")) return "cmc20";
4359
- return path3;
4360
- };
4361
- var recordCoinMarketCapFetchStats = (stats, path3, creditCount) => {
4352
+ var getCoinMarketCapPathLabel = (path4) => {
4353
+ if (path4.includes("global-metrics")) return "global";
4354
+ if (path4.includes("cryptocurrency/quotes")) return "reference";
4355
+ if (path4.includes("exchange/quotes")) return "exchangeLiquidity";
4356
+ if (path4.includes("fear-and-greed")) return "fearGreed";
4357
+ if (path4.includes("cmc100")) return "cmc100";
4358
+ if (path4.includes("cmc20")) return "cmc20";
4359
+ return path4;
4360
+ };
4361
+ var recordCoinMarketCapFetchStats = (stats, path4, creditCount) => {
4362
4362
  if (!stats) return;
4363
- const existing = stats.byPath.get(path3) ?? { requests: 0, credits: 0 };
4363
+ const existing = stats.byPath.get(path4) ?? { requests: 0, credits: 0 };
4364
4364
  existing.requests += 1;
4365
4365
  existing.credits += creditCount ?? 0;
4366
4366
  stats.requests += 1;
4367
4367
  stats.totalCredits += creditCount ?? 0;
4368
- stats.byPath.set(path3, existing);
4368
+ stats.byPath.set(path4, existing);
4369
4369
  };
4370
4370
  var formatCoinMarketCapFetchStats = (stats) => {
4371
4371
  const byPath = [...stats.byPath.entries()].map(
4372
- ([path3, item]) => `${getCoinMarketCapPathLabel(path3)}=${item.requests}req/${item.credits}cr`
4372
+ ([path4, item]) => `${getCoinMarketCapPathLabel(path4)}=${item.requests}req/${item.credits}cr`
4373
4373
  ).join(" ");
4374
4374
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
4375
4375
  };
@@ -5183,6 +5183,7 @@ var backfillCoinMarketCapContextForReplay = backfillCoinMarketCapContext;
5183
5183
  var backfillCoinMarketCapContextForSignals = (params) => backfillCoinMarketCapContext({ ...params, refreshStaleFearGreed: true });
5184
5184
 
5185
5185
  // src/lib/hyperliquidWhaleBackfill.ts
5186
+ var import_progress3 = __toESM(require("progress"));
5186
5187
  var import_timescale4 = require("@tradejs/infra/timescale");
5187
5188
  var import_strategies2 = require("@tradejs/node/strategies");
5188
5189
 
@@ -5557,11 +5558,36 @@ var backfillHyperliquidWhaleContext = async (params) => {
5557
5558
  toMs,
5558
5559
  ...identity
5559
5560
  });
5561
+ const totalCoverageBuckets = Math.ceil(
5562
+ (toMs - fromMs) / HYPERLIQUID_WHALE_BUCKET_MS
5563
+ );
5564
+ const coverageBar = new import_progress3.default(
5565
+ "Hyperliquid coverage :current/:total [:bar][:percent] :eta(s) rows=:rows chunk=:chunk",
5566
+ {
5567
+ total: Math.max(1, totalCoverageBuckets),
5568
+ width: 24
5569
+ }
5570
+ );
5571
+ let displayedCoverageBuckets = 0;
5560
5572
  const coverageBuckets = await (0, import_timescale4.rebuildHyperliquidWhaleCoverageRows)({
5561
5573
  fromMs,
5562
5574
  toMs,
5563
5575
  expectedWhales: whales.addresses.length,
5564
- ...identity
5576
+ ...identity,
5577
+ onProgress: (progress) => {
5578
+ const next = Math.max(
5579
+ displayedCoverageBuckets,
5580
+ Math.min(progress.completedBuckets, coverageBar.total)
5581
+ );
5582
+ const delta = next - displayedCoverageBuckets;
5583
+ if (delta > 0) {
5584
+ coverageBar.tick(delta, {
5585
+ rows: progress.rows,
5586
+ chunk: `${progress.chunkIndex}/${progress.totalChunks}`
5587
+ });
5588
+ displayedCoverageBuckets = next;
5589
+ }
5590
+ }
5565
5591
  });
5566
5592
  const result = {
5567
5593
  cached: false,
@@ -5923,6 +5949,10 @@ import_args.default.option(["f", "timeframe"], "Timeframe", 15);
5923
5949
  import_args.default.option(["d", "days"], "Run replay only for the last N days");
5924
5950
  import_args.default.option("startTime", "Explicit replay start timestamp (ms or seconds)");
5925
5951
  import_args.default.option("endTime", "Explicit replay end timestamp (ms or seconds)");
5952
+ import_args.default.option(
5953
+ "runtimeEvidence",
5954
+ "Immutable runtime evidence JSON used for offline runtime comparison"
5955
+ );
5926
5956
  import_args.default.option(["u", "updateOnly"], "Only update tickers history", false);
5927
5957
  import_args.default.option(["C", "cacheOnly"], "Do not update tickers history", false);
5928
5958
  import_args.default.option(
@@ -5948,6 +5978,7 @@ var replayFlags = import_args.default.parse(replayNormalizedArgv);
5948
5978
  var replayInterval = String(replayFlags.timeframe ?? 15);
5949
5979
  var replayUserName = String(replayFlags.user ?? "root");
5950
5980
  var replayProjectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
5981
+ var replayRuntimeEvidencePath = typeof replayFlags.runtimeEvidence === "string" && replayFlags.runtimeEvidence.trim() ? replayFlags.runtimeEvidence.trim() : null;
5951
5982
  var isReplayUpdateOnlyRun = Boolean(replayFlags.updateOnly);
5952
5983
 
5953
5984
  // src/lib/replay/support.ts
@@ -6059,7 +6090,7 @@ var compactReplaySignal = (signal) => {
6059
6090
 
6060
6091
  // src/lib/replay/historicalSignalsReplay.ts
6061
6092
  var import_chalk9 = __toESM(require("chalk"));
6062
- var import_progress3 = __toESM(require("progress"));
6093
+ var import_progress4 = __toESM(require("progress"));
6063
6094
  var import_backtest = require("@tradejs/core/backtest");
6064
6095
  var import_strategies4 = require("@tradejs/core/strategies");
6065
6096
  var import_time3 = require("@tradejs/core/time");
@@ -6943,7 +6974,7 @@ var runHistoricalSignalsReplay = async ({
6943
6974
  const sharedReplayKeyPrefixes = [];
6944
6975
  let preparedSymbols = 0;
6945
6976
  let skippedSymbols = 0;
6946
- const prepareBar = new import_progress3.default(
6977
+ const prepareBar = new import_progress4.default(
6947
6978
  "prepare :current/:total [:bar][:percent] skipped=:skipped :etas(s) :symbol",
6948
6979
  {
6949
6980
  total: preparedRun.tickers.length,
@@ -7065,7 +7096,7 @@ var runHistoricalSignalsReplay = async ({
7065
7096
  interval
7066
7097
  });
7067
7098
  let abortedCycles = 0;
7068
- const cycleBar = new import_progress3.default(
7099
+ const cycleBar = new import_progress4.default(
7069
7100
  "cycles :current/:total [:bar][:percent] sig=:signals abort=:aborted :etas(s) :ts",
7070
7101
  {
7071
7102
  total: orderedTimestamps.length,
@@ -8780,6 +8811,114 @@ var buildReplayExchangeComparisonDetails = ({
8780
8811
  };
8781
8812
  };
8782
8813
 
8814
+ // src/lib/replay/runtimeEvidenceSource.ts
8815
+ var import_promises2 = __toESM(require("fs/promises"));
8816
+ var import_node_path2 = __toESM(require("path"));
8817
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
8818
+ var asFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
8819
+ var unwrapRows = (value, field) => (Array.isArray(value) ? value : []).map((item) => {
8820
+ const row = asRecord(item);
8821
+ return asRecord(row?.[field]) ?? row;
8822
+ }).filter((item) => item != null);
8823
+ var buildRuntimeEvidenceLineageScopes = ({
8824
+ signals,
8825
+ evaluations
8826
+ }) => {
8827
+ const scopes = /* @__PURE__ */ new Map();
8828
+ const add = ({
8829
+ strategy,
8830
+ symbol,
8831
+ runtimeConfigId,
8832
+ timestamp,
8833
+ lineage
8834
+ }) => {
8835
+ if (!lineage || !Number.isFinite(timestamp)) {
8836
+ return;
8837
+ }
8838
+ const key = `${strategy}::${symbol}::${runtimeLineageKey(lineage)}`;
8839
+ const existing = scopes.get(key);
8840
+ scopes.set(key, {
8841
+ strategy,
8842
+ symbol,
8843
+ ...runtimeConfigId ? { runtimeConfigId } : {},
8844
+ lineage,
8845
+ firstTimestamp: existing ? Math.min(existing.firstTimestamp, timestamp) : timestamp,
8846
+ lastTimestamp: existing ? Math.max(existing.lastTimestamp, timestamp) : timestamp
8847
+ });
8848
+ };
8849
+ for (const signal of signals) {
8850
+ add({
8851
+ strategy: signal.strategy,
8852
+ symbol: signal.symbol,
8853
+ runtimeConfigId: signal.runtimeConfigId,
8854
+ timestamp: signal.timestamp,
8855
+ lineage: signal.runtimeLineage
8856
+ });
8857
+ }
8858
+ for (const evaluation of evaluations) {
8859
+ add({
8860
+ strategy: evaluation.strategy,
8861
+ symbol: evaluation.symbol,
8862
+ runtimeConfigId: evaluation.runtimeConfigId,
8863
+ timestamp: evaluation.timestamp,
8864
+ lineage: evaluation.runtimeLineage
8865
+ });
8866
+ }
8867
+ return [...scopes.values()].sort(
8868
+ (left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.firstTimestamp - right.firstTimestamp
8869
+ );
8870
+ };
8871
+ var loadReplayRuntimeEvidenceSource = async ({
8872
+ filePath,
8873
+ projectRoot,
8874
+ expectedUserName,
8875
+ expectedWindow
8876
+ }) => {
8877
+ const resolvedPath = import_node_path2.default.resolve(projectRoot, filePath);
8878
+ const root = asRecord(
8879
+ JSON.parse(await import_promises2.default.readFile(resolvedPath, "utf8"))
8880
+ );
8881
+ const runtime = asRecord(root?.runtime) ?? root;
8882
+ if (!root || !runtime) {
8883
+ throw new Error(`Invalid runtime evidence JSON: ${resolvedPath}`);
8884
+ }
8885
+ const rootWindow = asRecord(root.window);
8886
+ const runtimeWindow = asRecord(runtime.window);
8887
+ const startTime = asFiniteNumber(rootWindow?.startTime) ?? asFiniteNumber(runtimeWindow?.startTime);
8888
+ const endTime = asFiniteNumber(rootWindow?.endTime) ?? asFiniteNumber(runtimeWindow?.endTime);
8889
+ const userName = String(root.userName ?? runtime.userName ?? "").trim();
8890
+ if (!userName || userName !== expectedUserName) {
8891
+ throw new Error(
8892
+ `Runtime evidence user mismatch: expected=${expectedUserName}, actual=${userName || "missing"}`
8893
+ );
8894
+ }
8895
+ const endTimeMatches = endTime === expectedWindow.end || endTime === expectedWindow.end + 1;
8896
+ if (startTime !== expectedWindow.start || !endTimeMatches) {
8897
+ throw new Error(
8898
+ `Runtime evidence window mismatch: expected=${expectedWindow.start}..${expectedWindow.end}, actual=${startTime ?? "missing"}..${endTime ?? "missing"}`
8899
+ );
8900
+ }
8901
+ const trades = unwrapRows(runtime.trades, "trade");
8902
+ const signals = unwrapRows(runtime.signals, "signal");
8903
+ const evaluations = unwrapRows(
8904
+ runtime.evaluations,
8905
+ "evaluation"
8906
+ );
8907
+ return {
8908
+ path: resolvedPath,
8909
+ userName,
8910
+ startTime,
8911
+ endTime,
8912
+ trades,
8913
+ signals,
8914
+ evaluations,
8915
+ lineageScopes: buildRuntimeEvidenceLineageScopes({
8916
+ signals,
8917
+ evaluations
8918
+ })
8919
+ };
8920
+ };
8921
+
8783
8922
  // src/lib/replay/runtimeComparison.ts
8784
8923
  var getReplayEntryTimestampCompareOffsetMs = () => {
8785
8924
  const intervalMinutes = Number(replayInterval);
@@ -9549,7 +9688,8 @@ var saveAndPrintReplayRuntimeComparison = async ({
9549
9688
  liveStrategySummaries,
9550
9689
  backtestEntries,
9551
9690
  replaySignals,
9552
- replayLineages
9691
+ replayLineages,
9692
+ runtimeEvidencePath
9553
9693
  }) => {
9554
9694
  const { connector, connectorName, window: window2 } = getRuntimeCompareContext();
9555
9695
  if (!connector || !window2) {
@@ -9559,12 +9699,23 @@ var saveAndPrintReplayRuntimeComparison = async ({
9559
9699
  liveStrategySummaries.map((summary) => summary.strategyName)
9560
9700
  );
9561
9701
  const replaySignalEvaluations = buildReplaySignalEvaluations(replaySignals);
9702
+ const runtimeEvidence = runtimeEvidencePath ? await loadReplayRuntimeEvidenceSource({
9703
+ filePath: runtimeEvidencePath,
9704
+ projectRoot: replayProjectRoot,
9705
+ expectedUserName: replayUserName,
9706
+ expectedWindow: window2
9707
+ }) : null;
9562
9708
  const [
9563
9709
  rawRuntimeTrades,
9564
9710
  runtimeSignals,
9565
9711
  runtimeSignalEvaluations,
9566
9712
  runtimeLineageScopes
9567
- ] = await Promise.all([
9713
+ ] = runtimeEvidence ? [
9714
+ runtimeEvidence.trades,
9715
+ runtimeEvidence.signals,
9716
+ runtimeEvidence.evaluations,
9717
+ runtimeEvidence.lineageScopes
9718
+ ] : await Promise.all([
9568
9719
  loadRuntimeTrades(replayUserName, {
9569
9720
  startTime: window2.start,
9570
9721
  endTime: window2.end
@@ -9582,7 +9733,14 @@ var saveAndPrintReplayRuntimeComparison = async ({
9582
9733
  endTime: window2.end
9583
9734
  })
9584
9735
  ]);
9585
- const syncedRuntimeTrades = await syncRuntimeTrades({
9736
+ if (runtimeEvidence) {
9737
+ console.log(
9738
+ import_chalk11.default.gray(
9739
+ `runtime compare: using immutable evidence ${runtimeEvidence.path}`
9740
+ )
9741
+ );
9742
+ }
9743
+ const syncedRuntimeTrades = runtimeEvidence ? rawRuntimeTrades : await syncRuntimeTrades({
9586
9744
  userName: replayUserName,
9587
9745
  connector,
9588
9746
  trades: rawRuntimeTrades,
@@ -9633,7 +9791,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
9633
9791
  const windowRuntimeTrades = comparable.runtimeTrades.filter(
9634
9792
  (trade) => trade.entryTimestamp >= window2.start && trade.entryTimestamp < window2.end && relevantStrategies.has(trade.strategy)
9635
9793
  );
9636
- if (!windowRuntimeTrades.length) {
9794
+ if (!windowRuntimeTrades.length && !runtimeEvidence) {
9637
9795
  console.log("");
9638
9796
  console.log(
9639
9797
  import_chalk11.default.yellow(
@@ -9765,8 +9923,8 @@ var saveAndPrintReplayRuntimeComparison = async ({
9765
9923
  };
9766
9924
 
9767
9925
  // src/lib/replay/outputReport.ts
9768
- var import_promises2 = __toESM(require("fs/promises"));
9769
- var import_node_path2 = __toESM(require("path"));
9926
+ var import_promises3 = __toESM(require("fs/promises"));
9927
+ var import_node_path3 = __toESM(require("path"));
9770
9928
  var import_data = require("@tradejs/core/data");
9771
9929
  var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
9772
9930
  timeZone: "Europe/Moscow",
@@ -10070,8 +10228,8 @@ var writeReplayOutputReport = async ({
10070
10228
  strategySnapshot,
10071
10229
  runtimeComparison
10072
10230
  }) => {
10073
- const outputDir = import_node_path2.default.resolve(projectRoot, "data/replay/output");
10074
- await import_promises2.default.mkdir(outputDir, { recursive: true });
10231
+ const outputDir = import_node_path3.default.resolve(projectRoot, "data/replay/output");
10232
+ await import_promises3.default.mkdir(outputDir, { recursive: true });
10075
10233
  const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
10076
10234
  const report = {
10077
10235
  reportType: "replay-output",
@@ -10106,8 +10264,8 @@ var writeReplayOutputReport = async ({
10106
10264
  runtimeComparison,
10107
10265
  perTradeAnalysis
10108
10266
  };
10109
- const markdownPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.md`);
10110
- const jsonPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.json`);
10267
+ const markdownPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.md`);
10268
+ const jsonPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.json`);
10111
10269
  const lines = [
10112
10270
  "# Replay Report",
10113
10271
  "",
@@ -10173,8 +10331,8 @@ var writeReplayOutputReport = async ({
10173
10331
  ""
10174
10332
  ];
10175
10333
  await Promise.all([
10176
- import_promises2.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
10177
- import_promises2.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
10334
+ import_promises3.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
10335
+ import_promises3.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
10178
10336
  `, "utf8")
10179
10337
  ]);
10180
10338
  return {
@@ -10215,7 +10373,8 @@ var finishReplay = async ({
10215
10373
  liveStrategySummaries: replayStrategySnapshot.summaries,
10216
10374
  backtestEntries: replayStrategySnapshot.backtestEntries,
10217
10375
  replaySignals: replayResult.signals,
10218
- replayLineages: replayResult.runtimeLineages
10376
+ replayLineages: replayResult.runtimeLineages,
10377
+ runtimeEvidencePath: replayRuntimeEvidencePath
10219
10378
  });
10220
10379
  const finishedAt = /* @__PURE__ */ new Date();
10221
10380
  const durationSeconds = Number(