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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -95,10 +95,10 @@ var parseRuntimeEvidenceDeploymentSnapshot = (value) => {
95
95
  };
96
96
  var activeRuntimeEvidenceStrategies = (deployment) => deployment.enabled ? deployment.strategies.filter((strategy) => strategy.enabled) : [];
97
97
  var unwrapRuntimeEvidenceRows = (value, field) => (Array.isArray(value) ? value : []).map((candidate) => {
98
- const record = isRecord(candidate) ? candidate : null;
99
- const nested = record && isRecord(record[field]) ? record[field] : null;
100
- return nested ?? record;
101
- }).filter((record) => record != null);
98
+ const record2 = isRecord(candidate) ? candidate : null;
99
+ const nested = record2 && isRecord(record2[field]) ? record2[field] : null;
100
+ return nested ?? record2;
101
+ }).filter((record2) => record2 != null);
102
102
  var assertCurrentRuntimeEvidenceArtifact = (artifact) => {
103
103
  if (!isRecord(artifact) || artifact.reportType !== "runtime-evidence") {
104
104
  throw new Error("Invalid runtime evidence artifact");
@@ -840,7 +840,168 @@ var verifyRuntimeFeedbackReplaySource = async ({
840
840
  });
841
841
  return { feedback, runtimeEvidence };
842
842
  };
843
+
844
+ // src/diagnostics/mcpFeedbackContract.ts
845
+ var MCP_FEEDBACK_CONTRACT = {
846
+ version: 1,
847
+ hostProtocolVersion: 2,
848
+ replayQueue: {
849
+ waitingStates: ["waiting_resources", "waiting_executor"],
850
+ automaticFailedRetries: false
851
+ },
852
+ artifactEncoding: "base64",
853
+ maximumChunkBytes: 48e3,
854
+ artifactFiles: ["payload", "manifest.json", ".complete", "replay.log"],
855
+ requiredTools: [
856
+ "tradejs_info",
857
+ "diagnostics_list_reports",
858
+ "diagnostics_get_report",
859
+ "artifact_get",
860
+ "job_get",
861
+ "job_get_logs"
862
+ ]
863
+ };
864
+ var assertMcpFeedbackContract = (version, tools) => {
865
+ if (version !== MCP_FEEDBACK_CONTRACT.version)
866
+ throw new Error("MCP feedback contract version mismatch");
867
+ for (const name of MCP_FEEDBACK_CONTRACT.requiredTools)
868
+ if (!tools.some((tool) => tool.name === name))
869
+ throw new Error(`MCP feedback requires tool ${name}`);
870
+ const properties = tools.find((tool) => tool.name === "artifact_get")?.inputSchema.properties;
871
+ if (!properties || !("file" in properties))
872
+ throw new Error(
873
+ "MCP feedback requires artifact_get.file; refresh the client tool schema"
874
+ );
875
+ };
876
+
877
+ // src/diagnostics/mcpBundleDownload.ts
878
+ import { createHash as createHash4 } from "crypto";
879
+ import fs4 from "fs/promises";
880
+ import path4 from "path";
881
+ var McpReadTransportError = class extends Error {
882
+ constructor(message, retryAfterMs) {
883
+ super(message);
884
+ this.retryAfterMs = retryAfterMs;
885
+ }
886
+ };
887
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
888
+ var digest = (bytes) => createHash4("sha256").update(bytes).digest("hex");
889
+ var exists = async (file) => fs4.lstat(file).catch((error) => {
890
+ if (error.code === "ENOENT") return void 0;
891
+ throw error;
892
+ });
893
+ var checkFile = async (file) => {
894
+ const stat = await exists(file);
895
+ if (stat && (!stat.isFile() || stat.isSymbolicLink()))
896
+ throw new Error("Invalid local bundle file");
897
+ return stat;
898
+ };
899
+ var downloadMcpBundle = async ({
900
+ descriptor,
901
+ root,
902
+ contractVersion,
903
+ tools,
904
+ readChunk,
905
+ wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
906
+ }) => {
907
+ assertMcpFeedbackContract(contractVersion, tools);
908
+ if (!/^[a-f0-9]{64}$/.test(descriptor.id))
909
+ throw new Error("Invalid artifact identity");
910
+ const required = ["payload", "manifest.json", ".complete"];
911
+ if (descriptor.kind === "runtime-feedback-replay")
912
+ required.push("replay.log");
913
+ for (const name of required) {
914
+ const matches = descriptor.files.filter((f) => f.file === name);
915
+ if (matches.length !== 1 || !matches[0].available || !Number.isSafeInteger(matches[0].bytes) || matches[0].bytes < 0 || !/^[a-f0-9]{64}$/.test(matches[0].sha256))
916
+ throw new Error(`Incomplete bundle descriptor: ${name}`);
917
+ }
918
+ const bundleDir = path4.join(root, descriptor.id);
919
+ await fs4.mkdir(bundleDir, { recursive: true });
920
+ const dir = await fs4.lstat(bundleDir);
921
+ if (dir.isSymbolicLink()) throw new Error("Invalid bundle directory");
922
+ const lock = path4.join(bundleDir, ".download-lock");
923
+ await fs4.mkdir(lock);
924
+ try {
925
+ for (const name of [
926
+ ...required.filter((f) => f !== ".complete"),
927
+ ".complete"
928
+ ]) {
929
+ const expected = descriptor.files.find((f) => f.file === name);
930
+ if (!expected) throw new Error("Missing bundle file");
931
+ const filename = name === "payload" ? descriptor.kind === "runtime-evidence" ? "runtime-evidence.json" : "replay-runtime-evidence.json" : name;
932
+ const target = path4.join(bundleDir, filename);
933
+ const verified = await checkFile(target);
934
+ if (verified) {
935
+ if (verified.size !== expected.bytes || digest(await fs4.readFile(target)) !== expected.sha256)
936
+ throw new Error("Local bundle checksum mismatch");
937
+ continue;
938
+ }
939
+ const partial = target + ".part";
940
+ const checkpoint = partial + ".json";
941
+ const identity = JSON.stringify({
942
+ artifactId: descriptor.id,
943
+ file: name,
944
+ bytes: expected.bytes,
945
+ sha256: expected.sha256
946
+ });
947
+ await checkFile(checkpoint);
948
+ const stat = await checkFile(partial);
949
+ if (stat) {
950
+ if (await fs4.readFile(checkpoint, "utf8") !== identity || stat.size > expected.bytes)
951
+ throw new Error("Artifact changed during download");
952
+ } else {
953
+ if (await checkFile(checkpoint)) {
954
+ if (await fs4.readFile(checkpoint, "utf8") !== identity)
955
+ throw new Error("Artifact changed during download");
956
+ } else await fs4.writeFile(checkpoint, identity, { flag: "wx" });
957
+ }
958
+ let offset = stat?.size ?? 0;
959
+ if (!stat) await fs4.writeFile(partial, Buffer.alloc(0), { flag: "wx" });
960
+ while (offset < expected.bytes) {
961
+ const request = {
962
+ artifactId: descriptor.id,
963
+ file: name,
964
+ offset,
965
+ length: MCP_FEEDBACK_CONTRACT.maximumChunkBytes
966
+ };
967
+ let response;
968
+ for (let attempt = 0; ; attempt++) {
969
+ try {
970
+ response = await readChunk(request);
971
+ break;
972
+ } catch (error) {
973
+ if (!(error instanceof McpReadTransportError) || attempt >= 3)
974
+ throw error;
975
+ await wait(
976
+ Math.max([2e3, 5e3, 1e4][attempt], error.retryAfterMs ?? 0)
977
+ );
978
+ }
979
+ }
980
+ if (!record(response) || response.id !== descriptor.id || response.file !== name || response.offset !== offset || response.bytes !== expected.bytes || response.sha256 !== expected.sha256 || response.encoding !== "base64" || typeof response.data !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
981
+ response.data
982
+ ))
983
+ throw new Error("Invalid artifact chunk identity");
984
+ const bytes = Buffer.from(response.data, "base64");
985
+ const next = offset + bytes.length;
986
+ if (!bytes.length || bytes.length > request.length || next > expected.bytes || response.nextOffset !== (next === expected.bytes ? null : next))
987
+ throw new Error("Invalid artifact chunk range");
988
+ await fs4.appendFile(partial, bytes);
989
+ offset = next;
990
+ }
991
+ if (digest(await fs4.readFile(partial)) !== expected.sha256)
992
+ throw new Error("Downloaded bundle checksum mismatch");
993
+ await fs4.rename(partial, target);
994
+ await fs4.unlink(checkpoint);
995
+ }
996
+ const bundle = descriptor.kind === "runtime-evidence" ? await verifyRuntimeEvidenceBundle(bundleDir) : await verifyRuntimeFeedbackReplayBundle(bundleDir);
997
+ return bundle;
998
+ } finally {
999
+ await fs4.rmdir(lock);
1000
+ }
1001
+ };
843
1002
  export {
1003
+ MCP_FEEDBACK_CONTRACT,
1004
+ McpReadTransportError,
844
1005
  RUNTIME_EVIDENCE_COMPLETE_FILE,
845
1006
  RUNTIME_EVIDENCE_MANIFEST_FILE,
846
1007
  RUNTIME_EVIDENCE_PAYLOAD_FILE,
@@ -850,8 +1011,10 @@ export {
850
1011
  RUNTIME_FEEDBACK_REPLAY_FILE,
851
1012
  activeRuntimeEvidenceStrategies,
852
1013
  assertCurrentRuntimeEvidenceArtifact,
1014
+ assertMcpFeedbackContract,
853
1015
  discoverRuntimeEvidenceBundles,
854
1016
  discoverRuntimeFeedbackReplayBundles,
1017
+ downloadMcpBundle,
855
1018
  parseRuntimeEvidenceDeploymentSnapshot,
856
1019
  parseRuntimeEvidenceProducer,
857
1020
  publishRuntimeEvidenceBundle,
package/dist/mcp.d.mts CHANGED
@@ -12,7 +12,7 @@ declare const redactMcpJobLog: (line: string) => string;
12
12
  /** Offsets refer to the retained worker tail, which may shift while running. */
13
13
  declare const readMcpJobLogs: (principal: McpPrincipal, id: string, offset?: number, limit?: number) => Promise<{
14
14
  jobId: string;
15
- status: "completed" | "failed" | "running" | "queued" | "cancelled";
15
+ status: "running" | "completed" | "failed" | "queued" | "waiting_resources" | "waiting_executor" | "cancelled";
16
16
  updatedAt: number;
17
17
  offset: number;
18
18
  nextOffset: number | null;
@@ -30,7 +30,7 @@ declare const publicMcpJob: (job: McpJob) => {
30
30
  userName: string;
31
31
  clientId: string;
32
32
  kind: "backtest" | "runtime-evidence" | "runtime-feedback-replay";
33
- status: "queued" | "running" | "completed" | "failed" | "cancelled";
33
+ status: "queued" | "waiting_resources" | "waiting_executor" | "running" | "completed" | "failed" | "cancelled";
34
34
  createdAt: number;
35
35
  updatedAt: number;
36
36
  startedAt?: number;
package/dist/mcp.d.ts CHANGED
@@ -12,7 +12,7 @@ declare const redactMcpJobLog: (line: string) => string;
12
12
  /** Offsets refer to the retained worker tail, which may shift while running. */
13
13
  declare const readMcpJobLogs: (principal: McpPrincipal, id: string, offset?: number, limit?: number) => Promise<{
14
14
  jobId: string;
15
- status: "completed" | "failed" | "running" | "queued" | "cancelled";
15
+ status: "running" | "completed" | "failed" | "queued" | "waiting_resources" | "waiting_executor" | "cancelled";
16
16
  updatedAt: number;
17
17
  offset: number;
18
18
  nextOffset: number | null;
@@ -30,7 +30,7 @@ declare const publicMcpJob: (job: McpJob) => {
30
30
  userName: string;
31
31
  clientId: string;
32
32
  kind: "backtest" | "runtime-evidence" | "runtime-feedback-replay";
33
- status: "queued" | "running" | "completed" | "failed" | "cancelled";
33
+ status: "queued" | "waiting_resources" | "waiting_executor" | "running" | "completed" | "failed" | "cancelled";
34
34
  createdAt: number;
35
35
  updatedAt: number;
36
36
  startedAt?: number;
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,
@@ -8912,7 +9041,10 @@ var cancelMcpJob = async (principal, id) => {
8912
9041
  const job = await readMcpJob(principal.userName, id);
8913
9042
  if (job.clientId !== principal.clientId)
8914
9043
  throw new Error("Only the submitting client can cancel this job");
8915
- if (!["queued", "running"].includes(job.status)) return job;
9044
+ if (!["queued", "waiting_resources", "waiting_executor", "running"].includes(
9045
+ job.status
9046
+ ))
9047
+ return job;
8916
9048
  const next = {
8917
9049
  ...job,
8918
9050
  cancelRequested: true,
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";
@@ -236,7 +236,10 @@ var cancelMcpJob = async (principal, id) => {
236
236
  const job = await readMcpJob(principal.userName, id);
237
237
  if (job.clientId !== principal.clientId)
238
238
  throw new Error("Only the submitting client can cancel this job");
239
- if (!["queued", "running"].includes(job.status)) return job;
239
+ if (!["queued", "waiting_resources", "waiting_executor", "running"].includes(
240
+ job.status
241
+ ))
242
+ return job;
240
243
  const next = {
241
244
  ...job,
242
245
  cancelRequested: true,
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,