@tradejs/node 3.1.29-beta.283 → 3.1.29-beta.285

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.
@@ -3000,6 +3000,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3000
3000
  const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3001
3001
  const next = {
3002
3002
  ...existing,
3003
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3003
3004
  qty: resultingQty,
3004
3005
  entryPrice: resultingEntryPrice,
3005
3006
  entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
@@ -5728,6 +5729,7 @@ var executeEntryOrder = async ({
5728
5729
  openFee: estimatedOpenFee,
5729
5730
  totalFee: estimatedOpenFee,
5730
5731
  entryTimestamp: timestamp,
5732
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5731
5733
  universe: signal.universe,
5732
5734
  assetClass: signal.assetClass,
5733
5735
  accountId: signal.accountId,
@@ -5751,6 +5753,7 @@ var executeEntryOrder = async ({
5751
5753
  addedQty: filledQty,
5752
5754
  addedEntryPrice: fillPrice,
5753
5755
  entryTimestamp: timestamp,
5756
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5754
5757
  fee: estimatedOpenFee,
5755
5758
  accountId: signal.accountId,
5756
5759
  deploymentId: signal.deploymentId
@@ -6217,6 +6220,112 @@ var buildAiHookContext = ({
6217
6220
  };
6218
6221
  };
6219
6222
 
6223
+ // src/researchEntryPolicy.ts
6224
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6225
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6226
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6227
+ const raw = signal.additionalIndicators?.baseContext;
6228
+ const adapted = additional.baseContext;
6229
+ return {
6230
+ ...payload,
6231
+ additionalIndicators: {
6232
+ ...additional,
6233
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6234
+ baseContext: {
6235
+ ...raw && typeof raw === "object" ? raw : {},
6236
+ ...adapted && typeof adapted === "object" ? adapted : {}
6237
+ }
6238
+ }
6239
+ };
6240
+ };
6241
+ var parseResearchEntryPolicy = (value) => {
6242
+ if (!value || typeof value !== "object" || Array.isArray(value))
6243
+ throw new Error("Invalid research entry policy");
6244
+ const policy = value;
6245
+ 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")
6246
+ throw new Error("Invalid research entry policy contract");
6247
+ 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))))
6248
+ throw new Error("Invalid research policy directions");
6249
+ for (const side of ["LONG", "SHORT"]) {
6250
+ const predicates = policy.rules[side];
6251
+ if (predicates === null) continue;
6252
+ if (!Array.isArray(predicates) || predicates.length > 4)
6253
+ throw new Error(
6254
+ "Research policy needs bounded rules for both directions"
6255
+ );
6256
+ for (const predicate of predicates) {
6257
+ 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)))
6258
+ throw new Error(
6259
+ "Research policy contains an unsupported or non-causal predicate"
6260
+ );
6261
+ }
6262
+ }
6263
+ return policy;
6264
+ };
6265
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6266
+ const value = predicate.path.split(".").reduce(
6267
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6268
+ payload
6269
+ );
6270
+ if (value == null) return false;
6271
+ if (predicate.op === "==") return value === predicate.value;
6272
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6273
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6274
+ });
6275
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6276
+ if (signal.strategy !== policy.strategy)
6277
+ throw new Error("Research policy strategy mismatch");
6278
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6279
+ onDecision?.({
6280
+ mode: policy.mode,
6281
+ allowed: false,
6282
+ reason: "DIRECTION_BLOCKED"
6283
+ });
6284
+ return false;
6285
+ }
6286
+ if (policy.mode === "current-gate") {
6287
+ const analysis = await runAiPromptLocal(signal);
6288
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6289
+ onDecision?.({
6290
+ mode: policy.mode,
6291
+ allowed: allowed2,
6292
+ quality: analysis.quality ?? null,
6293
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6294
+ });
6295
+ return allowed2;
6296
+ }
6297
+ const original = buildAiPayload(signal);
6298
+ const gateContext = getDeterministicAiGateContext(original);
6299
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6300
+ onDecision?.({
6301
+ mode: policy.mode,
6302
+ allowed: false,
6303
+ reason: "STRUCTURAL_HARD_BLOCK"
6304
+ });
6305
+ return false;
6306
+ }
6307
+ const rules = policy.rules[signal.direction];
6308
+ const payload = buildResearchEntryPayload(signal, original);
6309
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6310
+ onDecision?.({
6311
+ mode: policy.mode,
6312
+ allowed,
6313
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6314
+ predicates: rules?.map((rule) => {
6315
+ const value = rule.path.split(".").reduce(
6316
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6317
+ payload
6318
+ );
6319
+ return {
6320
+ path: rule.path,
6321
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6322
+ passed: matchesResearchEntryRules(payload, [rule])
6323
+ };
6324
+ })
6325
+ });
6326
+ return allowed;
6327
+ };
6328
+
6220
6329
  // src/strategy/runtimeDecisionTelemetry.ts
6221
6330
  var finitePositive = (value) => {
6222
6331
  const parsed = Number(value);
@@ -6755,6 +6864,12 @@ var createStrategyRuntime = ({
6755
6864
  const projectConfig = await loadTradejsConfig(projectRoot);
6756
6865
  const projectHooks = projectConfig.hooks;
6757
6866
  const env = String(config.ENV ?? "BACKTEST");
6867
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6868
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6869
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6870
+ throw new Error(
6871
+ "Research entry policies require a BACKTEST simulation connector"
6872
+ );
6758
6873
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6759
6874
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6760
6875
  throw new Error(
@@ -7465,7 +7580,8 @@ var createStrategyRuntime = ({
7465
7580
  await enrichSignalWithMarketContextStages({
7466
7581
  signal,
7467
7582
  env,
7468
- includeHyperliquidWhales: false
7583
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7584
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7469
7585
  });
7470
7586
  if (jev) {
7471
7587
  await assessSignalWithJev({
@@ -7563,6 +7679,22 @@ var createStrategyRuntime = ({
7563
7679
  }
7564
7680
  return signal ?? decision.code;
7565
7681
  }
7682
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7683
+ researchEntryPolicy,
7684
+ signal,
7685
+ (decision2) => {
7686
+ signal.additionalIndicators = {
7687
+ ...signal.additionalIndicators,
7688
+ researchEntryDecision: decision2
7689
+ };
7690
+ }
7691
+ ))) {
7692
+ if (signal) {
7693
+ signal.orderStatus = "skipped";
7694
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7695
+ }
7696
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7697
+ }
7566
7698
  const entryGate = await invokeGateHooks(
7567
7699
  "beforeEntryGate",
7568
7700
  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-KOV3U6ZZ.mjs";
6
6
  import "./chunk-UL5Z46JL.mjs";
7
7
  import "./chunk-LZDXRXIU.mjs";
8
8
  import "./chunk-YR55Q355.mjs";
package/dist/mcp.js CHANGED
@@ -2994,6 +2994,7 @@ var recordRuntimeTradeIncrease = async (params) => {
2994
2994
  const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
2995
2995
  const next = {
2996
2996
  ...existing,
2997
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
2997
2998
  qty: resultingQty,
2998
2999
  entryPrice: resultingEntryPrice,
2999
3000
  entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
@@ -5722,6 +5723,7 @@ var executeEntryOrder = async ({
5722
5723
  openFee: estimatedOpenFee,
5723
5724
  totalFee: estimatedOpenFee,
5724
5725
  entryTimestamp: timestamp,
5726
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5725
5727
  universe: signal.universe,
5726
5728
  assetClass: signal.assetClass,
5727
5729
  accountId: signal.accountId,
@@ -5745,6 +5747,7 @@ var executeEntryOrder = async ({
5745
5747
  addedQty: filledQty,
5746
5748
  addedEntryPrice: fillPrice,
5747
5749
  entryTimestamp: timestamp,
5750
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5748
5751
  fee: estimatedOpenFee,
5749
5752
  accountId: signal.accountId,
5750
5753
  deploymentId: signal.deploymentId
@@ -6211,6 +6214,112 @@ var buildAiHookContext = ({
6211
6214
  };
6212
6215
  };
6213
6216
 
6217
+ // src/researchEntryPolicy.ts
6218
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6219
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6220
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6221
+ const raw = signal.additionalIndicators?.baseContext;
6222
+ const adapted = additional.baseContext;
6223
+ return {
6224
+ ...payload,
6225
+ additionalIndicators: {
6226
+ ...additional,
6227
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6228
+ baseContext: {
6229
+ ...raw && typeof raw === "object" ? raw : {},
6230
+ ...adapted && typeof adapted === "object" ? adapted : {}
6231
+ }
6232
+ }
6233
+ };
6234
+ };
6235
+ var parseResearchEntryPolicy = (value) => {
6236
+ if (!value || typeof value !== "object" || Array.isArray(value))
6237
+ throw new Error("Invalid research entry policy");
6238
+ const policy = value;
6239
+ 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")
6240
+ throw new Error("Invalid research entry policy contract");
6241
+ 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))))
6242
+ throw new Error("Invalid research policy directions");
6243
+ for (const side of ["LONG", "SHORT"]) {
6244
+ const predicates = policy.rules[side];
6245
+ if (predicates === null) continue;
6246
+ if (!Array.isArray(predicates) || predicates.length > 4)
6247
+ throw new Error(
6248
+ "Research policy needs bounded rules for both directions"
6249
+ );
6250
+ for (const predicate of predicates) {
6251
+ 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)))
6252
+ throw new Error(
6253
+ "Research policy contains an unsupported or non-causal predicate"
6254
+ );
6255
+ }
6256
+ }
6257
+ return policy;
6258
+ };
6259
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6260
+ const value = predicate.path.split(".").reduce(
6261
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6262
+ payload
6263
+ );
6264
+ if (value == null) return false;
6265
+ if (predicate.op === "==") return value === predicate.value;
6266
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6267
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6268
+ });
6269
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6270
+ if (signal.strategy !== policy.strategy)
6271
+ throw new Error("Research policy strategy mismatch");
6272
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6273
+ onDecision?.({
6274
+ mode: policy.mode,
6275
+ allowed: false,
6276
+ reason: "DIRECTION_BLOCKED"
6277
+ });
6278
+ return false;
6279
+ }
6280
+ if (policy.mode === "current-gate") {
6281
+ const analysis = await runAiPromptLocal(signal);
6282
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6283
+ onDecision?.({
6284
+ mode: policy.mode,
6285
+ allowed: allowed2,
6286
+ quality: analysis.quality ?? null,
6287
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6288
+ });
6289
+ return allowed2;
6290
+ }
6291
+ const original = buildAiPayload(signal);
6292
+ const gateContext = getDeterministicAiGateContext(original);
6293
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6294
+ onDecision?.({
6295
+ mode: policy.mode,
6296
+ allowed: false,
6297
+ reason: "STRUCTURAL_HARD_BLOCK"
6298
+ });
6299
+ return false;
6300
+ }
6301
+ const rules = policy.rules[signal.direction];
6302
+ const payload = buildResearchEntryPayload(signal, original);
6303
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6304
+ onDecision?.({
6305
+ mode: policy.mode,
6306
+ allowed,
6307
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6308
+ predicates: rules?.map((rule) => {
6309
+ const value = rule.path.split(".").reduce(
6310
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6311
+ payload
6312
+ );
6313
+ return {
6314
+ path: rule.path,
6315
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6316
+ passed: matchesResearchEntryRules(payload, [rule])
6317
+ };
6318
+ })
6319
+ });
6320
+ return allowed;
6321
+ };
6322
+
6214
6323
  // src/strategy/runtimeDecisionTelemetry.ts
6215
6324
  var finitePositive = (value) => {
6216
6325
  const parsed = Number(value);
@@ -6749,6 +6858,12 @@ var createStrategyRuntime = ({
6749
6858
  const projectConfig = await loadTradejsConfig(projectRoot);
6750
6859
  const projectHooks = projectConfig.hooks;
6751
6860
  const env = String(config.ENV ?? "BACKTEST");
6861
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6862
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6863
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6864
+ throw new Error(
6865
+ "Research entry policies require a BACKTEST simulation connector"
6866
+ );
6752
6867
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6753
6868
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6754
6869
  throw new Error(
@@ -7459,7 +7574,8 @@ var createStrategyRuntime = ({
7459
7574
  await enrichSignalWithMarketContextStages({
7460
7575
  signal,
7461
7576
  env,
7462
- includeHyperliquidWhales: false
7577
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7578
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7463
7579
  });
7464
7580
  if (jev) {
7465
7581
  await assessSignalWithJev({
@@ -7557,6 +7673,22 @@ var createStrategyRuntime = ({
7557
7673
  }
7558
7674
  return signal ?? decision.code;
7559
7675
  }
7676
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7677
+ researchEntryPolicy,
7678
+ signal,
7679
+ (decision2) => {
7680
+ signal.additionalIndicators = {
7681
+ ...signal.additionalIndicators,
7682
+ researchEntryDecision: decision2
7683
+ };
7684
+ }
7685
+ ))) {
7686
+ if (signal) {
7687
+ signal.orderStatus = "skipped";
7688
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7689
+ }
7690
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7691
+ }
7560
7692
  const entryGate = await invokeGateHooks(
7561
7693
  "beforeEntryGate",
7562
7694
  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-KOV3U6ZZ.mjs";
11
11
  import {
12
12
  ensureIndicatorPluginsLoaded
13
13
  } from "./chunk-UL5Z46JL.mjs";
package/dist/registry.js CHANGED
@@ -2996,6 +2996,7 @@ var recordRuntimeTradeIncrease = async (params) => {
2996
2996
  const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
2997
2997
  const next = {
2998
2998
  ...existing,
2999
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
2999
3000
  qty: resultingQty,
3000
3001
  entryPrice: resultingEntryPrice,
3001
3002
  entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
@@ -5724,6 +5725,7 @@ var executeEntryOrder = async ({
5724
5725
  openFee: estimatedOpenFee,
5725
5726
  totalFee: estimatedOpenFee,
5726
5727
  entryTimestamp: timestamp,
5728
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5727
5729
  universe: signal.universe,
5728
5730
  assetClass: signal.assetClass,
5729
5731
  accountId: signal.accountId,
@@ -5747,6 +5749,7 @@ var executeEntryOrder = async ({
5747
5749
  addedQty: filledQty,
5748
5750
  addedEntryPrice: fillPrice,
5749
5751
  entryTimestamp: timestamp,
5752
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5750
5753
  fee: estimatedOpenFee,
5751
5754
  accountId: signal.accountId,
5752
5755
  deploymentId: signal.deploymentId
@@ -6213,6 +6216,112 @@ var buildAiHookContext = ({
6213
6216
  };
6214
6217
  };
6215
6218
 
6219
+ // src/researchEntryPolicy.ts
6220
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6221
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6222
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6223
+ const raw = signal.additionalIndicators?.baseContext;
6224
+ const adapted = additional.baseContext;
6225
+ return {
6226
+ ...payload,
6227
+ additionalIndicators: {
6228
+ ...additional,
6229
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6230
+ baseContext: {
6231
+ ...raw && typeof raw === "object" ? raw : {},
6232
+ ...adapted && typeof adapted === "object" ? adapted : {}
6233
+ }
6234
+ }
6235
+ };
6236
+ };
6237
+ var parseResearchEntryPolicy = (value) => {
6238
+ if (!value || typeof value !== "object" || Array.isArray(value))
6239
+ throw new Error("Invalid research entry policy");
6240
+ const policy = value;
6241
+ 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")
6242
+ throw new Error("Invalid research entry policy contract");
6243
+ 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))))
6244
+ throw new Error("Invalid research policy directions");
6245
+ for (const side of ["LONG", "SHORT"]) {
6246
+ const predicates = policy.rules[side];
6247
+ if (predicates === null) continue;
6248
+ if (!Array.isArray(predicates) || predicates.length > 4)
6249
+ throw new Error(
6250
+ "Research policy needs bounded rules for both directions"
6251
+ );
6252
+ for (const predicate of predicates) {
6253
+ 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)))
6254
+ throw new Error(
6255
+ "Research policy contains an unsupported or non-causal predicate"
6256
+ );
6257
+ }
6258
+ }
6259
+ return policy;
6260
+ };
6261
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6262
+ const value = predicate.path.split(".").reduce(
6263
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6264
+ payload
6265
+ );
6266
+ if (value == null) return false;
6267
+ if (predicate.op === "==") return value === predicate.value;
6268
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6269
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6270
+ });
6271
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6272
+ if (signal.strategy !== policy.strategy)
6273
+ throw new Error("Research policy strategy mismatch");
6274
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6275
+ onDecision?.({
6276
+ mode: policy.mode,
6277
+ allowed: false,
6278
+ reason: "DIRECTION_BLOCKED"
6279
+ });
6280
+ return false;
6281
+ }
6282
+ if (policy.mode === "current-gate") {
6283
+ const analysis = await runAiPromptLocal(signal);
6284
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6285
+ onDecision?.({
6286
+ mode: policy.mode,
6287
+ allowed: allowed2,
6288
+ quality: analysis.quality ?? null,
6289
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6290
+ });
6291
+ return allowed2;
6292
+ }
6293
+ const original = buildAiPayload(signal);
6294
+ const gateContext = getDeterministicAiGateContext(original);
6295
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6296
+ onDecision?.({
6297
+ mode: policy.mode,
6298
+ allowed: false,
6299
+ reason: "STRUCTURAL_HARD_BLOCK"
6300
+ });
6301
+ return false;
6302
+ }
6303
+ const rules = policy.rules[signal.direction];
6304
+ const payload = buildResearchEntryPayload(signal, original);
6305
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6306
+ onDecision?.({
6307
+ mode: policy.mode,
6308
+ allowed,
6309
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6310
+ predicates: rules?.map((rule) => {
6311
+ const value = rule.path.split(".").reduce(
6312
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6313
+ payload
6314
+ );
6315
+ return {
6316
+ path: rule.path,
6317
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6318
+ passed: matchesResearchEntryRules(payload, [rule])
6319
+ };
6320
+ })
6321
+ });
6322
+ return allowed;
6323
+ };
6324
+
6216
6325
  // src/strategy/runtimeDecisionTelemetry.ts
6217
6326
  var finitePositive = (value) => {
6218
6327
  const parsed = Number(value);
@@ -6751,6 +6860,12 @@ var createStrategyRuntime = ({
6751
6860
  const projectConfig = await loadTradejsConfig(projectRoot);
6752
6861
  const projectHooks = projectConfig.hooks;
6753
6862
  const env = String(config.ENV ?? "BACKTEST");
6863
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6864
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6865
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6866
+ throw new Error(
6867
+ "Research entry policies require a BACKTEST simulation connector"
6868
+ );
6754
6869
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6755
6870
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6756
6871
  throw new Error(
@@ -7461,7 +7576,8 @@ var createStrategyRuntime = ({
7461
7576
  await enrichSignalWithMarketContextStages({
7462
7577
  signal,
7463
7578
  env,
7464
- includeHyperliquidWhales: false
7579
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7580
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7465
7581
  });
7466
7582
  if (jev) {
7467
7583
  await assessSignalWithJev({
@@ -7559,6 +7675,22 @@ var createStrategyRuntime = ({
7559
7675
  }
7560
7676
  return signal ?? decision.code;
7561
7677
  }
7678
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7679
+ researchEntryPolicy,
7680
+ signal,
7681
+ (decision2) => {
7682
+ signal.additionalIndicators = {
7683
+ ...signal.additionalIndicators,
7684
+ researchEntryDecision: decision2
7685
+ };
7686
+ }
7687
+ ))) {
7688
+ if (signal) {
7689
+ signal.orderStatus = "skipped";
7690
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7691
+ }
7692
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7693
+ }
7562
7694
  const entryGate = await invokeGateHooks(
7563
7695
  "beforeEntryGate",
7564
7696
  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-KOV3U6ZZ.mjs";
3
3
  import {
4
4
  ensureIndicatorPluginsLoaded,
5
5
  ensureStrategyPluginsLoaded,