@tradejs/cli 2.0.20 → 3.0.0

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.
@@ -43,6 +43,7 @@ var import_ai = require("@tradejs/infra/ai");
43
43
  var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
44
44
  var import_redis = require("@tradejs/infra/redis");
45
45
  var import_ai2 = require("@tradejs/node/ai");
46
+ var import_registry = require("@tradejs/node/registry");
46
47
  var import_constants2 = require("@tradejs/node/constants");
47
48
 
48
49
  // src/lib/aiTrainMetrics.ts
@@ -920,6 +921,38 @@ var parseTerminalWindowDays = (value) => {
920
921
  const raw = String(value ?? "90,30,7").split(",").map((part) => Number(part.trim())).filter((part) => Number.isFinite(part)).map((part) => Math.trunc(part)).filter((part) => part > 0);
921
922
  return [...new Set(raw)].sort((left, right) => right - left);
922
923
  };
924
+ var parseAiTrainDirectionPolicy = (value) => {
925
+ const normalized = String(value ?? "both").trim().toLowerCase().replaceAll("-", "_");
926
+ if (normalized === "both" || normalized === "long_only" || normalized === "short_only" || normalized === "direction_aware") {
927
+ return normalized;
928
+ }
929
+ throw new Error(
930
+ `Invalid --directionPolicy value "${String(value)}". Use both, long_only, short_only, or direction_aware.`
931
+ );
932
+ };
933
+ var assertAiTrainDirectionPolicy = ({
934
+ directionPolicy,
935
+ byDirection,
936
+ terminalWindows
937
+ }) => {
938
+ const suppressedDirection = directionPolicy === "long_only" ? "SHORT" : directionPolicy === "short_only" ? "LONG" : null;
939
+ if (!suppressedDirection) return;
940
+ for (const scope of [{ label: "full", byDirection }, ...terminalWindows]) {
941
+ const summary = scope.byDirection.find(
942
+ (entry) => entry.direction === suppressedDirection
943
+ );
944
+ if (!summary) {
945
+ throw new Error(
946
+ `${scope.label} ${suppressedDirection} direction summary is required for ${directionPolicy}`
947
+ );
948
+ }
949
+ if (summary.summary.approved !== 0) {
950
+ throw new Error(
951
+ `${scope.label} has ${summary.summary.approved} ${suppressedDirection} approved rows under ${directionPolicy}`
952
+ );
953
+ }
954
+ }
955
+ };
923
956
  var parseDumpFeatureMode = (value) => {
924
957
  const text = String(value ?? "none").trim().toLowerCase();
925
958
  if (!text || text === "none" || text === "false" || text === "0") {
@@ -1416,6 +1449,11 @@ import_args.default.option(
1416
1449
  "Minimum AI quality required to approve entry",
1417
1450
  4
1418
1451
  );
1452
+ import_args.default.option(
1453
+ ["Y", "directionPolicy"],
1454
+ "Evidence label for the enforced gate policy: both, long_only, short_only, or direction_aware",
1455
+ "both"
1456
+ );
1419
1457
  import_args.default.option(
1420
1458
  ["l", "localOnly"],
1421
1459
  "Replay deterministic adapter gate without AI provider calls",
@@ -1806,6 +1844,7 @@ var main = async () => {
1806
1844
  });
1807
1845
  const qualityThresholds = parseQualityThresholds(flags.qualityThresholds);
1808
1846
  const terminalWindowDays = parseTerminalWindowDays(flags.terminalWindows);
1847
+ const directionPolicy = parseAiTrainDirectionPolicy(flags.directionPolicy);
1809
1848
  const dumpEvaluationsPath = String(flags.dumpEvaluations || "").trim();
1810
1849
  const dumpFeatureMode = parseDumpFeatureMode(flags.dumpFeatures);
1811
1850
  if (dumpFeatureMode !== "none" && !dumpEvaluationsPath) {
@@ -1827,7 +1866,7 @@ var main = async () => {
1827
1866
  const userName = String(flags.user || "root").trim() || "root";
1828
1867
  const model = String(flags.model || import_ai2.DEFAULT_AI_MODEL).trim() || import_ai2.DEFAULT_AI_MODEL;
1829
1868
  const parallel = normalizePositiveInt(flags.parallel, import_constants2.AI_CONCURRENCY_LIMIT);
1830
- await (0, import_ai2.ensureAiStrategyPluginsLoaded)();
1869
+ await (0, import_registry.ensureStrategyPluginsLoaded)();
1831
1870
  const filePaths = await resolveDatasetFiles();
1832
1871
  const datasetId = getMergedGroupId(filePaths[0] || "")?.mergeId;
1833
1872
  const { totalRows, selectedRows } = await (0, import_ai.countAiDatasetRows)({
@@ -2020,18 +2059,29 @@ var main = async () => {
2020
2059
  finalEvaluations,
2021
2060
  terminalWindowDays
2022
2061
  );
2023
- const topRejectReasons = summarizeAiTrainRejectReasons(finalEvaluations);
2024
- const lineage = await buildAiTrainLineage({
2025
- projectRoot: String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd(),
2026
- strategyName,
2027
- configIds: finalEvaluations.map((evaluation) => evaluation.configId),
2028
- sourceSha256s: await Promise.all(filePaths.map(import_strategyReleaseEvidence.strategyEvidenceFileSha256)),
2029
- runContext: {
2030
- mode: localOnly ? "local-deterministic" : "llm",
2031
- model: localOnly ? "local-deterministic" : model,
2032
- minQuality
2033
- }
2062
+ assertAiTrainDirectionPolicy({
2063
+ directionPolicy,
2064
+ byDirection: directionSummaries,
2065
+ terminalWindows
2034
2066
  });
2067
+ const topRejectReasons = summarizeAiTrainRejectReasons(finalEvaluations);
2068
+ const lineage = {
2069
+ ...await buildAiTrainLineage({
2070
+ projectRoot: String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd(),
2071
+ strategyName,
2072
+ configIds: finalEvaluations.map((evaluation) => evaluation.configId),
2073
+ sourceSha256s: await Promise.all(
2074
+ filePaths.map(import_strategyReleaseEvidence.strategyEvidenceFileSha256)
2075
+ ),
2076
+ runContext: {
2077
+ mode: localOnly ? "local-deterministic" : "llm",
2078
+ model: localOnly ? "local-deterministic" : model,
2079
+ minQuality,
2080
+ directionPolicy
2081
+ }
2082
+ }),
2083
+ directionPolicy
2084
+ };
2035
2085
  const evaluated = summary.correct + summary.incorrect;
2036
2086
  const result = {
2037
2087
  run: {
@@ -2053,7 +2103,8 @@ var main = async () => {
2053
2103
  model: localOnly ? "local-deterministic" : model,
2054
2104
  parallel: concurrency,
2055
2105
  dumpFeatures: dumpFeatureMode,
2056
- terminalWindows: terminalWindowDays
2106
+ terminalWindows: terminalWindowDays,
2107
+ directionPolicy
2057
2108
  },
2058
2109
  outcome: summary,
2059
2110
  byDirection: directionSummaries,
@@ -1504,6 +1504,7 @@ var normalizeCompletedTrade = (params) => {
1504
1504
  runId: row.backtestRunId ?? null,
1505
1505
  configId: row.configId?.trim() || "<missing-config-id>",
1506
1506
  signalId,
1507
+ positionCycleId: typeof result.positionCycleId === "string" && result.positionCycleId.trim() ? result.positionCycleId.trim() : null,
1507
1508
  setupIdentity,
1508
1509
  setupIdentitySource: explicitIdentity ? "research.setupIdentity" : strategyIdentity ? "strategy-context" : "signal-time-fallback",
1509
1510
  strategy: row.strategyName,
@@ -1527,6 +1528,69 @@ var normalizeCompletedTrade = (params) => {
1527
1528
  };
1528
1529
  };
1529
1530
  var completedTradeIdentity = (trade) => [trade.runId ?? "", trade.configId, trade.signalId, trade.symbol].join("|");
1531
+ var positionCycleIdentity = (trade) => trade.positionCycleId ? [
1532
+ trade.runId ?? "",
1533
+ trade.configId,
1534
+ trade.strategy,
1535
+ trade.symbol,
1536
+ trade.direction,
1537
+ trade.positionCycleId
1538
+ ].join("|") : null;
1539
+ var sumBy = (trades, select) => trades.reduce((sum, trade) => sum + select(trade), 0);
1540
+ var weightedPrice = (trades, select) => {
1541
+ const withPrice = trades.filter((trade) => select(trade) != null);
1542
+ const qty = sumBy(withPrice, (trade) => trade.qty);
1543
+ if (qty <= 0) return null;
1544
+ return sumBy(withPrice, (trade) => Number(select(trade)) * trade.qty) / qty;
1545
+ };
1546
+ var aggregatePositionCycles = (trades) => {
1547
+ const standalone = [];
1548
+ const cycles = /* @__PURE__ */ new Map();
1549
+ for (const trade of trades) {
1550
+ const identity = positionCycleIdentity(trade);
1551
+ if (!identity) {
1552
+ standalone.push(trade);
1553
+ continue;
1554
+ }
1555
+ const cycle = cycles.get(identity) ?? [];
1556
+ cycle.push(trade);
1557
+ cycles.set(identity, cycle);
1558
+ }
1559
+ for (const [identity, cycle] of cycles) {
1560
+ const primaryRows = cycle.filter(
1561
+ (trade) => trade.signalId === trade.positionCycleId
1562
+ );
1563
+ if (primaryRows.length !== 1) {
1564
+ throw new Error(
1565
+ `Position cycle ${identity} must contain exactly one opening row; found ${primaryRows.length}`
1566
+ );
1567
+ }
1568
+ const primary = primaryRows[0];
1569
+ if (cycle.some(
1570
+ (trade) => trade.exitTimestamp !== primary.exitTimestamp || trade.exitReason !== primary.exitReason
1571
+ )) {
1572
+ throw new Error(
1573
+ `Position cycle ${identity} contains inconsistent final exits`
1574
+ );
1575
+ }
1576
+ if (cycle.length === 1) {
1577
+ standalone.push(primary);
1578
+ continue;
1579
+ }
1580
+ standalone.push({
1581
+ ...primary,
1582
+ entryTimestamp: Math.min(...cycle.map((trade) => trade.entryTimestamp)),
1583
+ entryPrice: weightedPrice(cycle, (trade) => trade.entryPrice) ?? 0,
1584
+ exitPrice: weightedPrice(cycle, (trade) => trade.exitPrice),
1585
+ qty: sumBy(cycle, (trade) => trade.qty),
1586
+ netProfit: sumBy(cycle, (trade) => trade.netProfit),
1587
+ grossProfit: sumBy(cycle, (trade) => trade.grossProfit),
1588
+ totalFee: sumBy(cycle, (trade) => trade.totalFee),
1589
+ totalSlippageCost: sumBy(cycle, (trade) => trade.totalSlippageCost)
1590
+ });
1591
+ }
1592
+ return standalone;
1593
+ };
1530
1594
  var compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
1531
1595
  var compareTrades = (left, right) => left.exitTimestamp - right.exitTimestamp || compareText(left.symbol, right.symbol) || compareText(left.direction, right.direction) || compareText(left.signalId, right.signalId) || compareText(left.sourceFile, right.sourceFile) || left.sourceLine - right.sourceLine;
1532
1596
  var readCoreResearchVariant = async (variant) => {
@@ -1603,7 +1667,7 @@ var readCoreResearchVariant = async (variant) => {
1603
1667
  variant,
1604
1668
  files: sources,
1605
1669
  duplicateRowsDropped,
1606
- trades: trades.sort(compareTrades)
1670
+ trades: aggregatePositionCycles(trades).sort(compareTrades)
1607
1671
  };
1608
1672
  };
1609
1673
 
@@ -2084,13 +2148,17 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2084
2148
  const loaded = await Promise.all(
2085
2149
  analysisSpec.variants.map(readCoreResearchVariant)
2086
2150
  );
2151
+ const periodDays = (spec.window.end - spec.window.start) / DAY_MS;
2087
2152
  const windowed = loaded.map((entry) => ({
2088
2153
  ...entry,
2154
+ reconciliationMetrics: summarizeCoreResearchCohorts(
2155
+ entry.trades,
2156
+ periodDays
2157
+ ).ALL,
2089
2158
  trades: entry.trades.filter(
2090
2159
  (trade) => trade.exitTimestamp >= spec.window.start && trade.exitTimestamp < spec.window.end
2091
2160
  )
2092
2161
  }));
2093
- const periodDays = (spec.window.end - spec.window.start) / DAY_MS;
2094
2162
  const analyses = await Promise.all(
2095
2163
  windowed.map(async (entry) => ({
2096
2164
  variant: entry.variant,
@@ -2148,12 +2216,9 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
2148
2216
  reconciliation: await reconcileCoreResearchVariant({
2149
2217
  variant: entry.variant,
2150
2218
  spec,
2151
- exportMetrics: summarizeCoreResearchWindow({
2152
- trades: entry.trades,
2153
- label: "full",
2154
- start: spec.window.start,
2155
- end: spec.window.end
2156
- }).cohorts.ALL
2219
+ // Redis result.stat includes the runner's terminal close at window.end.
2220
+ // Keep half-open research metrics, but reconcile the complete run export.
2221
+ exportMetrics: entry.reconciliationMetrics
2157
2222
  }),
2158
2223
  supplemental: {
2159
2224
  coldStart: Object.fromEntries(
@@ -4906,6 +4906,7 @@ var import_promises2 = __toESM(require("fs/promises"));
4906
4906
  var import_node_path2 = __toESM(require("path"));
4907
4907
  var import_types2 = require("@tradejs/types");
4908
4908
  var import_strategies3 = require("@tradejs/node/strategies");
4909
+ var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
4909
4910
 
4910
4911
  // src/lib/strategyGateFingerprint.ts
4911
4912
  var import_node_crypto = require("crypto");
@@ -5000,17 +5001,6 @@ var normalizeForStableJson = (value) => {
5000
5001
  }
5001
5002
  return value;
5002
5003
  };
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
- };
5014
5004
  var resolveRuntimeMaxLossValue = (config) => {
5015
5005
  if (!config || typeof config !== "object" || Array.isArray(config)) {
5016
5006
  return null;
@@ -5174,9 +5164,7 @@ var buildRuntimeLineage = async ({
5174
5164
  })).gateFingerprint,
5175
5165
  // Position risk is tracked separately so changing trade size does not
5176
5166
  // masquerade as a change to the core + gate decision logic.
5177
- configFingerprint: fingerprintRuntimeValue(
5178
- normalizeRuntimeLogicConfig(config)
5179
- ),
5167
+ configFingerprint: (0, import_strategyReleaseEvidence.strategyLogicConfigFingerprint)(config),
5180
5168
  contextFingerprint: fingerprintRuntimeValue(context),
5181
5169
  maxLossValue: resolveRuntimeMaxLossValue(config)
5182
5170
  };
@@ -4909,6 +4909,7 @@ var import_promises2 = __toESM(require("fs/promises"));
4909
4909
  var import_node_path2 = __toESM(require("path"));
4910
4910
  var import_types2 = require("@tradejs/types");
4911
4911
  var import_strategies3 = require("@tradejs/node/strategies");
4912
+ var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
4912
4913
 
4913
4914
  // src/lib/strategyGateFingerprint.ts
4914
4915
  var import_node_crypto = require("crypto");
@@ -5003,17 +5004,6 @@ var normalizeForStableJson = (value) => {
5003
5004
  }
5004
5005
  return value;
5005
5006
  };
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
- };
5017
5007
  var resolveRuntimeMaxLossValue = (config) => {
5018
5008
  if (!config || typeof config !== "object" || Array.isArray(config)) {
5019
5009
  return null;
@@ -5177,9 +5167,7 @@ var buildRuntimeLineage = async ({
5177
5167
  })).gateFingerprint,
5178
5168
  // Position risk is tracked separately so changing trade size does not
5179
5169
  // masquerade as a change to the core + gate decision logic.
5180
- configFingerprint: fingerprintRuntimeValue(
5181
- normalizeRuntimeLogicConfig(config)
5182
- ),
5170
+ configFingerprint: (0, import_strategyReleaseEvidence.strategyLogicConfigFingerprint)(config),
5183
5171
  contextFingerprint: fingerprintRuntimeValue(context),
5184
5172
  maxLossValue: resolveRuntimeMaxLossValue(config)
5185
5173
  };
@@ -564,6 +564,7 @@ var import_promises3 = __toESM(require("fs/promises"));
564
564
  var import_node_path3 = __toESM(require("path"));
565
565
  var import_types = require("@tradejs/types");
566
566
  var import_strategies = require("@tradejs/node/strategies");
567
+ var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
567
568
 
568
569
  // src/lib/strategyGateFingerprint.ts
569
570
  var import_node_crypto2 = require("crypto");
@@ -811,6 +811,7 @@ var deriveReleaseGates = (evidence) => {
811
811
  };
812
812
  };
813
813
  var verifyEvidenceComposition = (strategy, composition, evidence) => {
814
+ const directionPolicy = composition.directionPolicy ?? "both";
814
815
  const requireEqual = (actual, expected, label, artifactId) => assert(
815
816
  actual === expected,
816
817
  `evidence ${artifactId} ${label} does not match the frozen composition`
@@ -881,6 +882,14 @@ var verifyEvidenceComposition = (strategy, composition, evidence) => {
881
882
  "gateContextFingerprint",
882
883
  reference.artifactId
883
884
  );
885
+ if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
886
+ requireEqual(
887
+ lineage?.directionPolicy,
888
+ directionPolicy,
889
+ "directionPolicy",
890
+ reference.artifactId
891
+ );
892
+ }
884
893
  assert(
885
894
  lineage?.sourceSha256s.includes(composition.coreExportSha256),
886
895
  `evidence ${reference.artifactId} does not use the frozen core export`
@@ -925,6 +934,14 @@ var verifyEvidenceComposition = (strategy, composition, evidence) => {
925
934
  "MAX_LOSS_VALUE",
926
935
  reference.artifactId
927
936
  );
937
+ if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
938
+ requireEqual(
939
+ lineage?.directionPolicy,
940
+ directionPolicy,
941
+ "directionPolicy",
942
+ reference.artifactId
943
+ );
944
+ }
928
945
  continue;
929
946
  }
930
947
  if (reference.kind === "execution_calibration") {
@@ -965,6 +982,14 @@ var verifyEvidenceComposition = (strategy, composition, evidence) => {
965
982
  "MAX_LOSS_VALUE",
966
983
  reference.artifactId
967
984
  );
985
+ if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
986
+ requireEqual(
987
+ lineage?.directionPolicy,
988
+ directionPolicy,
989
+ "directionPolicy",
990
+ reference.artifactId
991
+ );
992
+ }
968
993
  }
969
994
  }
970
995
  };
@@ -1000,6 +1025,12 @@ var createStrategyReleaseManifest = (input) => {
1000
1025
  input.composition.longEnabled && input.composition.shortEnabled,
1001
1026
  "LONG and SHORT must both remain enabled"
1002
1027
  );
1028
+ assert(
1029
+ input.composition.directionPolicy == null || ["both", "long_only", "short_only", "direction_aware"].includes(
1030
+ input.composition.directionPolicy
1031
+ ),
1032
+ "composition.directionPolicy is invalid"
1033
+ );
1003
1034
  assert(
1004
1035
  Number.isInteger(input.monitoring.minimumProspectiveClosedTrades) && input.monitoring.minimumProspectiveClosedTrades > 0,
1005
1036
  "monitoring.minimumProspectiveClosedTrades must be positive"
@@ -1064,7 +1095,8 @@ var createStrategyReleaseManifest = (input) => {
1064
1095
  gateFingerprint: input.composition.gateFingerprint,
1065
1096
  gateContextFingerprint: input.composition.gateContextFingerprint,
1066
1097
  runtimeContextFingerprint: input.composition.runtimeContextFingerprint,
1067
- maxLossValue: input.composition.maxLossValue
1098
+ maxLossValue: input.composition.maxLossValue,
1099
+ ...input.composition.directionPolicy == null ? {} : { directionPolicy: input.composition.directionPolicy }
1068
1100
  };
1069
1101
  const compositionId = `${safeSegment2(input.strategy, "strategy")}_${strategyReleaseSha256(compositionIdentity).slice(0, 16)}`;
1070
1102
  const reasons = releaseReasons({ ...input, gates: evidenceGates });
@@ -1148,7 +1180,8 @@ var verifyStrategyReleaseEnvelope = async (valueOrPath) => {
1148
1180
  runtimeContextFingerprint: manifest.composition.runtimeContextFingerprint,
1149
1181
  maxLossValue: manifest.composition.maxLossValue,
1150
1182
  longEnabled: manifest.composition.longEnabled,
1151
- shortEnabled: manifest.composition.shortEnabled
1183
+ shortEnabled: manifest.composition.shortEnabled,
1184
+ directionPolicy: manifest.composition.directionPolicy
1152
1185
  },
1153
1186
  marketWindow: manifest.marketWindow,
1154
1187
  researchBudget: manifest.researchBudget,
@@ -4003,6 +4003,7 @@ var import_promises2 = __toESM(require("fs/promises"));
4003
4003
  var import_node_path2 = __toESM(require("path"));
4004
4004
  var import_types = require("@tradejs/types");
4005
4005
  var import_strategies4 = require("@tradejs/node/strategies");
4006
+ var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
4006
4007
 
4007
4008
  // src/lib/strategyGateFingerprint.ts
4008
4009
  var import_node_crypto = require("crypto");
@@ -4097,17 +4098,6 @@ var normalizeForStableJson = (value) => {
4097
4098
  }
4098
4099
  return value;
4099
4100
  };
4100
- var normalizeRuntimeLogicConfig = (value) => {
4101
- if (Array.isArray(value)) {
4102
- return value.map(normalizeRuntimeLogicConfig);
4103
- }
4104
- if (value && typeof value === "object") {
4105
- return Object.fromEntries(
4106
- Object.entries(value).filter(([key]) => key !== "MAX_LOSS_VALUE").sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeRuntimeLogicConfig(entry)])
4107
- );
4108
- }
4109
- return value;
4110
- };
4111
4101
  var resolveRuntimeMaxLossValue = (config2) => {
4112
4102
  if (!config2 || typeof config2 !== "object" || Array.isArray(config2)) {
4113
4103
  return null;
@@ -4271,9 +4261,7 @@ var buildRuntimeLineage = async ({
4271
4261
  })).gateFingerprint,
4272
4262
  // Position risk is tracked separately so changing trade size does not
4273
4263
  // masquerade as a change to the core + gate decision logic.
4274
- configFingerprint: fingerprintRuntimeValue(
4275
- normalizeRuntimeLogicConfig(config2)
4276
- ),
4264
+ configFingerprint: (0, import_strategyReleaseEvidence.strategyLogicConfigFingerprint)(config2),
4277
4265
  contextFingerprint: fingerprintRuntimeValue(context),
4278
4266
  maxLossValue: resolveRuntimeMaxLossValue(config2)
4279
4267
  };
@@ -4001,6 +4001,7 @@ var import_promises2 = __toESM(require("fs/promises"));
4001
4001
  var import_node_path2 = __toESM(require("path"));
4002
4002
  var import_types = require("@tradejs/types");
4003
4003
  var import_strategies4 = require("@tradejs/node/strategies");
4004
+ var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
4004
4005
 
4005
4006
  // src/lib/strategyGateFingerprint.ts
4006
4007
  var import_node_crypto = require("crypto");
@@ -4095,17 +4096,6 @@ var normalizeForStableJson = (value) => {
4095
4096
  }
4096
4097
  return value;
4097
4098
  };
4098
- var normalizeRuntimeLogicConfig = (value) => {
4099
- if (Array.isArray(value)) {
4100
- return value.map(normalizeRuntimeLogicConfig);
4101
- }
4102
- if (value && typeof value === "object") {
4103
- return Object.fromEntries(
4104
- Object.entries(value).filter(([key]) => key !== "MAX_LOSS_VALUE").sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeRuntimeLogicConfig(entry)])
4105
- );
4106
- }
4107
- return value;
4108
- };
4109
4099
  var resolveRuntimeMaxLossValue = (config2) => {
4110
4100
  if (!config2 || typeof config2 !== "object" || Array.isArray(config2)) {
4111
4101
  return null;
@@ -4269,9 +4259,7 @@ var buildRuntimeLineage = async ({
4269
4259
  })).gateFingerprint,
4270
4260
  // Position risk is tracked separately so changing trade size does not
4271
4261
  // masquerade as a change to the core + gate decision logic.
4272
- configFingerprint: fingerprintRuntimeValue(
4273
- normalizeRuntimeLogicConfig(config2)
4274
- ),
4262
+ configFingerprint: (0, import_strategyReleaseEvidence.strategyLogicConfigFingerprint)(config2),
4275
4263
  contextFingerprint: fingerprintRuntimeValue(context),
4276
4264
  maxLossValue: resolveRuntimeMaxLossValue(config2)
4277
4265
  };