@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/ai.js +4 -1
- package/dist/ai.mjs +1 -1
- package/dist/backtest.js +941 -35
- package/dist/backtest.mjs +7 -3
- package/dist/chunk-3BBXGZ7M.mjs +271 -0
- package/dist/{chunk-K2YQ3MQC.mjs → chunk-BO42QWAB.mjs} +11 -2
- package/dist/{chunk-UITD6LG6.mjs → chunk-UL5Z46JL.mjs} +4 -1
- package/dist/{chunk-JDFPF2RG.mjs → chunk-WGL7XJ3W.mjs} +24 -2
- package/dist/chunk-ZZQLTUTP.mjs +614 -0
- package/dist/cli.js +9 -1
- package/dist/cli.mjs +6 -1
- package/dist/jev.d.mts +342 -0
- package/dist/jev.d.ts +342 -0
- package/dist/jev.js +931 -0
- package/dist/jev.mjs +23 -0
- package/dist/jevTraining-JYXUVAZG.mjs +13 -0
- package/dist/registry.js +933 -29
- package/dist/registry.mjs +4 -2
- package/dist/runtimeDashboard.js +952 -39
- package/dist/runtimeDashboard.mjs +5 -3
- package/dist/runtimeStrategies.js +952 -39
- package/dist/runtimeStrategies.mjs +5 -3
- package/dist/strategies.js +931 -27
- package/dist/strategies.mjs +4 -2
- package/package.json +10 -5
package/dist/registry.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 });
|
|
@@ -27,6 +35,628 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
35
|
));
|
|
28
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
37
|
|
|
38
|
+
// src/jevInput.ts
|
|
39
|
+
var import_data, import_jev, object, at, finite, fact, buildJevInput, criteria, JEV_QUESTIONS, questionsForJevInput, stateForJevInput;
|
|
40
|
+
var init_jevInput = __esm({
|
|
41
|
+
"src/jevInput.ts"() {
|
|
42
|
+
"use strict";
|
|
43
|
+
import_data = require("@tradejs/core/data");
|
|
44
|
+
import_jev = require("@tradejs/core/jev");
|
|
45
|
+
object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
46
|
+
at = (value, ...path3) => path3.reduce((current, key) => object(current)[key], value);
|
|
47
|
+
finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
48
|
+
fact = (value) => {
|
|
49
|
+
if (value == null || typeof value === "boolean") return value ?? null;
|
|
50
|
+
if (typeof value === "number") return finite(value);
|
|
51
|
+
if (typeof value === "string" && value.length <= 60) return value;
|
|
52
|
+
throw new Error("Invalid Jev evidence fact");
|
|
53
|
+
};
|
|
54
|
+
buildJevInput = (signal) => {
|
|
55
|
+
if (!["LONG", "SHORT"].includes(signal.direction ?? "") || !Number.isFinite(signal.timestamp))
|
|
56
|
+
throw new Error("Jev requires a dated directional signal");
|
|
57
|
+
const base = object(signal.additionalIndicators?.baseContext);
|
|
58
|
+
const baseTimestamp = at(base, "candle", "timestamp");
|
|
59
|
+
const baseKnownAt = finite(baseTimestamp);
|
|
60
|
+
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;
|
|
61
|
+
const evidence = signal.jevEvidence;
|
|
62
|
+
const features = {};
|
|
63
|
+
const provenance = {};
|
|
64
|
+
const missing = {};
|
|
65
|
+
const put = (key, value, details, missingReason = "missing_value") => {
|
|
66
|
+
const normalized = fact(value);
|
|
67
|
+
if (normalized != null) {
|
|
68
|
+
features[key] = normalized;
|
|
69
|
+
provenance[key] = details;
|
|
70
|
+
} else {
|
|
71
|
+
missing[key] = missingReason;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const signalFact = {
|
|
75
|
+
knownAt: signal.timestamp,
|
|
76
|
+
scope: "target",
|
|
77
|
+
source: "signal"
|
|
78
|
+
};
|
|
79
|
+
put("signal.direction", signal.direction, signalFact);
|
|
80
|
+
const marketKeys = [
|
|
81
|
+
"market.trendBias",
|
|
82
|
+
"market.swingBias",
|
|
83
|
+
"market.volumeRel20",
|
|
84
|
+
"market.deltaPct",
|
|
85
|
+
"market.fastMaDistanceAtr"
|
|
86
|
+
];
|
|
87
|
+
if (baseProblem) {
|
|
88
|
+
for (const key of marketKeys) missing[key] = baseProblem;
|
|
89
|
+
} else {
|
|
90
|
+
const marketFact = {
|
|
91
|
+
knownAt: baseKnownAt,
|
|
92
|
+
scope: "target",
|
|
93
|
+
source: "baseContext"
|
|
94
|
+
};
|
|
95
|
+
put("market.trendBias", at(base, "regime", "trend", "bias"), marketFact);
|
|
96
|
+
put("market.swingBias", at(base, "structure", "swing", "bias"), marketFact);
|
|
97
|
+
put(
|
|
98
|
+
"market.volumeRel20",
|
|
99
|
+
at(base, "participation", "volume", "volumeRel20"),
|
|
100
|
+
{ ...marketFact, unit: "ratio" }
|
|
101
|
+
);
|
|
102
|
+
const deltaSource = at(base, "participation", "delta", "source");
|
|
103
|
+
put(
|
|
104
|
+
"market.deltaPct",
|
|
105
|
+
at(base, "participation", "delta", "deltaPct"),
|
|
106
|
+
{
|
|
107
|
+
...marketFact,
|
|
108
|
+
source: typeof deltaSource === "string" ? `baseContext.participation.delta.${deltaSource}` : "baseContext.participation.delta",
|
|
109
|
+
unit: "ratio"
|
|
110
|
+
},
|
|
111
|
+
deltaSource === "ohlcv_proxy" ? "ohlcv_proxy_no_taker_volume" : "missing_value"
|
|
112
|
+
);
|
|
113
|
+
put(
|
|
114
|
+
"market.fastMaDistanceAtr",
|
|
115
|
+
at(base, "regime", "trend", "priceDistanceToMaFastAtr"),
|
|
116
|
+
{ ...marketFact, unit: "ATR" }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const price = finite(signal.prices.currentPrice);
|
|
120
|
+
const stop = finite(signal.prices.stopLossPrice);
|
|
121
|
+
const target = finite(signal.prices.takeProfitPrice);
|
|
122
|
+
const atr = baseProblem ? null : finite(at(base, "raw", "volatility", "atr"));
|
|
123
|
+
if (price == null || stop == null || target == null)
|
|
124
|
+
throw new Error("Invalid Jev signal prices");
|
|
125
|
+
put(
|
|
126
|
+
"signal.validLevels",
|
|
127
|
+
signal.direction === "LONG" ? stop < price && price < target : target < price && price < stop,
|
|
128
|
+
signalFact
|
|
129
|
+
);
|
|
130
|
+
if (atr != null && atr > 0) {
|
|
131
|
+
put("signal.stopDistanceAtr", Math.abs(price - stop) / atr, {
|
|
132
|
+
...signalFact,
|
|
133
|
+
unit: "ATR"
|
|
134
|
+
});
|
|
135
|
+
put("signal.targetDistanceAtr", Math.abs(price - target) / atr, {
|
|
136
|
+
...signalFact,
|
|
137
|
+
unit: "ATR"
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
missing["signal.stopDistanceAtr"] = baseProblem ?? "missing_atr";
|
|
141
|
+
missing["signal.targetDistanceAtr"] = baseProblem ?? "missing_atr";
|
|
142
|
+
}
|
|
143
|
+
let geometryStatus = "absent";
|
|
144
|
+
const figures = signal.figures;
|
|
145
|
+
const groups = [figures?.lines, figures?.points, figures?.zones];
|
|
146
|
+
const points = [
|
|
147
|
+
...figures?.trendLine?.points ?? [],
|
|
148
|
+
...figures?.trendLine?.touches ?? [],
|
|
149
|
+
...groups.flatMap(
|
|
150
|
+
(group) => Array.isArray(group) ? group.flatMap(
|
|
151
|
+
(item) => "points" in item ? item.points : "start" in item ? [item.start, item.end] : []
|
|
152
|
+
) : []
|
|
153
|
+
)
|
|
154
|
+
];
|
|
155
|
+
if (points.length > 128 || groups.some(
|
|
156
|
+
(group) => group && (!Array.isArray(group) || group.length > 24)
|
|
157
|
+
) || points.some(
|
|
158
|
+
(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)
|
|
159
|
+
))
|
|
160
|
+
geometryStatus = "invalid";
|
|
161
|
+
else if (points.length) geometryStatus = "available";
|
|
162
|
+
if (evidence) {
|
|
163
|
+
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)))
|
|
164
|
+
throw new Error("Invalid Jev evidence");
|
|
165
|
+
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)
|
|
166
|
+
throw new Error("Invalid or oversized Jev evidence");
|
|
167
|
+
const strategyFact = {
|
|
168
|
+
knownAt: evidence.knownAt,
|
|
169
|
+
scope: "strategy",
|
|
170
|
+
source: "strategy.jevEvidence"
|
|
171
|
+
};
|
|
172
|
+
const detailFor = (key) => {
|
|
173
|
+
const detail = evidence.factDetails?.[key] ?? strategyFact;
|
|
174
|
+
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))
|
|
175
|
+
throw new Error("Invalid Jev fact provenance");
|
|
176
|
+
return { ...strategyFact, ...detail };
|
|
177
|
+
};
|
|
178
|
+
put("setup.version", evidence.version, strategyFact);
|
|
179
|
+
for (const [key, value] of Object.entries(evidence.facts)) {
|
|
180
|
+
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(
|
|
181
|
+
key
|
|
182
|
+
))
|
|
183
|
+
throw new Error("Invalid Jev evidence key");
|
|
184
|
+
put(`setup.${key}`, value, detailFor(`setup.${key}`));
|
|
185
|
+
}
|
|
186
|
+
for (const [key, value] of Object.entries(evidence.geometry ?? {})) {
|
|
187
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]{0,39}$/.test(key))
|
|
188
|
+
throw new Error("Invalid Jev geometry key");
|
|
189
|
+
put(`geometry.${key}`, value, detailFor(`geometry.${key}`));
|
|
190
|
+
}
|
|
191
|
+
if (Object.keys(evidence.factDetails ?? {}).some(
|
|
192
|
+
(key) => !(key in provenance)
|
|
193
|
+
))
|
|
194
|
+
throw new Error("Jev provenance references an unavailable fact");
|
|
195
|
+
}
|
|
196
|
+
if (evidence?.geometry && Object.keys(evidence.geometry).length && geometryStatus === "absent")
|
|
197
|
+
geometryStatus = "facts_only";
|
|
198
|
+
if (geometryStatus === "absent") missing["geometry.figures"] = "no_geometry";
|
|
199
|
+
if (geometryStatus === "invalid")
|
|
200
|
+
missing["geometry.figures"] = "invalid_geometry";
|
|
201
|
+
if (Object.keys(features).length > 40 || JSON.stringify(features).length > 4e3)
|
|
202
|
+
throw new Error("Jev input exceeds the microdecision budget");
|
|
203
|
+
const has = (key) => key in features;
|
|
204
|
+
const setupKeys = Object.keys(features).filter(
|
|
205
|
+
(key) => key.startsWith("setup.")
|
|
206
|
+
);
|
|
207
|
+
const questions = import_jev.JEV_DIMENSIONS.filter((dimension) => {
|
|
208
|
+
switch (dimension) {
|
|
209
|
+
case "trend":
|
|
210
|
+
return has("market.trendBias");
|
|
211
|
+
case "swing":
|
|
212
|
+
return has("market.swingBias");
|
|
213
|
+
case "participation":
|
|
214
|
+
return has("market.volumeRel20") || has("market.deltaPct");
|
|
215
|
+
case "setupParticipation":
|
|
216
|
+
return setupKeys.some(
|
|
217
|
+
(key) => /volume|delta|flow|liquidity|turnover/i.test(key)
|
|
218
|
+
);
|
|
219
|
+
case "extension":
|
|
220
|
+
return has("market.fastMaDistanceAtr") || has("setup.breakoutDistanceAtr") || has("signal.stopDistanceAtr");
|
|
221
|
+
case "confirmation":
|
|
222
|
+
return setupKeys.some(
|
|
223
|
+
(key) => /stage|confirm|retest|accept/i.test(key)
|
|
224
|
+
);
|
|
225
|
+
case "setupStrength":
|
|
226
|
+
return setupKeys.some(
|
|
227
|
+
(key) => key !== "setup.version" && !/volume|delta|flow|liquidity|turnover|stage|confirm|retest|accept|breakoutDistance/i.test(
|
|
228
|
+
key
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
case "geometry":
|
|
232
|
+
return geometryStatus !== "invalid" && Object.keys(features).some((key) => key.startsWith("geometry."));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
schema: "tradejs-jev-input/v4",
|
|
237
|
+
strategy: signal.strategy,
|
|
238
|
+
symbol: signal.symbol,
|
|
239
|
+
interval: String(signal.interval),
|
|
240
|
+
timestamp: signal.timestamp,
|
|
241
|
+
direction: signal.direction,
|
|
242
|
+
features,
|
|
243
|
+
provenance,
|
|
244
|
+
missing,
|
|
245
|
+
questions,
|
|
246
|
+
geometryStatus
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
criteria = (subject) => [
|
|
250
|
+
`${subject} is clearly contradicted by the supplied facts.`,
|
|
251
|
+
`${subject} has a material conflict.`,
|
|
252
|
+
`${subject} is uncertain from the supplied facts.`,
|
|
253
|
+
`${subject} has adequate support.`,
|
|
254
|
+
`${subject} has strong support without a material conflict.`
|
|
255
|
+
];
|
|
256
|
+
JEV_QUESTIONS = {
|
|
257
|
+
trend: {
|
|
258
|
+
type: "score",
|
|
259
|
+
instructions: "Does market.trendBias support signal.direction? Judge only the target-asset trend, not swings or setup quality.",
|
|
260
|
+
criteria: criteria("Target-asset trend alignment")
|
|
261
|
+
},
|
|
262
|
+
swing: {
|
|
263
|
+
type: "score",
|
|
264
|
+
instructions: "Does market.swingBias support signal.direction? Judge only the target-asset swing structure, not the broader trend.",
|
|
265
|
+
criteria: criteria("Target-asset swing alignment")
|
|
266
|
+
},
|
|
267
|
+
participation: {
|
|
268
|
+
type: "score",
|
|
269
|
+
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.",
|
|
270
|
+
criteria: criteria("Current target-asset participation")
|
|
271
|
+
},
|
|
272
|
+
setupParticipation: {
|
|
273
|
+
type: "score",
|
|
274
|
+
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.",
|
|
275
|
+
criteria: criteria("Participation within the setup")
|
|
276
|
+
},
|
|
277
|
+
extension: {
|
|
278
|
+
type: "score",
|
|
279
|
+
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.",
|
|
280
|
+
criteria: criteria("Entry location without excessive extension")
|
|
281
|
+
},
|
|
282
|
+
confirmation: {
|
|
283
|
+
type: "score",
|
|
284
|
+
instructions: "Do the supplied setup stage and confirmation facts indicate a confirmed rather than premature entry? Do not judge distance or predict the outcome.",
|
|
285
|
+
criteria: criteria("Setup confirmation at entry")
|
|
286
|
+
},
|
|
287
|
+
setupStrength: {
|
|
288
|
+
type: "score",
|
|
289
|
+
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.",
|
|
290
|
+
criteria: criteria("Non-geometric setup strength")
|
|
291
|
+
},
|
|
292
|
+
geometry: {
|
|
293
|
+
type: "score",
|
|
294
|
+
instructions: "Do the supplied geometry.* facts support the named setup? Do not infer unseen figure points or certify mathematical validity.",
|
|
295
|
+
criteria: criteria("Setup geometry")
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
questionsForJevInput = (input) => Object.fromEntries(input.questions.map((key) => [key, JEV_QUESTIONS[key]]));
|
|
299
|
+
stateForJevInput = (input) => ({
|
|
300
|
+
schema: input.schema,
|
|
301
|
+
strategy: input.strategy,
|
|
302
|
+
direction: input.direction,
|
|
303
|
+
facts: input.features,
|
|
304
|
+
missing: input.missing,
|
|
305
|
+
geometryStatus: input.geometryStatus,
|
|
306
|
+
units: Object.fromEntries(
|
|
307
|
+
Object.entries(input.provenance).filter(([, detail]) => detail.unit != null).map(([key, detail]) => [key, detail.unit])
|
|
308
|
+
),
|
|
309
|
+
sources: Object.fromEntries(
|
|
310
|
+
Object.entries(input.provenance).filter(([, detail]) => detail.source != null).map(([key, detail]) => [key, detail.source])
|
|
311
|
+
)
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// src/jevTraining.ts
|
|
317
|
+
var jevTraining_exports = {};
|
|
318
|
+
__export(jevTraining_exports, {
|
|
319
|
+
compareJevGate: () => compareJevGate,
|
|
320
|
+
trainJevGate: () => trainJevGate,
|
|
321
|
+
validateJevGateModel: () => validateJevGateModel,
|
|
322
|
+
validateJevStudy: () => validateJevStudy
|
|
323
|
+
});
|
|
324
|
+
var import_jev2, import_jev3, mean, variance, validateJevStudy, trainTree, validateJevGateModel, predict, economics, compareJevGate, trainJevGate;
|
|
325
|
+
var init_jevTraining = __esm({
|
|
326
|
+
"src/jevTraining.ts"() {
|
|
327
|
+
"use strict";
|
|
328
|
+
import_jev2 = require("@tradejs/core/jev");
|
|
329
|
+
import_jev3 = require("@tradejs/infra/jev");
|
|
330
|
+
init_jevInput();
|
|
331
|
+
mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
332
|
+
variance = (values) => {
|
|
333
|
+
const average = mean(values);
|
|
334
|
+
return values.reduce((sum, value) => sum + (value - average) ** 2, 0);
|
|
335
|
+
};
|
|
336
|
+
validateJevStudy = (rows) => {
|
|
337
|
+
const unique = /* @__PURE__ */ new Map();
|
|
338
|
+
if (rows.length > 2e4)
|
|
339
|
+
throw new Error(
|
|
340
|
+
"Jev training is bounded to 20000 rows; select a frozen sample first"
|
|
341
|
+
);
|
|
342
|
+
for (const row of rows) {
|
|
343
|
+
const record = row?.record;
|
|
344
|
+
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)({
|
|
345
|
+
inputHash: record.inputHash,
|
|
346
|
+
questionsHash: record.questionsHash,
|
|
347
|
+
provider: record.provider
|
|
348
|
+
}))
|
|
349
|
+
throw new Error("Invalid Jev study provenance");
|
|
350
|
+
(0, import_jev3.validateJevResponse)(record.response, record.input.questions);
|
|
351
|
+
for (const dimension of import_jev2.JEV_DIMENSIONS) {
|
|
352
|
+
const expected = record.input.questions.includes(dimension) ? record.response.answers[dimension].score / 4 : null;
|
|
353
|
+
if (record.scores[dimension] !== expected)
|
|
354
|
+
throw new Error("Jev study scores do not match the teacher response");
|
|
355
|
+
}
|
|
356
|
+
if (row.profit != null && !Number.isFinite(row.profit))
|
|
357
|
+
throw new Error("Invalid study outcome");
|
|
358
|
+
const prior = unique.get(record.inputHash);
|
|
359
|
+
if (prior && (prior.record.id !== record.id || prior.profit !== row.profit || (0, import_jev3.jevHash)(prior.record.response) !== (0, import_jev3.jevHash)(record.response)))
|
|
360
|
+
throw new Error("Conflicting duplicated Jev sample");
|
|
361
|
+
unique.set(record.inputHash, row);
|
|
362
|
+
}
|
|
363
|
+
const ordered = [...unique.values()].sort(
|
|
364
|
+
(a, b) => a.record.input.timestamp - b.record.input.timestamp || a.record.id.localeCompare(b.record.id)
|
|
365
|
+
);
|
|
366
|
+
if (!ordered.length) throw new Error("Empty Jev study");
|
|
367
|
+
const first = ordered[0].record;
|
|
368
|
+
if (ordered.some(
|
|
369
|
+
(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)
|
|
370
|
+
))
|
|
371
|
+
throw new Error(
|
|
372
|
+
"Train one strategy and one frozen teacher/provider lineage at a time"
|
|
373
|
+
);
|
|
374
|
+
return ordered;
|
|
375
|
+
};
|
|
376
|
+
trainTree = (rows, dimension, depth, minLeaf) => {
|
|
377
|
+
const values = rows.map((row) => row.record.scores[dimension]);
|
|
378
|
+
const leaf = { value: mean(values), samples: rows.length };
|
|
379
|
+
if (depth === 0 || rows.length < minLeaf * 2 || variance(values) < 1e-8)
|
|
380
|
+
return leaf;
|
|
381
|
+
const keys = [
|
|
382
|
+
...new Set(rows.flatMap((row) => Object.keys(row.record.input.features)))
|
|
383
|
+
].sort().filter(
|
|
384
|
+
(key) => new Set(rows.map((row) => row.record.input.features[key])).size > 1
|
|
385
|
+
);
|
|
386
|
+
if (keys.length > 2e3) throw new Error("Too many Jev student features");
|
|
387
|
+
let best;
|
|
388
|
+
let bestLoss = variance(values);
|
|
389
|
+
for (const feature of keys) {
|
|
390
|
+
const observed = [
|
|
391
|
+
...new Set(
|
|
392
|
+
rows.map((row) => row.record.input.features[feature]).filter((value) => value != null)
|
|
393
|
+
)
|
|
394
|
+
];
|
|
395
|
+
const numeric = observed.every((value) => typeof value === "number");
|
|
396
|
+
const sorted = numeric ? observed.sort((a, b) => a - b) : observed.sort((a, b) => String(a).localeCompare(String(b)));
|
|
397
|
+
const candidates = sorted.filter(
|
|
398
|
+
(_2, index) => index % Math.max(1, Math.ceil(sorted.length / 12)) === 0
|
|
399
|
+
).slice(0, 12);
|
|
400
|
+
for (const value of candidates)
|
|
401
|
+
for (const missingLeft of [true, false]) {
|
|
402
|
+
const left = [], right = [];
|
|
403
|
+
for (const row of rows) {
|
|
404
|
+
const input = row.record.input.features[feature];
|
|
405
|
+
const goesLeft = input == null ? missingLeft : numeric ? typeof input === "number" && input <= value : input === value;
|
|
406
|
+
(goesLeft ? left : right).push(row);
|
|
407
|
+
}
|
|
408
|
+
if (left.length < minLeaf || right.length < minLeaf) continue;
|
|
409
|
+
const loss = variance(left.map((row) => row.record.scores[dimension])) + variance(right.map((row) => row.record.scores[dimension]));
|
|
410
|
+
if (loss < bestLoss - 1e-8) {
|
|
411
|
+
bestLoss = loss;
|
|
412
|
+
best = {
|
|
413
|
+
feature,
|
|
414
|
+
operator: numeric ? "lte" : "eq",
|
|
415
|
+
value,
|
|
416
|
+
missingLeft,
|
|
417
|
+
left,
|
|
418
|
+
right
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (!best) return leaf;
|
|
424
|
+
return {
|
|
425
|
+
feature: best.feature,
|
|
426
|
+
operator: best.operator,
|
|
427
|
+
value: best.value,
|
|
428
|
+
missingLeft: best.missingLeft,
|
|
429
|
+
left: trainTree(best.left, dimension, depth - 1, minLeaf),
|
|
430
|
+
right: trainTree(best.right, dimension, depth - 1, minLeaf)
|
|
431
|
+
};
|
|
432
|
+
};
|
|
433
|
+
validateJevGateModel = (value) => {
|
|
434
|
+
const model = value;
|
|
435
|
+
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))
|
|
436
|
+
throw new Error("Invalid local Jev gate");
|
|
437
|
+
const { trainEnd, validationEnd, testEnd } = model.training;
|
|
438
|
+
if (![trainEnd, validationEnd, testEnd].every(Number.isFinite) || !(trainEnd < validationEnd && validationEnd < testEnd))
|
|
439
|
+
throw new Error("Invalid local Jev gate partitions");
|
|
440
|
+
const check = (tree, depth) => {
|
|
441
|
+
if (!tree || depth > 8) throw new Error("Invalid local Jev tree");
|
|
442
|
+
if ("samples" in tree) {
|
|
443
|
+
if (!Number.isFinite(tree.value) || tree.value < 0 || tree.value > 1 || !Number.isInteger(tree.samples) || tree.samples < 1)
|
|
444
|
+
throw new Error("Invalid local Jev leaf");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
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))
|
|
448
|
+
throw new Error("Invalid local Jev split");
|
|
449
|
+
check(tree.left, depth + 1);
|
|
450
|
+
check(tree.right, depth + 1);
|
|
451
|
+
};
|
|
452
|
+
for (const dimension of import_jev2.JEV_DIMENSIONS) {
|
|
453
|
+
if (model.trees[dimension] != null) check(model.trees[dimension], 0);
|
|
454
|
+
}
|
|
455
|
+
return model;
|
|
456
|
+
};
|
|
457
|
+
predict = (model, row) => Object.fromEntries(
|
|
458
|
+
import_jev2.JEV_DIMENSIONS.map((key) => [
|
|
459
|
+
key,
|
|
460
|
+
model.trees[key] && row.record.input.questions.includes(key) ? (0, import_jev2.predictJevTree)(model.trees[key], row.record.input.features) : null
|
|
461
|
+
])
|
|
462
|
+
);
|
|
463
|
+
economics = (rows) => {
|
|
464
|
+
const known = rows.filter((row) => row.profit != null);
|
|
465
|
+
const profits = known.map((row) => row.profit);
|
|
466
|
+
const wins = profits.filter((value) => value > 0).reduce((sum, value) => sum + value, 0);
|
|
467
|
+
const losses = -profits.filter((value) => value < 0).reduce((sum, value) => sum + value, 0);
|
|
468
|
+
return {
|
|
469
|
+
rows: rows.length,
|
|
470
|
+
knownOutcomes: known.length,
|
|
471
|
+
pnl: profits.reduce((a, b) => a + b, 0),
|
|
472
|
+
pnlPerTrade: profits.length ? mean(profits) : null,
|
|
473
|
+
profitFactor: losses ? wins / losses : null,
|
|
474
|
+
winRate: profits.length ? profits.filter((value) => value > 0).length / profits.length : null
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
compareJevGate = (model, input, minScores = {}) => {
|
|
478
|
+
validateJevGateModel(model);
|
|
479
|
+
const rows = validateJevStudy(input);
|
|
480
|
+
if (rows.some(
|
|
481
|
+
(row) => row.record.input.strategy !== model.strategy || row.record.response.model !== model.teacherModel
|
|
482
|
+
))
|
|
483
|
+
throw new Error("Model and study lineage do not match");
|
|
484
|
+
const decisions = rows.map((row) => {
|
|
485
|
+
const policy = {
|
|
486
|
+
source: "local",
|
|
487
|
+
mode: "gate",
|
|
488
|
+
minScores
|
|
489
|
+
};
|
|
490
|
+
const scores = predict(model, row);
|
|
491
|
+
const student = (0, import_jev2.decideJev)(
|
|
492
|
+
scores,
|
|
493
|
+
policy,
|
|
494
|
+
row.record.input.geometryStatus,
|
|
495
|
+
row.record.input.features,
|
|
496
|
+
row.record.input.questions
|
|
497
|
+
).allowed;
|
|
498
|
+
const teacher = (0, import_jev2.decideJev)(
|
|
499
|
+
row.record.scores,
|
|
500
|
+
policy,
|
|
501
|
+
row.record.input.geometryStatus,
|
|
502
|
+
row.record.input.features,
|
|
503
|
+
row.record.input.questions
|
|
504
|
+
).allowed;
|
|
505
|
+
return { row, scores, student, teacher };
|
|
506
|
+
});
|
|
507
|
+
const dimensionErrors = Object.fromEntries(
|
|
508
|
+
import_jev2.JEV_DIMENSIONS.map((key) => {
|
|
509
|
+
const pairs = decisions.filter(
|
|
510
|
+
(item) => item.row.record.scores[key] != null && item.scores[key] != null
|
|
511
|
+
);
|
|
512
|
+
return [
|
|
513
|
+
key,
|
|
514
|
+
{
|
|
515
|
+
n: pairs.length,
|
|
516
|
+
mae: pairs.length ? mean(
|
|
517
|
+
pairs.map(
|
|
518
|
+
(item) => Math.abs(item.scores[key] - item.row.record.scores[key])
|
|
519
|
+
)
|
|
520
|
+
) : null
|
|
521
|
+
}
|
|
522
|
+
];
|
|
523
|
+
})
|
|
524
|
+
);
|
|
525
|
+
const selected = (predicate) => decisions.filter(predicate).map((value) => value.row);
|
|
526
|
+
return {
|
|
527
|
+
rows: rows.length,
|
|
528
|
+
start: rows[0].record.input.timestamp,
|
|
529
|
+
end: rows[rows.length - 1].record.input.timestamp,
|
|
530
|
+
agreement: decisions.filter((item) => item.student === item.teacher).length / rows.length,
|
|
531
|
+
teacherOnly: decisions.filter((item) => item.teacher && !item.student).length,
|
|
532
|
+
studentOnly: decisions.filter((item) => item.student && !item.teacher).length,
|
|
533
|
+
dimensionErrors,
|
|
534
|
+
outcomeScope: "Matched completed outcomes only; this is not a sequential portfolio backtest. Unexecuted candidates have no observed outcome.",
|
|
535
|
+
cohorts: Object.fromEntries(
|
|
536
|
+
["ALL", "LONG", "SHORT"].map((side) => [
|
|
537
|
+
side,
|
|
538
|
+
{
|
|
539
|
+
core: economics(
|
|
540
|
+
selected(
|
|
541
|
+
(item) => side === "ALL" || item.row.record.input.direction === side
|
|
542
|
+
)
|
|
543
|
+
),
|
|
544
|
+
baseline: economics(
|
|
545
|
+
selected(
|
|
546
|
+
(item) => item.row.baselineAllowed === true && (side === "ALL" || item.row.record.input.direction === side)
|
|
547
|
+
)
|
|
548
|
+
),
|
|
549
|
+
jev: economics(
|
|
550
|
+
selected(
|
|
551
|
+
(item) => item.teacher && (side === "ALL" || item.row.record.input.direction === side)
|
|
552
|
+
)
|
|
553
|
+
),
|
|
554
|
+
local: economics(
|
|
555
|
+
selected(
|
|
556
|
+
(item) => item.student && (side === "ALL" || item.row.record.input.direction === side)
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
])
|
|
561
|
+
)
|
|
562
|
+
};
|
|
563
|
+
};
|
|
564
|
+
trainJevGate = (input, options = {}) => {
|
|
565
|
+
const rows = validateJevStudy(input);
|
|
566
|
+
const maxDepth = options.maxDepth ?? 3, minLeaf = options.minLeaf ?? 10;
|
|
567
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 6 || !Number.isInteger(minLeaf) || minLeaf < 2)
|
|
568
|
+
throw new Error("Invalid Jev training limits");
|
|
569
|
+
const timestamps = [
|
|
570
|
+
...new Set(rows.map((row) => row.record.input.timestamp))
|
|
571
|
+
];
|
|
572
|
+
if (rows.length < 30 || timestamps.length < 10)
|
|
573
|
+
throw new Error("Need at least 30 distinct samples across 10 signal times");
|
|
574
|
+
const trainEnd = timestamps[Math.floor(timestamps.length * 0.6) - 1];
|
|
575
|
+
const validationEnd = timestamps[Math.floor(timestamps.length * 0.8) - 1];
|
|
576
|
+
const train = rows.filter((row) => row.record.input.timestamp <= trainEnd);
|
|
577
|
+
const validation = rows.filter(
|
|
578
|
+
(row) => row.record.input.timestamp > trainEnd && row.record.input.timestamp <= validationEnd
|
|
579
|
+
);
|
|
580
|
+
const test = rows.filter((row) => row.record.input.timestamp > validationEnd);
|
|
581
|
+
const fit = (samples) => Object.fromEntries(
|
|
582
|
+
import_jev2.JEV_DIMENSIONS.map((key) => {
|
|
583
|
+
const eligible = samples.filter(
|
|
584
|
+
(row) => row.record.scores[key] != null
|
|
585
|
+
);
|
|
586
|
+
return [
|
|
587
|
+
key,
|
|
588
|
+
eligible.length ? trainTree(eligible, key, maxDepth, minLeaf) : null
|
|
589
|
+
];
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
const first = rows[0].record;
|
|
593
|
+
const model = {
|
|
594
|
+
schema: "tradejs-jev-gate/v2",
|
|
595
|
+
inputSchema: "tradejs-jev-input/v4",
|
|
596
|
+
strategy: first.input.strategy,
|
|
597
|
+
questionsHash: (0, import_jev3.jevHash)(JEV_QUESTIONS),
|
|
598
|
+
teacherModel: first.response.model,
|
|
599
|
+
trees: fit(train),
|
|
600
|
+
training: {
|
|
601
|
+
datasetHash: (0, import_jev3.jevHash)(
|
|
602
|
+
rows.map((row) => ({
|
|
603
|
+
id: row.record.id,
|
|
604
|
+
response: row.record.response
|
|
605
|
+
}))
|
|
606
|
+
),
|
|
607
|
+
trainEnd,
|
|
608
|
+
validationEnd,
|
|
609
|
+
testEnd: timestamps[timestamps.length - 1],
|
|
610
|
+
maxDepth,
|
|
611
|
+
minLeaf
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
validateJevGateModel(model);
|
|
615
|
+
const walkForward = [0.4, 0.6, 0.8].map((fraction) => {
|
|
616
|
+
const trainThrough = timestamps[Math.max(0, Math.floor(timestamps.length * (fraction - 0.1)) - 1)];
|
|
617
|
+
const validationThrough = timestamps[Math.floor(timestamps.length * fraction) - 1];
|
|
618
|
+
const testThrough = timestamps[Math.min(
|
|
619
|
+
timestamps.length - 1,
|
|
620
|
+
Math.round(timestamps.length * (fraction + 0.2)) - 1
|
|
621
|
+
)];
|
|
622
|
+
const fitting = rows.filter(
|
|
623
|
+
(row) => row.record.input.timestamp <= trainThrough
|
|
624
|
+
);
|
|
625
|
+
const evaluation = rows.filter(
|
|
626
|
+
(row) => row.record.input.timestamp > validationThrough && row.record.input.timestamp <= testThrough
|
|
627
|
+
);
|
|
628
|
+
const fold = {
|
|
629
|
+
...model,
|
|
630
|
+
trees: fit(fitting),
|
|
631
|
+
training: {
|
|
632
|
+
...model.training,
|
|
633
|
+
trainEnd: trainThrough,
|
|
634
|
+
validationEnd: validationThrough,
|
|
635
|
+
testEnd: testThrough
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
return {
|
|
639
|
+
trainThrough,
|
|
640
|
+
validationThrough,
|
|
641
|
+
testThrough,
|
|
642
|
+
trainRows: fitting.length,
|
|
643
|
+
test: compareJevGate(fold, evaluation)
|
|
644
|
+
};
|
|
645
|
+
});
|
|
646
|
+
return {
|
|
647
|
+
model,
|
|
648
|
+
report: {
|
|
649
|
+
train: compareJevGate(model, train),
|
|
650
|
+
validation: compareJevGate(model, validation),
|
|
651
|
+
test: compareJevGate(model, test),
|
|
652
|
+
walkForward,
|
|
653
|
+
promotion: "research-only; validate a sequential backtest and prospective signals before runtime admission"
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
30
660
|
// src/registry.ts
|
|
31
661
|
var registry_exports = {};
|
|
32
662
|
__export(registry_exports, {
|
|
@@ -45,6 +675,259 @@ __export(registry_exports, {
|
|
|
45
675
|
});
|
|
46
676
|
module.exports = __toCommonJS(registry_exports);
|
|
47
677
|
|
|
678
|
+
// src/strategyRuntime.ts
|
|
679
|
+
var import_jev6 = require("@tradejs/core/jev");
|
|
680
|
+
|
|
681
|
+
// src/jev.ts
|
|
682
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
683
|
+
var import_node_path = __toESM(require("path"));
|
|
684
|
+
var import_jev4 = require("@tradejs/core/jev");
|
|
685
|
+
var import_jev5 = require("@tradejs/infra/jev");
|
|
686
|
+
var import_userSettings = require("@tradejs/infra/userSettings");
|
|
687
|
+
init_jevInput();
|
|
688
|
+
init_jevInput();
|
|
689
|
+
init_jevTraining();
|
|
690
|
+
var resolveJevProvider = async (userName) => {
|
|
691
|
+
const settings = await (0, import_userSettings.getUserSettings)(userName);
|
|
692
|
+
const provider = {
|
|
693
|
+
endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
|
|
694
|
+
model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
|
|
695
|
+
};
|
|
696
|
+
(0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
|
|
697
|
+
if (!settings.JEV_API_KEY)
|
|
698
|
+
throw new Error("Configure the Jev API key in Account settings");
|
|
699
|
+
return provider;
|
|
700
|
+
};
|
|
701
|
+
var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
|
|
702
|
+
var pending = /* @__PURE__ */ new Map();
|
|
703
|
+
var pendingResponses = /* @__PURE__ */ new Map();
|
|
704
|
+
var evaluateJevInput = async ({
|
|
705
|
+
input,
|
|
706
|
+
config,
|
|
707
|
+
userName,
|
|
708
|
+
projectRoot
|
|
709
|
+
}) => {
|
|
710
|
+
config = (0, import_jev4.parseJevConfig)(config);
|
|
711
|
+
const inputHash = (0, import_jev5.jevHash)(input);
|
|
712
|
+
const questions = questionsForJevInput(input);
|
|
713
|
+
const questionsHash = (0, import_jev5.jevHash)(questions);
|
|
714
|
+
const base = {
|
|
715
|
+
schema: "tradejs-signal-assessment/v5",
|
|
716
|
+
source: config.source,
|
|
717
|
+
mode: config.mode,
|
|
718
|
+
inputHash,
|
|
719
|
+
questionsHash,
|
|
720
|
+
geometryStatus: input.geometryStatus,
|
|
721
|
+
levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
|
|
722
|
+
};
|
|
723
|
+
if (!input.questions.length)
|
|
724
|
+
return {
|
|
725
|
+
assessment: {
|
|
726
|
+
...base,
|
|
727
|
+
status: "unavailable",
|
|
728
|
+
model: "",
|
|
729
|
+
scores: emptyScores(),
|
|
730
|
+
confidence: emptyScores()
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
if (config.source === "local") {
|
|
734
|
+
const contents = await import_promises.default.readFile(
|
|
735
|
+
import_node_path.default.resolve(projectRoot, config.modelFile),
|
|
736
|
+
"utf8"
|
|
737
|
+
);
|
|
738
|
+
if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
|
|
739
|
+
throw new Error("Local Jev gate checksum mismatch");
|
|
740
|
+
const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
|
|
741
|
+
const model = validateJevGateModel2(JSON.parse(contents));
|
|
742
|
+
if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
|
|
743
|
+
throw new Error("Local Jev gate strategy or question version mismatch");
|
|
744
|
+
const scores = Object.fromEntries(
|
|
745
|
+
import_jev4.JEV_DIMENSIONS.map((key) => [
|
|
746
|
+
key,
|
|
747
|
+
input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
|
|
748
|
+
])
|
|
749
|
+
);
|
|
750
|
+
return {
|
|
751
|
+
assessment: {
|
|
752
|
+
...base,
|
|
753
|
+
status: "available",
|
|
754
|
+
model: config.modelSha256,
|
|
755
|
+
scores,
|
|
756
|
+
confidence: emptyScores()
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const provider = config.provider ?? await resolveJevProvider(userName);
|
|
761
|
+
const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
|
|
762
|
+
const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
|
|
763
|
+
const file = import_node_path.default.join(dir, "records", `${id}.json`);
|
|
764
|
+
const getRecord = async () => {
|
|
765
|
+
const existing = await (0, import_jev5.readJevArtifact)(file);
|
|
766
|
+
if (existing) {
|
|
767
|
+
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))
|
|
768
|
+
throw new Error("Jev recording identity mismatch");
|
|
769
|
+
(0, import_jev5.validateJevResponse)(existing.response, input.questions);
|
|
770
|
+
for (const key of import_jev4.JEV_DIMENSIONS) {
|
|
771
|
+
const expected = input.questions.includes(key) ? existing.response.answers[key].score / 4 : null;
|
|
772
|
+
if (existing.scores[key] !== expected)
|
|
773
|
+
throw new Error("Jev recording scores do not match the response");
|
|
774
|
+
}
|
|
775
|
+
return existing;
|
|
776
|
+
}
|
|
777
|
+
if (config.source === "recorded")
|
|
778
|
+
throw new Error(
|
|
779
|
+
`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.`
|
|
780
|
+
);
|
|
781
|
+
const settings = await (0, import_userSettings.getUserSettings)(userName);
|
|
782
|
+
if (!settings.JEV_API_KEY)
|
|
783
|
+
throw new Error("Configure the Jev API key in Account settings");
|
|
784
|
+
const apiKey = settings.JEV_API_KEY;
|
|
785
|
+
const configuredProvider = await resolveJevProvider(userName);
|
|
786
|
+
if (configuredProvider.endpoint !== provider.endpoint)
|
|
787
|
+
throw new Error(
|
|
788
|
+
"Jev account endpoint changed; update the frozen provider configuration"
|
|
789
|
+
);
|
|
790
|
+
const started = Date.now();
|
|
791
|
+
const state = stateForJevInput(input);
|
|
792
|
+
const responseId = (0, import_jev5.jevHash)({
|
|
793
|
+
state,
|
|
794
|
+
questionsHash,
|
|
795
|
+
provider,
|
|
796
|
+
user: (0, import_jev5.jevHash)(userName)
|
|
797
|
+
});
|
|
798
|
+
const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
|
|
799
|
+
let responsePromise = pendingResponses.get(responseFile);
|
|
800
|
+
if (!responsePromise) {
|
|
801
|
+
responsePromise = (async () => {
|
|
802
|
+
const saved = await (0, import_jev5.readJevArtifact)(responseFile);
|
|
803
|
+
if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
|
|
804
|
+
const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
|
|
805
|
+
await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
|
|
806
|
+
return fresh;
|
|
807
|
+
})();
|
|
808
|
+
pendingResponses.set(responseFile, responsePromise);
|
|
809
|
+
}
|
|
810
|
+
let response;
|
|
811
|
+
try {
|
|
812
|
+
response = await responsePromise;
|
|
813
|
+
} finally {
|
|
814
|
+
pendingResponses.delete(responseFile);
|
|
815
|
+
}
|
|
816
|
+
const scores = Object.fromEntries(
|
|
817
|
+
import_jev4.JEV_DIMENSIONS.map((key) => [
|
|
818
|
+
key,
|
|
819
|
+
input.questions.includes(key) ? response.answers[key].score / 4 : null
|
|
820
|
+
])
|
|
821
|
+
);
|
|
822
|
+
const value = {
|
|
823
|
+
schema: "tradejs-jev-record/v4",
|
|
824
|
+
id,
|
|
825
|
+
inputHash,
|
|
826
|
+
questionsHash,
|
|
827
|
+
provider,
|
|
828
|
+
input,
|
|
829
|
+
response,
|
|
830
|
+
scores,
|
|
831
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
832
|
+
elapsedMs: Date.now() - started
|
|
833
|
+
};
|
|
834
|
+
await (0, import_jev5.writeJevArtifact)(file, value);
|
|
835
|
+
return await (0, import_jev5.readJevArtifact)(file);
|
|
836
|
+
};
|
|
837
|
+
const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
|
|
838
|
+
let promise = pending.get(pendingKey);
|
|
839
|
+
if (!promise) {
|
|
840
|
+
promise = getRecord();
|
|
841
|
+
pending.set(pendingKey, promise);
|
|
842
|
+
}
|
|
843
|
+
let record;
|
|
844
|
+
try {
|
|
845
|
+
record = await promise;
|
|
846
|
+
} finally {
|
|
847
|
+
pending.delete(pendingKey);
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
record,
|
|
851
|
+
assessment: {
|
|
852
|
+
...base,
|
|
853
|
+
status: "available",
|
|
854
|
+
recordId: id,
|
|
855
|
+
model: record.response.model,
|
|
856
|
+
scores: record.scores,
|
|
857
|
+
confidence: Object.fromEntries(
|
|
858
|
+
import_jev4.JEV_DIMENSIONS.map((key) => [
|
|
859
|
+
key,
|
|
860
|
+
record.response.answers[key]?.confidence ?? null
|
|
861
|
+
])
|
|
862
|
+
)
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
};
|
|
866
|
+
var assessSignalWithJev = async ({
|
|
867
|
+
signal,
|
|
868
|
+
config,
|
|
869
|
+
userName,
|
|
870
|
+
projectRoot,
|
|
871
|
+
strict,
|
|
872
|
+
scope = "runtime"
|
|
873
|
+
}) => {
|
|
874
|
+
let assessment;
|
|
875
|
+
try {
|
|
876
|
+
const input = buildJevInput(signal);
|
|
877
|
+
({ assessment } = await evaluateJevInput({
|
|
878
|
+
input,
|
|
879
|
+
config,
|
|
880
|
+
userName,
|
|
881
|
+
projectRoot
|
|
882
|
+
}));
|
|
883
|
+
} catch (error) {
|
|
884
|
+
if (strict) throw error;
|
|
885
|
+
assessment = {
|
|
886
|
+
schema: "tradejs-signal-assessment/v5",
|
|
887
|
+
source: config.source,
|
|
888
|
+
mode: config.mode,
|
|
889
|
+
status: "unavailable",
|
|
890
|
+
inputHash: "",
|
|
891
|
+
questionsHash: "",
|
|
892
|
+
geometryStatus: "absent",
|
|
893
|
+
levelsValid: null,
|
|
894
|
+
model: config.provider?.model ?? config.modelSha256 ?? "",
|
|
895
|
+
scores: emptyScores(),
|
|
896
|
+
confidence: emptyScores()
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
signal.assessment = assessment;
|
|
900
|
+
signal.additionalIndicators = {
|
|
901
|
+
...signal.additionalIndicators,
|
|
902
|
+
jev: {
|
|
903
|
+
schema: "tradejs-jev-features/v2",
|
|
904
|
+
status: assessment.status,
|
|
905
|
+
scores: assessment.scores,
|
|
906
|
+
confidence: assessment.confidence
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
const identity = {
|
|
910
|
+
signalId: signal.signalId,
|
|
911
|
+
strategy: signal.strategy,
|
|
912
|
+
symbol: signal.symbol,
|
|
913
|
+
timestamp: signal.timestamp,
|
|
914
|
+
scope,
|
|
915
|
+
user: (0, import_jev5.jevHash)(userName),
|
|
916
|
+
config,
|
|
917
|
+
assessment
|
|
918
|
+
};
|
|
919
|
+
await (0, import_jev5.writeJevArtifact)(
|
|
920
|
+
import_node_path.default.resolve(
|
|
921
|
+
projectRoot,
|
|
922
|
+
config.recordsDir ?? "data/ai/jev",
|
|
923
|
+
"decisions",
|
|
924
|
+
`${(0, import_jev5.jevHash)(identity)}.json`
|
|
925
|
+
),
|
|
926
|
+
identity
|
|
927
|
+
);
|
|
928
|
+
return assessment;
|
|
929
|
+
};
|
|
930
|
+
|
|
48
931
|
// src/strategyRuntime.ts
|
|
49
932
|
var import_constants5 = require("@tradejs/core/constants");
|
|
50
933
|
var import_strategies6 = require("@tradejs/core/strategies");
|
|
@@ -1349,8 +2232,11 @@ var toRecord2 = (value) => {
|
|
|
1349
2232
|
return value;
|
|
1350
2233
|
};
|
|
1351
2234
|
var buildBaseAiPayload = (signal) => {
|
|
2235
|
+
const { jevEvidence: _jevEvidence, ...signalAdditionalIndicators } = toRecord2(
|
|
2236
|
+
signal.additionalIndicators
|
|
2237
|
+
);
|
|
1352
2238
|
const additionalIndicators = {
|
|
1353
|
-
...
|
|
2239
|
+
...signalAdditionalIndicators,
|
|
1354
2240
|
marketContext: buildAiMarketContext(signal)
|
|
1355
2241
|
};
|
|
1356
2242
|
return {
|
|
@@ -1404,7 +2290,7 @@ var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiP
|
|
|
1404
2290
|
var import_aiEndpoints = require("@tradejs/core/aiEndpoints");
|
|
1405
2291
|
var import_aiModels = require("@tradejs/core/aiModels");
|
|
1406
2292
|
var import_aiLanguages = require("@tradejs/core/aiLanguages");
|
|
1407
|
-
var
|
|
2293
|
+
var import_userSettings2 = require("@tradejs/infra/userSettings");
|
|
1408
2294
|
var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
|
|
1409
2295
|
var userSettingsCache = /* @__PURE__ */ new Map();
|
|
1410
2296
|
var aiModelCache = /* @__PURE__ */ new Map();
|
|
@@ -1452,7 +2338,7 @@ var getOpenRouterModelKwargs = (apiEndpoint) => {
|
|
|
1452
2338
|
var getAiUserSettings = async (userName = "root") => {
|
|
1453
2339
|
let settingsPromise = userSettingsCache.get(userName);
|
|
1454
2340
|
if (!settingsPromise) {
|
|
1455
|
-
settingsPromise = (0,
|
|
2341
|
+
settingsPromise = (0, import_userSettings2.getUserSettings)(userName).then((settings2) => {
|
|
1456
2342
|
const endpoint = (0, import_aiEndpoints.normalizeAiEndpoint)(settings2.AI_API_ENDPOINT);
|
|
1457
2343
|
return {
|
|
1458
2344
|
...settings2,
|
|
@@ -3412,7 +4298,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
|
|
|
3412
4298
|
|
|
3413
4299
|
// src/strategyHelpers/derivativesContext.ts
|
|
3414
4300
|
var import_indicators2 = require("@tradejs/core/indicators");
|
|
3415
|
-
var
|
|
4301
|
+
var import_data2 = require("@tradejs/core/data");
|
|
3416
4302
|
var import_strategies3 = require("@tradejs/core/strategies");
|
|
3417
4303
|
var import_constants2 = require("@tradejs/core/constants");
|
|
3418
4304
|
var import_derivatives = require("@tradejs/infra/timescale/derivatives");
|
|
@@ -3572,7 +4458,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
|
|
|
3572
4458
|
const referenceSymbols = getDerivativesContextReferenceSymbols();
|
|
3573
4459
|
const targetSymbol = normalizeSymbol(signal.symbol);
|
|
3574
4460
|
const lookbackMs = parseLookbackMs();
|
|
3575
|
-
const decisionTimeMs = signal.timestamp + (0,
|
|
4461
|
+
const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
|
|
3576
4462
|
const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
|
|
3577
4463
|
decisionTimeMs,
|
|
3578
4464
|
"15m"
|
|
@@ -3657,7 +4543,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
|
|
|
3657
4543
|
};
|
|
3658
4544
|
|
|
3659
4545
|
// src/strategyHelpers/hyperliquidWhaleContext.ts
|
|
3660
|
-
var
|
|
4546
|
+
var import_data3 = require("@tradejs/core/data");
|
|
3661
4547
|
var import_strategies4 = require("@tradejs/core/strategies");
|
|
3662
4548
|
var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
|
|
3663
4549
|
var import_logger6 = require("@tradejs/infra/logger");
|
|
@@ -4131,7 +5017,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
|
|
|
4131
5017
|
const symbol = resolveHyperliquidPerpFromSignalSymbol(params.symbol);
|
|
4132
5018
|
if (!symbol) return null;
|
|
4133
5019
|
const interval = params.marketInterval ?? signalIntervalToMarketInterval2(params.interval);
|
|
4134
|
-
const decisionTimeMs = params.timestamp + (0,
|
|
5020
|
+
const decisionTimeMs = params.timestamp + (0, import_data3.intervalToMs)(params.interval);
|
|
4135
5021
|
const maxAgeMs = params.maxAgeMs ?? DEFAULT_MAX_AGE_BY_INTERVAL2[interval];
|
|
4136
5022
|
const universe = getHyperliquidPerpUniverseSnapshot();
|
|
4137
5023
|
const whales = getHyperliquidWhaleRegistrySnapshot();
|
|
@@ -4158,9 +5044,9 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
|
|
|
4158
5044
|
abortSignal: params.abortSignal
|
|
4159
5045
|
});
|
|
4160
5046
|
} else {
|
|
4161
|
-
let
|
|
4162
|
-
if (!
|
|
4163
|
-
|
|
5047
|
+
let pending2 = contextCache.get(cacheKey);
|
|
5048
|
+
if (!pending2) {
|
|
5049
|
+
pending2 = (0, import_hyperliquidWhales2.getHyperliquidWhaleFlowAggregate)({
|
|
4164
5050
|
symbol,
|
|
4165
5051
|
interval,
|
|
4166
5052
|
decisionTimeMs,
|
|
@@ -4169,9 +5055,9 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
|
|
|
4169
5055
|
whaleRegistryFingerprint: whales.fingerprint,
|
|
4170
5056
|
...params.abortSignal ? { signal: params.abortSignal } : {}
|
|
4171
5057
|
});
|
|
4172
|
-
setBoundedCache2(cacheKey,
|
|
5058
|
+
setBoundedCache2(cacheKey, pending2);
|
|
4173
5059
|
}
|
|
4174
|
-
row = await
|
|
5060
|
+
row = await pending2;
|
|
4175
5061
|
}
|
|
4176
5062
|
if (!row) return null;
|
|
4177
5063
|
return toBaseHyperliquidWhaleFlowContext(row, minimumCoveragePct);
|
|
@@ -4732,7 +5618,8 @@ var executeEntryOrder = async ({
|
|
|
4732
5618
|
runtimeConfigId: signal.runtimeConfigId,
|
|
4733
5619
|
strategyRevision: signal.strategyRevision,
|
|
4734
5620
|
runtimeLineage: signal.runtimeLineage,
|
|
4735
|
-
...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
|
|
5621
|
+
...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
|
|
5622
|
+
...signal.assessment ? { assessment: signal.assessment } : {}
|
|
4736
5623
|
});
|
|
4737
5624
|
}
|
|
4738
5625
|
if (orderPlaced && recordRuntimeTrade && isPositionIncrease) {
|
|
@@ -4805,7 +5692,7 @@ var resolveStrategyConfig = async ({
|
|
|
4805
5692
|
|
|
4806
5693
|
// src/strategy/runtimeBacktestDelay.ts
|
|
4807
5694
|
var import_constants4 = require("@tradejs/core/constants");
|
|
4808
|
-
var
|
|
5695
|
+
var import_data4 = require("@tradejs/core/data");
|
|
4809
5696
|
var import_strategies5 = require("@tradejs/core/strategies");
|
|
4810
5697
|
var resolveBacktestEntryDelayBars = (value) => {
|
|
4811
5698
|
if (value == null || value === "") {
|
|
@@ -4833,7 +5720,7 @@ var resolveBacktestExecutionDelayMs = (value, fallbackDelayMs) => {
|
|
|
4833
5720
|
};
|
|
4834
5721
|
var safeIntervalToMs = (interval) => {
|
|
4835
5722
|
try {
|
|
4836
|
-
return (0,
|
|
5723
|
+
return (0, import_data4.intervalToMs)(interval);
|
|
4837
5724
|
} catch {
|
|
4838
5725
|
return null;
|
|
4839
5726
|
}
|
|
@@ -5457,6 +6344,7 @@ var handleExitDecision = async ({
|
|
|
5457
6344
|
onRuntimeClose?.({
|
|
5458
6345
|
userName,
|
|
5459
6346
|
strategy: strategyName,
|
|
6347
|
+
deploymentId: connector.deploymentId,
|
|
5460
6348
|
openedByStrategy: trade.strategy,
|
|
5461
6349
|
symbol,
|
|
5462
6350
|
direction: trade.direction,
|
|
@@ -5749,6 +6637,11 @@ var createStrategyRuntime = ({
|
|
|
5749
6637
|
const projectConfig = await loadTradejsConfig(projectRoot);
|
|
5750
6638
|
const projectHooks = projectConfig.hooks;
|
|
5751
6639
|
const env = String(config.ENV ?? "BACKTEST");
|
|
6640
|
+
const jev = (0, import_jev6.parseJevConfig)(config.JEV);
|
|
6641
|
+
if (jev && env !== "BACKTEST" && jev.source === "provider" && !jev.provider)
|
|
6642
|
+
throw new Error(
|
|
6643
|
+
"Runtime JEV requires a frozen provider endpoint and model"
|
|
6644
|
+
);
|
|
5752
6645
|
const backtestPriceMode = config.BACKTEST_PRICE_MODE ?? "open";
|
|
5753
6646
|
const backtestEntryDelayBars = env === "BACKTEST" ? resolveBacktestEntryDelayBars(config.BACKTEST_ENTRY_DELAY_BARS) : 0;
|
|
5754
6647
|
const resolvedBacktestExecutionInterval = config.BACKTEST_EXECUTION_INTERVAL ?? backtestExecutionMarketData?.interval ?? resolveBacktestExecutionIntervalForPrimary(config.INTERVAL ?? "15");
|
|
@@ -6169,44 +7062,44 @@ var createStrategyRuntime = ({
|
|
|
6169
7062
|
if (pendingBacktestEntry.delayBarsRemaining > 0) {
|
|
6170
7063
|
return `BACKTEST_ENTRY_DELAY_PENDING:${pendingBacktestEntry.delayBarsRemaining}`;
|
|
6171
7064
|
}
|
|
6172
|
-
const
|
|
7065
|
+
const pending2 = pendingBacktestEntry;
|
|
6173
7066
|
pendingBacktestEntry = null;
|
|
6174
7067
|
const executionCandleResolution = resolveBacktestExecutionCandle(
|
|
6175
7068
|
candle,
|
|
6176
7069
|
btcCandle
|
|
6177
7070
|
);
|
|
6178
7071
|
const execution = applyBacktestDelayedEntryExecution({
|
|
6179
|
-
decision:
|
|
7072
|
+
decision: pending2.decision,
|
|
6180
7073
|
execution: executionCandleResolution,
|
|
6181
7074
|
backtestPriceMode: executionCandleResolution.source === "primary_timeframe" ? "open" : backtestPriceMode,
|
|
6182
|
-
delayBars:
|
|
7075
|
+
delayBars: pending2.delayBars
|
|
6183
7076
|
});
|
|
6184
7077
|
if (execution.skipReason) {
|
|
6185
|
-
return
|
|
7078
|
+
return pending2.decision.signal ?? execution.skipReason;
|
|
6186
7079
|
}
|
|
6187
7080
|
if (!execution.executionCandle || !execution.btcExecutionCandle) {
|
|
6188
|
-
return
|
|
7081
|
+
return pending2.decision.signal ?? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING";
|
|
6189
7082
|
}
|
|
6190
7083
|
const market = {
|
|
6191
7084
|
candle: execution.executionCandle,
|
|
6192
7085
|
btcCandle: execution.btcExecutionCandle
|
|
6193
7086
|
};
|
|
6194
7087
|
const entry = buildHookEntry({
|
|
6195
|
-
decision:
|
|
6196
|
-
runtime:
|
|
7088
|
+
decision: pending2.decision,
|
|
7089
|
+
runtime: pending2.runtime
|
|
6197
7090
|
});
|
|
6198
7091
|
return executeEntryDecision({
|
|
6199
7092
|
connector,
|
|
6200
7093
|
symbol,
|
|
6201
|
-
decision:
|
|
6202
|
-
runtime:
|
|
6203
|
-
manifest:
|
|
6204
|
-
hookCtx:
|
|
7094
|
+
decision: pending2.decision,
|
|
7095
|
+
runtime: pending2.runtime,
|
|
7096
|
+
manifest: pending2.manifest,
|
|
7097
|
+
hookCtx: pending2.hookCtx,
|
|
6205
7098
|
market,
|
|
6206
7099
|
entry,
|
|
6207
|
-
policy:
|
|
6208
|
-
ml:
|
|
6209
|
-
ai:
|
|
7100
|
+
policy: pending2.policy,
|
|
7101
|
+
ml: pending2.ml,
|
|
7102
|
+
ai: pending2.ai,
|
|
6210
7103
|
recordRuntimeJournal,
|
|
6211
7104
|
invokeStageHooks,
|
|
6212
7105
|
notifyRuntimeError
|
|
@@ -6456,6 +7349,16 @@ var createStrategyRuntime = ({
|
|
|
6456
7349
|
env,
|
|
6457
7350
|
includeHyperliquidWhales: false
|
|
6458
7351
|
});
|
|
7352
|
+
if (jev) {
|
|
7353
|
+
await assessSignalWithJev({
|
|
7354
|
+
signal,
|
|
7355
|
+
config: jev,
|
|
7356
|
+
userName,
|
|
7357
|
+
projectRoot,
|
|
7358
|
+
strict: env === "BACKTEST",
|
|
7359
|
+
scope: runtimeLineage?.deploymentCompositionId ?? (env === "BACKTEST" ? "backtest" : "runtime")
|
|
7360
|
+
});
|
|
7361
|
+
}
|
|
6459
7362
|
quality = await enrichSignalWithAi({
|
|
6460
7363
|
signal,
|
|
6461
7364
|
userName,
|
|
@@ -6525,6 +7428,7 @@ var createStrategyRuntime = ({
|
|
|
6525
7428
|
if (!shouldMakeOrder) {
|
|
6526
7429
|
if (signal) {
|
|
6527
7430
|
const skipReason = getEntrySkipReason({
|
|
7431
|
+
signal,
|
|
6528
7432
|
makeOrdersEnabled,
|
|
6529
7433
|
env,
|
|
6530
7434
|
ml,
|