@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.
@@ -6192,6 +6192,112 @@ var buildAiHookContext = ({
6192
6192
  };
6193
6193
  };
6194
6194
 
6195
+ // src/researchEntryPolicy.ts
6196
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6197
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6198
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6199
+ const raw = signal.additionalIndicators?.baseContext;
6200
+ const adapted = additional.baseContext;
6201
+ return {
6202
+ ...payload,
6203
+ additionalIndicators: {
6204
+ ...additional,
6205
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6206
+ baseContext: {
6207
+ ...raw && typeof raw === "object" ? raw : {},
6208
+ ...adapted && typeof adapted === "object" ? adapted : {}
6209
+ }
6210
+ }
6211
+ };
6212
+ };
6213
+ var parseResearchEntryPolicy = (value) => {
6214
+ if (!value || typeof value !== "object" || Array.isArray(value))
6215
+ throw new Error("Invalid research entry policy");
6216
+ const policy = value;
6217
+ 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")
6218
+ throw new Error("Invalid research entry policy contract");
6219
+ 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))))
6220
+ throw new Error("Invalid research policy directions");
6221
+ for (const side of ["LONG", "SHORT"]) {
6222
+ const predicates = policy.rules[side];
6223
+ if (predicates === null) continue;
6224
+ if (!Array.isArray(predicates) || predicates.length > 4)
6225
+ throw new Error(
6226
+ "Research policy needs bounded rules for both directions"
6227
+ );
6228
+ for (const predicate of predicates) {
6229
+ 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)))
6230
+ throw new Error(
6231
+ "Research policy contains an unsupported or non-causal predicate"
6232
+ );
6233
+ }
6234
+ }
6235
+ return policy;
6236
+ };
6237
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6238
+ const value = predicate.path.split(".").reduce(
6239
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6240
+ payload
6241
+ );
6242
+ if (value == null) return false;
6243
+ if (predicate.op === "==") return value === predicate.value;
6244
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6245
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6246
+ });
6247
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6248
+ if (signal.strategy !== policy.strategy)
6249
+ throw new Error("Research policy strategy mismatch");
6250
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6251
+ onDecision?.({
6252
+ mode: policy.mode,
6253
+ allowed: false,
6254
+ reason: "DIRECTION_BLOCKED"
6255
+ });
6256
+ return false;
6257
+ }
6258
+ if (policy.mode === "current-gate") {
6259
+ const analysis = await runAiPromptLocal(signal);
6260
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6261
+ onDecision?.({
6262
+ mode: policy.mode,
6263
+ allowed: allowed2,
6264
+ quality: analysis.quality ?? null,
6265
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6266
+ });
6267
+ return allowed2;
6268
+ }
6269
+ const original = buildAiPayload(signal);
6270
+ const gateContext = getDeterministicAiGateContext(original);
6271
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6272
+ onDecision?.({
6273
+ mode: policy.mode,
6274
+ allowed: false,
6275
+ reason: "STRUCTURAL_HARD_BLOCK"
6276
+ });
6277
+ return false;
6278
+ }
6279
+ const rules = policy.rules[signal.direction];
6280
+ const payload = buildResearchEntryPayload(signal, original);
6281
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6282
+ onDecision?.({
6283
+ mode: policy.mode,
6284
+ allowed,
6285
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6286
+ predicates: rules?.map((rule) => {
6287
+ const value = rule.path.split(".").reduce(
6288
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6289
+ payload
6290
+ );
6291
+ return {
6292
+ path: rule.path,
6293
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6294
+ passed: matchesResearchEntryRules(payload, [rule])
6295
+ };
6296
+ })
6297
+ });
6298
+ return allowed;
6299
+ };
6300
+
6195
6301
  // src/strategy/runtimeDecisionTelemetry.ts
6196
6302
  var finitePositive = (value) => {
6197
6303
  const parsed = Number(value);
@@ -6730,6 +6836,12 @@ var createStrategyRuntime = ({
6730
6836
  const projectConfig = await loadTradejsConfig(projectRoot);
6731
6837
  const projectHooks = projectConfig.hooks;
6732
6838
  const env = String(config.ENV ?? "BACKTEST");
6839
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6840
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6841
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6842
+ throw new Error(
6843
+ "Research entry policies require a BACKTEST simulation connector"
6844
+ );
6733
6845
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6734
6846
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6735
6847
  throw new Error(
@@ -7440,7 +7552,8 @@ var createStrategyRuntime = ({
7440
7552
  await enrichSignalWithMarketContextStages({
7441
7553
  signal,
7442
7554
  env,
7443
- includeHyperliquidWhales: false
7555
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7556
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7444
7557
  });
7445
7558
  if (jev) {
7446
7559
  await assessSignalWithJev({
@@ -7538,6 +7651,22 @@ var createStrategyRuntime = ({
7538
7651
  }
7539
7652
  return signal ?? decision.code;
7540
7653
  }
7654
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7655
+ researchEntryPolicy,
7656
+ signal,
7657
+ (decision2) => {
7658
+ signal.additionalIndicators = {
7659
+ ...signal.additionalIndicators,
7660
+ researchEntryDecision: decision2
7661
+ };
7662
+ }
7663
+ ))) {
7664
+ if (signal) {
7665
+ signal.orderStatus = "skipped";
7666
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7667
+ }
7668
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7669
+ }
7541
7670
  const entryGate = await invokeGateHooks(
7542
7671
  "beforeEntryGate",
7543
7672
  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-K56EW2BB.mjs";
13
13
  import "./chunk-UL5Z46JL.mjs";
14
14
  import "./chunk-LZDXRXIU.mjs";
15
15
  import "./chunk-YR55Q355.mjs";
@@ -6454,6 +6454,112 @@ var buildAiHookContext = ({
6454
6454
  };
6455
6455
  };
6456
6456
 
6457
+ // src/researchEntryPolicy.ts
6458
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6459
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6460
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6461
+ const raw = signal.additionalIndicators?.baseContext;
6462
+ const adapted = additional.baseContext;
6463
+ return {
6464
+ ...payload,
6465
+ additionalIndicators: {
6466
+ ...additional,
6467
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6468
+ baseContext: {
6469
+ ...raw && typeof raw === "object" ? raw : {},
6470
+ ...adapted && typeof adapted === "object" ? adapted : {}
6471
+ }
6472
+ }
6473
+ };
6474
+ };
6475
+ var parseResearchEntryPolicy = (value) => {
6476
+ if (!value || typeof value !== "object" || Array.isArray(value))
6477
+ throw new Error("Invalid research entry policy");
6478
+ const policy = value;
6479
+ 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")
6480
+ throw new Error("Invalid research entry policy contract");
6481
+ 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))))
6482
+ throw new Error("Invalid research policy directions");
6483
+ for (const side of ["LONG", "SHORT"]) {
6484
+ const predicates = policy.rules[side];
6485
+ if (predicates === null) continue;
6486
+ if (!Array.isArray(predicates) || predicates.length > 4)
6487
+ throw new Error(
6488
+ "Research policy needs bounded rules for both directions"
6489
+ );
6490
+ for (const predicate of predicates) {
6491
+ 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)))
6492
+ throw new Error(
6493
+ "Research policy contains an unsupported or non-causal predicate"
6494
+ );
6495
+ }
6496
+ }
6497
+ return policy;
6498
+ };
6499
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6500
+ const value = predicate.path.split(".").reduce(
6501
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6502
+ payload
6503
+ );
6504
+ if (value == null) return false;
6505
+ if (predicate.op === "==") return value === predicate.value;
6506
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6507
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6508
+ });
6509
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6510
+ if (signal.strategy !== policy.strategy)
6511
+ throw new Error("Research policy strategy mismatch");
6512
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6513
+ onDecision?.({
6514
+ mode: policy.mode,
6515
+ allowed: false,
6516
+ reason: "DIRECTION_BLOCKED"
6517
+ });
6518
+ return false;
6519
+ }
6520
+ if (policy.mode === "current-gate") {
6521
+ const analysis = await runAiPromptLocal(signal);
6522
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6523
+ onDecision?.({
6524
+ mode: policy.mode,
6525
+ allowed: allowed2,
6526
+ quality: analysis.quality ?? null,
6527
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6528
+ });
6529
+ return allowed2;
6530
+ }
6531
+ const original = buildAiPayload(signal);
6532
+ const gateContext = getDeterministicAiGateContext(original);
6533
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6534
+ onDecision?.({
6535
+ mode: policy.mode,
6536
+ allowed: false,
6537
+ reason: "STRUCTURAL_HARD_BLOCK"
6538
+ });
6539
+ return false;
6540
+ }
6541
+ const rules = policy.rules[signal.direction];
6542
+ const payload = buildResearchEntryPayload(signal, original);
6543
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6544
+ onDecision?.({
6545
+ mode: policy.mode,
6546
+ allowed,
6547
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6548
+ predicates: rules?.map((rule) => {
6549
+ const value = rule.path.split(".").reduce(
6550
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6551
+ payload
6552
+ );
6553
+ return {
6554
+ path: rule.path,
6555
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6556
+ passed: matchesResearchEntryRules(payload, [rule])
6557
+ };
6558
+ })
6559
+ });
6560
+ return allowed;
6561
+ };
6562
+
6457
6563
  // src/strategy/runtimeDecisionTelemetry.ts
6458
6564
  var finitePositive = (value) => {
6459
6565
  const parsed = Number(value);
@@ -6992,6 +7098,12 @@ var createStrategyRuntime = ({
6992
7098
  const projectConfig = await loadTradejsConfig(projectRoot);
6993
7099
  const projectHooks = projectConfig.hooks;
6994
7100
  const env = String(config.ENV ?? "BACKTEST");
7101
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7102
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7103
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
7104
+ throw new Error(
7105
+ "Research entry policies require a BACKTEST simulation connector"
7106
+ );
6995
7107
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6996
7108
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6997
7109
  throw new Error(
@@ -7702,7 +7814,8 @@ var createStrategyRuntime = ({
7702
7814
  await enrichSignalWithMarketContextStages({
7703
7815
  signal,
7704
7816
  env,
7705
- includeHyperliquidWhales: false
7817
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7818
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7706
7819
  });
7707
7820
  if (jev) {
7708
7821
  await assessSignalWithJev({
@@ -7800,6 +7913,22 @@ var createStrategyRuntime = ({
7800
7913
  }
7801
7914
  return signal ?? decision.code;
7802
7915
  }
7916
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7917
+ researchEntryPolicy,
7918
+ signal,
7919
+ (decision2) => {
7920
+ signal.additionalIndicators = {
7921
+ ...signal.additionalIndicators,
7922
+ researchEntryDecision: decision2
7923
+ };
7924
+ }
7925
+ ))) {
7926
+ if (signal) {
7927
+ signal.orderStatus = "skipped";
7928
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7929
+ }
7930
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7931
+ }
7803
7932
  const entryGate = await invokeGateHooks(
7804
7933
  "beforeEntryGate",
7805
7934
  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-K56EW2BB.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.284",
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.284",
109
+ "@tradejs/infra": "^3.1.29-beta.284",
110
+ "@tradejs/types": "^3.1.29-beta.284",
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.284",
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",