@tradejs/node 3.1.28 → 3.1.29-beta.260

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.
@@ -5,6 +5,14 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
15
+ };
8
16
  var __export = (target, all) => {
9
17
  for (var name in all)
10
18
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -28,6 +36,628 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
36
  ));
29
37
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
38
 
39
+ // src/jevInput.ts
40
+ var import_data, import_jev, object, at, finite, fact, buildJevInput, criteria, JEV_QUESTIONS, questionsForJevInput, stateForJevInput;
41
+ var init_jevInput = __esm({
42
+ "src/jevInput.ts"() {
43
+ "use strict";
44
+ import_data = require("@tradejs/core/data");
45
+ import_jev = require("@tradejs/core/jev");
46
+ object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
47
+ at = (value, ...path3) => path3.reduce((current, key) => object(current)[key], value);
48
+ finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
49
+ fact = (value) => {
50
+ if (value == null || typeof value === "boolean") return value ?? null;
51
+ if (typeof value === "number") return finite(value);
52
+ if (typeof value === "string" && value.length <= 60) return value;
53
+ throw new Error("Invalid Jev evidence fact");
54
+ };
55
+ buildJevInput = (signal) => {
56
+ if (!["LONG", "SHORT"].includes(signal.direction ?? "") || !Number.isFinite(signal.timestamp))
57
+ throw new Error("Jev requires a dated directional signal");
58
+ const base = object(signal.additionalIndicators?.baseContext);
59
+ const baseTimestamp = at(base, "candle", "timestamp");
60
+ const baseKnownAt = finite(baseTimestamp);
61
+ const baseProblem = !Object.keys(base).length ? "no_base_context" : baseKnownAt == null ? "missing_timestamp" : baseKnownAt > signal.timestamp ? "future" : signal.timestamp - baseKnownAt > (0, import_data.intervalToMs)(signal.interval) ? "stale" : null;
62
+ const evidence = signal.jevEvidence;
63
+ const features = {};
64
+ const provenance = {};
65
+ const missing = {};
66
+ const put = (key, value, details, missingReason = "missing_value") => {
67
+ const normalized = fact(value);
68
+ if (normalized != null) {
69
+ features[key] = normalized;
70
+ provenance[key] = details;
71
+ } else {
72
+ missing[key] = missingReason;
73
+ }
74
+ };
75
+ const signalFact = {
76
+ knownAt: signal.timestamp,
77
+ scope: "target",
78
+ source: "signal"
79
+ };
80
+ put("signal.direction", signal.direction, signalFact);
81
+ const marketKeys = [
82
+ "market.trendBias",
83
+ "market.swingBias",
84
+ "market.volumeRel20",
85
+ "market.deltaPct",
86
+ "market.fastMaDistanceAtr"
87
+ ];
88
+ if (baseProblem) {
89
+ for (const key of marketKeys) missing[key] = baseProblem;
90
+ } else {
91
+ const marketFact = {
92
+ knownAt: baseKnownAt,
93
+ scope: "target",
94
+ source: "baseContext"
95
+ };
96
+ put("market.trendBias", at(base, "regime", "trend", "bias"), marketFact);
97
+ put("market.swingBias", at(base, "structure", "swing", "bias"), marketFact);
98
+ put(
99
+ "market.volumeRel20",
100
+ at(base, "participation", "volume", "volumeRel20"),
101
+ { ...marketFact, unit: "ratio" }
102
+ );
103
+ const deltaSource = at(base, "participation", "delta", "source");
104
+ put(
105
+ "market.deltaPct",
106
+ at(base, "participation", "delta", "deltaPct"),
107
+ {
108
+ ...marketFact,
109
+ source: typeof deltaSource === "string" ? `baseContext.participation.delta.${deltaSource}` : "baseContext.participation.delta",
110
+ unit: "ratio"
111
+ },
112
+ deltaSource === "ohlcv_proxy" ? "ohlcv_proxy_no_taker_volume" : "missing_value"
113
+ );
114
+ put(
115
+ "market.fastMaDistanceAtr",
116
+ at(base, "regime", "trend", "priceDistanceToMaFastAtr"),
117
+ { ...marketFact, unit: "ATR" }
118
+ );
119
+ }
120
+ const price = finite(signal.prices.currentPrice);
121
+ const stop = finite(signal.prices.stopLossPrice);
122
+ const target = finite(signal.prices.takeProfitPrice);
123
+ const atr = baseProblem ? null : finite(at(base, "raw", "volatility", "atr"));
124
+ if (price == null || stop == null || target == null)
125
+ throw new Error("Invalid Jev signal prices");
126
+ put(
127
+ "signal.validLevels",
128
+ signal.direction === "LONG" ? stop < price && price < target : target < price && price < stop,
129
+ signalFact
130
+ );
131
+ if (atr != null && atr > 0) {
132
+ put("signal.stopDistanceAtr", Math.abs(price - stop) / atr, {
133
+ ...signalFact,
134
+ unit: "ATR"
135
+ });
136
+ put("signal.targetDistanceAtr", Math.abs(price - target) / atr, {
137
+ ...signalFact,
138
+ unit: "ATR"
139
+ });
140
+ } else {
141
+ missing["signal.stopDistanceAtr"] = baseProblem ?? "missing_atr";
142
+ missing["signal.targetDistanceAtr"] = baseProblem ?? "missing_atr";
143
+ }
144
+ let geometryStatus = "absent";
145
+ const figures = signal.figures;
146
+ const groups = [figures?.lines, figures?.points, figures?.zones];
147
+ const points = [
148
+ ...figures?.trendLine?.points ?? [],
149
+ ...figures?.trendLine?.touches ?? [],
150
+ ...groups.flatMap(
151
+ (group) => Array.isArray(group) ? group.flatMap(
152
+ (item) => "points" in item ? item.points : "start" in item ? [item.start, item.end] : []
153
+ ) : []
154
+ )
155
+ ];
156
+ if (points.length > 128 || groups.some(
157
+ (group) => group && (!Array.isArray(group) || group.length > 24)
158
+ ) || points.some(
159
+ (point) => !Number.isFinite(point.timestamp) || !Number.isFinite(point.value) || point.timestamp > signal.timestamp || object(point).knownAt != null && (!Number.isFinite(object(point).knownAt) || Number(object(point).knownAt) > signal.timestamp)
160
+ ))
161
+ geometryStatus = "invalid";
162
+ else if (points.length) geometryStatus = "available";
163
+ if (evidence) {
164
+ if (typeof evidence !== "object" || Array.isArray(evidence) || !evidence.facts || typeof evidence.facts !== "object" || Array.isArray(evidence.facts) || evidence.geometry != null && (typeof evidence.geometry !== "object" || Array.isArray(evidence.geometry)))
165
+ throw new Error("Invalid Jev evidence");
166
+ if (!/^[a-zA-Z0-9._-]{1,40}$/.test(evidence.version) || !Number.isFinite(evidence.knownAt) || evidence.knownAt > signal.timestamp || Object.keys(evidence.facts ?? {}).length > 16 || Object.keys(evidence.geometry ?? {}).length > 16)
167
+ throw new Error("Invalid or oversized Jev evidence");
168
+ const strategyFact = {
169
+ knownAt: evidence.knownAt,
170
+ scope: "strategy",
171
+ source: "strategy.jevEvidence"
172
+ };
173
+ const detailFor = (key) => {
174
+ const detail = evidence.factDetails?.[key] ?? strategyFact;
175
+ if (!Number.isFinite(detail.knownAt) || detail.knownAt > signal.timestamp || !["target", "strategy"].includes(detail.scope) || detail.unit != null && !/^[a-zA-Z0-9%._/-]{1,20}$/.test(detail.unit) || detail.source != null && !/^[a-zA-Z0-9._/-]{1,60}$/.test(detail.source))
176
+ throw new Error("Invalid Jev fact provenance");
177
+ return { ...strategyFact, ...detail };
178
+ };
179
+ put("setup.version", evidence.version, strategyFact);
180
+ for (const [key, value] of Object.entries(evidence.facts)) {
181
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{0,39}$/.test(key) || /^(?:profit|pnl|outcome|label|score|scores|quality|verdict|approval|decision|result)$/i.test(
182
+ key
183
+ ))
184
+ throw new Error("Invalid Jev evidence key");
185
+ put(`setup.${key}`, value, detailFor(`setup.${key}`));
186
+ }
187
+ for (const [key, value] of Object.entries(evidence.geometry ?? {})) {
188
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{0,39}$/.test(key))
189
+ throw new Error("Invalid Jev geometry key");
190
+ put(`geometry.${key}`, value, detailFor(`geometry.${key}`));
191
+ }
192
+ if (Object.keys(evidence.factDetails ?? {}).some(
193
+ (key) => !(key in provenance)
194
+ ))
195
+ throw new Error("Jev provenance references an unavailable fact");
196
+ }
197
+ if (evidence?.geometry && Object.keys(evidence.geometry).length && geometryStatus === "absent")
198
+ geometryStatus = "facts_only";
199
+ if (geometryStatus === "absent") missing["geometry.figures"] = "no_geometry";
200
+ if (geometryStatus === "invalid")
201
+ missing["geometry.figures"] = "invalid_geometry";
202
+ if (Object.keys(features).length > 40 || JSON.stringify(features).length > 4e3)
203
+ throw new Error("Jev input exceeds the microdecision budget");
204
+ const has = (key) => key in features;
205
+ const setupKeys = Object.keys(features).filter(
206
+ (key) => key.startsWith("setup.")
207
+ );
208
+ const questions = import_jev.JEV_DIMENSIONS.filter((dimension) => {
209
+ switch (dimension) {
210
+ case "trend":
211
+ return has("market.trendBias");
212
+ case "swing":
213
+ return has("market.swingBias");
214
+ case "participation":
215
+ return has("market.volumeRel20") || has("market.deltaPct");
216
+ case "setupParticipation":
217
+ return setupKeys.some(
218
+ (key) => /volume|delta|flow|liquidity|turnover/i.test(key)
219
+ );
220
+ case "extension":
221
+ return has("market.fastMaDistanceAtr") || has("setup.breakoutDistanceAtr") || has("signal.stopDistanceAtr");
222
+ case "confirmation":
223
+ return setupKeys.some(
224
+ (key) => /stage|confirm|retest|accept/i.test(key)
225
+ );
226
+ case "setupStrength":
227
+ return setupKeys.some(
228
+ (key) => key !== "setup.version" && !/volume|delta|flow|liquidity|turnover|stage|confirm|retest|accept|breakoutDistance/i.test(
229
+ key
230
+ )
231
+ );
232
+ case "geometry":
233
+ return geometryStatus !== "invalid" && Object.keys(features).some((key) => key.startsWith("geometry."));
234
+ }
235
+ });
236
+ return {
237
+ schema: "tradejs-jev-input/v4",
238
+ strategy: signal.strategy,
239
+ symbol: signal.symbol,
240
+ interval: String(signal.interval),
241
+ timestamp: signal.timestamp,
242
+ direction: signal.direction,
243
+ features,
244
+ provenance,
245
+ missing,
246
+ questions,
247
+ geometryStatus
248
+ };
249
+ };
250
+ criteria = (subject) => [
251
+ `${subject} is clearly contradicted by the supplied facts.`,
252
+ `${subject} has a material conflict.`,
253
+ `${subject} is uncertain from the supplied facts.`,
254
+ `${subject} has adequate support.`,
255
+ `${subject} has strong support without a material conflict.`
256
+ ];
257
+ JEV_QUESTIONS = {
258
+ trend: {
259
+ type: "score",
260
+ instructions: "Does market.trendBias support signal.direction? Judge only the target-asset trend, not swings or setup quality.",
261
+ criteria: criteria("Target-asset trend alignment")
262
+ },
263
+ swing: {
264
+ type: "score",
265
+ instructions: "Does market.swingBias support signal.direction? Judge only the target-asset swing structure, not the broader trend.",
266
+ criteria: criteria("Target-asset swing alignment")
267
+ },
268
+ participation: {
269
+ type: "score",
270
+ instructions: "Does the supplied market.volumeRel20 or market.deltaPct show target-asset participation supportive of this direction? Ignore strategy setup volume and do not infer missing flow.",
271
+ criteria: criteria("Current target-asset participation")
272
+ },
273
+ setupParticipation: {
274
+ type: "score",
275
+ instructions: "Do the strategy-defined setup volume, flow or liquidity facts support the named setup? Use only relevant setup.* facts, not market.volumeRel20; do not infer missing data.",
276
+ criteria: criteria("Participation within the setup")
277
+ },
278
+ extension: {
279
+ type: "score",
280
+ instructions: "Do the supplied ATR-normalized entry-distance facts suggest the entry is not excessively extended? Use only distance facts; do not calculate new ratios or judge confirmation.",
281
+ criteria: criteria("Entry location without excessive extension")
282
+ },
283
+ confirmation: {
284
+ type: "score",
285
+ instructions: "Do the supplied setup stage and confirmation facts indicate a confirmed rather than premature entry? Do not judge distance or predict the outcome.",
286
+ criteria: criteria("Setup confirmation at entry")
287
+ },
288
+ setupStrength: {
289
+ type: "score",
290
+ instructions: "Do the strategy-defined setup strength and quality facts support this candidate? Exclude volume, confirmation, entry distance and geometry; do not assume missing facts.",
291
+ criteria: criteria("Non-geometric setup strength")
292
+ },
293
+ geometry: {
294
+ type: "score",
295
+ instructions: "Do the supplied geometry.* facts support the named setup? Do not infer unseen figure points or certify mathematical validity.",
296
+ criteria: criteria("Setup geometry")
297
+ }
298
+ };
299
+ questionsForJevInput = (input) => Object.fromEntries(input.questions.map((key) => [key, JEV_QUESTIONS[key]]));
300
+ stateForJevInput = (input) => ({
301
+ schema: input.schema,
302
+ strategy: input.strategy,
303
+ direction: input.direction,
304
+ facts: input.features,
305
+ missing: input.missing,
306
+ geometryStatus: input.geometryStatus,
307
+ units: Object.fromEntries(
308
+ Object.entries(input.provenance).filter(([, detail]) => detail.unit != null).map(([key, detail]) => [key, detail.unit])
309
+ ),
310
+ sources: Object.fromEntries(
311
+ Object.entries(input.provenance).filter(([, detail]) => detail.source != null).map(([key, detail]) => [key, detail.source])
312
+ )
313
+ });
314
+ }
315
+ });
316
+
317
+ // src/jevTraining.ts
318
+ var jevTraining_exports = {};
319
+ __export(jevTraining_exports, {
320
+ compareJevGate: () => compareJevGate,
321
+ trainJevGate: () => trainJevGate,
322
+ validateJevGateModel: () => validateJevGateModel,
323
+ validateJevStudy: () => validateJevStudy
324
+ });
325
+ var import_jev2, import_jev3, mean, variance, validateJevStudy, trainTree, validateJevGateModel, predict, economics, compareJevGate, trainJevGate;
326
+ var init_jevTraining = __esm({
327
+ "src/jevTraining.ts"() {
328
+ "use strict";
329
+ import_jev2 = require("@tradejs/core/jev");
330
+ import_jev3 = require("@tradejs/infra/jev");
331
+ init_jevInput();
332
+ mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
333
+ variance = (values) => {
334
+ const average = mean(values);
335
+ return values.reduce((sum, value) => sum + (value - average) ** 2, 0);
336
+ };
337
+ validateJevStudy = (rows) => {
338
+ const unique = /* @__PURE__ */ new Map();
339
+ if (rows.length > 2e4)
340
+ throw new Error(
341
+ "Jev training is bounded to 20000 rows; select a frozen sample first"
342
+ );
343
+ for (const row of rows) {
344
+ const record = row?.record;
345
+ if (row?.schema !== "tradejs-jev-study/v4" || !row.signalId || record?.schema !== "tradejs-jev-record/v4" || record.input?.schema !== "tradejs-jev-input/v4" || !Number.isFinite(record.input.timestamp) || record.inputHash !== (0, import_jev3.jevHash)(record.input) || record.questionsHash !== (0, import_jev3.jevHash)(questionsForJevInput(record.input)) || record.id !== (0, import_jev3.jevHash)({
346
+ inputHash: record.inputHash,
347
+ questionsHash: record.questionsHash,
348
+ provider: record.provider
349
+ }))
350
+ throw new Error("Invalid Jev study provenance");
351
+ (0, import_jev3.validateJevResponse)(record.response, record.input.questions);
352
+ for (const dimension of import_jev2.JEV_DIMENSIONS) {
353
+ const expected = record.input.questions.includes(dimension) ? record.response.answers[dimension].score / 4 : null;
354
+ if (record.scores[dimension] !== expected)
355
+ throw new Error("Jev study scores do not match the teacher response");
356
+ }
357
+ if (row.profit != null && !Number.isFinite(row.profit))
358
+ throw new Error("Invalid study outcome");
359
+ const prior = unique.get(record.inputHash);
360
+ if (prior && (prior.record.id !== record.id || prior.profit !== row.profit || (0, import_jev3.jevHash)(prior.record.response) !== (0, import_jev3.jevHash)(record.response)))
361
+ throw new Error("Conflicting duplicated Jev sample");
362
+ unique.set(record.inputHash, row);
363
+ }
364
+ const ordered = [...unique.values()].sort(
365
+ (a, b) => a.record.input.timestamp - b.record.input.timestamp || a.record.id.localeCompare(b.record.id)
366
+ );
367
+ if (!ordered.length) throw new Error("Empty Jev study");
368
+ const first = ordered[0].record;
369
+ if (ordered.some(
370
+ (row) => row.record.input.strategy !== first.input.strategy || row.record.response.model !== first.response.model || (0, import_jev3.jevHash)(row.record.provider) !== (0, import_jev3.jevHash)(first.provider)
371
+ ))
372
+ throw new Error(
373
+ "Train one strategy and one frozen teacher/provider lineage at a time"
374
+ );
375
+ return ordered;
376
+ };
377
+ trainTree = (rows, dimension, depth, minLeaf) => {
378
+ const values = rows.map((row) => row.record.scores[dimension]);
379
+ const leaf = { value: mean(values), samples: rows.length };
380
+ if (depth === 0 || rows.length < minLeaf * 2 || variance(values) < 1e-8)
381
+ return leaf;
382
+ const keys = [
383
+ ...new Set(rows.flatMap((row) => Object.keys(row.record.input.features)))
384
+ ].sort().filter(
385
+ (key) => new Set(rows.map((row) => row.record.input.features[key])).size > 1
386
+ );
387
+ if (keys.length > 2e3) throw new Error("Too many Jev student features");
388
+ let best;
389
+ let bestLoss = variance(values);
390
+ for (const feature of keys) {
391
+ const observed = [
392
+ ...new Set(
393
+ rows.map((row) => row.record.input.features[feature]).filter((value) => value != null)
394
+ )
395
+ ];
396
+ const numeric = observed.every((value) => typeof value === "number");
397
+ const sorted = numeric ? observed.sort((a, b) => a - b) : observed.sort((a, b) => String(a).localeCompare(String(b)));
398
+ const candidates = sorted.filter(
399
+ (_3, index) => index % Math.max(1, Math.ceil(sorted.length / 12)) === 0
400
+ ).slice(0, 12);
401
+ for (const value of candidates)
402
+ for (const missingLeft of [true, false]) {
403
+ const left = [], right = [];
404
+ for (const row of rows) {
405
+ const input = row.record.input.features[feature];
406
+ const goesLeft = input == null ? missingLeft : numeric ? typeof input === "number" && input <= value : input === value;
407
+ (goesLeft ? left : right).push(row);
408
+ }
409
+ if (left.length < minLeaf || right.length < minLeaf) continue;
410
+ const loss = variance(left.map((row) => row.record.scores[dimension])) + variance(right.map((row) => row.record.scores[dimension]));
411
+ if (loss < bestLoss - 1e-8) {
412
+ bestLoss = loss;
413
+ best = {
414
+ feature,
415
+ operator: numeric ? "lte" : "eq",
416
+ value,
417
+ missingLeft,
418
+ left,
419
+ right
420
+ };
421
+ }
422
+ }
423
+ }
424
+ if (!best) return leaf;
425
+ return {
426
+ feature: best.feature,
427
+ operator: best.operator,
428
+ value: best.value,
429
+ missingLeft: best.missingLeft,
430
+ left: trainTree(best.left, dimension, depth - 1, minLeaf),
431
+ right: trainTree(best.right, dimension, depth - 1, minLeaf)
432
+ };
433
+ };
434
+ validateJevGateModel = (value) => {
435
+ const model = value;
436
+ if (model?.schema !== "tradejs-jev-gate/v2" || model.inputSchema !== "tradejs-jev-input/v4" || typeof model.strategy !== "string" || !model.strategy || !model.teacherModel || !model.trees || model.questionsHash !== (0, import_jev3.jevHash)(JEV_QUESTIONS) || !model.training || !/^[a-f0-9]{64}$/.test(model.training.datasetHash))
437
+ throw new Error("Invalid local Jev gate");
438
+ const { trainEnd, validationEnd, testEnd } = model.training;
439
+ if (![trainEnd, validationEnd, testEnd].every(Number.isFinite) || !(trainEnd < validationEnd && validationEnd < testEnd))
440
+ throw new Error("Invalid local Jev gate partitions");
441
+ const check = (tree, depth) => {
442
+ if (!tree || depth > 8) throw new Error("Invalid local Jev tree");
443
+ if ("samples" in tree) {
444
+ if (!Number.isFinite(tree.value) || tree.value < 0 || tree.value > 1 || !Number.isInteger(tree.samples) || tree.samples < 1)
445
+ throw new Error("Invalid local Jev leaf");
446
+ return;
447
+ }
448
+ if (typeof tree.feature !== "string" || !["lte", "eq"].includes(tree.operator) || typeof tree.missingLeft !== "boolean" || tree.operator === "lte" && !Number.isFinite(tree.value) || !["number", "boolean", "string"].includes(typeof tree.value))
449
+ throw new Error("Invalid local Jev split");
450
+ check(tree.left, depth + 1);
451
+ check(tree.right, depth + 1);
452
+ };
453
+ for (const dimension of import_jev2.JEV_DIMENSIONS) {
454
+ if (model.trees[dimension] != null) check(model.trees[dimension], 0);
455
+ }
456
+ return model;
457
+ };
458
+ predict = (model, row) => Object.fromEntries(
459
+ import_jev2.JEV_DIMENSIONS.map((key) => [
460
+ key,
461
+ model.trees[key] && row.record.input.questions.includes(key) ? (0, import_jev2.predictJevTree)(model.trees[key], row.record.input.features) : null
462
+ ])
463
+ );
464
+ economics = (rows) => {
465
+ const known = rows.filter((row) => row.profit != null);
466
+ const profits = known.map((row) => row.profit);
467
+ const wins = profits.filter((value) => value > 0).reduce((sum, value) => sum + value, 0);
468
+ const losses = -profits.filter((value) => value < 0).reduce((sum, value) => sum + value, 0);
469
+ return {
470
+ rows: rows.length,
471
+ knownOutcomes: known.length,
472
+ pnl: profits.reduce((a, b) => a + b, 0),
473
+ pnlPerTrade: profits.length ? mean(profits) : null,
474
+ profitFactor: losses ? wins / losses : null,
475
+ winRate: profits.length ? profits.filter((value) => value > 0).length / profits.length : null
476
+ };
477
+ };
478
+ compareJevGate = (model, input, minScores = {}) => {
479
+ validateJevGateModel(model);
480
+ const rows = validateJevStudy(input);
481
+ if (rows.some(
482
+ (row) => row.record.input.strategy !== model.strategy || row.record.response.model !== model.teacherModel
483
+ ))
484
+ throw new Error("Model and study lineage do not match");
485
+ const decisions = rows.map((row) => {
486
+ const policy = {
487
+ source: "local",
488
+ mode: "gate",
489
+ minScores
490
+ };
491
+ const scores = predict(model, row);
492
+ const student = (0, import_jev2.decideJev)(
493
+ scores,
494
+ policy,
495
+ row.record.input.geometryStatus,
496
+ row.record.input.features,
497
+ row.record.input.questions
498
+ ).allowed;
499
+ const teacher = (0, import_jev2.decideJev)(
500
+ row.record.scores,
501
+ policy,
502
+ row.record.input.geometryStatus,
503
+ row.record.input.features,
504
+ row.record.input.questions
505
+ ).allowed;
506
+ return { row, scores, student, teacher };
507
+ });
508
+ const dimensionErrors = Object.fromEntries(
509
+ import_jev2.JEV_DIMENSIONS.map((key) => {
510
+ const pairs = decisions.filter(
511
+ (item) => item.row.record.scores[key] != null && item.scores[key] != null
512
+ );
513
+ return [
514
+ key,
515
+ {
516
+ n: pairs.length,
517
+ mae: pairs.length ? mean(
518
+ pairs.map(
519
+ (item) => Math.abs(item.scores[key] - item.row.record.scores[key])
520
+ )
521
+ ) : null
522
+ }
523
+ ];
524
+ })
525
+ );
526
+ const selected = (predicate) => decisions.filter(predicate).map((value) => value.row);
527
+ return {
528
+ rows: rows.length,
529
+ start: rows[0].record.input.timestamp,
530
+ end: rows[rows.length - 1].record.input.timestamp,
531
+ agreement: decisions.filter((item) => item.student === item.teacher).length / rows.length,
532
+ teacherOnly: decisions.filter((item) => item.teacher && !item.student).length,
533
+ studentOnly: decisions.filter((item) => item.student && !item.teacher).length,
534
+ dimensionErrors,
535
+ outcomeScope: "Matched completed outcomes only; this is not a sequential portfolio backtest. Unexecuted candidates have no observed outcome.",
536
+ cohorts: Object.fromEntries(
537
+ ["ALL", "LONG", "SHORT"].map((side) => [
538
+ side,
539
+ {
540
+ core: economics(
541
+ selected(
542
+ (item) => side === "ALL" || item.row.record.input.direction === side
543
+ )
544
+ ),
545
+ baseline: economics(
546
+ selected(
547
+ (item) => item.row.baselineAllowed === true && (side === "ALL" || item.row.record.input.direction === side)
548
+ )
549
+ ),
550
+ jev: economics(
551
+ selected(
552
+ (item) => item.teacher && (side === "ALL" || item.row.record.input.direction === side)
553
+ )
554
+ ),
555
+ local: economics(
556
+ selected(
557
+ (item) => item.student && (side === "ALL" || item.row.record.input.direction === side)
558
+ )
559
+ )
560
+ }
561
+ ])
562
+ )
563
+ };
564
+ };
565
+ trainJevGate = (input, options = {}) => {
566
+ const rows = validateJevStudy(input);
567
+ const maxDepth = options.maxDepth ?? 3, minLeaf = options.minLeaf ?? 10;
568
+ if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 6 || !Number.isInteger(minLeaf) || minLeaf < 2)
569
+ throw new Error("Invalid Jev training limits");
570
+ const timestamps = [
571
+ ...new Set(rows.map((row) => row.record.input.timestamp))
572
+ ];
573
+ if (rows.length < 30 || timestamps.length < 10)
574
+ throw new Error("Need at least 30 distinct samples across 10 signal times");
575
+ const trainEnd = timestamps[Math.floor(timestamps.length * 0.6) - 1];
576
+ const validationEnd = timestamps[Math.floor(timestamps.length * 0.8) - 1];
577
+ const train = rows.filter((row) => row.record.input.timestamp <= trainEnd);
578
+ const validation = rows.filter(
579
+ (row) => row.record.input.timestamp > trainEnd && row.record.input.timestamp <= validationEnd
580
+ );
581
+ const test = rows.filter((row) => row.record.input.timestamp > validationEnd);
582
+ const fit = (samples) => Object.fromEntries(
583
+ import_jev2.JEV_DIMENSIONS.map((key) => {
584
+ const eligible = samples.filter(
585
+ (row) => row.record.scores[key] != null
586
+ );
587
+ return [
588
+ key,
589
+ eligible.length ? trainTree(eligible, key, maxDepth, minLeaf) : null
590
+ ];
591
+ })
592
+ );
593
+ const first = rows[0].record;
594
+ const model = {
595
+ schema: "tradejs-jev-gate/v2",
596
+ inputSchema: "tradejs-jev-input/v4",
597
+ strategy: first.input.strategy,
598
+ questionsHash: (0, import_jev3.jevHash)(JEV_QUESTIONS),
599
+ teacherModel: first.response.model,
600
+ trees: fit(train),
601
+ training: {
602
+ datasetHash: (0, import_jev3.jevHash)(
603
+ rows.map((row) => ({
604
+ id: row.record.id,
605
+ response: row.record.response
606
+ }))
607
+ ),
608
+ trainEnd,
609
+ validationEnd,
610
+ testEnd: timestamps[timestamps.length - 1],
611
+ maxDepth,
612
+ minLeaf
613
+ }
614
+ };
615
+ validateJevGateModel(model);
616
+ const walkForward = [0.4, 0.6, 0.8].map((fraction) => {
617
+ const trainThrough = timestamps[Math.max(0, Math.floor(timestamps.length * (fraction - 0.1)) - 1)];
618
+ const validationThrough = timestamps[Math.floor(timestamps.length * fraction) - 1];
619
+ const testThrough = timestamps[Math.min(
620
+ timestamps.length - 1,
621
+ Math.round(timestamps.length * (fraction + 0.2)) - 1
622
+ )];
623
+ const fitting = rows.filter(
624
+ (row) => row.record.input.timestamp <= trainThrough
625
+ );
626
+ const evaluation = rows.filter(
627
+ (row) => row.record.input.timestamp > validationThrough && row.record.input.timestamp <= testThrough
628
+ );
629
+ const fold = {
630
+ ...model,
631
+ trees: fit(fitting),
632
+ training: {
633
+ ...model.training,
634
+ trainEnd: trainThrough,
635
+ validationEnd: validationThrough,
636
+ testEnd: testThrough
637
+ }
638
+ };
639
+ return {
640
+ trainThrough,
641
+ validationThrough,
642
+ testThrough,
643
+ trainRows: fitting.length,
644
+ test: compareJevGate(fold, evaluation)
645
+ };
646
+ });
647
+ return {
648
+ model,
649
+ report: {
650
+ train: compareJevGate(model, train),
651
+ validation: compareJevGate(model, validation),
652
+ test: compareJevGate(model, test),
653
+ walkForward,
654
+ promotion: "research-only; validate a sequential backtest and prospective signals before runtime admission"
655
+ }
656
+ };
657
+ };
658
+ }
659
+ });
660
+
31
661
  // src/strategies.ts
32
662
  var strategies_exports = {};
33
663
  __export(strategies_exports, {
@@ -1395,8 +2025,11 @@ var toRecord2 = (value) => {
1395
2025
  return value;
1396
2026
  };
1397
2027
  var buildBaseAiPayload = (signal) => {
2028
+ const { jevEvidence: _jevEvidence, ...signalAdditionalIndicators } = toRecord2(
2029
+ signal.additionalIndicators
2030
+ );
1398
2031
  const additionalIndicators = {
1399
- ...toRecord2(signal.additionalIndicators),
2032
+ ...signalAdditionalIndicators,
1400
2033
  marketContext: buildAiMarketContext(signal)
1401
2034
  };
1402
2035
  return {
@@ -1906,6 +2539,259 @@ var askAI = async (signal, options = {}) => {
1906
2539
  return content;
1907
2540
  };
1908
2541
 
2542
+ // src/strategyRuntime.ts
2543
+ var import_jev6 = require("@tradejs/core/jev");
2544
+
2545
+ // src/jev.ts
2546
+ var import_promises = __toESM(require("fs/promises"));
2547
+ var import_node_path = __toESM(require("path"));
2548
+ var import_jev4 = require("@tradejs/core/jev");
2549
+ var import_jev5 = require("@tradejs/infra/jev");
2550
+ var import_userSettings2 = require("@tradejs/infra/userSettings");
2551
+ init_jevInput();
2552
+ init_jevInput();
2553
+ init_jevTraining();
2554
+ var resolveJevProvider = async (userName) => {
2555
+ const settings = await (0, import_userSettings2.getUserSettings)(userName);
2556
+ const provider = {
2557
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
2558
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
2559
+ };
2560
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
2561
+ if (!settings.JEV_API_KEY)
2562
+ throw new Error("Configure the Jev API key in Account settings");
2563
+ return provider;
2564
+ };
2565
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
2566
+ var pending = /* @__PURE__ */ new Map();
2567
+ var pendingResponses = /* @__PURE__ */ new Map();
2568
+ var evaluateJevInput = async ({
2569
+ input,
2570
+ config,
2571
+ userName,
2572
+ projectRoot
2573
+ }) => {
2574
+ config = (0, import_jev4.parseJevConfig)(config);
2575
+ const inputHash = (0, import_jev5.jevHash)(input);
2576
+ const questions = questionsForJevInput(input);
2577
+ const questionsHash = (0, import_jev5.jevHash)(questions);
2578
+ const base = {
2579
+ schema: "tradejs-signal-assessment/v5",
2580
+ source: config.source,
2581
+ mode: config.mode,
2582
+ inputHash,
2583
+ questionsHash,
2584
+ geometryStatus: input.geometryStatus,
2585
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
2586
+ };
2587
+ if (!input.questions.length)
2588
+ return {
2589
+ assessment: {
2590
+ ...base,
2591
+ status: "unavailable",
2592
+ model: "",
2593
+ scores: emptyScores(),
2594
+ confidence: emptyScores()
2595
+ }
2596
+ };
2597
+ if (config.source === "local") {
2598
+ const contents = await import_promises.default.readFile(
2599
+ import_node_path.default.resolve(projectRoot, config.modelFile),
2600
+ "utf8"
2601
+ );
2602
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
2603
+ throw new Error("Local Jev gate checksum mismatch");
2604
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
2605
+ const model = validateJevGateModel2(JSON.parse(contents));
2606
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
2607
+ throw new Error("Local Jev gate strategy or question version mismatch");
2608
+ const scores = Object.fromEntries(
2609
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2610
+ key,
2611
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
2612
+ ])
2613
+ );
2614
+ return {
2615
+ assessment: {
2616
+ ...base,
2617
+ status: "available",
2618
+ model: config.modelSha256,
2619
+ scores,
2620
+ confidence: emptyScores()
2621
+ }
2622
+ };
2623
+ }
2624
+ const provider = config.provider ?? await resolveJevProvider(userName);
2625
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
2626
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
2627
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
2628
+ const getRecord = async () => {
2629
+ const existing = await (0, import_jev5.readJevArtifact)(file);
2630
+ if (existing) {
2631
+ if (existing.schema !== "tradejs-jev-record/v4" || existing.id !== id || existing.inputHash !== inputHash || existing.questionsHash !== questionsHash || (0, import_jev5.jevHash)(existing.input) !== inputHash || (0, import_jev5.jevHash)(existing.provider) !== (0, import_jev5.jevHash)(provider))
2632
+ throw new Error("Jev recording identity mismatch");
2633
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
2634
+ for (const key of import_jev4.JEV_DIMENSIONS) {
2635
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / 4 : null;
2636
+ if (existing.scores[key] !== expected)
2637
+ throw new Error("Jev recording scores do not match the response");
2638
+ }
2639
+ return existing;
2640
+ }
2641
+ if (config.source === "recorded")
2642
+ throw new Error(
2643
+ `Missing Jev recording ${id} for ${input.strategy} ${input.symbol} at ${new Date(input.timestamp).toISOString()}. Recorded mode requires the same signal-time facts and provider; a shorter backtest can change warm-up indicators or prior trade state.`
2644
+ );
2645
+ const settings = await (0, import_userSettings2.getUserSettings)(userName);
2646
+ if (!settings.JEV_API_KEY)
2647
+ throw new Error("Configure the Jev API key in Account settings");
2648
+ const apiKey = settings.JEV_API_KEY;
2649
+ const configuredProvider = await resolveJevProvider(userName);
2650
+ if (configuredProvider.endpoint !== provider.endpoint)
2651
+ throw new Error(
2652
+ "Jev account endpoint changed; update the frozen provider configuration"
2653
+ );
2654
+ const started = Date.now();
2655
+ const state = stateForJevInput(input);
2656
+ const responseId = (0, import_jev5.jevHash)({
2657
+ state,
2658
+ questionsHash,
2659
+ provider,
2660
+ user: (0, import_jev5.jevHash)(userName)
2661
+ });
2662
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
2663
+ let responsePromise = pendingResponses.get(responseFile);
2664
+ if (!responsePromise) {
2665
+ responsePromise = (async () => {
2666
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
2667
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
2668
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
2669
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
2670
+ return fresh;
2671
+ })();
2672
+ pendingResponses.set(responseFile, responsePromise);
2673
+ }
2674
+ let response;
2675
+ try {
2676
+ response = await responsePromise;
2677
+ } finally {
2678
+ pendingResponses.delete(responseFile);
2679
+ }
2680
+ const scores = Object.fromEntries(
2681
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2682
+ key,
2683
+ input.questions.includes(key) ? response.answers[key].score / 4 : null
2684
+ ])
2685
+ );
2686
+ const value = {
2687
+ schema: "tradejs-jev-record/v4",
2688
+ id,
2689
+ inputHash,
2690
+ questionsHash,
2691
+ provider,
2692
+ input,
2693
+ response,
2694
+ scores,
2695
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2696
+ elapsedMs: Date.now() - started
2697
+ };
2698
+ await (0, import_jev5.writeJevArtifact)(file, value);
2699
+ return await (0, import_jev5.readJevArtifact)(file);
2700
+ };
2701
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
2702
+ let promise = pending.get(pendingKey);
2703
+ if (!promise) {
2704
+ promise = getRecord();
2705
+ pending.set(pendingKey, promise);
2706
+ }
2707
+ let record;
2708
+ try {
2709
+ record = await promise;
2710
+ } finally {
2711
+ pending.delete(pendingKey);
2712
+ }
2713
+ return {
2714
+ record,
2715
+ assessment: {
2716
+ ...base,
2717
+ status: "available",
2718
+ recordId: id,
2719
+ model: record.response.model,
2720
+ scores: record.scores,
2721
+ confidence: Object.fromEntries(
2722
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2723
+ key,
2724
+ record.response.answers[key]?.confidence ?? null
2725
+ ])
2726
+ )
2727
+ }
2728
+ };
2729
+ };
2730
+ var assessSignalWithJev = async ({
2731
+ signal,
2732
+ config,
2733
+ userName,
2734
+ projectRoot,
2735
+ strict,
2736
+ scope = "runtime"
2737
+ }) => {
2738
+ let assessment;
2739
+ try {
2740
+ const input = buildJevInput(signal);
2741
+ ({ assessment } = await evaluateJevInput({
2742
+ input,
2743
+ config,
2744
+ userName,
2745
+ projectRoot
2746
+ }));
2747
+ } catch (error) {
2748
+ if (strict) throw error;
2749
+ assessment = {
2750
+ schema: "tradejs-signal-assessment/v5",
2751
+ source: config.source,
2752
+ mode: config.mode,
2753
+ status: "unavailable",
2754
+ inputHash: "",
2755
+ questionsHash: "",
2756
+ geometryStatus: "absent",
2757
+ levelsValid: null,
2758
+ model: config.provider?.model ?? config.modelSha256 ?? "",
2759
+ scores: emptyScores(),
2760
+ confidence: emptyScores()
2761
+ };
2762
+ }
2763
+ signal.assessment = assessment;
2764
+ signal.additionalIndicators = {
2765
+ ...signal.additionalIndicators,
2766
+ jev: {
2767
+ schema: "tradejs-jev-features/v2",
2768
+ status: assessment.status,
2769
+ scores: assessment.scores,
2770
+ confidence: assessment.confidence
2771
+ }
2772
+ };
2773
+ const identity = {
2774
+ signalId: signal.signalId,
2775
+ strategy: signal.strategy,
2776
+ symbol: signal.symbol,
2777
+ timestamp: signal.timestamp,
2778
+ scope,
2779
+ user: (0, import_jev5.jevHash)(userName),
2780
+ config,
2781
+ assessment
2782
+ };
2783
+ await (0, import_jev5.writeJevArtifact)(
2784
+ import_node_path.default.resolve(
2785
+ projectRoot,
2786
+ config.recordsDir ?? "data/ai/jev",
2787
+ "decisions",
2788
+ `${(0, import_jev5.jevHash)(identity)}.json`
2789
+ ),
2790
+ identity
2791
+ );
2792
+ return assessment;
2793
+ };
2794
+
1909
2795
  // src/strategyRuntime.ts
1910
2796
  var import_constants5 = require("@tradejs/core/constants");
1911
2797
  var import_strategies6 = require("@tradejs/core/strategies");
@@ -3636,7 +4522,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
3636
4522
 
3637
4523
  // src/strategyHelpers/derivativesContext.ts
3638
4524
  var import_indicators2 = require("@tradejs/core/indicators");
3639
- var import_data = require("@tradejs/core/data");
4525
+ var import_data2 = require("@tradejs/core/data");
3640
4526
  var import_strategies3 = require("@tradejs/core/strategies");
3641
4527
  var import_constants2 = require("@tradejs/core/constants");
3642
4528
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
@@ -3796,7 +4682,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
3796
4682
  const referenceSymbols = getDerivativesContextReferenceSymbols();
3797
4683
  const targetSymbol = normalizeSymbol(signal.symbol);
3798
4684
  const lookbackMs = parseLookbackMs();
3799
- const decisionTimeMs = signal.timestamp + (0, import_data.intervalToMs)(signal.interval);
4685
+ const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
3800
4686
  const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
3801
4687
  decisionTimeMs,
3802
4688
  "15m"
@@ -3881,7 +4767,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
3881
4767
  };
3882
4768
 
3883
4769
  // src/strategyHelpers/hyperliquidWhaleContext.ts
3884
- var import_data2 = require("@tradejs/core/data");
4770
+ var import_data3 = require("@tradejs/core/data");
3885
4771
  var import_strategies4 = require("@tradejs/core/strategies");
3886
4772
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
3887
4773
  var import_logger6 = require("@tradejs/infra/logger");
@@ -4359,7 +5245,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
4359
5245
  const symbol = resolveHyperliquidPerpFromSignalSymbol(params.symbol);
4360
5246
  if (!symbol) return null;
4361
5247
  const interval = params.marketInterval ?? signalIntervalToMarketInterval2(params.interval);
4362
- const decisionTimeMs = params.timestamp + (0, import_data2.intervalToMs)(params.interval);
5248
+ const decisionTimeMs = params.timestamp + (0, import_data3.intervalToMs)(params.interval);
4363
5249
  const maxAgeMs = params.maxAgeMs ?? DEFAULT_MAX_AGE_BY_INTERVAL2[interval];
4364
5250
  const universe = getHyperliquidPerpUniverseSnapshot();
4365
5251
  const whales = getHyperliquidWhaleRegistrySnapshot();
@@ -4386,9 +5272,9 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
4386
5272
  abortSignal: params.abortSignal
4387
5273
  });
4388
5274
  } else {
4389
- let pending = contextCache.get(cacheKey);
4390
- if (!pending) {
4391
- pending = (0, import_hyperliquidWhales2.getHyperliquidWhaleFlowAggregate)({
5275
+ let pending2 = contextCache.get(cacheKey);
5276
+ if (!pending2) {
5277
+ pending2 = (0, import_hyperliquidWhales2.getHyperliquidWhaleFlowAggregate)({
4392
5278
  symbol,
4393
5279
  interval,
4394
5280
  decisionTimeMs,
@@ -4397,9 +5283,9 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
4397
5283
  whaleRegistryFingerprint: whales.fingerprint,
4398
5284
  ...params.abortSignal ? { signal: params.abortSignal } : {}
4399
5285
  });
4400
- setBoundedCache2(cacheKey, pending);
5286
+ setBoundedCache2(cacheKey, pending2);
4401
5287
  }
4402
- row = await pending;
5288
+ row = await pending2;
4403
5289
  }
4404
5290
  if (!row) return null;
4405
5291
  return toBaseHyperliquidWhaleFlowContext(row, minimumCoveragePct);
@@ -4973,7 +5859,8 @@ var executeEntryOrder = async ({
4973
5859
  runtimeConfigId: signal.runtimeConfigId,
4974
5860
  strategyRevision: signal.strategyRevision,
4975
5861
  runtimeLineage: signal.runtimeLineage,
4976
- ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
5862
+ ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
5863
+ ...signal.assessment ? { assessment: signal.assessment } : {}
4977
5864
  });
4978
5865
  }
4979
5866
  if (orderPlaced && recordRuntimeTrade && isPositionIncrease) {
@@ -5046,7 +5933,7 @@ var resolveStrategyConfig = async ({
5046
5933
 
5047
5934
  // src/strategy/runtimeBacktestDelay.ts
5048
5935
  var import_constants4 = require("@tradejs/core/constants");
5049
- var import_data3 = require("@tradejs/core/data");
5936
+ var import_data4 = require("@tradejs/core/data");
5050
5937
  var import_strategies5 = require("@tradejs/core/strategies");
5051
5938
  var resolveBacktestEntryDelayBars = (value) => {
5052
5939
  if (value == null || value === "") {
@@ -5074,7 +5961,7 @@ var resolveBacktestExecutionDelayMs = (value, fallbackDelayMs) => {
5074
5961
  };
5075
5962
  var safeIntervalToMs = (interval) => {
5076
5963
  try {
5077
- return (0, import_data3.intervalToMs)(interval);
5964
+ return (0, import_data4.intervalToMs)(interval);
5078
5965
  } catch {
5079
5966
  return null;
5080
5967
  }
@@ -5698,6 +6585,7 @@ var handleExitDecision = async ({
5698
6585
  onRuntimeClose?.({
5699
6586
  userName,
5700
6587
  strategy: strategyName,
6588
+ deploymentId: connector.deploymentId,
5701
6589
  openedByStrategy: trade.strategy,
5702
6590
  symbol,
5703
6591
  direction: trade.direction,
@@ -5990,6 +6878,11 @@ var createStrategyRuntime = ({
5990
6878
  const projectConfig = await loadTradejsConfig(projectRoot);
5991
6879
  const projectHooks = projectConfig.hooks;
5992
6880
  const env = String(config.ENV ?? "BACKTEST");
6881
+ const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6882
+ if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6883
+ throw new Error(
6884
+ "Runtime JEV requires a frozen provider endpoint and model"
6885
+ );
5993
6886
  const backtestPriceMode = config.BACKTEST_PRICE_MODE ?? "open";
5994
6887
  const backtestEntryDelayBars = env === "BACKTEST" ? resolveBacktestEntryDelayBars(config.BACKTEST_ENTRY_DELAY_BARS) : 0;
5995
6888
  const resolvedBacktestExecutionInterval = config.BACKTEST_EXECUTION_INTERVAL ?? backtestExecutionMarketData?.interval ?? resolveBacktestExecutionIntervalForPrimary(config.INTERVAL ?? "15");
@@ -6410,44 +7303,44 @@ var createStrategyRuntime = ({
6410
7303
  if (pendingBacktestEntry.delayBarsRemaining > 0) {
6411
7304
  return `BACKTEST_ENTRY_DELAY_PENDING:${pendingBacktestEntry.delayBarsRemaining}`;
6412
7305
  }
6413
- const pending = pendingBacktestEntry;
7306
+ const pending2 = pendingBacktestEntry;
6414
7307
  pendingBacktestEntry = null;
6415
7308
  const executionCandleResolution = resolveBacktestExecutionCandle(
6416
7309
  candle,
6417
7310
  btcCandle
6418
7311
  );
6419
7312
  const execution = applyBacktestDelayedEntryExecution({
6420
- decision: pending.decision,
7313
+ decision: pending2.decision,
6421
7314
  execution: executionCandleResolution,
6422
7315
  backtestPriceMode: executionCandleResolution.source === "primary_timeframe" ? "open" : backtestPriceMode,
6423
- delayBars: pending.delayBars
7316
+ delayBars: pending2.delayBars
6424
7317
  });
6425
7318
  if (execution.skipReason) {
6426
- return pending.decision.signal ?? execution.skipReason;
7319
+ return pending2.decision.signal ?? execution.skipReason;
6427
7320
  }
6428
7321
  if (!execution.executionCandle || !execution.btcExecutionCandle) {
6429
- return pending.decision.signal ?? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING";
7322
+ return pending2.decision.signal ?? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING";
6430
7323
  }
6431
7324
  const market = {
6432
7325
  candle: execution.executionCandle,
6433
7326
  btcCandle: execution.btcExecutionCandle
6434
7327
  };
6435
7328
  const entry = buildHookEntry({
6436
- decision: pending.decision,
6437
- runtime: pending.runtime
7329
+ decision: pending2.decision,
7330
+ runtime: pending2.runtime
6438
7331
  });
6439
7332
  return executeEntryDecision({
6440
7333
  connector,
6441
7334
  symbol,
6442
- decision: pending.decision,
6443
- runtime: pending.runtime,
6444
- manifest: pending.manifest,
6445
- hookCtx: pending.hookCtx,
7335
+ decision: pending2.decision,
7336
+ runtime: pending2.runtime,
7337
+ manifest: pending2.manifest,
7338
+ hookCtx: pending2.hookCtx,
6446
7339
  market,
6447
7340
  entry,
6448
- policy: pending.policy,
6449
- ml: pending.ml,
6450
- ai: pending.ai,
7341
+ policy: pending2.policy,
7342
+ ml: pending2.ml,
7343
+ ai: pending2.ai,
6451
7344
  recordRuntimeJournal,
6452
7345
  invokeStageHooks,
6453
7346
  notifyRuntimeError
@@ -6697,6 +7590,16 @@ var createStrategyRuntime = ({
6697
7590
  env,
6698
7591
  includeHyperliquidWhales: false
6699
7592
  });
7593
+ if (jev) {
7594
+ await assessSignalWithJev({
7595
+ signal,
7596
+ config: jev,
7597
+ userName,
7598
+ projectRoot,
7599
+ strict: env === "BACKTEST",
7600
+ scope: runtimeLineage?.deploymentCompositionId ?? (env === "BACKTEST" ? "backtest" : "runtime")
7601
+ });
7602
+ }
6700
7603
  quality = await enrichSignalWithAi({
6701
7604
  signal,
6702
7605
  userName,
@@ -6766,6 +7669,7 @@ var createStrategyRuntime = ({
6766
7669
  if (!shouldMakeOrder) {
6767
7670
  if (signal) {
6768
7671
  const skipReason = getEntrySkipReason({
7672
+ signal,
6769
7673
  makeOrdersEnabled,
6770
7674
  env,
6771
7675
  ml,