@tradejs/node 3.1.29-beta.282 → 3.1.29-beta.284

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.
@@ -1,5 +1,6 @@
1
1
  export * from '@tradejs/core/backtest';
2
- import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint } from '@tradejs/types';
2
+ import * as _tradejs_types from '@tradejs/types';
3
+ import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint, Signal, AiPayload, ResearchEntryPolicy } from '@tradejs/types';
3
4
 
4
5
  type TestingGroupResult = {
5
6
  test: Test;
@@ -32,4 +33,14 @@ declare const resolveExecutionCosts: (params: {
32
33
  fundingRates: FundingRatePoint[];
33
34
  }>;
34
35
 
35
- export { canRunTestsInSharedCandleLoop, createTestConnector, releaseTestingSymbolCache, resetTestingKlineCache, resolveExecutionCosts, testing, testingGroupInSharedCandleLoop };
36
+ declare const RESEARCH_ENTRY_POLICY_CAPABILITIES: {
37
+ readonly directionSelection: true;
38
+ };
39
+ /** Preserve the canonical signal-time snapshot even when an AI adapter emits
40
+ * only its strategy-specific fields. Both discovery and execution use this. */
41
+ declare const buildResearchEntryPayload: (signal: Signal, payload?: AiPayload) => AiPayload;
42
+ declare const parseResearchEntryPolicy: (value: unknown) => ResearchEntryPolicy;
43
+ declare const matchesResearchEntryRules: (payload: AiPayload, predicates: NonNullable<ResearchEntryPolicy["rules"]["LONG"]>) => boolean;
44
+ declare const evaluateResearchEntryPolicy: (policy: ResearchEntryPolicy, signal: Signal, onDecision?: (decision: _tradejs_types.ResearchEntryDecision) => void) => Promise<boolean>;
45
+
46
+ export { RESEARCH_ENTRY_POLICY_CAPABILITIES, buildResearchEntryPayload, canRunTestsInSharedCandleLoop, createTestConnector, evaluateResearchEntryPolicy, matchesResearchEntryRules, parseResearchEntryPolicy, releaseTestingSymbolCache, resetTestingKlineCache, resolveExecutionCosts, testing, testingGroupInSharedCandleLoop };
@@ -1,5 +1,6 @@
1
1
  export * from '@tradejs/core/backtest';
2
- import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint } from '@tradejs/types';
2
+ import * as _tradejs_types from '@tradejs/types';
3
+ import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint, Signal, AiPayload, ResearchEntryPolicy } from '@tradejs/types';
3
4
 
4
5
  type TestingGroupResult = {
5
6
  test: Test;
@@ -32,4 +33,14 @@ declare const resolveExecutionCosts: (params: {
32
33
  fundingRates: FundingRatePoint[];
33
34
  }>;
34
35
 
35
- export { canRunTestsInSharedCandleLoop, createTestConnector, releaseTestingSymbolCache, resetTestingKlineCache, resolveExecutionCosts, testing, testingGroupInSharedCandleLoop };
36
+ declare const RESEARCH_ENTRY_POLICY_CAPABILITIES: {
37
+ readonly directionSelection: true;
38
+ };
39
+ /** Preserve the canonical signal-time snapshot even when an AI adapter emits
40
+ * only its strategy-specific fields. Both discovery and execution use this. */
41
+ declare const buildResearchEntryPayload: (signal: Signal, payload?: AiPayload) => AiPayload;
42
+ declare const parseResearchEntryPolicy: (value: unknown) => ResearchEntryPolicy;
43
+ declare const matchesResearchEntryRules: (payload: AiPayload, predicates: NonNullable<ResearchEntryPolicy["rules"]["LONG"]>) => boolean;
44
+ declare const evaluateResearchEntryPolicy: (policy: ResearchEntryPolicy, signal: Signal, onDecision?: (decision: _tradejs_types.ResearchEntryDecision) => void) => Promise<boolean>;
45
+
46
+ export { RESEARCH_ENTRY_POLICY_CAPABILITIES, buildResearchEntryPayload, canRunTestsInSharedCandleLoop, createTestConnector, evaluateResearchEntryPolicy, matchesResearchEntryRules, parseResearchEntryPolicy, releaseTestingSymbolCache, resetTestingKlineCache, resolveExecutionCosts, testing, testingGroupInSharedCandleLoop };
package/dist/backtest.js CHANGED
@@ -778,8 +778,13 @@ var init_jevTraining = __esm({
778
778
  // src/backtest.ts
779
779
  var backtest_exports = {};
780
780
  __export(backtest_exports, {
781
+ RESEARCH_ENTRY_POLICY_CAPABILITIES: () => RESEARCH_ENTRY_POLICY_CAPABILITIES,
782
+ buildResearchEntryPayload: () => buildResearchEntryPayload,
781
783
  canRunTestsInSharedCandleLoop: () => canRunTestsInSharedCandleLoop,
782
784
  createTestConnector: () => createTestConnector,
785
+ evaluateResearchEntryPolicy: () => evaluateResearchEntryPolicy,
786
+ matchesResearchEntryRules: () => matchesResearchEntryRules,
787
+ parseResearchEntryPolicy: () => parseResearchEntryPolicy,
783
788
  releaseTestingSymbolCache: () => releaseTestingSymbolCache,
784
789
  resetTestingKlineCache: () => resetTestingKlineCache,
785
790
  resolveExecutionCosts: () => resolveExecutionCosts,
@@ -6175,6 +6180,115 @@ var buildAiHookContext = ({
6175
6180
  };
6176
6181
  };
6177
6182
 
6183
+ // src/researchEntryPolicy.ts
6184
+ var RESEARCH_ENTRY_POLICY_CAPABILITIES = {
6185
+ directionSelection: true
6186
+ };
6187
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6188
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6189
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6190
+ const raw = signal.additionalIndicators?.baseContext;
6191
+ const adapted = additional.baseContext;
6192
+ return {
6193
+ ...payload,
6194
+ additionalIndicators: {
6195
+ ...additional,
6196
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6197
+ baseContext: {
6198
+ ...raw && typeof raw === "object" ? raw : {},
6199
+ ...adapted && typeof adapted === "object" ? adapted : {}
6200
+ }
6201
+ }
6202
+ };
6203
+ };
6204
+ var parseResearchEntryPolicy = (value) => {
6205
+ if (!value || typeof value !== "object" || Array.isArray(value))
6206
+ throw new Error("Invalid research entry policy");
6207
+ const policy = value;
6208
+ if (policy.schema !== "tradejs-research-entry-policy/v1" || typeof policy.strategy !== "string" || !policy.strategy.trim() || !["current-gate", "rules"].includes(policy.mode) || !Number.isInteger(policy.minQuality) || policy.minQuality < 1 || policy.minQuality > 5 || !policy.rules || typeof policy.rules !== "object")
6209
+ throw new Error("Invalid research entry policy contract");
6210
+ if (policy.directions !== void 0 && (!Array.isArray(policy.directions) || policy.directions.length > 2 || new Set(policy.directions).size !== policy.directions.length || policy.directions.some((side) => !["LONG", "SHORT"].includes(side))))
6211
+ throw new Error("Invalid research policy directions");
6212
+ for (const side of ["LONG", "SHORT"]) {
6213
+ const predicates = policy.rules[side];
6214
+ if (predicates === null) continue;
6215
+ if (!Array.isArray(predicates) || predicates.length > 4)
6216
+ throw new Error(
6217
+ "Research policy needs bounded rules for both directions"
6218
+ );
6219
+ for (const predicate of predicates) {
6220
+ if (!predicate || typeof predicate.path !== "string" || !/^additionalIndicators\.[a-zA-Z0-9_.]+$/.test(predicate.path) || forbidden.test(predicate.path) || !["<=", ">=", "=="].includes(predicate.op) || (predicate.op === "==" ? !["string", "boolean"].includes(typeof predicate.value) : typeof predicate.value !== "number" || !Number.isFinite(predicate.value)))
6221
+ throw new Error(
6222
+ "Research policy contains an unsupported or non-causal predicate"
6223
+ );
6224
+ }
6225
+ }
6226
+ return policy;
6227
+ };
6228
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6229
+ const value = predicate.path.split(".").reduce(
6230
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6231
+ payload
6232
+ );
6233
+ if (value == null) return false;
6234
+ if (predicate.op === "==") return value === predicate.value;
6235
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6236
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6237
+ });
6238
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6239
+ if (signal.strategy !== policy.strategy)
6240
+ throw new Error("Research policy strategy mismatch");
6241
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6242
+ onDecision?.({
6243
+ mode: policy.mode,
6244
+ allowed: false,
6245
+ reason: "DIRECTION_BLOCKED"
6246
+ });
6247
+ return false;
6248
+ }
6249
+ if (policy.mode === "current-gate") {
6250
+ const analysis = await runAiPromptLocal(signal);
6251
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6252
+ onDecision?.({
6253
+ mode: policy.mode,
6254
+ allowed: allowed2,
6255
+ quality: analysis.quality ?? null,
6256
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6257
+ });
6258
+ return allowed2;
6259
+ }
6260
+ const original = buildAiPayload(signal);
6261
+ const gateContext = getDeterministicAiGateContext(original);
6262
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6263
+ onDecision?.({
6264
+ mode: policy.mode,
6265
+ allowed: false,
6266
+ reason: "STRUCTURAL_HARD_BLOCK"
6267
+ });
6268
+ return false;
6269
+ }
6270
+ const rules = policy.rules[signal.direction];
6271
+ const payload = buildResearchEntryPayload(signal, original);
6272
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6273
+ onDecision?.({
6274
+ mode: policy.mode,
6275
+ allowed,
6276
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6277
+ predicates: rules?.map((rule) => {
6278
+ const value = rule.path.split(".").reduce(
6279
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6280
+ payload
6281
+ );
6282
+ return {
6283
+ path: rule.path,
6284
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6285
+ passed: matchesResearchEntryRules(payload, [rule])
6286
+ };
6287
+ })
6288
+ });
6289
+ return allowed;
6290
+ };
6291
+
6178
6292
  // src/strategy/runtimeDecisionTelemetry.ts
6179
6293
  var finitePositive = (value) => {
6180
6294
  const parsed = Number(value);
@@ -6713,6 +6827,12 @@ var createStrategyRuntime = ({
6713
6827
  const projectConfig = await loadTradejsConfig(projectRoot);
6714
6828
  const projectHooks = projectConfig.hooks;
6715
6829
  const env = String(config.ENV ?? "BACKTEST");
6830
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6831
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6832
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6833
+ throw new Error(
6834
+ "Research entry policies require a BACKTEST simulation connector"
6835
+ );
6716
6836
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6717
6837
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6718
6838
  throw new Error(
@@ -7423,7 +7543,8 @@ var createStrategyRuntime = ({
7423
7543
  await enrichSignalWithMarketContextStages({
7424
7544
  signal,
7425
7545
  env,
7426
- includeHyperliquidWhales: false
7546
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7547
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7427
7548
  });
7428
7549
  if (jev) {
7429
7550
  await assessSignalWithJev({
@@ -7521,6 +7642,22 @@ var createStrategyRuntime = ({
7521
7642
  }
7522
7643
  return signal ?? decision.code;
7523
7644
  }
7645
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7646
+ researchEntryPolicy,
7647
+ signal,
7648
+ (decision2) => {
7649
+ signal.additionalIndicators = {
7650
+ ...signal.additionalIndicators,
7651
+ researchEntryDecision: decision2
7652
+ };
7653
+ }
7654
+ ))) {
7655
+ if (signal) {
7656
+ signal.orderStatus = "skipped";
7657
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7658
+ }
7659
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7660
+ }
7524
7661
  const entryGate = await invokeGateHooks(
7525
7662
  "beforeEntryGate",
7526
7663
  decisionManifest?.hooks?.beforeEntryGate,
@@ -8251,7 +8388,7 @@ var createBacktestProgress = ({
8251
8388
 
8252
8389
  // src/backtest/session.ts
8253
8390
  var import_types2 = require("@tradejs/types");
8254
- var import_ai3 = require("@tradejs/infra/ai");
8391
+ var import_ai4 = require("@tradejs/infra/ai");
8255
8392
  var import_coreResearch = require("@tradejs/infra/coreResearch");
8256
8393
  var import_ml3 = require("@tradejs/infra/ml");
8257
8394
 
@@ -9555,7 +9692,8 @@ var createBacktestSession = async ({
9555
9692
  }
9556
9693
  if (test.researchTrace && signal && typeof signal !== "string" && signal.signalId) {
9557
9694
  const identity = resolveCoreResearchSetupIdentity(signal);
9558
- pendingResearchSetupBySignalId.set(signal.signalId, identity);
9695
+ if (signal.orderStatus !== "failed" && signal.orderStatus !== "skipped")
9696
+ pendingResearchSetupBySignalId.set(signal.signalId, identity);
9559
9697
  await (0, import_coreResearch.appendCoreResearchTraceEvent)({
9560
9698
  strategyName: test.strategyName,
9561
9699
  chunkId,
@@ -9570,7 +9708,11 @@ var createBacktestSession = async ({
9570
9708
  configId: test.configId,
9571
9709
  backtestRunId: test.backtestRunId,
9572
9710
  backtestTestKey: test.backtestTestKey,
9573
- ...identity
9711
+ ...identity,
9712
+ ...test.strategyConfig.RESEARCH_ENTRY_POLICY ? {
9713
+ reason: signal.orderSkipReason,
9714
+ researchEntryDecision: signal.additionalIndicators?.researchEntryDecision
9715
+ } : {}
9574
9716
  }
9575
9717
  });
9576
9718
  const traceEvent = signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted";
@@ -9598,7 +9740,7 @@ var createBacktestSession = async ({
9598
9740
  })
9599
9741
  );
9600
9742
  }
9601
- if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(signal.assessment && signal.orderStatus === "skipped")) {
9743
+ if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(test.strategyConfig.RESEARCH_ENTRY_POLICY && (signal.orderStatus === "skipped" || signal.orderStatus === "failed")) && !(signal.assessment && signal.orderStatus === "skipped")) {
9602
9744
  pendingAiRowBySignalId.set(signal.signalId, {
9603
9745
  signalId: signal.signalId,
9604
9746
  strategyName: signal.strategy || test.strategyName,
@@ -9639,7 +9781,7 @@ var createBacktestSession = async ({
9639
9781
  if (aiRowBase) {
9640
9782
  pendingAiRowBySignalId.delete(resultRecord.signalId);
9641
9783
  const { signal: aiSignal, ...rowBase } = aiRowBase;
9642
- await (0, import_ai3.appendAiDatasetRow)({
9784
+ await (0, import_ai4.appendAiDatasetRow)({
9643
9785
  strategyName: test.strategyName,
9644
9786
  chunkId,
9645
9787
  row: {
@@ -9649,7 +9791,14 @@ var createBacktestSession = async ({
9649
9791
  tradeResult: resultRecord.tradeResult,
9650
9792
  research: {
9651
9793
  schema: "tradejs-core-research-row/v1",
9652
- ...resolveCoreResearchSetupIdentity(aiSignal)
9794
+ ...resolveCoreResearchSetupIdentity(aiSignal),
9795
+ ...test.researchTrace ? {
9796
+ decisionContext: {
9797
+ schema: "tradejs-research-decision-context/v1",
9798
+ knownAt: aiSignal.timestamp,
9799
+ baseContext: aiSignal.additionalIndicators?.baseContext ?? null
9800
+ }
9801
+ } : {}
9653
9802
  }
9654
9803
  }
9655
9804
  });
@@ -10522,8 +10671,13 @@ var testingGroupInSharedCandleLoop = async (tests) => {
10522
10671
  };
10523
10672
  // Annotate the CommonJS export names for ESM import in node:
10524
10673
  0 && (module.exports = {
10674
+ RESEARCH_ENTRY_POLICY_CAPABILITIES,
10675
+ buildResearchEntryPayload,
10525
10676
  canRunTestsInSharedCandleLoop,
10526
10677
  createTestConnector,
10678
+ evaluateResearchEntryPolicy,
10679
+ matchesResearchEntryRules,
10680
+ parseResearchEntryPolicy,
10527
10681
  releaseTestingSymbolCache,
10528
10682
  resetTestingKlineCache,
10529
10683
  resolveExecutionCosts,
package/dist/backtest.mjs CHANGED
@@ -3,9 +3,14 @@ import {
3
3
  getConnectorCreatorByName
4
4
  } from "./chunk-QVKUROR2.mjs";
5
5
  import {
6
+ RESEARCH_ENTRY_POLICY_CAPABILITIES,
6
7
  buildMlPayload,
7
- enrichSignalWithMarketContextStages
8
- } from "./chunk-SKY2HNZU.mjs";
8
+ buildResearchEntryPayload,
9
+ enrichSignalWithMarketContextStages,
10
+ evaluateResearchEntryPolicy,
11
+ matchesResearchEntryRules,
12
+ parseResearchEntryPolicy
13
+ } from "./chunk-K56EW2BB.mjs";
9
14
  import {
10
15
  buildAiPayload,
11
16
  getStrategyCreator
@@ -1468,7 +1473,8 @@ var createBacktestSession = async ({
1468
1473
  }
1469
1474
  if (test.researchTrace && signal && typeof signal !== "string" && signal.signalId) {
1470
1475
  const identity = resolveCoreResearchSetupIdentity(signal);
1471
- pendingResearchSetupBySignalId.set(signal.signalId, identity);
1476
+ if (signal.orderStatus !== "failed" && signal.orderStatus !== "skipped")
1477
+ pendingResearchSetupBySignalId.set(signal.signalId, identity);
1472
1478
  await appendCoreResearchTraceEvent({
1473
1479
  strategyName: test.strategyName,
1474
1480
  chunkId,
@@ -1483,7 +1489,11 @@ var createBacktestSession = async ({
1483
1489
  configId: test.configId,
1484
1490
  backtestRunId: test.backtestRunId,
1485
1491
  backtestTestKey: test.backtestTestKey,
1486
- ...identity
1492
+ ...identity,
1493
+ ...test.strategyConfig.RESEARCH_ENTRY_POLICY ? {
1494
+ reason: signal.orderSkipReason,
1495
+ researchEntryDecision: signal.additionalIndicators?.researchEntryDecision
1496
+ } : {}
1487
1497
  }
1488
1498
  });
1489
1499
  const traceEvent = signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted";
@@ -1511,7 +1521,7 @@ var createBacktestSession = async ({
1511
1521
  })
1512
1522
  );
1513
1523
  }
1514
- if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(signal.assessment && signal.orderStatus === "skipped")) {
1524
+ if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(test.strategyConfig.RESEARCH_ENTRY_POLICY && (signal.orderStatus === "skipped" || signal.orderStatus === "failed")) && !(signal.assessment && signal.orderStatus === "skipped")) {
1515
1525
  pendingAiRowBySignalId.set(signal.signalId, {
1516
1526
  signalId: signal.signalId,
1517
1527
  strategyName: signal.strategy || test.strategyName,
@@ -1562,7 +1572,14 @@ var createBacktestSession = async ({
1562
1572
  tradeResult: resultRecord.tradeResult,
1563
1573
  research: {
1564
1574
  schema: "tradejs-core-research-row/v1",
1565
- ...resolveCoreResearchSetupIdentity(aiSignal)
1575
+ ...resolveCoreResearchSetupIdentity(aiSignal),
1576
+ ...test.researchTrace ? {
1577
+ decisionContext: {
1578
+ schema: "tradejs-research-decision-context/v1",
1579
+ knownAt: aiSignal.timestamp,
1580
+ baseContext: aiSignal.additionalIndicators?.baseContext ?? null
1581
+ }
1582
+ } : {}
1566
1583
  }
1567
1584
  }
1568
1585
  });
@@ -2434,8 +2451,13 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2434
2451
  }
2435
2452
  };
2436
2453
  export {
2454
+ RESEARCH_ENTRY_POLICY_CAPABILITIES,
2455
+ buildResearchEntryPayload,
2437
2456
  canRunTestsInSharedCandleLoop,
2438
2457
  createTestConnector,
2458
+ evaluateResearchEntryPolicy,
2459
+ matchesResearchEntryRules,
2460
+ parseResearchEntryPolicy,
2439
2461
  releaseTestingSymbolCache,
2440
2462
  resetTestingKlineCache,
2441
2463
  resolveExecutionCosts,
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  askAI,
3
+ buildAiPayload,
4
+ getDeterministicAiGateContext,
3
5
  getStrategyManifest,
4
6
  getStrategyProfileMlAdapter,
5
7
  resolveStrategyPolicyProfile,
@@ -3179,6 +3181,115 @@ var resolveStrategyConfig = async ({
3179
3181
  return { config, isConfigFromBacktest };
3180
3182
  };
3181
3183
 
3184
+ // src/researchEntryPolicy.ts
3185
+ var RESEARCH_ENTRY_POLICY_CAPABILITIES = {
3186
+ directionSelection: true
3187
+ };
3188
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
3189
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
3190
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
3191
+ const raw = signal.additionalIndicators?.baseContext;
3192
+ const adapted = additional.baseContext;
3193
+ return {
3194
+ ...payload,
3195
+ additionalIndicators: {
3196
+ ...additional,
3197
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
3198
+ baseContext: {
3199
+ ...raw && typeof raw === "object" ? raw : {},
3200
+ ...adapted && typeof adapted === "object" ? adapted : {}
3201
+ }
3202
+ }
3203
+ };
3204
+ };
3205
+ var parseResearchEntryPolicy = (value) => {
3206
+ if (!value || typeof value !== "object" || Array.isArray(value))
3207
+ throw new Error("Invalid research entry policy");
3208
+ const policy = value;
3209
+ if (policy.schema !== "tradejs-research-entry-policy/v1" || typeof policy.strategy !== "string" || !policy.strategy.trim() || !["current-gate", "rules"].includes(policy.mode) || !Number.isInteger(policy.minQuality) || policy.minQuality < 1 || policy.minQuality > 5 || !policy.rules || typeof policy.rules !== "object")
3210
+ throw new Error("Invalid research entry policy contract");
3211
+ if (policy.directions !== void 0 && (!Array.isArray(policy.directions) || policy.directions.length > 2 || new Set(policy.directions).size !== policy.directions.length || policy.directions.some((side) => !["LONG", "SHORT"].includes(side))))
3212
+ throw new Error("Invalid research policy directions");
3213
+ for (const side of ["LONG", "SHORT"]) {
3214
+ const predicates = policy.rules[side];
3215
+ if (predicates === null) continue;
3216
+ if (!Array.isArray(predicates) || predicates.length > 4)
3217
+ throw new Error(
3218
+ "Research policy needs bounded rules for both directions"
3219
+ );
3220
+ for (const predicate of predicates) {
3221
+ if (!predicate || typeof predicate.path !== "string" || !/^additionalIndicators\.[a-zA-Z0-9_.]+$/.test(predicate.path) || forbidden.test(predicate.path) || !["<=", ">=", "=="].includes(predicate.op) || (predicate.op === "==" ? !["string", "boolean"].includes(typeof predicate.value) : typeof predicate.value !== "number" || !Number.isFinite(predicate.value)))
3222
+ throw new Error(
3223
+ "Research policy contains an unsupported or non-causal predicate"
3224
+ );
3225
+ }
3226
+ }
3227
+ return policy;
3228
+ };
3229
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
3230
+ const value = predicate.path.split(".").reduce(
3231
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
3232
+ payload
3233
+ );
3234
+ if (value == null) return false;
3235
+ if (predicate.op === "==") return value === predicate.value;
3236
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
3237
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
3238
+ });
3239
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
3240
+ if (signal.strategy !== policy.strategy)
3241
+ throw new Error("Research policy strategy mismatch");
3242
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
3243
+ onDecision?.({
3244
+ mode: policy.mode,
3245
+ allowed: false,
3246
+ reason: "DIRECTION_BLOCKED"
3247
+ });
3248
+ return false;
3249
+ }
3250
+ if (policy.mode === "current-gate") {
3251
+ const analysis = await runAiPromptLocal(signal);
3252
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
3253
+ onDecision?.({
3254
+ mode: policy.mode,
3255
+ allowed: allowed2,
3256
+ quality: analysis.quality ?? null,
3257
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
3258
+ });
3259
+ return allowed2;
3260
+ }
3261
+ const original = buildAiPayload(signal);
3262
+ const gateContext = getDeterministicAiGateContext(original);
3263
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
3264
+ onDecision?.({
3265
+ mode: policy.mode,
3266
+ allowed: false,
3267
+ reason: "STRUCTURAL_HARD_BLOCK"
3268
+ });
3269
+ return false;
3270
+ }
3271
+ const rules = policy.rules[signal.direction];
3272
+ const payload = buildResearchEntryPayload(signal, original);
3273
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
3274
+ onDecision?.({
3275
+ mode: policy.mode,
3276
+ allowed,
3277
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
3278
+ predicates: rules?.map((rule) => {
3279
+ const value = rule.path.split(".").reduce(
3280
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
3281
+ payload
3282
+ );
3283
+ return {
3284
+ path: rule.path,
3285
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
3286
+ passed: matchesResearchEntryRules(payload, [rule])
3287
+ };
3288
+ })
3289
+ });
3290
+ return allowed;
3291
+ };
3292
+
3182
3293
  // src/strategyRuntime.ts
3183
3294
  import { parseJevConfig } from "@tradejs/core/jev";
3184
3295
  import {
@@ -4145,6 +4256,12 @@ var createStrategyRuntime = ({
4145
4256
  const projectConfig = await loadTradejsConfig(projectRoot);
4146
4257
  const projectHooks = projectConfig.hooks;
4147
4258
  const env = String(config.ENV ?? "BACKTEST");
4259
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
4260
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
4261
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
4262
+ throw new Error(
4263
+ "Research entry policies require a BACKTEST simulation connector"
4264
+ );
4148
4265
  const jev = parseJevConfig(config.JEV);
4149
4266
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
4150
4267
  throw new Error(
@@ -4855,7 +4972,8 @@ var createStrategyRuntime = ({
4855
4972
  await enrichSignalWithMarketContextStages({
4856
4973
  signal,
4857
4974
  env,
4858
- includeHyperliquidWhales: false
4975
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
4976
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
4859
4977
  });
4860
4978
  if (jev) {
4861
4979
  await assessSignalWithJev({
@@ -4953,6 +5071,22 @@ var createStrategyRuntime = ({
4953
5071
  }
4954
5072
  return signal ?? decision.code;
4955
5073
  }
5074
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
5075
+ researchEntryPolicy,
5076
+ signal,
5077
+ (decision2) => {
5078
+ signal.additionalIndicators = {
5079
+ ...signal.additionalIndicators,
5080
+ researchEntryDecision: decision2
5081
+ };
5082
+ }
5083
+ ))) {
5084
+ if (signal) {
5085
+ signal.orderStatus = "skipped";
5086
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
5087
+ }
5088
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
5089
+ }
4956
5090
  const entryGate = await invokeGateHooks(
4957
5091
  "beforeEntryGate",
4958
5092
  decisionManifest?.hooks?.beforeEntryGate,
@@ -5088,5 +5222,10 @@ export {
5088
5222
  validateEntryProtectionAtArrival,
5089
5223
  executeEntryOrder,
5090
5224
  resolveStrategyConfig,
5225
+ RESEARCH_ENTRY_POLICY_CAPABILITIES,
5226
+ buildResearchEntryPayload,
5227
+ parseResearchEntryPolicy,
5228
+ matchesResearchEntryRules,
5229
+ evaluateResearchEntryPolicy,
5091
5230
  createStrategyRuntime
5092
5231
  };
@@ -179,4 +179,57 @@ declare const verifyRuntimeFeedbackReplaySource: ({ bundleDir, runtimeEvidenceBu
179
179
  runtimeEvidence: VerifiedRuntimeEvidenceBundle;
180
180
  }>;
181
181
 
182
- export { RUNTIME_EVIDENCE_COMPLETE_FILE, RUNTIME_EVIDENCE_MANIFEST_FILE, RUNTIME_EVIDENCE_PAYLOAD_FILE, RUNTIME_FEEDBACK_COMPLETE_FILE, RUNTIME_FEEDBACK_LOG_FILE, RUNTIME_FEEDBACK_MANIFEST_FILE, RUNTIME_FEEDBACK_REPLAY_FILE, type RuntimeEvidenceArtifactManifest, type RuntimeEvidenceDeploymentSnapshot, type RuntimeEvidenceProducer, type RuntimeEvidenceStrategySnapshot, type RuntimeFeedbackReplayManifest, type VerifiedRuntimeEvidenceBundle, type VerifiedRuntimeFeedbackReplayBundle, activeRuntimeEvidenceStrategies, assertCurrentRuntimeEvidenceArtifact, discoverRuntimeEvidenceBundles, discoverRuntimeFeedbackReplayBundles, parseRuntimeEvidenceDeploymentSnapshot, parseRuntimeEvidenceProducer, publishRuntimeEvidenceBundle, resolveRuntimeEvidenceDeploymentSnapshot, resolveRuntimeEvidenceProducer, resolveRuntimeEvidenceTickerUniverse, runtimeDeploymentFromEvidence, runtimeLineageMatchesStrategySnapshot, sealRuntimeFeedbackReplayBundle, verifyRuntimeEvidenceBundle, verifyRuntimeFeedbackReplayBundle, verifyRuntimeFeedbackReplaySource };
182
+ /** Version of the complete feedback read contract, independent of package versions. */
183
+ declare const MCP_FEEDBACK_CONTRACT: {
184
+ readonly version: 1;
185
+ readonly hostProtocolVersion: 2;
186
+ readonly replayQueue: {
187
+ readonly waitingStates: readonly ["waiting_resources", "waiting_executor"];
188
+ readonly automaticFailedRetries: false;
189
+ };
190
+ readonly artifactEncoding: "base64";
191
+ readonly maximumChunkBytes: 48000;
192
+ readonly artifactFiles: readonly ["payload", "manifest.json", ".complete", "replay.log"];
193
+ readonly requiredTools: readonly ["tradejs_info", "diagnostics_list_reports", "diagnostics_get_report", "artifact_get", "job_get", "job_get_logs"];
194
+ };
195
+ type McpToolDescription = {
196
+ name: string;
197
+ inputSchema: {
198
+ properties?: Record<string, unknown>;
199
+ };
200
+ };
201
+ /** Check the actual client's tools/list, not only what the server advertises. */
202
+ declare const assertMcpFeedbackContract: (version: unknown, tools: McpToolDescription[]) => void;
203
+
204
+ type McpArtifactFile = 'payload' | 'manifest.json' | '.complete' | 'replay.log';
205
+ type McpBundleDescriptor = {
206
+ id: string;
207
+ kind: 'runtime-evidence' | 'runtime-feedback-replay';
208
+ files: {
209
+ file: McpArtifactFile;
210
+ bytes: number;
211
+ sha256: string;
212
+ available: boolean;
213
+ }[];
214
+ };
215
+ declare class McpReadTransportError extends Error {
216
+ readonly retryAfterMs?: number | undefined;
217
+ constructor(message: string, retryAfterMs?: number | undefined);
218
+ }
219
+ type ChunkRequest = {
220
+ artifactId: string;
221
+ file: McpArtifactFile;
222
+ offset: number;
223
+ length: number;
224
+ };
225
+ /** The caller owns the authenticated MCP connection. This function only reads artifacts. */
226
+ declare const downloadMcpBundle: ({ descriptor, root, contractVersion, tools, readChunk, wait, }: {
227
+ descriptor: McpBundleDescriptor;
228
+ root: string;
229
+ contractVersion: unknown;
230
+ tools: McpToolDescription[];
231
+ readChunk: (request: ChunkRequest) => Promise<unknown>;
232
+ wait?: (ms: number) => Promise<unknown>;
233
+ }) => Promise<VerifiedRuntimeEvidenceBundle | VerifiedRuntimeFeedbackReplayBundle>;
234
+
235
+ export { MCP_FEEDBACK_CONTRACT, type McpArtifactFile, type McpBundleDescriptor, McpReadTransportError, type McpToolDescription, RUNTIME_EVIDENCE_COMPLETE_FILE, RUNTIME_EVIDENCE_MANIFEST_FILE, RUNTIME_EVIDENCE_PAYLOAD_FILE, RUNTIME_FEEDBACK_COMPLETE_FILE, RUNTIME_FEEDBACK_LOG_FILE, RUNTIME_FEEDBACK_MANIFEST_FILE, RUNTIME_FEEDBACK_REPLAY_FILE, type RuntimeEvidenceArtifactManifest, type RuntimeEvidenceDeploymentSnapshot, type RuntimeEvidenceProducer, type RuntimeEvidenceStrategySnapshot, type RuntimeFeedbackReplayManifest, type VerifiedRuntimeEvidenceBundle, type VerifiedRuntimeFeedbackReplayBundle, activeRuntimeEvidenceStrategies, assertCurrentRuntimeEvidenceArtifact, assertMcpFeedbackContract, discoverRuntimeEvidenceBundles, discoverRuntimeFeedbackReplayBundles, downloadMcpBundle, parseRuntimeEvidenceDeploymentSnapshot, parseRuntimeEvidenceProducer, publishRuntimeEvidenceBundle, resolveRuntimeEvidenceDeploymentSnapshot, resolveRuntimeEvidenceProducer, resolveRuntimeEvidenceTickerUniverse, runtimeDeploymentFromEvidence, runtimeLineageMatchesStrategySnapshot, sealRuntimeFeedbackReplayBundle, verifyRuntimeEvidenceBundle, verifyRuntimeFeedbackReplayBundle, verifyRuntimeFeedbackReplaySource };