@tradejs/cli 3.1.8-beta.213 → 3.1.9-beta.214

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.
@@ -72,6 +72,57 @@ var import_connectors3 = require("@tradejs/node/connectors");
72
72
  var import_cli3 = require("@tradejs/node/cli");
73
73
  var import_constants = require("@tradejs/core/constants");
74
74
  var import_time2 = require("@tradejs/core/time");
75
+ var import_runtimeStrategies = require("@tradejs/node/runtimeStrategies");
76
+
77
+ // src/lib/runtimeStrategyBacktest.ts
78
+ var import_chalk = __toESM(require("chalk"));
79
+
80
+ // src/lib/runtimeRedis.ts
81
+ var import_redis = require("@tradejs/infra/redis");
82
+ var import_time = require("@tradejs/core/time");
83
+ var import_runtimeStrategyConfigs = require("@tradejs/infra/runtimeStrategyConfigs");
84
+ var isRuntimeTradeRecord = (value) => {
85
+ if (!value || typeof value !== "object") {
86
+ return false;
87
+ }
88
+ const record = value;
89
+ return typeof record.orderId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.entryTimestamp === "number" && typeof record.entryPrice === "number" && typeof record.qty === "number" && (record.direction === "LONG" || record.direction === "SHORT");
90
+ };
91
+ var loadRuntimeTrades = async (userName, {
92
+ startTime,
93
+ endTime
94
+ } = {}) => {
95
+ const hasWindow = Number.isFinite(startTime) && Number.isFinite(endTime) && startTime != null && endTime != null;
96
+ const filterByWindow = (trade) => !hasWindow || trade.entryTimestamp >= startTime && trade.entryTimestamp <= endTime;
97
+ if (hasWindow) {
98
+ const dayKeys = (0, import_time.getRuntimeStorageDayKeys)(startTime, endTime);
99
+ const bucketTrades = (await Promise.all(
100
+ dayKeys.map(
101
+ (dayKey) => (0, import_redis.getHashJsonValues)(
102
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey)
103
+ )
104
+ )
105
+ )).flat();
106
+ const dedupedBucketTrades = /* @__PURE__ */ new Map();
107
+ for (const trade of bucketTrades) {
108
+ if (!isRuntimeTradeRecord(trade)) {
109
+ continue;
110
+ }
111
+ dedupedBucketTrades.set(trade.orderId, trade);
112
+ }
113
+ const windowedTrades = [...dedupedBucketTrades.values()].filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
114
+ if (windowedTrades.length > 0 || dayKeys.length === 0) {
115
+ return windowedTrades;
116
+ }
117
+ }
118
+ const tradePrefix = import_redis.redisKeys.runtimeTrades(userName);
119
+ const bucketPrefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
120
+ const keys = (await (0, import_redis.getKeys)(tradePrefix)).filter(
121
+ (key) => !key.startsWith(bucketPrefix)
122
+ );
123
+ const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
124
+ return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
125
+ };
75
126
 
76
127
  // src/lib/timeWindow.ts
77
128
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
@@ -175,95 +226,6 @@ var resolveTimeWindow = ({
175
226
  );
176
227
  };
177
228
 
178
- // src/lib/runtimeStrategyBacktest.ts
179
- var import_chalk = __toESM(require("chalk"));
180
-
181
- // src/lib/runtimeRedis.ts
182
- var import_redis = require("@tradejs/infra/redis");
183
- var import_time = require("@tradejs/core/time");
184
- var import_runtimeStrategyConfigs = require("@tradejs/infra/runtimeStrategyConfigs");
185
- var isRuntimeStrategyEnabled = (strategyConfig) => {
186
- const enabled = strategyConfig.ENABLE;
187
- return enabled !== false;
188
- };
189
- var isRuntimeTradeRecord = (value) => {
190
- if (!value || typeof value !== "object") {
191
- return false;
192
- }
193
- const record = value;
194
- return typeof record.orderId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.entryTimestamp === "number" && typeof record.entryPrice === "number" && typeof record.qty === "number" && (record.direction === "LONG" || record.direction === "SHORT");
195
- };
196
- var loadRuntimeTrades = async (userName, {
197
- startTime,
198
- endTime
199
- } = {}) => {
200
- const hasWindow = Number.isFinite(startTime) && Number.isFinite(endTime) && startTime != null && endTime != null;
201
- const filterByWindow = (trade) => !hasWindow || trade.entryTimestamp >= startTime && trade.entryTimestamp <= endTime;
202
- if (hasWindow) {
203
- const dayKeys = (0, import_time.getRuntimeStorageDayKeys)(startTime, endTime);
204
- const bucketTrades = (await Promise.all(
205
- dayKeys.map(
206
- (dayKey) => (0, import_redis.getHashJsonValues)(
207
- import_redis.redisKeys.runtimeTradeBucket(userName, dayKey)
208
- )
209
- )
210
- )).flat();
211
- const dedupedBucketTrades = /* @__PURE__ */ new Map();
212
- for (const trade of bucketTrades) {
213
- if (!isRuntimeTradeRecord(trade)) {
214
- continue;
215
- }
216
- dedupedBucketTrades.set(trade.orderId, trade);
217
- }
218
- const windowedTrades = [...dedupedBucketTrades.values()].filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
219
- if (windowedTrades.length > 0 || dayKeys.length === 0) {
220
- return windowedTrades;
221
- }
222
- }
223
- const tradePrefix = import_redis.redisKeys.runtimeTrades(userName);
224
- const bucketPrefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
225
- const keys = (await (0, import_redis.getKeys)(tradePrefix)).filter(
226
- (key) => !key.startsWith(bucketPrefix)
227
- );
228
- const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
229
- return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
230
- };
231
-
232
- // src/lib/runtimeStrategyBacktest.ts
233
- var BACKTEST_CLI_RUNTIME_CONFIG_KEYS = /* @__PURE__ */ new Set([
234
- "ENV",
235
- "INTERVAL",
236
- "MAKE_ORDERS",
237
- "CLOSE_OPPOSITE_POSITIONS",
238
- "BACKTEST_PRICE_MODE",
239
- "BACKTEST_ENTRY_DELAY_BARS"
240
- ]);
241
- var toStrategyConfigGrid = (strategyConfig) => Object.fromEntries(
242
- Object.entries(strategyConfig).filter(([key]) => !BACKTEST_CLI_RUNTIME_CONFIG_KEYS.has(key)).map(([key, value]) => [key, [value]])
243
- );
244
- var loadRuntimeStrategyBacktestConfigs = async (userName) => {
245
- const configs = await (0, import_runtimeStrategyConfigs.loadRuntimeStrategyConfigs)(userName, {
246
- onInvalidConfig: (key) => {
247
- console.log(import_chalk.default.yellow(`Skip invalid runtime strategy config: ${key}`));
248
- }
249
- });
250
- return configs.filter(({ configId }) => configId === "config").filter(({ strategyName, strategyConfig }) => {
251
- const enabled = isRuntimeStrategyEnabled(strategyConfig);
252
- if (!enabled) {
253
- console.log(
254
- import_chalk.default.yellow(
255
- `Skip inactive runtime strategy config by ENABLE=false: ${strategyName}`
256
- )
257
- );
258
- }
259
- return enabled;
260
- }).map(({ strategyName, strategyConfig }) => ({
261
- strategyName,
262
- strategyConfig,
263
- backtestConfig: toStrategyConfigGrid(strategyConfig)
264
- }));
265
- };
266
-
267
229
  // src/lib/marketData/historyPrepare.ts
268
230
  var import_chalk2 = __toESM(require("chalk"));
269
231
  var import_connectors = require("@tradejs/connectors");
@@ -537,17 +499,43 @@ var resolveRunConnectorName = async ({
537
499
  );
538
500
  return import_connectors3.DEFAULT_CONNECTOR_NAME;
539
501
  };
540
- var loadReplayStrategies = async (userName) => {
541
- const runtimeStrategies = await loadRuntimeStrategyBacktestConfigs(userName);
542
- if (!runtimeStrategies.length) {
502
+ var loadDeploymentReplayStrategies = async ({
503
+ userName,
504
+ projectRoot,
505
+ deploymentId
506
+ }) => {
507
+ const [deployment, runtimeStrategies] = await Promise.all([
508
+ (0, import_runtimeStrategies.getRuntimeDeployment)({ userName, projectRoot, deploymentId }),
509
+ (0, import_runtimeStrategies.loadResolvedRuntimeStrategies)({ userName, projectRoot, deploymentId })
510
+ ]);
511
+ if (!deployment) {
512
+ throw new Error(`Runtime deployment not found: ${deploymentId}`);
513
+ }
514
+ const strategies = deployment.enabled ? runtimeStrategies.filter((strategy) => strategy.enabled).map(
515
+ ({
516
+ strategyName,
517
+ version,
518
+ strategyPackage,
519
+ strategyPackageVersion,
520
+ runtimePackageVersion,
521
+ sourceStrategyConfig
522
+ }) => ({
523
+ strategyName,
524
+ version,
525
+ strategyPackage,
526
+ strategyPackageVersion,
527
+ runtimePackageVersion,
528
+ strategyConfig: sourceStrategyConfig
529
+ })
530
+ ) : [];
531
+ if (!strategies.length) {
543
532
  console.log(
544
533
  import_chalk3.default.yellow(
545
- `No active runtime strategy configs found by users:${userName}:strategies:*:config`
534
+ `No enabled strategies found in runtime deployment ${deploymentId}`
546
535
  )
547
536
  );
548
- return [];
549
537
  }
550
- return runtimeStrategies;
538
+ return { deployment, strategies };
551
539
  };
552
540
  var prepareRunEnvironment = async ({
553
541
  connector,
@@ -4304,6 +4292,11 @@ import_args.default.option(
4304
4292
  "runtimeEvidence",
4305
4293
  "Immutable runtime evidence JSON used for offline runtime comparison"
4306
4294
  );
4295
+ import_args.default.option(
4296
+ "deployment",
4297
+ "Git-owned runtime deployment used when runtime evidence is omitted",
4298
+ "production"
4299
+ );
4307
4300
  import_args.default.option(["u", "updateOnly"], "Only update tickers history", false);
4308
4301
  import_args.default.option(["C", "cacheOnly"], "Do not update tickers history", false);
4309
4302
  import_args.default.option(
@@ -4330,97 +4323,673 @@ var replayInterval = String(replayFlags.timeframe ?? 15);
4330
4323
  var replayUserName = String(replayFlags.user ?? "root");
4331
4324
  var replayProjectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
4332
4325
  var replayRuntimeEvidencePath = typeof replayFlags.runtimeEvidence === "string" && replayFlags.runtimeEvidence.trim() ? replayFlags.runtimeEvidence.trim() : null;
4326
+ var replayDeploymentId = String(
4327
+ replayFlags.deployment ?? "production"
4328
+ ).trim();
4333
4329
  var isReplayUpdateOnlyRun = Boolean(replayFlags.updateOnly);
4334
4330
 
4335
- // src/lib/replay/support.ts
4336
- var import_chalk8 = __toESM(require("chalk"));
4331
+ // src/lib/replay/runtimeEvidenceSource.ts
4332
+ var import_promises3 = __toESM(require("fs/promises"));
4333
+ var import_node_path3 = __toESM(require("path"));
4337
4334
 
4338
- // src/lib/runtimeModeConfig.ts
4339
- var buildRuntimeModeStrategyConfig = ({
4340
- strategyConfig,
4341
- env,
4342
- interval,
4343
- makeOrders,
4344
- recordRuntimeTrades,
4345
- aiReplayAnalyses
4346
- }) => ({
4347
- ...strategyConfig,
4348
- ENV: env,
4349
- MAKE_ORDERS: makeOrders,
4350
- INTERVAL: strategyConfig.INTERVAL ?? interval,
4351
- ...typeof recordRuntimeTrades === "boolean" ? { RECORD_RUNTIME_TRADES: recordRuntimeTrades } : {},
4352
- ...aiReplayAnalyses?.length ? { AI_REPLAY_ANALYSES: aiReplayAnalyses } : {}
4353
- });
4335
+ // src/lib/runtimeLineage.ts
4336
+ var import_node_child_process = require("child_process");
4337
+ var import_node_crypto2 = require("crypto");
4338
+ var import_promises2 = __toESM(require("fs/promises"));
4339
+ var import_node_path2 = __toESM(require("path"));
4340
+ var import_types2 = require("@tradejs/types");
4341
+ var import_strategies3 = require("@tradejs/node/strategies");
4342
+ var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
4354
4343
 
4355
- // src/lib/replay/support.ts
4356
- var REPLAY_RESULTS_BY_STRATEGY_HEADERS = [
4357
- import_chalk8.default.blue("STRATEGY"),
4358
- import_chalk8.default.yellow("TICKERS"),
4359
- import_chalk8.default.yellow("TRADE TICKERS"),
4360
- import_chalk8.default.cyan("ORDERS"),
4361
- import_chalk8.default.cyan("WIN/LOSS (%)"),
4362
- import_chalk8.default.cyan("PROFIT"),
4363
- import_chalk8.default.cyan("AVG TRADE")
4364
- ];
4365
- var REPLAY_RUNTIME_COMPARISON_HEADERS = [
4366
- import_chalk8.default.blue("STRATEGY"),
4367
- import_chalk8.default.cyan("BT ENTRIES"),
4368
- import_chalk8.default.cyan("BT PNL"),
4369
- import_chalk8.default.yellow("RT TRADES"),
4370
- import_chalk8.default.yellow("RT PNL"),
4371
- import_chalk8.default.green("MATCHED"),
4372
- import_chalk8.default.red("ORDER FAILED"),
4373
- import_chalk8.default.yellow("RT ONLY"),
4374
- import_chalk8.default.magenta("BT ONLY"),
4375
- import_chalk8.default.red("UNMATCHED")
4344
+ // src/lib/strategyGateFingerprint.ts
4345
+ var import_node_crypto = require("crypto");
4346
+ var import_node_module = require("module");
4347
+ var import_promises = __toESM(require("fs/promises"));
4348
+ var import_node_path = __toESM(require("path"));
4349
+ var toPackageToken = (strategyName) => strategyName.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase();
4350
+ var getStrategyPackageName = (strategyName) => strategyName === "TrendLine" || strategyName === "ReverseTrendLine" ? "@tradejs/strategy-trend-line" : `@tradejs/strategy-${toPackageToken(strategyName)}`;
4351
+ var gateFingerprintSpecifiers = (strategyName) => [
4352
+ getStrategyPackageName(strategyName),
4353
+ "@tradejs/strategy-kit/ai-gate",
4354
+ "@tradejs/node/ai",
4355
+ "@tradejs/core/strategies"
4376
4356
  ];
4377
- var REPLAY_RESULTS_CONFIG = "replay";
4378
- var REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS = 1;
4379
- var REPLAY_RUNTIME_COMPARE_TOLERANCE_MS = REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS * 15 * 60 * 1e3;
4380
- var formatReplayRuntimeCompareTolerance = () => `${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS} ${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS === 1 ? "bar" : "bars"}`;
4381
- var getReplayRuntimeUnmatchedCount = (row) => row.orderFailed + row.runtimeOnly + row.backtestOnly;
4382
- var buildReplayStrategyConfig = ({
4383
- strategyConfig,
4384
- interval
4385
- }) => buildRuntimeModeStrategyConfig({
4386
- strategyConfig,
4387
- env: "PARITY",
4388
- interval,
4389
- makeOrders: true,
4390
- recordRuntimeTrades: false
4391
- });
4392
- var toRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
4393
- var compactReplayAdditionalIndicators = (additionalIndicators) => {
4394
- const additional = toRecord(additionalIndicators);
4395
- if (!additional) {
4396
- return void 0;
4397
- }
4398
- const compact = {};
4399
- if (additional.executionSlippage != null) {
4400
- compact.executionSlippage = additional.executionSlippage;
4401
- }
4402
- if (additional.marketContext != null) {
4403
- compact.marketContext = additional.marketContext;
4404
- }
4405
- const baseContext = toRecord(additional.baseContext);
4406
- const relative = toRecord(baseContext?.relative);
4407
- const execution = toRecord(relative?.execution);
4408
- if (execution) {
4409
- compact.baseContext = {
4410
- relative: {
4411
- execution
4357
+ var resolveStrategyGateFingerprint = async ({
4358
+ projectRoot,
4359
+ strategyName,
4360
+ gitSha
4361
+ }) => {
4362
+ const projectManifestPath = import_node_path.default.join(projectRoot, "package.json");
4363
+ const projectExists = await import_promises.default.access(projectManifestPath).then(() => true).catch(() => false);
4364
+ const requireFromProject = projectExists ? (0, import_node_module.createRequire)(projectManifestPath) : null;
4365
+ const candidates = requireFromProject ? await Promise.all(
4366
+ gateFingerprintSpecifiers(strategyName).map(async (specifier) => {
4367
+ let resolvedPath;
4368
+ try {
4369
+ resolvedPath = requireFromProject.resolve(specifier);
4370
+ } catch {
4371
+ return null;
4412
4372
  }
4413
- };
4414
- }
4415
- return Object.keys(compact).length ? compact : void 0;
4416
- };
4417
- var compactReplaySignal = (signal) => {
4418
- if (!signal) {
4419
- return void 0;
4420
- }
4421
- return {
4422
- signalId: signal.signalId,
4423
- strategy: signal.strategy,
4373
+ const content = await import_promises.default.readFile(resolvedPath).catch(() => null);
4374
+ return content == null ? null : { specifier, content };
4375
+ })
4376
+ ) : [];
4377
+ const sourceEntries = candidates.filter(
4378
+ (entry) => entry != null
4379
+ );
4380
+ const hash = (0, import_node_crypto.createHash)("sha256");
4381
+ hash.update(strategyName);
4382
+ if (sourceEntries.length) {
4383
+ for (const entry of sourceEntries) {
4384
+ hash.update(entry.specifier);
4385
+ hash.update(entry.content);
4386
+ }
4387
+ } else {
4388
+ hash.update(gitSha ?? "unknown-git-sha");
4389
+ }
4390
+ return {
4391
+ gateFingerprint: hash.digest("hex").slice(0, 16),
4392
+ gateFingerprintFiles: sourceEntries.map((entry) => entry.specifier)
4393
+ };
4394
+ };
4395
+
4396
+ // src/lib/runtimeLineage.ts
4397
+ var RUNTIME_CONTEXT_ENV_KEYS = [
4398
+ "AI_MODE",
4399
+ "MIN_AI_QUALITY",
4400
+ "INTERVAL",
4401
+ "DERIVATIVES_CONTEXT_ENABLED",
4402
+ "DERIVATIVES_CONTEXT_TARGET_ENABLED",
4403
+ "DERIVATIVES_CONTEXT_LOOKBACK_HOURS",
4404
+ "DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS",
4405
+ "DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY",
4406
+ "COINMARKETCAP_CONTEXT_ENABLED",
4407
+ "COINMARKETCAP_CONTEXT_MAX_AGE_MS",
4408
+ "COINMARKETCAP_CONTEXT_EXCHANGE_LIQUIDITY_ENABLED",
4409
+ "COINMARKETCAP_CONTEXT_EXCHANGE_SLUGS",
4410
+ "COINMARKETCAP_CONTEXT_FEAR_GREED_ENABLED",
4411
+ "COINMARKETCAP_CONTEXT_HISTORICAL_ACCESS_MONTHS",
4412
+ "COINMARKETCAP_CONTEXT_BACKFILL_ENABLED",
4413
+ "COINMARKETCAP_CONTEXT_BACKFILL_MAX_DAYS",
4414
+ "COINMARKETCAP_CONTEXT_BACKFILL_WARMUP_DAYS",
4415
+ "HYPERLIQUID_WHALE_CONTEXT_ENABLED",
4416
+ "HYPERLIQUID_WHALE_BACKFILL_ENABLED",
4417
+ "HYPERLIQUID_WHALE_MIN_COVERAGE_PCT",
4418
+ "HYPERLIQUID_WHALE_CONCURRENCY",
4419
+ "HYPERLIQUID_WHALE_RATE_LIMIT_WEIGHT"
4420
+ ];
4421
+ var RUNTIME_CONTEXT_DATA_MODEL = {
4422
+ derivativesSourceIntervals: "15m",
4423
+ derivativesDerivedIntervals: "1h",
4424
+ derivativesHourlyFallback: "stored-1h",
4425
+ derivativesDataModelVersion: 2,
4426
+ coinMarketCapAvailabilityModel: "closed-interval-all-daily",
4427
+ coinMarketCapDataModelVersion: 3,
4428
+ binanceBreadthUniverseModel: "fixed-versioned-top5-top10-top30-top50-top100",
4429
+ binanceBreadthDataModelVersion: 2,
4430
+ hyperliquidWhaleCanonicalInterval: "1m",
4431
+ hyperliquidWhaleGateMinNotionalUsd: 5e4,
4432
+ hyperliquidWhaleMinCoveragePct: 0.8,
4433
+ hyperliquidWhaleDataModelVersion: import_types2.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
4434
+ };
4435
+ var normalizeForStableJson = (value) => {
4436
+ if (Array.isArray(value)) {
4437
+ return value.map(normalizeForStableJson);
4438
+ }
4439
+ if (value && typeof value === "object") {
4440
+ return Object.fromEntries(
4441
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
4442
+ );
4443
+ }
4444
+ return value;
4445
+ };
4446
+ var resolveRuntimeMaxLossValue = (config) => {
4447
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
4448
+ return null;
4449
+ }
4450
+ const strategyConfig = config.strategyConfig;
4451
+ if (!strategyConfig || typeof strategyConfig !== "object" || Array.isArray(strategyConfig)) {
4452
+ return null;
4453
+ }
4454
+ const maxLossValue = Number(
4455
+ strategyConfig.MAX_LOSS_VALUE
4456
+ );
4457
+ return Number.isFinite(maxLossValue) ? maxLossValue : null;
4458
+ };
4459
+ var fingerprintRuntimeValue = (value) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
4460
+ var gitLineageCache = /* @__PURE__ */ new Map();
4461
+ var gateFingerprintCache = /* @__PURE__ */ new Map();
4462
+ var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
4463
+ var snapshotFingerprintCache = /* @__PURE__ */ new Map();
4464
+ var resolveGitLineage = (projectRoot) => {
4465
+ const cached = gitLineageCache.get(projectRoot);
4466
+ if (cached) {
4467
+ return cached;
4468
+ }
4469
+ const envSha = String(process.env.TRADEJS_GIT_SHA ?? "").trim();
4470
+ if (envSha && envSha !== "unknown") {
4471
+ const result = {
4472
+ gitSha: envSha,
4473
+ gitDirty: false
4474
+ };
4475
+ gitLineageCache.set(projectRoot, result);
4476
+ return result;
4477
+ }
4478
+ try {
4479
+ const gitSha = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "HEAD"], {
4480
+ cwd: projectRoot,
4481
+ encoding: "utf8",
4482
+ stdio: ["ignore", "pipe", "ignore"]
4483
+ }).trim();
4484
+ const status = (0, import_node_child_process.execFileSync)(
4485
+ "git",
4486
+ ["status", "--porcelain", "--untracked-files=normal"],
4487
+ {
4488
+ cwd: projectRoot,
4489
+ encoding: "utf8",
4490
+ stdio: ["ignore", "pipe", "ignore"]
4491
+ }
4492
+ ).trim();
4493
+ const result = {
4494
+ gitSha: gitSha || null,
4495
+ gitDirty: status.length > 0
4496
+ };
4497
+ gitLineageCache.set(projectRoot, result);
4498
+ return result;
4499
+ } catch {
4500
+ const result = {
4501
+ gitSha: null,
4502
+ gitDirty: null
4503
+ };
4504
+ gitLineageCache.set(projectRoot, result);
4505
+ return result;
4506
+ }
4507
+ };
4508
+ var readOptionalFile = async (filePath) => {
4509
+ try {
4510
+ return await import_promises2.default.readFile(filePath);
4511
+ } catch {
4512
+ return null;
4513
+ }
4514
+ };
4515
+ var resolveBinanceBreadthFingerprint = async (projectRoot) => {
4516
+ if (binanceBreadthFingerprintCache.has(projectRoot)) {
4517
+ return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
4518
+ }
4519
+ const content = await readOptionalFile(
4520
+ import_node_path2.default.join(
4521
+ projectRoot,
4522
+ "packages/node/src/config/binanceBreadthUniverses.json"
4523
+ )
4524
+ );
4525
+ let result = null;
4526
+ if (content) {
4527
+ try {
4528
+ const parsed = JSON.parse(content.toString("utf8"));
4529
+ result = typeof parsed.fingerprint === "string" && parsed.fingerprint.trim() ? parsed.fingerprint.trim() : null;
4530
+ } catch {
4531
+ result = null;
4532
+ }
4533
+ }
4534
+ binanceBreadthFingerprintCache.set(projectRoot, result);
4535
+ return result;
4536
+ };
4537
+ var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
4538
+ const cacheKey = `${projectRoot}:${relativePath}`;
4539
+ if (snapshotFingerprintCache.has(cacheKey)) {
4540
+ return snapshotFingerprintCache.get(cacheKey) ?? null;
4541
+ }
4542
+ const content = await readOptionalFile(import_node_path2.default.join(projectRoot, relativePath));
4543
+ let result = null;
4544
+ if (content) {
4545
+ try {
4546
+ const parsed = JSON.parse(content.toString("utf8"));
4547
+ result = typeof parsed.fingerprint === "string" && parsed.fingerprint.trim() ? parsed.fingerprint.trim() : null;
4548
+ } catch {
4549
+ result = null;
4550
+ }
4551
+ }
4552
+ snapshotFingerprintCache.set(cacheKey, result);
4553
+ return result;
4554
+ };
4555
+ var resolveRuntimeGateFingerprint = async ({
4556
+ projectRoot,
4557
+ strategyName,
4558
+ gitSha
4559
+ }) => {
4560
+ const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
4561
+ const cached = gateFingerprintCache.get(cacheKey);
4562
+ if (cached) return cached;
4563
+ const result = await resolveStrategyGateFingerprint({
4564
+ projectRoot,
4565
+ strategyName,
4566
+ gitSha
4567
+ });
4568
+ gateFingerprintCache.set(cacheKey, result);
4569
+ return result;
4570
+ };
4571
+ async function buildRuntimeLineage({
4572
+ projectRoot,
4573
+ strategyName,
4574
+ compositionId,
4575
+ version,
4576
+ strategyPackageVersion,
4577
+ runtimePackageVersion,
4578
+ config,
4579
+ runContext,
4580
+ env = process.env
4581
+ }) {
4582
+ if (version != null) {
4583
+ if (!Number.isSafeInteger(version) || version <= 0) {
4584
+ throw new Error(`Invalid runtime strategy version: ${version}`);
4585
+ }
4586
+ return {
4587
+ schemaVersion: 2,
4588
+ version,
4589
+ strategyPackageVersion,
4590
+ runtimePackageVersion,
4591
+ maxLossValue: resolveRuntimeMaxLossValue(config)
4592
+ };
4593
+ }
4594
+ const git = resolveGitLineage(projectRoot);
4595
+ const hyperliquidPerpUniverseFingerprint = typeof import_strategies3.getHyperliquidPerpUniverseSnapshot === "function" ? (0, import_strategies3.getHyperliquidPerpUniverseSnapshot)().fingerprint : await resolveSnapshotFingerprint(
4596
+ projectRoot,
4597
+ "packages/node/src/config/hyperliquidPerpUniverse.json"
4598
+ );
4599
+ const hyperliquidWhaleRegistryFingerprint = typeof import_strategies3.getHyperliquidWhaleRegistrySnapshot === "function" ? (0, import_strategies3.getHyperliquidWhaleRegistrySnapshot)().fingerprint : await resolveSnapshotFingerprint(
4600
+ projectRoot,
4601
+ "packages/node/src/config/hyperliquidWhales.json"
4602
+ );
4603
+ const context = {
4604
+ ...Object.fromEntries(
4605
+ RUNTIME_CONTEXT_ENV_KEYS.map((key) => [key, env[key] ?? null])
4606
+ ),
4607
+ ...RUNTIME_CONTEXT_DATA_MODEL,
4608
+ binanceBreadthUniverseFingerprint: await resolveBinanceBreadthFingerprint(projectRoot),
4609
+ hyperliquidPerpUniverseFingerprint,
4610
+ hyperliquidWhaleRegistryFingerprint,
4611
+ ...runContext
4612
+ };
4613
+ return {
4614
+ schemaVersion: 1,
4615
+ compositionId: compositionId?.trim() || null,
4616
+ ...git,
4617
+ gateFingerprint: (await resolveRuntimeGateFingerprint({
4618
+ projectRoot,
4619
+ strategyName,
4620
+ gitSha: git.gitSha
4621
+ })).gateFingerprint,
4622
+ // Position risk is tracked separately so changing trade size does not
4623
+ // masquerade as a change to the core + gate decision logic.
4624
+ configFingerprint: (0, import_strategyReleaseEvidence.strategyLogicConfigFingerprint)(config),
4625
+ contextFingerprint: fingerprintRuntimeValue(context),
4626
+ maxLossValue: resolveRuntimeMaxLossValue(config)
4627
+ };
4628
+ }
4629
+ var runtimeLineageKey = (lineage) => lineage.schemaVersion === 2 ? [
4630
+ "v2",
4631
+ lineage.version,
4632
+ lineage.strategyPackageVersion ?? "strategy:unknown",
4633
+ lineage.runtimePackageVersion ?? "runtime:unknown"
4634
+ ].join(":") : [
4635
+ lineage.schemaVersion,
4636
+ lineage.compositionId ?? "unbound-composition",
4637
+ lineage.gitSha ?? "unknown",
4638
+ lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
4639
+ lineage.gateFingerprint,
4640
+ lineage.configFingerprint,
4641
+ lineage.contextFingerprint
4642
+ ].join(":");
4643
+ var runtimeLineagesMatch = (left, right) => left != null && right != null && runtimeLineageKey(left) === runtimeLineageKey(right);
4644
+ var runtimeLineagesComparable = (left, right) => runtimeLineagesMatch(left, right) && typeof left?.maxLossValue === "number" && Number.isFinite(left.maxLossValue) && typeof right?.maxLossValue === "number" && Number.isFinite(right.maxLossValue) && left.maxLossValue === right.maxLossValue;
4645
+
4646
+ // src/lib/runtimeEvidenceDeployment.ts
4647
+ var import_runtimeStrategies2 = require("@tradejs/node/runtimeStrategies");
4648
+ var isRecord = (value) => value != null && typeof value === "object" && !Array.isArray(value);
4649
+ var isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
4650
+ var INTERVALS = /* @__PURE__ */ new Set([
4651
+ "1",
4652
+ "3",
4653
+ "5",
4654
+ "15",
4655
+ "30",
4656
+ "60",
4657
+ "120",
4658
+ "240",
4659
+ "360",
4660
+ "720",
4661
+ "D",
4662
+ "W",
4663
+ "M"
4664
+ ]);
4665
+ var UNIVERSES = /* @__PURE__ */ new Set(["crypto", "tradfi"]);
4666
+ var parseRuntimeEvidenceDeploymentSnapshot = (value) => {
4667
+ if (!isRecord(value) || value.schemaVersion !== 1 || !isNonEmptyString(value.id) || !isNonEmptyString(value.label) || !isNonEmptyString(value.connectorName) || !isNonEmptyString(value.provider) || !isNonEmptyString(value.accountId) || typeof value.enabled !== "boolean" || !Array.isArray(value.strategies)) {
4668
+ throw new Error(
4669
+ "Runtime evidence deployment snapshot is missing or invalid"
4670
+ );
4671
+ }
4672
+ const strategies = value.strategies.map((candidate) => {
4673
+ if (!isRecord(candidate) || !isNonEmptyString(candidate.strategyName) || !Number.isSafeInteger(candidate.version) || Number(candidate.version) <= 0 || typeof candidate.enabled !== "boolean" || !["active", "entries_paused"].includes(String(candidate.controlState)) || !INTERVALS.has(String(candidate.interval)) || !UNIVERSES.has(String(candidate.universe)) || !isNonEmptyString(candidate.strategyPackage) || !isNonEmptyString(candidate.strategyPackageVersion) || !isNonEmptyString(candidate.runtimePackageVersion) || !isRecord(candidate.strategyConfig)) {
4674
+ throw new Error(
4675
+ "Runtime evidence deployment strategy snapshot is invalid"
4676
+ );
4677
+ }
4678
+ if (String(candidate.strategyConfig.INTERVAL) !== String(candidate.interval) || String(candidate.strategyConfig.UNIVERSE) !== String(candidate.universe)) {
4679
+ throw new Error(
4680
+ `Runtime evidence strategy config does not match its scope: ${candidate.strategyName}`
4681
+ );
4682
+ }
4683
+ if (candidate.accountId != null && (!isNonEmptyString(candidate.accountId) || candidate.accountId !== value.accountId)) {
4684
+ throw new Error(
4685
+ `Runtime evidence strategy account does not match deployment: ${candidate.strategyName}`
4686
+ );
4687
+ }
4688
+ return candidate;
4689
+ });
4690
+ const identities = /* @__PURE__ */ new Set();
4691
+ for (const strategy of strategies) {
4692
+ if (identities.has(strategy.strategyName)) {
4693
+ throw new Error(
4694
+ `Duplicate runtime evidence strategy: ${strategy.strategyName}`
4695
+ );
4696
+ }
4697
+ identities.add(strategy.strategyName);
4698
+ }
4699
+ return {
4700
+ ...value,
4701
+ strategies
4702
+ };
4703
+ };
4704
+ var activeRuntimeEvidenceStrategies = (deployment) => deployment.enabled ? deployment.strategies.filter((strategy) => strategy.enabled) : [];
4705
+ var runtimeDeploymentFromEvidence = (deployment) => ({
4706
+ id: deployment.id,
4707
+ label: deployment.label,
4708
+ connectorName: deployment.connectorName,
4709
+ provider: deployment.provider,
4710
+ accountId: deployment.accountId,
4711
+ enabled: deployment.enabled,
4712
+ strategies: deployment.strategies.map(
4713
+ ({ strategyName, version, enabled, controlState }) => ({
4714
+ strategyName,
4715
+ version,
4716
+ enabled,
4717
+ controlState
4718
+ })
4719
+ ),
4720
+ ...deployment.assetClasses ? { assetClasses: [...deployment.assetClasses] } : {},
4721
+ ...deployment.tickers ? { tickers: [...deployment.tickers] } : {}
4722
+ });
4723
+
4724
+ // src/lib/replay/runtimeEvidenceSource.ts
4725
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
4726
+ var asFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
4727
+ var unwrapRows = (value, field) => (Array.isArray(value) ? value : []).map((item) => {
4728
+ const row = asRecord(item);
4729
+ return asRecord(row?.[field]) ?? row;
4730
+ }).filter((item) => item != null);
4731
+ var assertRuntimeRowsMatchDeployment = ({
4732
+ label,
4733
+ rows,
4734
+ deployment
4735
+ }) => {
4736
+ const activeStrategies = new Set(
4737
+ activeRuntimeEvidenceStrategies(deployment).map(
4738
+ ({ strategyName }) => strategyName
4739
+ )
4740
+ );
4741
+ for (const row of rows) {
4742
+ if (row.deploymentId !== deployment.id || row.accountId !== deployment.accountId || !row.strategy || !activeStrategies.has(row.strategy)) {
4743
+ throw new Error(
4744
+ `Runtime evidence ${label} row is outside embedded deployment composition`
4745
+ );
4746
+ }
4747
+ }
4748
+ };
4749
+ var readRuntimeEvidenceArtifact = async ({
4750
+ filePath,
4751
+ projectRoot
4752
+ }) => {
4753
+ const resolvedPath = import_node_path3.default.resolve(projectRoot, filePath);
4754
+ const root = asRecord(
4755
+ JSON.parse(await import_promises3.default.readFile(resolvedPath, "utf8"))
4756
+ );
4757
+ const runtime = asRecord(root?.runtime) ?? root;
4758
+ if (!root || !runtime) {
4759
+ throw new Error(`Invalid runtime evidence JSON: ${resolvedPath}`);
4760
+ }
4761
+ const rootWindow = asRecord(root.window);
4762
+ const runtimeWindow = asRecord(runtime.window);
4763
+ return {
4764
+ resolvedPath,
4765
+ root,
4766
+ runtime,
4767
+ userName: String(root.userName ?? runtime.userName ?? "").trim(),
4768
+ startTime: asFiniteNumber(rootWindow?.startTime) ?? asFiniteNumber(runtimeWindow?.startTime),
4769
+ endTime: asFiniteNumber(rootWindow?.endTime) ?? asFiniteNumber(runtimeWindow?.endTime),
4770
+ deployment: parseRuntimeEvidenceDeploymentSnapshot(root.deployment)
4771
+ };
4772
+ };
4773
+ var loadReplayRuntimeEvidenceMetadata = async (params) => {
4774
+ const artifact = await readRuntimeEvidenceArtifact(params);
4775
+ return {
4776
+ path: artifact.resolvedPath,
4777
+ userName: artifact.userName,
4778
+ startTime: artifact.startTime,
4779
+ endTime: artifact.endTime,
4780
+ deployment: artifact.deployment
4781
+ };
4782
+ };
4783
+ var buildRuntimeEvidenceLineageScopes = ({
4784
+ signals,
4785
+ evaluations
4786
+ }) => {
4787
+ const scopes = /* @__PURE__ */ new Map();
4788
+ const add = ({
4789
+ strategy,
4790
+ symbol,
4791
+ deploymentId,
4792
+ accountId,
4793
+ runtimeConfigId,
4794
+ runtimeVersion,
4795
+ timestamp,
4796
+ lineage
4797
+ }) => {
4798
+ if (!lineage || !Number.isFinite(timestamp)) {
4799
+ return;
4800
+ }
4801
+ const key = `${deploymentId ?? "default"}::${accountId ?? "default"}::${strategy}::${symbol}::${runtimeLineageKey(lineage)}`;
4802
+ const existing = scopes.get(key);
4803
+ scopes.set(key, {
4804
+ strategy,
4805
+ symbol,
4806
+ ...deploymentId ? { deploymentId } : {},
4807
+ ...accountId ? { accountId } : {},
4808
+ ...runtimeConfigId ? { runtimeConfigId } : {},
4809
+ ...runtimeVersion ? { runtimeVersion } : {},
4810
+ lineage,
4811
+ firstTimestamp: existing ? Math.min(existing.firstTimestamp, timestamp) : timestamp,
4812
+ lastTimestamp: existing ? Math.max(existing.lastTimestamp, timestamp) : timestamp
4813
+ });
4814
+ };
4815
+ for (const signal of signals) {
4816
+ add({
4817
+ strategy: signal.strategy,
4818
+ symbol: signal.symbol,
4819
+ deploymentId: signal.deploymentId,
4820
+ accountId: signal.accountId,
4821
+ runtimeConfigId: signal.runtimeConfigId,
4822
+ runtimeVersion: signal.runtimeVersion,
4823
+ timestamp: signal.timestamp,
4824
+ lineage: signal.runtimeLineage
4825
+ });
4826
+ }
4827
+ for (const evaluation of evaluations) {
4828
+ add({
4829
+ strategy: evaluation.strategy,
4830
+ symbol: evaluation.symbol,
4831
+ deploymentId: evaluation.deploymentId,
4832
+ accountId: evaluation.accountId,
4833
+ runtimeConfigId: evaluation.runtimeConfigId,
4834
+ runtimeVersion: evaluation.runtimeVersion,
4835
+ timestamp: evaluation.timestamp,
4836
+ lineage: evaluation.runtimeLineage
4837
+ });
4838
+ }
4839
+ return [...scopes.values()].sort(
4840
+ (left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.firstTimestamp - right.firstTimestamp
4841
+ );
4842
+ };
4843
+ var loadReplayRuntimeEvidenceSource = async ({
4844
+ filePath,
4845
+ projectRoot,
4846
+ expectedUserName,
4847
+ expectedWindow
4848
+ }) => {
4849
+ const { resolvedPath, runtime, userName, startTime, endTime, deployment } = await readRuntimeEvidenceArtifact({ filePath, projectRoot });
4850
+ if (!userName || userName !== expectedUserName) {
4851
+ throw new Error(
4852
+ `Runtime evidence user mismatch: expected=${expectedUserName}, actual=${userName || "missing"}`
4853
+ );
4854
+ }
4855
+ const endTimeMatches = endTime === expectedWindow.end || endTime === expectedWindow.end + 1;
4856
+ if (startTime !== expectedWindow.start || !endTimeMatches) {
4857
+ throw new Error(
4858
+ `Runtime evidence window mismatch: expected=${expectedWindow.start}..${expectedWindow.end}, actual=${startTime ?? "missing"}..${endTime ?? "missing"}`
4859
+ );
4860
+ }
4861
+ const trades = unwrapRows(runtime.trades, "trade");
4862
+ const signals = unwrapRows(runtime.signals, "signal");
4863
+ const evaluations = unwrapRows(
4864
+ runtime.evaluations,
4865
+ "evaluation"
4866
+ );
4867
+ const storedLineageScopes = unwrapRows(
4868
+ runtime.lineageScopes,
4869
+ "lineageScope"
4870
+ );
4871
+ assertRuntimeRowsMatchDeployment({
4872
+ label: "trade",
4873
+ rows: trades,
4874
+ deployment
4875
+ });
4876
+ assertRuntimeRowsMatchDeployment({
4877
+ label: "signal",
4878
+ rows: signals,
4879
+ deployment
4880
+ });
4881
+ assertRuntimeRowsMatchDeployment({
4882
+ label: "evaluation",
4883
+ rows: evaluations,
4884
+ deployment
4885
+ });
4886
+ assertRuntimeRowsMatchDeployment({
4887
+ label: "lineage scope",
4888
+ rows: storedLineageScopes,
4889
+ deployment
4890
+ });
4891
+ return {
4892
+ path: resolvedPath,
4893
+ userName,
4894
+ startTime,
4895
+ endTime,
4896
+ trades,
4897
+ signals,
4898
+ evaluations,
4899
+ lineageScopes: storedLineageScopes.length ? storedLineageScopes : buildRuntimeEvidenceLineageScopes({ signals, evaluations }),
4900
+ deployment
4901
+ };
4902
+ };
4903
+
4904
+ // src/lib/replay/support.ts
4905
+ var import_chalk8 = __toESM(require("chalk"));
4906
+
4907
+ // src/lib/runtimeModeConfig.ts
4908
+ var buildRuntimeModeStrategyConfig = ({
4909
+ strategyConfig,
4910
+ env,
4911
+ interval,
4912
+ makeOrders,
4913
+ recordRuntimeTrades,
4914
+ aiReplayAnalyses
4915
+ }) => ({
4916
+ ...strategyConfig,
4917
+ ENV: env,
4918
+ MAKE_ORDERS: makeOrders,
4919
+ INTERVAL: strategyConfig.INTERVAL ?? interval,
4920
+ ...typeof recordRuntimeTrades === "boolean" ? { RECORD_RUNTIME_TRADES: recordRuntimeTrades } : {},
4921
+ ...aiReplayAnalyses?.length ? { AI_REPLAY_ANALYSES: aiReplayAnalyses } : {}
4922
+ });
4923
+
4924
+ // src/lib/replay/support.ts
4925
+ var REPLAY_RESULTS_BY_STRATEGY_HEADERS = [
4926
+ import_chalk8.default.blue("STRATEGY"),
4927
+ import_chalk8.default.yellow("TICKERS"),
4928
+ import_chalk8.default.yellow("TRADE TICKERS"),
4929
+ import_chalk8.default.cyan("ORDERS"),
4930
+ import_chalk8.default.cyan("WIN/LOSS (%)"),
4931
+ import_chalk8.default.cyan("PROFIT"),
4932
+ import_chalk8.default.cyan("AVG TRADE")
4933
+ ];
4934
+ var REPLAY_RUNTIME_COMPARISON_HEADERS = [
4935
+ import_chalk8.default.blue("STRATEGY"),
4936
+ import_chalk8.default.cyan("BT ENTRIES"),
4937
+ import_chalk8.default.cyan("BT PNL"),
4938
+ import_chalk8.default.yellow("RT TRADES"),
4939
+ import_chalk8.default.yellow("RT PNL"),
4940
+ import_chalk8.default.green("MATCHED"),
4941
+ import_chalk8.default.red("ORDER FAILED"),
4942
+ import_chalk8.default.yellow("RT ONLY"),
4943
+ import_chalk8.default.magenta("BT ONLY"),
4944
+ import_chalk8.default.red("UNMATCHED")
4945
+ ];
4946
+ var REPLAY_RESULTS_CONFIG = "replay";
4947
+ var REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS = 1;
4948
+ var REPLAY_RUNTIME_COMPARE_TOLERANCE_MS = REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS * 15 * 60 * 1e3;
4949
+ var formatReplayRuntimeCompareTolerance = () => `${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS} ${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS === 1 ? "bar" : "bars"}`;
4950
+ var getReplayRuntimeUnmatchedCount = (row) => row.orderFailed + row.runtimeOnly + row.backtestOnly;
4951
+ var buildReplayStrategyConfig = ({
4952
+ strategyConfig,
4953
+ interval
4954
+ }) => buildRuntimeModeStrategyConfig({
4955
+ strategyConfig,
4956
+ env: "PARITY",
4957
+ interval,
4958
+ makeOrders: true,
4959
+ recordRuntimeTrades: false
4960
+ });
4961
+ var toRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
4962
+ var compactReplayAdditionalIndicators = (additionalIndicators) => {
4963
+ const additional = toRecord(additionalIndicators);
4964
+ if (!additional) {
4965
+ return void 0;
4966
+ }
4967
+ const compact = {};
4968
+ if (additional.executionSlippage != null) {
4969
+ compact.executionSlippage = additional.executionSlippage;
4970
+ }
4971
+ if (additional.marketContext != null) {
4972
+ compact.marketContext = additional.marketContext;
4973
+ }
4974
+ const baseContext = toRecord(additional.baseContext);
4975
+ const relative = toRecord(baseContext?.relative);
4976
+ const execution = toRecord(relative?.execution);
4977
+ if (execution) {
4978
+ compact.baseContext = {
4979
+ relative: {
4980
+ execution
4981
+ }
4982
+ };
4983
+ }
4984
+ return Object.keys(compact).length ? compact : void 0;
4985
+ };
4986
+ var compactReplaySignal = (signal) => {
4987
+ if (!signal) {
4988
+ return void 0;
4989
+ }
4990
+ return {
4991
+ signalId: signal.signalId,
4992
+ strategy: signal.strategy,
4424
4993
  symbol: signal.symbol,
4425
4994
  interval: signal.interval,
4426
4995
  direction: signal.direction,
@@ -4450,6 +5019,7 @@ var import_redis3 = require("@tradejs/infra/redis");
4450
5019
  var import_connectors6 = require("@tradejs/node/connectors");
4451
5020
  var import_cli4 = require("@tradejs/node/cli");
4452
5021
  var import_strategies5 = require("@tradejs/node/strategies");
5022
+ var import_runtimeStrategies3 = require("@tradejs/node/runtimeStrategies");
4453
5023
 
4454
5024
  // src/lib/replay/portfolioReplayConnector.ts
4455
5025
  var import_constants3 = require("@tradejs/core/constants");
@@ -4869,345 +5439,48 @@ var splitCandlesForReplayWindow = (candles, start, preloadStart) => {
4869
5439
  }
4870
5440
  replayData.push(candle);
4871
5441
  }
4872
- return { prevData, replayData };
4873
- };
4874
- var alignSymbolWithBtcReference = (symbolCandles, btcCandles) => (0, import_indicators2.alignSortedCandlesByTimestamp)(symbolCandles, btcCandles);
4875
-
4876
- // src/lib/signals/hooks.ts
4877
- var normalizeHookList = (value) => {
4878
- if (Array.isArray(value)) {
4879
- return value;
4880
- }
4881
- return value ? [value] : [];
4882
- };
4883
- var invokeBeforeSignalsHooks = async (hooks, params) => {
4884
- for (const hook of normalizeHookList(
4885
- hooks?.beforeSignals
4886
- )) {
4887
- const result = await hook(params);
4888
- if (result?.abort === true) {
4889
- return result;
4890
- }
4891
- }
4892
- return void 0;
4893
- };
4894
- var invokeAfterSignalsHooks = async (hooks, params) => {
4895
- for (const hook of normalizeHookList(
4896
- hooks?.afterSignals
4897
- )) {
4898
- await hook(params);
4899
- }
4900
- };
4901
-
4902
- // src/lib/runtimeLineage.ts
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
- var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
4910
-
4911
- // src/lib/strategyGateFingerprint.ts
4912
- var import_node_crypto = require("crypto");
4913
- var import_node_module = require("module");
4914
- var import_promises = __toESM(require("fs/promises"));
4915
- var import_node_path = __toESM(require("path"));
4916
- var toPackageToken = (strategyName) => strategyName.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase();
4917
- var getStrategyPackageName = (strategyName) => strategyName === "TrendLine" || strategyName === "ReverseTrendLine" ? "@tradejs/strategy-trend-line" : `@tradejs/strategy-${toPackageToken(strategyName)}`;
4918
- var gateFingerprintSpecifiers = (strategyName) => [
4919
- getStrategyPackageName(strategyName),
4920
- "@tradejs/strategy-kit/ai-gate",
4921
- "@tradejs/node/ai",
4922
- "@tradejs/core/strategies"
4923
- ];
4924
- var resolveStrategyGateFingerprint = async ({
4925
- projectRoot,
4926
- strategyName,
4927
- gitSha
4928
- }) => {
4929
- const projectManifestPath = import_node_path.default.join(projectRoot, "package.json");
4930
- const projectExists = await import_promises.default.access(projectManifestPath).then(() => true).catch(() => false);
4931
- const requireFromProject = projectExists ? (0, import_node_module.createRequire)(projectManifestPath) : null;
4932
- const candidates = requireFromProject ? await Promise.all(
4933
- gateFingerprintSpecifiers(strategyName).map(async (specifier) => {
4934
- let resolvedPath;
4935
- try {
4936
- resolvedPath = requireFromProject.resolve(specifier);
4937
- } catch {
4938
- return null;
4939
- }
4940
- const content = await import_promises.default.readFile(resolvedPath).catch(() => null);
4941
- return content == null ? null : { specifier, content };
4942
- })
4943
- ) : [];
4944
- const sourceEntries = candidates.filter(
4945
- (entry) => entry != null
4946
- );
4947
- const hash = (0, import_node_crypto.createHash)("sha256");
4948
- hash.update(strategyName);
4949
- if (sourceEntries.length) {
4950
- for (const entry of sourceEntries) {
4951
- hash.update(entry.specifier);
4952
- hash.update(entry.content);
4953
- }
4954
- } else {
4955
- hash.update(gitSha ?? "unknown-git-sha");
4956
- }
4957
- return {
4958
- gateFingerprint: hash.digest("hex").slice(0, 16),
4959
- gateFingerprintFiles: sourceEntries.map((entry) => entry.specifier)
4960
- };
4961
- };
4962
-
4963
- // src/lib/runtimeLineage.ts
4964
- var RUNTIME_CONTEXT_ENV_KEYS = [
4965
- "AI_MODE",
4966
- "MIN_AI_QUALITY",
4967
- "INTERVAL",
4968
- "DERIVATIVES_CONTEXT_ENABLED",
4969
- "DERIVATIVES_CONTEXT_TARGET_ENABLED",
4970
- "DERIVATIVES_CONTEXT_LOOKBACK_HOURS",
4971
- "DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS",
4972
- "DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY",
4973
- "COINMARKETCAP_CONTEXT_ENABLED",
4974
- "COINMARKETCAP_CONTEXT_MAX_AGE_MS",
4975
- "COINMARKETCAP_CONTEXT_EXCHANGE_LIQUIDITY_ENABLED",
4976
- "COINMARKETCAP_CONTEXT_EXCHANGE_SLUGS",
4977
- "COINMARKETCAP_CONTEXT_FEAR_GREED_ENABLED",
4978
- "COINMARKETCAP_CONTEXT_HISTORICAL_ACCESS_MONTHS",
4979
- "COINMARKETCAP_CONTEXT_BACKFILL_ENABLED",
4980
- "COINMARKETCAP_CONTEXT_BACKFILL_MAX_DAYS",
4981
- "COINMARKETCAP_CONTEXT_BACKFILL_WARMUP_DAYS",
4982
- "HYPERLIQUID_WHALE_CONTEXT_ENABLED",
4983
- "HYPERLIQUID_WHALE_BACKFILL_ENABLED",
4984
- "HYPERLIQUID_WHALE_MIN_COVERAGE_PCT",
4985
- "HYPERLIQUID_WHALE_CONCURRENCY",
4986
- "HYPERLIQUID_WHALE_RATE_LIMIT_WEIGHT"
4987
- ];
4988
- var RUNTIME_CONTEXT_DATA_MODEL = {
4989
- derivativesSourceIntervals: "15m",
4990
- derivativesDerivedIntervals: "1h",
4991
- derivativesHourlyFallback: "stored-1h",
4992
- derivativesDataModelVersion: 2,
4993
- coinMarketCapAvailabilityModel: "closed-interval-all-daily",
4994
- coinMarketCapDataModelVersion: 3,
4995
- binanceBreadthUniverseModel: "fixed-versioned-top5-top10-top30-top50-top100",
4996
- binanceBreadthDataModelVersion: 2,
4997
- hyperliquidWhaleCanonicalInterval: "1m",
4998
- hyperliquidWhaleGateMinNotionalUsd: 5e4,
4999
- hyperliquidWhaleMinCoveragePct: 0.8,
5000
- hyperliquidWhaleDataModelVersion: import_types2.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
5001
- };
5002
- var normalizeForStableJson = (value) => {
5003
- if (Array.isArray(value)) {
5004
- return value.map(normalizeForStableJson);
5005
- }
5006
- if (value && typeof value === "object") {
5007
- return Object.fromEntries(
5008
- Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
5009
- );
5010
- }
5011
- return value;
5012
- };
5013
- var resolveRuntimeMaxLossValue = (config) => {
5014
- if (!config || typeof config !== "object" || Array.isArray(config)) {
5015
- return null;
5016
- }
5017
- const strategyConfig = config.strategyConfig;
5018
- if (!strategyConfig || typeof strategyConfig !== "object" || Array.isArray(strategyConfig)) {
5019
- return null;
5020
- }
5021
- const maxLossValue = Number(
5022
- strategyConfig.MAX_LOSS_VALUE
5023
- );
5024
- return Number.isFinite(maxLossValue) ? maxLossValue : null;
5025
- };
5026
- var fingerprintRuntimeValue = (value) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
5027
- var gitLineageCache = /* @__PURE__ */ new Map();
5028
- var gateFingerprintCache = /* @__PURE__ */ new Map();
5029
- var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
5030
- var snapshotFingerprintCache = /* @__PURE__ */ new Map();
5031
- var resolveGitLineage = (projectRoot) => {
5032
- const cached = gitLineageCache.get(projectRoot);
5033
- if (cached) {
5034
- return cached;
5035
- }
5036
- const envSha = String(process.env.TRADEJS_GIT_SHA ?? "").trim();
5037
- if (envSha && envSha !== "unknown") {
5038
- const result = {
5039
- gitSha: envSha,
5040
- gitDirty: false
5041
- };
5042
- gitLineageCache.set(projectRoot, result);
5043
- return result;
5044
- }
5045
- try {
5046
- const gitSha = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "HEAD"], {
5047
- cwd: projectRoot,
5048
- encoding: "utf8",
5049
- stdio: ["ignore", "pipe", "ignore"]
5050
- }).trim();
5051
- const status = (0, import_node_child_process.execFileSync)(
5052
- "git",
5053
- ["status", "--porcelain", "--untracked-files=normal"],
5054
- {
5055
- cwd: projectRoot,
5056
- encoding: "utf8",
5057
- stdio: ["ignore", "pipe", "ignore"]
5058
- }
5059
- ).trim();
5060
- const result = {
5061
- gitSha: gitSha || null,
5062
- gitDirty: status.length > 0
5063
- };
5064
- gitLineageCache.set(projectRoot, result);
5065
- return result;
5066
- } catch {
5067
- const result = {
5068
- gitSha: null,
5069
- gitDirty: null
5070
- };
5071
- gitLineageCache.set(projectRoot, result);
5072
- return result;
5073
- }
5074
- };
5075
- var readOptionalFile = async (filePath) => {
5076
- try {
5077
- return await import_promises2.default.readFile(filePath);
5078
- } catch {
5079
- return null;
5080
- }
5081
- };
5082
- var resolveBinanceBreadthFingerprint = async (projectRoot) => {
5083
- if (binanceBreadthFingerprintCache.has(projectRoot)) {
5084
- return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
5085
- }
5086
- const content = await readOptionalFile(
5087
- import_node_path2.default.join(
5088
- projectRoot,
5089
- "packages/node/src/config/binanceBreadthUniverses.json"
5090
- )
5091
- );
5092
- let result = null;
5093
- if (content) {
5094
- try {
5095
- const parsed = JSON.parse(content.toString("utf8"));
5096
- result = typeof parsed.fingerprint === "string" && parsed.fingerprint.trim() ? parsed.fingerprint.trim() : null;
5097
- } catch {
5098
- result = null;
5099
- }
5100
- }
5101
- binanceBreadthFingerprintCache.set(projectRoot, result);
5102
- return result;
5103
- };
5104
- var resolveSnapshotFingerprint = async (projectRoot, relativePath) => {
5105
- const cacheKey = `${projectRoot}:${relativePath}`;
5106
- if (snapshotFingerprintCache.has(cacheKey)) {
5107
- return snapshotFingerprintCache.get(cacheKey) ?? null;
5108
- }
5109
- const content = await readOptionalFile(import_node_path2.default.join(projectRoot, relativePath));
5110
- let result = null;
5111
- if (content) {
5112
- try {
5113
- const parsed = JSON.parse(content.toString("utf8"));
5114
- result = typeof parsed.fingerprint === "string" && parsed.fingerprint.trim() ? parsed.fingerprint.trim() : null;
5115
- } catch {
5116
- result = null;
5117
- }
5118
- }
5119
- snapshotFingerprintCache.set(cacheKey, result);
5120
- return result;
5442
+ return { prevData, replayData };
5121
5443
  };
5122
- var resolveRuntimeGateFingerprint = async ({
5123
- projectRoot,
5124
- strategyName,
5125
- gitSha
5126
- }) => {
5127
- const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
5128
- const cached = gateFingerprintCache.get(cacheKey);
5129
- if (cached) return cached;
5130
- const result = await resolveStrategyGateFingerprint({
5131
- projectRoot,
5132
- strategyName,
5133
- gitSha
5134
- });
5135
- gateFingerprintCache.set(cacheKey, result);
5136
- return result;
5444
+ var alignSymbolWithBtcReference = (symbolCandles, btcCandles) => (0, import_indicators2.alignSortedCandlesByTimestamp)(symbolCandles, btcCandles);
5445
+
5446
+ // src/lib/signals/hooks.ts
5447
+ var normalizeHookList = (value) => {
5448
+ if (Array.isArray(value)) {
5449
+ return value;
5450
+ }
5451
+ return value ? [value] : [];
5137
5452
  };
5138
- async function buildRuntimeLineage({
5139
- projectRoot,
5140
- strategyName,
5141
- compositionId,
5142
- version,
5143
- strategyPackageVersion,
5144
- runtimePackageVersion,
5145
- config,
5146
- runContext,
5147
- env = process.env
5148
- }) {
5149
- if (version != null) {
5150
- if (!Number.isSafeInteger(version) || version <= 0) {
5151
- throw new Error(`Invalid runtime strategy version: ${version}`);
5453
+ var invokeBeforeSignalsHooks = async (hooks, params) => {
5454
+ for (const hook of normalizeHookList(
5455
+ hooks?.beforeSignals
5456
+ )) {
5457
+ const result = await hook(params);
5458
+ if (result?.abort === true) {
5459
+ return result;
5152
5460
  }
5153
- return {
5154
- schemaVersion: 2,
5155
- version,
5156
- strategyPackageVersion,
5157
- runtimePackageVersion,
5158
- maxLossValue: resolveRuntimeMaxLossValue(config)
5159
- };
5160
5461
  }
5161
- const git = resolveGitLineage(projectRoot);
5162
- const hyperliquidPerpUniverseFingerprint = typeof import_strategies3.getHyperliquidPerpUniverseSnapshot === "function" ? (0, import_strategies3.getHyperliquidPerpUniverseSnapshot)().fingerprint : await resolveSnapshotFingerprint(
5163
- projectRoot,
5164
- "packages/node/src/config/hyperliquidPerpUniverse.json"
5165
- );
5166
- const hyperliquidWhaleRegistryFingerprint = typeof import_strategies3.getHyperliquidWhaleRegistrySnapshot === "function" ? (0, import_strategies3.getHyperliquidWhaleRegistrySnapshot)().fingerprint : await resolveSnapshotFingerprint(
5167
- projectRoot,
5168
- "packages/node/src/config/hyperliquidWhales.json"
5169
- );
5170
- const context = {
5171
- ...Object.fromEntries(
5172
- RUNTIME_CONTEXT_ENV_KEYS.map((key) => [key, env[key] ?? null])
5173
- ),
5174
- ...RUNTIME_CONTEXT_DATA_MODEL,
5175
- binanceBreadthUniverseFingerprint: await resolveBinanceBreadthFingerprint(projectRoot),
5176
- hyperliquidPerpUniverseFingerprint,
5177
- hyperliquidWhaleRegistryFingerprint,
5178
- ...runContext
5179
- };
5180
- return {
5181
- schemaVersion: 1,
5182
- compositionId: compositionId?.trim() || null,
5183
- ...git,
5184
- gateFingerprint: (await resolveRuntimeGateFingerprint({
5185
- projectRoot,
5186
- strategyName,
5187
- gitSha: git.gitSha
5188
- })).gateFingerprint,
5189
- // Position risk is tracked separately so changing trade size does not
5190
- // masquerade as a change to the core + gate decision logic.
5191
- configFingerprint: (0, import_strategyReleaseEvidence.strategyLogicConfigFingerprint)(config),
5192
- contextFingerprint: fingerprintRuntimeValue(context),
5193
- maxLossValue: resolveRuntimeMaxLossValue(config)
5194
- };
5195
- }
5196
- var runtimeLineageKey = (lineage) => lineage.schemaVersion === 2 ? ["v2", lineage.version].join(":") : [
5197
- lineage.schemaVersion,
5198
- lineage.compositionId ?? "unbound-composition",
5199
- lineage.gitSha ?? "unknown",
5200
- lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
5201
- lineage.gateFingerprint,
5202
- lineage.configFingerprint,
5203
- lineage.contextFingerprint
5204
- ].join(":");
5205
- var runtimeLineagesMatch = (left, right) => left != null && right != null && runtimeLineageKey(left) === runtimeLineageKey(right);
5462
+ return void 0;
5463
+ };
5464
+ var invokeAfterSignalsHooks = async (hooks, params) => {
5465
+ for (const hook of normalizeHookList(
5466
+ hooks?.afterSignals
5467
+ )) {
5468
+ await hook(params);
5469
+ }
5470
+ };
5206
5471
 
5207
5472
  // src/lib/replay/historicalSignalsReplay.ts
5208
5473
  var loadRuntimeStrategies = async (runtimeStrategies) => {
5209
5474
  const strategies = await Promise.all(
5210
- runtimeStrategies.map(async ({ strategyName, strategyConfig }) => {
5475
+ runtimeStrategies.map(async (runtimeStrategy) => {
5476
+ const {
5477
+ strategyName,
5478
+ version,
5479
+ strategyPackage,
5480
+ strategyPackageVersion,
5481
+ runtimePackageVersion,
5482
+ strategyConfig
5483
+ } = runtimeStrategy;
5211
5484
  const strategyCreator = await (0, import_strategies5.getStrategyCreator)(
5212
5485
  strategyName,
5213
5486
  replayProjectRoot
@@ -5215,12 +5488,25 @@ var loadRuntimeStrategies = async (runtimeStrategies) => {
5215
5488
  if (!strategyCreator) {
5216
5489
  throw new Error(`Unknown strategy: ${strategyName}`);
5217
5490
  }
5491
+ const installed = await (0, import_runtimeStrategies3.getRuntimeStrategyPackageMetadata)({
5492
+ strategyName,
5493
+ projectRoot: replayProjectRoot
5494
+ });
5495
+ if (installed.strategyPackage !== strategyPackage || installed.strategyPackageVersion !== strategyPackageVersion || installed.runtimePackageVersion !== runtimePackageVersion) {
5496
+ throw new Error(
5497
+ `Runtime evidence package mismatch for ${strategyName}: expected=${strategyPackage}@${strategyPackageVersion}/@tradejs/node@${runtimePackageVersion}, installed=${installed.strategyPackage ?? "missing"}@${installed.strategyPackageVersion ?? "missing"}/@tradejs/node@${installed.runtimePackageVersion ?? "missing"}`
5498
+ );
5499
+ }
5218
5500
  const strategyResults = await (0, import_redis3.getData)(
5219
5501
  import_redis3.redisKeys.strategyResults(replayUserName, strategyName),
5220
5502
  {}
5221
5503
  );
5222
5504
  return {
5223
5505
  strategyName,
5506
+ version,
5507
+ strategyPackage,
5508
+ strategyPackageVersion,
5509
+ runtimePackageVersion,
5224
5510
  strategyCreator,
5225
5511
  strategyConfig,
5226
5512
  strategyResults
@@ -5414,6 +5700,9 @@ var runHistoricalSignalsReplay = async ({
5414
5700
  loadedStrategies.map(
5415
5701
  async ({
5416
5702
  strategyName,
5703
+ version,
5704
+ strategyPackageVersion,
5705
+ runtimePackageVersion,
5417
5706
  strategyCreator,
5418
5707
  strategyConfig,
5419
5708
  strategyResults
@@ -5421,8 +5710,11 @@ var runHistoricalSignalsReplay = async ({
5421
5710
  const runtimeLineage = await buildRuntimeLineage({
5422
5711
  projectRoot: replayProjectRoot,
5423
5712
  strategyName,
5713
+ version,
5714
+ strategyPackageVersion,
5715
+ runtimePackageVersion,
5424
5716
  config: {
5425
- configId: "config",
5717
+ configId: `v${version}`,
5426
5718
  strategyConfig,
5427
5719
  symbolResultConfig: strategyResults?.[symbol]?.config ?? null
5428
5720
  },
@@ -5435,6 +5727,8 @@ var runHistoricalSignalsReplay = async ({
5435
5727
  runtimeLineages.push({
5436
5728
  strategy: strategyName,
5437
5729
  symbol,
5730
+ ...preparedRun.deploymentId ? { deploymentId: preparedRun.deploymentId } : {},
5731
+ ...preparedRun.accountId ? { accountId: preparedRun.accountId } : {},
5438
5732
  lineage: runtimeLineage
5439
5733
  });
5440
5734
  return {
@@ -5444,8 +5738,11 @@ var runHistoricalSignalsReplay = async ({
5444
5738
  run: await strategyCreator({
5445
5739
  userName: replayUserName,
5446
5740
  connectorName,
5447
- runtimeConfigId: "config",
5741
+ runtimeConfigId: `v${version}`,
5448
5742
  runtimeLineage,
5743
+ universe: preparedRun.universe,
5744
+ accountId: preparedRun.accountId,
5745
+ deploymentId: preparedRun.deploymentId,
5449
5746
  config: buildReplayStrategyConfig({
5450
5747
  strategyConfig,
5451
5748
  interval
@@ -7208,118 +7505,6 @@ var buildReplayExchangeComparisonDetails = ({
7208
7505
  };
7209
7506
  };
7210
7507
 
7211
- // src/lib/replay/runtimeEvidenceSource.ts
7212
- var import_promises3 = __toESM(require("fs/promises"));
7213
- var import_node_path3 = __toESM(require("path"));
7214
- var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
7215
- var asFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
7216
- var unwrapRows = (value, field) => (Array.isArray(value) ? value : []).map((item) => {
7217
- const row = asRecord(item);
7218
- return asRecord(row?.[field]) ?? row;
7219
- }).filter((item) => item != null);
7220
- var buildRuntimeEvidenceLineageScopes = ({
7221
- signals,
7222
- evaluations
7223
- }) => {
7224
- const scopes = /* @__PURE__ */ new Map();
7225
- const add = ({
7226
- strategy,
7227
- symbol,
7228
- runtimeConfigId,
7229
- runtimeVersion,
7230
- timestamp,
7231
- lineage
7232
- }) => {
7233
- if (!lineage || !Number.isFinite(timestamp)) {
7234
- return;
7235
- }
7236
- const key = `${strategy}::${symbol}::${runtimeLineageKey(lineage)}`;
7237
- const existing = scopes.get(key);
7238
- scopes.set(key, {
7239
- strategy,
7240
- symbol,
7241
- ...runtimeConfigId ? { runtimeConfigId } : {},
7242
- ...runtimeVersion ? { runtimeVersion } : {},
7243
- lineage,
7244
- firstTimestamp: existing ? Math.min(existing.firstTimestamp, timestamp) : timestamp,
7245
- lastTimestamp: existing ? Math.max(existing.lastTimestamp, timestamp) : timestamp
7246
- });
7247
- };
7248
- for (const signal of signals) {
7249
- add({
7250
- strategy: signal.strategy,
7251
- symbol: signal.symbol,
7252
- runtimeConfigId: signal.runtimeConfigId,
7253
- runtimeVersion: signal.runtimeVersion,
7254
- timestamp: signal.timestamp,
7255
- lineage: signal.runtimeLineage
7256
- });
7257
- }
7258
- for (const evaluation of evaluations) {
7259
- add({
7260
- strategy: evaluation.strategy,
7261
- symbol: evaluation.symbol,
7262
- runtimeConfigId: evaluation.runtimeConfigId,
7263
- runtimeVersion: evaluation.runtimeVersion,
7264
- timestamp: evaluation.timestamp,
7265
- lineage: evaluation.runtimeLineage
7266
- });
7267
- }
7268
- return [...scopes.values()].sort(
7269
- (left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.firstTimestamp - right.firstTimestamp
7270
- );
7271
- };
7272
- var loadReplayRuntimeEvidenceSource = async ({
7273
- filePath,
7274
- projectRoot,
7275
- expectedUserName,
7276
- expectedWindow
7277
- }) => {
7278
- const resolvedPath = import_node_path3.default.resolve(projectRoot, filePath);
7279
- const root = asRecord(
7280
- JSON.parse(await import_promises3.default.readFile(resolvedPath, "utf8"))
7281
- );
7282
- const runtime = asRecord(root?.runtime) ?? root;
7283
- if (!root || !runtime) {
7284
- throw new Error(`Invalid runtime evidence JSON: ${resolvedPath}`);
7285
- }
7286
- const rootWindow = asRecord(root.window);
7287
- const runtimeWindow = asRecord(runtime.window);
7288
- const startTime = asFiniteNumber(rootWindow?.startTime) ?? asFiniteNumber(runtimeWindow?.startTime);
7289
- const endTime = asFiniteNumber(rootWindow?.endTime) ?? asFiniteNumber(runtimeWindow?.endTime);
7290
- const userName = String(root.userName ?? runtime.userName ?? "").trim();
7291
- if (!userName || userName !== expectedUserName) {
7292
- throw new Error(
7293
- `Runtime evidence user mismatch: expected=${expectedUserName}, actual=${userName || "missing"}`
7294
- );
7295
- }
7296
- const endTimeMatches = endTime === expectedWindow.end || endTime === expectedWindow.end + 1;
7297
- if (startTime !== expectedWindow.start || !endTimeMatches) {
7298
- throw new Error(
7299
- `Runtime evidence window mismatch: expected=${expectedWindow.start}..${expectedWindow.end}, actual=${startTime ?? "missing"}..${endTime ?? "missing"}`
7300
- );
7301
- }
7302
- const trades = unwrapRows(runtime.trades, "trade");
7303
- const signals = unwrapRows(runtime.signals, "signal");
7304
- const evaluations = unwrapRows(
7305
- runtime.evaluations,
7306
- "evaluation"
7307
- );
7308
- return {
7309
- path: resolvedPath,
7310
- userName,
7311
- startTime,
7312
- endTime,
7313
- trades,
7314
- signals,
7315
- evaluations,
7316
- lineageScopes: buildRuntimeEvidenceLineageScopes({
7317
- signals,
7318
- evaluations
7319
- })
7320
- };
7321
- };
7322
-
7323
7508
  // src/lib/replay/runtimeComparison.ts
7324
7509
  var getReplayEntryTimestampCompareOffsetMs = () => {
7325
7510
  const intervalMinutes = Number(replayInterval);
@@ -7337,27 +7522,40 @@ var buildReplaySignalEvaluations = (signals) => signals.map((signal) => ({
7337
7522
  reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
7338
7523
  signalId: signal.signalId,
7339
7524
  direction: signal.direction,
7525
+ accountId: signal.accountId,
7526
+ deploymentId: signal.deploymentId,
7340
7527
  runtimeLineage: signal.runtimeLineage,
7341
7528
  orderStatus: signal.orderStatus,
7342
7529
  orderSkipReason: signal.orderSkipReason,
7343
7530
  ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
7344
7531
  ...signal.ml ? { ml: signal.ml } : {}
7345
7532
  }));
7346
- var lineageScopeKey = (strategy, symbol) => `${strategy}::${symbol}`;
7533
+ var lineageScopeKey = ({
7534
+ strategy,
7535
+ symbol,
7536
+ deploymentId,
7537
+ accountId
7538
+ }) => `${deploymentId ?? "default-deployment"}::${accountId ?? "default-account"}::${strategy}::${symbol}`;
7347
7539
  var buildExpectedLineageByScope = (replayLineages) => new Map(
7348
- replayLineages.map((record) => [
7349
- lineageScopeKey(record.strategy, record.symbol),
7350
- record.lineage
7351
- ])
7540
+ replayLineages.map((record) => [lineageScopeKey(record), record.lineage])
7352
7541
  );
7353
7542
  var hasExpectedLineage = ({
7354
7543
  strategy,
7355
7544
  symbol,
7545
+ deploymentId,
7546
+ accountId,
7356
7547
  lineage,
7357
7548
  expectedByScope
7358
- }) => runtimeLineagesMatch(
7549
+ }) => runtimeLineagesComparable(
7359
7550
  lineage,
7360
- expectedByScope.get(lineageScopeKey(strategy, symbol))
7551
+ expectedByScope.get(
7552
+ lineageScopeKey({
7553
+ strategy,
7554
+ symbol,
7555
+ deploymentId,
7556
+ accountId
7557
+ })
7558
+ )
7361
7559
  );
7362
7560
  var filterReplayComparisonByLineage = ({
7363
7561
  replayLineages,
@@ -7372,6 +7570,8 @@ var filterReplayComparisonByLineage = ({
7372
7570
  (signal) => hasExpectedLineage({
7373
7571
  strategy: signal.strategy,
7374
7572
  symbol: signal.symbol,
7573
+ deploymentId: signal.deploymentId,
7574
+ accountId: signal.accountId,
7375
7575
  lineage: signal.runtimeLineage,
7376
7576
  expectedByScope
7377
7577
  })
@@ -7380,6 +7580,8 @@ var filterReplayComparisonByLineage = ({
7380
7580
  (evaluation) => hasExpectedLineage({
7381
7581
  strategy: evaluation.strategy,
7382
7582
  symbol: evaluation.symbol,
7583
+ deploymentId: evaluation.deploymentId,
7584
+ accountId: evaluation.accountId,
7383
7585
  lineage: evaluation.runtimeLineage,
7384
7586
  expectedByScope
7385
7587
  })
@@ -7388,13 +7590,15 @@ var filterReplayComparisonByLineage = ({
7388
7590
  (scope) => hasExpectedLineage({
7389
7591
  strategy: scope.strategy,
7390
7592
  symbol: scope.symbol,
7593
+ deploymentId: scope.deploymentId,
7594
+ accountId: scope.accountId,
7391
7595
  lineage: scope.lineage,
7392
7596
  expectedByScope
7393
7597
  })
7394
7598
  );
7395
7599
  const deploymentWindows = /* @__PURE__ */ new Map();
7396
7600
  for (const scope of comparableRuntimeLineageScopes) {
7397
- const key = lineageScopeKey(scope.strategy, scope.symbol);
7601
+ const key = lineageScopeKey(scope);
7398
7602
  const existing = deploymentWindows.get(key);
7399
7603
  deploymentWindows.set(key, {
7400
7604
  firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
@@ -7418,13 +7622,13 @@ var filterReplayComparisonByLineage = ({
7418
7622
  return hasExpectedLineage({
7419
7623
  strategy: trade.strategy,
7420
7624
  symbol: trade.symbol,
7625
+ deploymentId: trade.deploymentId,
7626
+ accountId: trade.accountId,
7421
7627
  lineage,
7422
7628
  expectedByScope
7423
7629
  });
7424
7630
  }
7425
- const window = deploymentWindows.get(
7426
- lineageScopeKey(trade.strategy, trade.symbol)
7427
- );
7631
+ const window = deploymentWindows.get(lineageScopeKey(trade));
7428
7632
  const signalTimestamp = trade.signalTimestamp ?? trade.entryTimestamp;
7429
7633
  return window != null && signalTimestamp >= window.firstTimestamp && signalTimestamp <= window.lastTimestamp;
7430
7634
  });
@@ -7432,17 +7636,24 @@ var filterReplayComparisonByLineage = ({
7432
7636
  ...comparableRuntimeSignals,
7433
7637
  ...comparableRuntimeEvaluations
7434
7638
  ]) {
7435
- const key = lineageScopeKey(artifact.strategy, artifact.symbol);
7639
+ const key = lineageScopeKey(artifact);
7436
7640
  const existing = deploymentWindows.get(key);
7437
7641
  deploymentWindows.set(key, {
7438
7642
  firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
7439
7643
  lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
7440
7644
  });
7441
7645
  }
7646
+ const replayScopeByStrategySymbol = new Map(
7647
+ replayLineages.map((record) => [
7648
+ `${record.strategy}::${record.symbol}`,
7649
+ lineageScopeKey(record)
7650
+ ])
7651
+ );
7442
7652
  const comparableBacktestEntries = backtestEntries.filter((entry) => {
7443
- const window = deploymentWindows.get(
7444
- lineageScopeKey(entry.strategy, entry.symbol)
7653
+ const replayScope = replayScopeByStrategySymbol.get(
7654
+ `${entry.strategy}::${entry.symbol}`
7445
7655
  );
7656
+ const window = replayScope ? deploymentWindows.get(replayScope) : null;
7446
7657
  const signalTimestamp = entry.signalTimestamp ?? entry.timestamp;
7447
7658
  return window != null && signalTimestamp >= window.firstTimestamp && signalTimestamp <= window.lastTimestamp;
7448
7659
  });
@@ -7464,10 +7675,12 @@ var filterReplayComparisonByLineage = ({
7464
7675
  excludedBacktestEntries: backtestEntries.length - comparableBacktestEntries.length,
7465
7676
  reason: comparableScopeKeys.size > 0 ? null : "no_runtime_artifacts_with_matching_lineage",
7466
7677
  replay: [...expectedByScope.entries()].map(([scope, lineage]) => {
7467
- const separator = scope.indexOf("::");
7678
+ const [deploymentId, accountId, strategy, symbol] = scope.split("::");
7468
7679
  return {
7469
- strategy: scope.slice(0, separator),
7470
- symbol: scope.slice(separator + 2),
7680
+ deploymentId,
7681
+ accountId,
7682
+ strategy,
7683
+ symbol,
7471
7684
  lineage
7472
7685
  };
7473
7686
  }).sort(
@@ -8871,10 +9084,69 @@ var finishReplay = async ({
8871
9084
  };
8872
9085
  var replayBacktest = async () => {
8873
9086
  resetRunState();
9087
+ const evidenceMetadata = replayRuntimeEvidencePath ? await loadReplayRuntimeEvidenceMetadata({
9088
+ filePath: replayRuntimeEvidencePath,
9089
+ projectRoot: replayProjectRoot
9090
+ }) : null;
9091
+ if (evidenceMetadata && evidenceMetadata.userName !== replayUserName) {
9092
+ throw new Error(
9093
+ `Runtime evidence user mismatch: expected=${replayUserName}, actual=${evidenceMetadata.userName || "missing"}`
9094
+ );
9095
+ }
9096
+ const replayComposition = evidenceMetadata ? {
9097
+ deployment: runtimeDeploymentFromEvidence(evidenceMetadata.deployment),
9098
+ strategies: activeRuntimeEvidenceStrategies(
9099
+ evidenceMetadata.deployment
9100
+ ).map(
9101
+ ({
9102
+ strategyName,
9103
+ version,
9104
+ strategyPackage,
9105
+ strategyPackageVersion,
9106
+ runtimePackageVersion,
9107
+ strategyConfig
9108
+ }) => ({
9109
+ strategyName,
9110
+ version,
9111
+ strategyPackage,
9112
+ strategyPackageVersion,
9113
+ runtimePackageVersion,
9114
+ strategyConfig
9115
+ })
9116
+ )
9117
+ } : await loadDeploymentReplayStrategies({
9118
+ userName: replayUserName,
9119
+ projectRoot: replayProjectRoot,
9120
+ deploymentId: replayDeploymentId
9121
+ });
9122
+ const { deployment, strategies: replayStrategies } = replayComposition;
9123
+ if (!replayStrategies.length) {
9124
+ throw new Error(`No enabled strategies in deployment ${deployment.id}`);
9125
+ }
9126
+ const strategyIntervals = new Set(
9127
+ replayStrategies.map(
9128
+ ({ strategyConfig }) => String(strategyConfig.INTERVAL)
9129
+ )
9130
+ );
9131
+ if (strategyIntervals.size !== 1 || !strategyIntervals.has(replayInterval)) {
9132
+ throw new Error(
9133
+ `Replay interval does not match deployment ${deployment.id}: replay=${replayInterval}, strategies=${[...strategyIntervals].join(",")}`
9134
+ );
9135
+ }
9136
+ const strategyUniverses = new Set(
9137
+ replayStrategies.map(
9138
+ ({ strategyConfig }) => String(strategyConfig.UNIVERSE)
9139
+ )
9140
+ );
9141
+ if (strategyUniverses.size !== 1) {
9142
+ throw new Error(
9143
+ `Replay requires one deployment universe: ${[...strategyUniverses].join(",")}`
9144
+ );
9145
+ }
8874
9146
  const preparedRun = await prepareRunEnvironment({
8875
- connector: replayFlags.connector,
9147
+ connector: deployment.connectorName,
8876
9148
  userName: replayUserName,
8877
- tickers: replayFlags.tickers,
9149
+ tickers: replayFlags.tickers ?? deployment.tickers,
8878
9150
  exclude: replayFlags.exclude,
8879
9151
  tickersLimit: replayFlags.tickersLimit,
8880
9152
  showTickersList: replayFlags.showTickersList,
@@ -8884,6 +9156,11 @@ var replayBacktest = async () => {
8884
9156
  cacheOnly: replayFlags.cacheOnly,
8885
9157
  interval: replayInterval,
8886
9158
  projectRoot: replayProjectRoot,
9159
+ universe: [...strategyUniverses][0],
9160
+ accountId: deployment.accountId,
9161
+ deploymentId: deployment.id,
9162
+ assetClasses: deployment.assetClasses,
9163
+ deployment,
8887
9164
  closedIntervalMs: (0, import_data2.intervalToMs)(replayInterval)
8888
9165
  });
8889
9166
  if (!preparedRun || isReplayUpdateOnlyRun) {
@@ -8897,10 +9174,6 @@ var replayBacktest = async () => {
8897
9174
  end: preparedRun.window.end
8898
9175
  }
8899
9176
  });
8900
- const replayStrategies = await loadReplayStrategies(replayUserName);
8901
- if (!replayStrategies.length) {
8902
- return;
8903
- }
8904
9177
  await prepareReplayBinanceMarketContext({
8905
9178
  ...preparedRun,
8906
9179
  aiEnabled: replayStrategies.some(