@tradejs/cli 2.0.19 → 2.0.20

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.
@@ -169,7 +169,8 @@ var extractRuntimeTradeRows = (runtimePayload) => {
169
169
  const signal = asRecord(redisValues?.signal) ?? (signalId ? signalsById.get(signalId) ?? null : null) ?? (shapeKey ? signalsByShape.get(shapeKey) ?? null : null);
170
170
  return {
171
171
  trade,
172
- signal
172
+ signal,
173
+ runtimeLineage: asRecord(trade.runtimeLineage) ?? null
173
174
  };
174
175
  }).filter((row) => row != null);
175
176
  };
@@ -471,7 +472,8 @@ var buildSample = ({
471
472
  direction: direction ?? match?.direction ?? null,
472
473
  expectedPrice: match?.backtestPrice ?? null,
473
474
  actualPrice: match?.runtimePrice ?? null
474
- })
475
+ }),
476
+ runtimeLineage: row.runtimeLineage
475
477
  };
476
478
  };
477
479
  var buildReplayOnlySample = (match) => ({
@@ -518,7 +520,8 @@ var buildReplayOnlySample = (match) => ({
518
520
  direction: match.direction,
519
521
  expectedPrice: match.backtestPrice,
520
522
  actualPrice: match.runtimePrice
521
- })
523
+ }),
524
+ runtimeLineage: null
522
525
  });
523
526
  var hasTelemetry = (sample) => sample.signalClosePrice != null || sample.arrivalMid != null || sample.orderSubmitTime != null || sample.fillAvgPrice != null || sample.fillTime != null || sample.fee != null;
524
527
  var hasFullTelemetry = (sample) => sample.signalClosePrice != null && sample.arrivalMid != null && sample.orderSubmitTime != null && sample.orderAckTime != null && sample.fillAvgPrice != null && sample.fillTime != null;
@@ -2700,27 +2700,27 @@ var createCoinMarketCapFetchStats = () => ({
2700
2700
  totalCredits: 0,
2701
2701
  byPath: /* @__PURE__ */ new Map()
2702
2702
  });
2703
- var getCoinMarketCapPathLabel = (path4) => {
2704
- if (path4.includes("global-metrics")) return "global";
2705
- if (path4.includes("cryptocurrency/quotes")) return "reference";
2706
- if (path4.includes("exchange/quotes")) return "exchangeLiquidity";
2707
- if (path4.includes("fear-and-greed")) return "fearGreed";
2708
- if (path4.includes("cmc100")) return "cmc100";
2709
- if (path4.includes("cmc20")) return "cmc20";
2710
- return path4;
2711
- };
2712
- var recordCoinMarketCapFetchStats = (stats, path4, creditCount) => {
2703
+ var getCoinMarketCapPathLabel = (path5) => {
2704
+ if (path5.includes("global-metrics")) return "global";
2705
+ if (path5.includes("cryptocurrency/quotes")) return "reference";
2706
+ if (path5.includes("exchange/quotes")) return "exchangeLiquidity";
2707
+ if (path5.includes("fear-and-greed")) return "fearGreed";
2708
+ if (path5.includes("cmc100")) return "cmc100";
2709
+ if (path5.includes("cmc20")) return "cmc20";
2710
+ return path5;
2711
+ };
2712
+ var recordCoinMarketCapFetchStats = (stats, path5, creditCount) => {
2713
2713
  if (!stats) return;
2714
- const existing = stats.byPath.get(path4) ?? { requests: 0, credits: 0 };
2714
+ const existing = stats.byPath.get(path5) ?? { requests: 0, credits: 0 };
2715
2715
  existing.requests += 1;
2716
2716
  existing.credits += creditCount ?? 0;
2717
2717
  stats.requests += 1;
2718
2718
  stats.totalCredits += creditCount ?? 0;
2719
- stats.byPath.set(path4, existing);
2719
+ stats.byPath.set(path5, existing);
2720
2720
  };
2721
2721
  var formatCoinMarketCapFetchStats = (stats) => {
2722
2722
  const byPath = [...stats.byPath.entries()].map(
2723
- ([path4, item]) => `${getCoinMarketCapPathLabel(path4)}=${item.requests}req/${item.credits}cr`
2723
+ ([path5, item]) => `${getCoinMarketCapPathLabel(path5)}=${item.requests}req/${item.credits}cr`
2724
2724
  ).join(" ");
2725
2725
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
2726
2726
  };
@@ -4901,11 +4901,56 @@ var invokeAfterSignalsHooks = async (hooks, params) => {
4901
4901
 
4902
4902
  // src/lib/runtimeLineage.ts
4903
4903
  var import_node_child_process = require("child_process");
4904
+ var import_node_crypto2 = require("crypto");
4905
+ var import_promises2 = __toESM(require("fs/promises"));
4906
+ var import_node_path2 = __toESM(require("path"));
4907
+ var import_types2 = require("@tradejs/types");
4908
+ var import_strategies3 = require("@tradejs/node/strategies");
4909
+
4910
+ // src/lib/strategyGateFingerprint.ts
4904
4911
  var import_node_crypto = require("crypto");
4905
4912
  var import_promises = __toESM(require("fs/promises"));
4906
4913
  var import_node_path = __toESM(require("path"));
4907
- var import_types2 = require("@tradejs/types");
4908
- var import_strategies3 = require("@tradejs/node/strategies");
4914
+ var gateFingerprintRelativePaths = (strategyName) => [
4915
+ `packages/strategies/src/${strategyName}/adapters/ai.ts`,
4916
+ `packages/strategies/src/${strategyName}/guardrails.ts`,
4917
+ `packages/strategies/src/${strategyName}/pockets.ts`,
4918
+ `packages/strategies/src/${strategyName}/config.ts`,
4919
+ "packages/strategies/src/shared/localAiGate.ts",
4920
+ "packages/node/src/ai.ts",
4921
+ "packages/core/src/utils/strategyHelpers/signalBuilders.ts"
4922
+ ];
4923
+ var resolveStrategyGateFingerprint = async ({
4924
+ projectRoot,
4925
+ strategyName,
4926
+ gitSha
4927
+ }) => {
4928
+ const candidates = await Promise.all(
4929
+ gateFingerprintRelativePaths(strategyName).map(async (relativePath) => {
4930
+ const content = await import_promises.default.readFile(import_node_path.default.join(projectRoot, relativePath)).catch(() => null);
4931
+ return content == null ? null : { relativePath, content };
4932
+ })
4933
+ );
4934
+ const sourceEntries = candidates.filter(
4935
+ (entry) => entry != null
4936
+ );
4937
+ const hash = (0, import_node_crypto.createHash)("sha256");
4938
+ hash.update(strategyName);
4939
+ if (sourceEntries.length) {
4940
+ for (const entry of sourceEntries) {
4941
+ hash.update(entry.relativePath);
4942
+ hash.update(entry.content);
4943
+ }
4944
+ } else {
4945
+ hash.update(gitSha ?? "unknown-git-sha");
4946
+ }
4947
+ return {
4948
+ gateFingerprint: hash.digest("hex").slice(0, 16),
4949
+ gateFingerprintFiles: sourceEntries.map((entry) => entry.relativePath)
4950
+ };
4951
+ };
4952
+
4953
+ // src/lib/runtimeLineage.ts
4909
4954
  var RUNTIME_CONTEXT_ENV_KEYS = [
4910
4955
  "AI_MODE",
4911
4956
  "MIN_AI_QUALITY",
@@ -4955,6 +5000,17 @@ var normalizeForStableJson = (value) => {
4955
5000
  }
4956
5001
  return value;
4957
5002
  };
5003
+ var normalizeRuntimeLogicConfig = (value) => {
5004
+ if (Array.isArray(value)) {
5005
+ return value.map(normalizeRuntimeLogicConfig);
5006
+ }
5007
+ if (value && typeof value === "object") {
5008
+ return Object.fromEntries(
5009
+ Object.entries(value).filter(([key]) => key !== "MAX_LOSS_VALUE").sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeRuntimeLogicConfig(entry)])
5010
+ );
5011
+ }
5012
+ return value;
5013
+ };
4958
5014
  var resolveRuntimeMaxLossValue = (config) => {
4959
5015
  if (!config || typeof config !== "object" || Array.isArray(config)) {
4960
5016
  return null;
@@ -4968,7 +5024,7 @@ var resolveRuntimeMaxLossValue = (config) => {
4968
5024
  );
4969
5025
  return Number.isFinite(maxLossValue) ? maxLossValue : null;
4970
5026
  };
4971
- var fingerprintRuntimeValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
5027
+ var fingerprintRuntimeValue = (value) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
4972
5028
  var gitLineageCache = /* @__PURE__ */ new Map();
4973
5029
  var gateFingerprintCache = /* @__PURE__ */ new Map();
4974
5030
  var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
@@ -5019,7 +5075,7 @@ var resolveGitLineage = (projectRoot) => {
5019
5075
  };
5020
5076
  var readOptionalFile = async (filePath) => {
5021
5077
  try {
5022
- return await import_promises.default.readFile(filePath);
5078
+ return await import_promises2.default.readFile(filePath);
5023
5079
  } catch {
5024
5080
  return null;
5025
5081
  }
@@ -5029,7 +5085,7 @@ var resolveBinanceBreadthFingerprint = async (projectRoot) => {
5029
5085
  return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
5030
5086
  }
5031
5087
  const content = await readOptionalFile(
5032
- import_node_path.default.join(
5088
+ import_node_path2.default.join(
5033
5089
  projectRoot,
5034
5090
  "packages/node/src/config/binanceBreadthUniverses.json"
5035
5091
  )
@@ -5051,7 +5107,7 @@ var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
5051
5107
  if (snapshotFingerprintCache.has(cacheKey)) {
5052
5108
  return snapshotFingerprintCache.get(cacheKey) ?? null;
5053
5109
  }
5054
- const content = await readOptionalFile(import_node_path.default.join(projectRoot, relativePath));
5110
+ const content = await readOptionalFile(import_node_path2.default.join(projectRoot, relativePath));
5055
5111
  let result = null;
5056
5112
  if (content) {
5057
5113
  try {
@@ -5064,55 +5120,26 @@ var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
5064
5120
  snapshotFingerprintCache.set(cacheKey, result);
5065
5121
  return result;
5066
5122
  };
5067
- var resolveGateFingerprint = async ({
5123
+ var resolveRuntimeGateFingerprint = async ({
5068
5124
  projectRoot,
5069
5125
  strategyName,
5070
5126
  gitSha
5071
5127
  }) => {
5072
5128
  const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
5073
5129
  const cached = gateFingerprintCache.get(cacheKey);
5074
- if (cached) {
5075
- return cached;
5076
- }
5077
- const relativeCandidates = [
5078
- `packages/strategies/src/${strategyName}/adapters/ai.ts`,
5079
- `packages/strategies/src/${strategyName}/guardrails.ts`,
5080
- `packages/strategies/src/${strategyName}/pockets.ts`,
5081
- `packages/strategies/src/${strategyName}/config.ts`,
5082
- "packages/node/src/ai.ts",
5083
- "packages/core/src/utils/strategyHelpers/signalBuilders.ts"
5084
- ];
5085
- const optionalEntries = await Promise.all(
5086
- relativeCandidates.map(async (relativePath) => {
5087
- const content = await readOptionalFile(
5088
- import_node_path.default.join(projectRoot, relativePath)
5089
- );
5090
- return content == null ? null : { relativePath, content };
5091
- })
5092
- );
5093
- const sourceEntries = [];
5094
- for (const entry of optionalEntries) {
5095
- if (entry != null) {
5096
- sourceEntries.push(entry);
5097
- }
5098
- }
5099
- const hash = (0, import_node_crypto.createHash)("sha256");
5100
- hash.update(strategyName);
5101
- if (sourceEntries.length) {
5102
- for (const entry of sourceEntries) {
5103
- hash.update(entry.relativePath);
5104
- hash.update(entry.content);
5105
- }
5106
- } else {
5107
- hash.update(gitSha ?? "unknown-git-sha");
5108
- }
5109
- const fingerprint = hash.digest("hex").slice(0, 16);
5110
- gateFingerprintCache.set(cacheKey, fingerprint);
5111
- return fingerprint;
5130
+ if (cached) return cached;
5131
+ const result = await resolveStrategyGateFingerprint({
5132
+ projectRoot,
5133
+ strategyName,
5134
+ gitSha
5135
+ });
5136
+ gateFingerprintCache.set(cacheKey, result);
5137
+ return result;
5112
5138
  };
5113
5139
  var buildRuntimeLineage = async ({
5114
5140
  projectRoot,
5115
5141
  strategyName,
5142
+ compositionId,
5116
5143
  config,
5117
5144
  runContext,
5118
5145
  env = process.env
@@ -5138,19 +5165,25 @@ var buildRuntimeLineage = async ({
5138
5165
  };
5139
5166
  return {
5140
5167
  schemaVersion: 1,
5168
+ compositionId: compositionId?.trim() || null,
5141
5169
  ...git,
5142
- gateFingerprint: await resolveGateFingerprint({
5170
+ gateFingerprint: (await resolveRuntimeGateFingerprint({
5143
5171
  projectRoot,
5144
5172
  strategyName,
5145
5173
  gitSha: git.gitSha
5146
- }),
5147
- configFingerprint: fingerprintRuntimeValue(config),
5174
+ })).gateFingerprint,
5175
+ // Position risk is tracked separately so changing trade size does not
5176
+ // masquerade as a change to the core + gate decision logic.
5177
+ configFingerprint: fingerprintRuntimeValue(
5178
+ normalizeRuntimeLogicConfig(config)
5179
+ ),
5148
5180
  contextFingerprint: fingerprintRuntimeValue(context),
5149
5181
  maxLossValue: resolveRuntimeMaxLossValue(config)
5150
5182
  };
5151
5183
  };
5152
5184
  var runtimeLineageKey = (lineage) => [
5153
5185
  lineage.schemaVersion,
5186
+ lineage.compositionId ?? "unbound-composition",
5154
5187
  lineage.gitSha ?? "unknown",
5155
5188
  lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
5156
5189
  lineage.gateFingerprint,
@@ -7163,8 +7196,8 @@ var buildReplayExchangeComparisonDetails = ({
7163
7196
  };
7164
7197
 
7165
7198
  // src/lib/replay/runtimeEvidenceSource.ts
7166
- var import_promises2 = __toESM(require("fs/promises"));
7167
- var import_node_path2 = __toESM(require("path"));
7199
+ var import_promises3 = __toESM(require("fs/promises"));
7200
+ var import_node_path3 = __toESM(require("path"));
7168
7201
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
7169
7202
  var asFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
7170
7203
  var unwrapRows = (value, field) => (Array.isArray(value) ? value : []).map((item) => {
@@ -7225,9 +7258,9 @@ var loadReplayRuntimeEvidenceSource = async ({
7225
7258
  expectedUserName,
7226
7259
  expectedWindow
7227
7260
  }) => {
7228
- const resolvedPath = import_node_path2.default.resolve(projectRoot, filePath);
7261
+ const resolvedPath = import_node_path3.default.resolve(projectRoot, filePath);
7229
7262
  const root = asRecord(
7230
- JSON.parse(await import_promises2.default.readFile(resolvedPath, "utf8"))
7263
+ JSON.parse(await import_promises3.default.readFile(resolvedPath, "utf8"))
7231
7264
  );
7232
7265
  const runtime = asRecord(root?.runtime) ?? root;
7233
7266
  if (!root || !runtime) {
@@ -8274,8 +8307,8 @@ var saveAndPrintReplayRuntimeComparison = async ({
8274
8307
  };
8275
8308
 
8276
8309
  // src/lib/replay/outputReport.ts
8277
- var import_promises3 = __toESM(require("fs/promises"));
8278
- var import_node_path3 = __toESM(require("path"));
8310
+ var import_promises4 = __toESM(require("fs/promises"));
8311
+ var import_node_path4 = __toESM(require("path"));
8279
8312
  var import_data = require("@tradejs/core/data");
8280
8313
  var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
8281
8314
  timeZone: "Europe/Moscow",
@@ -8579,8 +8612,8 @@ var writeReplayOutputReport = async ({
8579
8612
  strategySnapshot,
8580
8613
  runtimeComparison
8581
8614
  }) => {
8582
- const outputDir = import_node_path3.default.resolve(projectRoot, "data/replay/output");
8583
- await import_promises3.default.mkdir(outputDir, { recursive: true });
8615
+ const outputDir = import_node_path4.default.resolve(projectRoot, "data/replay/output");
8616
+ await import_promises4.default.mkdir(outputDir, { recursive: true });
8584
8617
  const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
8585
8618
  const report = {
8586
8619
  reportType: "replay-output",
@@ -8615,8 +8648,8 @@ var writeReplayOutputReport = async ({
8615
8648
  runtimeComparison,
8616
8649
  perTradeAnalysis
8617
8650
  };
8618
- const markdownPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.md`);
8619
- const jsonPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.json`);
8651
+ const markdownPath = import_node_path4.default.join(outputDir, `${timestamp}-replay-report.md`);
8652
+ const jsonPath = import_node_path4.default.join(outputDir, `${timestamp}-replay-report.json`);
8620
8653
  const lines = [
8621
8654
  "# Replay Report",
8622
8655
  "",
@@ -8682,8 +8715,8 @@ var writeReplayOutputReport = async ({
8682
8715
  ""
8683
8716
  ];
8684
8717
  await Promise.all([
8685
- import_promises3.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
8686
- import_promises3.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
8718
+ import_promises4.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
8719
+ import_promises4.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
8687
8720
  `, "utf8")
8688
8721
  ]);
8689
8722
  return {
@@ -2703,27 +2703,27 @@ var createCoinMarketCapFetchStats = () => ({
2703
2703
  totalCredits: 0,
2704
2704
  byPath: /* @__PURE__ */ new Map()
2705
2705
  });
2706
- var getCoinMarketCapPathLabel = (path4) => {
2707
- if (path4.includes("global-metrics")) return "global";
2708
- if (path4.includes("cryptocurrency/quotes")) return "reference";
2709
- if (path4.includes("exchange/quotes")) return "exchangeLiquidity";
2710
- if (path4.includes("fear-and-greed")) return "fearGreed";
2711
- if (path4.includes("cmc100")) return "cmc100";
2712
- if (path4.includes("cmc20")) return "cmc20";
2713
- return path4;
2714
- };
2715
- var recordCoinMarketCapFetchStats = (stats, path4, creditCount) => {
2706
+ var getCoinMarketCapPathLabel = (path5) => {
2707
+ if (path5.includes("global-metrics")) return "global";
2708
+ if (path5.includes("cryptocurrency/quotes")) return "reference";
2709
+ if (path5.includes("exchange/quotes")) return "exchangeLiquidity";
2710
+ if (path5.includes("fear-and-greed")) return "fearGreed";
2711
+ if (path5.includes("cmc100")) return "cmc100";
2712
+ if (path5.includes("cmc20")) return "cmc20";
2713
+ return path5;
2714
+ };
2715
+ var recordCoinMarketCapFetchStats = (stats, path5, creditCount) => {
2716
2716
  if (!stats) return;
2717
- const existing = stats.byPath.get(path4) ?? { requests: 0, credits: 0 };
2717
+ const existing = stats.byPath.get(path5) ?? { requests: 0, credits: 0 };
2718
2718
  existing.requests += 1;
2719
2719
  existing.credits += creditCount ?? 0;
2720
2720
  stats.requests += 1;
2721
2721
  stats.totalCredits += creditCount ?? 0;
2722
- stats.byPath.set(path4, existing);
2722
+ stats.byPath.set(path5, existing);
2723
2723
  };
2724
2724
  var formatCoinMarketCapFetchStats = (stats) => {
2725
2725
  const byPath = [...stats.byPath.entries()].map(
2726
- ([path4, item]) => `${getCoinMarketCapPathLabel(path4)}=${item.requests}req/${item.credits}cr`
2726
+ ([path5, item]) => `${getCoinMarketCapPathLabel(path5)}=${item.requests}req/${item.credits}cr`
2727
2727
  ).join(" ");
2728
2728
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
2729
2729
  };
@@ -4904,11 +4904,56 @@ var invokeAfterSignalsHooks = async (hooks, params) => {
4904
4904
 
4905
4905
  // src/lib/runtimeLineage.ts
4906
4906
  var import_node_child_process = require("child_process");
4907
+ var import_node_crypto2 = require("crypto");
4908
+ var import_promises2 = __toESM(require("fs/promises"));
4909
+ var import_node_path2 = __toESM(require("path"));
4910
+ var import_types2 = require("@tradejs/types");
4911
+ var import_strategies3 = require("@tradejs/node/strategies");
4912
+
4913
+ // src/lib/strategyGateFingerprint.ts
4907
4914
  var import_node_crypto = require("crypto");
4908
4915
  var import_promises = __toESM(require("fs/promises"));
4909
4916
  var import_node_path = __toESM(require("path"));
4910
- var import_types2 = require("@tradejs/types");
4911
- var import_strategies3 = require("@tradejs/node/strategies");
4917
+ var gateFingerprintRelativePaths = (strategyName) => [
4918
+ `packages/strategies/src/${strategyName}/adapters/ai.ts`,
4919
+ `packages/strategies/src/${strategyName}/guardrails.ts`,
4920
+ `packages/strategies/src/${strategyName}/pockets.ts`,
4921
+ `packages/strategies/src/${strategyName}/config.ts`,
4922
+ "packages/strategies/src/shared/localAiGate.ts",
4923
+ "packages/node/src/ai.ts",
4924
+ "packages/core/src/utils/strategyHelpers/signalBuilders.ts"
4925
+ ];
4926
+ var resolveStrategyGateFingerprint = async ({
4927
+ projectRoot,
4928
+ strategyName,
4929
+ gitSha
4930
+ }) => {
4931
+ const candidates = await Promise.all(
4932
+ gateFingerprintRelativePaths(strategyName).map(async (relativePath) => {
4933
+ const content = await import_promises.default.readFile(import_node_path.default.join(projectRoot, relativePath)).catch(() => null);
4934
+ return content == null ? null : { relativePath, content };
4935
+ })
4936
+ );
4937
+ const sourceEntries = candidates.filter(
4938
+ (entry) => entry != null
4939
+ );
4940
+ const hash = (0, import_node_crypto.createHash)("sha256");
4941
+ hash.update(strategyName);
4942
+ if (sourceEntries.length) {
4943
+ for (const entry of sourceEntries) {
4944
+ hash.update(entry.relativePath);
4945
+ hash.update(entry.content);
4946
+ }
4947
+ } else {
4948
+ hash.update(gitSha ?? "unknown-git-sha");
4949
+ }
4950
+ return {
4951
+ gateFingerprint: hash.digest("hex").slice(0, 16),
4952
+ gateFingerprintFiles: sourceEntries.map((entry) => entry.relativePath)
4953
+ };
4954
+ };
4955
+
4956
+ // src/lib/runtimeLineage.ts
4912
4957
  var RUNTIME_CONTEXT_ENV_KEYS = [
4913
4958
  "AI_MODE",
4914
4959
  "MIN_AI_QUALITY",
@@ -4958,6 +5003,17 @@ var normalizeForStableJson = (value) => {
4958
5003
  }
4959
5004
  return value;
4960
5005
  };
5006
+ var normalizeRuntimeLogicConfig = (value) => {
5007
+ if (Array.isArray(value)) {
5008
+ return value.map(normalizeRuntimeLogicConfig);
5009
+ }
5010
+ if (value && typeof value === "object") {
5011
+ return Object.fromEntries(
5012
+ Object.entries(value).filter(([key]) => key !== "MAX_LOSS_VALUE").sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeRuntimeLogicConfig(entry)])
5013
+ );
5014
+ }
5015
+ return value;
5016
+ };
4961
5017
  var resolveRuntimeMaxLossValue = (config) => {
4962
5018
  if (!config || typeof config !== "object" || Array.isArray(config)) {
4963
5019
  return null;
@@ -4971,7 +5027,7 @@ var resolveRuntimeMaxLossValue = (config) => {
4971
5027
  );
4972
5028
  return Number.isFinite(maxLossValue) ? maxLossValue : null;
4973
5029
  };
4974
- var fingerprintRuntimeValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
5030
+ var fingerprintRuntimeValue = (value) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
4975
5031
  var gitLineageCache = /* @__PURE__ */ new Map();
4976
5032
  var gateFingerprintCache = /* @__PURE__ */ new Map();
4977
5033
  var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
@@ -5022,7 +5078,7 @@ var resolveGitLineage = (projectRoot) => {
5022
5078
  };
5023
5079
  var readOptionalFile = async (filePath) => {
5024
5080
  try {
5025
- return await import_promises.default.readFile(filePath);
5081
+ return await import_promises2.default.readFile(filePath);
5026
5082
  } catch {
5027
5083
  return null;
5028
5084
  }
@@ -5032,7 +5088,7 @@ var resolveBinanceBreadthFingerprint = async (projectRoot) => {
5032
5088
  return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
5033
5089
  }
5034
5090
  const content = await readOptionalFile(
5035
- import_node_path.default.join(
5091
+ import_node_path2.default.join(
5036
5092
  projectRoot,
5037
5093
  "packages/node/src/config/binanceBreadthUniverses.json"
5038
5094
  )
@@ -5054,7 +5110,7 @@ var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
5054
5110
  if (snapshotFingerprintCache.has(cacheKey)) {
5055
5111
  return snapshotFingerprintCache.get(cacheKey) ?? null;
5056
5112
  }
5057
- const content = await readOptionalFile(import_node_path.default.join(projectRoot, relativePath));
5113
+ const content = await readOptionalFile(import_node_path2.default.join(projectRoot, relativePath));
5058
5114
  let result = null;
5059
5115
  if (content) {
5060
5116
  try {
@@ -5067,55 +5123,26 @@ var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
5067
5123
  snapshotFingerprintCache.set(cacheKey, result);
5068
5124
  return result;
5069
5125
  };
5070
- var resolveGateFingerprint = async ({
5126
+ var resolveRuntimeGateFingerprint = async ({
5071
5127
  projectRoot,
5072
5128
  strategyName,
5073
5129
  gitSha
5074
5130
  }) => {
5075
5131
  const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
5076
5132
  const cached = gateFingerprintCache.get(cacheKey);
5077
- if (cached) {
5078
- return cached;
5079
- }
5080
- const relativeCandidates = [
5081
- `packages/strategies/src/${strategyName}/adapters/ai.ts`,
5082
- `packages/strategies/src/${strategyName}/guardrails.ts`,
5083
- `packages/strategies/src/${strategyName}/pockets.ts`,
5084
- `packages/strategies/src/${strategyName}/config.ts`,
5085
- "packages/node/src/ai.ts",
5086
- "packages/core/src/utils/strategyHelpers/signalBuilders.ts"
5087
- ];
5088
- const optionalEntries = await Promise.all(
5089
- relativeCandidates.map(async (relativePath) => {
5090
- const content = await readOptionalFile(
5091
- import_node_path.default.join(projectRoot, relativePath)
5092
- );
5093
- return content == null ? null : { relativePath, content };
5094
- })
5095
- );
5096
- const sourceEntries = [];
5097
- for (const entry of optionalEntries) {
5098
- if (entry != null) {
5099
- sourceEntries.push(entry);
5100
- }
5101
- }
5102
- const hash = (0, import_node_crypto.createHash)("sha256");
5103
- hash.update(strategyName);
5104
- if (sourceEntries.length) {
5105
- for (const entry of sourceEntries) {
5106
- hash.update(entry.relativePath);
5107
- hash.update(entry.content);
5108
- }
5109
- } else {
5110
- hash.update(gitSha ?? "unknown-git-sha");
5111
- }
5112
- const fingerprint = hash.digest("hex").slice(0, 16);
5113
- gateFingerprintCache.set(cacheKey, fingerprint);
5114
- return fingerprint;
5133
+ if (cached) return cached;
5134
+ const result = await resolveStrategyGateFingerprint({
5135
+ projectRoot,
5136
+ strategyName,
5137
+ gitSha
5138
+ });
5139
+ gateFingerprintCache.set(cacheKey, result);
5140
+ return result;
5115
5141
  };
5116
5142
  var buildRuntimeLineage = async ({
5117
5143
  projectRoot,
5118
5144
  strategyName,
5145
+ compositionId,
5119
5146
  config,
5120
5147
  runContext,
5121
5148
  env = process.env
@@ -5141,19 +5168,25 @@ var buildRuntimeLineage = async ({
5141
5168
  };
5142
5169
  return {
5143
5170
  schemaVersion: 1,
5171
+ compositionId: compositionId?.trim() || null,
5144
5172
  ...git,
5145
- gateFingerprint: await resolveGateFingerprint({
5173
+ gateFingerprint: (await resolveRuntimeGateFingerprint({
5146
5174
  projectRoot,
5147
5175
  strategyName,
5148
5176
  gitSha: git.gitSha
5149
- }),
5150
- configFingerprint: fingerprintRuntimeValue(config),
5177
+ })).gateFingerprint,
5178
+ // Position risk is tracked separately so changing trade size does not
5179
+ // masquerade as a change to the core + gate decision logic.
5180
+ configFingerprint: fingerprintRuntimeValue(
5181
+ normalizeRuntimeLogicConfig(config)
5182
+ ),
5151
5183
  contextFingerprint: fingerprintRuntimeValue(context),
5152
5184
  maxLossValue: resolveRuntimeMaxLossValue(config)
5153
5185
  };
5154
5186
  };
5155
5187
  var runtimeLineageKey = (lineage) => [
5156
5188
  lineage.schemaVersion,
5189
+ lineage.compositionId ?? "unbound-composition",
5157
5190
  lineage.gitSha ?? "unknown",
5158
5191
  lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
5159
5192
  lineage.gateFingerprint,
@@ -7166,8 +7199,8 @@ var buildReplayExchangeComparisonDetails = ({
7166
7199
  };
7167
7200
 
7168
7201
  // src/lib/replay/runtimeEvidenceSource.ts
7169
- var import_promises2 = __toESM(require("fs/promises"));
7170
- var import_node_path2 = __toESM(require("path"));
7202
+ var import_promises3 = __toESM(require("fs/promises"));
7203
+ var import_node_path3 = __toESM(require("path"));
7171
7204
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
7172
7205
  var asFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
7173
7206
  var unwrapRows = (value, field) => (Array.isArray(value) ? value : []).map((item) => {
@@ -7228,9 +7261,9 @@ var loadReplayRuntimeEvidenceSource = async ({
7228
7261
  expectedUserName,
7229
7262
  expectedWindow
7230
7263
  }) => {
7231
- const resolvedPath = import_node_path2.default.resolve(projectRoot, filePath);
7264
+ const resolvedPath = import_node_path3.default.resolve(projectRoot, filePath);
7232
7265
  const root = asRecord(
7233
- JSON.parse(await import_promises2.default.readFile(resolvedPath, "utf8"))
7266
+ JSON.parse(await import_promises3.default.readFile(resolvedPath, "utf8"))
7234
7267
  );
7235
7268
  const runtime = asRecord(root?.runtime) ?? root;
7236
7269
  if (!root || !runtime) {
@@ -8277,8 +8310,8 @@ var saveAndPrintReplayRuntimeComparison = async ({
8277
8310
  };
8278
8311
 
8279
8312
  // src/lib/replay/outputReport.ts
8280
- var import_promises3 = __toESM(require("fs/promises"));
8281
- var import_node_path3 = __toESM(require("path"));
8313
+ var import_promises4 = __toESM(require("fs/promises"));
8314
+ var import_node_path4 = __toESM(require("path"));
8282
8315
  var import_data = require("@tradejs/core/data");
8283
8316
  var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
8284
8317
  timeZone: "Europe/Moscow",
@@ -8582,8 +8615,8 @@ var writeReplayOutputReport = async ({
8582
8615
  strategySnapshot,
8583
8616
  runtimeComparison
8584
8617
  }) => {
8585
- const outputDir = import_node_path3.default.resolve(projectRoot, "data/replay/output");
8586
- await import_promises3.default.mkdir(outputDir, { recursive: true });
8618
+ const outputDir = import_node_path4.default.resolve(projectRoot, "data/replay/output");
8619
+ await import_promises4.default.mkdir(outputDir, { recursive: true });
8587
8620
  const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
8588
8621
  const report = {
8589
8622
  reportType: "replay-output",
@@ -8618,8 +8651,8 @@ var writeReplayOutputReport = async ({
8618
8651
  runtimeComparison,
8619
8652
  perTradeAnalysis
8620
8653
  };
8621
- const markdownPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.md`);
8622
- const jsonPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.json`);
8654
+ const markdownPath = import_node_path4.default.join(outputDir, `${timestamp}-replay-report.md`);
8655
+ const jsonPath = import_node_path4.default.join(outputDir, `${timestamp}-replay-report.json`);
8623
8656
  const lines = [
8624
8657
  "# Replay Report",
8625
8658
  "",
@@ -8685,8 +8718,8 @@ var writeReplayOutputReport = async ({
8685
8718
  ""
8686
8719
  ];
8687
8720
  await Promise.all([
8688
- import_promises3.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
8689
- import_promises3.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
8721
+ import_promises4.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
8722
+ import_promises4.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
8690
8723
  `, "utf8")
8691
8724
  ]);
8692
8725
  return {