@tradejs/node 3.1.28 → 3.1.29-beta.259

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.js CHANGED
@@ -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
+ (_2, 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/backtest.ts
32
662
  var backtest_exports = {};
33
663
  __export(backtest_exports, {
@@ -49,6 +679,259 @@ var import_strategies7 = require("@tradejs/core/strategies");
49
679
  var import_time3 = require("@tradejs/core/time");
50
680
  var import_logger14 = require("@tradejs/infra/logger");
51
681
 
682
+ // src/strategyRuntime.ts
683
+ var import_jev6 = require("@tradejs/core/jev");
684
+
685
+ // src/jev.ts
686
+ var import_promises = __toESM(require("fs/promises"));
687
+ var import_node_path = __toESM(require("path"));
688
+ var import_jev4 = require("@tradejs/core/jev");
689
+ var import_jev5 = require("@tradejs/infra/jev");
690
+ var import_userSettings = require("@tradejs/infra/userSettings");
691
+ init_jevInput();
692
+ init_jevInput();
693
+ init_jevTraining();
694
+ var resolveJevProvider = async (userName) => {
695
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
696
+ const provider = {
697
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
698
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
699
+ };
700
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
701
+ if (!settings.JEV_API_KEY)
702
+ throw new Error("Configure the Jev API key in Account settings");
703
+ return provider;
704
+ };
705
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
706
+ var pending = /* @__PURE__ */ new Map();
707
+ var pendingResponses = /* @__PURE__ */ new Map();
708
+ var evaluateJevInput = async ({
709
+ input,
710
+ config,
711
+ userName,
712
+ projectRoot
713
+ }) => {
714
+ config = (0, import_jev4.parseJevConfig)(config);
715
+ const inputHash = (0, import_jev5.jevHash)(input);
716
+ const questions = questionsForJevInput(input);
717
+ const questionsHash = (0, import_jev5.jevHash)(questions);
718
+ const base = {
719
+ schema: "tradejs-signal-assessment/v5",
720
+ source: config.source,
721
+ mode: config.mode,
722
+ inputHash,
723
+ questionsHash,
724
+ geometryStatus: input.geometryStatus,
725
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
726
+ };
727
+ if (!input.questions.length)
728
+ return {
729
+ assessment: {
730
+ ...base,
731
+ status: "unavailable",
732
+ model: "",
733
+ scores: emptyScores(),
734
+ confidence: emptyScores()
735
+ }
736
+ };
737
+ if (config.source === "local") {
738
+ const contents = await import_promises.default.readFile(
739
+ import_node_path.default.resolve(projectRoot, config.modelFile),
740
+ "utf8"
741
+ );
742
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
743
+ throw new Error("Local Jev gate checksum mismatch");
744
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
745
+ const model = validateJevGateModel2(JSON.parse(contents));
746
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
747
+ throw new Error("Local Jev gate strategy or question version mismatch");
748
+ const scores = Object.fromEntries(
749
+ import_jev4.JEV_DIMENSIONS.map((key) => [
750
+ key,
751
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
752
+ ])
753
+ );
754
+ return {
755
+ assessment: {
756
+ ...base,
757
+ status: "available",
758
+ model: config.modelSha256,
759
+ scores,
760
+ confidence: emptyScores()
761
+ }
762
+ };
763
+ }
764
+ const provider = config.provider ?? await resolveJevProvider(userName);
765
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
766
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
767
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
768
+ const getRecord = async () => {
769
+ const existing = await (0, import_jev5.readJevArtifact)(file);
770
+ if (existing) {
771
+ 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))
772
+ throw new Error("Jev recording identity mismatch");
773
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
774
+ for (const key of import_jev4.JEV_DIMENSIONS) {
775
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / 4 : null;
776
+ if (existing.scores[key] !== expected)
777
+ throw new Error("Jev recording scores do not match the response");
778
+ }
779
+ return existing;
780
+ }
781
+ if (config.source === "recorded")
782
+ throw new Error(
783
+ `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.`
784
+ );
785
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
786
+ if (!settings.JEV_API_KEY)
787
+ throw new Error("Configure the Jev API key in Account settings");
788
+ const apiKey = settings.JEV_API_KEY;
789
+ const configuredProvider = await resolveJevProvider(userName);
790
+ if (configuredProvider.endpoint !== provider.endpoint)
791
+ throw new Error(
792
+ "Jev account endpoint changed; update the frozen provider configuration"
793
+ );
794
+ const started = Date.now();
795
+ const state = stateForJevInput(input);
796
+ const responseId = (0, import_jev5.jevHash)({
797
+ state,
798
+ questionsHash,
799
+ provider,
800
+ user: (0, import_jev5.jevHash)(userName)
801
+ });
802
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
803
+ let responsePromise = pendingResponses.get(responseFile);
804
+ if (!responsePromise) {
805
+ responsePromise = (async () => {
806
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
807
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
808
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
809
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
810
+ return fresh;
811
+ })();
812
+ pendingResponses.set(responseFile, responsePromise);
813
+ }
814
+ let response;
815
+ try {
816
+ response = await responsePromise;
817
+ } finally {
818
+ pendingResponses.delete(responseFile);
819
+ }
820
+ const scores = Object.fromEntries(
821
+ import_jev4.JEV_DIMENSIONS.map((key) => [
822
+ key,
823
+ input.questions.includes(key) ? response.answers[key].score / 4 : null
824
+ ])
825
+ );
826
+ const value = {
827
+ schema: "tradejs-jev-record/v4",
828
+ id,
829
+ inputHash,
830
+ questionsHash,
831
+ provider,
832
+ input,
833
+ response,
834
+ scores,
835
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
836
+ elapsedMs: Date.now() - started
837
+ };
838
+ await (0, import_jev5.writeJevArtifact)(file, value);
839
+ return await (0, import_jev5.readJevArtifact)(file);
840
+ };
841
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
842
+ let promise = pending.get(pendingKey);
843
+ if (!promise) {
844
+ promise = getRecord();
845
+ pending.set(pendingKey, promise);
846
+ }
847
+ let record;
848
+ try {
849
+ record = await promise;
850
+ } finally {
851
+ pending.delete(pendingKey);
852
+ }
853
+ return {
854
+ record,
855
+ assessment: {
856
+ ...base,
857
+ status: "available",
858
+ recordId: id,
859
+ model: record.response.model,
860
+ scores: record.scores,
861
+ confidence: Object.fromEntries(
862
+ import_jev4.JEV_DIMENSIONS.map((key) => [
863
+ key,
864
+ record.response.answers[key]?.confidence ?? null
865
+ ])
866
+ )
867
+ }
868
+ };
869
+ };
870
+ var assessSignalWithJev = async ({
871
+ signal,
872
+ config,
873
+ userName,
874
+ projectRoot,
875
+ strict,
876
+ scope = "runtime"
877
+ }) => {
878
+ let assessment;
879
+ try {
880
+ const input = buildJevInput(signal);
881
+ ({ assessment } = await evaluateJevInput({
882
+ input,
883
+ config,
884
+ userName,
885
+ projectRoot
886
+ }));
887
+ } catch (error) {
888
+ if (strict) throw error;
889
+ assessment = {
890
+ schema: "tradejs-signal-assessment/v5",
891
+ source: config.source,
892
+ mode: config.mode,
893
+ status: "unavailable",
894
+ inputHash: "",
895
+ questionsHash: "",
896
+ geometryStatus: "absent",
897
+ levelsValid: null,
898
+ model: config.provider?.model ?? config.modelSha256 ?? "",
899
+ scores: emptyScores(),
900
+ confidence: emptyScores()
901
+ };
902
+ }
903
+ signal.assessment = assessment;
904
+ signal.additionalIndicators = {
905
+ ...signal.additionalIndicators,
906
+ jev: {
907
+ schema: "tradejs-jev-features/v2",
908
+ status: assessment.status,
909
+ scores: assessment.scores,
910
+ confidence: assessment.confidence
911
+ }
912
+ };
913
+ const identity = {
914
+ signalId: signal.signalId,
915
+ strategy: signal.strategy,
916
+ symbol: signal.symbol,
917
+ timestamp: signal.timestamp,
918
+ scope,
919
+ user: (0, import_jev5.jevHash)(userName),
920
+ config,
921
+ assessment
922
+ };
923
+ await (0, import_jev5.writeJevArtifact)(
924
+ import_node_path.default.resolve(
925
+ projectRoot,
926
+ config.recordsDir ?? "data/ai/jev",
927
+ "decisions",
928
+ `${(0, import_jev5.jevHash)(identity)}.json`
929
+ ),
930
+ identity
931
+ );
932
+ return assessment;
933
+ };
934
+
52
935
  // src/strategyRuntime.ts
53
936
  var import_constants5 = require("@tradejs/core/constants");
54
937
  var import_strategies6 = require("@tradejs/core/strategies");
@@ -1311,8 +2194,11 @@ var toRecord2 = (value) => {
1311
2194
  return value;
1312
2195
  };
1313
2196
  var buildBaseAiPayload = (signal) => {
2197
+ const { jevEvidence: _jevEvidence, ...signalAdditionalIndicators } = toRecord2(
2198
+ signal.additionalIndicators
2199
+ );
1314
2200
  const additionalIndicators = {
1315
- ...toRecord2(signal.additionalIndicators),
2201
+ ...signalAdditionalIndicators,
1316
2202
  marketContext: buildAiMarketContext(signal)
1317
2203
  };
1318
2204
  return {
@@ -1366,7 +2252,7 @@ var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiP
1366
2252
  var import_aiEndpoints = require("@tradejs/core/aiEndpoints");
1367
2253
  var import_aiModels = require("@tradejs/core/aiModels");
1368
2254
  var import_aiLanguages = require("@tradejs/core/aiLanguages");
1369
- var import_userSettings = require("@tradejs/infra/userSettings");
2255
+ var import_userSettings2 = require("@tradejs/infra/userSettings");
1370
2256
  var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
1371
2257
  var userSettingsCache = /* @__PURE__ */ new Map();
1372
2258
  var aiModelCache = /* @__PURE__ */ new Map();
@@ -1414,7 +2300,7 @@ var getOpenRouterModelKwargs = (apiEndpoint) => {
1414
2300
  var getAiUserSettings = async (userName = "root") => {
1415
2301
  let settingsPromise = userSettingsCache.get(userName);
1416
2302
  if (!settingsPromise) {
1417
- settingsPromise = (0, import_userSettings.getUserSettings)(userName).then((settings2) => {
2303
+ settingsPromise = (0, import_userSettings2.getUserSettings)(userName).then((settings2) => {
1418
2304
  const endpoint = (0, import_aiEndpoints.normalizeAiEndpoint)(settings2.AI_API_ENDPOINT);
1419
2305
  return {
1420
2306
  ...settings2,
@@ -3374,7 +4260,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
3374
4260
 
3375
4261
  // src/strategyHelpers/derivativesContext.ts
3376
4262
  var import_indicators2 = require("@tradejs/core/indicators");
3377
- var import_data = require("@tradejs/core/data");
4263
+ var import_data2 = require("@tradejs/core/data");
3378
4264
  var import_strategies3 = require("@tradejs/core/strategies");
3379
4265
  var import_constants2 = require("@tradejs/core/constants");
3380
4266
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
@@ -3534,7 +4420,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
3534
4420
  const referenceSymbols = getDerivativesContextReferenceSymbols();
3535
4421
  const targetSymbol = normalizeSymbol(signal.symbol);
3536
4422
  const lookbackMs = parseLookbackMs();
3537
- const decisionTimeMs = signal.timestamp + (0, import_data.intervalToMs)(signal.interval);
4423
+ const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
3538
4424
  const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
3539
4425
  decisionTimeMs,
3540
4426
  "15m"
@@ -3619,7 +4505,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
3619
4505
  };
3620
4506
 
3621
4507
  // src/strategyHelpers/hyperliquidWhaleContext.ts
3622
- var import_data2 = require("@tradejs/core/data");
4508
+ var import_data3 = require("@tradejs/core/data");
3623
4509
  var import_strategies4 = require("@tradejs/core/strategies");
3624
4510
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
3625
4511
  var import_logger6 = require("@tradejs/infra/logger");
@@ -4093,7 +4979,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
4093
4979
  const symbol = resolveHyperliquidPerpFromSignalSymbol(params.symbol);
4094
4980
  if (!symbol) return null;
4095
4981
  const interval = params.marketInterval ?? signalIntervalToMarketInterval2(params.interval);
4096
- const decisionTimeMs = params.timestamp + (0, import_data2.intervalToMs)(params.interval);
4982
+ const decisionTimeMs = params.timestamp + (0, import_data3.intervalToMs)(params.interval);
4097
4983
  const maxAgeMs = params.maxAgeMs ?? DEFAULT_MAX_AGE_BY_INTERVAL2[interval];
4098
4984
  const universe = getHyperliquidPerpUniverseSnapshot();
4099
4985
  const whales = getHyperliquidWhaleRegistrySnapshot();
@@ -4120,9 +5006,9 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
4120
5006
  abortSignal: params.abortSignal
4121
5007
  });
4122
5008
  } else {
4123
- let pending = contextCache.get(cacheKey);
4124
- if (!pending) {
4125
- pending = (0, import_hyperliquidWhales2.getHyperliquidWhaleFlowAggregate)({
5009
+ let pending2 = contextCache.get(cacheKey);
5010
+ if (!pending2) {
5011
+ pending2 = (0, import_hyperliquidWhales2.getHyperliquidWhaleFlowAggregate)({
4126
5012
  symbol,
4127
5013
  interval,
4128
5014
  decisionTimeMs,
@@ -4131,9 +5017,9 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
4131
5017
  whaleRegistryFingerprint: whales.fingerprint,
4132
5018
  ...params.abortSignal ? { signal: params.abortSignal } : {}
4133
5019
  });
4134
- setBoundedCache2(cacheKey, pending);
5020
+ setBoundedCache2(cacheKey, pending2);
4135
5021
  }
4136
- row = await pending;
5022
+ row = await pending2;
4137
5023
  }
4138
5024
  if (!row) return null;
4139
5025
  return toBaseHyperliquidWhaleFlowContext(row, minimumCoveragePct);
@@ -4694,7 +5580,8 @@ var executeEntryOrder = async ({
4694
5580
  runtimeConfigId: signal.runtimeConfigId,
4695
5581
  strategyRevision: signal.strategyRevision,
4696
5582
  runtimeLineage: signal.runtimeLineage,
4697
- ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
5583
+ ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
5584
+ ...signal.assessment ? { assessment: signal.assessment } : {}
4698
5585
  });
4699
5586
  }
4700
5587
  if (orderPlaced && recordRuntimeTrade && isPositionIncrease) {
@@ -4767,7 +5654,7 @@ var resolveStrategyConfig = async ({
4767
5654
 
4768
5655
  // src/strategy/runtimeBacktestDelay.ts
4769
5656
  var import_constants4 = require("@tradejs/core/constants");
4770
- var import_data3 = require("@tradejs/core/data");
5657
+ var import_data4 = require("@tradejs/core/data");
4771
5658
  var import_strategies5 = require("@tradejs/core/strategies");
4772
5659
  var resolveBacktestEntryDelayBars = (value) => {
4773
5660
  if (value == null || value === "") {
@@ -4795,7 +5682,7 @@ var resolveBacktestExecutionDelayMs = (value, fallbackDelayMs) => {
4795
5682
  };
4796
5683
  var safeIntervalToMs = (interval) => {
4797
5684
  try {
4798
- return (0, import_data3.intervalToMs)(interval);
5685
+ return (0, import_data4.intervalToMs)(interval);
4799
5686
  } catch {
4800
5687
  return null;
4801
5688
  }
@@ -5419,6 +6306,7 @@ var handleExitDecision = async ({
5419
6306
  onRuntimeClose?.({
5420
6307
  userName,
5421
6308
  strategy: strategyName,
6309
+ deploymentId: connector.deploymentId,
5422
6310
  openedByStrategy: trade.strategy,
5423
6311
  symbol,
5424
6312
  direction: trade.direction,
@@ -5711,6 +6599,11 @@ var createStrategyRuntime = ({
5711
6599
  const projectConfig = await loadTradejsConfig(projectRoot);
5712
6600
  const projectHooks = projectConfig.hooks;
5713
6601
  const env = String(config.ENV ?? "BACKTEST");
6602
+ const jev = (0, import_jev6.parseJevConfig)(config.JEV);
6603
+ if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
6604
+ throw new Error(
6605
+ "Runtime JEV requires a frozen provider endpoint and model"
6606
+ );
5714
6607
  const backtestPriceMode = config.BACKTEST_PRICE_MODE ?? "open";
5715
6608
  const backtestEntryDelayBars = env === "BACKTEST" ? resolveBacktestEntryDelayBars(config.BACKTEST_ENTRY_DELAY_BARS) : 0;
5716
6609
  const resolvedBacktestExecutionInterval = config.BACKTEST_EXECUTION_INTERVAL ?? backtestExecutionMarketData?.interval ?? resolveBacktestExecutionIntervalForPrimary(config.INTERVAL ?? "15");
@@ -6131,44 +7024,44 @@ var createStrategyRuntime = ({
6131
7024
  if (pendingBacktestEntry.delayBarsRemaining > 0) {
6132
7025
  return `BACKTEST_ENTRY_DELAY_PENDING:${pendingBacktestEntry.delayBarsRemaining}`;
6133
7026
  }
6134
- const pending = pendingBacktestEntry;
7027
+ const pending2 = pendingBacktestEntry;
6135
7028
  pendingBacktestEntry = null;
6136
7029
  const executionCandleResolution = resolveBacktestExecutionCandle(
6137
7030
  candle,
6138
7031
  btcCandle
6139
7032
  );
6140
7033
  const execution = applyBacktestDelayedEntryExecution({
6141
- decision: pending.decision,
7034
+ decision: pending2.decision,
6142
7035
  execution: executionCandleResolution,
6143
7036
  backtestPriceMode: executionCandleResolution.source === "primary_timeframe" ? "open" : backtestPriceMode,
6144
- delayBars: pending.delayBars
7037
+ delayBars: pending2.delayBars
6145
7038
  });
6146
7039
  if (execution.skipReason) {
6147
- return pending.decision.signal ?? execution.skipReason;
7040
+ return pending2.decision.signal ?? execution.skipReason;
6148
7041
  }
6149
7042
  if (!execution.executionCandle || !execution.btcExecutionCandle) {
6150
- return pending.decision.signal ?? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING";
7043
+ return pending2.decision.signal ?? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING";
6151
7044
  }
6152
7045
  const market = {
6153
7046
  candle: execution.executionCandle,
6154
7047
  btcCandle: execution.btcExecutionCandle
6155
7048
  };
6156
7049
  const entry = buildHookEntry({
6157
- decision: pending.decision,
6158
- runtime: pending.runtime
7050
+ decision: pending2.decision,
7051
+ runtime: pending2.runtime
6159
7052
  });
6160
7053
  return executeEntryDecision({
6161
7054
  connector,
6162
7055
  symbol,
6163
- decision: pending.decision,
6164
- runtime: pending.runtime,
6165
- manifest: pending.manifest,
6166
- hookCtx: pending.hookCtx,
7056
+ decision: pending2.decision,
7057
+ runtime: pending2.runtime,
7058
+ manifest: pending2.manifest,
7059
+ hookCtx: pending2.hookCtx,
6167
7060
  market,
6168
7061
  entry,
6169
- policy: pending.policy,
6170
- ml: pending.ml,
6171
- ai: pending.ai,
7062
+ policy: pending2.policy,
7063
+ ml: pending2.ml,
7064
+ ai: pending2.ai,
6172
7065
  recordRuntimeJournal,
6173
7066
  invokeStageHooks,
6174
7067
  notifyRuntimeError
@@ -6418,6 +7311,16 @@ var createStrategyRuntime = ({
6418
7311
  env,
6419
7312
  includeHyperliquidWhales: false
6420
7313
  });
7314
+ if (jev) {
7315
+ await assessSignalWithJev({
7316
+ signal,
7317
+ config: jev,
7318
+ userName,
7319
+ projectRoot,
7320
+ strict: env === "BACKTEST",
7321
+ scope: runtimeLineage?.deploymentCompositionId ?? (env === "BACKTEST" ? "backtest" : "runtime")
7322
+ });
7323
+ }
6421
7324
  quality = await enrichSignalWithAi({
6422
7325
  signal,
6423
7326
  userName,
@@ -6487,6 +7390,7 @@ var createStrategyRuntime = ({
6487
7390
  if (!shouldMakeOrder) {
6488
7391
  if (signal) {
6489
7392
  const skipReason = getEntrySkipReason({
7393
+ signal,
6490
7394
  makeOrdersEnabled,
6491
7395
  env,
6492
7396
  ml,
@@ -6621,7 +7525,7 @@ var import_tradingAccounts = require("@tradejs/infra/tradingAccounts");
6621
7525
 
6622
7526
  // src/timescaleCachedKline.ts
6623
7527
  var import_constants6 = require("@tradejs/core/constants");
6624
- var import_data4 = require("@tradejs/core/data");
7528
+ var import_data5 = require("@tradejs/core/data");
6625
7529
  var import_time2 = require("@tradejs/core/time");
6626
7530
  var import_async = require("@tradejs/core/async");
6627
7531
  var import_logger11 = require("@tradejs/infra/logger");
@@ -6714,7 +7618,7 @@ var createTimescaleCachedKline = ({
6714
7618
  await options.onPartData(partData);
6715
7619
  }
6716
7620
  if (shouldAccumulate) {
6717
- accumulated = direction === "older" ? (0, import_data4.mergeData)(partData, accumulated) : (0, import_data4.mergeData)(accumulated, partData);
7621
+ accumulated = direction === "older" ? (0, import_data5.mergeData)(partData, accumulated) : (0, import_data5.mergeData)(accumulated, partData);
6718
7622
  }
6719
7623
  const boundaryReached = limitBoundary !== void 0 && (direction === "older" && currentPointer <= limitBoundary || direction === "newer" && currentPointer >= limitBoundary);
6720
7624
  if (partData.length < limit || boundaryReached) {
@@ -7270,9 +8174,9 @@ var loadFundingRates = (connector, symbol, startTime, endTime) => {
7270
8174
  const key = `${symbol.toUpperCase()}:${startTime}:${endTime}`;
7271
8175
  const cached = cache.get(key);
7272
8176
  if (cached) return cached;
7273
- const pending = connector.getFundingRateHistory({ symbol, startTime, endTime }).catch(() => []);
7274
- cache.set(key, pending);
7275
- return pending;
8177
+ const pending2 = connector.getFundingRateHistory({ symbol, startTime, endTime }).catch(() => []);
8178
+ cache.set(key, pending2);
8179
+ return pending2;
7276
8180
  };
7277
8181
  var resolveExecutionCosts = async (params) => {
7278
8182
  const { connector, symbol, config, startTime, endTime, instrument } = params;
@@ -8435,6 +9339,7 @@ var buildReplayEvaluation = ({
8435
9339
  orderStatus: signal.orderStatus,
8436
9340
  orderSkipReason: signal.orderSkipReason,
8437
9341
  aiAnalysis: signal.aiAnalysis ?? null,
9342
+ assessment: signal.assessment,
8438
9343
  ml: signal.ml
8439
9344
  };
8440
9345
  };
@@ -8573,7 +9478,7 @@ var createBacktestSession = async ({
8573
9478
  })
8574
9479
  );
8575
9480
  }
8576
- if (test.ai && signal && typeof signal !== "string" && signal.signalId) {
9481
+ if (test.ai && signal && typeof signal !== "string" && signal.signalId && !(signal.assessment && signal.orderStatus === "skipped")) {
8577
9482
  pendingAiRowBySignalId.set(signal.signalId, {
8578
9483
  signalId: signal.signalId,
8579
9484
  strategyName: signal.strategy || test.strategyName,
@@ -8581,6 +9486,7 @@ var createBacktestSession = async ({
8581
9486
  direction: signal.direction,
8582
9487
  timestamp: signal.timestamp,
8583
9488
  signal: cloneSignal(signal),
9489
+ assessment: signal.assessment,
8584
9490
  testId: test.testId,
8585
9491
  testSuiteId: test.testSuiteId,
8586
9492
  testName: test.name,