@tradejs/cli 3.1.8 → 3.1.9

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