@tradejs/cli 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/README.md +126 -0
- package/dist/cli.js +1370 -1016
- package/dist/lib/jevResearch.js +229 -0
- package/dist/lib/runtimeEvidenceCompositions.js +8 -1
- package/dist/scripts/backtest.js +80 -19
- package/dist/scripts/jev.js +269 -0
- package/dist/scripts/runtimeEvidence.js +8 -1
- package/dist/scripts/signals.js +1 -0
- package/dist/scripts/signalsDaemon.js +1 -0
- package/dist/scripts/signalsSummary.js +108 -78
- package/package.json +7 -7
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/lib/jevResearch.ts
|
|
31
|
+
var jevResearch_exports = {};
|
|
32
|
+
__export(jevResearch_exports, {
|
|
33
|
+
readJevJsonl: () => readJevJsonl,
|
|
34
|
+
runJevResearch: () => runJevResearch
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(jevResearch_exports);
|
|
37
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
38
|
+
var import_node_fs = require("fs");
|
|
39
|
+
var import_node_readline = __toESM(require("readline"));
|
|
40
|
+
var import_node_path = __toESM(require("path"));
|
|
41
|
+
var import_node_crypto = require("crypto");
|
|
42
|
+
var import_jev = require("@tradejs/infra/jev");
|
|
43
|
+
var import_jev2 = require("@tradejs/node/jev");
|
|
44
|
+
var readJevJsonl = async (file) => {
|
|
45
|
+
const rows = [];
|
|
46
|
+
const stream = (0, import_node_fs.createReadStream)(file);
|
|
47
|
+
const reader = import_node_readline.default.createInterface({
|
|
48
|
+
input: stream,
|
|
49
|
+
crlfDelay: Infinity
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
for await (const line of reader) {
|
|
53
|
+
if (!line.trim()) continue;
|
|
54
|
+
if (rows.length >= 2e4)
|
|
55
|
+
throw new Error("Select at most 20000 rows for this bounded Jev study");
|
|
56
|
+
rows.push(JSON.parse(line));
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
reader.close();
|
|
60
|
+
stream.destroy();
|
|
61
|
+
}
|
|
62
|
+
return rows;
|
|
63
|
+
};
|
|
64
|
+
var writeNew = async (file, contents) => {
|
|
65
|
+
await import_promises.default.mkdir(import_node_path.default.dirname(file), { recursive: true });
|
|
66
|
+
await import_promises.default.writeFile(file, contents, { flag: "wx", mode: 384 });
|
|
67
|
+
};
|
|
68
|
+
var runJevResearch = async (options) => {
|
|
69
|
+
const resolve = (file) => import_node_path.default.resolve(options.projectRoot, file);
|
|
70
|
+
const out = resolve(options.out);
|
|
71
|
+
try {
|
|
72
|
+
await import_promises.default.access(out);
|
|
73
|
+
throw new Error("Jev output already exists; choose a new output path");
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error.code !== "ENOENT") throw error;
|
|
76
|
+
}
|
|
77
|
+
if (options.action === "evaluate") {
|
|
78
|
+
if (!options.input)
|
|
79
|
+
throw new Error("Provide an AI dataset JSONL with --input");
|
|
80
|
+
const rows2 = await readJevJsonl(resolve(options.input));
|
|
81
|
+
const selected = rows2.filter(
|
|
82
|
+
(row) => !options.strategy || row.strategyName === options.strategy
|
|
83
|
+
);
|
|
84
|
+
if (!selected.length) throw new Error("No matching Jev input rows");
|
|
85
|
+
for (const row of selected) {
|
|
86
|
+
if (!row.assessment?.recordId)
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Signal ${row.signalId} has no Jev recording. Run the backtest with --ai --jev and use its --jevRecordsDir; an --ai-only export cannot restore the signal-time Jev facts.`
|
|
89
|
+
);
|
|
90
|
+
if (!/^[a-f0-9]{64}$/.test(row.assessment.recordId))
|
|
91
|
+
throw new Error(`Invalid Jev recording id for signal ${row.signalId}`);
|
|
92
|
+
}
|
|
93
|
+
await import_promises.default.mkdir(import_node_path.default.dirname(out), { recursive: true });
|
|
94
|
+
const temp = `${out}.${(0, import_node_crypto.randomUUID)()}.tmp`;
|
|
95
|
+
try {
|
|
96
|
+
const handle = await import_promises.default.open(temp, "wx", 384);
|
|
97
|
+
try {
|
|
98
|
+
for (const row of selected) {
|
|
99
|
+
const recordId = row.assessment.recordId;
|
|
100
|
+
const saved = await (0, import_jev.readJevArtifact)(
|
|
101
|
+
resolve(
|
|
102
|
+
import_node_path.default.join(options.recordsDir, "records", `${recordId}.json`)
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
if (!saved)
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Jev recording ${recordId} for signal ${row.signalId} is missing from --jevRecordsDir. Use the recordings from the original --ai --jev backtest.`
|
|
108
|
+
);
|
|
109
|
+
if (saved.schema !== "tradejs-jev-record/v4")
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Jev recording ${recordId} uses an older schema; rerun the backtest with the current --jev integration.`
|
|
112
|
+
);
|
|
113
|
+
if (saved.id !== recordId || saved.inputHash !== row.assessment?.inputHash || saved.questionsHash !== row.assessment?.questionsHash)
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Jev recording ${recordId} does not match signal ${row.signalId}`
|
|
116
|
+
);
|
|
117
|
+
const { record } = await (0, import_jev2.evaluateJevInput)({
|
|
118
|
+
input: saved.input,
|
|
119
|
+
config: {
|
|
120
|
+
source: "recorded",
|
|
121
|
+
mode: "observe",
|
|
122
|
+
provider: saved.provider,
|
|
123
|
+
recordsDir: options.recordsDir
|
|
124
|
+
},
|
|
125
|
+
userName: options.userName,
|
|
126
|
+
projectRoot: options.projectRoot
|
|
127
|
+
});
|
|
128
|
+
if (!record)
|
|
129
|
+
throw new Error(
|
|
130
|
+
`No eligible Jev questions for signal ${row.signalId}`
|
|
131
|
+
);
|
|
132
|
+
const study = {
|
|
133
|
+
schema: "tradejs-jev-study/v4",
|
|
134
|
+
signalId: row.signalId,
|
|
135
|
+
record,
|
|
136
|
+
...Number.isFinite(row.profit) ? { profit: row.profit } : {}
|
|
137
|
+
};
|
|
138
|
+
await handle.write(`${JSON.stringify(study)}
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
await handle.close();
|
|
143
|
+
}
|
|
144
|
+
await import_promises.default.link(temp, out);
|
|
145
|
+
} finally {
|
|
146
|
+
await import_promises.default.rm(temp, { force: true });
|
|
147
|
+
}
|
|
148
|
+
return { rows: selected.length, out };
|
|
149
|
+
}
|
|
150
|
+
if (options.action === "export") {
|
|
151
|
+
const dir = resolve(import_node_path.default.join(options.recordsDir, "records"));
|
|
152
|
+
const rows2 = [];
|
|
153
|
+
const outcomes = options.input ? await readJevJsonl(resolve(options.input)) : [];
|
|
154
|
+
const byRecord = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const row of outcomes) {
|
|
156
|
+
const id = row.assessment?.recordId;
|
|
157
|
+
if (!id) continue;
|
|
158
|
+
if (!/^[a-f0-9]{64}$/.test(id))
|
|
159
|
+
throw new Error("Invalid Jev recording id");
|
|
160
|
+
const prior = byRecord.get(id);
|
|
161
|
+
if (prior && (prior.profit !== row.profit || prior.signalId !== row.signalId))
|
|
162
|
+
throw new Error("Conflicting outcomes for one Jev recording");
|
|
163
|
+
byRecord.set(id, row);
|
|
164
|
+
}
|
|
165
|
+
for (const file of (await import_promises.default.readdir(dir)).filter((name) => /^[a-f0-9]{64}\.json$/.test(name)).sort()) {
|
|
166
|
+
const record = await (0, import_jev.readJevArtifact)(import_node_path.default.join(dir, file));
|
|
167
|
+
if (!record || options.strategy && record.input.strategy !== options.strategy)
|
|
168
|
+
continue;
|
|
169
|
+
if (rows2.length >= 2e4)
|
|
170
|
+
throw new Error(
|
|
171
|
+
"Export exceeds 20000 samples; select a bounded recording directory"
|
|
172
|
+
);
|
|
173
|
+
const outcome = byRecord.get(record.id);
|
|
174
|
+
rows2.push({
|
|
175
|
+
schema: "tradejs-jev-study/v4",
|
|
176
|
+
signalId: outcome?.signalId ?? record.id,
|
|
177
|
+
record,
|
|
178
|
+
...outcome && Number.isFinite(outcome.profit) ? { profit: outcome.profit } : {}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (!rows2.length) throw new Error("No Jev recordings to export");
|
|
182
|
+
await writeNew(
|
|
183
|
+
out,
|
|
184
|
+
rows2.map((row) => JSON.stringify(row)).join("\n") + "\n"
|
|
185
|
+
);
|
|
186
|
+
return { rows: rows2.length, out };
|
|
187
|
+
}
|
|
188
|
+
if (!options.input) throw new Error("Provide a Jev study JSONL with --input");
|
|
189
|
+
const rows = await readJevJsonl(resolve(options.input));
|
|
190
|
+
if (options.action === "train") {
|
|
191
|
+
const { model, report } = (0, import_jev2.trainJevGate)(rows, options);
|
|
192
|
+
const contents = `${JSON.stringify(model, null, 2)}
|
|
193
|
+
`;
|
|
194
|
+
await writeNew(
|
|
195
|
+
`${out}.report.json`,
|
|
196
|
+
`${JSON.stringify(report, null, 2)}
|
|
197
|
+
`
|
|
198
|
+
);
|
|
199
|
+
await writeNew(out, contents);
|
|
200
|
+
return {
|
|
201
|
+
model: out,
|
|
202
|
+
modelSha256: (0, import_jev.jevFileHash)(contents),
|
|
203
|
+
report: `${out}.report.json`,
|
|
204
|
+
trainEnd: model.training.trainEnd,
|
|
205
|
+
validationEnd: model.training.validationEnd
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (options.action === "compare") {
|
|
209
|
+
if (!options.model) throw new Error("Provide --model");
|
|
210
|
+
const model = (0, import_jev2.validateJevGateModel)(
|
|
211
|
+
JSON.parse(await import_promises.default.readFile(resolve(options.model), "utf8"))
|
|
212
|
+
);
|
|
213
|
+
const heldOut = rows.filter(
|
|
214
|
+
(row) => row.record.input.timestamp > model.training.validationEnd
|
|
215
|
+
);
|
|
216
|
+
if (!heldOut.length)
|
|
217
|
+
throw new Error("No held-out rows after model validationEnd");
|
|
218
|
+
const report = (0, import_jev2.compareJevGate)(model, heldOut);
|
|
219
|
+
await writeNew(out, `${JSON.stringify(report, null, 2)}
|
|
220
|
+
`);
|
|
221
|
+
return report;
|
|
222
|
+
}
|
|
223
|
+
throw new Error("Jev action must be evaluate, export, train, or compare");
|
|
224
|
+
};
|
|
225
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
226
|
+
0 && (module.exports = {
|
|
227
|
+
readJevJsonl,
|
|
228
|
+
runJevResearch
|
|
229
|
+
});
|
|
@@ -402,7 +402,14 @@ var discoverRuntimeEvidenceCompositionSnapshots = async (evidenceRoot) => {
|
|
|
402
402
|
};
|
|
403
403
|
|
|
404
404
|
// src/lib/runtimeEvidenceCompositions.ts
|
|
405
|
-
var stableSnapshotIdentity = (deployment) => JSON.stringify({
|
|
405
|
+
var stableSnapshotIdentity = (deployment) => JSON.stringify({
|
|
406
|
+
...deployment,
|
|
407
|
+
tickers: void 0,
|
|
408
|
+
strategies: deployment.strategies.map((strategy) => ({
|
|
409
|
+
...strategy,
|
|
410
|
+
controlState: void 0
|
|
411
|
+
}))
|
|
412
|
+
});
|
|
406
413
|
var producerIdentity = (producer) => JSON.stringify(producer);
|
|
407
414
|
var loadRuntimeEvidenceCompositionSnapshots = async ({
|
|
408
415
|
publishRoot,
|
package/dist/scripts/backtest.js
CHANGED
|
@@ -65,6 +65,52 @@ var import_data = require("@tradejs/core/data");
|
|
|
65
65
|
var import_redis5 = require("@tradejs/infra/redis");
|
|
66
66
|
var import_constants6 = require("@tradejs/core/constants");
|
|
67
67
|
|
|
68
|
+
// src/lib/backtest/jevOptions.ts
|
|
69
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
70
|
+
var import_node_path = __toESM(require("path"));
|
|
71
|
+
var import_jev = require("@tradejs/core/jev");
|
|
72
|
+
var import_jev2 = require("@tradejs/infra/jev");
|
|
73
|
+
var import_userSettings = require("@tradejs/infra/userSettings");
|
|
74
|
+
var import_jev3 = require("@tradejs/node/jev");
|
|
75
|
+
var resolveBacktestAiExportEnabled = (flags2) => Boolean(flags2.ai);
|
|
76
|
+
var resolveBacktestJev = async (flags2, userName2, projectRoot2) => {
|
|
77
|
+
if (!flags2.jev) {
|
|
78
|
+
if (flags2.jevRecorded || flags2.jevModelFile)
|
|
79
|
+
throw new Error("Jev options require --jev");
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
if (flags2.jevRecorded && flags2.jevModelFile)
|
|
83
|
+
throw new Error("Choose --jevRecorded or --jevModelFile");
|
|
84
|
+
const common = {
|
|
85
|
+
mode: "observe",
|
|
86
|
+
recordsDir: String(flags2.jevRecordsDir || "data/ai/jev")
|
|
87
|
+
};
|
|
88
|
+
if (flags2.jevModelFile) {
|
|
89
|
+
const modelFile = String(flags2.jevModelFile);
|
|
90
|
+
const contents = await import_promises.default.readFile(
|
|
91
|
+
import_node_path.default.resolve(projectRoot2, modelFile),
|
|
92
|
+
"utf8"
|
|
93
|
+
);
|
|
94
|
+
(0, import_jev3.validateJevGateModel)(JSON.parse(contents));
|
|
95
|
+
return (0, import_jev.parseJevConfig)({
|
|
96
|
+
...common,
|
|
97
|
+
source: "local",
|
|
98
|
+
modelFile,
|
|
99
|
+
modelSha256: (0, import_jev2.jevFileHash)(contents)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const settings = flags2.jevRecorded ? await (0, import_userSettings.getUserSettings)(userName2) : null;
|
|
103
|
+
const provider = settings ? {
|
|
104
|
+
endpoint: settings.JEV_API_ENDPOINT || import_jev.JEV_PROVIDERS[0].endpoint,
|
|
105
|
+
model: settings.JEV_MODEL || import_jev.JEV_PROVIDERS[0].model
|
|
106
|
+
} : await (0, import_jev3.resolveJevProvider)(userName2);
|
|
107
|
+
return (0, import_jev.parseJevConfig)({
|
|
108
|
+
...common,
|
|
109
|
+
source: flags2.jevRecorded ? "recorded" : "provider",
|
|
110
|
+
provider
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
68
114
|
// src/lib/backtest/runnerCore.ts
|
|
69
115
|
var import_fs = __toESM(require("fs"));
|
|
70
116
|
var import_path = __toESM(require("path"));
|
|
@@ -906,7 +952,7 @@ var import_constants = require("@tradejs/core/constants");
|
|
|
906
952
|
var import_indicators = require("@tradejs/core/indicators");
|
|
907
953
|
var import_derivatives = require("@tradejs/infra/timescale/derivatives");
|
|
908
954
|
var import_client2 = require("@tradejs/infra/timescale/client");
|
|
909
|
-
var
|
|
955
|
+
var import_userSettings2 = require("@tradejs/infra/userSettings");
|
|
910
956
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
911
957
|
var DAY_MS2 = 24 * HOUR_MS;
|
|
912
958
|
var DEFAULT_LOOKBACK_HOURS = 48;
|
|
@@ -1142,7 +1188,7 @@ var extendEdges = (current, fromMs, toMs) => ({
|
|
|
1142
1188
|
max: current?.max == null ? toMs : Math.max(current.max, Math.trunc(toMs))
|
|
1143
1189
|
});
|
|
1144
1190
|
var getCoinalyzeApiKey = async (userName2) => {
|
|
1145
|
-
const settings = await (0,
|
|
1191
|
+
const settings = await (0, import_userSettings2.getUserSettings)(userName2);
|
|
1146
1192
|
return settings.COINALYZE_API_KEY.trim() || String(process.env.COINALYZE_API_KEY ?? "").trim();
|
|
1147
1193
|
};
|
|
1148
1194
|
var getCoinalyzeBaseUrl = () => process.env.COINALYZE_BASE_URL?.trim() || "https://api.coinalyze.net/v1";
|
|
@@ -1881,7 +1927,7 @@ var import_chalk3 = __toESM(require("chalk"));
|
|
|
1881
1927
|
var import_async2 = require("@tradejs/core/async");
|
|
1882
1928
|
var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
|
|
1883
1929
|
var import_client3 = require("@tradejs/infra/timescale/client");
|
|
1884
|
-
var
|
|
1930
|
+
var import_userSettings3 = require("@tradejs/infra/userSettings");
|
|
1885
1931
|
var DAY_MS3 = 864e5;
|
|
1886
1932
|
var DEFAULT_CONTEXT_MAX_AGE_MS = 48 * 60 * 6e4;
|
|
1887
1933
|
var DEFAULT_FEAR_GREED_STALE_RETRY_MS = 60 * 6e4;
|
|
@@ -2105,27 +2151,27 @@ var createCoinMarketCapFetchStats = () => ({
|
|
|
2105
2151
|
totalCredits: 0,
|
|
2106
2152
|
byPath: /* @__PURE__ */ new Map()
|
|
2107
2153
|
});
|
|
2108
|
-
var getCoinMarketCapPathLabel = (
|
|
2109
|
-
if (
|
|
2110
|
-
if (
|
|
2111
|
-
if (
|
|
2112
|
-
if (
|
|
2113
|
-
if (
|
|
2114
|
-
if (
|
|
2115
|
-
return
|
|
2116
|
-
};
|
|
2117
|
-
var recordCoinMarketCapFetchStats = (stats,
|
|
2154
|
+
var getCoinMarketCapPathLabel = (path4) => {
|
|
2155
|
+
if (path4.includes("global-metrics")) return "global";
|
|
2156
|
+
if (path4.includes("cryptocurrency/quotes")) return "reference";
|
|
2157
|
+
if (path4.includes("exchange/quotes")) return "exchangeLiquidity";
|
|
2158
|
+
if (path4.includes("fear-and-greed")) return "fearGreed";
|
|
2159
|
+
if (path4.includes("cmc100")) return "cmc100";
|
|
2160
|
+
if (path4.includes("cmc20")) return "cmc20";
|
|
2161
|
+
return path4;
|
|
2162
|
+
};
|
|
2163
|
+
var recordCoinMarketCapFetchStats = (stats, path4, creditCount) => {
|
|
2118
2164
|
if (!stats) return;
|
|
2119
|
-
const existing = stats.byPath.get(
|
|
2165
|
+
const existing = stats.byPath.get(path4) ?? { requests: 0, credits: 0 };
|
|
2120
2166
|
existing.requests += 1;
|
|
2121
2167
|
existing.credits += creditCount ?? 0;
|
|
2122
2168
|
stats.requests += 1;
|
|
2123
2169
|
stats.totalCredits += creditCount ?? 0;
|
|
2124
|
-
stats.byPath.set(
|
|
2170
|
+
stats.byPath.set(path4, existing);
|
|
2125
2171
|
};
|
|
2126
2172
|
var formatCoinMarketCapFetchStats = (stats) => {
|
|
2127
2173
|
const byPath = [...stats.byPath.entries()].map(
|
|
2128
|
-
([
|
|
2174
|
+
([path4, item]) => `${getCoinMarketCapPathLabel(path4)}=${item.requests}req/${item.credits}cr`
|
|
2129
2175
|
).join(" ");
|
|
2130
2176
|
return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
|
|
2131
2177
|
};
|
|
@@ -2698,7 +2744,7 @@ var backfillCoinMarketCapContext = async (params) => {
|
|
|
2698
2744
|
);
|
|
2699
2745
|
return skippedResult(true);
|
|
2700
2746
|
}
|
|
2701
|
-
const settings = await (0,
|
|
2747
|
+
const settings = await (0, import_userSettings3.getUserSettings)(params.userName);
|
|
2702
2748
|
const apiKey = settings.COINMARKETCAP_API_KEY.trim();
|
|
2703
2749
|
if (!apiKey) {
|
|
2704
2750
|
throw new Error(
|
|
@@ -4262,6 +4308,18 @@ import_args.default.option(
|
|
|
4262
4308
|
"Write AI prompt rows to per-worker JSONL chunks",
|
|
4263
4309
|
false
|
|
4264
4310
|
);
|
|
4311
|
+
import_args.default.option("jev", "Enrich backtest signals with Jev assessments", false);
|
|
4312
|
+
import_args.default.option(
|
|
4313
|
+
["Q", "jevRecorded"],
|
|
4314
|
+
"Use recorded Jev answers only; fail on a missing answer",
|
|
4315
|
+
false
|
|
4316
|
+
);
|
|
4317
|
+
import_args.default.option(["M", "jevModelFile"], "Use a trained local Jev gate JSON file");
|
|
4318
|
+
import_args.default.option(
|
|
4319
|
+
["W", "jevRecordsDir"],
|
|
4320
|
+
"Jev answer and decision directory",
|
|
4321
|
+
"data/ai/jev"
|
|
4322
|
+
);
|
|
4265
4323
|
import_args.default.option(
|
|
4266
4324
|
"fast",
|
|
4267
4325
|
"Skip per-test artifact persistence and keep only in-memory summary/AI-ML dataset output",
|
|
@@ -5095,7 +5153,8 @@ var buildPreparedTestSuite = async ({
|
|
|
5095
5153
|
const executionCosts = flags.executionCosts == null ? void 0 : (0, import_backtest2.parseBacktestExecutionCosts)(JSON.parse(String(flags.executionCosts)));
|
|
5096
5154
|
for (const test of testSuite)
|
|
5097
5155
|
(0, import_backtest2.assertStrategyExecutionIsolation)(test.strategyConfig);
|
|
5098
|
-
const
|
|
5156
|
+
const jev = await resolveBacktestJev(flags, userName, projectRoot);
|
|
5157
|
+
const aiEnabled = resolveBacktestAiExportEnabled(flags);
|
|
5099
5158
|
const requestedTestsLimit = resolveRequestedTestsLimit({
|
|
5100
5159
|
isLiveMode: isReplay,
|
|
5101
5160
|
requestedLimit: testsLimit,
|
|
@@ -5108,6 +5167,7 @@ var buildPreparedTestSuite = async ({
|
|
|
5108
5167
|
executionCostsCacheOnly: Boolean(flags.cacheOnly),
|
|
5109
5168
|
strategyConfig: {
|
|
5110
5169
|
...test.strategyConfig,
|
|
5170
|
+
JEV: jev,
|
|
5111
5171
|
ENV: "BACKTEST",
|
|
5112
5172
|
INTERVAL: interval,
|
|
5113
5173
|
MAKE_ORDERS: true,
|
|
@@ -5770,7 +5830,8 @@ var backtest = async () => {
|
|
|
5770
5830
|
window: preparedRun.window,
|
|
5771
5831
|
preloadStart: preparedRun.preloadStart,
|
|
5772
5832
|
flags: {
|
|
5773
|
-
ai: Boolean(flags.ai),
|
|
5833
|
+
ai: Boolean(flags.ai || flags.jev),
|
|
5834
|
+
jev: Boolean(flags.jev),
|
|
5774
5835
|
backtestEntryDelayBars,
|
|
5775
5836
|
backtestPriceMode,
|
|
5776
5837
|
cacheOnly: Boolean(flags.cacheOnly),
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/scripts/jev.ts
|
|
31
|
+
var jev_exports = {};
|
|
32
|
+
__export(jev_exports, {
|
|
33
|
+
main: () => main
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(jev_exports);
|
|
36
|
+
var import_args = __toESM(require("args"));
|
|
37
|
+
|
|
38
|
+
// src/lib/jevResearch.ts
|
|
39
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
40
|
+
var import_node_fs = require("fs");
|
|
41
|
+
var import_node_readline = __toESM(require("readline"));
|
|
42
|
+
var import_node_path = __toESM(require("path"));
|
|
43
|
+
var import_node_crypto = require("crypto");
|
|
44
|
+
var import_jev = require("@tradejs/infra/jev");
|
|
45
|
+
var import_jev2 = require("@tradejs/node/jev");
|
|
46
|
+
var readJevJsonl = async (file) => {
|
|
47
|
+
const rows = [];
|
|
48
|
+
const stream = (0, import_node_fs.createReadStream)(file);
|
|
49
|
+
const reader = import_node_readline.default.createInterface({
|
|
50
|
+
input: stream,
|
|
51
|
+
crlfDelay: Infinity
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
for await (const line of reader) {
|
|
55
|
+
if (!line.trim()) continue;
|
|
56
|
+
if (rows.length >= 2e4)
|
|
57
|
+
throw new Error("Select at most 20000 rows for this bounded Jev study");
|
|
58
|
+
rows.push(JSON.parse(line));
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
reader.close();
|
|
62
|
+
stream.destroy();
|
|
63
|
+
}
|
|
64
|
+
return rows;
|
|
65
|
+
};
|
|
66
|
+
var writeNew = async (file, contents) => {
|
|
67
|
+
await import_promises.default.mkdir(import_node_path.default.dirname(file), { recursive: true });
|
|
68
|
+
await import_promises.default.writeFile(file, contents, { flag: "wx", mode: 384 });
|
|
69
|
+
};
|
|
70
|
+
var runJevResearch = async (options) => {
|
|
71
|
+
const resolve = (file) => import_node_path.default.resolve(options.projectRoot, file);
|
|
72
|
+
const out = resolve(options.out);
|
|
73
|
+
try {
|
|
74
|
+
await import_promises.default.access(out);
|
|
75
|
+
throw new Error("Jev output already exists; choose a new output path");
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.code !== "ENOENT") throw error;
|
|
78
|
+
}
|
|
79
|
+
if (options.action === "evaluate") {
|
|
80
|
+
if (!options.input)
|
|
81
|
+
throw new Error("Provide an AI dataset JSONL with --input");
|
|
82
|
+
const rows2 = await readJevJsonl(resolve(options.input));
|
|
83
|
+
const selected = rows2.filter(
|
|
84
|
+
(row) => !options.strategy || row.strategyName === options.strategy
|
|
85
|
+
);
|
|
86
|
+
if (!selected.length) throw new Error("No matching Jev input rows");
|
|
87
|
+
for (const row of selected) {
|
|
88
|
+
if (!row.assessment?.recordId)
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Signal ${row.signalId} has no Jev recording. Run the backtest with --ai --jev and use its --jevRecordsDir; an --ai-only export cannot restore the signal-time Jev facts.`
|
|
91
|
+
);
|
|
92
|
+
if (!/^[a-f0-9]{64}$/.test(row.assessment.recordId))
|
|
93
|
+
throw new Error(`Invalid Jev recording id for signal ${row.signalId}`);
|
|
94
|
+
}
|
|
95
|
+
await import_promises.default.mkdir(import_node_path.default.dirname(out), { recursive: true });
|
|
96
|
+
const temp = `${out}.${(0, import_node_crypto.randomUUID)()}.tmp`;
|
|
97
|
+
try {
|
|
98
|
+
const handle = await import_promises.default.open(temp, "wx", 384);
|
|
99
|
+
try {
|
|
100
|
+
for (const row of selected) {
|
|
101
|
+
const recordId = row.assessment.recordId;
|
|
102
|
+
const saved = await (0, import_jev.readJevArtifact)(
|
|
103
|
+
resolve(
|
|
104
|
+
import_node_path.default.join(options.recordsDir, "records", `${recordId}.json`)
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
if (!saved)
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Jev recording ${recordId} for signal ${row.signalId} is missing from --jevRecordsDir. Use the recordings from the original --ai --jev backtest.`
|
|
110
|
+
);
|
|
111
|
+
if (saved.schema !== "tradejs-jev-record/v4")
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Jev recording ${recordId} uses an older schema; rerun the backtest with the current --jev integration.`
|
|
114
|
+
);
|
|
115
|
+
if (saved.id !== recordId || saved.inputHash !== row.assessment?.inputHash || saved.questionsHash !== row.assessment?.questionsHash)
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Jev recording ${recordId} does not match signal ${row.signalId}`
|
|
118
|
+
);
|
|
119
|
+
const { record } = await (0, import_jev2.evaluateJevInput)({
|
|
120
|
+
input: saved.input,
|
|
121
|
+
config: {
|
|
122
|
+
source: "recorded",
|
|
123
|
+
mode: "observe",
|
|
124
|
+
provider: saved.provider,
|
|
125
|
+
recordsDir: options.recordsDir
|
|
126
|
+
},
|
|
127
|
+
userName: options.userName,
|
|
128
|
+
projectRoot: options.projectRoot
|
|
129
|
+
});
|
|
130
|
+
if (!record)
|
|
131
|
+
throw new Error(
|
|
132
|
+
`No eligible Jev questions for signal ${row.signalId}`
|
|
133
|
+
);
|
|
134
|
+
const study = {
|
|
135
|
+
schema: "tradejs-jev-study/v4",
|
|
136
|
+
signalId: row.signalId,
|
|
137
|
+
record,
|
|
138
|
+
...Number.isFinite(row.profit) ? { profit: row.profit } : {}
|
|
139
|
+
};
|
|
140
|
+
await handle.write(`${JSON.stringify(study)}
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
await handle.close();
|
|
145
|
+
}
|
|
146
|
+
await import_promises.default.link(temp, out);
|
|
147
|
+
} finally {
|
|
148
|
+
await import_promises.default.rm(temp, { force: true });
|
|
149
|
+
}
|
|
150
|
+
return { rows: selected.length, out };
|
|
151
|
+
}
|
|
152
|
+
if (options.action === "export") {
|
|
153
|
+
const dir = resolve(import_node_path.default.join(options.recordsDir, "records"));
|
|
154
|
+
const rows2 = [];
|
|
155
|
+
const outcomes = options.input ? await readJevJsonl(resolve(options.input)) : [];
|
|
156
|
+
const byRecord = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const row of outcomes) {
|
|
158
|
+
const id = row.assessment?.recordId;
|
|
159
|
+
if (!id) continue;
|
|
160
|
+
if (!/^[a-f0-9]{64}$/.test(id))
|
|
161
|
+
throw new Error("Invalid Jev recording id");
|
|
162
|
+
const prior = byRecord.get(id);
|
|
163
|
+
if (prior && (prior.profit !== row.profit || prior.signalId !== row.signalId))
|
|
164
|
+
throw new Error("Conflicting outcomes for one Jev recording");
|
|
165
|
+
byRecord.set(id, row);
|
|
166
|
+
}
|
|
167
|
+
for (const file of (await import_promises.default.readdir(dir)).filter((name) => /^[a-f0-9]{64}\.json$/.test(name)).sort()) {
|
|
168
|
+
const record = await (0, import_jev.readJevArtifact)(import_node_path.default.join(dir, file));
|
|
169
|
+
if (!record || options.strategy && record.input.strategy !== options.strategy)
|
|
170
|
+
continue;
|
|
171
|
+
if (rows2.length >= 2e4)
|
|
172
|
+
throw new Error(
|
|
173
|
+
"Export exceeds 20000 samples; select a bounded recording directory"
|
|
174
|
+
);
|
|
175
|
+
const outcome = byRecord.get(record.id);
|
|
176
|
+
rows2.push({
|
|
177
|
+
schema: "tradejs-jev-study/v4",
|
|
178
|
+
signalId: outcome?.signalId ?? record.id,
|
|
179
|
+
record,
|
|
180
|
+
...outcome && Number.isFinite(outcome.profit) ? { profit: outcome.profit } : {}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (!rows2.length) throw new Error("No Jev recordings to export");
|
|
184
|
+
await writeNew(
|
|
185
|
+
out,
|
|
186
|
+
rows2.map((row) => JSON.stringify(row)).join("\n") + "\n"
|
|
187
|
+
);
|
|
188
|
+
return { rows: rows2.length, out };
|
|
189
|
+
}
|
|
190
|
+
if (!options.input) throw new Error("Provide a Jev study JSONL with --input");
|
|
191
|
+
const rows = await readJevJsonl(resolve(options.input));
|
|
192
|
+
if (options.action === "train") {
|
|
193
|
+
const { model, report } = (0, import_jev2.trainJevGate)(rows, options);
|
|
194
|
+
const contents = `${JSON.stringify(model, null, 2)}
|
|
195
|
+
`;
|
|
196
|
+
await writeNew(
|
|
197
|
+
`${out}.report.json`,
|
|
198
|
+
`${JSON.stringify(report, null, 2)}
|
|
199
|
+
`
|
|
200
|
+
);
|
|
201
|
+
await writeNew(out, contents);
|
|
202
|
+
return {
|
|
203
|
+
model: out,
|
|
204
|
+
modelSha256: (0, import_jev.jevFileHash)(contents),
|
|
205
|
+
report: `${out}.report.json`,
|
|
206
|
+
trainEnd: model.training.trainEnd,
|
|
207
|
+
validationEnd: model.training.validationEnd
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (options.action === "compare") {
|
|
211
|
+
if (!options.model) throw new Error("Provide --model");
|
|
212
|
+
const model = (0, import_jev2.validateJevGateModel)(
|
|
213
|
+
JSON.parse(await import_promises.default.readFile(resolve(options.model), "utf8"))
|
|
214
|
+
);
|
|
215
|
+
const heldOut = rows.filter(
|
|
216
|
+
(row) => row.record.input.timestamp > model.training.validationEnd
|
|
217
|
+
);
|
|
218
|
+
if (!heldOut.length)
|
|
219
|
+
throw new Error("No held-out rows after model validationEnd");
|
|
220
|
+
const report = (0, import_jev2.compareJevGate)(model, heldOut);
|
|
221
|
+
await writeNew(out, `${JSON.stringify(report, null, 2)}
|
|
222
|
+
`);
|
|
223
|
+
return report;
|
|
224
|
+
}
|
|
225
|
+
throw new Error("Jev action must be evaluate, export, train, or compare");
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/scripts/jev.ts
|
|
229
|
+
import_args.default.option(
|
|
230
|
+
"action",
|
|
231
|
+
"Jev workflow: evaluate recorded rows, export, train, compare"
|
|
232
|
+
);
|
|
233
|
+
import_args.default.option(
|
|
234
|
+
"input",
|
|
235
|
+
"Input JSONL; export optionally joins outcomes by exact assessment record"
|
|
236
|
+
);
|
|
237
|
+
import_args.default.option("out", "New output file (existing files are never overwritten)");
|
|
238
|
+
import_args.default.option("user", "Account settings user", "root");
|
|
239
|
+
import_args.default.option("strategy", "Select one strategy");
|
|
240
|
+
import_args.default.option("recordsDir", "Jev recordings directory", "data/ai/jev");
|
|
241
|
+
import_args.default.option("model", "Local Jev gate JSON for comparison");
|
|
242
|
+
import_args.default.option("maxDepth", "Maximum local rule tree depth", 3);
|
|
243
|
+
import_args.default.option(["l", "minLeaf"], "Minimum training samples per leaf", 10);
|
|
244
|
+
var main = async () => {
|
|
245
|
+
const flags = import_args.default.parse(process.argv);
|
|
246
|
+
if (!flags.out) throw new Error("Provide --out for the immutable Jev result");
|
|
247
|
+
console.log(
|
|
248
|
+
JSON.stringify(
|
|
249
|
+
await runJevResearch({
|
|
250
|
+
action: String(flags.action),
|
|
251
|
+
projectRoot: process.env.PROJECT_CWD || process.cwd(),
|
|
252
|
+
userName: String(flags.user),
|
|
253
|
+
input: flags.input,
|
|
254
|
+
out: String(flags.out),
|
|
255
|
+
strategy: flags.strategy,
|
|
256
|
+
recordsDir: String(flags.recordsDir),
|
|
257
|
+
model: flags.model,
|
|
258
|
+
maxDepth: Number(flags.maxDepth),
|
|
259
|
+
minLeaf: Number(flags.minLeaf)
|
|
260
|
+
}),
|
|
261
|
+
null,
|
|
262
|
+
2
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
267
|
+
0 && (module.exports = {
|
|
268
|
+
main
|
|
269
|
+
});
|