@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.
- package/dist/backtest.d.mts +13 -2
- package/dist/backtest.d.ts +13 -2
- package/dist/backtest.js +164 -7
- package/dist/backtest.mjs +28 -6
- package/dist/{chunk-SKY2HNZU.mjs → chunk-KOV3U6ZZ.mjs} +143 -1
- package/dist/diagnostics.js +133 -1
- package/dist/diagnostics.mjs +1 -1
- package/dist/mcp.js +133 -1
- package/dist/mcp.mjs +1 -1
- package/dist/registry.js +133 -1
- package/dist/registry.mjs +1 -1
- package/dist/runtimeDashboard.js +133 -1
- package/dist/runtimeDashboard.mjs +1 -1
- package/dist/runtimeStrategies.js +133 -1
- package/dist/runtimeStrategies.mjs +1 -1
- package/dist/strategies.js +133 -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,
|
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
};
|