@tradejs/cli 2.0.20 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -241,7 +241,7 @@ var agentRun_exports = {};
241
241
  __export(agentRun_exports, {
242
242
  main: () => main
243
243
  });
244
- var import_args, import_chalk, import_promises, import_node_os, import_node_path, import_node_child_process, import_constants, import_redis, import_userSettings, flags, projectRoot, AGENT_SYSTEM_PROMPT, trimTextTail, escapeHtml, isOpenRouterEndpoint, resolveChatCompletionsUrl, runCommand, runCommandOrThrow, listFilesRecursive, collectStrategyContextFiles, renderFileContext, buildPrompt, extractAssistantContent, requestAgentDiff, applyDiff, getChangedFilesFromGit, runValidationSuite, buildAgentTelegramReport, loadRun, saveAgentRun, resolveGithubRepository, createGithubPullRequest, createWorktree, cleanupWorktree, main;
244
+ var import_args, import_chalk, import_promises, import_node_os, import_node_path, import_node_child_process, import_constants, import_aiEndpoints, import_redis, import_userSettings, flags, projectRoot, AGENT_SYSTEM_PROMPT, trimTextTail, escapeHtml, isOpenRouterEndpoint, resolveChatCompletionsUrl, runCommand, runCommandOrThrow, listFilesRecursive, collectStrategyContextFiles, renderFileContext, buildPrompt, extractAssistantContent, requestAgentDiff, applyDiff, getChangedFilesFromGit, runValidationSuite, buildAgentTelegramReport, loadRun, saveAgentRun, resolveGithubRepository, createGithubPullRequest, createWorktree, cleanupWorktree, main;
245
245
  var init_agentRun = __esm({
246
246
  "src/scripts/agentRun.ts"() {
247
247
  "use strict";
@@ -252,6 +252,7 @@ var init_agentRun = __esm({
252
252
  import_node_path = __toESM(require("path"));
253
253
  import_node_child_process = require("child_process");
254
254
  import_constants = require("@tradejs/core/constants");
255
+ import_aiEndpoints = require("@tradejs/core/aiEndpoints");
255
256
  import_redis = require("@tradejs/infra/redis");
256
257
  import_userSettings = require("@tradejs/infra/userSettings");
257
258
  init_telegramReports();
@@ -439,35 +440,33 @@ Rules:
439
440
  };
440
441
  requestAgentDiff = async (userName2, messages) => {
441
442
  const settings = await (0, import_userSettings.getUserSettings)(userName2);
442
- if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
443
+ const aiApiEndpoint = (0, import_aiEndpoints.normalizeAiEndpoint)(settings.AI_API_ENDPOINT);
444
+ if (!settings.AI_API_KEY || !aiApiEndpoint) {
443
445
  throw new Error(`AI settings are incomplete for user ${userName2}`);
444
446
  }
445
- if (!isOpenRouterEndpoint(settings.AI_API_ENDPOINT)) {
447
+ if (!isOpenRouterEndpoint(aiApiEndpoint)) {
446
448
  throw new Error(
447
- `Research agent requires OpenRouter endpoint, got: ${settings.AI_API_ENDPOINT}`
449
+ `Research agent requires OpenRouter endpoint, got: ${aiApiEndpoint}`
448
450
  );
449
451
  }
450
- const response = await fetch(
451
- resolveChatCompletionsUrl(settings.AI_API_ENDPOINT),
452
- {
453
- method: "POST",
454
- headers: {
455
- Authorization: `Bearer ${settings.AI_API_KEY}`,
456
- "Content-Type": "application/json",
457
- "HTTP-Referer": "https://tradejs.dev",
458
- "X-Title": "TradeJS Research Agent"
452
+ const response = await fetch(resolveChatCompletionsUrl(aiApiEndpoint), {
453
+ method: "POST",
454
+ headers: {
455
+ Authorization: `Bearer ${settings.AI_API_KEY}`,
456
+ "Content-Type": "application/json",
457
+ "HTTP-Referer": "https://tradejs.dev",
458
+ "X-Title": "TradeJS Research Agent"
459
+ },
460
+ body: JSON.stringify({
461
+ model: RESEARCH_AGENT_MODEL,
462
+ messages,
463
+ reasoning: {
464
+ effort: RESEARCH_AGENT_REASONING_EFFORT,
465
+ exclude: true
459
466
  },
460
- body: JSON.stringify({
461
- model: RESEARCH_AGENT_MODEL,
462
- messages,
463
- reasoning: {
464
- effort: RESEARCH_AGENT_REASONING_EFFORT,
465
- exclude: true
466
- },
467
- temperature: 0.2
468
- })
469
- }
470
- );
467
+ temperature: 0.2
468
+ })
469
+ });
471
470
  if (!response.ok) {
472
471
  throw new Error(
473
472
  `OpenRouter request failed (${response.status}): ${await response.text()}`
@@ -1857,7 +1856,7 @@ var init_aiTrainMetrics = __esm({
1857
1856
  });
1858
1857
 
1859
1858
  // src/lib/aiTrainOptions.ts
1860
- var parseTimestampFilter, parseTrailingPeriodMs, parseQualityThresholds, parseTerminalWindowDays, parseDumpFeatureMode, hasCliOption, resolveAiTrainRecentLimit;
1859
+ var parseTimestampFilter, parseTrailingPeriodMs, parseQualityThresholds, parseTerminalWindowDays, parseAiTrainDirectionPolicy, assertAiTrainDirectionPolicy, parseDumpFeatureMode, hasCliOption, resolveAiTrainRecentLimit;
1861
1860
  var init_aiTrainOptions = __esm({
1862
1861
  "src/lib/aiTrainOptions.ts"() {
1863
1862
  "use strict";
@@ -1913,6 +1912,38 @@ var init_aiTrainOptions = __esm({
1913
1912
  const raw = String(value ?? "90,30,7").split(",").map((part) => Number(part.trim())).filter((part) => Number.isFinite(part)).map((part) => Math.trunc(part)).filter((part) => part > 0);
1914
1913
  return [...new Set(raw)].sort((left, right) => right - left);
1915
1914
  };
1915
+ parseAiTrainDirectionPolicy = (value) => {
1916
+ const normalized = String(value ?? "both").trim().toLowerCase().replaceAll("-", "_");
1917
+ if (normalized === "both" || normalized === "long_only" || normalized === "short_only" || normalized === "direction_aware") {
1918
+ return normalized;
1919
+ }
1920
+ throw new Error(
1921
+ `Invalid --directionPolicy value "${String(value)}". Use both, long_only, short_only, or direction_aware.`
1922
+ );
1923
+ };
1924
+ assertAiTrainDirectionPolicy = ({
1925
+ directionPolicy,
1926
+ byDirection,
1927
+ terminalWindows
1928
+ }) => {
1929
+ const suppressedDirection = directionPolicy === "long_only" ? "SHORT" : directionPolicy === "short_only" ? "LONG" : null;
1930
+ if (!suppressedDirection) return;
1931
+ for (const scope of [{ label: "full", byDirection }, ...terminalWindows]) {
1932
+ const summary = scope.byDirection.find(
1933
+ (entry) => entry.direction === suppressedDirection
1934
+ );
1935
+ if (!summary) {
1936
+ throw new Error(
1937
+ `${scope.label} ${suppressedDirection} direction summary is required for ${directionPolicy}`
1938
+ );
1939
+ }
1940
+ if (summary.summary.approved !== 0) {
1941
+ throw new Error(
1942
+ `${scope.label} has ${summary.summary.approved} ${suppressedDirection} approved rows under ${directionPolicy}`
1943
+ );
1944
+ }
1945
+ }
1946
+ };
1916
1947
  parseDumpFeatureMode = (value) => {
1917
1948
  const text = String(value ?? "none").trim().toLowerCase();
1918
1949
  if (!text || text === "none" || text === "false" || text === "0") {
@@ -4333,7 +4364,7 @@ var aiPocketSearch_exports = {};
4333
4364
  __export(aiPocketSearch_exports, {
4334
4365
  main: () => main3
4335
4366
  });
4336
- var import_args3, import_chalk4, import_promises4, import_path2, import_progress2, import_ai3, import_ai4, ListIt2, flags3, createListIt2, createTable2, formatRatio, formatProfit, formatNumber2, colorizeRatio, colorizeProfit, colorizeNumber, toReportTimestamp, markdownPlainTable, buildPendingMarkdownReport, normalizeInt, normalizePositiveInt, normalizeNonNegativeNumber, normalizeRatio, normalizeUnitRatio, normalizeFeatureProfile, normalizeFeaturePolicy, normalizeCoverageMode, normalizeCadenceMode, normalizeObjective, normalizeQuality, isAiApproval, listMergedFiles, deriveStrategyNameFromFile, getMergedGroupId, resolveMarkdownReportPath, sortDatasetPartPaths, resolveMergedDatasetFiles, getDatasetRowTimestamp, findMaxSelectedTimestamp, getDeterministicModelCandidate, resolveScopeRows, printSection, buildSummaryRows, buildPocketRows, buildQualityRows, tickProgressBarTo, searchPhaseLabels, hasCliOption2, main3;
4367
+ var import_args3, import_chalk4, import_promises4, import_path2, import_progress2, import_ai3, import_ai4, import_registry, ListIt2, flags3, createListIt2, createTable2, formatRatio, formatProfit, formatNumber2, colorizeRatio, colorizeProfit, colorizeNumber, toReportTimestamp, markdownPlainTable, buildPendingMarkdownReport, normalizeInt, normalizePositiveInt, normalizeNonNegativeNumber, normalizeRatio, normalizeUnitRatio, normalizeFeatureProfile, normalizeFeaturePolicy, normalizeCoverageMode, normalizeCadenceMode, normalizeObjective, normalizeQuality, isAiApproval, listMergedFiles, deriveStrategyNameFromFile, getMergedGroupId, resolveMarkdownReportPath, sortDatasetPartPaths, resolveMergedDatasetFiles, getDatasetRowTimestamp, findMaxSelectedTimestamp, getDeterministicModelCandidate, resolveScopeRows, printSection, buildSummaryRows, buildPocketRows, buildQualityRows, tickProgressBarTo, searchPhaseLabels, hasCliOption2, main3;
4337
4368
  var init_aiPocketSearch2 = __esm({
4338
4369
  "src/scripts/aiPocketSearch.ts"() {
4339
4370
  "use strict";
@@ -4344,6 +4375,7 @@ var init_aiPocketSearch2 = __esm({
4344
4375
  import_progress2 = __toESM(require("progress"));
4345
4376
  import_ai3 = require("@tradejs/infra/ai");
4346
4377
  import_ai4 = require("@tradejs/node/ai");
4378
+ import_registry = require("@tradejs/node/registry");
4347
4379
  init_aiTrainDataset();
4348
4380
  init_aiTrainMetrics();
4349
4381
  init_aiTrainOptions();
@@ -5057,7 +5089,7 @@ var init_aiPocketSearch2 = __esm({
5057
5089
  const explicitReportFile = String(flags3.reportFile || "").trim();
5058
5090
  const generatedAt = Date.now();
5059
5091
  const excludedFeaturePaths = /* @__PURE__ */ new Map();
5060
- await (0, import_ai4.ensureAiStrategyPluginsLoaded)();
5092
+ await (0, import_registry.ensureStrategyPluginsLoaded)();
5061
5093
  const filePaths = await resolveMergedDatasetFiles({
5062
5094
  outDir,
5063
5095
  strategyName,
@@ -6856,7 +6888,7 @@ var aiTrain_exports = {};
6856
6888
  __export(aiTrain_exports, {
6857
6889
  main: () => main4
6858
6890
  });
6859
- var import_args4, import_chalk5, import_promises7, import_path3, import_progress3, import_constants3, import_ai5, import_strategyReleaseEvidence, import_redis3, import_ai6, import_constants4, ListIt3, flags4, createListIt3, createTable3, formatRatio3, formatProfit2, formatMetricNumber, colorizeRatio2, colorizePercent, colorizeProfit2, colorizeMetricNumber, colorizeQuality, normalizeInt2, normalizePositiveInt2, normalizeNonNegativeNumber2, normalizeQuality2, isProfitableTrade, isAiApproval2, listMergedFiles2, deriveStrategyNameFromFile2, getMergedGroupId2, sortDatasetPartPaths2, resolveMergedDatasetFiles2, resolvePromptRunContext, getDeterministicGateContext, resolveDeterministicGateEvaluation, summarizeDeterministicGateEvaluations, printSection2, persistAiChartSnapshot, resolveDatasetFiles, getDatasetRowTimestamp2, findMaxSelectedTimestamp2, main4;
6891
+ var import_args4, import_chalk5, import_promises7, import_path3, import_progress3, import_constants3, import_ai5, import_strategyReleaseEvidence, import_redis3, import_ai6, import_registry2, import_constants4, ListIt3, flags4, createListIt3, createTable3, formatRatio3, formatProfit2, formatMetricNumber, colorizeRatio2, colorizePercent, colorizeProfit2, colorizeMetricNumber, colorizeQuality, normalizeInt2, normalizePositiveInt2, normalizeNonNegativeNumber2, normalizeQuality2, isProfitableTrade, isAiApproval2, listMergedFiles2, deriveStrategyNameFromFile2, getMergedGroupId2, sortDatasetPartPaths2, resolveMergedDatasetFiles2, resolvePromptRunContext, getDeterministicGateContext, resolveDeterministicGateEvaluation, summarizeDeterministicGateEvaluations, printSection2, persistAiChartSnapshot, resolveDatasetFiles, getDatasetRowTimestamp2, findMaxSelectedTimestamp2, main4;
6860
6892
  var init_aiTrain = __esm({
6861
6893
  "src/scripts/aiTrain.ts"() {
6862
6894
  "use strict";
@@ -6870,6 +6902,7 @@ var init_aiTrain = __esm({
6870
6902
  import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
6871
6903
  import_redis3 = require("@tradejs/infra/redis");
6872
6904
  import_ai6 = require("@tradejs/node/ai");
6905
+ import_registry2 = require("@tradejs/node/registry");
6873
6906
  import_constants4 = require("@tradejs/node/constants");
6874
6907
  init_aiTrainCharts();
6875
6908
  init_aiTrainMetrics();
@@ -6911,6 +6944,11 @@ var init_aiTrain = __esm({
6911
6944
  "Minimum AI quality required to approve entry",
6912
6945
  4
6913
6946
  );
6947
+ import_args4.default.option(
6948
+ ["Y", "directionPolicy"],
6949
+ "Evidence label for the enforced gate policy: both, long_only, short_only, or direction_aware",
6950
+ "both"
6951
+ );
6914
6952
  import_args4.default.option(
6915
6953
  ["l", "localOnly"],
6916
6954
  "Replay deterministic adapter gate without AI provider calls",
@@ -7301,6 +7339,7 @@ var init_aiTrain = __esm({
7301
7339
  });
7302
7340
  const qualityThresholds = parseQualityThresholds(flags4.qualityThresholds);
7303
7341
  const terminalWindowDays = parseTerminalWindowDays(flags4.terminalWindows);
7342
+ const directionPolicy = parseAiTrainDirectionPolicy(flags4.directionPolicy);
7304
7343
  const dumpEvaluationsPath = String(flags4.dumpEvaluations || "").trim();
7305
7344
  const dumpFeatureMode = parseDumpFeatureMode(flags4.dumpFeatures);
7306
7345
  if (dumpFeatureMode !== "none" && !dumpEvaluationsPath) {
@@ -7322,7 +7361,7 @@ var init_aiTrain = __esm({
7322
7361
  const userName2 = String(flags4.user || "root").trim() || "root";
7323
7362
  const model = String(flags4.model || import_ai6.DEFAULT_AI_MODEL).trim() || import_ai6.DEFAULT_AI_MODEL;
7324
7363
  const parallel = normalizePositiveInt2(flags4.parallel, import_constants4.AI_CONCURRENCY_LIMIT);
7325
- await (0, import_ai6.ensureAiStrategyPluginsLoaded)();
7364
+ await (0, import_registry2.ensureStrategyPluginsLoaded)();
7326
7365
  const filePaths = await resolveDatasetFiles();
7327
7366
  const datasetId = getMergedGroupId2(filePaths[0] || "")?.mergeId;
7328
7367
  const { totalRows, selectedRows } = await (0, import_ai5.countAiDatasetRows)({
@@ -7515,18 +7554,29 @@ var init_aiTrain = __esm({
7515
7554
  finalEvaluations,
7516
7555
  terminalWindowDays
7517
7556
  );
7518
- const topRejectReasons = summarizeAiTrainRejectReasons(finalEvaluations);
7519
- const lineage = await buildAiTrainLineage({
7520
- projectRoot: String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd(),
7521
- strategyName,
7522
- configIds: finalEvaluations.map((evaluation) => evaluation.configId),
7523
- sourceSha256s: await Promise.all(filePaths.map(import_strategyReleaseEvidence.strategyEvidenceFileSha256)),
7524
- runContext: {
7525
- mode: localOnly ? "local-deterministic" : "llm",
7526
- model: localOnly ? "local-deterministic" : model,
7527
- minQuality
7528
- }
7557
+ assertAiTrainDirectionPolicy({
7558
+ directionPolicy,
7559
+ byDirection: directionSummaries,
7560
+ terminalWindows
7529
7561
  });
7562
+ const topRejectReasons = summarizeAiTrainRejectReasons(finalEvaluations);
7563
+ const lineage = {
7564
+ ...await buildAiTrainLineage({
7565
+ projectRoot: String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd(),
7566
+ strategyName,
7567
+ configIds: finalEvaluations.map((evaluation) => evaluation.configId),
7568
+ sourceSha256s: await Promise.all(
7569
+ filePaths.map(import_strategyReleaseEvidence.strategyEvidenceFileSha256)
7570
+ ),
7571
+ runContext: {
7572
+ mode: localOnly ? "local-deterministic" : "llm",
7573
+ model: localOnly ? "local-deterministic" : model,
7574
+ minQuality,
7575
+ directionPolicy
7576
+ }
7577
+ }),
7578
+ directionPolicy
7579
+ };
7530
7580
  const evaluated = summary.correct + summary.incorrect;
7531
7581
  const result = {
7532
7582
  run: {
@@ -7548,7 +7598,8 @@ var init_aiTrain = __esm({
7548
7598
  model: localOnly ? "local-deterministic" : model,
7549
7599
  parallel: concurrency,
7550
7600
  dumpFeatures: dumpFeatureMode,
7551
- terminalWindows: terminalWindowDays
7601
+ terminalWindows: terminalWindowDays,
7602
+ directionPolicy
7552
7603
  },
7553
7604
  outcome: summary,
7554
7605
  byDirection: directionSummaries,
@@ -11495,13 +11546,13 @@ var init_hyperliquidWhaleBackfill = __esm({
11495
11546
  });
11496
11547
 
11497
11548
  // src/lib/marketContextPrepare.ts
11498
- var import_chalk9, import_marketContext3, import_registry, uniqueReasons, shouldPrepareDerivativesContextForRun, shouldPrepareBinanceMarketContextForRun, shouldPrepareCoinMarketCapContextForRun, shouldPrepareHyperliquidWhaleContextForRun, resolveCoreContextSources, resolveMarketContextRunRequirements, resolveCoinMarketCapBackfillForMode, buildCoinMarketCapBackfillParams, buildBinanceMarketBackfillParams, buildDerivativesBackfillParams, prepareMarketContextForRun;
11549
+ var import_chalk9, import_marketContext3, import_registry3, uniqueReasons, shouldPrepareDerivativesContextForRun, shouldPrepareBinanceMarketContextForRun, shouldPrepareCoinMarketCapContextForRun, shouldPrepareHyperliquidWhaleContextForRun, resolveCoreContextSources, resolveMarketContextRunRequirements, resolveCoinMarketCapBackfillForMode, buildCoinMarketCapBackfillParams, buildBinanceMarketBackfillParams, buildDerivativesBackfillParams, prepareMarketContextForRun;
11499
11550
  var init_marketContextPrepare = __esm({
11500
11551
  "src/lib/marketContextPrepare.ts"() {
11501
11552
  "use strict";
11502
11553
  import_chalk9 = __toESM(require("chalk"));
11503
11554
  import_marketContext3 = require("@tradejs/infra/timescale/marketContext");
11504
- import_registry = require("@tradejs/node/registry");
11555
+ import_registry3 = require("@tradejs/node/registry");
11505
11556
  init_binanceMarketContextBackfill();
11506
11557
  init_derivativesContextBackfill();
11507
11558
  init_coinMarketCapContextBackfill();
@@ -11579,11 +11630,11 @@ var init_marketContextPrepare = __esm({
11579
11630
  resolveCoreContextSources = async (params) => {
11580
11631
  const strategyNames = [...new Set(params.strategyNames ?? [])];
11581
11632
  if (!strategyNames.length) return [];
11582
- await (0, import_registry.ensureStrategyPluginsLoaded)(params.projectRoot);
11633
+ await (0, import_registry3.ensureStrategyPluginsLoaded)(params.projectRoot);
11583
11634
  return [
11584
11635
  ...new Set(
11585
11636
  strategyNames.flatMap(
11586
- (strategyName) => (0, import_registry.getStrategyManifest)(strategyName, params.projectRoot)?.contextRequirements?.core ?? []
11637
+ (strategyName) => (0, import_registry3.getStrategyManifest)(strategyName, params.projectRoot)?.contextRequirements?.core ?? []
11587
11638
  )
11588
11639
  )
11589
11640
  ];
@@ -21566,7 +21617,7 @@ var init_comparison = __esm({
21566
21617
  });
21567
21618
 
21568
21619
  // src/lib/coreResearch/dataset.ts
21569
- var import_node_crypto8, import_node_fs4, import_node_path10, import_node_readline, asRecord3, finiteNumber4, normalizeDirection, findStrategySetupIdentity, classifyBreadth, classifyDerivatives, resolveCoreResearchRegime, normalizeCompletedTrade, completedTradeIdentity, compareText, compareTrades, readCoreResearchVariant;
21620
+ var import_node_crypto8, import_node_fs4, import_node_path10, import_node_readline, asRecord3, finiteNumber4, normalizeDirection, findStrategySetupIdentity, classifyBreadth, classifyDerivatives, resolveCoreResearchRegime, normalizeCompletedTrade, completedTradeIdentity, positionCycleIdentity, sumBy, weightedPrice, aggregatePositionCycles, compareText, compareTrades, readCoreResearchVariant;
21570
21621
  var init_dataset = __esm({
21571
21622
  "src/lib/coreResearch/dataset.ts"() {
21572
21623
  "use strict";
@@ -21687,6 +21738,7 @@ var init_dataset = __esm({
21687
21738
  runId: row.backtestRunId ?? null,
21688
21739
  configId: row.configId?.trim() || "<missing-config-id>",
21689
21740
  signalId,
21741
+ positionCycleId: typeof result.positionCycleId === "string" && result.positionCycleId.trim() ? result.positionCycleId.trim() : null,
21690
21742
  setupIdentity,
21691
21743
  setupIdentitySource: explicitIdentity ? "research.setupIdentity" : strategyIdentity ? "strategy-context" : "signal-time-fallback",
21692
21744
  strategy: row.strategyName,
@@ -21710,6 +21762,69 @@ var init_dataset = __esm({
21710
21762
  };
21711
21763
  };
21712
21764
  completedTradeIdentity = (trade) => [trade.runId ?? "", trade.configId, trade.signalId, trade.symbol].join("|");
21765
+ positionCycleIdentity = (trade) => trade.positionCycleId ? [
21766
+ trade.runId ?? "",
21767
+ trade.configId,
21768
+ trade.strategy,
21769
+ trade.symbol,
21770
+ trade.direction,
21771
+ trade.positionCycleId
21772
+ ].join("|") : null;
21773
+ sumBy = (trades, select) => trades.reduce((sum3, trade) => sum3 + select(trade), 0);
21774
+ weightedPrice = (trades, select) => {
21775
+ const withPrice = trades.filter((trade) => select(trade) != null);
21776
+ const qty = sumBy(withPrice, (trade) => trade.qty);
21777
+ if (qty <= 0) return null;
21778
+ return sumBy(withPrice, (trade) => Number(select(trade)) * trade.qty) / qty;
21779
+ };
21780
+ aggregatePositionCycles = (trades) => {
21781
+ const standalone = [];
21782
+ const cycles = /* @__PURE__ */ new Map();
21783
+ for (const trade of trades) {
21784
+ const identity = positionCycleIdentity(trade);
21785
+ if (!identity) {
21786
+ standalone.push(trade);
21787
+ continue;
21788
+ }
21789
+ const cycle = cycles.get(identity) ?? [];
21790
+ cycle.push(trade);
21791
+ cycles.set(identity, cycle);
21792
+ }
21793
+ for (const [identity, cycle] of cycles) {
21794
+ const primaryRows = cycle.filter(
21795
+ (trade) => trade.signalId === trade.positionCycleId
21796
+ );
21797
+ if (primaryRows.length !== 1) {
21798
+ throw new Error(
21799
+ `Position cycle ${identity} must contain exactly one opening row; found ${primaryRows.length}`
21800
+ );
21801
+ }
21802
+ const primary = primaryRows[0];
21803
+ if (cycle.some(
21804
+ (trade) => trade.exitTimestamp !== primary.exitTimestamp || trade.exitReason !== primary.exitReason
21805
+ )) {
21806
+ throw new Error(
21807
+ `Position cycle ${identity} contains inconsistent final exits`
21808
+ );
21809
+ }
21810
+ if (cycle.length === 1) {
21811
+ standalone.push(primary);
21812
+ continue;
21813
+ }
21814
+ standalone.push({
21815
+ ...primary,
21816
+ entryTimestamp: Math.min(...cycle.map((trade) => trade.entryTimestamp)),
21817
+ entryPrice: weightedPrice(cycle, (trade) => trade.entryPrice) ?? 0,
21818
+ exitPrice: weightedPrice(cycle, (trade) => trade.exitPrice),
21819
+ qty: sumBy(cycle, (trade) => trade.qty),
21820
+ netProfit: sumBy(cycle, (trade) => trade.netProfit),
21821
+ grossProfit: sumBy(cycle, (trade) => trade.grossProfit),
21822
+ totalFee: sumBy(cycle, (trade) => trade.totalFee),
21823
+ totalSlippageCost: sumBy(cycle, (trade) => trade.totalSlippageCost)
21824
+ });
21825
+ }
21826
+ return standalone;
21827
+ };
21713
21828
  compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
21714
21829
  compareTrades = (left, right) => left.exitTimestamp - right.exitTimestamp || compareText(left.symbol, right.symbol) || compareText(left.direction, right.direction) || compareText(left.signalId, right.signalId) || compareText(left.sourceFile, right.sourceFile) || left.sourceLine - right.sourceLine;
21715
21830
  readCoreResearchVariant = async (variant) => {
@@ -21786,7 +21901,7 @@ var init_dataset = __esm({
21786
21901
  variant,
21787
21902
  files: sources,
21788
21903
  duplicateRowsDropped,
21789
- trades: trades.sort(compareTrades)
21904
+ trades: aggregatePositionCycles(trades).sort(compareTrades)
21790
21905
  };
21791
21906
  };
21792
21907
  }
@@ -22293,13 +22408,17 @@ var init_orchestrator = __esm({
22293
22408
  const loaded = await Promise.all(
22294
22409
  analysisSpec.variants.map(readCoreResearchVariant)
22295
22410
  );
22411
+ const periodDays = (spec.window.end - spec.window.start) / DAY_MS11;
22296
22412
  const windowed = loaded.map((entry) => ({
22297
22413
  ...entry,
22414
+ reconciliationMetrics: summarizeCoreResearchCohorts(
22415
+ entry.trades,
22416
+ periodDays
22417
+ ).ALL,
22298
22418
  trades: entry.trades.filter(
22299
22419
  (trade) => trade.exitTimestamp >= spec.window.start && trade.exitTimestamp < spec.window.end
22300
22420
  )
22301
22421
  }));
22302
- const periodDays = (spec.window.end - spec.window.start) / DAY_MS11;
22303
22422
  const analyses = await Promise.all(
22304
22423
  windowed.map(async (entry) => ({
22305
22424
  variant: entry.variant,
@@ -22357,12 +22476,9 @@ var init_orchestrator = __esm({
22357
22476
  reconciliation: await reconcileCoreResearchVariant({
22358
22477
  variant: entry.variant,
22359
22478
  spec,
22360
- exportMetrics: summarizeCoreResearchWindow({
22361
- trades: entry.trades,
22362
- label: "full",
22363
- start: spec.window.start,
22364
- end: spec.window.end
22365
- }).cohorts.ALL
22479
+ // Redis result.stat includes the runner's terminal close at window.end.
22480
+ // Keep half-open research metrics, but reconcile the complete run export.
22481
+ exportMetrics: entry.reconciliationMetrics
22366
22482
  }),
22367
22483
  supplemental: {
22368
22484
  coldStart: Object.fromEntries(
@@ -24654,7 +24770,7 @@ var init_runtimeEvidenceArtifacts = __esm({
24654
24770
  });
24655
24771
 
24656
24772
  // src/lib/runtimeLineage.ts
24657
- var import_node_child_process6, import_node_crypto11, import_promises22, import_node_path16, import_types8, import_strategies11, RUNTIME_CONTEXT_ENV_KEYS, RUNTIME_CONTEXT_DATA_MODEL, normalizeForStableJson2, normalizeRuntimeLogicConfig, resolveRuntimeMaxLossValue, fingerprintRuntimeValue, gitLineageCache, gateFingerprintCache, binanceBreadthFingerprintCache, snapshotFingerprintCache, resolveGitLineage, readOptionalFile, resolveBinanceBreadthFingerprint, resolveSnapshotFingerprint, resolveRuntimeGateFingerprint, buildRuntimeLineage, runtimeLineageKey, runtimeLineagesMatch;
24773
+ var import_node_child_process6, import_node_crypto11, import_promises22, import_node_path16, import_types8, import_strategies11, import_strategyReleaseEvidence2, RUNTIME_CONTEXT_ENV_KEYS, RUNTIME_CONTEXT_DATA_MODEL, normalizeForStableJson2, resolveRuntimeMaxLossValue, fingerprintRuntimeValue, gitLineageCache, gateFingerprintCache, binanceBreadthFingerprintCache, snapshotFingerprintCache, resolveGitLineage, readOptionalFile, resolveBinanceBreadthFingerprint, resolveSnapshotFingerprint, resolveRuntimeGateFingerprint, buildRuntimeLineage, runtimeLineageKey, runtimeLineagesMatch;
24658
24774
  var init_runtimeLineage = __esm({
24659
24775
  "src/lib/runtimeLineage.ts"() {
24660
24776
  "use strict";
@@ -24664,6 +24780,7 @@ var init_runtimeLineage = __esm({
24664
24780
  import_node_path16 = __toESM(require("path"));
24665
24781
  import_types8 = require("@tradejs/types");
24666
24782
  import_strategies11 = require("@tradejs/node/strategies");
24783
+ import_strategyReleaseEvidence2 = require("@tradejs/infra/strategyReleaseEvidence");
24667
24784
  init_strategyGateFingerprint();
24668
24785
  RUNTIME_CONTEXT_ENV_KEYS = [
24669
24786
  "AI_MODE",
@@ -24714,17 +24831,6 @@ var init_runtimeLineage = __esm({
24714
24831
  }
24715
24832
  return value;
24716
24833
  };
24717
- normalizeRuntimeLogicConfig = (value) => {
24718
- if (Array.isArray(value)) {
24719
- return value.map(normalizeRuntimeLogicConfig);
24720
- }
24721
- if (value && typeof value === "object") {
24722
- return Object.fromEntries(
24723
- Object.entries(value).filter(([key]) => key !== "MAX_LOSS_VALUE").sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeRuntimeLogicConfig(entry)])
24724
- );
24725
- }
24726
- return value;
24727
- };
24728
24834
  resolveRuntimeMaxLossValue = (config2) => {
24729
24835
  if (!config2 || typeof config2 !== "object" || Array.isArray(config2)) {
24730
24836
  return null;
@@ -24888,9 +24994,7 @@ var init_runtimeLineage = __esm({
24888
24994
  })).gateFingerprint,
24889
24995
  // Position risk is tracked separately so changing trade size does not
24890
24996
  // masquerade as a change to the core + gate decision logic.
24891
- configFingerprint: fingerprintRuntimeValue(
24892
- normalizeRuntimeLogicConfig(config2)
24893
- ),
24997
+ configFingerprint: (0, import_strategyReleaseEvidence2.strategyLogicConfigFingerprint)(config2),
24894
24998
  contextFingerprint: fingerprintRuntimeValue(context),
24895
24999
  maxLossValue: resolveRuntimeMaxLossValue(config2)
24896
25000
  };
@@ -25880,7 +25984,7 @@ function assert2(condition, message) {
25880
25984
  async function collectReleaseEvidenceReferences(references) {
25881
25985
  return Promise.all(
25882
25986
  references.map(async (reference) => {
25883
- const actualSha256 = await (0, import_strategyReleaseEvidence2.strategyEvidenceFileSha256)(reference.path);
25987
+ const actualSha256 = await (0, import_strategyReleaseEvidence3.strategyEvidenceFileSha256)(reference.path);
25884
25988
  assert2(
25885
25989
  actualSha256 === reference.sha256,
25886
25990
  `evidence ${reference.artifactId} checksum mismatch`
@@ -25960,9 +26064,10 @@ async function collectReleaseEvidenceReferences(references) {
25960
26064
  if (reference.kind === "ai_gate") {
25961
26065
  const outcome = nested(artifact.outcome);
25962
26066
  const approvedRisk = nested(outcome?.approvedRisk);
25963
- const terminalWindows = array(
25964
- nested(artifact.research)?.terminalWindows
25965
- ).map(nested).filter((entry) => entry != null);
26067
+ const run = nested(artifact.run);
26068
+ const research = nested(artifact.research);
26069
+ const researchLineage = nested(research?.lineage);
26070
+ const terminalWindows = array(research?.terminalWindows).map(nested).filter((entry) => entry != null);
25966
26071
  const hasLongShortDirections = (value) => {
25967
26072
  const directions = new Map(
25968
26073
  array(value).map(nested).filter(
@@ -25976,6 +26081,22 @@ async function collectReleaseEvidenceReferences(references) {
25976
26081
  const directionalEvidenceComplete = hasLongShortDirections(artifact.byDirection) && terminalWindows.every(
25977
26082
  (window) => hasLongShortDirections(window.byDirection)
25978
26083
  );
26084
+ const directionPolicy = finiteDirectionPolicy(researchLineage?.directionPolicy) ?? "both";
26085
+ const runDirectionPolicy = finiteDirectionPolicy(run?.directionPolicy) ?? "both";
26086
+ const suppressedDirection = directionPolicy === "long_only" ? "SHORT" : directionPolicy === "short_only" ? "LONG" : null;
26087
+ const approvedForDirection = (value, direction) => {
26088
+ const entry = array(value).map(nested).find((candidate) => candidate?.direction === direction);
26089
+ return Number(nested(entry?.summary)?.approved ?? Number.NaN);
26090
+ };
26091
+ const directionPolicyValid = runDirectionPolicy === directionPolicy && (suppressedDirection == null || approvedForDirection(
26092
+ artifact.byDirection,
26093
+ suppressedDirection
26094
+ ) === 0 && terminalWindows.every(
26095
+ (window) => approvedForDirection(
26096
+ window.byDirection,
26097
+ suppressedDirection
26098
+ ) === 0
26099
+ ));
25979
26100
  const terminalsPass = terminalWindows.length > 0 && terminalWindows.every((window) => {
25980
26101
  const terminalOutcome = nested(window.outcome);
25981
26102
  return window.complete === true && Number(terminalOutcome?.approved ?? 0) > 0 && Number(
@@ -25983,7 +26104,7 @@ async function collectReleaseEvidenceReferences(references) {
25983
26104
  ) > 0;
25984
26105
  });
25985
26106
  return {
25986
- aiGateAddsValue: Number(outcome?.expectancyDelta ?? Number.NEGATIVE_INFINITY) > 0 && Number(approvedRisk?.totalProfit ?? Number.NEGATIVE_INFINITY) > 0 && Number(approvedRisk?.profitFactor ?? Number.NEGATIVE_INFINITY) > 1 && directionalEvidenceComplete && terminalsPass
26107
+ aiGateAddsValue: Number(outcome?.expectancyDelta ?? Number.NEGATIVE_INFINITY) > 0 && Number(approvedRisk?.totalProfit ?? Number.NEGATIVE_INFINITY) > 0 && Number(approvedRisk?.profitFactor ?? Number.NEGATIVE_INFINITY) > 1 && directionalEvidenceComplete && directionPolicyValid && terminalsPass
25987
26108
  };
25988
26109
  }
25989
26110
  if (reference.kind === "runtime_parity") {
@@ -26018,7 +26139,7 @@ async function collectReleaseEvidenceReferences(references) {
26018
26139
  })
26019
26140
  );
26020
26141
  }
26021
- var import_node_crypto12, import_promises25, import_node_path20, import_types9, import_strategyReleaseEvidence2, SHA256_RE2, LINEAGE_FINGERPRINT_RE, DAY_MS12, REQUIRED_LONG_WINDOWS, REQUIRED_STABLE_TERMINAL_WINDOWS, MIN_RECENT_DIRECTION_REPAIR_TRADES, safeSegment3, compactTimestamp2, verifyFullPeriodChartArtifact, hasExactRuntimeTarget, deriveStrategyReleaseResearchDecision, canonicalStrategyReleaseJson, strategyReleaseSha256, assertFingerprint, assertLineageFingerprint, releaseReasons, deriveReleaseGates, verifyEvidenceComposition, createStrategyReleaseManifest, createStrategyReleaseEnvelope, verifyStrategyReleaseEnvelope, verifyStrategyEvidenceMarkerEnvelope, releaseMarkers, writeJsonAtomic2, publishStrategyRelease, buildStrategyLiveDiagnosis, buildStrategyLiveDiagnosisFromScorecard, realizedDrawdown, lowerBoundExitTimestamp, percentile2, buildStrategyMonitoringProfile, publishStrategyLiveDiagnosis, planStrategyEvidenceRetention, finiteString3, finiteNumber6, uniqueStrings, incompleteEvidenceLineage, extractEvidenceLineage, verifyCompletedCoreResearchBundle;
26142
+ var import_node_crypto12, import_promises25, import_node_path20, import_types9, import_strategyReleaseEvidence3, SHA256_RE2, LINEAGE_FINGERPRINT_RE, DAY_MS12, REQUIRED_LONG_WINDOWS, REQUIRED_STABLE_TERMINAL_WINDOWS, MIN_RECENT_DIRECTION_REPAIR_TRADES, safeSegment3, compactTimestamp2, verifyFullPeriodChartArtifact, hasExactRuntimeTarget, deriveStrategyReleaseResearchDecision, canonicalStrategyReleaseJson, strategyReleaseSha256, assertFingerprint, assertLineageFingerprint, releaseReasons, deriveReleaseGates, verifyEvidenceComposition, createStrategyReleaseManifest, createStrategyReleaseEnvelope, verifyStrategyReleaseEnvelope, verifyStrategyEvidenceMarkerEnvelope, releaseMarkers, writeJsonAtomic2, publishStrategyRelease, buildStrategyLiveDiagnosis, buildStrategyLiveDiagnosisFromScorecard, realizedDrawdown, lowerBoundExitTimestamp, percentile2, buildStrategyMonitoringProfile, publishStrategyLiveDiagnosis, planStrategyEvidenceRetention, finiteString3, finiteNumber6, finiteDirectionPolicy, uniqueStrings, incompleteEvidenceLineage, extractEvidenceLineage, verifyCompletedCoreResearchBundle;
26022
26143
  var init_strategyRelease = __esm({
26023
26144
  "src/lib/strategyRelease.ts"() {
26024
26145
  "use strict";
@@ -26026,21 +26147,21 @@ var init_strategyRelease = __esm({
26026
26147
  import_promises25 = __toESM(require("fs/promises"));
26027
26148
  import_node_path20 = __toESM(require("path"));
26028
26149
  import_types9 = require("@tradejs/types");
26029
- import_strategyReleaseEvidence2 = require("@tradejs/infra/strategyReleaseEvidence");
26150
+ import_strategyReleaseEvidence3 = require("@tradejs/infra/strategyReleaseEvidence");
26030
26151
  SHA256_RE2 = /^[a-f0-9]{64}$/;
26031
26152
  LINEAGE_FINGERPRINT_RE = /^[a-f0-9]{16}$/;
26032
26153
  DAY_MS12 = 864e5;
26033
26154
  REQUIRED_LONG_WINDOWS = [1095, 1460, 1825];
26034
26155
  REQUIRED_STABLE_TERMINAL_WINDOWS = [365, 180, 90];
26035
26156
  MIN_RECENT_DIRECTION_REPAIR_TRADES = 20;
26036
- safeSegment3 = import_strategyReleaseEvidence2.safeStrategyEvidenceSegment;
26037
- compactTimestamp2 = import_strategyReleaseEvidence2.compactStrategyEvidenceTimestamp;
26157
+ safeSegment3 = import_strategyReleaseEvidence3.safeStrategyEvidenceSegment;
26158
+ compactTimestamp2 = import_strategyReleaseEvidence3.compactStrategyEvidenceTimestamp;
26038
26159
  verifyFullPeriodChartArtifact = async (input) => {
26039
26160
  const artifact = input.chartArtifact;
26040
26161
  if (!artifact?.path || !SHA256_RE2.test(artifact.sha256)) return false;
26041
26162
  try {
26042
26163
  const artifactPath = import_node_path20.default.resolve(artifact.path);
26043
- if (await (0, import_strategyReleaseEvidence2.strategyEvidenceFileSha256)(artifactPath) !== artifact.sha256) {
26164
+ if (await (0, import_strategyReleaseEvidence3.strategyEvidenceFileSha256)(artifactPath) !== artifact.sha256) {
26044
26165
  return false;
26045
26166
  }
26046
26167
  const report = JSON.parse(await import_promises25.default.readFile(artifactPath, "utf8"));
@@ -26056,6 +26177,12 @@ var init_strategyRelease = __esm({
26056
26177
  );
26057
26178
  deriveStrategyReleaseResearchDecision = async (input) => {
26058
26179
  const blockers = [];
26180
+ const directionPolicy = input.directionPolicy ?? "both";
26181
+ const activeDirections = directionPolicy === "long_only" ? ["long"] : directionPolicy === "short_only" ? ["short"] : ["long", "short"];
26182
+ const suppressedDirection = directionPolicy === "long_only" ? "short" : directionPolicy === "short_only" ? "long" : null;
26183
+ const hasHistoricalEdge = (entry) => entry.pnl > 0 && entry.profitFactor >= 1 && activeDirections.every(
26184
+ (direction) => entry[direction].pnl > 0 && entry[direction].profitFactor >= 1
26185
+ ) && (suppressedDirection == null || entry[suppressedDirection].trades === 0 && entry[suppressedDirection].pnl === 0);
26059
26186
  const byDays = new Map(
26060
26187
  input.historicalWindows.map((entry) => [entry.days, entry])
26061
26188
  );
@@ -26068,17 +26195,13 @@ var init_strategyRelease = __esm({
26068
26195
  const longWindows = REQUIRED_LONG_WINDOWS.map(
26069
26196
  (days) => byDays.get(days)
26070
26197
  ).filter((entry) => Boolean(entry));
26071
- if (longWindows.some(
26072
- (entry) => entry.pnl <= 0 || entry.profitFactor < 1 || entry.long.pnl <= 0 || entry.long.profitFactor < 1 || entry.short.pnl <= 0 || entry.short.profitFactor < 1
26073
- )) {
26198
+ if (longWindows.some((entry) => !hasHistoricalEdge(entry))) {
26074
26199
  blockers.push("HISTORICAL_EDGE_FAILED");
26075
26200
  }
26076
26201
  const stableTerminalWindows = REQUIRED_STABLE_TERMINAL_WINDOWS.map(
26077
26202
  (days) => byDays.get(days)
26078
26203
  ).filter((entry) => Boolean(entry));
26079
- if (stableTerminalWindows.some(
26080
- (entry) => entry.pnl <= 0 || entry.profitFactor < 1 || entry.long.pnl <= 0 || entry.long.profitFactor < 1 || entry.short.pnl <= 0 || entry.short.profitFactor < 1
26081
- )) {
26204
+ if (stableTerminalWindows.some((entry) => !hasHistoricalEdge(entry))) {
26082
26205
  blockers.push("HISTORICAL_EDGE_FAILED");
26083
26206
  }
26084
26207
  if (blockers.length) {
@@ -26088,8 +26211,9 @@ var init_strategyRelease = __esm({
26088
26211
  repairAllowed: false,
26089
26212
  targetDirection: input.recentFailure?.direction ?? null,
26090
26213
  maxLossValue: null,
26214
+ requiresRuntimeBinding: false,
26091
26215
  blockers,
26092
- summary: "The final composition does not have complete positive ALL/LONG/SHORT evidence on the frozen 3y/4y/max-window matrix."
26216
+ summary: `The final ${directionPolicy} composition does not have complete positive evidence for ALL and every active direction on the frozen 3y/4y/max-window matrix.`
26093
26217
  };
26094
26218
  }
26095
26219
  const repairAllowed = Boolean(
@@ -26102,6 +26226,7 @@ var init_strategyRelease = __esm({
26102
26226
  repairAllowed: true,
26103
26227
  targetDirection: input.recentFailure.direction,
26104
26228
  maxLossValue: null,
26229
+ requiresRuntimeBinding: false,
26105
26230
  blockers: [],
26106
26231
  summary: "Spend the single terminal-direction repair round on the preregistered causal mechanism, then rebuild the full evidence matrix."
26107
26232
  };
@@ -26120,6 +26245,7 @@ var init_strategyRelease = __esm({
26120
26245
  repairAllowed: false,
26121
26246
  targetDirection: input.recentFailure?.direction ?? null,
26122
26247
  maxLossValue: null,
26248
+ requiresRuntimeBinding: false,
26123
26249
  blockers,
26124
26250
  summary: "Complete the frozen candidate implementation and full-period chart before any micro-forward execution."
26125
26251
  };
@@ -26131,6 +26257,9 @@ var init_strategyRelease = __esm({
26131
26257
  repairAllowed: false,
26132
26258
  targetDirection: input.recentFailure?.direction ?? null,
26133
26259
  maxLossValue: 1,
26260
+ requiresRuntimeBinding: !hasExactRuntimeTarget(
26261
+ input.forwardTest.runtimeTarget
26262
+ ),
26134
26263
  blockers: ["FORWARD_NOT_AUTHORIZED"],
26135
26264
  summary: "The candidate is ready for a separately authorized micro-forward test at MAX_LOSS_VALUE=1."
26136
26265
  };
@@ -26138,12 +26267,13 @@ var init_strategyRelease = __esm({
26138
26267
  if (!hasExactRuntimeTarget(input.forwardTest.runtimeTarget)) {
26139
26268
  return {
26140
26269
  strategy: input.strategy,
26141
- action: "FORWARD_BLOCKED",
26270
+ action: "MICRO_FORWARD_READY",
26142
26271
  repairAllowed: false,
26143
26272
  targetDirection: input.recentFailure?.direction ?? null,
26144
26273
  maxLossValue: 1,
26145
- blockers: ["RUNTIME_TARGET_UNRESOLVED"],
26146
- summary: "Forward testing is authorized, but an exact runtime deployment/account target must be resolved first."
26274
+ requiresRuntimeBinding: true,
26275
+ blockers: [],
26276
+ summary: "The portable micro-forward handoff is ready. Bind it on the runtime server to that server's deployment/account ids and verify canonical composition fingerprints there."
26147
26277
  };
26148
26278
  }
26149
26279
  return {
@@ -26152,12 +26282,13 @@ var init_strategyRelease = __esm({
26152
26282
  repairAllowed: false,
26153
26283
  targetDirection: input.recentFailure?.direction ?? null,
26154
26284
  maxLossValue: 1,
26285
+ requiresRuntimeBinding: false,
26155
26286
  blockers: [],
26156
26287
  summary: "Start the exact frozen composition as a micro-forward test at MAX_LOSS_VALUE=1; do not retune an underpowered or exposed recent tail."
26157
26288
  };
26158
26289
  };
26159
- canonicalStrategyReleaseJson = (value) => (0, import_strategyReleaseEvidence2.canonicalStrategyEvidenceJson)(value);
26160
- strategyReleaseSha256 = (value) => (0, import_strategyReleaseEvidence2.strategyEvidenceSha256)(value);
26290
+ canonicalStrategyReleaseJson = (value) => (0, import_strategyReleaseEvidence3.canonicalStrategyEvidenceJson)(value);
26291
+ strategyReleaseSha256 = (value) => (0, import_strategyReleaseEvidence3.strategyEvidenceSha256)(value);
26161
26292
  assertFingerprint = (value, label) => assert2(SHA256_RE2.test(value), `${label} must be a lowercase SHA-256`);
26162
26293
  assertLineageFingerprint = (value, label) => assert2(
26163
26294
  LINEAGE_FINGERPRINT_RE.test(value),
@@ -26206,6 +26337,7 @@ var init_strategyRelease = __esm({
26206
26337
  };
26207
26338
  };
26208
26339
  verifyEvidenceComposition = (strategy, composition, evidence) => {
26340
+ const directionPolicy = composition.directionPolicy ?? "both";
26209
26341
  const requireEqual = (actual, expected, label, artifactId) => assert2(
26210
26342
  actual === expected,
26211
26343
  `evidence ${artifactId} ${label} does not match the frozen composition`
@@ -26276,6 +26408,14 @@ var init_strategyRelease = __esm({
26276
26408
  "gateContextFingerprint",
26277
26409
  reference.artifactId
26278
26410
  );
26411
+ if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
26412
+ requireEqual(
26413
+ lineage?.directionPolicy,
26414
+ directionPolicy,
26415
+ "directionPolicy",
26416
+ reference.artifactId
26417
+ );
26418
+ }
26279
26419
  assert2(
26280
26420
  lineage?.sourceSha256s.includes(composition.coreExportSha256),
26281
26421
  `evidence ${reference.artifactId} does not use the frozen core export`
@@ -26320,6 +26460,14 @@ var init_strategyRelease = __esm({
26320
26460
  "MAX_LOSS_VALUE",
26321
26461
  reference.artifactId
26322
26462
  );
26463
+ if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
26464
+ requireEqual(
26465
+ lineage?.directionPolicy,
26466
+ directionPolicy,
26467
+ "directionPolicy",
26468
+ reference.artifactId
26469
+ );
26470
+ }
26323
26471
  continue;
26324
26472
  }
26325
26473
  if (reference.kind === "execution_calibration") {
@@ -26360,6 +26508,14 @@ var init_strategyRelease = __esm({
26360
26508
  "MAX_LOSS_VALUE",
26361
26509
  reference.artifactId
26362
26510
  );
26511
+ if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
26512
+ requireEqual(
26513
+ lineage?.directionPolicy,
26514
+ directionPolicy,
26515
+ "directionPolicy",
26516
+ reference.artifactId
26517
+ );
26518
+ }
26363
26519
  }
26364
26520
  }
26365
26521
  };
@@ -26395,6 +26551,12 @@ var init_strategyRelease = __esm({
26395
26551
  input.composition.longEnabled && input.composition.shortEnabled,
26396
26552
  "LONG and SHORT must both remain enabled"
26397
26553
  );
26554
+ assert2(
26555
+ input.composition.directionPolicy == null || ["both", "long_only", "short_only", "direction_aware"].includes(
26556
+ input.composition.directionPolicy
26557
+ ),
26558
+ "composition.directionPolicy is invalid"
26559
+ );
26398
26560
  assert2(
26399
26561
  Number.isInteger(input.monitoring.minimumProspectiveClosedTrades) && input.monitoring.minimumProspectiveClosedTrades > 0,
26400
26562
  "monitoring.minimumProspectiveClosedTrades must be positive"
@@ -26459,7 +26621,8 @@ var init_strategyRelease = __esm({
26459
26621
  gateFingerprint: input.composition.gateFingerprint,
26460
26622
  gateContextFingerprint: input.composition.gateContextFingerprint,
26461
26623
  runtimeContextFingerprint: input.composition.runtimeContextFingerprint,
26462
- maxLossValue: input.composition.maxLossValue
26624
+ maxLossValue: input.composition.maxLossValue,
26625
+ ...input.composition.directionPolicy == null ? {} : { directionPolicy: input.composition.directionPolicy }
26463
26626
  };
26464
26627
  const compositionId = `${safeSegment3(input.strategy, "strategy")}_${strategyReleaseSha256(compositionIdentity).slice(0, 16)}`;
26465
26628
  const reasons = releaseReasons({ ...input, gates: evidenceGates });
@@ -26549,7 +26712,8 @@ var init_strategyRelease = __esm({
26549
26712
  runtimeContextFingerprint: manifest.composition.runtimeContextFingerprint,
26550
26713
  maxLossValue: manifest.composition.maxLossValue,
26551
26714
  longEnabled: manifest.composition.longEnabled,
26552
- shortEnabled: manifest.composition.shortEnabled
26715
+ shortEnabled: manifest.composition.shortEnabled,
26716
+ directionPolicy: manifest.composition.directionPolicy
26553
26717
  },
26554
26718
  marketWindow: manifest.marketWindow,
26555
26719
  researchBudget: manifest.researchBudget,
@@ -26567,7 +26731,7 @@ var init_strategyRelease = __esm({
26567
26731
  };
26568
26732
  verifyStrategyEvidenceMarkerEnvelope = async (valueOrPath) => {
26569
26733
  const value = typeof valueOrPath === "string" ? JSON.parse(await import_promises25.default.readFile(valueOrPath, "utf8")) : valueOrPath;
26570
- return (0, import_strategyReleaseEvidence2.verifyStrategyEvidenceMarkerEnvelope)(value);
26734
+ return (0, import_strategyReleaseEvidence3.verifyStrategyEvidenceMarkerEnvelope)(value);
26571
26735
  };
26572
26736
  releaseMarkers = (manifest) => {
26573
26737
  const common = {
@@ -26642,7 +26806,7 @@ var init_strategyRelease = __esm({
26642
26806
  manifest
26643
26807
  }) => {
26644
26808
  const envelope = createStrategyReleaseEnvelope(manifest);
26645
- const markerEnvelope = (0, import_strategyReleaseEvidence2.createStrategyEvidenceMarkerEnvelope)({
26809
+ const markerEnvelope = (0, import_strategyReleaseEvidence3.createStrategyEvidenceMarkerEnvelope)({
26646
26810
  strategy: manifest.strategy,
26647
26811
  createdAt: manifest.createdAt,
26648
26812
  markers: releaseMarkers(manifest),
@@ -26917,7 +27081,7 @@ var init_strategyRelease = __esm({
26917
27081
  severity: diagnosis.verdict === "EXPECTED_DRAWDOWN" ? "info" : "warning"
26918
27082
  }
26919
27083
  ];
26920
- const markerEnvelope = (0, import_strategyReleaseEvidence2.createStrategyEvidenceMarkerEnvelope)({
27084
+ const markerEnvelope = (0, import_strategyReleaseEvidence3.createStrategyEvidenceMarkerEnvelope)({
26921
27085
  strategy: diagnosis.strategy,
26922
27086
  createdAt: diagnosis.createdAt,
26923
27087
  markers,
@@ -26973,6 +27137,7 @@ var init_strategyRelease = __esm({
26973
27137
  const number = Number(value);
26974
27138
  return Number.isFinite(number) ? number : null;
26975
27139
  };
27140
+ finiteDirectionPolicy = (value) => ["both", "long_only", "short_only", "direction_aware"].includes(String(value)) ? value : null;
26976
27141
  uniqueStrings = (values) => [
26977
27142
  ...new Set(
26978
27143
  values.map(finiteString3).filter((value) => value != null)
@@ -26989,6 +27154,7 @@ var init_strategyRelease = __esm({
26989
27154
  gateContextFingerprint: null,
26990
27155
  runtimeContextFingerprint: null,
26991
27156
  maxLossValue: null,
27157
+ directionPolicy: null,
26992
27158
  sourceSha256s: []
26993
27159
  });
26994
27160
  extractEvidenceLineage = ({
@@ -27051,6 +27217,7 @@ var init_strategyRelease = __esm({
27051
27217
  gateConfigIdsFingerprint: finiteString3(lineage?.configIdsFingerprint),
27052
27218
  gateFingerprint: finiteString3(lineage?.gateFingerprint),
27053
27219
  gateContextFingerprint: finiteString3(lineage?.contextFingerprint),
27220
+ directionPolicy: finiteDirectionPolicy(lineage?.directionPolicy),
27054
27221
  sourceSha256s: uniqueStrings(array(lineage?.sourceSha256s))
27055
27222
  };
27056
27223
  }
@@ -27086,7 +27253,8 @@ var init_strategyRelease = __esm({
27086
27253
  gateFingerprint: one("gateFingerprint"),
27087
27254
  runtimeConfigFingerprint: one("configFingerprint"),
27088
27255
  runtimeContextFingerprint: one("contextFingerprint"),
27089
- maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null
27256
+ maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null,
27257
+ directionPolicy: finiteDirectionPolicy(one("directionPolicy"))
27090
27258
  };
27091
27259
  }
27092
27260
  if (reference.kind === "execution_calibration") {
@@ -27123,6 +27291,7 @@ var init_strategyRelease = __esm({
27123
27291
  runtimeConfigFingerprint: one("configFingerprint"),
27124
27292
  runtimeContextFingerprint: one("contextFingerprint"),
27125
27293
  maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null,
27294
+ directionPolicy: finiteDirectionPolicy(one("directionPolicy")),
27126
27295
  sourceSha256s: uniqueStrings([
27127
27296
  ...array(nested(artifact.sources)?.sha256s),
27128
27297
  ...array(artifact.sourceSha256s)
@@ -27167,7 +27336,7 @@ var init_strategyRelease = __esm({
27167
27336
  typeof expectedSha256 === "string" && SHA256_RE2.test(expectedSha256),
27168
27337
  `core bundle artifact ${relativePath} has an invalid checksum`
27169
27338
  );
27170
- const actualSha256 = await (0, import_strategyReleaseEvidence2.strategyEvidenceFileSha256)(
27339
+ const actualSha256 = await (0, import_strategyReleaseEvidence3.strategyEvidenceFileSha256)(
27171
27340
  import_node_path20.default.join(researchDir, relativePath)
27172
27341
  );
27173
27342
  assert2(