@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/diagnostics.js
CHANGED
|
@@ -3000,6 +3000,7 @@ var recordRuntimeTradeIncrease = async (params) => {
|
|
|
3000
3000
|
const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
|
|
3001
3001
|
const next = {
|
|
3002
3002
|
...existing,
|
|
3003
|
+
...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
|
|
3003
3004
|
qty: resultingQty,
|
|
3004
3005
|
entryPrice: resultingEntryPrice,
|
|
3005
3006
|
entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
|
|
@@ -5728,6 +5729,7 @@ var executeEntryOrder = async ({
|
|
|
5728
5729
|
openFee: estimatedOpenFee,
|
|
5729
5730
|
totalFee: estimatedOpenFee,
|
|
5730
5731
|
entryTimestamp: timestamp,
|
|
5732
|
+
...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
|
|
5731
5733
|
universe: signal.universe,
|
|
5732
5734
|
assetClass: signal.assetClass,
|
|
5733
5735
|
accountId: signal.accountId,
|
|
@@ -5751,6 +5753,7 @@ var executeEntryOrder = async ({
|
|
|
5751
5753
|
addedQty: filledQty,
|
|
5752
5754
|
addedEntryPrice: fillPrice,
|
|
5753
5755
|
entryTimestamp: timestamp,
|
|
5756
|
+
...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
|
|
5754
5757
|
fee: estimatedOpenFee,
|
|
5755
5758
|
accountId: signal.accountId,
|
|
5756
5759
|
deploymentId: signal.deploymentId
|
|
@@ -6217,6 +6220,112 @@ var buildAiHookContext = ({
|
|
|
6217
6220
|
};
|
|
6218
6221
|
};
|
|
6219
6222
|
|
|
6223
|
+
// src/researchEntryPolicy.ts
|
|
6224
|
+
var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
|
|
6225
|
+
var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
|
|
6226
|
+
const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
|
|
6227
|
+
const raw = signal.additionalIndicators?.baseContext;
|
|
6228
|
+
const adapted = additional.baseContext;
|
|
6229
|
+
return {
|
|
6230
|
+
...payload,
|
|
6231
|
+
additionalIndicators: {
|
|
6232
|
+
...additional,
|
|
6233
|
+
...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
|
|
6234
|
+
baseContext: {
|
|
6235
|
+
...raw && typeof raw === "object" ? raw : {},
|
|
6236
|
+
...adapted && typeof adapted === "object" ? adapted : {}
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
};
|
|
6240
|
+
};
|
|
6241
|
+
var parseResearchEntryPolicy = (value) => {
|
|
6242
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6243
|
+
throw new Error("Invalid research entry policy");
|
|
6244
|
+
const policy = value;
|
|
6245
|
+
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")
|
|
6246
|
+
throw new Error("Invalid research entry policy contract");
|
|
6247
|
+
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))))
|
|
6248
|
+
throw new Error("Invalid research policy directions");
|
|
6249
|
+
for (const side of ["LONG", "SHORT"]) {
|
|
6250
|
+
const predicates = policy.rules[side];
|
|
6251
|
+
if (predicates === null) continue;
|
|
6252
|
+
if (!Array.isArray(predicates) || predicates.length > 4)
|
|
6253
|
+
throw new Error(
|
|
6254
|
+
"Research policy needs bounded rules for both directions"
|
|
6255
|
+
);
|
|
6256
|
+
for (const predicate of predicates) {
|
|
6257
|
+
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)))
|
|
6258
|
+
throw new Error(
|
|
6259
|
+
"Research policy contains an unsupported or non-causal predicate"
|
|
6260
|
+
);
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
return policy;
|
|
6264
|
+
};
|
|
6265
|
+
var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
|
|
6266
|
+
const value = predicate.path.split(".").reduce(
|
|
6267
|
+
(current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
6268
|
+
payload
|
|
6269
|
+
);
|
|
6270
|
+
if (value == null) return false;
|
|
6271
|
+
if (predicate.op === "==") return value === predicate.value;
|
|
6272
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return false;
|
|
6273
|
+
return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
|
|
6274
|
+
});
|
|
6275
|
+
var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
|
|
6276
|
+
if (signal.strategy !== policy.strategy)
|
|
6277
|
+
throw new Error("Research policy strategy mismatch");
|
|
6278
|
+
if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
|
|
6279
|
+
onDecision?.({
|
|
6280
|
+
mode: policy.mode,
|
|
6281
|
+
allowed: false,
|
|
6282
|
+
reason: "DIRECTION_BLOCKED"
|
|
6283
|
+
});
|
|
6284
|
+
return false;
|
|
6285
|
+
}
|
|
6286
|
+
if (policy.mode === "current-gate") {
|
|
6287
|
+
const analysis = await runAiPromptLocal(signal);
|
|
6288
|
+
const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
|
|
6289
|
+
onDecision?.({
|
|
6290
|
+
mode: policy.mode,
|
|
6291
|
+
allowed: allowed2,
|
|
6292
|
+
quality: analysis.quality ?? null,
|
|
6293
|
+
reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
|
|
6294
|
+
});
|
|
6295
|
+
return allowed2;
|
|
6296
|
+
}
|
|
6297
|
+
const original = buildAiPayload(signal);
|
|
6298
|
+
const gateContext = getDeterministicAiGateContext(original);
|
|
6299
|
+
if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
|
|
6300
|
+
onDecision?.({
|
|
6301
|
+
mode: policy.mode,
|
|
6302
|
+
allowed: false,
|
|
6303
|
+
reason: "STRUCTURAL_HARD_BLOCK"
|
|
6304
|
+
});
|
|
6305
|
+
return false;
|
|
6306
|
+
}
|
|
6307
|
+
const rules = policy.rules[signal.direction];
|
|
6308
|
+
const payload = buildResearchEntryPayload(signal, original);
|
|
6309
|
+
const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
|
|
6310
|
+
onDecision?.({
|
|
6311
|
+
mode: policy.mode,
|
|
6312
|
+
allowed,
|
|
6313
|
+
reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
|
|
6314
|
+
predicates: rules?.map((rule) => {
|
|
6315
|
+
const value = rule.path.split(".").reduce(
|
|
6316
|
+
(current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
6317
|
+
payload
|
|
6318
|
+
);
|
|
6319
|
+
return {
|
|
6320
|
+
path: rule.path,
|
|
6321
|
+
actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
|
|
6322
|
+
passed: matchesResearchEntryRules(payload, [rule])
|
|
6323
|
+
};
|
|
6324
|
+
})
|
|
6325
|
+
});
|
|
6326
|
+
return allowed;
|
|
6327
|
+
};
|
|
6328
|
+
|
|
6220
6329
|
// src/strategy/runtimeDecisionTelemetry.ts
|
|
6221
6330
|
var finitePositive = (value) => {
|
|
6222
6331
|
const parsed = Number(value);
|
|
@@ -6755,6 +6864,12 @@ var createStrategyRuntime = ({
|
|
|
6755
6864
|
const projectConfig = await loadTradejsConfig(projectRoot);
|
|
6756
6865
|
const projectHooks = projectConfig.hooks;
|
|
6757
6866
|
const env = String(config.ENV ?? "BACKTEST");
|
|
6867
|
+
const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
|
|
6868
|
+
const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
|
|
6869
|
+
if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
|
|
6870
|
+
throw new Error(
|
|
6871
|
+
"Research entry policies require a BACKTEST simulation connector"
|
|
6872
|
+
);
|
|
6758
6873
|
const jev = (0, import_jev6.parseJevConfig)(config.JEV);
|
|
6759
6874
|
if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
|
|
6760
6875
|
throw new Error(
|
|
@@ -7465,7 +7580,8 @@ var createStrategyRuntime = ({
|
|
|
7465
7580
|
await enrichSignalWithMarketContextStages({
|
|
7466
7581
|
signal,
|
|
7467
7582
|
env,
|
|
7468
|
-
includeHyperliquidWhales:
|
|
7583
|
+
includeHyperliquidWhales: Boolean(researchEntryPolicy),
|
|
7584
|
+
...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
|
|
7469
7585
|
});
|
|
7470
7586
|
if (jev) {
|
|
7471
7587
|
await assessSignalWithJev({
|
|
@@ -7563,6 +7679,22 @@ var createStrategyRuntime = ({
|
|
|
7563
7679
|
}
|
|
7564
7680
|
return signal ?? decision.code;
|
|
7565
7681
|
}
|
|
7682
|
+
if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
|
|
7683
|
+
researchEntryPolicy,
|
|
7684
|
+
signal,
|
|
7685
|
+
(decision2) => {
|
|
7686
|
+
signal.additionalIndicators = {
|
|
7687
|
+
...signal.additionalIndicators,
|
|
7688
|
+
researchEntryDecision: decision2
|
|
7689
|
+
};
|
|
7690
|
+
}
|
|
7691
|
+
))) {
|
|
7692
|
+
if (signal) {
|
|
7693
|
+
signal.orderStatus = "skipped";
|
|
7694
|
+
signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
7695
|
+
}
|
|
7696
|
+
return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
7697
|
+
}
|
|
7566
7698
|
const entryGate = await invokeGateHooks(
|
|
7567
7699
|
"beforeEntryGate",
|
|
7568
7700
|
decisionManifest?.hooks?.beforeEntryGate,
|
package/dist/diagnostics.mjs
CHANGED
package/dist/mcp.js
CHANGED
|
@@ -2994,6 +2994,7 @@ var recordRuntimeTradeIncrease = async (params) => {
|
|
|
2994
2994
|
const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
|
|
2995
2995
|
const next = {
|
|
2996
2996
|
...existing,
|
|
2997
|
+
...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
|
|
2997
2998
|
qty: resultingQty,
|
|
2998
2999
|
entryPrice: resultingEntryPrice,
|
|
2999
3000
|
entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
|
|
@@ -5722,6 +5723,7 @@ var executeEntryOrder = async ({
|
|
|
5722
5723
|
openFee: estimatedOpenFee,
|
|
5723
5724
|
totalFee: estimatedOpenFee,
|
|
5724
5725
|
entryTimestamp: timestamp,
|
|
5726
|
+
...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
|
|
5725
5727
|
universe: signal.universe,
|
|
5726
5728
|
assetClass: signal.assetClass,
|
|
5727
5729
|
accountId: signal.accountId,
|
|
@@ -5745,6 +5747,7 @@ var executeEntryOrder = async ({
|
|
|
5745
5747
|
addedQty: filledQty,
|
|
5746
5748
|
addedEntryPrice: fillPrice,
|
|
5747
5749
|
entryTimestamp: timestamp,
|
|
5750
|
+
...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
|
|
5748
5751
|
fee: estimatedOpenFee,
|
|
5749
5752
|
accountId: signal.accountId,
|
|
5750
5753
|
deploymentId: signal.deploymentId
|
|
@@ -6211,6 +6214,112 @@ var buildAiHookContext = ({
|
|
|
6211
6214
|
};
|
|
6212
6215
|
};
|
|
6213
6216
|
|
|
6217
|
+
// src/researchEntryPolicy.ts
|
|
6218
|
+
var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
|
|
6219
|
+
var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
|
|
6220
|
+
const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
|
|
6221
|
+
const raw = signal.additionalIndicators?.baseContext;
|
|
6222
|
+
const adapted = additional.baseContext;
|
|
6223
|
+
return {
|
|
6224
|
+
...payload,
|
|
6225
|
+
additionalIndicators: {
|
|
6226
|
+
...additional,
|
|
6227
|
+
...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
|
|
6228
|
+
baseContext: {
|
|
6229
|
+
...raw && typeof raw === "object" ? raw : {},
|
|
6230
|
+
...adapted && typeof adapted === "object" ? adapted : {}
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
};
|
|
6234
|
+
};
|
|
6235
|
+
var parseResearchEntryPolicy = (value) => {
|
|
6236
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6237
|
+
throw new Error("Invalid research entry policy");
|
|
6238
|
+
const policy = value;
|
|
6239
|
+
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")
|
|
6240
|
+
throw new Error("Invalid research entry policy contract");
|
|
6241
|
+
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))))
|
|
6242
|
+
throw new Error("Invalid research policy directions");
|
|
6243
|
+
for (const side of ["LONG", "SHORT"]) {
|
|
6244
|
+
const predicates = policy.rules[side];
|
|
6245
|
+
if (predicates === null) continue;
|
|
6246
|
+
if (!Array.isArray(predicates) || predicates.length > 4)
|
|
6247
|
+
throw new Error(
|
|
6248
|
+
"Research policy needs bounded rules for both directions"
|
|
6249
|
+
);
|
|
6250
|
+
for (const predicate of predicates) {
|
|
6251
|
+
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)))
|
|
6252
|
+
throw new Error(
|
|
6253
|
+
"Research policy contains an unsupported or non-causal predicate"
|
|
6254
|
+
);
|
|
6255
|
+
}
|
|
6256
|
+
}
|
|
6257
|
+
return policy;
|
|
6258
|
+
};
|
|
6259
|
+
var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
|
|
6260
|
+
const value = predicate.path.split(".").reduce(
|
|
6261
|
+
(current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
6262
|
+
payload
|
|
6263
|
+
);
|
|
6264
|
+
if (value == null) return false;
|
|
6265
|
+
if (predicate.op === "==") return value === predicate.value;
|
|
6266
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return false;
|
|
6267
|
+
return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
|
|
6268
|
+
});
|
|
6269
|
+
var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
|
|
6270
|
+
if (signal.strategy !== policy.strategy)
|
|
6271
|
+
throw new Error("Research policy strategy mismatch");
|
|
6272
|
+
if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
|
|
6273
|
+
onDecision?.({
|
|
6274
|
+
mode: policy.mode,
|
|
6275
|
+
allowed: false,
|
|
6276
|
+
reason: "DIRECTION_BLOCKED"
|
|
6277
|
+
});
|
|
6278
|
+
return false;
|
|
6279
|
+
}
|
|
6280
|
+
if (policy.mode === "current-gate") {
|
|
6281
|
+
const analysis = await runAiPromptLocal(signal);
|
|
6282
|
+
const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
|
|
6283
|
+
onDecision?.({
|
|
6284
|
+
mode: policy.mode,
|
|
6285
|
+
allowed: allowed2,
|
|
6286
|
+
quality: analysis.quality ?? null,
|
|
6287
|
+
reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
|
|
6288
|
+
});
|
|
6289
|
+
return allowed2;
|
|
6290
|
+
}
|
|
6291
|
+
const original = buildAiPayload(signal);
|
|
6292
|
+
const gateContext = getDeterministicAiGateContext(original);
|
|
6293
|
+
if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
|
|
6294
|
+
onDecision?.({
|
|
6295
|
+
mode: policy.mode,
|
|
6296
|
+
allowed: false,
|
|
6297
|
+
reason: "STRUCTURAL_HARD_BLOCK"
|
|
6298
|
+
});
|
|
6299
|
+
return false;
|
|
6300
|
+
}
|
|
6301
|
+
const rules = policy.rules[signal.direction];
|
|
6302
|
+
const payload = buildResearchEntryPayload(signal, original);
|
|
6303
|
+
const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
|
|
6304
|
+
onDecision?.({
|
|
6305
|
+
mode: policy.mode,
|
|
6306
|
+
allowed,
|
|
6307
|
+
reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
|
|
6308
|
+
predicates: rules?.map((rule) => {
|
|
6309
|
+
const value = rule.path.split(".").reduce(
|
|
6310
|
+
(current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
6311
|
+
payload
|
|
6312
|
+
);
|
|
6313
|
+
return {
|
|
6314
|
+
path: rule.path,
|
|
6315
|
+
actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
|
|
6316
|
+
passed: matchesResearchEntryRules(payload, [rule])
|
|
6317
|
+
};
|
|
6318
|
+
})
|
|
6319
|
+
});
|
|
6320
|
+
return allowed;
|
|
6321
|
+
};
|
|
6322
|
+
|
|
6214
6323
|
// src/strategy/runtimeDecisionTelemetry.ts
|
|
6215
6324
|
var finitePositive = (value) => {
|
|
6216
6325
|
const parsed = Number(value);
|
|
@@ -6749,6 +6858,12 @@ var createStrategyRuntime = ({
|
|
|
6749
6858
|
const projectConfig = await loadTradejsConfig(projectRoot);
|
|
6750
6859
|
const projectHooks = projectConfig.hooks;
|
|
6751
6860
|
const env = String(config.ENV ?? "BACKTEST");
|
|
6861
|
+
const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
|
|
6862
|
+
const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
|
|
6863
|
+
if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
|
|
6864
|
+
throw new Error(
|
|
6865
|
+
"Research entry policies require a BACKTEST simulation connector"
|
|
6866
|
+
);
|
|
6752
6867
|
const jev = (0, import_jev6.parseJevConfig)(config.JEV);
|
|
6753
6868
|
if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
|
|
6754
6869
|
throw new Error(
|
|
@@ -7459,7 +7574,8 @@ var createStrategyRuntime = ({
|
|
|
7459
7574
|
await enrichSignalWithMarketContextStages({
|
|
7460
7575
|
signal,
|
|
7461
7576
|
env,
|
|
7462
|
-
includeHyperliquidWhales:
|
|
7577
|
+
includeHyperliquidWhales: Boolean(researchEntryPolicy),
|
|
7578
|
+
...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
|
|
7463
7579
|
});
|
|
7464
7580
|
if (jev) {
|
|
7465
7581
|
await assessSignalWithJev({
|
|
@@ -7557,6 +7673,22 @@ var createStrategyRuntime = ({
|
|
|
7557
7673
|
}
|
|
7558
7674
|
return signal ?? decision.code;
|
|
7559
7675
|
}
|
|
7676
|
+
if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
|
|
7677
|
+
researchEntryPolicy,
|
|
7678
|
+
signal,
|
|
7679
|
+
(decision2) => {
|
|
7680
|
+
signal.additionalIndicators = {
|
|
7681
|
+
...signal.additionalIndicators,
|
|
7682
|
+
researchEntryDecision: decision2
|
|
7683
|
+
};
|
|
7684
|
+
}
|
|
7685
|
+
))) {
|
|
7686
|
+
if (signal) {
|
|
7687
|
+
signal.orderStatus = "skipped";
|
|
7688
|
+
signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
7689
|
+
}
|
|
7690
|
+
return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
7691
|
+
}
|
|
7560
7692
|
const entryGate = await invokeGateHooks(
|
|
7561
7693
|
"beforeEntryGate",
|
|
7562
7694
|
decisionManifest?.hooks?.beforeEntryGate,
|
package/dist/mcp.mjs
CHANGED
package/dist/registry.js
CHANGED
|
@@ -2996,6 +2996,7 @@ var recordRuntimeTradeIncrease = async (params) => {
|
|
|
2996
2996
|
const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
|
|
2997
2997
|
const next = {
|
|
2998
2998
|
...existing,
|
|
2999
|
+
...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
|
|
2999
3000
|
qty: resultingQty,
|
|
3000
3001
|
entryPrice: resultingEntryPrice,
|
|
3001
3002
|
entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
|
|
@@ -5724,6 +5725,7 @@ var executeEntryOrder = async ({
|
|
|
5724
5725
|
openFee: estimatedOpenFee,
|
|
5725
5726
|
totalFee: estimatedOpenFee,
|
|
5726
5727
|
entryTimestamp: timestamp,
|
|
5728
|
+
...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
|
|
5727
5729
|
universe: signal.universe,
|
|
5728
5730
|
assetClass: signal.assetClass,
|
|
5729
5731
|
accountId: signal.accountId,
|
|
@@ -5747,6 +5749,7 @@ var executeEntryOrder = async ({
|
|
|
5747
5749
|
addedQty: filledQty,
|
|
5748
5750
|
addedEntryPrice: fillPrice,
|
|
5749
5751
|
entryTimestamp: timestamp,
|
|
5752
|
+
...signal.orderLeverage ? { orderLeverage: signal.orderLeverage } : {},
|
|
5750
5753
|
fee: estimatedOpenFee,
|
|
5751
5754
|
accountId: signal.accountId,
|
|
5752
5755
|
deploymentId: signal.deploymentId
|
|
@@ -6213,6 +6216,112 @@ var buildAiHookContext = ({
|
|
|
6213
6216
|
};
|
|
6214
6217
|
};
|
|
6215
6218
|
|
|
6219
|
+
// src/researchEntryPolicy.ts
|
|
6220
|
+
var forbidden = /(?:^|\.)(?:__proto__|prototype|constructor|profit|pnl|outcome|tradeResult|backtestExecution|quality|deterministicQuality|approvalAllowedNow|timestamp|knownAt|source|coverage|points|rows|length)(?:\.|$)/i;
|
|
6221
|
+
var buildResearchEntryPayload = (signal, payload = buildAiPayload(signal)) => {
|
|
6222
|
+
const additional = payload.additionalIndicators && typeof payload.additionalIndicators === "object" ? payload.additionalIndicators : {};
|
|
6223
|
+
const raw = signal.additionalIndicators?.baseContext;
|
|
6224
|
+
const adapted = additional.baseContext;
|
|
6225
|
+
return {
|
|
6226
|
+
...payload,
|
|
6227
|
+
additionalIndicators: {
|
|
6228
|
+
...additional,
|
|
6229
|
+
...signal.additionalIndicators?.jev && typeof signal.additionalIndicators.jev === "object" ? { jev: signal.additionalIndicators.jev } : {},
|
|
6230
|
+
baseContext: {
|
|
6231
|
+
...raw && typeof raw === "object" ? raw : {},
|
|
6232
|
+
...adapted && typeof adapted === "object" ? adapted : {}
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
};
|
|
6236
|
+
};
|
|
6237
|
+
var parseResearchEntryPolicy = (value) => {
|
|
6238
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6239
|
+
throw new Error("Invalid research entry policy");
|
|
6240
|
+
const policy = value;
|
|
6241
|
+
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")
|
|
6242
|
+
throw new Error("Invalid research entry policy contract");
|
|
6243
|
+
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))))
|
|
6244
|
+
throw new Error("Invalid research policy directions");
|
|
6245
|
+
for (const side of ["LONG", "SHORT"]) {
|
|
6246
|
+
const predicates = policy.rules[side];
|
|
6247
|
+
if (predicates === null) continue;
|
|
6248
|
+
if (!Array.isArray(predicates) || predicates.length > 4)
|
|
6249
|
+
throw new Error(
|
|
6250
|
+
"Research policy needs bounded rules for both directions"
|
|
6251
|
+
);
|
|
6252
|
+
for (const predicate of predicates) {
|
|
6253
|
+
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)))
|
|
6254
|
+
throw new Error(
|
|
6255
|
+
"Research policy contains an unsupported or non-causal predicate"
|
|
6256
|
+
);
|
|
6257
|
+
}
|
|
6258
|
+
}
|
|
6259
|
+
return policy;
|
|
6260
|
+
};
|
|
6261
|
+
var matchesResearchEntryRules = (payload, predicates) => predicates.every((predicate) => {
|
|
6262
|
+
const value = predicate.path.split(".").reduce(
|
|
6263
|
+
(current, key) => current != null && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
6264
|
+
payload
|
|
6265
|
+
);
|
|
6266
|
+
if (value == null) return false;
|
|
6267
|
+
if (predicate.op === "==") return value === predicate.value;
|
|
6268
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return false;
|
|
6269
|
+
return predicate.op === "<=" ? value <= predicate.value : value >= predicate.value;
|
|
6270
|
+
});
|
|
6271
|
+
var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
|
|
6272
|
+
if (signal.strategy !== policy.strategy)
|
|
6273
|
+
throw new Error("Research policy strategy mismatch");
|
|
6274
|
+
if (policy.directions && !policy.directions.includes(signal.direction) || policy.mode === "rules" && policy.rules[signal.direction] === null) {
|
|
6275
|
+
onDecision?.({
|
|
6276
|
+
mode: policy.mode,
|
|
6277
|
+
allowed: false,
|
|
6278
|
+
reason: "DIRECTION_BLOCKED"
|
|
6279
|
+
});
|
|
6280
|
+
return false;
|
|
6281
|
+
}
|
|
6282
|
+
if (policy.mode === "current-gate") {
|
|
6283
|
+
const analysis = await runAiPromptLocal(signal);
|
|
6284
|
+
const allowed2 = analysis.direction === signal.direction && typeof analysis.quality === "number" && analysis.quality >= policy.minQuality;
|
|
6285
|
+
onDecision?.({
|
|
6286
|
+
mode: policy.mode,
|
|
6287
|
+
allowed: allowed2,
|
|
6288
|
+
quality: analysis.quality ?? null,
|
|
6289
|
+
reason: allowed2 ? "CURRENT_GATE_APPROVED" : "CURRENT_GATE_REJECTED"
|
|
6290
|
+
});
|
|
6291
|
+
return allowed2;
|
|
6292
|
+
}
|
|
6293
|
+
const original = buildAiPayload(signal);
|
|
6294
|
+
const gateContext = getDeterministicAiGateContext(original);
|
|
6295
|
+
if (Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length) {
|
|
6296
|
+
onDecision?.({
|
|
6297
|
+
mode: policy.mode,
|
|
6298
|
+
allowed: false,
|
|
6299
|
+
reason: "STRUCTURAL_HARD_BLOCK"
|
|
6300
|
+
});
|
|
6301
|
+
return false;
|
|
6302
|
+
}
|
|
6303
|
+
const rules = policy.rules[signal.direction];
|
|
6304
|
+
const payload = buildResearchEntryPayload(signal, original);
|
|
6305
|
+
const allowed = rules !== null && matchesResearchEntryRules(payload, rules);
|
|
6306
|
+
onDecision?.({
|
|
6307
|
+
mode: policy.mode,
|
|
6308
|
+
allowed,
|
|
6309
|
+
reason: allowed ? "RULES_APPROVED" : "PREDICATE_REJECTED",
|
|
6310
|
+
predicates: rules?.map((rule) => {
|
|
6311
|
+
const value = rule.path.split(".").reduce(
|
|
6312
|
+
(current, key) => current && typeof current === "object" && Object.hasOwn(current, key) ? Reflect.get(current, key) : void 0,
|
|
6313
|
+
payload
|
|
6314
|
+
);
|
|
6315
|
+
return {
|
|
6316
|
+
path: rule.path,
|
|
6317
|
+
actual: typeof value === "number" && Number.isFinite(value) || typeof value === "string" || typeof value === "boolean" ? value : null,
|
|
6318
|
+
passed: matchesResearchEntryRules(payload, [rule])
|
|
6319
|
+
};
|
|
6320
|
+
})
|
|
6321
|
+
});
|
|
6322
|
+
return allowed;
|
|
6323
|
+
};
|
|
6324
|
+
|
|
6216
6325
|
// src/strategy/runtimeDecisionTelemetry.ts
|
|
6217
6326
|
var finitePositive = (value) => {
|
|
6218
6327
|
const parsed = Number(value);
|
|
@@ -6751,6 +6860,12 @@ var createStrategyRuntime = ({
|
|
|
6751
6860
|
const projectConfig = await loadTradejsConfig(projectRoot);
|
|
6752
6861
|
const projectHooks = projectConfig.hooks;
|
|
6753
6862
|
const env = String(config.ENV ?? "BACKTEST");
|
|
6863
|
+
const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
|
|
6864
|
+
const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
|
|
6865
|
+
if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
|
|
6866
|
+
throw new Error(
|
|
6867
|
+
"Research entry policies require a BACKTEST simulation connector"
|
|
6868
|
+
);
|
|
6754
6869
|
const jev = (0, import_jev6.parseJevConfig)(config.JEV);
|
|
6755
6870
|
if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
|
|
6756
6871
|
throw new Error(
|
|
@@ -7461,7 +7576,8 @@ var createStrategyRuntime = ({
|
|
|
7461
7576
|
await enrichSignalWithMarketContextStages({
|
|
7462
7577
|
signal,
|
|
7463
7578
|
env,
|
|
7464
|
-
includeHyperliquidWhales:
|
|
7579
|
+
includeHyperliquidWhales: Boolean(researchEntryPolicy),
|
|
7580
|
+
...researchEntryPolicy ? { coinMarketCapEnabled: true } : {}
|
|
7465
7581
|
});
|
|
7466
7582
|
if (jev) {
|
|
7467
7583
|
await assessSignalWithJev({
|
|
@@ -7559,6 +7675,22 @@ var createStrategyRuntime = ({
|
|
|
7559
7675
|
}
|
|
7560
7676
|
return signal ?? decision.code;
|
|
7561
7677
|
}
|
|
7678
|
+
if (researchEntryPolicy && (!signal || !await evaluateResearchEntryPolicy(
|
|
7679
|
+
researchEntryPolicy,
|
|
7680
|
+
signal,
|
|
7681
|
+
(decision2) => {
|
|
7682
|
+
signal.additionalIndicators = {
|
|
7683
|
+
...signal.additionalIndicators,
|
|
7684
|
+
researchEntryDecision: decision2
|
|
7685
|
+
};
|
|
7686
|
+
}
|
|
7687
|
+
))) {
|
|
7688
|
+
if (signal) {
|
|
7689
|
+
signal.orderStatus = "skipped";
|
|
7690
|
+
signal.orderSkipReason = "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
7691
|
+
}
|
|
7692
|
+
return signal ?? "RESEARCH_ENTRY_POLICY_REJECTED";
|
|
7693
|
+
}
|
|
7562
7694
|
const entryGate = await invokeGateHooks(
|
|
7563
7695
|
"beforeEntryGate",
|
|
7564
7696
|
decisionManifest?.hooks?.beforeEntryGate,
|
package/dist/registry.mjs
CHANGED