@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.
@@ -0,0 +1,614 @@
1
+ // src/jevTraining.ts
2
+ import { JEV_DIMENSIONS as JEV_DIMENSIONS2, decideJev, predictJevTree } from "@tradejs/core/jev";
3
+ import { jevHash, validateJevResponse } from "@tradejs/infra/jev";
4
+
5
+ // src/jevInput.ts
6
+ import { intervalToMs } from "@tradejs/core/data";
7
+ import { JEV_DIMENSIONS } from "@tradejs/core/jev";
8
+ var object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
9
+ var at = (value, ...path) => path.reduce((current, key) => object(current)[key], value);
10
+ var finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
11
+ var fact = (value) => {
12
+ if (value == null || typeof value === "boolean") return value ?? null;
13
+ if (typeof value === "number") return finite(value);
14
+ if (typeof value === "string" && value.length <= 60) return value;
15
+ throw new Error("Invalid Jev evidence fact");
16
+ };
17
+ var buildJevInput = (signal) => {
18
+ if (!["LONG", "SHORT"].includes(signal.direction ?? "") || !Number.isFinite(signal.timestamp))
19
+ throw new Error("Jev requires a dated directional signal");
20
+ const base = object(signal.additionalIndicators?.baseContext);
21
+ const baseTimestamp = at(base, "candle", "timestamp");
22
+ const baseKnownAt = finite(baseTimestamp);
23
+ const baseProblem = !Object.keys(base).length ? "no_base_context" : baseKnownAt == null ? "missing_timestamp" : baseKnownAt > signal.timestamp ? "future" : signal.timestamp - baseKnownAt > intervalToMs(signal.interval) ? "stale" : null;
24
+ const evidence = signal.jevEvidence;
25
+ const features = {};
26
+ const provenance = {};
27
+ const missing = {};
28
+ const put = (key, value, details, missingReason = "missing_value") => {
29
+ const normalized = fact(value);
30
+ if (normalized != null) {
31
+ features[key] = normalized;
32
+ provenance[key] = details;
33
+ } else {
34
+ missing[key] = missingReason;
35
+ }
36
+ };
37
+ const signalFact = {
38
+ knownAt: signal.timestamp,
39
+ scope: "target",
40
+ source: "signal"
41
+ };
42
+ put("signal.direction", signal.direction, signalFact);
43
+ const marketKeys = [
44
+ "market.trendBias",
45
+ "market.swingBias",
46
+ "market.volumeRel20",
47
+ "market.deltaPct",
48
+ "market.fastMaDistanceAtr"
49
+ ];
50
+ if (baseProblem) {
51
+ for (const key of marketKeys) missing[key] = baseProblem;
52
+ } else {
53
+ const marketFact = {
54
+ knownAt: baseKnownAt,
55
+ scope: "target",
56
+ source: "baseContext"
57
+ };
58
+ put("market.trendBias", at(base, "regime", "trend", "bias"), marketFact);
59
+ put("market.swingBias", at(base, "structure", "swing", "bias"), marketFact);
60
+ put(
61
+ "market.volumeRel20",
62
+ at(base, "participation", "volume", "volumeRel20"),
63
+ { ...marketFact, unit: "ratio" }
64
+ );
65
+ const deltaSource = at(base, "participation", "delta", "source");
66
+ put(
67
+ "market.deltaPct",
68
+ at(base, "participation", "delta", "deltaPct"),
69
+ {
70
+ ...marketFact,
71
+ source: typeof deltaSource === "string" ? `baseContext.participation.delta.${deltaSource}` : "baseContext.participation.delta",
72
+ unit: "ratio"
73
+ },
74
+ deltaSource === "ohlcv_proxy" ? "ohlcv_proxy_no_taker_volume" : "missing_value"
75
+ );
76
+ put(
77
+ "market.fastMaDistanceAtr",
78
+ at(base, "regime", "trend", "priceDistanceToMaFastAtr"),
79
+ { ...marketFact, unit: "ATR" }
80
+ );
81
+ }
82
+ const price = finite(signal.prices.currentPrice);
83
+ const stop = finite(signal.prices.stopLossPrice);
84
+ const target = finite(signal.prices.takeProfitPrice);
85
+ const atr = baseProblem ? null : finite(at(base, "raw", "volatility", "atr"));
86
+ if (price == null || stop == null || target == null)
87
+ throw new Error("Invalid Jev signal prices");
88
+ put(
89
+ "signal.validLevels",
90
+ signal.direction === "LONG" ? stop < price && price < target : target < price && price < stop,
91
+ signalFact
92
+ );
93
+ if (atr != null && atr > 0) {
94
+ put("signal.stopDistanceAtr", Math.abs(price - stop) / atr, {
95
+ ...signalFact,
96
+ unit: "ATR"
97
+ });
98
+ put("signal.targetDistanceAtr", Math.abs(price - target) / atr, {
99
+ ...signalFact,
100
+ unit: "ATR"
101
+ });
102
+ } else {
103
+ missing["signal.stopDistanceAtr"] = baseProblem ?? "missing_atr";
104
+ missing["signal.targetDistanceAtr"] = baseProblem ?? "missing_atr";
105
+ }
106
+ let geometryStatus = "absent";
107
+ const figures = signal.figures;
108
+ const groups = [figures?.lines, figures?.points, figures?.zones];
109
+ const points = [
110
+ ...figures?.trendLine?.points ?? [],
111
+ ...figures?.trendLine?.touches ?? [],
112
+ ...groups.flatMap(
113
+ (group) => Array.isArray(group) ? group.flatMap(
114
+ (item) => "points" in item ? item.points : "start" in item ? [item.start, item.end] : []
115
+ ) : []
116
+ )
117
+ ];
118
+ if (points.length > 128 || groups.some(
119
+ (group) => group && (!Array.isArray(group) || group.length > 24)
120
+ ) || points.some(
121
+ (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)
122
+ ))
123
+ geometryStatus = "invalid";
124
+ else if (points.length) geometryStatus = "available";
125
+ if (evidence) {
126
+ 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)))
127
+ throw new Error("Invalid Jev evidence");
128
+ 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)
129
+ throw new Error("Invalid or oversized Jev evidence");
130
+ const strategyFact = {
131
+ knownAt: evidence.knownAt,
132
+ scope: "strategy",
133
+ source: "strategy.jevEvidence"
134
+ };
135
+ const detailFor = (key) => {
136
+ const detail = evidence.factDetails?.[key] ?? strategyFact;
137
+ 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))
138
+ throw new Error("Invalid Jev fact provenance");
139
+ return { ...strategyFact, ...detail };
140
+ };
141
+ put("setup.version", evidence.version, strategyFact);
142
+ for (const [key, value] of Object.entries(evidence.facts)) {
143
+ 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(
144
+ key
145
+ ))
146
+ throw new Error("Invalid Jev evidence key");
147
+ put(`setup.${key}`, value, detailFor(`setup.${key}`));
148
+ }
149
+ for (const [key, value] of Object.entries(evidence.geometry ?? {})) {
150
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{0,39}$/.test(key))
151
+ throw new Error("Invalid Jev geometry key");
152
+ put(`geometry.${key}`, value, detailFor(`geometry.${key}`));
153
+ }
154
+ if (Object.keys(evidence.factDetails ?? {}).some(
155
+ (key) => !(key in provenance)
156
+ ))
157
+ throw new Error("Jev provenance references an unavailable fact");
158
+ }
159
+ if (evidence?.geometry && Object.keys(evidence.geometry).length && geometryStatus === "absent")
160
+ geometryStatus = "facts_only";
161
+ if (geometryStatus === "absent") missing["geometry.figures"] = "no_geometry";
162
+ if (geometryStatus === "invalid")
163
+ missing["geometry.figures"] = "invalid_geometry";
164
+ if (Object.keys(features).length > 40 || JSON.stringify(features).length > 4e3)
165
+ throw new Error("Jev input exceeds the microdecision budget");
166
+ const has = (key) => key in features;
167
+ const setupKeys = Object.keys(features).filter(
168
+ (key) => key.startsWith("setup.")
169
+ );
170
+ const questions = JEV_DIMENSIONS.filter((dimension) => {
171
+ switch (dimension) {
172
+ case "trend":
173
+ return has("market.trendBias");
174
+ case "swing":
175
+ return has("market.swingBias");
176
+ case "participation":
177
+ return has("market.volumeRel20") || has("market.deltaPct");
178
+ case "setupParticipation":
179
+ return setupKeys.some(
180
+ (key) => /volume|delta|flow|liquidity|turnover/i.test(key)
181
+ );
182
+ case "extension":
183
+ return has("market.fastMaDistanceAtr") || has("setup.breakoutDistanceAtr") || has("signal.stopDistanceAtr");
184
+ case "confirmation":
185
+ return setupKeys.some(
186
+ (key) => /stage|confirm|retest|accept/i.test(key)
187
+ );
188
+ case "setupStrength":
189
+ return setupKeys.some(
190
+ (key) => key !== "setup.version" && !/volume|delta|flow|liquidity|turnover|stage|confirm|retest|accept|breakoutDistance/i.test(
191
+ key
192
+ )
193
+ );
194
+ case "geometry":
195
+ return geometryStatus !== "invalid" && Object.keys(features).some((key) => key.startsWith("geometry."));
196
+ }
197
+ });
198
+ return {
199
+ schema: "tradejs-jev-input/v4",
200
+ strategy: signal.strategy,
201
+ symbol: signal.symbol,
202
+ interval: String(signal.interval),
203
+ timestamp: signal.timestamp,
204
+ direction: signal.direction,
205
+ features,
206
+ provenance,
207
+ missing,
208
+ questions,
209
+ geometryStatus
210
+ };
211
+ };
212
+ var criteria = (subject) => [
213
+ `${subject} is clearly contradicted by the supplied facts.`,
214
+ `${subject} has a material conflict.`,
215
+ `${subject} is uncertain from the supplied facts.`,
216
+ `${subject} has adequate support.`,
217
+ `${subject} has strong support without a material conflict.`
218
+ ];
219
+ var JEV_QUESTIONS = {
220
+ trend: {
221
+ type: "score",
222
+ instructions: "Does market.trendBias support signal.direction? Judge only the target-asset trend, not swings or setup quality.",
223
+ criteria: criteria("Target-asset trend alignment")
224
+ },
225
+ swing: {
226
+ type: "score",
227
+ instructions: "Does market.swingBias support signal.direction? Judge only the target-asset swing structure, not the broader trend.",
228
+ criteria: criteria("Target-asset swing alignment")
229
+ },
230
+ participation: {
231
+ type: "score",
232
+ 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.",
233
+ criteria: criteria("Current target-asset participation")
234
+ },
235
+ setupParticipation: {
236
+ type: "score",
237
+ 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.",
238
+ criteria: criteria("Participation within the setup")
239
+ },
240
+ extension: {
241
+ type: "score",
242
+ 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.",
243
+ criteria: criteria("Entry location without excessive extension")
244
+ },
245
+ confirmation: {
246
+ type: "score",
247
+ instructions: "Do the supplied setup stage and confirmation facts indicate a confirmed rather than premature entry? Do not judge distance or predict the outcome.",
248
+ criteria: criteria("Setup confirmation at entry")
249
+ },
250
+ setupStrength: {
251
+ type: "score",
252
+ 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.",
253
+ criteria: criteria("Non-geometric setup strength")
254
+ },
255
+ geometry: {
256
+ type: "score",
257
+ instructions: "Do the supplied geometry.* facts support the named setup? Do not infer unseen figure points or certify mathematical validity.",
258
+ criteria: criteria("Setup geometry")
259
+ }
260
+ };
261
+ var questionsForJevInput = (input) => Object.fromEntries(input.questions.map((key) => [key, JEV_QUESTIONS[key]]));
262
+ var stateForJevInput = (input) => ({
263
+ schema: input.schema,
264
+ strategy: input.strategy,
265
+ direction: input.direction,
266
+ facts: input.features,
267
+ missing: input.missing,
268
+ geometryStatus: input.geometryStatus,
269
+ units: Object.fromEntries(
270
+ Object.entries(input.provenance).filter(([, detail]) => detail.unit != null).map(([key, detail]) => [key, detail.unit])
271
+ ),
272
+ sources: Object.fromEntries(
273
+ Object.entries(input.provenance).filter(([, detail]) => detail.source != null).map(([key, detail]) => [key, detail.source])
274
+ )
275
+ });
276
+
277
+ // src/jevTraining.ts
278
+ var mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
279
+ var variance = (values) => {
280
+ const average = mean(values);
281
+ return values.reduce((sum, value) => sum + (value - average) ** 2, 0);
282
+ };
283
+ var validateJevStudy = (rows) => {
284
+ const unique = /* @__PURE__ */ new Map();
285
+ if (rows.length > 2e4)
286
+ throw new Error(
287
+ "Jev training is bounded to 20000 rows; select a frozen sample first"
288
+ );
289
+ for (const row of rows) {
290
+ const record = row?.record;
291
+ 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 !== jevHash(record.input) || record.questionsHash !== jevHash(questionsForJevInput(record.input)) || record.id !== jevHash({
292
+ inputHash: record.inputHash,
293
+ questionsHash: record.questionsHash,
294
+ provider: record.provider
295
+ }))
296
+ throw new Error("Invalid Jev study provenance");
297
+ validateJevResponse(record.response, record.input.questions);
298
+ for (const dimension of JEV_DIMENSIONS2) {
299
+ const expected = record.input.questions.includes(dimension) ? record.response.answers[dimension].score / 4 : null;
300
+ if (record.scores[dimension] !== expected)
301
+ throw new Error("Jev study scores do not match the teacher response");
302
+ }
303
+ if (row.profit != null && !Number.isFinite(row.profit))
304
+ throw new Error("Invalid study outcome");
305
+ const prior = unique.get(record.inputHash);
306
+ if (prior && (prior.record.id !== record.id || prior.profit !== row.profit || jevHash(prior.record.response) !== jevHash(record.response)))
307
+ throw new Error("Conflicting duplicated Jev sample");
308
+ unique.set(record.inputHash, row);
309
+ }
310
+ const ordered = [...unique.values()].sort(
311
+ (a, b) => a.record.input.timestamp - b.record.input.timestamp || a.record.id.localeCompare(b.record.id)
312
+ );
313
+ if (!ordered.length) throw new Error("Empty Jev study");
314
+ const first = ordered[0].record;
315
+ if (ordered.some(
316
+ (row) => row.record.input.strategy !== first.input.strategy || row.record.response.model !== first.response.model || jevHash(row.record.provider) !== jevHash(first.provider)
317
+ ))
318
+ throw new Error(
319
+ "Train one strategy and one frozen teacher/provider lineage at a time"
320
+ );
321
+ return ordered;
322
+ };
323
+ var trainTree = (rows, dimension, depth, minLeaf) => {
324
+ const values = rows.map((row) => row.record.scores[dimension]);
325
+ const leaf = { value: mean(values), samples: rows.length };
326
+ if (depth === 0 || rows.length < minLeaf * 2 || variance(values) < 1e-8)
327
+ return leaf;
328
+ const keys = [
329
+ ...new Set(rows.flatMap((row) => Object.keys(row.record.input.features)))
330
+ ].sort().filter(
331
+ (key) => new Set(rows.map((row) => row.record.input.features[key])).size > 1
332
+ );
333
+ if (keys.length > 2e3) throw new Error("Too many Jev student features");
334
+ let best;
335
+ let bestLoss = variance(values);
336
+ for (const feature of keys) {
337
+ const observed = [
338
+ ...new Set(
339
+ rows.map((row) => row.record.input.features[feature]).filter((value) => value != null)
340
+ )
341
+ ];
342
+ const numeric = observed.every((value) => typeof value === "number");
343
+ const sorted = numeric ? observed.sort((a, b) => a - b) : observed.sort((a, b) => String(a).localeCompare(String(b)));
344
+ const candidates = sorted.filter(
345
+ (_, index) => index % Math.max(1, Math.ceil(sorted.length / 12)) === 0
346
+ ).slice(0, 12);
347
+ for (const value of candidates)
348
+ for (const missingLeft of [true, false]) {
349
+ const left = [], right = [];
350
+ for (const row of rows) {
351
+ const input = row.record.input.features[feature];
352
+ const goesLeft = input == null ? missingLeft : numeric ? typeof input === "number" && input <= value : input === value;
353
+ (goesLeft ? left : right).push(row);
354
+ }
355
+ if (left.length < minLeaf || right.length < minLeaf) continue;
356
+ const loss = variance(left.map((row) => row.record.scores[dimension])) + variance(right.map((row) => row.record.scores[dimension]));
357
+ if (loss < bestLoss - 1e-8) {
358
+ bestLoss = loss;
359
+ best = {
360
+ feature,
361
+ operator: numeric ? "lte" : "eq",
362
+ value,
363
+ missingLeft,
364
+ left,
365
+ right
366
+ };
367
+ }
368
+ }
369
+ }
370
+ if (!best) return leaf;
371
+ return {
372
+ feature: best.feature,
373
+ operator: best.operator,
374
+ value: best.value,
375
+ missingLeft: best.missingLeft,
376
+ left: trainTree(best.left, dimension, depth - 1, minLeaf),
377
+ right: trainTree(best.right, dimension, depth - 1, minLeaf)
378
+ };
379
+ };
380
+ var validateJevGateModel = (value) => {
381
+ const model = value;
382
+ 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 !== jevHash(JEV_QUESTIONS) || !model.training || !/^[a-f0-9]{64}$/.test(model.training.datasetHash))
383
+ throw new Error("Invalid local Jev gate");
384
+ const { trainEnd, validationEnd, testEnd } = model.training;
385
+ if (![trainEnd, validationEnd, testEnd].every(Number.isFinite) || !(trainEnd < validationEnd && validationEnd < testEnd))
386
+ throw new Error("Invalid local Jev gate partitions");
387
+ const check = (tree, depth) => {
388
+ if (!tree || depth > 8) throw new Error("Invalid local Jev tree");
389
+ if ("samples" in tree) {
390
+ if (!Number.isFinite(tree.value) || tree.value < 0 || tree.value > 1 || !Number.isInteger(tree.samples) || tree.samples < 1)
391
+ throw new Error("Invalid local Jev leaf");
392
+ return;
393
+ }
394
+ 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))
395
+ throw new Error("Invalid local Jev split");
396
+ check(tree.left, depth + 1);
397
+ check(tree.right, depth + 1);
398
+ };
399
+ for (const dimension of JEV_DIMENSIONS2) {
400
+ if (model.trees[dimension] != null) check(model.trees[dimension], 0);
401
+ }
402
+ return model;
403
+ };
404
+ var predict = (model, row) => Object.fromEntries(
405
+ JEV_DIMENSIONS2.map((key) => [
406
+ key,
407
+ model.trees[key] && row.record.input.questions.includes(key) ? predictJevTree(model.trees[key], row.record.input.features) : null
408
+ ])
409
+ );
410
+ var economics = (rows) => {
411
+ const known = rows.filter((row) => row.profit != null);
412
+ const profits = known.map((row) => row.profit);
413
+ const wins = profits.filter((value) => value > 0).reduce((sum, value) => sum + value, 0);
414
+ const losses = -profits.filter((value) => value < 0).reduce((sum, value) => sum + value, 0);
415
+ return {
416
+ rows: rows.length,
417
+ knownOutcomes: known.length,
418
+ pnl: profits.reduce((a, b) => a + b, 0),
419
+ pnlPerTrade: profits.length ? mean(profits) : null,
420
+ profitFactor: losses ? wins / losses : null,
421
+ winRate: profits.length ? profits.filter((value) => value > 0).length / profits.length : null
422
+ };
423
+ };
424
+ var compareJevGate = (model, input, minScores = {}) => {
425
+ validateJevGateModel(model);
426
+ const rows = validateJevStudy(input);
427
+ if (rows.some(
428
+ (row) => row.record.input.strategy !== model.strategy || row.record.response.model !== model.teacherModel
429
+ ))
430
+ throw new Error("Model and study lineage do not match");
431
+ const decisions = rows.map((row) => {
432
+ const policy = {
433
+ source: "local",
434
+ mode: "gate",
435
+ minScores
436
+ };
437
+ const scores = predict(model, row);
438
+ const student = decideJev(
439
+ scores,
440
+ policy,
441
+ row.record.input.geometryStatus,
442
+ row.record.input.features,
443
+ row.record.input.questions
444
+ ).allowed;
445
+ const teacher = decideJev(
446
+ row.record.scores,
447
+ policy,
448
+ row.record.input.geometryStatus,
449
+ row.record.input.features,
450
+ row.record.input.questions
451
+ ).allowed;
452
+ return { row, scores, student, teacher };
453
+ });
454
+ const dimensionErrors = Object.fromEntries(
455
+ JEV_DIMENSIONS2.map((key) => {
456
+ const pairs = decisions.filter(
457
+ (item) => item.row.record.scores[key] != null && item.scores[key] != null
458
+ );
459
+ return [
460
+ key,
461
+ {
462
+ n: pairs.length,
463
+ mae: pairs.length ? mean(
464
+ pairs.map(
465
+ (item) => Math.abs(item.scores[key] - item.row.record.scores[key])
466
+ )
467
+ ) : null
468
+ }
469
+ ];
470
+ })
471
+ );
472
+ const selected = (predicate) => decisions.filter(predicate).map((value) => value.row);
473
+ return {
474
+ rows: rows.length,
475
+ start: rows[0].record.input.timestamp,
476
+ end: rows[rows.length - 1].record.input.timestamp,
477
+ agreement: decisions.filter((item) => item.student === item.teacher).length / rows.length,
478
+ teacherOnly: decisions.filter((item) => item.teacher && !item.student).length,
479
+ studentOnly: decisions.filter((item) => item.student && !item.teacher).length,
480
+ dimensionErrors,
481
+ outcomeScope: "Matched completed outcomes only; this is not a sequential portfolio backtest. Unexecuted candidates have no observed outcome.",
482
+ cohorts: Object.fromEntries(
483
+ ["ALL", "LONG", "SHORT"].map((side) => [
484
+ side,
485
+ {
486
+ core: economics(
487
+ selected(
488
+ (item) => side === "ALL" || item.row.record.input.direction === side
489
+ )
490
+ ),
491
+ baseline: economics(
492
+ selected(
493
+ (item) => item.row.baselineAllowed === true && (side === "ALL" || item.row.record.input.direction === side)
494
+ )
495
+ ),
496
+ jev: economics(
497
+ selected(
498
+ (item) => item.teacher && (side === "ALL" || item.row.record.input.direction === side)
499
+ )
500
+ ),
501
+ local: economics(
502
+ selected(
503
+ (item) => item.student && (side === "ALL" || item.row.record.input.direction === side)
504
+ )
505
+ )
506
+ }
507
+ ])
508
+ )
509
+ };
510
+ };
511
+ var trainJevGate = (input, options = {}) => {
512
+ const rows = validateJevStudy(input);
513
+ const maxDepth = options.maxDepth ?? 3, minLeaf = options.minLeaf ?? 10;
514
+ if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 6 || !Number.isInteger(minLeaf) || minLeaf < 2)
515
+ throw new Error("Invalid Jev training limits");
516
+ const timestamps = [
517
+ ...new Set(rows.map((row) => row.record.input.timestamp))
518
+ ];
519
+ if (rows.length < 30 || timestamps.length < 10)
520
+ throw new Error("Need at least 30 distinct samples across 10 signal times");
521
+ const trainEnd = timestamps[Math.floor(timestamps.length * 0.6) - 1];
522
+ const validationEnd = timestamps[Math.floor(timestamps.length * 0.8) - 1];
523
+ const train = rows.filter((row) => row.record.input.timestamp <= trainEnd);
524
+ const validation = rows.filter(
525
+ (row) => row.record.input.timestamp > trainEnd && row.record.input.timestamp <= validationEnd
526
+ );
527
+ const test = rows.filter((row) => row.record.input.timestamp > validationEnd);
528
+ const fit = (samples) => Object.fromEntries(
529
+ JEV_DIMENSIONS2.map((key) => {
530
+ const eligible = samples.filter(
531
+ (row) => row.record.scores[key] != null
532
+ );
533
+ return [
534
+ key,
535
+ eligible.length ? trainTree(eligible, key, maxDepth, minLeaf) : null
536
+ ];
537
+ })
538
+ );
539
+ const first = rows[0].record;
540
+ const model = {
541
+ schema: "tradejs-jev-gate/v2",
542
+ inputSchema: "tradejs-jev-input/v4",
543
+ strategy: first.input.strategy,
544
+ questionsHash: jevHash(JEV_QUESTIONS),
545
+ teacherModel: first.response.model,
546
+ trees: fit(train),
547
+ training: {
548
+ datasetHash: jevHash(
549
+ rows.map((row) => ({
550
+ id: row.record.id,
551
+ response: row.record.response
552
+ }))
553
+ ),
554
+ trainEnd,
555
+ validationEnd,
556
+ testEnd: timestamps[timestamps.length - 1],
557
+ maxDepth,
558
+ minLeaf
559
+ }
560
+ };
561
+ validateJevGateModel(model);
562
+ const walkForward = [0.4, 0.6, 0.8].map((fraction) => {
563
+ const trainThrough = timestamps[Math.max(0, Math.floor(timestamps.length * (fraction - 0.1)) - 1)];
564
+ const validationThrough = timestamps[Math.floor(timestamps.length * fraction) - 1];
565
+ const testThrough = timestamps[Math.min(
566
+ timestamps.length - 1,
567
+ Math.round(timestamps.length * (fraction + 0.2)) - 1
568
+ )];
569
+ const fitting = rows.filter(
570
+ (row) => row.record.input.timestamp <= trainThrough
571
+ );
572
+ const evaluation = rows.filter(
573
+ (row) => row.record.input.timestamp > validationThrough && row.record.input.timestamp <= testThrough
574
+ );
575
+ const fold = {
576
+ ...model,
577
+ trees: fit(fitting),
578
+ training: {
579
+ ...model.training,
580
+ trainEnd: trainThrough,
581
+ validationEnd: validationThrough,
582
+ testEnd: testThrough
583
+ }
584
+ };
585
+ return {
586
+ trainThrough,
587
+ validationThrough,
588
+ testThrough,
589
+ trainRows: fitting.length,
590
+ test: compareJevGate(fold, evaluation)
591
+ };
592
+ });
593
+ return {
594
+ model,
595
+ report: {
596
+ train: compareJevGate(model, train),
597
+ validation: compareJevGate(model, validation),
598
+ test: compareJevGate(model, test),
599
+ walkForward,
600
+ promotion: "research-only; validate a sequential backtest and prospective signals before runtime admission"
601
+ }
602
+ };
603
+ };
604
+
605
+ export {
606
+ buildJevInput,
607
+ JEV_QUESTIONS,
608
+ questionsForJevInput,
609
+ stateForJevInput,
610
+ validateJevStudy,
611
+ validateJevGateModel,
612
+ compareJevGate,
613
+ trainJevGate
614
+ };
package/dist/cli.js CHANGED
@@ -1184,8 +1184,11 @@ var toRecord2 = (value) => {
1184
1184
  return value;
1185
1185
  };
1186
1186
  var buildBaseAiPayload = (signal) => {
1187
+ const { jevEvidence: _jevEvidence, ...signalAdditionalIndicators } = toRecord2(
1188
+ signal.additionalIndicators
1189
+ );
1187
1190
  const additionalIndicators = {
1188
- ...toRecord2(signal.additionalIndicators),
1191
+ ...signalAdditionalIndicators,
1189
1192
  marketContext: buildAiMarketContext(signal)
1190
1193
  };
1191
1194
  return {
@@ -2328,6 +2331,7 @@ var formatMessage = (signal, analysis, _options = {}) => {
2328
2331
  symbol,
2329
2332
  direction,
2330
2333
  strategy,
2334
+ deploymentId,
2331
2335
  orderStatus,
2332
2336
  orderSkipReason,
2333
2337
  orderFailureReason,
@@ -2360,6 +2364,9 @@ var formatMessage = (signal, analysis, _options = {}) => {
2360
2364
  const emojiDir = direction === "LONG" ? "\u{1F7E9} LONG" : direction === "SHORT" ? "\u{1F7E5} SHORT" : "\u2B1C\uFE0F NO TRADE";
2361
2365
  lines.push(`<b>${emojiDir} ${symbol}</b>`);
2362
2366
  lines.push(`Strategy: ${strategy}`);
2367
+ if (deploymentId) {
2368
+ lines.push(`Deployment: ${escapeHtml(deploymentId)}`);
2369
+ }
2363
2370
  lines.push("");
2364
2371
  if (orderStatus) {
2365
2372
  let orderStatusText = "\u26AA\uFE0F Order skipped";
@@ -2870,6 +2877,7 @@ var formatRuntimeCloseNotification = (event, _userName = event.userName ?? "root
2870
2877
  "<b>Strategy self-close</b>",
2871
2878
  `Symbol: <b>${escapeHtml2(event.symbol)}</b>`,
2872
2879
  `Strategy: <b>${escapeHtml2(event.strategy)}</b>`,
2880
+ ...event.deploymentId ? [`Deployment: <b>${escapeHtml2(event.deploymentId)}</b>`] : [],
2873
2881
  `Direction: <b>${escapeHtml2(event.direction)}</b>`,
2874
2882
  `Reason: <code>${escapeHtml2(event.code)}</code>`,
2875
2883
  "",
package/dist/cli.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  askAI,
8
8
  ensureStrategyPluginsLoaded
9
- } from "./chunk-UITD6LG6.mjs";
9
+ } from "./chunk-UL5Z46JL.mjs";
10
10
  import {
11
11
  getTradejsProjectCwd,
12
12
  loadTradejsConfig
@@ -741,6 +741,7 @@ var formatMessage = (signal, analysis, _options = {}) => {
741
741
  symbol,
742
742
  direction,
743
743
  strategy,
744
+ deploymentId,
744
745
  orderStatus,
745
746
  orderSkipReason,
746
747
  orderFailureReason,
@@ -773,6 +774,9 @@ var formatMessage = (signal, analysis, _options = {}) => {
773
774
  const emojiDir = direction === "LONG" ? "\u{1F7E9} LONG" : direction === "SHORT" ? "\u{1F7E5} SHORT" : "\u2B1C\uFE0F NO TRADE";
774
775
  lines.push(`<b>${emojiDir} ${symbol}</b>`);
775
776
  lines.push(`Strategy: ${strategy}`);
777
+ if (deploymentId) {
778
+ lines.push(`Deployment: ${escapeHtml(deploymentId)}`);
779
+ }
776
780
  lines.push("");
777
781
  if (orderStatus) {
778
782
  let orderStatusText = "\u26AA\uFE0F Order skipped";
@@ -1283,6 +1287,7 @@ var formatRuntimeCloseNotification = (event, _userName = event.userName ?? "root
1283
1287
  "<b>Strategy self-close</b>",
1284
1288
  `Symbol: <b>${escapeHtml2(event.symbol)}</b>`,
1285
1289
  `Strategy: <b>${escapeHtml2(event.strategy)}</b>`,
1290
+ ...event.deploymentId ? [`Deployment: <b>${escapeHtml2(event.deploymentId)}</b>`] : [],
1286
1291
  `Direction: <b>${escapeHtml2(event.direction)}</b>`,
1287
1292
  `Reason: <code>${escapeHtml2(event.code)}</code>`,
1288
1293
  "",