@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.
@@ -3978,6 +3978,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3978
3978
  const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3979
3979
  const next = {
3980
3980
  ...existing,
3981
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3981
3982
  qty: resultingQty,
3982
3983
  entryPrice: resultingEntryPrice,
3983
3984
  entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
@@ -6706,6 +6707,7 @@ var executeEntryOrder = async ({
6706
6707
  openFee: estimatedOpenFee,
6707
6708
  totalFee: estimatedOpenFee,
6708
6709
  entryTimestamp: timestamp,
6710
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
6709
6711
  universe: signal.universe,
6710
6712
  assetClass: signal.assetClass,
6711
6713
  accountId: signal.accountId,
@@ -6729,6 +6731,7 @@ var executeEntryOrder = async ({
6729
6731
  addedQty: filledQty,
6730
6732
  addedEntryPrice: fillPrice,
6731
6733
  entryTimestamp: timestamp,
6734
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
6732
6735
  fee: estimatedOpenFee,
6733
6736
  accountId: signal.accountId,
6734
6737
  deploymentId: signal.deploymentId
@@ -7195,6 +7198,112 @@ var buildAiHookContext = ({
7195
7198
  };
7196
7199
  };
7197
7200
 
7201
+ // src/researchEntryPolicy.ts
7202
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
7203
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
7204
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
7205
+ const raw = signal.additionalIndicators?.baseContext;
7206
+ const adapted = additional.baseContext;
7207
+ return {
7208
+ ...payload,
7209
+ additionalIndicators: {
7210
+ ...additional,
7211
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
7212
+ baseContext: {
7213
+ ...raw && typeof raw === "object" ? raw : {},
7214
+ ...adapted && typeof adapted === "object" ? adapted : {}
7215
+ }
7216
+ }
7217
+ };
7218
+ };
7219
+ var parseResearchEntryPolicy = (value) => {
7220
+ if (!value || typeof value !== "object" || Array.isArray(value))
7221
+ throw new Error("Invalid research entry policy");
7222
+ const policy = value;
7223
+ 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")
7224
+ throw new Error("Invalid research entry policy contract");
7225
+ 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))))
7226
+ throw new Error("Invalid research policy directions");
7227
+ for (const side of ["LONG", "SHORT"]) {
7228
+ const predicates = policy.rules[side];
7229
+ if (predicates === null) continue;
7230
+ if (!Array.isArray(predicates) || predicates.length > 4)
7231
+ throw new Error(
7232
+ "Research policy needs bounded rules for both directions"
7233
+ );
7234
+ for (const predicate of predicates) {
7235
+ 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)))
7236
+ throw new Error(
7237
+ "Research policy contains an unsupported or non-causal predicate"
7238
+ );
7239
+ }
7240
+ }
7241
+ return policy;
7242
+ };
7243
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
7244
+ const value = predicate.path.split(".").reduce(
7245
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
7246
+ payload
7247
+ );
7248
+ if (value == null) return false;
7249
+ if (predicate.op === "==") return value === predicate.value;
7250
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
7251
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
7252
+ });
7253
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
7254
+ if (signal.strategy !== policy.strategy)
7255
+ throw new Error("Research policy strategy mismatch");
7256
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
7257
+ onDecision?.({
7258
+ mode: policy.mode,
7259
+ allowed: false,
7260
+ reason: "DIRECTION_BLOCKED"
7261
+ });
7262
+ return false;
7263
+ }
7264
+ if (policy.mode === "current-gate") {
7265
+ const analysis = await runAiPromptLocal(signal);
7266
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
7267
+ onDecision?.({
7268
+ mode: policy.mode,
7269
+ allowed: allowed2,
7270
+ quality: analysis.quality ?? null,
7271
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
7272
+ });
7273
+ return allowed2;
7274
+ }
7275
+ const original = buildAiPayload(signal);
7276
+ const gateContext = getDeterministicAiGateContext(original);
7277
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
7278
+ onDecision?.({
7279
+ mode: policy.mode,
7280
+ allowed: false,
7281
+ reason: "STRUCTURAL_HARD_BLOCK"
7282
+ });
7283
+ return false;
7284
+ }
7285
+ const rules = policy.rules[signal.direction];
7286
+ const payload = buildResearchEntryPayload(signal, original);
7287
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
7288
+ onDecision?.({
7289
+ mode: policy.mode,
7290
+ allowed,
7291
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
7292
+ predicates: rules?.map((rule) => {
7293
+ const value = rule.path.split(".").reduce(
7294
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
7295
+ payload
7296
+ );
7297
+ return {
7298
+ path: rule.path,
7299
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
7300
+ passed: matchesResearchEntryRules(payload, [rule])
7301
+ };
7302
+ })
7303
+ });
7304
+ return allowed;
7305
+ };
7306
+
7198
7307
  // src/strategy/runtimeDecisionTelemetry.ts
7199
7308
  var finitePositive = (value) => {
7200
7309
  const parsed = Number(value);
@@ -7733,6 +7842,12 @@ var createStrategyRuntime = ({
7733
7842
  const projectConfig = await loadTradejsConfig(projectRoot);
7734
7843
  const projectHooks = projectConfig.hooks;
7735
7844
  const env = String(config.ENV ?? "BACKTEST");
7845
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7846
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7847
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
7848
+ throw new Error(
7849
+ "Research entry policies require a BACKTEST simulation connector"
7850
+ );
7736
7851
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
7737
7852
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
7738
7853
  throw new Error(
@@ -8443,7 +8558,8 @@ var createStrategyRuntime = ({
8443
8558
  await enrichSignalWithMarketContextStages({
8444
8559
  signal,
8445
8560
  env,
8446
- includeHyperliquidWhales: false
8561
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
8562
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
8447
8563
  });
8448
8564
  if (jev) {
8449
8565
  await assessSignalWithJev({
@@ -8541,6 +8657,22 @@ var createStrategyRuntime = ({
8541
8657
  }
8542
8658
  return signal ?? decision.code;
8543
8659
  }
8660
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
8661
+ researchEntryPolicy,
8662
+ signal,
8663
+ (decision2) => {
8664
+ signal.additionalIndicators = {
8665
+ ...signal.additionalIndicators,
8666
+ researchEntryDecision: decision2
8667
+ };
8668
+ }
8669
+ ))) {
8670
+ if (signal) {
8671
+ signal.orderStatus = "skipped";
8672
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
8673
+ }
8674
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
8675
+ }
8544
8676
  const entryGate = await invokeGateHooks(
8545
8677
  "beforeEntryGate",
8546
8678
  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-KOV3U6ZZ.mjs";
13
13
  import "./chunk-UL5Z46JL.mjs";
14
14
  import "./chunk-LZDXRXIU.mjs";
15
15
  import "./chunk-YR55Q355.mjs";
@@ -2975,6 +2975,7 @@ var recordRuntimeTradeIncrease = async (params) => {
2975
2975
  const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
2976
2976
  const next = {
2977
2977
  ...existing,
2978
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
2978
2979
  qty: resultingQty,
2979
2980
  entryPrice: resultingEntryPrice,
2980
2981
  entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
@@ -5703,6 +5704,7 @@ var executeEntryOrder = async ({
5703
5704
  openFee: estimatedOpenFee,
5704
5705
  totalFee: estimatedOpenFee,
5705
5706
  entryTimestamp: timestamp,
5707
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5706
5708
  universe: signal.universe,
5707
5709
  assetClass: signal.assetClass,
5708
5710
  accountId: signal.accountId,
@@ -5726,6 +5728,7 @@ var executeEntryOrder = async ({
5726
5728
  addedQty: filledQty,
5727
5729
  addedEntryPrice: fillPrice,
5728
5730
  entryTimestamp: timestamp,
5731
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5729
5732
  fee: estimatedOpenFee,
5730
5733
  accountId: signal.accountId,
5731
5734
  deploymentId: signal.deploymentId
@@ -6192,6 +6195,112 @@ var buildAiHookContext = ({
6192
6195
  };
6193
6196
  };
6194
6197
 
6198
+ // src/researchEntryPolicy.ts
6199
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6200
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6201
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6202
+ const raw = signal.additionalIndicators?.baseContext;
6203
+ const adapted = additional.baseContext;
6204
+ return {
6205
+ ...payload,
6206
+ additionalIndicators: {
6207
+ ...additional,
6208
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6209
+ baseContext: {
6210
+ ...raw && typeof raw === "object" ? raw : {},
6211
+ ...adapted && typeof adapted === "object" ? adapted : {}
6212
+ }
6213
+ }
6214
+ };
6215
+ };
6216
+ var parseResearchEntryPolicy = (value) => {
6217
+ if (!value || typeof value !== "object" || Array.isArray(value))
6218
+ throw new Error("Invalid research entry policy");
6219
+ const policy = value;
6220
+ 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")
6221
+ throw new Error("Invalid research entry policy contract");
6222
+ 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))))
6223
+ throw new Error("Invalid research policy directions");
6224
+ for (const side of ["LONG", "SHORT"]) {
6225
+ const predicates = policy.rules[side];
6226
+ if (predicates === null) continue;
6227
+ if (!Array.isArray(predicates) || predicates.length > 4)
6228
+ throw new Error(
6229
+ "Research policy needs bounded rules for both directions"
6230
+ );
6231
+ for (const predicate of predicates) {
6232
+ 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)))
6233
+ throw new Error(
6234
+ "Research policy contains an unsupported or non-causal predicate"
6235
+ );
6236
+ }
6237
+ }
6238
+ return policy;
6239
+ };
6240
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6241
+ const value = predicate.path.split(".").reduce(
6242
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6243
+ payload
6244
+ );
6245
+ if (value == null) return false;
6246
+ if (predicate.op === "==") return value === predicate.value;
6247
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6248
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6249
+ });
6250
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6251
+ if (signal.strategy !== policy.strategy)
6252
+ throw new Error("Research policy strategy mismatch");
6253
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6254
+ onDecision?.({
6255
+ mode: policy.mode,
6256
+ allowed: false,
6257
+ reason: "DIRECTION_BLOCKED"
6258
+ });
6259
+ return false;
6260
+ }
6261
+ if (policy.mode === "current-gate") {
6262
+ const analysis = await runAiPromptLocal(signal);
6263
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6264
+ onDecision?.({
6265
+ mode: policy.mode,
6266
+ allowed: allowed2,
6267
+ quality: analysis.quality ?? null,
6268
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6269
+ });
6270
+ return allowed2;
6271
+ }
6272
+ const original = buildAiPayload(signal);
6273
+ const gateContext = getDeterministicAiGateContext(original);
6274
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6275
+ onDecision?.({
6276
+ mode: policy.mode,
6277
+ allowed: false,
6278
+ reason: "STRUCTURAL_HARD_BLOCK"
6279
+ });
6280
+ return false;
6281
+ }
6282
+ const rules = policy.rules[signal.direction];
6283
+ const payload = buildResearchEntryPayload(signal, original);
6284
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6285
+ onDecision?.({
6286
+ mode: policy.mode,
6287
+ allowed,
6288
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6289
+ predicates: rules?.map((rule) => {
6290
+ const value = rule.path.split(".").reduce(
6291
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6292
+ payload
6293
+ );
6294
+ return {
6295
+ path: rule.path,
6296
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6297
+ passed: matchesResearchEntryRules(payload, [rule])
6298
+ };
6299
+ })
6300
+ });
6301
+ return allowed;
6302
+ };
6303
+
6195
6304
  // src/strategy/runtimeDecisionTelemetry.ts
6196
6305
  var finitePositive = (value) => {
6197
6306
  const parsed = Number(value);
@@ -6730,6 +6839,12 @@ var createStrategyRuntime = ({
6730
6839
  const projectConfig = await loadTradejsConfig(projectRoot);
6731
6840
  const projectHooks = projectConfig.hooks;
6732
6841
  const env = String(config.ENV ?? "BACKTEST");
6842
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6843
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6844
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6845
+ throw new Error(
6846
+ "Research entry policies require a BACKTEST simulation connector"
6847
+ );
6733
6848
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6734
6849
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6735
6850
  throw new Error(
@@ -7440,7 +7555,8 @@ var createStrategyRuntime = ({
7440
7555
  await enrichSignalWithMarketContextStages({
7441
7556
  signal,
7442
7557
  env,
7443
- includeHyperliquidWhales: false
7558
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7559
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7444
7560
  });
7445
7561
  if (jev) {
7446
7562
  await assessSignalWithJev({
@@ -7538,6 +7654,22 @@ var createStrategyRuntime = ({
7538
7654
  }
7539
7655
  return signal ?? decision.code;
7540
7656
  }
7657
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7658
+ researchEntryPolicy,
7659
+ signal,
7660
+ (decision2) => {
7661
+ signal.additionalIndicators = {
7662
+ ...signal.additionalIndicators,
7663
+ researchEntryDecision: decision2
7664
+ };
7665
+ }
7666
+ ))) {
7667
+ if (signal) {
7668
+ signal.orderStatus = "skipped";
7669
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7670
+ }
7671
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7672
+ }
7541
7673
  const entryGate = await invokeGateHooks(
7542
7674
  "beforeEntryGate",
7543
7675
  decisionManifest?.hooks?.beforeEntryGate,
@@ -9,7 +9,7 @@ import {
9
9
  resolveRuntimeComposition,
10
10
  verifyRuntimeDeclaration
11
11
  } from "./chunk-BO42QWAB.mjs";
12
- import "./chunk-SKY2HNZU.mjs";
12
+ import "./chunk-KOV3U6ZZ.mjs";
13
13
  import "./chunk-UL5Z46JL.mjs";
14
14
  import "./chunk-LZDXRXIU.mjs";
15
15
  import "./chunk-YR55Q355.mjs";
@@ -3068,6 +3068,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3068
3068
  const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3069
3069
  const next = {
3070
3070
  ...existing,
3071
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3071
3072
  qty: resultingQty,
3072
3073
  entryPrice: resultingEntryPrice,
3073
3074
  entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
@@ -5965,6 +5966,7 @@ var executeEntryOrder = async ({
5965
5966
  openFee: estimatedOpenFee,
5966
5967
  totalFee: estimatedOpenFee,
5967
5968
  entryTimestamp: timestamp,
5969
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5968
5970
  universe: signal.universe,
5969
5971
  assetClass: signal.assetClass,
5970
5972
  accountId: signal.accountId,
@@ -5988,6 +5990,7 @@ var executeEntryOrder = async ({
5988
5990
  addedQty: filledQty,
5989
5991
  addedEntryPrice: fillPrice,
5990
5992
  entryTimestamp: timestamp,
5993
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5991
5994
  fee: estimatedOpenFee,
5992
5995
  accountId: signal.accountId,
5993
5996
  deploymentId: signal.deploymentId
@@ -6454,6 +6457,112 @@ var buildAiHookContext = ({
6454
6457
  };
6455
6458
  };
6456
6459
 
6460
+ // src/researchEntryPolicy.ts
6461
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6462
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6463
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6464
+ const raw = signal.additionalIndicators?.baseContext;
6465
+ const adapted = additional.baseContext;
6466
+ return {
6467
+ ...payload,
6468
+ additionalIndicators: {
6469
+ ...additional,
6470
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6471
+ baseContext: {
6472
+ ...raw && typeof raw === "object" ? raw : {},
6473
+ ...adapted && typeof adapted === "object" ? adapted : {}
6474
+ }
6475
+ }
6476
+ };
6477
+ };
6478
+ var parseResearchEntryPolicy = (value) => {
6479
+ if (!value || typeof value !== "object" || Array.isArray(value))
6480
+ throw new Error("Invalid research entry policy");
6481
+ const policy = value;
6482
+ 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")
6483
+ throw new Error("Invalid research entry policy contract");
6484
+ 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))))
6485
+ throw new Error("Invalid research policy directions");
6486
+ for (const side of ["LONG", "SHORT"]) {
6487
+ const predicates = policy.rules[side];
6488
+ if (predicates === null) continue;
6489
+ if (!Array.isArray(predicates) || predicates.length > 4)
6490
+ throw new Error(
6491
+ "Research policy needs bounded rules for both directions"
6492
+ );
6493
+ for (const predicate of predicates) {
6494
+ 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)))
6495
+ throw new Error(
6496
+ "Research policy contains an unsupported or non-causal predicate"
6497
+ );
6498
+ }
6499
+ }
6500
+ return policy;
6501
+ };
6502
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6503
+ const value = predicate.path.split(".").reduce(
6504
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6505
+ payload
6506
+ );
6507
+ if (value == null) return false;
6508
+ if (predicate.op === "==") return value === predicate.value;
6509
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6510
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6511
+ });
6512
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6513
+ if (signal.strategy !== policy.strategy)
6514
+ throw new Error("Research policy strategy mismatch");
6515
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6516
+ onDecision?.({
6517
+ mode: policy.mode,
6518
+ allowed: false,
6519
+ reason: "DIRECTION_BLOCKED"
6520
+ });
6521
+ return false;
6522
+ }
6523
+ if (policy.mode === "current-gate") {
6524
+ const analysis = await runAiPromptLocal(signal);
6525
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6526
+ onDecision?.({
6527
+ mode: policy.mode,
6528
+ allowed: allowed2,
6529
+ quality: analysis.quality ?? null,
6530
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6531
+ });
6532
+ return allowed2;
6533
+ }
6534
+ const original = buildAiPayload(signal);
6535
+ const gateContext = getDeterministicAiGateContext(original);
6536
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6537
+ onDecision?.({
6538
+ mode: policy.mode,
6539
+ allowed: false,
6540
+ reason: "STRUCTURAL_HARD_BLOCK"
6541
+ });
6542
+ return false;
6543
+ }
6544
+ const rules = policy.rules[signal.direction];
6545
+ const payload = buildResearchEntryPayload(signal, original);
6546
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6547
+ onDecision?.({
6548
+ mode: policy.mode,
6549
+ allowed,
6550
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6551
+ predicates: rules?.map((rule) => {
6552
+ const value = rule.path.split(".").reduce(
6553
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6554
+ payload
6555
+ );
6556
+ return {
6557
+ path: rule.path,
6558
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6559
+ passed: matchesResearchEntryRules(payload, [rule])
6560
+ };
6561
+ })
6562
+ });
6563
+ return allowed;
6564
+ };
6565
+
6457
6566
  // src/strategy/runtimeDecisionTelemetry.ts
6458
6567
  var finitePositive = (value) => {
6459
6568
  const parsed = Number(value);
@@ -6992,6 +7101,12 @@ var createStrategyRuntime = ({
6992
7101
  const projectConfig = await loadTradejsConfig(projectRoot);
6993
7102
  const projectHooks = projectConfig.hooks;
6994
7103
  const env = String(config.ENV ?? "BACKTEST");
7104
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7105
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7106
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
7107
+ throw new Error(
7108
+ "Research entry policies require a BACKTEST simulation connector"
7109
+ );
6995
7110
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6996
7111
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6997
7112
  throw new Error(
@@ -7702,7 +7817,8 @@ var createStrategyRuntime = ({
7702
7817
  await enrichSignalWithMarketContextStages({
7703
7818
  signal,
7704
7819
  env,
7705
- includeHyperliquidWhales: false
7820
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7821
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7706
7822
  });
7707
7823
  if (jev) {
7708
7824
  await assessSignalWithJev({
@@ -7800,6 +7916,22 @@ var createStrategyRuntime = ({
7800
7916
  }
7801
7917
  return signal ?? decision.code;
7802
7918
  }
7919
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7920
+ researchEntryPolicy,
7921
+ signal,
7922
+ (decision2) => {
7923
+ signal.additionalIndicators = {
7924
+ ...signal.additionalIndicators,
7925
+ researchEntryDecision: decision2
7926
+ };
7927
+ }
7928
+ ))) {
7929
+ if (signal) {
7930
+ signal.orderStatus = "skipped";
7931
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7932
+ }
7933
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7934
+ }
7803
7935
  const entryGate = await invokeGateHooks(
7804
7936
  "beforeEntryGate",
7805
7937
  decisionManifest?.hooks?.beforeEntryGate,
@@ -23,7 +23,7 @@ import {
23
23
  resolveHyperliquidPerpFromSignalSymbol,
24
24
  resolveStrategyConfig,
25
25
  validateEntryProtectionAtArrival
26
- } from "./chunk-SKY2HNZU.mjs";
26
+ } from "./chunk-KOV3U6ZZ.mjs";
27
27
  import {
28
28
  DEFAULT_AI_MODEL,
29
29
  MAX_AI_SERIES_POINTS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/node",
3
- "version": "3.1.29-beta.283",
3
+ "version": "3.1.29-beta.285",
4
4
  "description": "Node-only runtime for the TradeJS TypeScript framework: strategies, backtests, Pine strategy loading, and plugin registries.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -105,9 +105,9 @@
105
105
  "dependencies": {
106
106
  "@langchain/core": "^1.2.3",
107
107
  "@langchain/openai": "^1.5.5",
108
- "@tradejs/core": "^3.1.29-beta.283",
109
- "@tradejs/infra": "^3.1.29-beta.283",
110
- "@tradejs/types": "^3.1.29-beta.283",
108
+ "@tradejs/core": "^3.1.29-beta.285",
109
+ "@tradejs/infra": "^3.1.29-beta.285",
110
+ "@tradejs/types": "^3.1.29-beta.285",
111
111
  "@types/node": "^24",
112
112
  "chalk": "4.1.2",
113
113
  "ioredis": "5.11.1",
@@ -120,7 +120,7 @@
120
120
  "typescript": "^5.9"
121
121
  },
122
122
  "devDependencies": {
123
- "@tradejs/indicators": "^3.1.29-beta.283",
123
+ "@tradejs/indicators": "^3.1.29-beta.285",
124
124
  "@tradejs/strategy-adaptive-momentum-ribbon": "3.0.5",
125
125
  "@tradejs/strategy-hyperliquid-consensus": "3.0.5",
126
126
  "@tradejs/strategy-kit": "3.0.6",