@tradejs/cli 2.0.21 → 3.0.1

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
  ];
@@ -13940,9 +13991,9 @@ var init_backtest = __esm({
13940
13991
  const deploymentStrategy = preparedRun.deployment?.strategies.find(
13941
13992
  ({ strategyName }) => strategyName === config2.strategyName
13942
13993
  );
13943
- if (preparedRun.deployment && !deploymentStrategy) {
13994
+ if (deploymentStrategy?.enabled === false) {
13944
13995
  throw new Error(
13945
- `Strategy ${config2.strategyName} is not enabled in deployment ${preparedRun.deployment.id}`
13996
+ `Strategy ${config2.strategyName} is disabled in deployment ${preparedRun.deployment?.id}`
13946
13997
  );
13947
13998
  }
13948
13999
  const policyProfileId = (typeof flags5.policyProfile === "string" && flags5.policyProfile.trim() ? flags5.policyProfile.trim() : deploymentStrategy?.policyProfileId) || void 0;
@@ -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(
@@ -25948,9 +26064,10 @@ async function collectReleaseEvidenceReferences(references) {
25948
26064
  if (reference.kind === "ai_gate") {
25949
26065
  const outcome = nested(artifact.outcome);
25950
26066
  const approvedRisk = nested(outcome?.approvedRisk);
25951
- const terminalWindows = array(
25952
- nested(artifact.research)?.terminalWindows
25953
- ).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);
25954
26071
  const hasLongShortDirections = (value) => {
25955
26072
  const directions = new Map(
25956
26073
  array(value).map(nested).filter(
@@ -25964,6 +26081,22 @@ async function collectReleaseEvidenceReferences(references) {
25964
26081
  const directionalEvidenceComplete = hasLongShortDirections(artifact.byDirection) && terminalWindows.every(
25965
26082
  (window) => hasLongShortDirections(window.byDirection)
25966
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
+ ));
25967
26100
  const terminalsPass = terminalWindows.length > 0 && terminalWindows.every((window) => {
25968
26101
  const terminalOutcome = nested(window.outcome);
25969
26102
  return window.complete === true && Number(terminalOutcome?.approved ?? 0) > 0 && Number(
@@ -25971,7 +26104,7 @@ async function collectReleaseEvidenceReferences(references) {
25971
26104
  ) > 0;
25972
26105
  });
25973
26106
  return {
25974
- 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
25975
26108
  };
25976
26109
  }
25977
26110
  if (reference.kind === "runtime_parity") {
@@ -26006,7 +26139,7 @@ async function collectReleaseEvidenceReferences(references) {
26006
26139
  })
26007
26140
  );
26008
26141
  }
26009
- 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, 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;
26010
26143
  var init_strategyRelease = __esm({
26011
26144
  "src/lib/strategyRelease.ts"() {
26012
26145
  "use strict";
@@ -26044,6 +26177,12 @@ var init_strategyRelease = __esm({
26044
26177
  );
26045
26178
  deriveStrategyReleaseResearchDecision = async (input) => {
26046
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);
26047
26186
  const byDays = new Map(
26048
26187
  input.historicalWindows.map((entry) => [entry.days, entry])
26049
26188
  );
@@ -26056,17 +26195,13 @@ var init_strategyRelease = __esm({
26056
26195
  const longWindows = REQUIRED_LONG_WINDOWS.map(
26057
26196
  (days) => byDays.get(days)
26058
26197
  ).filter((entry) => Boolean(entry));
26059
- if (longWindows.some(
26060
- (entry) => entry.pnl <= 0 || entry.profitFactor < 1 || entry.long.pnl <= 0 || entry.long.profitFactor < 1 || entry.short.pnl <= 0 || entry.short.profitFactor < 1
26061
- )) {
26198
+ if (longWindows.some((entry) => !hasHistoricalEdge(entry))) {
26062
26199
  blockers.push("HISTORICAL_EDGE_FAILED");
26063
26200
  }
26064
26201
  const stableTerminalWindows = REQUIRED_STABLE_TERMINAL_WINDOWS.map(
26065
26202
  (days) => byDays.get(days)
26066
26203
  ).filter((entry) => Boolean(entry));
26067
- if (stableTerminalWindows.some(
26068
- (entry) => entry.pnl <= 0 || entry.profitFactor < 1 || entry.long.pnl <= 0 || entry.long.profitFactor < 1 || entry.short.pnl <= 0 || entry.short.profitFactor < 1
26069
- )) {
26204
+ if (stableTerminalWindows.some((entry) => !hasHistoricalEdge(entry))) {
26070
26205
  blockers.push("HISTORICAL_EDGE_FAILED");
26071
26206
  }
26072
26207
  if (blockers.length) {
@@ -26078,7 +26213,7 @@ var init_strategyRelease = __esm({
26078
26213
  maxLossValue: null,
26079
26214
  requiresRuntimeBinding: false,
26080
26215
  blockers,
26081
- 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.`
26082
26217
  };
26083
26218
  }
26084
26219
  const repairAllowed = Boolean(
@@ -26202,6 +26337,7 @@ var init_strategyRelease = __esm({
26202
26337
  };
26203
26338
  };
26204
26339
  verifyEvidenceComposition = (strategy, composition, evidence) => {
26340
+ const directionPolicy = composition.directionPolicy ?? "both";
26205
26341
  const requireEqual = (actual, expected, label, artifactId) => assert2(
26206
26342
  actual === expected,
26207
26343
  `evidence ${artifactId} ${label} does not match the frozen composition`
@@ -26272,6 +26408,14 @@ var init_strategyRelease = __esm({
26272
26408
  "gateContextFingerprint",
26273
26409
  reference.artifactId
26274
26410
  );
26411
+ if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
26412
+ requireEqual(
26413
+ lineage?.directionPolicy,
26414
+ directionPolicy,
26415
+ "directionPolicy",
26416
+ reference.artifactId
26417
+ );
26418
+ }
26275
26419
  assert2(
26276
26420
  lineage?.sourceSha256s.includes(composition.coreExportSha256),
26277
26421
  `evidence ${reference.artifactId} does not use the frozen core export`
@@ -26316,6 +26460,14 @@ var init_strategyRelease = __esm({
26316
26460
  "MAX_LOSS_VALUE",
26317
26461
  reference.artifactId
26318
26462
  );
26463
+ if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
26464
+ requireEqual(
26465
+ lineage?.directionPolicy,
26466
+ directionPolicy,
26467
+ "directionPolicy",
26468
+ reference.artifactId
26469
+ );
26470
+ }
26319
26471
  continue;
26320
26472
  }
26321
26473
  if (reference.kind === "execution_calibration") {
@@ -26356,6 +26508,14 @@ var init_strategyRelease = __esm({
26356
26508
  "MAX_LOSS_VALUE",
26357
26509
  reference.artifactId
26358
26510
  );
26511
+ if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
26512
+ requireEqual(
26513
+ lineage?.directionPolicy,
26514
+ directionPolicy,
26515
+ "directionPolicy",
26516
+ reference.artifactId
26517
+ );
26518
+ }
26359
26519
  }
26360
26520
  }
26361
26521
  };
@@ -26391,6 +26551,12 @@ var init_strategyRelease = __esm({
26391
26551
  input.composition.longEnabled && input.composition.shortEnabled,
26392
26552
  "LONG and SHORT must both remain enabled"
26393
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
+ );
26394
26560
  assert2(
26395
26561
  Number.isInteger(input.monitoring.minimumProspectiveClosedTrades) && input.monitoring.minimumProspectiveClosedTrades > 0,
26396
26562
  "monitoring.minimumProspectiveClosedTrades must be positive"
@@ -26455,7 +26621,8 @@ var init_strategyRelease = __esm({
26455
26621
  gateFingerprint: input.composition.gateFingerprint,
26456
26622
  gateContextFingerprint: input.composition.gateContextFingerprint,
26457
26623
  runtimeContextFingerprint: input.composition.runtimeContextFingerprint,
26458
- maxLossValue: input.composition.maxLossValue
26624
+ maxLossValue: input.composition.maxLossValue,
26625
+ ...input.composition.directionPolicy == null ? {} : { directionPolicy: input.composition.directionPolicy }
26459
26626
  };
26460
26627
  const compositionId = `${safeSegment3(input.strategy, "strategy")}_${strategyReleaseSha256(compositionIdentity).slice(0, 16)}`;
26461
26628
  const reasons = releaseReasons({ ...input, gates: evidenceGates });
@@ -26545,7 +26712,8 @@ var init_strategyRelease = __esm({
26545
26712
  runtimeContextFingerprint: manifest.composition.runtimeContextFingerprint,
26546
26713
  maxLossValue: manifest.composition.maxLossValue,
26547
26714
  longEnabled: manifest.composition.longEnabled,
26548
- shortEnabled: manifest.composition.shortEnabled
26715
+ shortEnabled: manifest.composition.shortEnabled,
26716
+ directionPolicy: manifest.composition.directionPolicy
26549
26717
  },
26550
26718
  marketWindow: manifest.marketWindow,
26551
26719
  researchBudget: manifest.researchBudget,
@@ -26969,6 +27137,7 @@ var init_strategyRelease = __esm({
26969
27137
  const number = Number(value);
26970
27138
  return Number.isFinite(number) ? number : null;
26971
27139
  };
27140
+ finiteDirectionPolicy = (value) => ["both", "long_only", "short_only", "direction_aware"].includes(String(value)) ? value : null;
26972
27141
  uniqueStrings = (values) => [
26973
27142
  ...new Set(
26974
27143
  values.map(finiteString3).filter((value) => value != null)
@@ -26985,6 +27154,7 @@ var init_strategyRelease = __esm({
26985
27154
  gateContextFingerprint: null,
26986
27155
  runtimeContextFingerprint: null,
26987
27156
  maxLossValue: null,
27157
+ directionPolicy: null,
26988
27158
  sourceSha256s: []
26989
27159
  });
26990
27160
  extractEvidenceLineage = ({
@@ -27047,6 +27217,7 @@ var init_strategyRelease = __esm({
27047
27217
  gateConfigIdsFingerprint: finiteString3(lineage?.configIdsFingerprint),
27048
27218
  gateFingerprint: finiteString3(lineage?.gateFingerprint),
27049
27219
  gateContextFingerprint: finiteString3(lineage?.contextFingerprint),
27220
+ directionPolicy: finiteDirectionPolicy(lineage?.directionPolicy),
27050
27221
  sourceSha256s: uniqueStrings(array(lineage?.sourceSha256s))
27051
27222
  };
27052
27223
  }
@@ -27082,7 +27253,8 @@ var init_strategyRelease = __esm({
27082
27253
  gateFingerprint: one("gateFingerprint"),
27083
27254
  runtimeConfigFingerprint: one("configFingerprint"),
27084
27255
  runtimeContextFingerprint: one("contextFingerprint"),
27085
- maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null
27256
+ maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null,
27257
+ directionPolicy: finiteDirectionPolicy(one("directionPolicy"))
27086
27258
  };
27087
27259
  }
27088
27260
  if (reference.kind === "execution_calibration") {
@@ -27119,6 +27291,7 @@ var init_strategyRelease = __esm({
27119
27291
  runtimeConfigFingerprint: one("configFingerprint"),
27120
27292
  runtimeContextFingerprint: one("contextFingerprint"),
27121
27293
  maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null,
27294
+ directionPolicy: finiteDirectionPolicy(one("directionPolicy")),
27122
27295
  sourceSha256s: uniqueStrings([
27123
27296
  ...array(nested(artifact.sources)?.sha256s),
27124
27297
  ...array(artifact.sourceSha256s)
@@ -30340,7 +30513,10 @@ var init_runtimeStrategies = __esm({
30340
30513
  interval: interval4
30341
30514
  }) => {
30342
30515
  const deploymentStrategies = new Map(
30343
- (deployment?.strategies ?? []).filter(({ enabled }) => enabled !== false).map((strategy) => [strategy.strategyName, strategy])
30516
+ (deployment?.strategies ?? []).map((strategy) => [
30517
+ strategy.strategyName,
30518
+ strategy
30519
+ ])
30344
30520
  );
30345
30521
  const strategyConfigs = await Promise.all(
30346
30522
  (await (0, import_runtimeStrategyConfigs.loadRuntimeStrategyConfigs)(userName2)).map(
@@ -30351,7 +30527,13 @@ var init_runtimeStrategies = __esm({
30351
30527
  strategyConfig
30352
30528
  }) => {
30353
30529
  const deploymentStrategy = deployment ? deploymentStrategies.get(strategyName) : void 0;
30354
- if (deployment && !deploymentStrategy) return null;
30530
+ if (deploymentStrategy?.enabled === false) {
30531
+ import_logger7.logger.info(
30532
+ "Skip deployment-disabled strategy config: %s",
30533
+ strategyName
30534
+ );
30535
+ return null;
30536
+ }
30355
30537
  if (!isRuntimeStrategyEnabled(strategyConfig)) {
30356
30538
  import_logger7.logger.info(
30357
30539
  "Skip inactive strategy config by ENABLE=false: %s",