@tradejs/node 3.1.29-beta.283 → 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.
@@ -6217,6 +6217,112 @@ var buildAiHookContext = ({
6217
6217
  };
6218
6218
  };
6219
6219
 
6220
+ // src/researchEntryPolicy.ts
6221
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6222
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6223
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6224
+ const raw = signal.additionalIndicators?.baseContext;
6225
+ const adapted = additional.baseContext;
6226
+ return {
6227
+ ...payload,
6228
+ additionalIndicators: {
6229
+ ...additional,
6230
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6231
+ baseContext: {
6232
+ ...raw && typeof raw === "object" ? raw : {},
6233
+ ...adapted && typeof adapted === "object" ? adapted : {}
6234
+ }
6235
+ }
6236
+ };
6237
+ };
6238
+ var parseResearchEntryPolicy = (value) => {
6239
+ if (!value || typeof value !== "object" || Array.isArray(value))
6240
+ throw new Error("Invalid research entry policy");
6241
+ const policy = value;
6242
+ 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")
6243
+ throw new Error("Invalid research entry policy contract");
6244
+ 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))))
6245
+ throw new Error("Invalid research policy directions");
6246
+ for (const side of ["LONG", "SHORT"]) {
6247
+ const predicates = policy.rules[side];
6248
+ if (predicates === null) continue;
6249
+ if (!Array.isArray(predicates) || predicates.length > 4)
6250
+ throw new Error(
6251
+ "Research policy needs bounded rules for both directions"
6252
+ );
6253
+ for (const predicate of predicates) {
6254
+ 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)))
6255
+ throw new Error(
6256
+ "Research policy contains an unsupported or non-causal predicate"
6257
+ );
6258
+ }
6259
+ }
6260
+ return policy;
6261
+ };
6262
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6263
+ const value = predicate.path.split(".").reduce(
6264
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6265
+ payload
6266
+ );
6267
+ if (value == null) return false;
6268
+ if (predicate.op === "==") return value === predicate.value;
6269
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6270
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6271
+ });
6272
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6273
+ if (signal.strategy !== policy.strategy)
6274
+ throw new Error("Research policy strategy mismatch");
6275
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6276
+ onDecision?.({
6277
+ mode: policy.mode,
6278
+ allowed: false,
6279
+ reason: "DIRECTION_BLOCKED"
6280
+ });
6281
+ return false;
6282
+ }
6283
+ if (policy.mode === "current-gate") {
6284
+ const analysis = await runAiPromptLocal(signal);
6285
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6286
+ onDecision?.({
6287
+ mode: policy.mode,
6288
+ allowed: allowed2,
6289
+ quality: analysis.quality ?? null,
6290
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6291
+ });
6292
+ return allowed2;
6293
+ }
6294
+ const original = buildAiPayload(signal);
6295
+ const gateContext = getDeterministicAiGateContext(original);
6296
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6297
+ onDecision?.({
6298
+ mode: policy.mode,
6299
+ allowed: false,
6300
+ reason: "STRUCTURAL_HARD_BLOCK"
6301
+ });
6302
+ return false;
6303
+ }
6304
+ const rules = policy.rules[signal.direction];
6305
+ const payload = buildResearchEntryPayload(signal, original);
6306
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6307
+ onDecision?.({
6308
+ mode: policy.mode,
6309
+ allowed,
6310
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6311
+ predicates: rules?.map((rule) => {
6312
+ const value = rule.path.split(".").reduce(
6313
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6314
+ payload
6315
+ );
6316
+ return {
6317
+ path: rule.path,
6318
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6319
+ passed: matchesResearchEntryRules(payload, [rule])
6320
+ };
6321
+ })
6322
+ });
6323
+ return allowed;
6324
+ };
6325
+
6220
6326
  // src/strategy/runtimeDecisionTelemetry.ts
6221
6327
  var finitePositive = (value) => {
6222
6328
  const parsed = Number(value);
@@ -6755,6 +6861,12 @@ var createStrategyRuntime = ({
6755
6861
  const projectConfig = await loadTradejsConfig(projectRoot);
6756
6862
  const projectHooks = projectConfig.hooks;
6757
6863
  const env = String(config.ENV ?? "BACKTEST");
6864
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6865
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6866
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6867
+ throw new Error(
6868
+ "Research entry policies require a BACKTEST simulation connector"
6869
+ );
6758
6870
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6759
6871
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6760
6872
  throw new Error(
@@ -7465,7 +7577,8 @@ var createStrategyRuntime = ({
7465
7577
  await enrichSignalWithMarketContextStages({
7466
7578
  signal,
7467
7579
  env,
7468
- includeHyperliquidWhales: false
7580
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7581
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7469
7582
  });
7470
7583
  if (jev) {
7471
7584
  await assessSignalWithJev({
@@ -7563,6 +7676,22 @@ var createStrategyRuntime = ({
7563
7676
  }
7564
7677
  return signal ?? decision.code;
7565
7678
  }
7679
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7680
+ researchEntryPolicy,
7681
+ signal,
7682
+ (decision2) => {
7683
+ signal.additionalIndicators = {
7684
+ ...signal.additionalIndicators,
7685
+ researchEntryDecision: decision2
7686
+ };
7687
+ }
7688
+ ))) {
7689
+ if (signal) {
7690
+ signal.orderStatus = "skipped";
7691
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7692
+ }
7693
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7694
+ }
7566
7695
  const entryGate = await invokeGateHooks(
7567
7696
  "beforeEntryGate",
7568
7697
  decisionManifest?.hooks?.beforeEntryGate,
@@ -2,7 +2,7 @@ import {
2
2
  getRuntimeDeployment,
3
3
  loadResolvedRuntimeStrategies
4
4
  } from "./chunk-BO42QWAB.mjs";
5
- import "./chunk-SKY2HNZU.mjs";
5
+ import "./chunk-K56EW2BB.mjs";
6
6
  import "./chunk-UL5Z46JL.mjs";
7
7
  import "./chunk-LZDXRXIU.mjs";
8
8
  import "./chunk-YR55Q355.mjs";
package/dist/mcp.js CHANGED
@@ -6211,6 +6211,112 @@ var buildAiHookContext = ({
6211
6211
  };
6212
6212
  };
6213
6213
 
6214
+ // src/researchEntryPolicy.ts
6215
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6216
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6217
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6218
+ const raw = signal.additionalIndicators?.baseContext;
6219
+ const adapted = additional.baseContext;
6220
+ return {
6221
+ ...payload,
6222
+ additionalIndicators: {
6223
+ ...additional,
6224
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6225
+ baseContext: {
6226
+ ...raw && typeof raw === "object" ? raw : {},
6227
+ ...adapted && typeof adapted === "object" ? adapted : {}
6228
+ }
6229
+ }
6230
+ };
6231
+ };
6232
+ var parseResearchEntryPolicy = (value) => {
6233
+ if (!value || typeof value !== "object" || Array.isArray(value))
6234
+ throw new Error("Invalid research entry policy");
6235
+ const policy = value;
6236
+ 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")
6237
+ throw new Error("Invalid research entry policy contract");
6238
+ 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))))
6239
+ throw new Error("Invalid research policy directions");
6240
+ for (const side of ["LONG", "SHORT"]) {
6241
+ const predicates = policy.rules[side];
6242
+ if (predicates === null) continue;
6243
+ if (!Array.isArray(predicates) || predicates.length > 4)
6244
+ throw new Error(
6245
+ "Research policy needs bounded rules for both directions"
6246
+ );
6247
+ for (const predicate of predicates) {
6248
+ 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)))
6249
+ throw new Error(
6250
+ "Research policy contains an unsupported or non-causal predicate"
6251
+ );
6252
+ }
6253
+ }
6254
+ return policy;
6255
+ };
6256
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6257
+ const value = predicate.path.split(".").reduce(
6258
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6259
+ payload
6260
+ );
6261
+ if (value == null) return false;
6262
+ if (predicate.op === "==") return value === predicate.value;
6263
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6264
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6265
+ });
6266
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6267
+ if (signal.strategy !== policy.strategy)
6268
+ throw new Error("Research policy strategy mismatch");
6269
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6270
+ onDecision?.({
6271
+ mode: policy.mode,
6272
+ allowed: false,
6273
+ reason: "DIRECTION_BLOCKED"
6274
+ });
6275
+ return false;
6276
+ }
6277
+ if (policy.mode === "current-gate") {
6278
+ const analysis = await runAiPromptLocal(signal);
6279
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6280
+ onDecision?.({
6281
+ mode: policy.mode,
6282
+ allowed: allowed2,
6283
+ quality: analysis.quality ?? null,
6284
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6285
+ });
6286
+ return allowed2;
6287
+ }
6288
+ const original = buildAiPayload(signal);
6289
+ const gateContext = getDeterministicAiGateContext(original);
6290
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6291
+ onDecision?.({
6292
+ mode: policy.mode,
6293
+ allowed: false,
6294
+ reason: "STRUCTURAL_HARD_BLOCK"
6295
+ });
6296
+ return false;
6297
+ }
6298
+ const rules = policy.rules[signal.direction];
6299
+ const payload = buildResearchEntryPayload(signal, original);
6300
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6301
+ onDecision?.({
6302
+ mode: policy.mode,
6303
+ allowed,
6304
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6305
+ predicates: rules?.map((rule) => {
6306
+ const value = rule.path.split(".").reduce(
6307
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6308
+ payload
6309
+ );
6310
+ return {
6311
+ path: rule.path,
6312
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6313
+ passed: matchesResearchEntryRules(payload, [rule])
6314
+ };
6315
+ })
6316
+ });
6317
+ return allowed;
6318
+ };
6319
+
6214
6320
  // src/strategy/runtimeDecisionTelemetry.ts
6215
6321
  var finitePositive = (value) => {
6216
6322
  const parsed = Number(value);
@@ -6749,6 +6855,12 @@ var createStrategyRuntime = ({
6749
6855
  const projectConfig = await loadTradejsConfig(projectRoot);
6750
6856
  const projectHooks = projectConfig.hooks;
6751
6857
  const env = String(config.ENV ?? "BACKTEST");
6858
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6859
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6860
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6861
+ throw new Error(
6862
+ "Research entry policies require a BACKTEST simulation connector"
6863
+ );
6752
6864
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6753
6865
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6754
6866
  throw new Error(
@@ -7459,7 +7571,8 @@ var createStrategyRuntime = ({
7459
7571
  await enrichSignalWithMarketContextStages({
7460
7572
  signal,
7461
7573
  env,
7462
- includeHyperliquidWhales: false
7574
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7575
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7463
7576
  });
7464
7577
  if (jev) {
7465
7578
  await assessSignalWithJev({
@@ -7557,6 +7670,22 @@ var createStrategyRuntime = ({
7557
7670
  }
7558
7671
  return signal ?? decision.code;
7559
7672
  }
7673
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7674
+ researchEntryPolicy,
7675
+ signal,
7676
+ (decision2) => {
7677
+ signal.additionalIndicators = {
7678
+ ...signal.additionalIndicators,
7679
+ researchEntryDecision: decision2
7680
+ };
7681
+ }
7682
+ ))) {
7683
+ if (signal) {
7684
+ signal.orderStatus = "skipped";
7685
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7686
+ }
7687
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7688
+ }
7560
7689
  const entryGate = await invokeGateHooks(
7561
7690
  "beforeEntryGate",
7562
7691
  decisionManifest?.hooks?.beforeEntryGate,
package/dist/mcp.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  listRuntimeDeployments,
8
8
  loadResolvedRuntimeStrategies
9
9
  } from "./chunk-BO42QWAB.mjs";
10
- import "./chunk-SKY2HNZU.mjs";
10
+ import "./chunk-K56EW2BB.mjs";
11
11
  import {
12
12
  ensureIndicatorPluginsLoaded
13
13
  } from "./chunk-UL5Z46JL.mjs";
package/dist/registry.js CHANGED
@@ -6213,6 +6213,112 @@ var buildAiHookContext = ({
6213
6213
  };
6214
6214
  };
6215
6215
 
6216
+ // src/researchEntryPolicy.ts
6217
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6218
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6219
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6220
+ const raw = signal.additionalIndicators?.baseContext;
6221
+ const adapted = additional.baseContext;
6222
+ return {
6223
+ ...payload,
6224
+ additionalIndicators: {
6225
+ ...additional,
6226
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6227
+ baseContext: {
6228
+ ...raw && typeof raw === "object" ? raw : {},
6229
+ ...adapted && typeof adapted === "object" ? adapted : {}
6230
+ }
6231
+ }
6232
+ };
6233
+ };
6234
+ var parseResearchEntryPolicy = (value) => {
6235
+ if (!value || typeof value !== "object" || Array.isArray(value))
6236
+ throw new Error("Invalid research entry policy");
6237
+ const policy = value;
6238
+ 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")
6239
+ throw new Error("Invalid research entry policy contract");
6240
+ 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))))
6241
+ throw new Error("Invalid research policy directions");
6242
+ for (const side of ["LONG", "SHORT"]) {
6243
+ const predicates = policy.rules[side];
6244
+ if (predicates === null) continue;
6245
+ if (!Array.isArray(predicates) || predicates.length > 4)
6246
+ throw new Error(
6247
+ "Research policy needs bounded rules for both directions"
6248
+ );
6249
+ for (const predicate of predicates) {
6250
+ 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)))
6251
+ throw new Error(
6252
+ "Research policy contains an unsupported or non-causal predicate"
6253
+ );
6254
+ }
6255
+ }
6256
+ return policy;
6257
+ };
6258
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6259
+ const value = predicate.path.split(".").reduce(
6260
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6261
+ payload
6262
+ );
6263
+ if (value == null) return false;
6264
+ if (predicate.op === "==") return value === predicate.value;
6265
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6266
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6267
+ });
6268
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6269
+ if (signal.strategy !== policy.strategy)
6270
+ throw new Error("Research policy strategy mismatch");
6271
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6272
+ onDecision?.({
6273
+ mode: policy.mode,
6274
+ allowed: false,
6275
+ reason: "DIRECTION_BLOCKED"
6276
+ });
6277
+ return false;
6278
+ }
6279
+ if (policy.mode === "current-gate") {
6280
+ const analysis = await runAiPromptLocal(signal);
6281
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6282
+ onDecision?.({
6283
+ mode: policy.mode,
6284
+ allowed: allowed2,
6285
+ quality: analysis.quality ?? null,
6286
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6287
+ });
6288
+ return allowed2;
6289
+ }
6290
+ const original = buildAiPayload(signal);
6291
+ const gateContext = getDeterministicAiGateContext(original);
6292
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6293
+ onDecision?.({
6294
+ mode: policy.mode,
6295
+ allowed: false,
6296
+ reason: "STRUCTURAL_HARD_BLOCK"
6297
+ });
6298
+ return false;
6299
+ }
6300
+ const rules = policy.rules[signal.direction];
6301
+ const payload = buildResearchEntryPayload(signal, original);
6302
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6303
+ onDecision?.({
6304
+ mode: policy.mode,
6305
+ allowed,
6306
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6307
+ predicates: rules?.map((rule) => {
6308
+ const value = rule.path.split(".").reduce(
6309
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6310
+ payload
6311
+ );
6312
+ return {
6313
+ path: rule.path,
6314
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6315
+ passed: matchesResearchEntryRules(payload, [rule])
6316
+ };
6317
+ })
6318
+ });
6319
+ return allowed;
6320
+ };
6321
+
6216
6322
  // src/strategy/runtimeDecisionTelemetry.ts
6217
6323
  var finitePositive = (value) => {
6218
6324
  const parsed = Number(value);
@@ -6751,6 +6857,12 @@ var createStrategyRuntime = ({
6751
6857
  const projectConfig = await loadTradejsConfig(projectRoot);
6752
6858
  const projectHooks = projectConfig.hooks;
6753
6859
  const env = String(config.ENV ?? "BACKTEST");
6860
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6861
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6862
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6863
+ throw new Error(
6864
+ "Research entry policies require a BACKTEST simulation connector"
6865
+ );
6754
6866
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6755
6867
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6756
6868
  throw new Error(
@@ -7461,7 +7573,8 @@ var createStrategyRuntime = ({
7461
7573
  await enrichSignalWithMarketContextStages({
7462
7574
  signal,
7463
7575
  env,
7464
- includeHyperliquidWhales: false
7576
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7577
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7465
7578
  });
7466
7579
  if (jev) {
7467
7580
  await assessSignalWithJev({
@@ -7559,6 +7672,22 @@ var createStrategyRuntime = ({
7559
7672
  }
7560
7673
  return signal ?? decision.code;
7561
7674
  }
7675
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7676
+ researchEntryPolicy,
7677
+ signal,
7678
+ (decision2) => {
7679
+ signal.additionalIndicators = {
7680
+ ...signal.additionalIndicators,
7681
+ researchEntryDecision: decision2
7682
+ };
7683
+ }
7684
+ ))) {
7685
+ if (signal) {
7686
+ signal.orderStatus = "skipped";
7687
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7688
+ }
7689
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7690
+ }
7562
7691
  const entryGate = await invokeGateHooks(
7563
7692
  "beforeEntryGate",
7564
7693
  decisionManifest?.hooks?.beforeEntryGate,
package/dist/registry.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./chunk-JQOLTLEY.mjs";
2
- import "./chunk-SKY2HNZU.mjs";
2
+ import "./chunk-K56EW2BB.mjs";
3
3
  import {
4
4
  ensureIndicatorPluginsLoaded,
5
5
  ensureStrategyPluginsLoaded,
@@ -7195,6 +7195,112 @@ var buildAiHookContext = ({
7195
7195
  };
7196
7196
  };
7197
7197
 
7198
+ // src/researchEntryPolicy.ts
7199
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
7200
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
7201
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
7202
+ const raw = signal.additionalIndicators?.baseContext;
7203
+ const adapted = additional.baseContext;
7204
+ return {
7205
+ ...payload,
7206
+ additionalIndicators: {
7207
+ ...additional,
7208
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
7209
+ baseContext: {
7210
+ ...raw && typeof raw === "object" ? raw : {},
7211
+ ...adapted && typeof adapted === "object" ? adapted : {}
7212
+ }
7213
+ }
7214
+ };
7215
+ };
7216
+ var parseResearchEntryPolicy = (value) => {
7217
+ if (!value || typeof value !== "object" || Array.isArray(value))
7218
+ throw new Error("Invalid research entry policy");
7219
+ const policy = value;
7220
+ 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")
7221
+ throw new Error("Invalid research entry policy contract");
7222
+ 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))))
7223
+ throw new Error("Invalid research policy directions");
7224
+ for (const side of ["LONG", "SHORT"]) {
7225
+ const predicates = policy.rules[side];
7226
+ if (predicates === null) continue;
7227
+ if (!Array.isArray(predicates) || predicates.length > 4)
7228
+ throw new Error(
7229
+ "Research policy needs bounded rules for both directions"
7230
+ );
7231
+ for (const predicate of predicates) {
7232
+ 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)))
7233
+ throw new Error(
7234
+ "Research policy contains an unsupported or non-causal predicate"
7235
+ );
7236
+ }
7237
+ }
7238
+ return policy;
7239
+ };
7240
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
7241
+ const value = predicate.path.split(".").reduce(
7242
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
7243
+ payload
7244
+ );
7245
+ if (value == null) return false;
7246
+ if (predicate.op === "==") return value === predicate.value;
7247
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
7248
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
7249
+ });
7250
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
7251
+ if (signal.strategy !== policy.strategy)
7252
+ throw new Error("Research policy strategy mismatch");
7253
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
7254
+ onDecision?.({
7255
+ mode: policy.mode,
7256
+ allowed: false,
7257
+ reason: "DIRECTION_BLOCKED"
7258
+ });
7259
+ return false;
7260
+ }
7261
+ if (policy.mode === "current-gate") {
7262
+ const analysis = await runAiPromptLocal(signal);
7263
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
7264
+ onDecision?.({
7265
+ mode: policy.mode,
7266
+ allowed: allowed2,
7267
+ quality: analysis.quality ?? null,
7268
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
7269
+ });
7270
+ return allowed2;
7271
+ }
7272
+ const original = buildAiPayload(signal);
7273
+ const gateContext = getDeterministicAiGateContext(original);
7274
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
7275
+ onDecision?.({
7276
+ mode: policy.mode,
7277
+ allowed: false,
7278
+ reason: "STRUCTURAL_HARD_BLOCK"
7279
+ });
7280
+ return false;
7281
+ }
7282
+ const rules = policy.rules[signal.direction];
7283
+ const payload = buildResearchEntryPayload(signal, original);
7284
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
7285
+ onDecision?.({
7286
+ mode: policy.mode,
7287
+ allowed,
7288
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
7289
+ predicates: rules?.map((rule) => {
7290
+ const value = rule.path.split(".").reduce(
7291
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
7292
+ payload
7293
+ );
7294
+ return {
7295
+ path: rule.path,
7296
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
7297
+ passed: matchesResearchEntryRules(payload, [rule])
7298
+ };
7299
+ })
7300
+ });
7301
+ return allowed;
7302
+ };
7303
+
7198
7304
  // src/strategy/runtimeDecisionTelemetry.ts
7199
7305
  var finitePositive = (value) => {
7200
7306
  const parsed = Number(value);
@@ -7733,6 +7839,12 @@ var createStrategyRuntime = ({
7733
7839
  const projectConfig = await loadTradejsConfig(projectRoot);
7734
7840
  const projectHooks = projectConfig.hooks;
7735
7841
  const env = String(config.ENV ?? "BACKTEST");
7842
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7843
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7844
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
7845
+ throw new Error(
7846
+ "Research entry policies require a BACKTEST simulation connector"
7847
+ );
7736
7848
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
7737
7849
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
7738
7850
  throw new Error(
@@ -8443,7 +8555,8 @@ var createStrategyRuntime = ({
8443
8555
  await enrichSignalWithMarketContextStages({
8444
8556
  signal,
8445
8557
  env,
8446
- includeHyperliquidWhales: false
8558
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
8559
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
8447
8560
  });
8448
8561
  if (jev) {
8449
8562
  await assessSignalWithJev({
@@ -8541,6 +8654,22 @@ var createStrategyRuntime = ({
8541
8654
  }
8542
8655
  return signal ?? decision.code;
8543
8656
  }
8657
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
8658
+ researchEntryPolicy,
8659
+ signal,
8660
+ (decision2) => {
8661
+ signal.additionalIndicators = {
8662
+ ...signal.additionalIndicators,
8663
+ researchEntryDecision: decision2
8664
+ };
8665
+ }
8666
+ ))) {
8667
+ if (signal) {
8668
+ signal.orderStatus = "skipped";
8669
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
8670
+ }
8671
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
8672
+ }
8544
8673
  const entryGate = await invokeGateHooks(
8545
8674
  "beforeEntryGate",
8546
8675
  decisionManifest?.hooks?.beforeEntryGate,
@@ -9,7 +9,7 @@ import {
9
9
  listRuntimeDeployments,
10
10
  loadResolvedRuntimeStrategies
11
11
  } from "./chunk-BO42QWAB.mjs";
12
- import "./chunk-SKY2HNZU.mjs";
12
+ import "./chunk-K56EW2BB.mjs";
13
13
  import "./chunk-UL5Z46JL.mjs";
14
14
  import "./chunk-LZDXRXIU.mjs";
15
15
  import "./chunk-YR55Q355.mjs";