@tradejs/cli 2.0.19 → 2.0.21

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.
@@ -2117,27 +2117,27 @@ var createCoinMarketCapFetchStats = () => ({
2117
2117
  totalCredits: 0,
2118
2118
  byPath: /* @__PURE__ */ new Map()
2119
2119
  });
2120
- var getCoinMarketCapPathLabel = (path2) => {
2121
- if (path2.includes("global-metrics")) return "global";
2122
- if (path2.includes("cryptocurrency/quotes")) return "reference";
2123
- if (path2.includes("exchange/quotes")) return "exchangeLiquidity";
2124
- if (path2.includes("fear-and-greed")) return "fearGreed";
2125
- if (path2.includes("cmc100")) return "cmc100";
2126
- if (path2.includes("cmc20")) return "cmc20";
2127
- return path2;
2128
- };
2129
- var recordCoinMarketCapFetchStats = (stats, path2, creditCount) => {
2120
+ var getCoinMarketCapPathLabel = (path3) => {
2121
+ if (path3.includes("global-metrics")) return "global";
2122
+ if (path3.includes("cryptocurrency/quotes")) return "reference";
2123
+ if (path3.includes("exchange/quotes")) return "exchangeLiquidity";
2124
+ if (path3.includes("fear-and-greed")) return "fearGreed";
2125
+ if (path3.includes("cmc100")) return "cmc100";
2126
+ if (path3.includes("cmc20")) return "cmc20";
2127
+ return path3;
2128
+ };
2129
+ var recordCoinMarketCapFetchStats = (stats, path3, creditCount) => {
2130
2130
  if (!stats) return;
2131
- const existing = stats.byPath.get(path2) ?? { requests: 0, credits: 0 };
2131
+ const existing = stats.byPath.get(path3) ?? { requests: 0, credits: 0 };
2132
2132
  existing.requests += 1;
2133
2133
  existing.credits += creditCount ?? 0;
2134
2134
  stats.requests += 1;
2135
2135
  stats.totalCredits += creditCount ?? 0;
2136
- stats.byPath.set(path2, existing);
2136
+ stats.byPath.set(path3, existing);
2137
2137
  };
2138
2138
  var formatCoinMarketCapFetchStats = (stats) => {
2139
2139
  const byPath = [...stats.byPath.entries()].map(
2140
- ([path2, item]) => `${getCoinMarketCapPathLabel(path2)}=${item.requests}req/${item.credits}cr`
2140
+ ([path3, item]) => `${getCoinMarketCapPathLabel(path3)}=${item.requests}req/${item.credits}cr`
2141
2141
  ).join(" ");
2142
2142
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
2143
2143
  };
@@ -3807,6 +3807,7 @@ var loadRuntimeStrategies = async ({
3807
3807
  interval: configInterval,
3808
3808
  universe: configUniverse,
3809
3809
  accountId: effectiveAccountId,
3810
+ releaseCompositionId: deploymentStrategy?.releaseCompositionId,
3810
3811
  strategyCreator,
3811
3812
  sourceStrategyConfig: strategyConfig,
3812
3813
  strategyConfig: {
@@ -3997,11 +3998,57 @@ var import_redis3 = require("@tradejs/infra/redis");
3997
3998
 
3998
3999
  // src/lib/runtimeLineage.ts
3999
4000
  var import_node_child_process = require("child_process");
4001
+ var import_node_crypto2 = require("crypto");
4002
+ var import_promises2 = __toESM(require("fs/promises"));
4003
+ var import_node_path2 = __toESM(require("path"));
4004
+ var import_types = require("@tradejs/types");
4005
+ var import_strategies4 = require("@tradejs/node/strategies");
4006
+ var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
4007
+
4008
+ // src/lib/strategyGateFingerprint.ts
4000
4009
  var import_node_crypto = require("crypto");
4001
4010
  var import_promises = __toESM(require("fs/promises"));
4002
4011
  var import_node_path = __toESM(require("path"));
4003
- var import_types = require("@tradejs/types");
4004
- var import_strategies4 = require("@tradejs/node/strategies");
4012
+ var gateFingerprintRelativePaths = (strategyName) => [
4013
+ `packages/strategies/src/${strategyName}/adapters/ai.ts`,
4014
+ `packages/strategies/src/${strategyName}/guardrails.ts`,
4015
+ `packages/strategies/src/${strategyName}/pockets.ts`,
4016
+ `packages/strategies/src/${strategyName}/config.ts`,
4017
+ "packages/strategies/src/shared/localAiGate.ts",
4018
+ "packages/node/src/ai.ts",
4019
+ "packages/core/src/utils/strategyHelpers/signalBuilders.ts"
4020
+ ];
4021
+ var resolveStrategyGateFingerprint = async ({
4022
+ projectRoot,
4023
+ strategyName,
4024
+ gitSha
4025
+ }) => {
4026
+ const candidates = await Promise.all(
4027
+ gateFingerprintRelativePaths(strategyName).map(async (relativePath) => {
4028
+ const content = await import_promises.default.readFile(import_node_path.default.join(projectRoot, relativePath)).catch(() => null);
4029
+ return content == null ? null : { relativePath, content };
4030
+ })
4031
+ );
4032
+ const sourceEntries = candidates.filter(
4033
+ (entry) => entry != null
4034
+ );
4035
+ const hash = (0, import_node_crypto.createHash)("sha256");
4036
+ hash.update(strategyName);
4037
+ if (sourceEntries.length) {
4038
+ for (const entry of sourceEntries) {
4039
+ hash.update(entry.relativePath);
4040
+ hash.update(entry.content);
4041
+ }
4042
+ } else {
4043
+ hash.update(gitSha ?? "unknown-git-sha");
4044
+ }
4045
+ return {
4046
+ gateFingerprint: hash.digest("hex").slice(0, 16),
4047
+ gateFingerprintFiles: sourceEntries.map((entry) => entry.relativePath)
4048
+ };
4049
+ };
4050
+
4051
+ // src/lib/runtimeLineage.ts
4005
4052
  var RUNTIME_CONTEXT_ENV_KEYS = [
4006
4053
  "AI_MODE",
4007
4054
  "MIN_AI_QUALITY",
@@ -4064,7 +4111,7 @@ var resolveRuntimeMaxLossValue = (config2) => {
4064
4111
  );
4065
4112
  return Number.isFinite(maxLossValue) ? maxLossValue : null;
4066
4113
  };
4067
- var fingerprintRuntimeValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
4114
+ var fingerprintRuntimeValue = (value) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
4068
4115
  var gitLineageCache = /* @__PURE__ */ new Map();
4069
4116
  var gateFingerprintCache = /* @__PURE__ */ new Map();
4070
4117
  var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
@@ -4115,7 +4162,7 @@ var resolveGitLineage = (projectRoot) => {
4115
4162
  };
4116
4163
  var readOptionalFile = async (filePath) => {
4117
4164
  try {
4118
- return await import_promises.default.readFile(filePath);
4165
+ return await import_promises2.default.readFile(filePath);
4119
4166
  } catch {
4120
4167
  return null;
4121
4168
  }
@@ -4125,7 +4172,7 @@ var resolveBinanceBreadthFingerprint = async (projectRoot) => {
4125
4172
  return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
4126
4173
  }
4127
4174
  const content = await readOptionalFile(
4128
- import_node_path.default.join(
4175
+ import_node_path2.default.join(
4129
4176
  projectRoot,
4130
4177
  "packages/node/src/config/binanceBreadthUniverses.json"
4131
4178
  )
@@ -4147,7 +4194,7 @@ var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
4147
4194
  if (snapshotFingerprintCache.has(cacheKey)) {
4148
4195
  return snapshotFingerprintCache.get(cacheKey) ?? null;
4149
4196
  }
4150
- const content = await readOptionalFile(import_node_path.default.join(projectRoot, relativePath));
4197
+ const content = await readOptionalFile(import_node_path2.default.join(projectRoot, relativePath));
4151
4198
  let result = null;
4152
4199
  if (content) {
4153
4200
  try {
@@ -4160,55 +4207,26 @@ var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
4160
4207
  snapshotFingerprintCache.set(cacheKey, result);
4161
4208
  return result;
4162
4209
  };
4163
- var resolveGateFingerprint = async ({
4210
+ var resolveRuntimeGateFingerprint = async ({
4164
4211
  projectRoot,
4165
4212
  strategyName,
4166
4213
  gitSha
4167
4214
  }) => {
4168
4215
  const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
4169
4216
  const cached = gateFingerprintCache.get(cacheKey);
4170
- if (cached) {
4171
- return cached;
4172
- }
4173
- const relativeCandidates = [
4174
- `packages/strategies/src/${strategyName}/adapters/ai.ts`,
4175
- `packages/strategies/src/${strategyName}/guardrails.ts`,
4176
- `packages/strategies/src/${strategyName}/pockets.ts`,
4177
- `packages/strategies/src/${strategyName}/config.ts`,
4178
- "packages/node/src/ai.ts",
4179
- "packages/core/src/utils/strategyHelpers/signalBuilders.ts"
4180
- ];
4181
- const optionalEntries = await Promise.all(
4182
- relativeCandidates.map(async (relativePath) => {
4183
- const content = await readOptionalFile(
4184
- import_node_path.default.join(projectRoot, relativePath)
4185
- );
4186
- return content == null ? null : { relativePath, content };
4187
- })
4188
- );
4189
- const sourceEntries = [];
4190
- for (const entry of optionalEntries) {
4191
- if (entry != null) {
4192
- sourceEntries.push(entry);
4193
- }
4194
- }
4195
- const hash = (0, import_node_crypto.createHash)("sha256");
4196
- hash.update(strategyName);
4197
- if (sourceEntries.length) {
4198
- for (const entry of sourceEntries) {
4199
- hash.update(entry.relativePath);
4200
- hash.update(entry.content);
4201
- }
4202
- } else {
4203
- hash.update(gitSha ?? "unknown-git-sha");
4204
- }
4205
- const fingerprint = hash.digest("hex").slice(0, 16);
4206
- gateFingerprintCache.set(cacheKey, fingerprint);
4207
- return fingerprint;
4217
+ if (cached) return cached;
4218
+ const result = await resolveStrategyGateFingerprint({
4219
+ projectRoot,
4220
+ strategyName,
4221
+ gitSha
4222
+ });
4223
+ gateFingerprintCache.set(cacheKey, result);
4224
+ return result;
4208
4225
  };
4209
4226
  var buildRuntimeLineage = async ({
4210
4227
  projectRoot,
4211
4228
  strategyName,
4229
+ compositionId,
4212
4230
  config: config2,
4213
4231
  runContext,
4214
4232
  env = process.env
@@ -4234,19 +4252,23 @@ var buildRuntimeLineage = async ({
4234
4252
  };
4235
4253
  return {
4236
4254
  schemaVersion: 1,
4255
+ compositionId: compositionId?.trim() || null,
4237
4256
  ...git,
4238
- gateFingerprint: await resolveGateFingerprint({
4257
+ gateFingerprint: (await resolveRuntimeGateFingerprint({
4239
4258
  projectRoot,
4240
4259
  strategyName,
4241
4260
  gitSha: git.gitSha
4242
- }),
4243
- configFingerprint: fingerprintRuntimeValue(config2),
4261
+ })).gateFingerprint,
4262
+ // Position risk is tracked separately so changing trade size does not
4263
+ // masquerade as a change to the core + gate decision logic.
4264
+ configFingerprint: (0, import_strategyReleaseEvidence.strategyLogicConfigFingerprint)(config2),
4244
4265
  contextFingerprint: fingerprintRuntimeValue(context),
4245
4266
  maxLossValue: resolveRuntimeMaxLossValue(config2)
4246
4267
  };
4247
4268
  };
4248
4269
  var runtimeLineageKey = (lineage) => [
4249
4270
  lineage.schemaVersion,
4271
+ lineage.compositionId ?? "unbound-composition",
4250
4272
  lineage.gitSha ?? "unknown",
4251
4273
  lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
4252
4274
  lineage.gateFingerprint,
@@ -4383,6 +4405,39 @@ var flushRuntimeSignalEvaluations = async (evaluations) => {
4383
4405
  var createRuntimeSignalEvaluationBuffer = () => {
4384
4406
  const evaluations = [];
4385
4407
  const signals2 = [];
4408
+ const flushSignals = async () => {
4409
+ const pendingSignals = signals2.splice(0);
4410
+ if (pendingSignals.length === 0) return;
4411
+ const runtimeSignalRetentionTtl = getRuntimeSignalRetentionTtlSeconds();
4412
+ const signalRefsByBucket = /* @__PURE__ */ new Map();
4413
+ for (const { userName, signal } of pendingSignals) {
4414
+ const bucket = import_redis3.redisKeys.runtimeSignalBucket(
4415
+ userName,
4416
+ (0, import_time2.getRuntimeStorageDayKey)(signal.timestamp),
4417
+ signal.strategy
4418
+ );
4419
+ const entries = signalRefsByBucket.get(bucket) ?? [];
4420
+ entries.push({
4421
+ field: signal.signalId,
4422
+ data: toRuntimeSignalBucketRef(signal)
4423
+ });
4424
+ signalRefsByBucket.set(bucket, entries);
4425
+ }
4426
+ await Promise.all([
4427
+ ...pendingSignals.map(
4428
+ ({ signal }) => (0, import_redis3.setData)(
4429
+ import_redis3.redisKeys.storeSignal(signal.symbol, signal.signalId),
4430
+ toStoredRuntimeSignal(signal),
4431
+ { expire: runtimeSignalRetentionTtl }
4432
+ )
4433
+ ),
4434
+ ...[...signalRefsByBucket].map(
4435
+ ([bucket, entries]) => (0, import_redis3.setHashJsonFields)(bucket, entries, {
4436
+ expire: runtimeSignalRetentionTtl
4437
+ })
4438
+ )
4439
+ ]);
4440
+ };
4386
4441
  return {
4387
4442
  save: async (evaluation) => {
4388
4443
  evaluations.push(evaluation);
@@ -4390,38 +4445,12 @@ var createRuntimeSignalEvaluationBuffer = () => {
4390
4445
  saveSignal: (userName, signal) => {
4391
4446
  signals2.push({ userName, signal });
4392
4447
  },
4448
+ flushSignals,
4393
4449
  flush: async () => {
4394
4450
  const pending = evaluations.splice(0);
4395
- const pendingSignals = signals2.splice(0);
4396
- const runtimeSignalRetentionTtl = getRuntimeSignalRetentionTtlSeconds();
4397
- const signalRefsByBucket = /* @__PURE__ */ new Map();
4398
- for (const { userName, signal } of pendingSignals) {
4399
- const bucket = import_redis3.redisKeys.runtimeSignalBucket(
4400
- userName,
4401
- (0, import_time2.getRuntimeStorageDayKey)(signal.timestamp),
4402
- signal.strategy
4403
- );
4404
- const entries = signalRefsByBucket.get(bucket) ?? [];
4405
- entries.push({
4406
- field: signal.signalId,
4407
- data: toRuntimeSignalBucketRef(signal)
4408
- });
4409
- signalRefsByBucket.set(bucket, entries);
4410
- }
4411
4451
  await Promise.all([
4412
4452
  flushRuntimeSignalEvaluations(pending),
4413
- ...pendingSignals.map(
4414
- ({ signal }) => (0, import_redis3.setData)(
4415
- import_redis3.redisKeys.storeSignal(signal.symbol, signal.signalId),
4416
- toStoredRuntimeSignal(signal),
4417
- { expire: runtimeSignalRetentionTtl }
4418
- )
4419
- ),
4420
- ...[...signalRefsByBucket].map(
4421
- ([bucket, entries]) => (0, import_redis3.setHashJsonFields)(bucket, entries, {
4422
- expire: runtimeSignalRetentionTtl
4423
- })
4424
- )
4453
+ flushSignals()
4425
4454
  ]);
4426
4455
  }
4427
4456
  };
@@ -4848,6 +4877,7 @@ var createSignalsTickerEvaluator = ({
4848
4877
  strategyName,
4849
4878
  configId,
4850
4879
  strategyCreator,
4880
+ releaseCompositionId,
4851
4881
  sourceStrategyConfig,
4852
4882
  strategyConfig,
4853
4883
  strategyResults
@@ -4862,6 +4892,7 @@ var createSignalsTickerEvaluator = ({
4862
4892
  const runtimeLineage = await buildRuntimeLineage({
4863
4893
  projectRoot,
4864
4894
  strategyName,
4895
+ compositionId: releaseCompositionId,
4865
4896
  config: { configId, strategyConfig, symbolResultConfig },
4866
4897
  runContext: {
4867
4898
  connectorName: connectorName.toLowerCase(),
@@ -5441,6 +5472,7 @@ var createSignalsRunner = (config2) => {
5441
5472
  if (config2.notify) {
5442
5473
  const telegramSignals = getTelegramDeliverableSignals(signals3);
5443
5474
  if (!config2.skipScreenshots && telegramSignals.length > 0) {
5475
+ await runtimeSignalEvaluations.flushSignals();
5444
5476
  await (0, import_cli2.makeScreenshots)(telegramSignals, interval2, config2.userName);
5445
5477
  }
5446
5478
  await (0, import_cli2.sendToTG)(telegramSignals, interval2, config2.userName);
@@ -2115,27 +2115,27 @@ var createCoinMarketCapFetchStats = () => ({
2115
2115
  totalCredits: 0,
2116
2116
  byPath: /* @__PURE__ */ new Map()
2117
2117
  });
2118
- var getCoinMarketCapPathLabel = (path2) => {
2119
- if (path2.includes("global-metrics")) return "global";
2120
- if (path2.includes("cryptocurrency/quotes")) return "reference";
2121
- if (path2.includes("exchange/quotes")) return "exchangeLiquidity";
2122
- if (path2.includes("fear-and-greed")) return "fearGreed";
2123
- if (path2.includes("cmc100")) return "cmc100";
2124
- if (path2.includes("cmc20")) return "cmc20";
2125
- return path2;
2126
- };
2127
- var recordCoinMarketCapFetchStats = (stats, path2, creditCount) => {
2118
+ var getCoinMarketCapPathLabel = (path3) => {
2119
+ if (path3.includes("global-metrics")) return "global";
2120
+ if (path3.includes("cryptocurrency/quotes")) return "reference";
2121
+ if (path3.includes("exchange/quotes")) return "exchangeLiquidity";
2122
+ if (path3.includes("fear-and-greed")) return "fearGreed";
2123
+ if (path3.includes("cmc100")) return "cmc100";
2124
+ if (path3.includes("cmc20")) return "cmc20";
2125
+ return path3;
2126
+ };
2127
+ var recordCoinMarketCapFetchStats = (stats, path3, creditCount) => {
2128
2128
  if (!stats) return;
2129
- const existing = stats.byPath.get(path2) ?? { requests: 0, credits: 0 };
2129
+ const existing = stats.byPath.get(path3) ?? { requests: 0, credits: 0 };
2130
2130
  existing.requests += 1;
2131
2131
  existing.credits += creditCount ?? 0;
2132
2132
  stats.requests += 1;
2133
2133
  stats.totalCredits += creditCount ?? 0;
2134
- stats.byPath.set(path2, existing);
2134
+ stats.byPath.set(path3, existing);
2135
2135
  };
2136
2136
  var formatCoinMarketCapFetchStats = (stats) => {
2137
2137
  const byPath = [...stats.byPath.entries()].map(
2138
- ([path2, item]) => `${getCoinMarketCapPathLabel(path2)}=${item.requests}req/${item.credits}cr`
2138
+ ([path3, item]) => `${getCoinMarketCapPathLabel(path3)}=${item.requests}req/${item.credits}cr`
2139
2139
  ).join(" ");
2140
2140
  return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
2141
2141
  };
@@ -3805,6 +3805,7 @@ var loadRuntimeStrategies = async ({
3805
3805
  interval: configInterval,
3806
3806
  universe: configUniverse,
3807
3807
  accountId: effectiveAccountId,
3808
+ releaseCompositionId: deploymentStrategy?.releaseCompositionId,
3808
3809
  strategyCreator,
3809
3810
  sourceStrategyConfig: strategyConfig,
3810
3811
  strategyConfig: {
@@ -3995,11 +3996,57 @@ var import_redis3 = require("@tradejs/infra/redis");
3995
3996
 
3996
3997
  // src/lib/runtimeLineage.ts
3997
3998
  var import_node_child_process = require("child_process");
3999
+ var import_node_crypto2 = require("crypto");
4000
+ var import_promises2 = __toESM(require("fs/promises"));
4001
+ var import_node_path2 = __toESM(require("path"));
4002
+ var import_types = require("@tradejs/types");
4003
+ var import_strategies4 = require("@tradejs/node/strategies");
4004
+ var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
4005
+
4006
+ // src/lib/strategyGateFingerprint.ts
3998
4007
  var import_node_crypto = require("crypto");
3999
4008
  var import_promises = __toESM(require("fs/promises"));
4000
4009
  var import_node_path = __toESM(require("path"));
4001
- var import_types = require("@tradejs/types");
4002
- var import_strategies4 = require("@tradejs/node/strategies");
4010
+ var gateFingerprintRelativePaths = (strategyName) => [
4011
+ `packages/strategies/src/${strategyName}/adapters/ai.ts`,
4012
+ `packages/strategies/src/${strategyName}/guardrails.ts`,
4013
+ `packages/strategies/src/${strategyName}/pockets.ts`,
4014
+ `packages/strategies/src/${strategyName}/config.ts`,
4015
+ "packages/strategies/src/shared/localAiGate.ts",
4016
+ "packages/node/src/ai.ts",
4017
+ "packages/core/src/utils/strategyHelpers/signalBuilders.ts"
4018
+ ];
4019
+ var resolveStrategyGateFingerprint = async ({
4020
+ projectRoot,
4021
+ strategyName,
4022
+ gitSha
4023
+ }) => {
4024
+ const candidates = await Promise.all(
4025
+ gateFingerprintRelativePaths(strategyName).map(async (relativePath) => {
4026
+ const content = await import_promises.default.readFile(import_node_path.default.join(projectRoot, relativePath)).catch(() => null);
4027
+ return content == null ? null : { relativePath, content };
4028
+ })
4029
+ );
4030
+ const sourceEntries = candidates.filter(
4031
+ (entry) => entry != null
4032
+ );
4033
+ const hash = (0, import_node_crypto.createHash)("sha256");
4034
+ hash.update(strategyName);
4035
+ if (sourceEntries.length) {
4036
+ for (const entry of sourceEntries) {
4037
+ hash.update(entry.relativePath);
4038
+ hash.update(entry.content);
4039
+ }
4040
+ } else {
4041
+ hash.update(gitSha ?? "unknown-git-sha");
4042
+ }
4043
+ return {
4044
+ gateFingerprint: hash.digest("hex").slice(0, 16),
4045
+ gateFingerprintFiles: sourceEntries.map((entry) => entry.relativePath)
4046
+ };
4047
+ };
4048
+
4049
+ // src/lib/runtimeLineage.ts
4003
4050
  var RUNTIME_CONTEXT_ENV_KEYS = [
4004
4051
  "AI_MODE",
4005
4052
  "MIN_AI_QUALITY",
@@ -4062,7 +4109,7 @@ var resolveRuntimeMaxLossValue = (config2) => {
4062
4109
  );
4063
4110
  return Number.isFinite(maxLossValue) ? maxLossValue : null;
4064
4111
  };
4065
- var fingerprintRuntimeValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
4112
+ var fingerprintRuntimeValue = (value) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
4066
4113
  var gitLineageCache = /* @__PURE__ */ new Map();
4067
4114
  var gateFingerprintCache = /* @__PURE__ */ new Map();
4068
4115
  var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
@@ -4113,7 +4160,7 @@ var resolveGitLineage = (projectRoot) => {
4113
4160
  };
4114
4161
  var readOptionalFile = async (filePath) => {
4115
4162
  try {
4116
- return await import_promises.default.readFile(filePath);
4163
+ return await import_promises2.default.readFile(filePath);
4117
4164
  } catch {
4118
4165
  return null;
4119
4166
  }
@@ -4123,7 +4170,7 @@ var resolveBinanceBreadthFingerprint = async (projectRoot) => {
4123
4170
  return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
4124
4171
  }
4125
4172
  const content = await readOptionalFile(
4126
- import_node_path.default.join(
4173
+ import_node_path2.default.join(
4127
4174
  projectRoot,
4128
4175
  "packages/node/src/config/binanceBreadthUniverses.json"
4129
4176
  )
@@ -4145,7 +4192,7 @@ var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
4145
4192
  if (snapshotFingerprintCache.has(cacheKey)) {
4146
4193
  return snapshotFingerprintCache.get(cacheKey) ?? null;
4147
4194
  }
4148
- const content = await readOptionalFile(import_node_path.default.join(projectRoot, relativePath));
4195
+ const content = await readOptionalFile(import_node_path2.default.join(projectRoot, relativePath));
4149
4196
  let result = null;
4150
4197
  if (content) {
4151
4198
  try {
@@ -4158,55 +4205,26 @@ var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
4158
4205
  snapshotFingerprintCache.set(cacheKey, result);
4159
4206
  return result;
4160
4207
  };
4161
- var resolveGateFingerprint = async ({
4208
+ var resolveRuntimeGateFingerprint = async ({
4162
4209
  projectRoot,
4163
4210
  strategyName,
4164
4211
  gitSha
4165
4212
  }) => {
4166
4213
  const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
4167
4214
  const cached = gateFingerprintCache.get(cacheKey);
4168
- if (cached) {
4169
- return cached;
4170
- }
4171
- const relativeCandidates = [
4172
- `packages/strategies/src/${strategyName}/adapters/ai.ts`,
4173
- `packages/strategies/src/${strategyName}/guardrails.ts`,
4174
- `packages/strategies/src/${strategyName}/pockets.ts`,
4175
- `packages/strategies/src/${strategyName}/config.ts`,
4176
- "packages/node/src/ai.ts",
4177
- "packages/core/src/utils/strategyHelpers/signalBuilders.ts"
4178
- ];
4179
- const optionalEntries = await Promise.all(
4180
- relativeCandidates.map(async (relativePath) => {
4181
- const content = await readOptionalFile(
4182
- import_node_path.default.join(projectRoot, relativePath)
4183
- );
4184
- return content == null ? null : { relativePath, content };
4185
- })
4186
- );
4187
- const sourceEntries = [];
4188
- for (const entry of optionalEntries) {
4189
- if (entry != null) {
4190
- sourceEntries.push(entry);
4191
- }
4192
- }
4193
- const hash = (0, import_node_crypto.createHash)("sha256");
4194
- hash.update(strategyName);
4195
- if (sourceEntries.length) {
4196
- for (const entry of sourceEntries) {
4197
- hash.update(entry.relativePath);
4198
- hash.update(entry.content);
4199
- }
4200
- } else {
4201
- hash.update(gitSha ?? "unknown-git-sha");
4202
- }
4203
- const fingerprint = hash.digest("hex").slice(0, 16);
4204
- gateFingerprintCache.set(cacheKey, fingerprint);
4205
- return fingerprint;
4215
+ if (cached) return cached;
4216
+ const result = await resolveStrategyGateFingerprint({
4217
+ projectRoot,
4218
+ strategyName,
4219
+ gitSha
4220
+ });
4221
+ gateFingerprintCache.set(cacheKey, result);
4222
+ return result;
4206
4223
  };
4207
4224
  var buildRuntimeLineage = async ({
4208
4225
  projectRoot,
4209
4226
  strategyName,
4227
+ compositionId,
4210
4228
  config: config2,
4211
4229
  runContext,
4212
4230
  env = process.env
@@ -4232,19 +4250,23 @@ var buildRuntimeLineage = async ({
4232
4250
  };
4233
4251
  return {
4234
4252
  schemaVersion: 1,
4253
+ compositionId: compositionId?.trim() || null,
4235
4254
  ...git,
4236
- gateFingerprint: await resolveGateFingerprint({
4255
+ gateFingerprint: (await resolveRuntimeGateFingerprint({
4237
4256
  projectRoot,
4238
4257
  strategyName,
4239
4258
  gitSha: git.gitSha
4240
- }),
4241
- configFingerprint: fingerprintRuntimeValue(config2),
4259
+ })).gateFingerprint,
4260
+ // Position risk is tracked separately so changing trade size does not
4261
+ // masquerade as a change to the core + gate decision logic.
4262
+ configFingerprint: (0, import_strategyReleaseEvidence.strategyLogicConfigFingerprint)(config2),
4242
4263
  contextFingerprint: fingerprintRuntimeValue(context),
4243
4264
  maxLossValue: resolveRuntimeMaxLossValue(config2)
4244
4265
  };
4245
4266
  };
4246
4267
  var runtimeLineageKey = (lineage) => [
4247
4268
  lineage.schemaVersion,
4269
+ lineage.compositionId ?? "unbound-composition",
4248
4270
  lineage.gitSha ?? "unknown",
4249
4271
  lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
4250
4272
  lineage.gateFingerprint,
@@ -4381,6 +4403,39 @@ var flushRuntimeSignalEvaluations = async (evaluations) => {
4381
4403
  var createRuntimeSignalEvaluationBuffer = () => {
4382
4404
  const evaluations = [];
4383
4405
  const signals2 = [];
4406
+ const flushSignals = async () => {
4407
+ const pendingSignals = signals2.splice(0);
4408
+ if (pendingSignals.length === 0) return;
4409
+ const runtimeSignalRetentionTtl = getRuntimeSignalRetentionTtlSeconds();
4410
+ const signalRefsByBucket = /* @__PURE__ */ new Map();
4411
+ for (const { userName, signal } of pendingSignals) {
4412
+ const bucket = import_redis3.redisKeys.runtimeSignalBucket(
4413
+ userName,
4414
+ (0, import_time2.getRuntimeStorageDayKey)(signal.timestamp),
4415
+ signal.strategy
4416
+ );
4417
+ const entries = signalRefsByBucket.get(bucket) ?? [];
4418
+ entries.push({
4419
+ field: signal.signalId,
4420
+ data: toRuntimeSignalBucketRef(signal)
4421
+ });
4422
+ signalRefsByBucket.set(bucket, entries);
4423
+ }
4424
+ await Promise.all([
4425
+ ...pendingSignals.map(
4426
+ ({ signal }) => (0, import_redis3.setData)(
4427
+ import_redis3.redisKeys.storeSignal(signal.symbol, signal.signalId),
4428
+ toStoredRuntimeSignal(signal),
4429
+ { expire: runtimeSignalRetentionTtl }
4430
+ )
4431
+ ),
4432
+ ...[...signalRefsByBucket].map(
4433
+ ([bucket, entries]) => (0, import_redis3.setHashJsonFields)(bucket, entries, {
4434
+ expire: runtimeSignalRetentionTtl
4435
+ })
4436
+ )
4437
+ ]);
4438
+ };
4384
4439
  return {
4385
4440
  save: async (evaluation) => {
4386
4441
  evaluations.push(evaluation);
@@ -4388,38 +4443,12 @@ var createRuntimeSignalEvaluationBuffer = () => {
4388
4443
  saveSignal: (userName, signal) => {
4389
4444
  signals2.push({ userName, signal });
4390
4445
  },
4446
+ flushSignals,
4391
4447
  flush: async () => {
4392
4448
  const pending = evaluations.splice(0);
4393
- const pendingSignals = signals2.splice(0);
4394
- const runtimeSignalRetentionTtl = getRuntimeSignalRetentionTtlSeconds();
4395
- const signalRefsByBucket = /* @__PURE__ */ new Map();
4396
- for (const { userName, signal } of pendingSignals) {
4397
- const bucket = import_redis3.redisKeys.runtimeSignalBucket(
4398
- userName,
4399
- (0, import_time2.getRuntimeStorageDayKey)(signal.timestamp),
4400
- signal.strategy
4401
- );
4402
- const entries = signalRefsByBucket.get(bucket) ?? [];
4403
- entries.push({
4404
- field: signal.signalId,
4405
- data: toRuntimeSignalBucketRef(signal)
4406
- });
4407
- signalRefsByBucket.set(bucket, entries);
4408
- }
4409
4449
  await Promise.all([
4410
4450
  flushRuntimeSignalEvaluations(pending),
4411
- ...pendingSignals.map(
4412
- ({ signal }) => (0, import_redis3.setData)(
4413
- import_redis3.redisKeys.storeSignal(signal.symbol, signal.signalId),
4414
- toStoredRuntimeSignal(signal),
4415
- { expire: runtimeSignalRetentionTtl }
4416
- )
4417
- ),
4418
- ...[...signalRefsByBucket].map(
4419
- ([bucket, entries]) => (0, import_redis3.setHashJsonFields)(bucket, entries, {
4420
- expire: runtimeSignalRetentionTtl
4421
- })
4422
- )
4451
+ flushSignals()
4423
4452
  ]);
4424
4453
  }
4425
4454
  };
@@ -4846,6 +4875,7 @@ var createSignalsTickerEvaluator = ({
4846
4875
  strategyName,
4847
4876
  configId,
4848
4877
  strategyCreator,
4878
+ releaseCompositionId,
4849
4879
  sourceStrategyConfig,
4850
4880
  strategyConfig,
4851
4881
  strategyResults
@@ -4860,6 +4890,7 @@ var createSignalsTickerEvaluator = ({
4860
4890
  const runtimeLineage = await buildRuntimeLineage({
4861
4891
  projectRoot,
4862
4892
  strategyName,
4893
+ compositionId: releaseCompositionId,
4863
4894
  config: { configId, strategyConfig, symbolResultConfig },
4864
4895
  runContext: {
4865
4896
  connectorName: connectorName.toLowerCase(),
@@ -5439,6 +5470,7 @@ var createSignalsRunner = (config2) => {
5439
5470
  if (config2.notify) {
5440
5471
  const telegramSignals = getTelegramDeliverableSignals(signals3);
5441
5472
  if (!config2.skipScreenshots && telegramSignals.length > 0) {
5473
+ await runtimeSignalEvaluations.flushSignals();
5442
5474
  await (0, import_cli2.makeScreenshots)(telegramSignals, interval2, config2.userName);
5443
5475
  }
5444
5476
  await (0, import_cli2.sendToTG)(telegramSignals, interval2, config2.userName);