@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.
- 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/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
|
-
...
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
4124
|
-
if (!
|
|
4125
|
-
|
|
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,
|
|
5020
|
+
setBoundedCache2(cacheKey, pending2);
|
|
4135
5021
|
}
|
|
4136
|
-
row = await
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
7034
|
+
decision: pending2.decision,
|
|
6142
7035
|
execution: executionCandleResolution,
|
|
6143
7036
|
backtestPriceMode: executionCandleResolution.source === "primary_timeframe" ? "open" : backtestPriceMode,
|
|
6144
|
-
delayBars:
|
|
7037
|
+
delayBars: pending2.delayBars
|
|
6145
7038
|
});
|
|
6146
7039
|
if (execution.skipReason) {
|
|
6147
|
-
return
|
|
7040
|
+
return pending2.decision.signal ?? execution.skipReason;
|
|
6148
7041
|
}
|
|
6149
7042
|
if (!execution.executionCandle || !execution.btcExecutionCandle) {
|
|
6150
|
-
return
|
|
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:
|
|
6158
|
-
runtime:
|
|
7050
|
+
decision: pending2.decision,
|
|
7051
|
+
runtime: pending2.runtime
|
|
6159
7052
|
});
|
|
6160
7053
|
return executeEntryDecision({
|
|
6161
7054
|
connector,
|
|
6162
7055
|
symbol,
|
|
6163
|
-
decision:
|
|
6164
|
-
runtime:
|
|
6165
|
-
manifest:
|
|
6166
|
-
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:
|
|
6170
|
-
ml:
|
|
6171
|
-
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
|
|
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,
|
|
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
|
|
7274
|
-
cache.set(key,
|
|
7275
|
-
return
|
|
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,
|