@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.
@@ -1,5 +1,6 @@
1
1
  export * from '@tradejs/core/backtest';
2
- import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint } from '@tradejs/types';
2
+ import * as _tradejs_types from '@tradejs/types';
3
+ import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint, Signal, AiPayload, ResearchEntryPolicy } from '@tradejs/types';
3
4
 
4
5
  type TestingGroupResult = {
5
6
  test: Test;
@@ -32,4 +33,14 @@ declare const resolveExecutionCosts: (params: {
32
33
  fundingRates: FundingRatePoint[];
33
34
  }>;
34
35
 
35
- export { canRunTestsInSharedCandleLoop, createTestConnector, releaseTestingSymbolCache, resetTestingKlineCache, resolveExecutionCosts, testing, testingGroupInSharedCandleLoop };
36
+ declare const RESEARCH_ENTRY_POLICY_CAPABILITIES: {
37
+ readonly directionSelection: true;
38
+ };
39
+ /** Preserve the canonical signal-time snapshot even when an AI adapter emits
40
+ * only its strategy-specific fields. Both discovery and execution use this. */
41
+ declare const buildResearchEntryPayload: (signal: Signal, payload?: AiPayload) => AiPayload;
42
+ declare const parseResearchEntryPolicy: (value: unknown) => ResearchEntryPolicy;
43
+ declare const matchesResearchEntryRules: (payload: AiPayload, predicates: NonNullable<ResearchEntryPolicy["rules"]["LONG"]>) => boolean;
44
+ declare const evaluateResearchEntryPolicy: (policy: ResearchEntryPolicy, signal: Signal, onDecision?: (decision: _tradejs_types.ResearchEntryDecision) => void) => Promise<boolean>;
45
+
46
+ export { RESEARCH_ENTRY_POLICY_CAPABILITIES, buildResearchEntryPayload, canRunTestsInSharedCandleLoop, createTestConnector, evaluateResearchEntryPolicy, matchesResearchEntryRules, parseResearchEntryPolicy, releaseTestingSymbolCache, resetTestingKlineCache, resolveExecutionCosts, testing, testingGroupInSharedCandleLoop };
@@ -1,5 +1,6 @@
1
1
  export * from '@tradejs/core/backtest';
2
- import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint } from '@tradejs/types';
2
+ import * as _tradejs_types from '@tradejs/types';
3
+ import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint, Signal, AiPayload, ResearchEntryPolicy } from '@tradejs/types';
3
4
 
4
5
  type TestingGroupResult = {
5
6
  test: Test;
@@ -32,4 +33,14 @@ declare const resolveExecutionCosts: (params: {
32
33
  fundingRates: FundingRatePoint[];
33
34
  }>;
34
35
 
35
- export { canRunTestsInSharedCandleLoop, createTestConnector, releaseTestingSymbolCache, resetTestingKlineCache, resolveExecutionCosts, testing, testingGroupInSharedCandleLoop };
36
+ declare const RESEARCH_ENTRY_POLICY_CAPABILITIES: {
37
+ readonly directionSelection: true;
38
+ };
39
+ /** Preserve the canonical signal-time snapshot even when an AI adapter emits
40
+ * only its strategy-specific fields. Both discovery and execution use this. */
41
+ declare const buildResearchEntryPayload: (signal: Signal, payload?: AiPayload) => AiPayload;
42
+ declare const parseResearchEntryPolicy: (value: unknown) => ResearchEntryPolicy;
43
+ declare const matchesResearchEntryRules: (payload: AiPayload, predicates: NonNullable<ResearchEntryPolicy["rules"]["LONG"]>) => boolean;
44
+ declare const evaluateResearchEntryPolicy: (policy: ResearchEntryPolicy, signal: Signal, onDecision?: (decision: _tradejs_types.ResearchEntryDecision) => void) => Promise<boolean>;
45
+
46
+ export { RESEARCH_ENTRY_POLICY_CAPABILITIES, buildResearchEntryPayload, canRunTestsInSharedCandleLoop, createTestConnector, evaluateResearchEntryPolicy, matchesResearchEntryRules, parseResearchEntryPolicy, releaseTestingSymbolCache, resetTestingKlineCache, resolveExecutionCosts, testing, testingGroupInSharedCandleLoop };
package/dist/backtest.js CHANGED
@@ -778,8 +778,13 @@ var init_jevTraining = __esm({
778
778
  // src/backtest.ts
779
779
  var backtest_exports = {};
780
780
  __export(backtest_exports, {
781
+ RESEARCH_ENTRY_POLICY_CAPABILITIES: () => RESEARCH_ENTRY_POLICY_CAPABILITIES,
782
+ buildResearchEntryPayload: () => buildResearchEntryPayload,
781
783
  canRunTestsInSharedCandleLoop: () => canRunTestsInSharedCandleLoop,
782
784
  createTestConnector: () => createTestConnector,
785
+ evaluateResearchEntryPolicy: () => evaluateResearchEntryPolicy,
786
+ matchesResearchEntryRules: () => matchesResearchEntryRules,
787
+ parseResearchEntryPolicy: () => parseResearchEntryPolicy,
783
788
  releaseTestingSymbolCache: () => releaseTestingSymbolCache,
784
789
  resetTestingKlineCache: () => resetTestingKlineCache,
785
790
  resolveExecutionCosts: () => resolveExecutionCosts,
@@ -2958,6 +2963,7 @@ var recordRuntimeTradeIncrease = async (params) => {
2958
2963
  const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
2959
2964
  const next = {
2960
2965
  ...existing,
2966
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
2961
2967
  qty: resultingQty,
2962
2968
  entryPrice: resultingEntryPrice,
2963
2969
  entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
@@ -5686,6 +5692,7 @@ var executeEntryOrder = async ({
5686
5692
  openFee: estimatedOpenFee,
5687
5693
  totalFee: estimatedOpenFee,
5688
5694
  entryTimestamp: timestamp,
5695
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5689
5696
  universe: signal.universe,
5690
5697
  assetClass: signal.assetClass,
5691
5698
  accountId: signal.accountId,
@@ -5709,6 +5716,7 @@ var executeEntryOrder = async ({
5709
5716
  addedQty: filledQty,
5710
5717
  addedEntryPrice: fillPrice,
5711
5718
  entryTimestamp: timestamp,
5719
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
5712
5720
  fee: estimatedOpenFee,
5713
5721
  accountId: signal.accountId,
5714
5722
  deploymentId: signal.deploymentId
@@ -6175,6 +6183,115 @@ var buildAiHookContext = ({
6175
6183
  };
6176
6184
  };
6177
6185
 
6186
+ // src/researchEntryPolicy.ts
6187
+ var RESEARCH_ENTRY_POLICY_CAPABILITIES = {
6188
+ directionSelection: true
6189
+ };
6190
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
6191
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
6192
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
6193
+ const raw = signal.additionalIndicators?.baseContext;
6194
+ const adapted = additional.baseContext;
6195
+ return {
6196
+ ...payload,
6197
+ additionalIndicators: {
6198
+ ...additional,
6199
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
6200
+ baseContext: {
6201
+ ...raw && typeof raw === "object" ? raw : {},
6202
+ ...adapted && typeof adapted === "object" ? adapted : {}
6203
+ }
6204
+ }
6205
+ };
6206
+ };
6207
+ var parseResearchEntryPolicy = (value) => {
6208
+ if (!value || typeof value !== "object" || Array.isArray(value))
6209
+ throw new Error("Invalid research entry policy");
6210
+ const policy = value;
6211
+ 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")
6212
+ throw new Error("Invalid research entry policy contract");
6213
+ 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))))
6214
+ throw new Error("Invalid research policy directions");
6215
+ for (const side of ["LONG", "SHORT"]) {
6216
+ const predicates = policy.rules[side];
6217
+ if (predicates === null) continue;
6218
+ if (!Array.isArray(predicates) || predicates.length > 4)
6219
+ throw new Error(
6220
+ "Research policy needs bounded rules for both directions"
6221
+ );
6222
+ for (const predicate of predicates) {
6223
+ 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)))
6224
+ throw new Error(
6225
+ "Research policy contains an unsupported or non-causal predicate"
6226
+ );
6227
+ }
6228
+ }
6229
+ return policy;
6230
+ };
6231
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
6232
+ const value = predicate.path.split(".").reduce(
6233
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6234
+ payload
6235
+ );
6236
+ if (value == null) return false;
6237
+ if (predicate.op === "==") return value === predicate.value;
6238
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6239
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
6240
+ });
6241
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
6242
+ if (signal.strategy !== policy.strategy)
6243
+ throw new Error("Research policy strategy mismatch");
6244
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
6245
+ onDecision?.({
6246
+ mode: policy.mode,
6247
+ allowed: false,
6248
+ reason: "DIRECTION_BLOCKED"
6249
+ });
6250
+ return false;
6251
+ }
6252
+ if (policy.mode === "current-gate") {
6253
+ const analysis = await runAiPromptLocal(signal);
6254
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
6255
+ onDecision?.({
6256
+ mode: policy.mode,
6257
+ allowed: allowed2,
6258
+ quality: analysis.quality ?? null,
6259
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
6260
+ });
6261
+ return allowed2;
6262
+ }
6263
+ const original = buildAiPayload(signal);
6264
+ const gateContext = getDeterministicAiGateContext(original);
6265
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
6266
+ onDecision?.({
6267
+ mode: policy.mode,
6268
+ allowed: false,
6269
+ reason: "STRUCTURAL_HARD_BLOCK"
6270
+ });
6271
+ return false;
6272
+ }
6273
+ const rules = policy.rules[signal.direction];
6274
+ const payload = buildResearchEntryPayload(signal, original);
6275
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
6276
+ onDecision?.({
6277
+ mode: policy.mode,
6278
+ allowed,
6279
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
6280
+ predicates: rules?.map((rule) => {
6281
+ const value = rule.path.split(".").reduce(
6282
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
6283
+ payload
6284
+ );
6285
+ return {
6286
+ path: rule.path,
6287
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
6288
+ passed: matchesResearchEntryRules(payload, [rule])
6289
+ };
6290
+ })
6291
+ });
6292
+ return allowed;
6293
+ };
6294
+
6178
6295
  // src/strategy/runtimeDecisionTelemetry.ts
6179
6296
  var finitePositive = (value) => {
6180
6297
  const parsed = Number(value);
@@ -6713,6 +6830,12 @@ var createStrategyRuntime = ({
6713
6830
  const projectConfig = await loadTradejsConfig(projectRoot);
6714
6831
  const projectHooks = projectConfig.hooks;
6715
6832
  const env = String(config.ENV ?? "BACKTEST");
6833
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
6834
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
6835
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
6836
+ throw new Error(
6837
+ "Research entry policies require a BACKTEST simulation connector"
6838
+ );
6716
6839
  const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6717
6840
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6718
6841
  throw new Error(
@@ -7423,7 +7546,8 @@ var createStrategyRuntime = ({
7423
7546
  await enrichSignalWithMarketContextStages({
7424
7547
  signal,
7425
7548
  env,
7426
- includeHyperliquidWhales: false
7549
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
7550
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
7427
7551
  });
7428
7552
  if (jev) {
7429
7553
  await assessSignalWithJev({
@@ -7521,6 +7645,22 @@ var createStrategyRuntime = ({
7521
7645
  }
7522
7646
  return signal ?? decision.code;
7523
7647
  }
7648
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
7649
+ researchEntryPolicy,
7650
+ signal,
7651
+ (decision2) => {
7652
+ signal.additionalIndicators = {
7653
+ ...signal.additionalIndicators,
7654
+ researchEntryDecision: decision2
7655
+ };
7656
+ }
7657
+ ))) {
7658
+ if (signal) {
7659
+ signal.orderStatus = "skipped";
7660
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
7661
+ }
7662
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
7663
+ }
7524
7664
  const entryGate = await invokeGateHooks(
7525
7665
  "beforeEntryGate",
7526
7666
  decisionManifest?.hooks?.beforeEntryGate,
@@ -8251,7 +8391,7 @@ var createBacktestProgress = ({
8251
8391
 
8252
8392
  // src/backtest/session.ts
8253
8393
  var import_types2 = require("@tradejs/types");
8254
- var import_ai3 = require("@tradejs/infra/ai");
8394
+ var import_ai4 = require("@tradejs/infra/ai");
8255
8395
  var import_coreResearch = require("@tradejs/infra/coreResearch");
8256
8396
  var import_ml3 = require("@tradejs/infra/ml");
8257
8397
 
@@ -9555,7 +9695,8 @@ var createBacktestSession = async ({
9555
9695
  }
9556
9696
  if (test.researchTrace && signal && typeof signal !== "string" && signal.signalId) {
9557
9697
  const identity = resolveCoreResearchSetupIdentity(signal);
9558
- pendingResearchSetupBySignalId.set(signal.signalId, identity);
9698
+ if (signal.orderStatus !== "failed" && signal.orderStatus !== "skipped")
9699
+ pendingResearchSetupBySignalId.set(signal.signalId, identity);
9559
9700
  await (0, import_coreResearch.appendCoreResearchTraceEvent)({
9560
9701
  strategyName: test.strategyName,
9561
9702
  chunkId,
@@ -9570,7 +9711,11 @@ var createBacktestSession = async ({
9570
9711
  configId: test.configId,
9571
9712
  backtestRunId: test.backtestRunId,
9572
9713
  backtestTestKey: test.backtestTestKey,
9573
- ...identity
9714
+ ...identity,
9715
+ ...test.strategyConfig.RESEARCH_ENTRY_POLICY ? {
9716
+ reason: signal.orderSkipReason,
9717
+ researchEntryDecision: signal.additionalIndicators?.researchEntryDecision
9718
+ } : {}
9574
9719
  }
9575
9720
  });
9576
9721
  const traceEvent = signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted";
@@ -9598,7 +9743,7 @@ var createBacktestSession = async ({
9598
9743
  })
9599
9744
  );
9600
9745
  }
9601
- if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(signal.assessment && signal.orderStatus === "skipped")) {
9746
+ if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(test.strategyConfig.RESEARCH_ENTRY_POLICY && (signal.orderStatus === "skipped" || signal.orderStatus === "failed")) && !(signal.assessment && signal.orderStatus === "skipped")) {
9602
9747
  pendingAiRowBySignalId.set(signal.signalId, {
9603
9748
  signalId: signal.signalId,
9604
9749
  strategyName: signal.strategy || test.strategyName,
@@ -9639,7 +9784,7 @@ var createBacktestSession = async ({
9639
9784
  if (aiRowBase) {
9640
9785
  pendingAiRowBySignalId.delete(resultRecord.signalId);
9641
9786
  const { signal: aiSignal, ...rowBase } = aiRowBase;
9642
- await (0, import_ai3.appendAiDatasetRow)({
9787
+ await (0, import_ai4.appendAiDatasetRow)({
9643
9788
  strategyName: test.strategyName,
9644
9789
  chunkId,
9645
9790
  row: {
@@ -9649,7 +9794,14 @@ var createBacktestSession = async ({
9649
9794
  tradeResult: resultRecord.tradeResult,
9650
9795
  research: {
9651
9796
  schema: "tradejs-core-research-row/v1",
9652
- ...resolveCoreResearchSetupIdentity(aiSignal)
9797
+ ...resolveCoreResearchSetupIdentity(aiSignal),
9798
+ ...test.researchTrace ? {
9799
+ decisionContext: {
9800
+ schema: "tradejs-research-decision-context/v1",
9801
+ knownAt: aiSignal.timestamp,
9802
+ baseContext: aiSignal.additionalIndicators?.baseContext ?? null
9803
+ }
9804
+ } : {}
9653
9805
  }
9654
9806
  }
9655
9807
  });
@@ -10522,8 +10674,13 @@ var testingGroupInSharedCandleLoop = async (tests) => {
10522
10674
  };
10523
10675
  // Annotate the CommonJS export names for ESM import in node:
10524
10676
  0 && (module.exports = {
10677
+ RESEARCH_ENTRY_POLICY_CAPABILITIES,
10678
+ buildResearchEntryPayload,
10525
10679
  canRunTestsInSharedCandleLoop,
10526
10680
  createTestConnector,
10681
+ evaluateResearchEntryPolicy,
10682
+ matchesResearchEntryRules,
10683
+ parseResearchEntryPolicy,
10527
10684
  releaseTestingSymbolCache,
10528
10685
  resetTestingKlineCache,
10529
10686
  resolveExecutionCosts,
package/dist/backtest.mjs CHANGED
@@ -3,9 +3,14 @@ import {
3
3
  getConnectorCreatorByName
4
4
  } from "./chunk-QVKUROR2.mjs";
5
5
  import {
6
+ RESEARCH_ENTRY_POLICY_CAPABILITIES,
6
7
  buildMlPayload,
7
- enrichSignalWithMarketContextStages
8
- } from "./chunk-SKY2HNZU.mjs";
8
+ buildResearchEntryPayload,
9
+ enrichSignalWithMarketContextStages,
10
+ evaluateResearchEntryPolicy,
11
+ matchesResearchEntryRules,
12
+ parseResearchEntryPolicy
13
+ } from "./chunk-KOV3U6ZZ.mjs";
9
14
  import {
10
15
  buildAiPayload,
11
16
  getStrategyCreator
@@ -1468,7 +1473,8 @@ var createBacktestSession = async ({
1468
1473
  }
1469
1474
  if (test.researchTrace && signal && typeof signal !== "string" && signal.signalId) {
1470
1475
  const identity = resolveCoreResearchSetupIdentity(signal);
1471
- pendingResearchSetupBySignalId.set(signal.signalId, identity);
1476
+ if (signal.orderStatus !== "failed" && signal.orderStatus !== "skipped")
1477
+ pendingResearchSetupBySignalId.set(signal.signalId, identity);
1472
1478
  await appendCoreResearchTraceEvent({
1473
1479
  strategyName: test.strategyName,
1474
1480
  chunkId,
@@ -1483,7 +1489,11 @@ var createBacktestSession = async ({
1483
1489
  configId: test.configId,
1484
1490
  backtestRunId: test.backtestRunId,
1485
1491
  backtestTestKey: test.backtestTestKey,
1486
- ...identity
1492
+ ...identity,
1493
+ ...test.strategyConfig.RESEARCH_ENTRY_POLICY ? {
1494
+ reason: signal.orderSkipReason,
1495
+ researchEntryDecision: signal.additionalIndicators?.researchEntryDecision
1496
+ } : {}
1487
1497
  }
1488
1498
  });
1489
1499
  const traceEvent = signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted";
@@ -1511,7 +1521,7 @@ var createBacktestSession = async ({
1511
1521
  })
1512
1522
  );
1513
1523
  }
1514
- if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(signal.assessment && signal.orderStatus === "skipped")) {
1524
+ if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(test.strategyConfig.RESEARCH_ENTRY_POLICY && (signal.orderStatus === "skipped" || signal.orderStatus === "failed")) && !(signal.assessment && signal.orderStatus === "skipped")) {
1515
1525
  pendingAiRowBySignalId.set(signal.signalId, {
1516
1526
  signalId: signal.signalId,
1517
1527
  strategyName: signal.strategy || test.strategyName,
@@ -1562,7 +1572,14 @@ var createBacktestSession = async ({
1562
1572
  tradeResult: resultRecord.tradeResult,
1563
1573
  research: {
1564
1574
  schema: "tradejs-core-research-row/v1",
1565
- ...resolveCoreResearchSetupIdentity(aiSignal)
1575
+ ...resolveCoreResearchSetupIdentity(aiSignal),
1576
+ ...test.researchTrace ? {
1577
+ decisionContext: {
1578
+ schema: "tradejs-research-decision-context/v1",
1579
+ knownAt: aiSignal.timestamp,
1580
+ baseContext: aiSignal.additionalIndicators?.baseContext ?? null
1581
+ }
1582
+ } : {}
1566
1583
  }
1567
1584
  }
1568
1585
  });
@@ -2434,8 +2451,13 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2434
2451
  }
2435
2452
  };
2436
2453
  export {
2454
+ RESEARCH_ENTRY_POLICY_CAPABILITIES,
2455
+ buildResearchEntryPayload,
2437
2456
  canRunTestsInSharedCandleLoop,
2438
2457
  createTestConnector,
2458
+ evaluateResearchEntryPolicy,
2459
+ matchesResearchEntryRules,
2460
+ parseResearchEntryPolicy,
2439
2461
  releaseTestingSymbolCache,
2440
2462
  resetTestingKlineCache,
2441
2463
  resolveExecutionCosts,
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  askAI,
3
+ buildAiPayload,
4
+ getDeterministicAiGateContext,
3
5
  getStrategyManifest,
4
6
  getStrategyProfileMlAdapter,
5
7
  resolveStrategyPolicyProfile,
@@ -2181,6 +2183,7 @@ var recordRuntimeTradeIncrease = async (params) => {
2181
2183
  const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
2182
2184
  const next = {
2183
2185
  ...existing,
2186
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
2184
2187
  qty: resultingQty,
2185
2188
  entryPrice: resultingEntryPrice,
2186
2189
  entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
@@ -3099,6 +3102,7 @@ var executeEntryOrder = async ({
3099
3102
  openFee: estimatedOpenFee,
3100
3103
  totalFee: estimatedOpenFee,
3101
3104
  entryTimestamp: timestamp,
3105
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
3102
3106
  universe: signal.universe,
3103
3107
  assetClass: signal.assetClass,
3104
3108
  accountId: signal.accountId,
@@ -3122,6 +3126,7 @@ var executeEntryOrder = async ({
3122
3126
  addedQty: filledQty,
3123
3127
  addedEntryPrice: fillPrice,
3124
3128
  entryTimestamp: timestamp,
3129
+ ...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
3125
3130
  fee: estimatedOpenFee,
3126
3131
  accountId: signal.accountId,
3127
3132
  deploymentId: signal.deploymentId
@@ -3179,6 +3184,115 @@ var resolveStrategyConfig = async ({
3179
3184
  return { config, isConfigFromBacktest };
3180
3185
  };
3181
3186
 
3187
+ // src/researchEntryPolicy.ts
3188
+ var RESEARCH_ENTRY_POLICY_CAPABILITIES = {
3189
+ directionSelection: true
3190
+ };
3191
+ var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
3192
+ var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
3193
+ const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
3194
+ const raw = signal.additionalIndicators?.baseContext;
3195
+ const adapted = additional.baseContext;
3196
+ return {
3197
+ ...payload,
3198
+ additionalIndicators: {
3199
+ ...additional,
3200
+ ...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
3201
+ baseContext: {
3202
+ ...raw && typeof raw === "object" ? raw : {},
3203
+ ...adapted && typeof adapted === "object" ? adapted : {}
3204
+ }
3205
+ }
3206
+ };
3207
+ };
3208
+ var parseResearchEntryPolicy = (value) => {
3209
+ if (!value || typeof value !== "object" || Array.isArray(value))
3210
+ throw new Error("Invalid research entry policy");
3211
+ const policy = value;
3212
+ 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")
3213
+ throw new Error("Invalid research entry policy contract");
3214
+ 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))))
3215
+ throw new Error("Invalid research policy directions");
3216
+ for (const side of ["LONG", "SHORT"]) {
3217
+ const predicates = policy.rules[side];
3218
+ if (predicates === null) continue;
3219
+ if (!Array.isArray(predicates) || predicates.length > 4)
3220
+ throw new Error(
3221
+ "Research policy needs bounded rules for both directions"
3222
+ );
3223
+ for (const predicate of predicates) {
3224
+ 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)))
3225
+ throw new Error(
3226
+ "Research policy contains an unsupported or non-causal predicate"
3227
+ );
3228
+ }
3229
+ }
3230
+ return policy;
3231
+ };
3232
+ var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
3233
+ const value = predicate.path.split(".").reduce(
3234
+ (current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
3235
+ payload
3236
+ );
3237
+ if (value == null) return false;
3238
+ if (predicate.op === "==") return value === predicate.value;
3239
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
3240
+ return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
3241
+ });
3242
+ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
3243
+ if (signal.strategy !== policy.strategy)
3244
+ throw new Error("Research policy strategy mismatch");
3245
+ if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
3246
+ onDecision?.({
3247
+ mode: policy.mode,
3248
+ allowed: false,
3249
+ reason: "DIRECTION_BLOCKED"
3250
+ });
3251
+ return false;
3252
+ }
3253
+ if (policy.mode === "current-gate") {
3254
+ const analysis = await runAiPromptLocal(signal);
3255
+ const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
3256
+ onDecision?.({
3257
+ mode: policy.mode,
3258
+ allowed: allowed2,
3259
+ quality: analysis.quality ?? null,
3260
+ reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
3261
+ });
3262
+ return allowed2;
3263
+ }
3264
+ const original = buildAiPayload(signal);
3265
+ const gateContext = getDeterministicAiGateContext(original);
3266
+ if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
3267
+ onDecision?.({
3268
+ mode: policy.mode,
3269
+ allowed: false,
3270
+ reason: "STRUCTURAL_HARD_BLOCK"
3271
+ });
3272
+ return false;
3273
+ }
3274
+ const rules = policy.rules[signal.direction];
3275
+ const payload = buildResearchEntryPayload(signal, original);
3276
+ const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
3277
+ onDecision?.({
3278
+ mode: policy.mode,
3279
+ allowed,
3280
+ reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
3281
+ predicates: rules?.map((rule) => {
3282
+ const value = rule.path.split(".").reduce(
3283
+ (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
3284
+ payload
3285
+ );
3286
+ return {
3287
+ path: rule.path,
3288
+ actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
3289
+ passed: matchesResearchEntryRules(payload, [rule])
3290
+ };
3291
+ })
3292
+ });
3293
+ return allowed;
3294
+ };
3295
+
3182
3296
  // src/strategyRuntime.ts
3183
3297
  import { parseJevConfig } from "@tradejs/core/jev";
3184
3298
  import {
@@ -4145,6 +4259,12 @@ var createStrategyRuntime = ({
4145
4259
  const projectConfig = await loadTradejsConfig(projectRoot);
4146
4260
  const projectHooks = projectConfig.hooks;
4147
4261
  const env = String(config.ENV ?? "BACKTEST");
4262
+ const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
4263
+ const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
4264
+ if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
4265
+ throw new Error(
4266
+ "Research entry policies require a BACKTEST simulation connector"
4267
+ );
4148
4268
  const jev = parseJevConfig(config.JEV);
4149
4269
  if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
4150
4270
  throw new Error(
@@ -4855,7 +4975,8 @@ var createStrategyRuntime = ({
4855
4975
  await enrichSignalWithMarketContextStages({
4856
4976
  signal,
4857
4977
  env,
4858
- includeHyperliquidWhales: false
4978
+ includeHyperliquidWhales: Boolean(researchEntryPolicy),
4979
+ ...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
4859
4980
  });
4860
4981
  if (jev) {
4861
4982
  await assessSignalWithJev({
@@ -4953,6 +5074,22 @@ var createStrategyRuntime = ({
4953
5074
  }
4954
5075
  return signal ?? decision.code;
4955
5076
  }
5077
+ if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
5078
+ researchEntryPolicy,
5079
+ signal,
5080
+ (decision2) => {
5081
+ signal.additionalIndicators = {
5082
+ ...signal.additionalIndicators,
5083
+ researchEntryDecision: decision2
5084
+ };
5085
+ }
5086
+ ))) {
5087
+ if (signal) {
5088
+ signal.orderStatus = "skipped";
5089
+ signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
5090
+ }
5091
+ return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
5092
+ }
4956
5093
  const entryGate = await invokeGateHooks(
4957
5094
  "beforeEntryGate",
4958
5095
  decisionManifest?.hooks?.beforeEntryGate,
@@ -5088,5 +5225,10 @@ export {
5088
5225
  validateEntryProtectionAtArrival,
5089
5226
  executeEntryOrder,
5090
5227
  resolveStrategyConfig,
5228
+ RESEARCH_ENTRY_POLICY_CAPABILITIES,
5229
+ buildResearchEntryPayload,
5230
+ parseResearchEntryPolicy,
5231
+ matchesResearchEntryRules,
5232
+ evaluateResearchEntryPolicy,
5091
5233
  createStrategyRuntime
5092
5234
  };