@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.
- package/dist/backtest.d.mts +13 -2
- package/dist/backtest.d.ts +13 -2
- package/dist/backtest.js +161 -7
- package/dist/backtest.mjs +28 -6
- package/dist/{chunk-SKY2HNZU.mjs → chunk-K56EW2BB.mjs} +140 -1
- package/dist/diagnostics.d.mts +54 -1
- package/dist/diagnostics.d.ts +54 -1
- package/dist/diagnostics.js +329 -33
- package/dist/diagnostics.mjs +168 -5
- package/dist/mcp.d.mts +2 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +134 -2
- package/dist/mcp.mjs +5 -2
- package/dist/registry.js +130 -1
- package/dist/registry.mjs +1 -1
- package/dist/runtimeDashboard.js +130 -1
- package/dist/runtimeDashboard.mjs +1 -1
- package/dist/runtimeStrategies.js +130 -1
- package/dist/runtimeStrategies.mjs +1 -1
- package/dist/strategies.js +130 -1
- package/dist/strategies.mjs +1 -1
- package/package.json +5 -5
package/dist/backtest.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from '@tradejs/core/backtest';
|
|
2
|
-
import
|
|
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
|
-
|
|
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.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from '@tradejs/core/backtest';
|
|
2
|
-
import
|
|
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
|
-
|
|
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,
|
|
@@ -6175,6 +6180,115 @@ var buildAiHookContext = ({
|
|
|
6175
6180
|
};
|
|
6176
6181
|
};
|
|
6177
6182
|
|
|
6183
|
+
// src/researchEntryPolicy.ts
|
|
6184
|
+
var RESEARCH_ENTRY_POLICY_CAPABILITIES = {
|
|
6185
|
+
directionSelection: true
|
|
6186
|
+
};
|
|
6187
|
+
var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
|
|
6188
|
+
var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
|
|
6189
|
+
const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
|
|
6190
|
+
const raw = signal.additionalIndicators?.baseContext;
|
|
6191
|
+
const adapted = additional.baseContext;
|
|
6192
|
+
return {
|
|
6193
|
+
...payload,
|
|
6194
|
+
additionalIndicators: {
|
|
6195
|
+
...additional,
|
|
6196
|
+
...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
|
|
6197
|
+
baseContext: {
|
|
6198
|
+
...raw && typeof raw === "object" ? raw : {},
|
|
6199
|
+
...adapted && typeof adapted === "object" ? adapted : {}
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
};
|
|
6203
|
+
};
|
|
6204
|
+
var parseResearchEntryPolicy = (value) => {
|
|
6205
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6206
|
+
throw new Error("Invalid research entry policy");
|
|
6207
|
+
const policy = value;
|
|
6208
|
+
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")
|
|
6209
|
+
throw new Error("Invalid research entry policy contract");
|
|
6210
|
+
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))))
|
|
6211
|
+
throw new Error("Invalid research policy directions");
|
|
6212
|
+
for (const side of ["LONG", "SHORT"]) {
|
|
6213
|
+
const predicates = policy.rules[side];
|
|
6214
|
+
if (predicates === null) continue;
|
|
6215
|
+
if (!Array.isArray(predicates) || predicates.length > 4)
|
|
6216
|
+
throw new Error(
|
|
6217
|
+
"Research policy needs bounded rules for both directions"
|
|
6218
|
+
);
|
|
6219
|
+
for (const predicate of predicates) {
|
|
6220
|
+
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)))
|
|
6221
|
+
throw new Error(
|
|
6222
|
+
"Research policy contains an unsupported or non-causal predicate"
|
|
6223
|
+
);
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
return policy;
|
|
6227
|
+
};
|
|
6228
|
+
var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
|
|
6229
|
+
const value = predicate.path.split(".").reduce(
|
|
6230
|
+
(current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
6231
|
+
payload
|
|
6232
|
+
);
|
|
6233
|
+
if (value == null) return false;
|
|
6234
|
+
if (predicate.op === "==") return value === predicate.value;
|
|
6235
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return false;
|
|
6236
|
+
return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
|
|
6237
|
+
});
|
|
6238
|
+
var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
|
|
6239
|
+
if (signal.strategy !== policy.strategy)
|
|
6240
|
+
throw new Error("Research policy strategy mismatch");
|
|
6241
|
+
if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
|
|
6242
|
+
onDecision?.({
|
|
6243
|
+
mode: policy.mode,
|
|
6244
|
+
allowed: false,
|
|
6245
|
+
reason: "DIRECTION_BLOCKED"
|
|
6246
|
+
});
|
|
6247
|
+
return false;
|
|
6248
|
+
}
|
|
6249
|
+
if (policy.mode === "current-gate") {
|
|
6250
|
+
const analysis = await runAiPromptLocal(signal);
|
|
6251
|
+
const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
|
|
6252
|
+
onDecision?.({
|
|
6253
|
+
mode: policy.mode,
|
|
6254
|
+
allowed: allowed2,
|
|
6255
|
+
quality: analysis.quality ?? null,
|
|
6256
|
+
reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
|
|
6257
|
+
});
|
|
6258
|
+
return allowed2;
|
|
6259
|
+
}
|
|
6260
|
+
const original = buildAiPayload(signal);
|
|
6261
|
+
const gateContext = getDeterministicAiGateContext(original);
|
|
6262
|
+
if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
|
|
6263
|
+
onDecision?.({
|
|
6264
|
+
mode: policy.mode,
|
|
6265
|
+
allowed: false,
|
|
6266
|
+
reason: "STRUCTURAL_HARD_BLOCK"
|
|
6267
|
+
});
|
|
6268
|
+
return false;
|
|
6269
|
+
}
|
|
6270
|
+
const rules = policy.rules[signal.direction];
|
|
6271
|
+
const payload = buildResearchEntryPayload(signal, original);
|
|
6272
|
+
const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
|
|
6273
|
+
onDecision?.({
|
|
6274
|
+
mode: policy.mode,
|
|
6275
|
+
allowed,
|
|
6276
|
+
reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
|
|
6277
|
+
predicates: rules?.map((rule) => {
|
|
6278
|
+
const value = rule.path.split(".").reduce(
|
|
6279
|
+
(current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
6280
|
+
payload
|
|
6281
|
+
);
|
|
6282
|
+
return {
|
|
6283
|
+
path: rule.path,
|
|
6284
|
+
actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
|
|
6285
|
+
passed: matchesResearchEntryRules(payload, [rule])
|
|
6286
|
+
};
|
|
6287
|
+
})
|
|
6288
|
+
});
|
|
6289
|
+
return allowed;
|
|
6290
|
+
};
|
|
6291
|
+
|
|
6178
6292
|
// src/strategy/runtimeDecisionTelemetry.ts
|
|
6179
6293
|
var finitePositive = (value) => {
|
|
6180
6294
|
const parsed = Number(value);
|
|
@@ -6713,6 +6827,12 @@ var createStrategyRuntime = ({
|
|
|
6713
6827
|
const projectConfig = await loadTradejsConfig(projectRoot);
|
|
6714
6828
|
const projectHooks = projectConfig.hooks;
|
|
6715
6829
|
const env = String(config.ENV ?? "BACKTEST");
|
|
6830
|
+
const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
|
|
6831
|
+
const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
|
|
6832
|
+
if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
|
|
6833
|
+
throw new Error(
|
|
6834
|
+
"Research entry policies require a BACKTEST simulation connector"
|
|
6835
|
+
);
|
|
6716
6836
|
const jev = (0, import_jev6.parseJevConfig)(config.JEV);
|
|
6717
6837
|
if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
|
|
6718
6838
|
throw new Error(
|
|
@@ -7423,7 +7543,8 @@ var createStrategyRuntime = ({
|
|
|
7423
7543
|
await enrichSignalWithMarketContextStages({
|
|
7424
7544
|
signal,
|
|
7425
7545
|
env,
|
|
7426
|
-
includeHyperliquidWhales:
|
|
7546
|
+
includeHyperliquidWhales: Boolean(researchEntryPolicy),
|
|
7547
|
+
...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
|
|
7427
7548
|
});
|
|
7428
7549
|
if (jev) {
|
|
7429
7550
|
await assessSignalWithJev({
|
|
@@ -7521,6 +7642,22 @@ var createStrategyRuntime = ({
|
|
|
7521
7642
|
}
|
|
7522
7643
|
return signal ?? decision.code;
|
|
7523
7644
|
}
|
|
7645
|
+
if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
|
|
7646
|
+
researchEntryPolicy,
|
|
7647
|
+
signal,
|
|
7648
|
+
(decision2) => {
|
|
7649
|
+
signal.additionalIndicators = {
|
|
7650
|
+
...signal.additionalIndicators,
|
|
7651
|
+
researchEntryDecision: decision2
|
|
7652
|
+
};
|
|
7653
|
+
}
|
|
7654
|
+
))) {
|
|
7655
|
+
if (signal) {
|
|
7656
|
+
signal.orderStatus = "skipped";
|
|
7657
|
+
signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
7658
|
+
}
|
|
7659
|
+
return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
7660
|
+
}
|
|
7524
7661
|
const entryGate = await invokeGateHooks(
|
|
7525
7662
|
"beforeEntryGate",
|
|
7526
7663
|
decisionManifest?.hooks?.beforeEntryGate,
|
|
@@ -8251,7 +8388,7 @@ var createBacktestProgress = ({
|
|
|
8251
8388
|
|
|
8252
8389
|
// src/backtest/session.ts
|
|
8253
8390
|
var import_types2 = require("@tradejs/types");
|
|
8254
|
-
var
|
|
8391
|
+
var import_ai4 = require("@tradejs/infra/ai");
|
|
8255
8392
|
var import_coreResearch = require("@tradejs/infra/coreResearch");
|
|
8256
8393
|
var import_ml3 = require("@tradejs/infra/ml");
|
|
8257
8394
|
|
|
@@ -9555,7 +9692,8 @@ var createBacktestSession = async ({
|
|
|
9555
9692
|
}
|
|
9556
9693
|
if (test.researchTrace && signal && typeof signal !== "string" && signal.signalId) {
|
|
9557
9694
|
const identity = resolveCoreResearchSetupIdentity(signal);
|
|
9558
|
-
|
|
9695
|
+
if (signal.orderStatus !== "failed" && signal.orderStatus !== "skipped")
|
|
9696
|
+
pendingResearchSetupBySignalId.set(signal.signalId, identity);
|
|
9559
9697
|
await (0, import_coreResearch.appendCoreResearchTraceEvent)({
|
|
9560
9698
|
strategyName: test.strategyName,
|
|
9561
9699
|
chunkId,
|
|
@@ -9570,7 +9708,11 @@ var createBacktestSession = async ({
|
|
|
9570
9708
|
configId: test.configId,
|
|
9571
9709
|
backtestRunId: test.backtestRunId,
|
|
9572
9710
|
backtestTestKey: test.backtestTestKey,
|
|
9573
|
-
...identity
|
|
9711
|
+
...identity,
|
|
9712
|
+
...test.strategyConfig.RESEARCH_ENTRY_POLICY ? {
|
|
9713
|
+
reason: signal.orderSkipReason,
|
|
9714
|
+
researchEntryDecision: signal.additionalIndicators?.researchEntryDecision
|
|
9715
|
+
} : {}
|
|
9574
9716
|
}
|
|
9575
9717
|
});
|
|
9576
9718
|
const traceEvent = signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted";
|
|
@@ -9598,7 +9740,7 @@ var createBacktestSession = async ({
|
|
|
9598
9740
|
})
|
|
9599
9741
|
);
|
|
9600
9742
|
}
|
|
9601
|
-
if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(signal.assessment && signal.orderStatus === "skipped")) {
|
|
9743
|
+
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
9744
|
pendingAiRowBySignalId.set(signal.signalId, {
|
|
9603
9745
|
signalId: signal.signalId,
|
|
9604
9746
|
strategyName: signal.strategy || test.strategyName,
|
|
@@ -9639,7 +9781,7 @@ var createBacktestSession = async ({
|
|
|
9639
9781
|
if (aiRowBase) {
|
|
9640
9782
|
pendingAiRowBySignalId.delete(resultRecord.signalId);
|
|
9641
9783
|
const { signal: aiSignal, ...rowBase } = aiRowBase;
|
|
9642
|
-
await (0,
|
|
9784
|
+
await (0, import_ai4.appendAiDatasetRow)({
|
|
9643
9785
|
strategyName: test.strategyName,
|
|
9644
9786
|
chunkId,
|
|
9645
9787
|
row: {
|
|
@@ -9649,7 +9791,14 @@ var createBacktestSession = async ({
|
|
|
9649
9791
|
tradeResult: resultRecord.tradeResult,
|
|
9650
9792
|
research: {
|
|
9651
9793
|
schema: "tradejs-core-research-row/v1",
|
|
9652
|
-
...resolveCoreResearchSetupIdentity(aiSignal)
|
|
9794
|
+
...resolveCoreResearchSetupIdentity(aiSignal),
|
|
9795
|
+
...test.researchTrace ? {
|
|
9796
|
+
decisionContext: {
|
|
9797
|
+
schema: "tradejs-research-decision-context/v1",
|
|
9798
|
+
knownAt: aiSignal.timestamp,
|
|
9799
|
+
baseContext: aiSignal.additionalIndicators?.baseContext ?? null
|
|
9800
|
+
}
|
|
9801
|
+
} : {}
|
|
9653
9802
|
}
|
|
9654
9803
|
}
|
|
9655
9804
|
});
|
|
@@ -10522,8 +10671,13 @@ var testingGroupInSharedCandleLoop = async (tests) => {
|
|
|
10522
10671
|
};
|
|
10523
10672
|
// Annotate the CommonJS export names for ESM import in node:
|
|
10524
10673
|
0 && (module.exports = {
|
|
10674
|
+
RESEARCH_ENTRY_POLICY_CAPABILITIES,
|
|
10675
|
+
buildResearchEntryPayload,
|
|
10525
10676
|
canRunTestsInSharedCandleLoop,
|
|
10526
10677
|
createTestConnector,
|
|
10678
|
+
evaluateResearchEntryPolicy,
|
|
10679
|
+
matchesResearchEntryRules,
|
|
10680
|
+
parseResearchEntryPolicy,
|
|
10527
10681
|
releaseTestingSymbolCache,
|
|
10528
10682
|
resetTestingKlineCache,
|
|
10529
10683
|
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
|
-
|
|
8
|
-
|
|
8
|
+
buildResearchEntryPayload,
|
|
9
|
+
enrichSignalWithMarketContextStages,
|
|
10
|
+
evaluateResearchEntryPolicy,
|
|
11
|
+
matchesResearchEntryRules,
|
|
12
|
+
parseResearchEntryPolicy
|
|
13
|
+
} from "./chunk-K56EW2BB.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
|
-
|
|
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,
|
|
@@ -3179,6 +3181,115 @@ var resolveStrategyConfig = async ({
|
|
|
3179
3181
|
return { config, isConfigFromBacktest };
|
|
3180
3182
|
};
|
|
3181
3183
|
|
|
3184
|
+
// src/researchEntryPolicy.ts
|
|
3185
|
+
var RESEARCH_ENTRY_POLICY_CAPABILITIES = {
|
|
3186
|
+
directionSelection: true
|
|
3187
|
+
};
|
|
3188
|
+
var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
|
|
3189
|
+
var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
|
|
3190
|
+
const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
|
|
3191
|
+
const raw = signal.additionalIndicators?.baseContext;
|
|
3192
|
+
const adapted = additional.baseContext;
|
|
3193
|
+
return {
|
|
3194
|
+
...payload,
|
|
3195
|
+
additionalIndicators: {
|
|
3196
|
+
...additional,
|
|
3197
|
+
...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
|
|
3198
|
+
baseContext: {
|
|
3199
|
+
...raw && typeof raw === "object" ? raw : {},
|
|
3200
|
+
...adapted && typeof adapted === "object" ? adapted : {}
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
};
|
|
3204
|
+
};
|
|
3205
|
+
var parseResearchEntryPolicy = (value) => {
|
|
3206
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3207
|
+
throw new Error("Invalid research entry policy");
|
|
3208
|
+
const policy = value;
|
|
3209
|
+
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")
|
|
3210
|
+
throw new Error("Invalid research entry policy contract");
|
|
3211
|
+
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))))
|
|
3212
|
+
throw new Error("Invalid research policy directions");
|
|
3213
|
+
for (const side of ["LONG", "SHORT"]) {
|
|
3214
|
+
const predicates = policy.rules[side];
|
|
3215
|
+
if (predicates === null) continue;
|
|
3216
|
+
if (!Array.isArray(predicates) || predicates.length > 4)
|
|
3217
|
+
throw new Error(
|
|
3218
|
+
"Research policy needs bounded rules for both directions"
|
|
3219
|
+
);
|
|
3220
|
+
for (const predicate of predicates) {
|
|
3221
|
+
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)))
|
|
3222
|
+
throw new Error(
|
|
3223
|
+
"Research policy contains an unsupported or non-causal predicate"
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
return policy;
|
|
3228
|
+
};
|
|
3229
|
+
var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
|
|
3230
|
+
const value = predicate.path.split(".").reduce(
|
|
3231
|
+
(current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
3232
|
+
payload
|
|
3233
|
+
);
|
|
3234
|
+
if (value == null) return false;
|
|
3235
|
+
if (predicate.op === "==") return value === predicate.value;
|
|
3236
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return false;
|
|
3237
|
+
return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
|
|
3238
|
+
});
|
|
3239
|
+
var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
|
|
3240
|
+
if (signal.strategy !== policy.strategy)
|
|
3241
|
+
throw new Error("Research policy strategy mismatch");
|
|
3242
|
+
if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
|
|
3243
|
+
onDecision?.({
|
|
3244
|
+
mode: policy.mode,
|
|
3245
|
+
allowed: false,
|
|
3246
|
+
reason: "DIRECTION_BLOCKED"
|
|
3247
|
+
});
|
|
3248
|
+
return false;
|
|
3249
|
+
}
|
|
3250
|
+
if (policy.mode === "current-gate") {
|
|
3251
|
+
const analysis = await runAiPromptLocal(signal);
|
|
3252
|
+
const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
|
|
3253
|
+
onDecision?.({
|
|
3254
|
+
mode: policy.mode,
|
|
3255
|
+
allowed: allowed2,
|
|
3256
|
+
quality: analysis.quality ?? null,
|
|
3257
|
+
reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
|
|
3258
|
+
});
|
|
3259
|
+
return allowed2;
|
|
3260
|
+
}
|
|
3261
|
+
const original = buildAiPayload(signal);
|
|
3262
|
+
const gateContext = getDeterministicAiGateContext(original);
|
|
3263
|
+
if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
|
|
3264
|
+
onDecision?.({
|
|
3265
|
+
mode: policy.mode,
|
|
3266
|
+
allowed: false,
|
|
3267
|
+
reason: "STRUCTURAL_HARD_BLOCK"
|
|
3268
|
+
});
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
const rules = policy.rules[signal.direction];
|
|
3272
|
+
const payload = buildResearchEntryPayload(signal, original);
|
|
3273
|
+
const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
|
|
3274
|
+
onDecision?.({
|
|
3275
|
+
mode: policy.mode,
|
|
3276
|
+
allowed,
|
|
3277
|
+
reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
|
|
3278
|
+
predicates: rules?.map((rule) => {
|
|
3279
|
+
const value = rule.path.split(".").reduce(
|
|
3280
|
+
(current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
3281
|
+
payload
|
|
3282
|
+
);
|
|
3283
|
+
return {
|
|
3284
|
+
path: rule.path,
|
|
3285
|
+
actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
|
|
3286
|
+
passed: matchesResearchEntryRules(payload, [rule])
|
|
3287
|
+
};
|
|
3288
|
+
})
|
|
3289
|
+
});
|
|
3290
|
+
return allowed;
|
|
3291
|
+
};
|
|
3292
|
+
|
|
3182
3293
|
// src/strategyRuntime.ts
|
|
3183
3294
|
import { parseJevConfig } from "@tradejs/core/jev";
|
|
3184
3295
|
import {
|
|
@@ -4145,6 +4256,12 @@ var createStrategyRuntime = ({
|
|
|
4145
4256
|
const projectConfig = await loadTradejsConfig(projectRoot);
|
|
4146
4257
|
const projectHooks = projectConfig.hooks;
|
|
4147
4258
|
const env = String(config.ENV ?? "BACKTEST");
|
|
4259
|
+
const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
|
|
4260
|
+
const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
|
|
4261
|
+
if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
|
|
4262
|
+
throw new Error(
|
|
4263
|
+
"Research entry policies require a BACKTEST simulation connector"
|
|
4264
|
+
);
|
|
4148
4265
|
const jev = parseJevConfig(config.JEV);
|
|
4149
4266
|
if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
|
|
4150
4267
|
throw new Error(
|
|
@@ -4855,7 +4972,8 @@ var createStrategyRuntime = ({
|
|
|
4855
4972
|
await enrichSignalWithMarketContextStages({
|
|
4856
4973
|
signal,
|
|
4857
4974
|
env,
|
|
4858
|
-
includeHyperliquidWhales:
|
|
4975
|
+
includeHyperliquidWhales: Boolean(researchEntryPolicy),
|
|
4976
|
+
...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
|
|
4859
4977
|
});
|
|
4860
4978
|
if (jev) {
|
|
4861
4979
|
await assessSignalWithJev({
|
|
@@ -4953,6 +5071,22 @@ var createStrategyRuntime = ({
|
|
|
4953
5071
|
}
|
|
4954
5072
|
return signal ?? decision.code;
|
|
4955
5073
|
}
|
|
5074
|
+
if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
|
|
5075
|
+
researchEntryPolicy,
|
|
5076
|
+
signal,
|
|
5077
|
+
(decision2) => {
|
|
5078
|
+
signal.additionalIndicators = {
|
|
5079
|
+
...signal.additionalIndicators,
|
|
5080
|
+
researchEntryDecision: decision2
|
|
5081
|
+
};
|
|
5082
|
+
}
|
|
5083
|
+
))) {
|
|
5084
|
+
if (signal) {
|
|
5085
|
+
signal.orderStatus = "skipped";
|
|
5086
|
+
signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
5087
|
+
}
|
|
5088
|
+
return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
5089
|
+
}
|
|
4956
5090
|
const entryGate = await invokeGateHooks(
|
|
4957
5091
|
"beforeEntryGate",
|
|
4958
5092
|
decisionManifest?.hooks?.beforeEntryGate,
|
|
@@ -5088,5 +5222,10 @@ export {
|
|
|
5088
5222
|
validateEntryProtectionAtArrival,
|
|
5089
5223
|
executeEntryOrder,
|
|
5090
5224
|
resolveStrategyConfig,
|
|
5225
|
+
RESEARCH_ENTRY_POLICY_CAPABILITIES,
|
|
5226
|
+
buildResearchEntryPayload,
|
|
5227
|
+
parseResearchEntryPolicy,
|
|
5228
|
+
matchesResearchEntryRules,
|
|
5229
|
+
evaluateResearchEntryPolicy,
|
|
5091
5230
|
createStrategyRuntime
|
|
5092
5231
|
};
|
package/dist/diagnostics.d.mts
CHANGED
|
@@ -179,4 +179,57 @@ declare const verifyRuntimeFeedbackReplaySource: ({ bundleDir, runtimeEvidenceBu
|
|
|
179
179
|
runtimeEvidence: VerifiedRuntimeEvidenceBundle;
|
|
180
180
|
}>;
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
/** Version of the complete feedback read contract, independent of package versions. */
|
|
183
|
+
declare const MCP_FEEDBACK_CONTRACT: {
|
|
184
|
+
readonly version: 1;
|
|
185
|
+
readonly hostProtocolVersion: 2;
|
|
186
|
+
readonly replayQueue: {
|
|
187
|
+
readonly waitingStates: readonly ["waiting_resources", "waiting_executor"];
|
|
188
|
+
readonly automaticFailedRetries: false;
|
|
189
|
+
};
|
|
190
|
+
readonly artifactEncoding: "base64";
|
|
191
|
+
readonly maximumChunkBytes: 48000;
|
|
192
|
+
readonly artifactFiles: readonly ["payload", "manifest.json", ".complete", "replay.log"];
|
|
193
|
+
readonly requiredTools: readonly ["tradejs_info", "diagnostics_list_reports", "diagnostics_get_report", "artifact_get", "job_get", "job_get_logs"];
|
|
194
|
+
};
|
|
195
|
+
type McpToolDescription = {
|
|
196
|
+
name: string;
|
|
197
|
+
inputSchema: {
|
|
198
|
+
properties?: Record<string, unknown>;
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
/** Check the actual client's tools/list, not only what the server advertises. */
|
|
202
|
+
declare const assertMcpFeedbackContract: (version: unknown, tools: McpToolDescription[]) => void;
|
|
203
|
+
|
|
204
|
+
type McpArtifactFile = 'payload' | 'manifest.json' | '.complete' | 'replay.log';
|
|
205
|
+
type McpBundleDescriptor = {
|
|
206
|
+
id: string;
|
|
207
|
+
kind: 'runtime-evidence' | 'runtime-feedback-replay';
|
|
208
|
+
files: {
|
|
209
|
+
file: McpArtifactFile;
|
|
210
|
+
bytes: number;
|
|
211
|
+
sha256: string;
|
|
212
|
+
available: boolean;
|
|
213
|
+
}[];
|
|
214
|
+
};
|
|
215
|
+
declare class McpReadTransportError extends Error {
|
|
216
|
+
readonly retryAfterMs?: number | undefined;
|
|
217
|
+
constructor(message: string, retryAfterMs?: number | undefined);
|
|
218
|
+
}
|
|
219
|
+
type ChunkRequest = {
|
|
220
|
+
artifactId: string;
|
|
221
|
+
file: McpArtifactFile;
|
|
222
|
+
offset: number;
|
|
223
|
+
length: number;
|
|
224
|
+
};
|
|
225
|
+
/** The caller owns the authenticated MCP connection. This function only reads artifacts. */
|
|
226
|
+
declare const downloadMcpBundle: ({ descriptor, root, contractVersion, tools, readChunk, wait, }: {
|
|
227
|
+
descriptor: McpBundleDescriptor;
|
|
228
|
+
root: string;
|
|
229
|
+
contractVersion: unknown;
|
|
230
|
+
tools: McpToolDescription[];
|
|
231
|
+
readChunk: (request: ChunkRequest) => Promise<unknown>;
|
|
232
|
+
wait?: (ms: number) => Promise<unknown>;
|
|
233
|
+
}) => Promise<VerifiedRuntimeEvidenceBundle | VerifiedRuntimeFeedbackReplayBundle>;
|
|
234
|
+
|
|
235
|
+
export { MCP_FEEDBACK_CONTRACT, type McpArtifactFile, type McpBundleDescriptor, McpReadTransportError, type McpToolDescription, RUNTIME_EVIDENCE_COMPLETE_FILE, RUNTIME_EVIDENCE_MANIFEST_FILE, RUNTIME_EVIDENCE_PAYLOAD_FILE, RUNTIME_FEEDBACK_COMPLETE_FILE, RUNTIME_FEEDBACK_LOG_FILE, RUNTIME_FEEDBACK_MANIFEST_FILE, RUNTIME_FEEDBACK_REPLAY_FILE, type RuntimeEvidenceArtifactManifest, type RuntimeEvidenceDeploymentSnapshot, type RuntimeEvidenceProducer, type RuntimeEvidenceStrategySnapshot, type RuntimeFeedbackReplayManifest, type VerifiedRuntimeEvidenceBundle, type VerifiedRuntimeFeedbackReplayBundle, activeRuntimeEvidenceStrategies, assertCurrentRuntimeEvidenceArtifact, assertMcpFeedbackContract, discoverRuntimeEvidenceBundles, discoverRuntimeFeedbackReplayBundles, downloadMcpBundle, parseRuntimeEvidenceDeploymentSnapshot, parseRuntimeEvidenceProducer, publishRuntimeEvidenceBundle, resolveRuntimeEvidenceDeploymentSnapshot, resolveRuntimeEvidenceProducer, resolveRuntimeEvidenceTickerUniverse, runtimeDeploymentFromEvidence, runtimeLineageMatchesStrategySnapshot, sealRuntimeFeedbackReplayBundle, verifyRuntimeEvidenceBundle, verifyRuntimeFeedbackReplayBundle, verifyRuntimeFeedbackReplaySource };
|